bitcoinrb 1.11.0 → 1.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +2 -2
  3. data/.ruby-version +1 -1
  4. data/bitcoinrb.gemspec +1 -6
  5. data/lib/bitcoin/bip324/fs_chacha20.rb +3 -5
  6. data/lib/bitcoin/bip324/fs_chacha_poly1305.rb +19 -17
  7. data/lib/bitcoin/bip324.rb +1 -1
  8. data/lib/bitcoin/constants.rb +1 -0
  9. data/lib/bitcoin/descriptor/expression.rb +2 -2
  10. data/lib/bitcoin/descriptor/sp.rb +219 -0
  11. data/lib/bitcoin/descriptor.rb +20 -0
  12. data/lib/bitcoin/ext_key.rb +3 -3
  13. data/lib/bitcoin/message/ping.rb +4 -2
  14. data/lib/bitcoin/message/send_tx_rcncl.rb +35 -0
  15. data/lib/bitcoin/message/version.rb +1 -1
  16. data/lib/bitcoin/message/wtxid_relay.rb +14 -0
  17. data/lib/bitcoin/message.rb +65 -58
  18. data/lib/bitcoin/message_sign.rb +98 -38
  19. data/lib/bitcoin/rpc.rb +0 -2
  20. data/lib/bitcoin/script/script_error.rb +5 -0
  21. data/lib/bitcoin/script/script_interpreter.rb +5 -3
  22. data/lib/bitcoin/silent_payment/output.rb +46 -0
  23. data/lib/bitcoin/silent_payment.rb +113 -12
  24. data/lib/bitcoin/util.rb +2 -2
  25. data/lib/bitcoin/version.rb +1 -1
  26. data/lib/bitcoin.rb +0 -6
  27. metadata +21 -109
  28. data/exe/bitcoinrb-cli +0 -5
  29. data/exe/bitcoinrbd +0 -46
  30. data/lib/bitcoin/network/connection.rb +0 -73
  31. data/lib/bitcoin/network/message_handler.rb +0 -243
  32. data/lib/bitcoin/network/peer.rb +0 -228
  33. data/lib/bitcoin/network/peer_discovery.rb +0 -42
  34. data/lib/bitcoin/network/pool.rb +0 -135
  35. data/lib/bitcoin/network.rb +0 -13
  36. data/lib/bitcoin/node/cli.rb +0 -122
  37. data/lib/bitcoin/node/configuration.rb +0 -40
  38. data/lib/bitcoin/node/spv.rb +0 -87
  39. data/lib/bitcoin/node.rb +0 -7
  40. data/lib/bitcoin/rpc/http_server.rb +0 -65
  41. data/lib/bitcoin/rpc/request_handler.rb +0 -150
  42. data/lib/bitcoin/store/chain_entry.rb +0 -68
  43. data/lib/bitcoin/store/db/level_db.rb +0 -98
  44. data/lib/bitcoin/store/db.rb +0 -9
  45. data/lib/bitcoin/store/spv_chain.rb +0 -101
  46. data/lib/bitcoin/store/utxo_db.rb +0 -226
  47. data/lib/bitcoin/store.rb +0 -12
@@ -10,6 +10,22 @@ module Bitcoin
10
10
  FORMAT_SIMPLE = :simple
11
11
  FORMAT_FULL = :full
12
12
 
13
+ # Prefix
14
+ SIGNATURE_PREFIX_SIMPLE = 'smp'
15
+ SIGNATURE_PREFIX_FULL = 'ful'
16
+ SIGNATURE_PREFIX_POF = 'pof'
17
+
18
+ # BIP-322 Required rules
19
+ BIP322_VERIFY_FLAGS = [
20
+ SCRIPT_VERIFY_P2SH, SCRIPT_VERIFY_DERSIG, SCRIPT_VERIFY_STRICTENC,
21
+ SCRIPT_VERIFY_LOW_S, SCRIPT_VERIFY_NULLFAIL, SCRIPT_VERIFY_MINIMALDATA,
22
+ SCRIPT_VERIFY_CLEANSTACK, SCRIPT_VERIFY_MINIMALIF,
23
+ SCRIPT_VERIFY_CONST_SCRIPTCODE,
24
+ SCRIPT_VERIFY_WITNESS, SCRIPT_VERIFY_TAPROOT,
25
+ SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_NOPS,
26
+ SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM
27
+ ].inject(:|)
28
+
13
29
  # Sign a message.
