bitcoinrb 1.12.1 → 1.14.0

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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/bitcoinrb.gemspec +2 -2
  3. data/lib/bitcoin/base58.rb +15 -3
  4. data/lib/bitcoin/bip321_uri.rb +3 -6
  5. data/lib/bitcoin/bip324/fs_chacha20.rb +1 -1
  6. data/lib/bitcoin/bip324/fs_chacha_poly1305.rb +1 -0
  7. data/lib/bitcoin/bip324.rb +6 -1
  8. data/lib/bitcoin/block.rb +3 -3
  9. data/lib/bitcoin/descriptor/checksum.rb +6 -2
  10. data/lib/bitcoin/ext_key.rb +8 -4
  11. data/lib/bitcoin/key.rb +2 -2
  12. data/lib/bitcoin/message/base.rb +7 -1
  13. data/lib/bitcoin/message/cf_parser.rb +2 -2
  14. data/lib/bitcoin/message/fee_filter.rb +2 -2
  15. data/lib/bitcoin/message/filter_load.rb +10 -0
  16. data/lib/bitcoin/message/header_and_short_ids.rb +4 -4
  17. data/lib/bitcoin/message/inventory.rb +1 -1
  18. data/lib/bitcoin/message/merkle_block.rb +6 -0
  19. data/lib/bitcoin/message/network_addr.rb +7 -3
  20. data/lib/bitcoin/message/ping.rb +2 -2
  21. data/lib/bitcoin/message/pong.rb +2 -2
  22. data/lib/bitcoin/message/send_cmpct.rb +2 -2
  23. data/lib/bitcoin/message/version.rb +3 -3
  24. data/lib/bitcoin/message.rb +15 -2
  25. data/lib/bitcoin/mnemonic.rb +9 -4
  26. data/lib/bitcoin/partial_tree.rb +25 -0
  27. data/lib/bitcoin/psbt/input.rb +58 -33
  28. data/lib/bitcoin/psbt/key_origin_info.rb +2 -2
  29. data/lib/bitcoin/psbt/output.rb +9 -5
  30. data/lib/bitcoin/psbt/tx.rb +22 -15
  31. data/lib/bitcoin/psbt.rb +1 -1
  32. data/lib/bitcoin/rpc/bitcoin_core_client.rb +3 -2
  33. data/lib/bitcoin/script/script.rb +15 -5
  34. data/lib/bitcoin/script/script_interpreter.rb +1 -1
  35. data/lib/bitcoin/script/tx_checker.rb +7 -2
  36. data/lib/bitcoin/secp256k1/native.rb +50 -1
  37. data/lib/bitcoin/secp256k1/rfc6979.rb +6 -3
  38. data/lib/bitcoin/secp256k1/ruby.rb +115 -2
  39. data/lib/bitcoin/sighash_generator.rb +8 -2
  40. data/lib/bitcoin/silent_payment.rb +56 -90
  41. data/lib/bitcoin/taproot/control_block.rb +1 -0
  42. data/lib/bitcoin/taproot/custom_depth_builder.rb +2 -2
  43. data/lib/bitcoin/taproot/simple_builder.rb +9 -7
  44. data/lib/bitcoin/taproot.rb +6 -1
  45. data/lib/bitcoin/tx.rb +8 -6
  46. data/lib/bitcoin/tx_out.rb +2 -2
  47. data/lib/bitcoin/util.rb +4 -4
  48. data/lib/bitcoin/version.rb +1 -1
  49. data/lib/bitcoin/wallet/account.rb +2 -2
  50. data/lib/bitcoin/wallet/db.rb +3 -3
  51. data/lib/bitcoin/wallet/master_key.rb +83 -24
  52. data/lib/bitcoin/wallet/utxo.rb +2 -2
  53. data/lib/bitcoin.rb +1 -0
  54. metadata +5 -5
@@ -55,8 +55,9 @@ module Bitcoin
55
55
  found_sep = true
56
56
  break
57
57
  end
58
- key_type = buf.read(1).unpack1('C')
59
- key = buf.read(key_len - 1)
58
+ key_start = buf.pos
59
+ key_type = Bitcoin.unpack_var_int_from_io(buf)
60
+ key = buf.read(key_len - (buf.pos - key_start))
60
61
  value = buf.read(Bitcoin.unpack_var_int_from_io(buf))
61
62
 
62
63
  case key_type
@@ -79,7 +80,8 @@ module Bitcoin
79
80
  when PSBT_IN_TYPES[:sighash]
80
81
  raise ArgumentError, 'Invalid input sighash type typed key.' unless key_len == 1
81
82
  raise ArgumentError, 'Duplicate Key, input sighash type already provided.' if input.sighash_type
82
- input.sighash_type = value.unpack1('I')
83
+ raise ArgumentError, 'Invalid input sighash type value.' unless value.bytesize == 4
84
+ input.sighash_type = value.unpack1('V')
83
85
  when PSBT_IN_TYPES[:redeem_script]
84
86
  raise ArgumentError, 'Invalid redeemscript typed key.' unless key_len == 1
85
87
  raise ArgumentError, 'Duplicate Key, input redeemScript already provided.' if input.redeem_script
@@ -117,10 +119,12 @@ module Bitcoin
117
119
  raise ArgumentError, 'Duplicate Key, input hash256 preimage already provided' if input.hash256_preimages[key.bth]
118
120
  input.hash256_preimages[key.bth] = value.bth
119
121
  when PSBT_IN_TYPES[:proprietary]
120
- raise ArgumentError, 'Duplicate Key, key for proprietary value already provided.' if input.proprietaries.any?{|p| p.key == key}
121
- input.proprietaries << Proprietary.new(key, value)
122
+ proprietary = Proprietary.new(key, value)
123
+ raise ArgumentError, 'Duplicate Key, key for proprietary value already provided.' if input.proprietaries.any?{|p| p.key == proprietary.key}
124
+ input.proprietaries << proprietary
122
125
  when PSBT_IN_TYPES[:tap_key_sig]