14
30
  # @param [Bitcoin::Key] key Private key to sign.
15
31
  # @param [String] message The message to be signed.
@@ -20,28 +36,38 @@ module Bitcoin
20
36
  def sign_message(key, message, prefix: Bitcoin.chain_params.message_magic, format: FORMAT_LEGACY, address: nil)
21
37
  validate_format!(format)
22
38
  digest = message_hash(message, prefix: prefix, legacy: format == FORMAT_LEGACY)
39
+ prefix_marker = ''
23
40
  sig = case format
24
41
  when FORMAT_LEGACY
25
42
  key.sign_compact(digest)
26
43
  else
27
44
  validate_address!(address)
28
45
  addr = Bitcoin::Script.parse_from_addr(address)
29
- sig_ver, algo = if addr.p2wpkh?
46
+ sig_ver, algo = if addr.p2wpkh? || addr.p2wsh?
30
47
  [:witness_v0, :ecdsa]
31
48
  elsif addr.p2tr?
32
49
  [:taproot, :schnorr]
33
50
  else
34
- raise ArgumentError "#{address} dose not supported."
51
+
35
52
  end
36
53
  tx = to_sign_tx(digest, address)
37
- prev_out = Bitcoin::TxOut.new(script_pubkey: addr)
38
- sighash = tx.sighash_for_input(0, addr, sig_version: sig_ver, amount: 0, prevouts: [prev_out])
39
- sig = key.sign(sighash, algo: algo) + [Bitcoin::SIGHASH_TYPE[:all]].pack('C')
40
- tx.in[0].script_witness.stack << sig
41
- tx.in[0].script_witness.stack << key.pubkey.htb
54
+ prev_out = Bitcoin::TxOut.new(script_pubkey: addr, value: 0)
55
+ if addr.p2tr?
56
+ sighash = tx.sighash_for_input(0, addr, sig_version: :taproot, prevouts: [prev_out], hash_type: Bitcoin::SIGHASH_TYPE[:default])
57
+ tweaked = Bitcoin::Taproot.tweak_private_key(key, '')
58
+ tx.in[0].script_witness.stack << tweaked.sign(sighash, algo: :schnorr)
59
+ elsif addr.p2wpkh? || addr.p2wsh?
60
+ sighash = tx.sighash_for_input(0, addr, sig_version: :witness_v0, amount: 0, prevouts: [prev_out])
61
+ ecdsa = key.sign(sighash, algo: :ecdsa) + [Bitcoin::SIGHASH_TYPE[:all]].pack('C')
62
+ tx.in[0].script_witness.stack << ecdsa
63
+ tx.in[0].script_witness.stack << key.pubkey.htb
64
+ else
65
+ raise ArgumentError, "#{address} dose not supported."
66
+ end
67
+ prefix_marker = format == FORMAT_SIMPLE ? SIGNATURE_PREFIX_SIMPLE : SIGNATURE_PREFIX_FULL
42
68
  format == FORMAT_SIMPLE ? tx.in[0].script_witness.to_payload : tx.to_payload
43
69
  end
44
- Base64.strict_encode64(sig)
70
+ prefix_marker + Base64.strict_encode64(sig)
45
71
  end
46
72
 
47
73
  # Verify a signed message.
@@ -51,41 +77,41 @@ module Bitcoin
51
77
  # @return [Boolean] Verification result.
52
78
  def verify_message(address, signature, message, prefix: Bitcoin.chain_params.message_magic)
53
79
  addr_script = Bitcoin::Script.parse_from_addr(address)
80
+ variant, body = case signature
81
+ when ''
82
+ raise ArgumentError, 'signature too short'
83
+ when /\A#{SIGNATURE_PREFIX_SIMPLE}/
84
+ [:simple, signature[3..]]
85
+ when /\A#{SIGNATURE_PREFIX_FULL}/
86
+ [:full, signature[3..]]
87
+ when /\A#{SIGNATURE_PREFIX_POF}/
88
+ [:pof, signature[3..]]
89
+ else
90
+ [:fallback, signature]
91
+ end
54
92
  begin
55
- sig = Base64.strict_decode64(signature)
93
+ payload = Base64.strict_decode64(body)
56
94
  rescue ArgumentError
57
95
  raise ArgumentError, 'Invalid signature'
58
96
  end
59
- if addr_script.p2pkh?
60
- begin
61
- # Legacy verification
62
- pubkey = Bitcoin::Key.recover_compact(message_hash(message, prefix: prefix, legacy: true), sig)
63
- return false unless pubkey
64
- pubkey.to_p2pkh == address
65
- rescue Exception
66
- false
67
- end
68
- elsif addr_script.witness_program?
69
- # BIP322 verification
70
- digest = message_hash(message, prefix: prefix, legacy: false)
97
+
98
+ case variant
99
+ when :simple, :fallback
100
+ return verify_legacy(address, payload, message, prefix: prefix) if addr_script.p2pkh?
101
+ raise ArgumentError, 'simple format not supported for this address' unless
102
+ addr_script.p2wpkh? || addr_script.p2wsh? || addr_script.p2tr?
103
+ verify_simple(addr_script, payload, message, prefix: prefix)
104
+ when :full
71
105
  begin
72
- # Full
73
- tx = Bitcoin::Tx.parse_from_payload(sig)
74
- validate_to_sign_tx!(tx)
75
- to_spend = to_spend_tx(digest, address)
76
- return false unless tx.in[0].out_point.tx_hash == to_spend.tx_hash
77
- rescue Exception
78
- # Simple
79
- tx = to_sign_tx(digest, address)
80
- tx.in[0].script_witness = Bitcoin::ScriptWitness.parse_from_payload(sig)
106
+ tx = Bitcoin::Tx.parse_from_payload(payload)
107
+ rescue StandardError
108
+ raise ArgumentError, 'error parsing signature as full variant'
81
109
  end
82
- script_pubkey = Bitcoin::Script.parse_from_addr(address)
83
- tx_out = Bitcoin::TxOut.new(script_pubkey: script_pubkey)
84
- flags = Bitcoin::STANDARD_SCRIPT_VERIFY_FLAGS
85
- interpreter = Bitcoin::ScriptInterpreter.new(flags: flags, checker: Bitcoin::TxChecker.new(tx: tx, input_index: 0, prevouts: [tx_out]))
86
- interpreter.verify_script(Bitcoin::Script.new, script_pubkey, tx.in[0].script_witness)
110
+ verify_full(addr_script, tx, message, prefix: prefix)
111
+ when :pof
112
+ raise NotImplementedError, 'proof of funds variant is not supported'
87
113
  else
88
- raise ArgumentError, "This address unsupported."
114
+ raise ArgumentError, "unknown signature variant: #{variant.inspect}"
89
115
  end
90
116
  end
91
117
 
@@ -99,8 +125,7 @@ module Bitcoin
99
125
  end
100
126
 
101
127
  def validate_address!(address)
102
- script = Bitcoin::Script.parse_from_addr(address)
103
- raise ArgumentError, 'This address unsupported' if script.p2sh? || script.p2wsh?
128
+ Bitcoin::Script.parse_from_addr(address)
104
129
  end
105
130
 
106
131
  def validate_format!(format)
@@ -110,6 +135,7 @@ module Bitcoin
110
135
  end
111
136
 
112
137
  def validate_to_sign_tx!(tx)
138
+ raise ArgumentError, "Invalid version." unless [0, 2].include?(tx.version)
113
139
  raise ArgumentError, "Multiple inputs (proof of funds) are not supported." unless tx.in.length == 1
114
140
  raise ArgumentError, "vin[0].prevout.n must be 0." unless tx.in[0].out_point.index == 0