123
126
  raise ArgumentError, 'Size of key was not the expected size for the type tap key sig' unless key_len == 1
127
+ raise ArgumentError, 'Duplicate Key, input tap key sig already provided.' if input.tap_key_sig
124
128
  raise ArgumentError, 'Invalid schnorr signature size for the type tap key sig' unless [64, 65].include?(value.bytesize)
125
129
  input.tap_key_sig = value.bth
126
130
  when PSBT_IN_TYPES[:tap_script_sig]
@@ -142,14 +146,16 @@ module Bitcoin
142
146
  input.tap_bip32_derivations[key.bth] = value.bth
143
147
  when PSBT_IN_TYPES[:tap_internal_key]
144
148
  raise ArgumentError, 'Size of key was not the expected size for the type tap internal key' unless key_len == 1
149
+ raise ArgumentError, 'Duplicate Key, input tap internal key already provided.' if input.tap_internal_key
145
150
  raise ArgumentError, 'Invalid x-only public key size for the type tap internal key' unless value.bytesize == X_ONLY_PUBKEY_SIZE
146
151
  input.tap_internal_key = value.bth
147
152
  when PSBT_IN_TYPES[:tap_merkle_root]
148
153
  raise ArgumentError, 'Size of key was not the expected size for the type tap merkle root' unless key_len == 1
154
+ raise ArgumentError, 'Duplicate Key, input tap merkle root already provided.' if input.tap_merkle_root
149
155
  raise ArgumentError, 'Invalid merkle root hash size for the type tap merkle root' unless value.bytesize == 32
150
156
  input.tap_merkle_root = value.bth
151
157
  else
152
- unknown_key = ([key_type].pack('C') + key).bth
158
+ unknown_key = (Bitcoin.pack_var_int(key_type) + key).bth
153
159
  raise ArgumentError, 'Duplicate Key, key for unknown value already provided.' if input.unknowns[unknown_key]
154
160
  input.unknowns[unknown_key] = value
155
161
  end
@@ -163,23 +169,21 @@ module Bitcoin
163
169
  payload << PSBT.serialize_to_vector(PSBT_IN_TYPES[:non_witness_utxo], value:
164
170
  (witness_utxo && valid_witness_input?) ? non_witness_utxo.serialize_old_format : non_witness_utxo.to_payload) if non_witness_utxo
165
171
  payload << PSBT.serialize_to_vector(PSBT_IN_TYPES[:witness_utxo], value: witness_utxo.to_payload) if witness_utxo
166
- if final_script_sig.nil? && final_script_witness.nil?
167
- payload << partial_sigs.map{|k, v|PSBT.serialize_to_vector(PSBT_IN_TYPES[:partial_sig], key: k.htb, value: v)}.join
168
- payload << PSBT.serialize_to_vector(PSBT_IN_TYPES[:sighash], value: [sighash_type].pack('I')) if sighash_type
169
- payload << PSBT.serialize_to_vector(PSBT_IN_TYPES[:redeem_script], value: redeem_script.to_payload) if redeem_script
170
- payload << PSBT.serialize_to_vector(PSBT_IN_TYPES[:witness_script], value: witness_script.to_payload) if witness_script
171
- payload << hd_key_paths.values.map(&:to_payload).join
172
- payload << ripemd160_preimages.map{|k, v|PSBT.serialize_to_vector(PSBT_IN_TYPES[:ripemd160], key: k.htb, value: v.htb)}.join
173
- payload << sha256_preimages.map{|k, v|PSBT.serialize_to_vector(PSBT_IN_TYPES[:sha256], key: k.htb, value: v.htb)}.join
174
- payload << hash160_preimages.map{|k, v|PSBT.serialize_to_vector(PSBT_IN_TYPES[:hash160], key: k.htb, value: v.htb)}.join
175
- payload << hash256_preimages.map{|k, v|PSBT.serialize_to_vector(PSBT_IN_TYPES[:hash256], key: k.htb, value: v.htb)}.join
176
- payload << PSBT.serialize_to_vector(PSBT_IN_TYPES[:tap_key_sig], value: tap_key_sig.htb) if tap_key_sig
177
- payload << tap_script_sigs.map{|k, v| PSBT.serialize_to_vector(PSBT_IN_TYPES[:tap_script_sig], key: k.htb, value: v.htb)}.join
178
- payload << tap_leaf_scripts.map{|k, v| PSBT.serialize_to_vector(PSBT_IN_TYPES[:tap_leaf_script], key: k.to_payload, value: v.htb)}.join
179
- payload << tap_bip32_derivations.map{|k, v| PSBT.serialize_to_vector(PSBT_IN_TYPES[:tap_bip32_derivation], key: k.htb, value: v.htb)}.join
180
- payload << PSBT.serialize_to_vector(PSBT_IN_TYPES[:tap_internal_key], value: tap_internal_key.htb) if tap_internal_key
181
- payload << PSBT.serialize_to_vector(PSBT_IN_TYPES[:tap_merkle_root], value: tap_merkle_root.htb) if tap_merkle_root
182
- end
172
+ payload << partial_sigs.map{|k, v|PSBT.serialize_to_vector(PSBT_IN_TYPES[:partial_sig], key: k.htb, value: v)}.join
173
+ payload << PSBT.serialize_to_vector(PSBT_IN_TYPES[:sighash], value: [sighash_type].pack('V')) if sighash_type
174
+ payload << PSBT.serialize_to_vector(PSBT_IN_TYPES[:redeem_script], value: redeem_script.to_payload) if redeem_script
175
+ payload << PSBT.serialize_to_vector(PSBT_IN_TYPES[:witness_script], value: witness_script.to_payload) if witness_script
176
+ payload << hd_key_paths.values.map(&:to_payload).join
177
+ payload << ripemd160_preimages.map{|k, v|PSBT.serialize_to_vector(PSBT_IN_TYPES[:ripemd160], key: k.htb, value: v.htb)}.join
178
+ payload << sha256_preimages.map{|k, v|PSBT.serialize_to_vector(PSBT_IN_TYPES[:sha256], key: k.htb, value: v.htb)}.join
179
+ payload << hash160_preimages.map{|k, v|PSBT.serialize_to_vector(PSBT_IN_TYPES[:hash160], key: k.htb, value: v.htb)}.join
180
+ payload << hash256_preimages.map{|k, v|PSBT.serialize_to_vector(PSBT_IN_TYPES[:hash256], key: k.htb, value: v.htb)}.join
181
+ payload << PSBT.serialize_to_vector(PSBT_IN_TYPES[:tap_key_sig], value: tap_key_sig.htb) if tap_key_sig
182
+ payload << tap_script_sigs.map{|k, v| PSBT.serialize_to_vector(PSBT_IN_TYPES[:tap_script_sig], key: k.htb, value: v.htb)}.join
183
+ payload << tap_leaf_scripts.map{|k, v| PSBT.serialize_to_vector(PSBT_IN_TYPES[:tap_leaf_script], key: k.to_payload, value: v.htb)}.join
184
+ payload << tap_bip32_derivations.map{|k, v| PSBT.serialize_to_vector(PSBT_IN_TYPES[:tap_bip32_derivation], key: k.htb, value: v.htb)}.join
185
+ payload << PSBT.serialize_to_vector(PSBT_IN_TYPES[:tap_internal_key], value: tap_internal_key.htb) if tap_internal_key
186
+ payload << PSBT.serialize_to_vector(PSBT_IN_TYPES[:tap_merkle_root], value: tap_merkle_root.htb) if tap_merkle_root
183
187
  payload << PSBT.serialize_to_vector(PSBT_IN_TYPES[:script_sig], value: final_script_sig.to_payload) if final_script_sig