115
141
  raise ArgumentError, "Multiple outputs are not supported." unless tx.out.length == 1
@@ -140,8 +166,42 @@ module Bitcoin
140
166
  tx
141
167
  end
142
168
 
169
+ def verify_simple(addr_script, witness_payload, message, prefix:)
170
+ digest = message_hash(message, prefix: prefix, legacy: false)
171
+ tx = to_sign_tx(digest, addr_script.to_addr)
172
+ tx.in[0].script_witness = Bitcoin::ScriptWitness.parse_from_payload(witness_payload)
173
+ run_interpreter(tx, addr_script)
174
+ end
175
+
176
+ def verify_full(addr_script, tx, message, prefix:)
177
+ digest = message_hash(message, prefix: prefix, legacy: false)
178
+ to_spend = to_spend_tx(digest, addr_script.to_addr)
179
+ validate_to_sign_tx!(tx)
180
+ return false unless tx.in[0].out_point.tx_hash == to_spend.tx_hash
181
+ run_interpreter(tx, addr_script)
182
+ end
183
+
184
+ def run_interpreter(tx, script_pubkey)
185
+ tx_out = Bitcoin::TxOut.new(script_pubkey: script_pubkey)
186
+ checker = Bitcoin::TxChecker.new(tx: tx, input_index: 0, prevouts: [tx_out])
187
+ interpreter = Bitcoin::ScriptInterpreter.new(flags: BIP322_VERIFY_FLAGS, checker: checker)
188
+ interpreter.verify_script(tx.in[0].script_sig, script_pubkey, tx.in[0].script_witness)
189
+ end
190
+
191
+ def verify_legacy(address, sig_bytes, message, prefix:)
192
+ pubkey = Bitcoin::Key.recover_compact(message_hash(message, prefix: prefix, legacy: true), sig_bytes)
193
+ return false unless pubkey
194
+ pubkey.to_p2pkh == address
195
+ rescue StandardError
196
+ false
197
+ end
198
+
143
199
  private_class_method :validate_address!
144
200
  private_class_method :validate_format!
145
201
  private_class_method :validate_to_sign_tx!
202
+ private_class_method :run_interpreter
203
+ private_class_method :verify_simple
204
+ private_class_method :verify_full
205
+ private_class_method :verify_legacy
146
206
  end
147
207
  end
data/lib/bitcoin/rpc.rb CHANGED
@@ -1,7 +1,5 @@
1
1
  module Bitcoin
2
2
  module RPC
3
- autoload :HttpServer, 'bitcoin/rpc/http_server'
4
- autoload :RequestHandler, 'bitcoin/rpc/request_handler'
5
3
  autoload :BitcoinCoreClient, 'bitcoin/rpc/bitcoin_core_client'
6
4
  end
7
5
  end
@@ -1,5 +1,8 @@
1
1
  module Bitcoin
2
2
 
3
+ class ScriptNumError < StandardError
4
+ end
5
+
3
6
  # bitcoin script error
4
7
  class ScriptError < Exception
5
8
 
@@ -122,6 +125,8 @@ module Bitcoin
122
125
  'Signature is found in scriptCode'
123
126
  when SCRIPT_ERR_UNKNOWN_ERROR, SCRIPT_ERR_ERROR_COUNT
124
127
  'unknown error'
128
+ when SCRIPT_ERR_SCRIPTNUM
129
+ 'Script number overflowed or is non-minimally encoded'
125
130
  else
126
131
  extra_msg ? extra_msg : 'unknown error'
127
132
  end
@@ -258,7 +258,7 @@ module Bitcoin
258
258
  when OP_IF, OP_NOTIF
259
259
  result = false
260
260
  if need_exec
261
- return set_error(SCRIPT_ERR_UNBALANCED_CONDITIONAL) if stack.size < 1
261
+ return set_error(SCRIPT_ERR_INVALID_STACK_OPERATION) if stack.size < 1
262
262
  value = pop_string.htb
263
263
  if sig_version == :witness_v0 && flag?(SCRIPT_VERIFY_MINIMALIF) || sig_version == :tapscript