184
188
  payload << PSBT.serialize_to_vector(PSBT_IN_TYPES[:script_witness], value: final_script_witness.to_payload) if final_script_witness
185
189
  payload << proprietaries.map(&:to_payload).join
@@ -188,13 +192,23 @@ module Bitcoin
188
192
  payload
189
193
  end
190
194
 
195
+ # Get the previous output this input spends.
196
+ # @param [Integer] index The index of the outpoint this input refers to.
197
+ # @return [Bitcoin::TxOut] The previous output, or nil if this input does not carry it.
198
+ def utxo(index)
199
+ (non_witness_utxo ? non_witness_utxo.out[index] : nil) || witness_utxo
200
+ end
201
+
191
202
  # Check whether input's scriptPubkey is correct witness.
192
203
  # @return [Boolean]
193
204
  def valid_witness_input?
194
- return true if witness_utxo&.script_pubkey.p2wpkh? # P2WPKH
195
- return true if witness_utxo&.script_pubkey.p2wsh? && witness_utxo&.script_pubkey == redeem_script.to_p2wsh # P2WSH
205
+ return false unless witness_utxo
206
+ return true if witness_utxo.script_pubkey.p2wpkh? # P2WPKH
207
+ # P2WSH. The scriptPubkey commits to the witness script directly, so a redeem script is not used.
208
+ return true if witness_utxo.script_pubkey.p2wsh? && witness_script &&
209
+ Bitcoin::Script.to_p2wsh(witness_script) == witness_utxo.script_pubkey
196
210
  # segwit nested in P2SH
197
- if witness_utxo&.script_pubkey.p2sh? && redeem_script&.witness_program? && redeem_script.to_p2sh == witness_utxo&.script_pubkey
211
+ if witness_utxo.script_pubkey.p2sh? && redeem_script&.witness_program? && redeem_script.to_p2sh == witness_utxo.script_pubkey
198
212
  return true if redeem_script.p2wpkh?# nested p2wpkh
199
213
  return true if witness_script&.to_sha256 == redeem_script.witness_data[1].bth # nested p2wsh
200
214
  end
@@ -235,19 +249,19 @@ module Bitcoin
235
249
  # @return [Bitcoin::PSBT::Input] combined object.
236
250
  def merge(psbi)
237
251
  raise ArgumentError, 'The argument psbt must be an instance of Bitcoin::PSBT::Input.' unless psbi.is_a?(Bitcoin::PSBT::Input)
238
- raise ArgumentError, 'The Partially Signed Input\'s non_witness_utxo are different.' unless non_witness_utxo == psbi.non_witness_utxo
239
- raise ArgumentError, 'The Partially Signed Input\'s witness_utxo are different.' unless witness_utxo == psbi.witness_utxo
252
+ raise ArgumentError, 'The Partially Signed Input\'s non_witness_utxo are different.' unless same_field?(non_witness_utxo, psbi.non_witness_utxo)
253
+ raise ArgumentError, 'The Partially Signed Input\'s witness_utxo are different.' unless same_field?(witness_utxo, psbi.witness_utxo)
240
254
  raise ArgumentError, 'The Partially Signed Input\'s sighash_type are different.' if sighash_type && psbi.sighash_type && sighash_type != psbi.sighash_type
241
- raise ArgumentError, 'The Partially Signed Input\'s redeem_script are different.' unless redeem_script == psbi.redeem_script
242
- raise ArgumentError, 'The Partially Signed Input\'s witness_script are different.' unless witness_script == psbi.witness_script
255
+ raise ArgumentError, 'The Partially Signed Input\'s redeem_script are different.' unless same_field?(redeem_script, psbi.redeem_script)
256
+ raise ArgumentError, 'The Partially Signed Input\'s witness_script are different.' unless same_field?(witness_script, psbi.witness_script)
243
257
  combined = Bitcoin::PSBT::Input.new(non_witness_utxo: non_witness_utxo, witness_utxo: witness_utxo)
244
258
  combined.unknowns = Hash[unknowns.merge(psbi.unknowns).sort]
245
259
  combined.redeem_script = redeem_script
246
260
  combined.witness_script = witness_script
247
261
  combined.sighash_type = sighash_type
248
262
  sigs = Hash[partial_sigs.merge(psbi.partial_sigs)]
249
- redeem_script.get_multisig_pubkeys.each{|pubkey|combined.partial_sigs[pubkey.bth] = sigs[pubkey.bth]} if redeem_script&.multisig?
250
- witness_script.get_multisig_pubkeys.each{|pubkey|combined.partial_sigs[pubkey.bth] = sigs[pubkey.bth]} if witness_script&.multisig?
263
+ redeem_script.get_multisig_pubkeys.each{|pubkey|combined.partial_sigs[pubkey.bth] = sigs[pubkey.bth] if sigs[pubkey.bth]} if redeem_script&.multisig?
264
+ witness_script.get_multisig_pubkeys.each{|pubkey|combined.partial_sigs[pubkey.bth] = sigs[pubkey.bth] if sigs[pubkey.bth]} if witness_script&.multisig?
251
265
  combined.hd_key_paths = hd_key_paths.merge(psbi.hd_key_paths)
252
266
  combined
253
267
  end
@@ -288,7 +302,7 @@ module Bitcoin
288
302
  h[:non_witness_utxo] = non_witness_utxo.to_h if non_witness_utxo
289
303
  h[:witness_utxo] = witness_utxo.to_h if witness_utxo
290
304
  h[:redeem_script] = redeem_script.to_h if redeem_script
291
- h[:witness_script] = witness_script.to_h if redeem_script
305
+ h[:witness_script] = witness_script.to_h if witness_script
292
306
  h[:final_script_sig] = final_script_sig.to_h if final_script_sig
293
307
  h[:final_script_witness] = final_script_witness.to_h if final_script_witness
294
308
  h[:bip32_derivs] = hd_key_paths.values.map(&:to_h) unless hd_key_paths.empty?
@@ -309,6 +323,17 @@ module Bitcoin
309
323
  h
310
324
  end
311
325
 
326
+ private
327
+
328
+ # Whether +a+ and +b+ are the same field value. Unlike ==, this does not raise
329
+ # NoMethodError when only one of them is nil.
330
+ # @return [Boolean]
331
+ def same_field?(a, b)
332
+ return true if a.nil? && b.nil?
333
+ return false if a.nil? || b.nil?
334
+ a == b
335
+ end
336
+
312
337
  end
313
338
 
314
339
  end
@@ -15,11 +15,11 @@ module Bitcoin
15
15
 
16
16
  def self.parse_from_payload(payload)
17
17
  buf = StringIO.new(payload)
18
- self.new(fingerprint: buf.read(4).bth, key_paths: buf.read.unpack('I*'))
18
+ self.new(fingerprint: buf.read(4).bth, key_paths: buf.read.unpack('V*'))
19
19
  end
20
20
 
21
21
  def to_payload
22
- fingerprint.htb + key_paths.pack('I*')
22
+ fingerprint.htb + key_paths.pack('V*')
23
23
  end
24
24
 
25
25
  def to_h
@@ -32,8 +32,9 @@ module Bitcoin
32
32
  found_sep = true
33
33
  break
34
34
  end
35
- key_type = buf.read(1).unpack1('C')
36
- key = buf.read(key_len - 1)
35
+ key_start = buf.pos
36
+ key_type = Bitcoin.unpack_var_int_from_io(buf)
37
+ key = buf.read(key_len - (buf.pos - key_start))
37
38
  value = buf.read(Bitcoin.unpack_var_int_from_io(buf))
38
39
  case key_type
39
40
  when PSBT_OUT_TYPES[:redeem_script]
@@ -48,21 +49,24 @@ module Bitcoin
48
49
  raise ArgumentError, 'Duplicate Key, pubkey derivation path already provided' if output.hd_key_paths[key.bth]
49
50
  output.hd_key_paths[key.bth] = Bitcoin::PSBT::HDKeyPath.new(key, Bitcoin::PSBT::KeyOriginInfo.parse_from_payload(value))
50
51
  when PSBT_OUT_TYPES[:proprietary]
51
- raise ArgumentError, 'Duplicate Key, key for proprietary value already provided.' if output.proprietaries.any?{|p| p.key == key}
52
- output.proprietaries << Proprietary.new(key, value)
52
+ proprietary = Proprietary.new(key, value)
53
+ raise ArgumentError, 'Duplicate Key, key for proprietary value already provided.' if output.proprietaries.any?{|p| p.key == proprietary.key}
54
+ output.proprietaries << proprietary
53
55
  when PSBT_OUT_TYPES[:tap_internal_key]
54
56
  raise ArgumentError, 'Invalid output tap internal key typed key.' unless key_len == 1
57
+ raise ArgumentError, 'Duplicate Key, output tap internal key already provided.' if output.tap_internal_key
55
58
  raise ArgumentError, 'Invalid x-only public key size for the type tap internal key' unless value.bytesize == X_ONLY_PUBKEY_SIZE
56
59
  output.tap_internal_key = value.bth
57
60
  when PSBT_OUT_TYPES[:tap_tree]
58
61
  raise ArgumentError, 'Invalid output tap tree typed key.' unless key_len == 1
62
+ raise ArgumentError, 'Duplicate Key, output tap tree already provided.' if output.tap_tree
59
63
  output.tap_tree = value.bth # TODO implement builder.
60
64
  when PSBT_OUT_TYPES[:tap_bip32_derivation]
61
65
  raise ArgumentError, 'Duplicate Key, key for tap bip32 derivation value already provided.' if output.tap_bip32_derivations[key.bth]
62
66
  raise ArgumentError, 'Size of key was not the expected size for the type tap bip32 derivation' unless key.bytesize == X_ONLY_PUBKEY_SIZE
63
67
  output.tap_bip32_derivations[key.bth] = value.bth
64
68
  else