264
264
  # Under witness v0 rules it is only a policy rule, enabled through SCRIPT_VERIFY_MINIMALIF.
@@ -566,6 +566,8 @@ module Bitcoin
566
566
  # max stack size check
567
567
  return set_error(SCRIPT_ERR_STACK_SIZE) if stack.size + alt_stack.size > MAX_STACK_SIZE
568
568
  end
569
+ rescue ScriptNumError => e
570
+ return set_error(SCRIPT_ERR_SCRIPTNUM, e.message)
569
571
  rescue Exception => e
570
572
  return set_error(SCRIPT_ERR_UNKNOWN_ERROR, e.message)
571
573
  end
@@ -591,10 +593,10 @@ module Bitcoin
591
593
  # cast item to int value.
592
594
  def cast_to_int(s, max_num_size = DEFAULT_MAX_NUM_SIZE)
593
595
  data = s.htb
594
- raise '"script number overflow"' if data.bytesize > max_num_size
596
+ raise ScriptNumError, '"script number overflow"' if data.bytesize > max_num_size
595
597
  if require_minimal && data.bytesize > 0
596
598
  if data.bytes[-1] & 0x7f == 0 && (data.bytesize <= 1 || data.bytes[data.bytesize - 2] & 0x80 == 0)
597
- raise 'non-minimally encoded script number'
599
+ raise ScriptNumError, 'non-minimally encoded script number'
598
600
  end
599
601
  end
600
602
  Script.decode_number(s)
@@ -0,0 +1,46 @@
1
+ module Bitcoin
2
+ module SilentPayment
3
+ # Represents a detected silent payment output.
4
+ class Output
5
+ # @return [Bitcoin::TxOut] The detected transaction output.
6
+ attr_reader :tx_out
7
+
8
+ # @return [String] The tweak value (t_k) used to derive the output key (32 bytes binary).
9
+ attr_reader :tweak
10
+
11
+ # @return [Integer, nil] The label used for this output, or nil if no label was used.
12
+ attr_reader :label
13
+
14
+ # @param [Bitcoin::TxOut] tx_out The detected transaction output.
15
+ # @param [String] tweak The tweak value (t_k) as 32 bytes binary.
16
+ # @param [Integer, nil] label The label integer, or nil if no label was used.
17
+ # @raise [ArgumentError] If any parameter has an invalid type.
18
+ def initialize(tx_out, tweak, label = nil)
19
+ raise ArgumentError, "tx_out must be a Bitcoin::TxOut." unless tx_out.is_a?(Bitcoin::TxOut)
20
+ raise ArgumentError, "tweak must be a 32-byte String." unless tweak.is_a?(String) && tweak.bytesize == 32
21
+ raise ArgumentError, "label must be an Integer or nil." unless label.nil? || label.is_a?(Integer)
22
+ @tx_out = tx_out
23
+ @tweak = tweak
24
+ @label = label
25
+ end
26
+
27
+ # Returns the x-only public key of the output.
28
+ # @return [String] The x-only public key as hex string.
29
+ def pubkey
30
+ tx_out.script_pubkey.witness_data[1].bth
31
+ end
32
+
33
+ # Returns the tweak as hex string.
34
+ # @return [String] The tweak value as hex string.
35
+ def tweak_hex
36
+ tweak.bth
37
+ end
38
+
39
+ # Returns whether this output was detected using a label.
40
+ # @return [Boolean]
41
+ def labeled?
42
+ !label.nil?
43
+ end
44
+ end
45
+ end
46
+ end
@@ -1,12 +1,22 @@
1
+ require 'set'
2
+
1
3
  module Bitcoin
4
+ # BIP-352 silent payment module.
5
+ # @see https://github.com/bitcoin/bips/blob/master/bip-0352.mediawiki
2
6
  module SilentPayment
7
+ autoload :Output, 'bitcoin/silent_payment/output'
3
8
 