65
- unknown_key = ([key_type].pack('C') + key).bth
69
+ unknown_key = (Bitcoin.pack_var_int(key_type) + key).bth
66
70
  raise ArgumentError, 'Duplicate Key, key for unknown value already provided' if output.unknowns[unknown_key]
67
71
  output.unknowns[unknown_key] = value
68
72
  end
@@ -68,8 +68,9 @@ module Bitcoin
68
68
  found_sep = true
69
69
  break
70
70
  end
71
+ key_start = buf.pos
71
72
  key_type = Bitcoin.unpack_var_int_from_io(buf)
72
- key = buf.read(key_len - 1)
73
+ key = buf.read(key_len - (buf.pos - key_start))
73
74
  value = buf.read(Bitcoin.unpack_var_int_from_io(buf))
74
75
 
75
76
  case key_type
@@ -89,14 +90,19 @@ module Bitcoin
89
90
  raise ArgumentError, "global xpub's depth and the number of indexes not matched." unless xpub.depth == info.key_paths.size
90
91
  partial_tx.xpubs << Bitcoin::PSBT::GlobalXpub.new(xpub, info)
91
92
  when PSBT_GLOBAL_TYPES[:ver]
93
+ raise ArgumentError, 'Invalid global version typed key.' unless key_len == 1
94
+ raise ArgumentError, 'Duplicate Key, global version already provided.' if partial_tx.version_number
95
+ raise ArgumentError, 'Invalid global version value.' unless value.bytesize == 4
92
96
  partial_tx.version_number = value.unpack1('V')
93
97
  raise ArgumentError, "An unsupported version was detected." if SUPPORT_VERSION < partial_tx.version_number
94
98
  when PSBT_GLOBAL_TYPES[:proprietary]
95
- raise ArgumentError, 'Duplicate Key, key for proprietary value already provided.' if partial_tx.proprietaries.any?{|p| p.key == key}
96
- partial_tx.proprietaries << Proprietary.new(key, value)
99
+ proprietary = Proprietary.new(key, value)
100
+ raise ArgumentError, 'Duplicate Key, key for proprietary value already provided.' if partial_tx.proprietaries.any?{|p| p.key == proprietary.key}
101
+ partial_tx.proprietaries << proprietary
97
102
  else
98
- raise ArgumentError, 'Duplicate Key, key for unknown value already provided.' if partial_tx.unknowns[key]
99
- partial_tx.unknowns[([key_type].pack('C') + key).bth] = value
103
+ unknown_key = (Bitcoin.pack_var_int(key_type) + key).bth
104
+ raise ArgumentError, 'Duplicate Key, key for unknown value already provided.' if partial_tx.unknowns[unknown_key]
105
+ partial_tx.unknowns[unknown_key] = value
100
106
  end
101
107
  end
102
108
 
@@ -206,7 +212,6 @@ module Bitcoin
206
212
  inputs[i].redeem_script = redeem_script if redeem_script
207
213
  inputs[i].witness_script = witness_script if witness_script
208
214
  inputs[i].hd_key_paths = hd_key_paths.map(&:pubkey).zip(hd_key_paths).to_h
209
- break
210
215
  end
211
216
  end
212
217
  end
@@ -267,20 +272,22 @@ module Bitcoin
267
272
  # extract final tx.
268
273
  # @return [Bitcoin::Tx] final tx.
269
274
  def extract_tx
270
- extract_tx = tx.dup
275
+ extract_tx = Bitcoin::Tx.parse_from_payload(tx.to_payload)
271
276
  inputs.each_with_index do |input, index|
272
277
  extract_tx.in[index].script_sig = input.final_script_sig if input.final_script_sig
273
278
  extract_tx.in[index].script_witness = input.final_script_witness if input.final_script_witness
274
279
  end
280
+ # A taproot sighash commits to every prevout of the tx, see BIP-341, so they are all
281
+ # collected before any input is verified.
282
+ prevouts = extract_tx.in.each_with_index.map do |tx_in, index|
283
+ utxo = inputs[index].utxo(tx_in.out_point.index)
284
+ raise ArgumentError, "input[#{index}] does not have utxo." unless utxo
285
+ utxo
286
+ end
275
287
  # validate signature
276
- tx.in.each_with_index do |tx_in, index|
277
- input = inputs[index]
278
- if input.non_witness_utxo
279
- utxo = input.non_witness_utxo.out[tx_in.out_point.index]
280
- raise "input[#{index}]'s signature is invalid.'" unless tx.verify_input_sig(index, utxo.script_pubkey)
281
- else
282
- utxo = input.witness_utxo
283
- raise "input[#{index}]'s signature is invalid.'" unless tx.verify_input_sig(index, utxo.script_pubkey, amount: input.witness_utxo.value)
288
+ prevouts.each_with_index do |utxo, index|
289
+ unless extract_tx.verify_input_sig(index, utxo.script_pubkey, amount: utxo.value, prevouts: prevouts)
290
+ raise "input[#{index}]'s signature is invalid.'"
284
291
  end
285
292
  end
286
293
  extract_tx
data/lib/bitcoin/psbt.rb CHANGED
@@ -55,7 +55,7 @@ module Bitcoin
55
55
  module_function
56
56
 
57
57
  def self.serialize_to_vector(key_type, key: nil, value: nil)
58
- key_len = key_type.itb.bytesize
58
+ key_len = Bitcoin.pack_var_int(key_type).bytesize
59
59
  key_len += key.bytesize if key
60
60
  s = Bitcoin.pack_var_int(key_len) << Bitcoin.pack_var_int(key_type)
61
61
  s << key if key
@@ -61,8 +61,9 @@ module Bitcoin
61
61
  request.content_type = 'application/json'
62
62
  request.body = data.to_json
63
63
  response = http.request(request)