4
-
5
- # Derive payment
6
- # @param [Array] prevouts An array of previous output script(Bitcoin::Script).
7
- # @param [Array] private_keys An array of private key corresponding to each public key in prevouts.
8
- # @param [Array] recipients
9
- # @return [Array]
9
+ # Maximum number of silent payment addresses that can share the same scan public key
10
+ # within a single transaction. This is the maximum number of P2TR outputs that can fit
11
+ # within a 100KB transaction under current standardness rules.
12
+ K_MAX = 2323
13
+
14
+ # Derive payment point.
15
+ #
16
+ # @param [Array<Bitcoin::Script>] prevouts An array of previous output script.
17
+ # @param [Array<Bitcoin::Key>] private_keys An array of Bitcoin::Key objects corresponding to each public key in prevouts.
18
+ # @param [Array<Bech32::SilentPaymentAddr>] recipients
19
+ # @return [Array<ECDSA::Point>] An array of derived points.
10
20
  # @raise [ArgumentError]
11
21
  def derive_payment_points(prevouts, private_keys, recipients)
12
22
  raise ArgumentError, "prevouts must be Array." unless prevouts.is_a? Array
@@ -14,19 +24,19 @@ module Bitcoin
14
24
  raise ArgumentError, "prevouts and private_keys must be the same length." unless prevouts.length == private_keys.length
15
25
  raise ArgumentError, "recipients must be Array." unless recipients.is_a? Array
16
26
 
17
- outpoint_l = inputs.map{|i|i.out_point.to_hex}.sort.first
18
-
19
27
  input_pub_keys = []
20
28
  field = ECDSA::PrimeField.new(Bitcoin::Secp256k1::GROUP.order)
21
29
  sum_priv_keys = 0
22
30
  prevouts.each_with_index do |prevout, index|
23
- k = Bitcoin::Key.new(priv_key: private_keys[index].to_s(16))
31
+ key = private_keys[index]
32
+ raise ArgumentError, "private_keys element must be Bitcoin::Key." unless key.is_a? Bitcoin::Key
33
+ priv_key_int = key.priv_key.to_i(16)
24
34
  public_key = extract_public_key(prevout, inputs[index])
25
35
  next if public_key.nil?
26
- private_key = if public_key.p2tr? && k.to_point.y.odd?
27
- field.mod(-private_keys[index])
36
+ private_key = if public_key.p2tr? && key.to_point.y.odd?
37
+ field.mod(-priv_key_int)
28
38
  else
29
- private_keys[index]
39
+ priv_key_int
30
40
  end
31
41
  input_pub_keys << public_key
32
42
  sum_priv_keys = field.mod(sum_priv_keys + private_key)
@@ -34,6 +44,8 @@ module Bitcoin
34
44
  agg_pubkey = (Bitcoin::Secp256k1::GROUP.generator.to_jacobian * sum_priv_keys).to_affine
35
45
  return [] if agg_pubkey.infinity?
36
46
 
47
+ outpoint_l = inputs.map{|i|i.out_point.to_hex}.sort.first
48
+
37
49
  input_hash = Bitcoin.tagged_hash("BIP0352/Inputs", outpoint_l.htb + agg_pubkey.to_hex.htb).bth
38
50
 
39
51
  destinations = {}
@@ -42,6 +54,12 @@ module Bitcoin
42
54
  destinations[sp_addr.scan_key] = [] unless destinations.has_key?(sp_addr.scan_key)
43
55
  destinations[sp_addr.scan_key] << sp_addr.spend_key
44
56
  end
57
+
58
+ # Check K_max limit: fail if any group exceeds the limit
59
+ destinations.each_value do |spends|
60
+ raise ArgumentError, "Recipient group exceeds K_max limit (#{K_MAX})." if spends.length > K_MAX
61
+ end
62
+
45
63
  outputs = []
46
64
  destinations.each do |scan_key, spends|
47
65
  scan_key = Bitcoin::Key.new(pubkey: scan_key).to_point.to_jacobian
@@ -55,6 +73,89 @@ module Bitcoin
55
73
  outputs
56
74
  end
57
75
 