64
- body = response.body
65
- json_data = JSON.parse(body.gsub(/\\u([\da-fA-F]{4})/) { [$1].pack('H*').unpack('n*').pack('U*').encode('ISO-8859-1').force_encoding('UTF-8') })
64
+ # Unescape \uXXXX with JSON.parse itself. Doing it beforehand lets a " contained in
65
+ # a string value close that string and inject arbitrary JSON into the parsed result.
66
+ json_data = JSON.parse(response.body)
66
67
  response = convert_floats_to_strings(json_data)
67
68
  raise response['error'].to_json if response['error']
68
69
  response['result']
@@ -100,12 +100,22 @@ module Bitcoin
100
100
  if opcode
101
101
  script << (v =~ /^\d/ && Opcodes.small_int_to_opcode(v.ord) ? v.ord : opcode)
102
102
  else
103
- script << (v =~ /^[0-9]+$/ ? v.to_i : v)
103
+ script << (number_token?(v) ? v.to_i : v)
104
104
  end
105
105
  end
106
106
  script
107
107
  end
108
108
 
109
+ # Whether +token+ is a script number rather than pushed data with hex format.
110
+ # A digit only token is ambiguous since #to_s emits pushed data as hex. #to_s never emits
111
+ # a number with more than 10 digits, so a longer even-length token must be pushed data.
112
+ # @param [String] token a token of the script string.
113
+ # @return [Boolean] whether +token+ should be interpreted as a script number.
114
+ def self.number_token?(token)
115
+ return false unless token =~ /^[0-9]+$/
116
+ token.length < 12 || token.length.odd?
117
+ end
118
+
109
119
  # generate script from addr.
110
120
  # @param [String] addr address.
111
121
  # @return [Bitcoin::Script] parsed script.
@@ -114,7 +124,7 @@ module Bitcoin
114
124
  segwit_addr = Bech32::SegwitAddr.new(addr)
115
125
  raise ArgumentError, 'Invalid address.' unless Bitcoin.chain_params.bech32_hrp == segwit_addr.hrp
116
126
  Bitcoin::Script.parse_from_payload(segwit_addr.to_script_pubkey.htb)
117
- rescue Exception => e
127
+ rescue StandardError => e
118
128
  begin
119
129
  hex, addr_version = Bitcoin.decode_base58_address(addr)
120
130
  rescue
@@ -491,11 +501,11 @@ module Bitcoin
491
501
  size = data.bytesize
492
502
  header = if size < OP_PUSHDATA1
493
503
  [size].pack('C')
494
- elsif size < 0xff
504
+ elsif size <= 0xff
495
505
  [OP_PUSHDATA1, size].pack('CC')
496
- elsif size < 0xffff
506
+ elsif size <= 0xffff
497
507
  [OP_PUSHDATA2, size].pack('Cv')
498
- elsif size < 0xffffffff
508
+ elsif size <= 0xffffffff
499
509
  [OP_PUSHDATA4, size].pack('CV')
500
510
  else
501
511
  raise ArgumentError, 'data size is too big.'
@@ -568,7 +568,7 @@ module Bitcoin
568
568
  end
569
569
  rescue ScriptNumError => e
570
570
  return set_error(SCRIPT_ERR_SCRIPTNUM, e.message)
571
- rescue Exception => e
571
+ rescue StandardError => e
572
572
  return set_error(SCRIPT_ERR_UNKNOWN_ERROR, e.message)
573
573
  end
574
574
 
@@ -30,7 +30,7 @@ module Bitcoin
30
30
  begin
31
31
  key = Key.new(pubkey: pubkey, key_type: key_type, allow_hybrid: allow_hybrid)
32
32
  key.verify(sig, sighash)
33
- rescue Exception
33
+ rescue StandardError
34
34
  false
35
35
  end
36
36
  end
@@ -40,9 +40,14 @@ module Bitcoin
40
40
  # @param [String] pubkey a public key with hex format.
41
41
  # @param [Symbol] sig_version whether :taproot or :tapscript
42
42
  # @return [Boolean] verification result
43
+ # @raise [ArgumentError] If +prevouts+ does not cover every input of the tx.
43
44
  def check_schnorr_sig(sig, pubkey, sig_version, opts = {})
44
45
  return false unless [:taproot, :tapscript].include?(sig_version)
45
- return false if prevouts.size < input_index
46
+ # A taproot sighash commits to the amount and scriptPubkey of every prevout, so a partial
47
+ # set does not fail to verify, it verifies against a sighash consensus never computes.
48
+ # Checked here rather than left to the generator, since the rescue below would turn it
49
+ # into a script error.
50
+ raise ArgumentError, 'prevouts must be specified for all inputs.' unless prevouts.size == tx.in.size
46
51
 
47
52
  sig = sig.htb
48
53
  return set_error(SCRIPT_ERR_SCHNORR_SIG_SIZE) unless [64, 65].include?(sig.bytesize)
@@ -6,7 +6,8 @@ module Bitcoin
6
6
  module Secp256k1
7
7
 
8
8
  # binding for secp256k1 (https://github.com/bitcoin-core/secp256k1/)
9
- # tag: v0.4.0
9
+ # tag: v0.8.0, which secp256k1rb 0.8.0 binds the public API of. An older library is missing
10
+ # symbols the gem attaches, such as secp256k1_ec_pubkey_sort and the musig module.
10
11
  # this is not included by default, to enable set shared object path to ENV['SECP256K1_LIB_PATH']
11
12
  # for linux, ENV['SECP256K1_LIB_PATH'] = '/usr/local/lib/libsecp256k1.so' or '/usr/lib64/libsecp256k1.so'
12
13
  # for mac,
@@ -98,6 +99,54 @@ module Bitcoin
98
99
  raise ArgumentError, "unknown algo: #{algo}"
99
100
  end
100
101
  end