76
+
77
+ # Scan transaction outputs for silent payment outputs belonging to the receiver.
78
+ #
79
+ # @param [Array<Bitcoin::Script>] prevouts An array of previous output scripts corresponding to each input.
80
+ # @param [Bitcoin::Key] scan_private_key The receiver's scan private key (b_scan).
81
+ # @param [Bitcoin::Key] spend_pubkey The receiver's spend key. Pass a Key initialized with spend_priv_key to derive the public key.
82
+ # @param [Array<Integer>] labels An array of label integers for labeled addresses (default: []).
83
+ # @return [Array<Bitcoin::SilentPayment::Output>] An array of detected silent payment outputs.
84
+ # @raise [ArgumentError] If any of the required parameters are invalid.
85
+ def scan_sp_outputs(prevouts, scan_private_key, spend_pubkey, labels = [])
86
+ raise ArgumentError, "prevouts must be Array." unless prevouts.is_a? Array
87
+ raise ArgumentError, "scan_private_key must be Bitcoin::Key." unless scan_private_key.is_a? Bitcoin::Key
88
+ raise ArgumentError, "spend_pubkey must be Bitcoin::Key." unless spend_pubkey.is_a? Bitcoin::Key
89
+
90
+ has_taproot = !outputs.find{|o| o.script_pubkey.p2tr? }.nil?
91
+ return [] unless has_taproot
92
+ sum_pub_keys = Bitcoin::Secp256k1::GROUP.infinity.to_jacobian
93
+ maximum_witness_version = Bitcoin::Opcodes.opcode_to_small_int(Bitcoin::Opcodes::OP_1)
94
+ prevouts.each.with_index do |prevout, index|
95
+ return [] if prevout.witness_program? && prevout.witness_data.first > maximum_witness_version
96
+
97
+ public_key = extract_public_key(prevout, inputs[index])
98
+ next if public_key.nil?
99
+ sum_pub_keys += public_key.to_point.to_jacobian
100
+ end
101
+ return [] if sum_pub_keys.infinity?
102
+
103
+ field = ECDSA::PrimeField.new(Bitcoin::Secp256k1::GROUP.order)
104
+ outpoint_l = inputs.map{|i|i.out_point.to_hex}.sort.first
105
+ input_hash = Bitcoin.tagged_hash("BIP0352/Inputs", outpoint_l.htb + sum_pub_keys.to_affine.to_hex.htb).bth
106
+ ecdh_shared_secret = (sum_pub_keys * field.mod(input_hash.to_i(16) * scan_private_key.priv_key.to_i(16))).to_affine.to_hex.htb
107
+
108
+ # Pre-compute label tweak points with their label values and scalar tweaks
109
+ label_tweaks = labels.map do |m|
110
+ label_tweak = Bitcoin.tagged_hash('BIP0352/Label', scan_private_key.priv_key.htb + [m].pack('N'))
111
+ label_point = Bitcoin::Secp256k1::GROUP.generator.to_jacobian * label_tweak.bti
112
+ [m, label_tweak, label_point]
113
+ end
114
+
115
+ k = 0
116
+ results = []
117
+ found_outputs = Set.new
118
+ loop do
119
+ # Stop scanning if K_max limit is reached
120
+ break if k == K_MAX
121
+
122
+ t_k = Bitcoin.tagged_hash('BIP0352/SharedSecret', ecdh_shared_secret + [k].pack('N'))
123
+ p_k = Bitcoin::Secp256k1::GROUP.generator.to_jacobian * t_k.bti + spend_pubkey.to_point.to_jacobian
124
+ found = false
125
+ outputs.each do |output|
126
+ next unless output.script_pubkey.p2tr?
127
+ next if found_outputs.include?(output)
128
+ output_pubkey = Bitcoin::Key.from_xonly_pubkey(output.script_pubkey.witness_data[1].bth)
129
+
130
+ # Check basic match (no label)
131
+ if p_k.to_affine.x == output_pubkey.to_point.x
132
+ results << SilentPayment::Output.new(output, t_k)
133
+ found_outputs << output
134
+ k += 1
135
+ found = true
136
+ break
137
+ end
138
+
139
+ # Check labeled matches
140
+ label_tweaks.each do |label_value, label_tweak_scalar, label_point|
141
+ p_k_labeled = p_k + label_point
142
+ if p_k_labeled.to_affine.x == output_pubkey.to_point.x
143
+ # Full tweak is t_k + label_tweak (mod order)
144
+ full_tweak = field.mod(t_k.bti + label_tweak_scalar.bti).to_s(16).rjust(64, '0').htb
145
+ results << SilentPayment::Output.new(output, full_tweak, label_value)
146
+ found_outputs << output
147
+ k += 1
148
+ found = true
149
+ break
150
+ end
151
+ end
152
+ break if found
153
+ end
154
+ break unless found
155
+ end
156
+ results
157
+ end
158
+
58
159
  # Extract public keys from +prevout+ and input.
59
160
  def extract_public_key(prevout, input)
60
161
  if prevout.p2pkh?
data/lib/bitcoin/util.rb CHANGED
@@ -14,7 +14,7 @@ module Bitcoin
14
14
 
15
15
  def unpack_var_string(payload)
16
16
  size, payload = unpack_var_int(payload)
17
- size > 0 ? payload.unpack("a#{size}a*") : [nil, payload]
17
+ size && size > 0 ? payload.unpack("a#{size}a*") : ['', payload.to_s]
18
18
  end
19
19
 
20
20
  def pack_var_int(i)
@@ -102,7 +102,7 @@ module Bitcoin
102
102
  # @return [String] the hash value with binary format.
103
103
  def tagged_hash(tag, msg)
104
104
  tag_hash = Digest::SHA256.digest(tag)
105
- Digest::SHA256.digest(tag_hash + tag_hash + msg)
105
+ Digest::SHA256.digest(tag_hash + tag_hash + msg.b)
106
106
  end
107
107
 
108
108
  # encode Base58 check address.
@@ -1,3 +1,3 @@
1
1
  module Bitcoin
2
- VERSION = "1.11.0"
2
+ VERSION = "1.12.1"
3
3
  end
data/lib/bitcoin.rb CHANGED
@@ -2,13 +2,11 @@
2
2
  # https://github.com/lian/bitcoin-ruby/blob/master/COPYING
3
3
 
4
4
  require 'bitcoin/version'
5
- require 'eventmachine'
6
5
  require 'schnorr'
7
6
  require 'securerandom'
8
7
  require 'json'
9
8
  require 'bech32'
10
9
  require 'base64'
11
- require 'observer'
12
10
  require 'tmpdir'
13
11
  require 'merkle'
14
12
 
@@ -36,13 +34,10 @@ module Bitcoin
36
34
  autoload :ExtKey, 'bitcoin/ext_key'
37
35
  autoload :ExtPubkey, 'bitcoin/ext_key'
38
36
  autoload :Opcodes, 'bitcoin/opcodes'
39
- autoload :Node, 'bitcoin/node'
40
37
  autoload :Base58, 'bitcoin/base58'
41
38
  autoload :Secp256k1, 'bitcoin/secp256k1'
42
39
  autoload :Mnemonic, 'bitcoin/mnemonic'
43
40
  autoload :ValidationState, 'bitcoin/validation'
44
- autoload :Network, 'bitcoin/network'
45
- autoload :Store, 'bitcoin/store'
46
41
  autoload :RPC, 'bitcoin/rpc'
47
42
  autoload :Wallet, 'bitcoin/wallet'
48
43
  autoload :BloomFilter, 'bitcoin/bloom_filter'
@@ -64,7 +59,6 @@ module Bitcoin
64
59
  autoload :SilentPayment, 'bitcoin/silent_payment'
65
60
  autoload :BIP324, 'bitcoin/bip324'
66
61
  autoload :BIP321URI, 'bitcoin/bip321_uri'
67
- autoload :BIP353, 'bitcoin/bip353'
68
62
 
69
63
  require_relative 'bitcoin/constants'
70
64
  require_relative 'bitcoin/ext/ecdsa'