102
+
103
+ # Whether the loaded library supports BIP-352 silent payments.
104
+ # The module exists in libsecp256k1 v0.8.0 or later.
105
+ # @return [Boolean]
106
+ def sp_available?
107
+ silentpayments_available?
108
+ end
109
+
110
+ # Create the silent payment outputs for +recipients+.
111
+ # @param [Array] recipients An array of [scan public key, spend public key], both with hex
112
+ # format(33 bytes). A recipient may appear more than once.
113
+ # @param [String] outpoint_smallest The lexicographically smallest outpoint of the tx inputs
114
+ # with hex format(36 bytes).
115
+ # @param [Array] plain_seckeys Private keys of the non-taproot inputs with hex format.
116
+ # @param [Array] taproot_seckeys Private keys of the taproot inputs with hex format.
117
+ # @return [Array] An x-only public key with hex format for each recipient, in the same order.
118
+ # @raise [Secp256k1::Error] If the outputs could not be created.
119
+ def sp_create_outputs(recipients, outpoint_smallest, plain_seckeys: [], taproot_seckeys: [])
120
+ silentpayments_sender_create_outputs(
121
+ recipients, outpoint_smallest, plain_seckeys: plain_seckeys, taproot_seckeys: taproot_seckeys)
122
+ end
123
+
124
+ # Create the label and the label tweak of the +m+ th label of +scan_key+.
125
+ # @param [String] scan_key The recipient's scan private key with hex format(32 bytes).
126
+ # @param [Integer] m The label index.
127
+ # @return [Array] The serialized label(33 bytes) and its tweak(32 bytes), both with hex format.
128
+ def sp_create_label(scan_key, m)
129
+ silentpayments_create_label(scan_key, m)
130
+ end
131
+
132
+ # Scan +tx_outputs+ for the silent payment outputs of the recipient.
133
+ # @param [Array] tx_outputs The x-only public key of each taproot output with hex format.
134
+ # @param [String] scan_key The recipient's scan private key with hex format(32 bytes).
135
+ # @param [String] outpoint_smallest The lexicographically smallest outpoint of the tx inputs
136
+ # with hex format(36 bytes).
137
+ # @param [String] spend_pubkey The recipient's spend public key with hex format(33 bytes).
138
+ # @param [Array] plain_pubkeys Public keys of the non-taproot inputs with hex format(33 bytes).
139
+ # @param [Array] xonly_pubkeys X-only public keys of the taproot inputs with hex format.
140
+ # @param [Hash] labels A serialized label to label tweak map, both with hex format.
141
+ # @return [Array] A hash per found output, with the :output, :tweak and :label keys.
142
+ # @raise [Secp256k1::Error] If the tx is not a silent payment transaction.
143
+ def sp_scan_outputs(tx_outputs, scan_key, outpoint_smallest, spend_pubkey,
144
+ plain_pubkeys: [], xonly_pubkeys: [], labels: {})
145
+ summary = silentpayments_create_prevouts_summary(
146
+ outpoint_smallest, plain_pubkeys: plain_pubkeys, xonly_pubkeys: xonly_pubkeys)
147
+ silentpayments_scan_outputs(
148
+ tx_outputs, scan_key, summary, spend_pubkey, labels: labels.empty? ? nil : labels)
149
+ end
101
150
  end
102
151
  end
103
152
  end
@@ -26,10 +26,13 @@ module Bitcoin
26
26
  # 3.2.g
27
27
  v = Bitcoin.hmac_sha256(k, v)
28
28
  # 3.2.h
29
- t = ''
30
29
  10000.times do
31
- v = Bitcoin.hmac_sha256(k, v)
32
- t = (t + v)
30
+ # T is reset for each candidate, otherwise a retry can never produce a value below the order.
31
+ t = ''
32
+ while t.bytesize < 32
33
+ v = Bitcoin.hmac_sha256(k, v)
34
+ t = (t + v)
35
+ end
33
36
  t_num = t.bth.to_i(16)
34
37
  return t_num if 1 <= t_num && t_num < Bitcoin::Secp256k1::GROUP.order
35
38
  k = Bitcoin.hmac_sha256(k, v + '00'.htb)
@@ -45,7 +45,7 @@ module Bitcoin
45
45
  return false unless pubkey.bytesize == X_ONLY_PUBKEY_SIZE
46
46
  begin
47
47
  ECDSA::Format::PointOctetString.decode(pubkey, ECDSA::Group::Secp256k1)
48
- rescue Exception
48
+ rescue StandardError
49
49
  return false
50
50
  end
51
51
  true
@@ -201,7 +201,7 @@ module Bitcoin
201
201
  k = ECDSA::Format::PointOctetString.decode(repack_pubkey(pubkey), GROUP)
202
202
  signature = ECDSA::Format::SignatureDerString.decode(sig)
203
203
  ECDSA.valid_signature?(k, data, signature)
204
- rescue Exception
204
+ rescue StandardError
205
205
  false
206
206
  end
207
207
  end
@@ -210,6 +210,119 @@ module Bitcoin
210
210
  Schnorr.valid_sig?(data, pubkey.htb, sig)
211
211
  end
212
212
 
213
+ # Whether this module supports BIP-352 silent payments.
214
+ # @return [Boolean]
215
+ def sp_available?
216
+ true
217
+ end
218
+
219
+ # Create the silent payment outputs for +recipients+.
220
+ # See Bitcoin::Secp256k1::Native#sp_create_outputs for the parameters.
221
+ # @return [Array] An x-only public key with hex format for each recipient, in the same order.
222
+ # @raise [ArgumentError] If the input private keys sum to zero.
223
+ def sp_create_outputs(recipients, outpoint_smallest, plain_seckeys: [], taproot_seckeys: [])
224
+ field = ECDSA::PrimeField.new(GROUP.order)
225
+ sum = sp_sum_seckeys(plain_seckeys, taproot_seckeys, field)
226
+ raise ArgumentError, 'The input private keys sum to zero.' if sum.zero?
227
+ agg_pubkey = (GROUP.generator.to_jacobian * sum).to_affine
228
+ input_hash = Bitcoin.tagged_hash('BIP0352/Inputs', outpoint_smallest.htb + agg_pubkey.to_hex.htb)
229
+
230
+ # k counts up within the group of recipients sharing a scan key, but an output keeps the
231
+ # position of the recipient it pays.
232
+ groups = {}
233
+ recipients.each_with_index do |(scan_pubkey, spend_pubkey), index|
234
+ (groups[scan_pubkey] ||= []) << [spend_pubkey, index]
235
+ end
236
+ results = Array.new(recipients.length)
237
+ groups.each do |scan_pubkey, spends|
238
+ scan_point = Bitcoin::Key.new(pubkey: scan_pubkey).to_point.to_jacobian
239
+ shared_secret = (scan_point * field.mod(input_hash.bti * sum)).to_affine.to_hex.htb
240
+ spends.each_with_index do |(spend_pubkey, index), k|
241
+ t_k = Bitcoin.tagged_hash('BIP0352/SharedSecret', shared_secret + [k].pack('N'))
242
+ spend_point = Bitcoin::Key.new(pubkey: spend_pubkey).to_point.to_jacobian
243
+ output = (spend_point + GROUP.generator.to_jacobian * t_k.bti).to_affine
244
+ results[index] = sp_xonly(output.x)
245
+ end
246
+ end
247
+ results
248
+ end
249
+
250
+ # Create the label and the label tweak of the +m+ th label of +scan_key+.
251
+ # See Bitcoin::Secp256k1::Native#sp_create_label for the parameters.
252
+ # @return [Array] The serialized label(33 bytes) and its tweak(32 bytes), both with hex format.
253
+ def sp_create_label(scan_key, m)
254
+ tweak = Bitcoin.tagged_hash('BIP0352/Label', scan_key.htb + [m].pack('N'))
255
+ [(GROUP.generator.to_jacobian * tweak.bti).to_affine.to_hex(true), tweak.bth]
256
+ end
257
+
258
+ # Scan +tx_outputs+ for the silent payment outputs of the recipient.
259
+ # See Bitcoin::Secp256k1::Native#sp_scan_outputs for the parameters.
260
+ # @return [Array] A hash per found output, with the :output, :tweak and :label keys.
261
+ # @raise [ArgumentError] If the input public keys sum to the point at infinity.
262
+ def sp_scan_outputs(tx_outputs, scan_key, outpoint_smallest, spend_pubkey,
263
+ plain_pubkeys: [], xonly_pubkeys: [], labels: {})
264
+ field = ECDSA::PrimeField.new(GROUP.order)
265
+ sum_pubkeys = GROUP.infinity.to_jacobian
266
+ plain_pubkeys.each { |p| sum_pubkeys += Bitcoin::Key.new(pubkey: p).to_point.to_jacobian }
267
+ xonly_pubkeys.each { |p| sum_pubkeys += Bitcoin::Key.from_xonly_pubkey(p).to_point.to_jacobian }
268
+ raise ArgumentError, 'The input public keys sum to the point at infinity.' if sum_pubkeys.infinity?
269
+
270
+ input_hash = Bitcoin.tagged_hash(
271
+ 'BIP0352/Inputs', outpoint_smallest.htb + sum_pubkeys.to_affine.to_hex.htb)
272
+ shared_secret = (sum_pubkeys * field.mod(input_hash.bti * scan_key.to_i(16))).to_affine.to_hex.htb
273
+ spend_point = Bitcoin::Key.new(pubkey: spend_pubkey).to_point.to_jacobian
274
+ # A labeled output is P_k + label. Only the x coordinate is compared, which covers the
275
+ # label of either parity without negating the output.
276
+ label_points = labels.map do |label, tweak|
277
+ [label, tweak, Bitcoin::Key.new(pubkey: label).to_point.to_jacobian]
278
+ end
279
+
280
+ results = []
281
+ remaining = tx_outputs.map(&:downcase)
282
+ k = 0
283
+ while k < Bitcoin::SilentPayment::K_MAX
284
+ t_k = Bitcoin.tagged_hash('BIP0352/SharedSecret', shared_secret + [k].pack('N'))
285
+ p_k = GROUP.generator.to_jacobian * t_k.bti + spend_point
286
+ index = remaining.index(sp_xonly(p_k.to_affine.x))
287
+ found = if index
288
+ {output: remaining.delete_at(index), tweak: t_k.bth, label: nil}
289
+ else
290
+ sp_find_labeled(p_k, remaining, label_points, t_k, field)
291
+ end
292
+ break unless found
293
+ results << found
294
+ k += 1
295
+ end
296
+ results
297
+ end
298
+
299
+ # Sum the private keys of the inputs, negating a taproot key whose public key has odd y.
300
+ def sp_sum_seckeys(plain_seckeys, taproot_seckeys, field)
301
+ sum = plain_seckeys.inject(0) { |total, sk| field.mod(total + sk.to_i(16)) }
302
+ taproot_seckeys.inject(sum) do |total, sk|
303
+ d = sk.to_i(16)
304
+ d = field.mod(-d) unless (GROUP.generator.to_jacobian * d).to_affine.has_even_y?
305
+ field.mod(total + d)
306
+ end
307
+ end
308
+
309
+ # Find the output +p_k+ pays through one of +label_points+, or nil.
310
+ def sp_find_labeled(p_k, remaining, label_points, t_k, field)
311
+ label_points.each do |label, tweak, point|
312
+ index = remaining.index(sp_xonly((p_k + point).to_affine.x))
313
+ next unless index
314
+ return {output: remaining.delete_at(index),
315
+ tweak: sp_xonly(field.mod(t_k.bti + tweak.to_i(16))),
316
+ label: label}
317
+ end
318
+ nil
319
+ end
320
+
321
+ # Serialize a field element as a 32 byte value with hex format.
322
+ def sp_xonly(value)
323
+ ECDSA::Format::IntegerOctetString.encode(value, 32).bth
324
+ end
325
+
213
326
  end
214
327
  end
215
328
  end