bitcoinrb 1.15.0 → 1.16.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 (101) hide show
  1. checksums.yaml +4 -4
  2. data/bitcoinrb.gemspec +5 -4
  3. data/lib/bitcoin/bip321_uri.rb +95 -32
  4. data/lib/bitcoin/bip324/ell_swift_pubkey.rb +3 -1
  5. data/lib/bitcoin/bip324/fs_chacha20.rb +3 -0
  6. data/lib/bitcoin/bip324.rb +1 -1
  7. data/lib/bitcoin/bip85_entropy.rb +79 -29
  8. data/lib/bitcoin/bit_stream.rb +2 -2
  9. data/lib/bitcoin/block.rb +46 -7
  10. data/lib/bitcoin/block_filter.rb +3 -2
  11. data/lib/bitcoin/block_header.rb +31 -8
  12. data/lib/bitcoin/bloom_filter.rb +5 -2
  13. data/lib/bitcoin/chainparams/regtest.yml +0 -1
  14. data/lib/bitcoin/chainparams/signet.yml +1 -1
  15. data/lib/bitcoin/constants.rb +13 -3
  16. data/lib/bitcoin/descriptor/combo.rb +13 -1
  17. data/lib/bitcoin/descriptor/expression.rb +35 -15
  18. data/lib/bitcoin/descriptor/multi.rb +9 -1
  19. data/lib/bitcoin/descriptor/multi_a.rb +4 -0
  20. data/lib/bitcoin/descriptor/musig.rb +1 -0
  21. data/lib/bitcoin/descriptor/pk.rb +7 -0
  22. data/lib/bitcoin/descriptor/pkh.rb +5 -0
  23. data/lib/bitcoin/descriptor/raw.rb +1 -1
  24. data/lib/bitcoin/descriptor/sh.rb +1 -0
  25. data/lib/bitcoin/descriptor/sp.rb +2 -2
  26. data/lib/bitcoin/descriptor/tr.rb +29 -3
  27. data/lib/bitcoin/descriptor/wpkh.rb +1 -0
  28. data/lib/bitcoin/descriptor/wsh.rb +1 -0
  29. data/lib/bitcoin/descriptor.rb +59 -35
  30. data/lib/bitcoin/ext/array_ext.rb +3 -0
  31. data/lib/bitcoin/ext/ecdsa.rb +7 -1
  32. data/lib/bitcoin/ext_key.rb +10 -0
  33. data/lib/bitcoin/gcs_filter.rb +21 -6
  34. data/lib/bitcoin/key.rb +66 -14
  35. data/lib/bitcoin/key_path.rb +8 -5
  36. data/lib/bitcoin/message/addr.rb +1 -0
  37. data/lib/bitcoin/message/addr_v2.rb +1 -0
  38. data/lib/bitcoin/message/base.rb +8 -1
  39. data/lib/bitcoin/message/block.rb +4 -8
  40. data/lib/bitcoin/message/block_transaction_request.rb +11 -8
  41. data/lib/bitcoin/message/block_transactions.rb +2 -1
  42. data/lib/bitcoin/message/cf_parser.rb +1 -0
  43. data/lib/bitcoin/message/cfcheckpt.rb +3 -3
  44. data/lib/bitcoin/message/cfheaders.rb +5 -4
  45. data/lib/bitcoin/message/cfilter.rb +3 -3
  46. data/lib/bitcoin/message/error.rb +3 -1
  47. data/lib/bitcoin/message/fee_filter.rb +1 -0
  48. data/lib/bitcoin/message/filter_add.rb +3 -1
  49. data/lib/bitcoin/message/filter_load.rb +4 -4
  50. data/lib/bitcoin/message/get_cfcheckpt.rb +1 -0
  51. data/lib/bitcoin/message/header_and_short_ids.rb +26 -11
  52. data/lib/bitcoin/message/headers.rb +10 -2
  53. data/lib/bitcoin/message/headers_parser.rb +4 -2
  54. data/lib/bitcoin/message/inventories_parser.rb +2 -1
  55. data/lib/bitcoin/message/inventory.rb +15 -6
  56. data/lib/bitcoin/message/merkle_block.rb +4 -4
  57. data/lib/bitcoin/message/network_addr.rb +13 -13
  58. data/lib/bitcoin/message/ping.rb +1 -0
  59. data/lib/bitcoin/message/pong.rb +1 -0
  60. data/lib/bitcoin/message/reject.rb +1 -0
  61. data/lib/bitcoin/message/send_cmpct.rb +2 -2
  62. data/lib/bitcoin/message/send_tx_rcncl.rb +1 -1
  63. data/lib/bitcoin/message/version.rb +23 -8
  64. data/lib/bitcoin/message.rb +15 -0
  65. data/lib/bitcoin/message_sign.rb +44 -6
  66. data/lib/bitcoin/mnemonic.rb +8 -0
  67. data/lib/bitcoin/out_point.rb +6 -2
  68. data/lib/bitcoin/partial_tree.rb +41 -28
  69. data/lib/bitcoin/payment_code.rb +26 -8
  70. data/lib/bitcoin/psbt/hd_key_path.rb +1 -2
  71. data/lib/bitcoin/psbt/input.rb +199 -48
  72. data/lib/bitcoin/psbt/key_origin_info.rb +2 -0
  73. data/lib/bitcoin/psbt/output.rb +9 -6
  74. data/lib/bitcoin/psbt/proprietary.rb +6 -3
  75. data/lib/bitcoin/psbt/tx.rb +9 -4
  76. data/lib/bitcoin/psbt.rb +46 -0
  77. data/lib/bitcoin/rpc/bitcoin_core_client.rb +54 -13
  78. data/lib/bitcoin/rpc/error.rb +43 -0
  79. data/lib/bitcoin/rpc.rb +1 -0
  80. data/lib/bitcoin/script/script.rb +114 -33
  81. data/lib/bitcoin/script/script_interpreter.rb +11 -6
  82. data/lib/bitcoin/script/tx_checker.rb +7 -2
  83. data/lib/bitcoin/script_witness.rb +5 -1
  84. data/lib/bitcoin/secp256k1/ruby.rb +10 -4
  85. data/lib/bitcoin/silent_payment.rb +6 -3
  86. data/lib/bitcoin/slip39/share.rb +35 -9
  87. data/lib/bitcoin/slip39/sss.rb +26 -11
  88. data/lib/bitcoin/slip39.rb +13 -5
  89. data/lib/bitcoin/taproot/control_block.rb +6 -1
  90. data/lib/bitcoin/taproot/custom_depth_builder.rb +12 -0
  91. data/lib/bitcoin/tx.rb +35 -14
  92. data/lib/bitcoin/tx_in.rb +2 -2
  93. data/lib/bitcoin/tx_out.rb +2 -2
  94. data/lib/bitcoin/util.rb +49 -28
  95. data/lib/bitcoin/validation.rb +64 -12
  96. data/lib/bitcoin/version.rb +1 -1
  97. data/lib/bitcoin/wallet/account.rb +35 -11
  98. data/lib/bitcoin/wallet/base.rb +40 -15
  99. data/lib/bitcoin/wallet/utxo.rb +22 -4
  100. data/lib/bitcoin.rb +16 -4
  101. metadata +24 -9
@@ -73,8 +73,7 @@ module Bitcoin
73
73
  if key.size != Bitcoin::Key::PUBLIC_KEY_SIZE && key.size != Bitcoin::Key::COMPRESSED_PUBLIC_KEY_SIZE
74
74
  raise ArgumentError, 'Size of key was not the expected size for the type partial signature pubkey.'
75
75
  end
76
- pubkey = Bitcoin::Key.new(pubkey: key.bth)
77
- raise ArgumentError, Errors::Messages::INVALID_PUBLIC_KEY unless pubkey.fully_valid_pubkey?
76
+ pubkey = PSBT.parse_pubkey(key.bth)
78
77
  raise ArgumentError, 'Duplicate Key, input partial signature for pubkey already provided.' if input.partial_sigs[pubkey.pubkey]
79
78
  input.partial_sigs[pubkey.pubkey] = value
80
79
  when PSBT_IN_TYPES[:sighash]
@@ -136,6 +135,7 @@ module Bitcoin
136
135
  begin
137
136
  cb = Bitcoin::Taproot::ControlBlock.parse_from_payload(key)
138
137
  raise ArgumentError, 'Duplicate Key, key for tap leaf script value already provided.' if input.tap_leaf_scripts[cb]
138
+ raise ArgumentError, 'Tap leaf script value must have a leaf version.' if value.bytesize < 1
139
139
  input.tap_leaf_scripts[cb] = value.bth
140
140
  rescue Bitcoin::Taproot::Error => e
141
141
  raise ArgumentError, e.message
@@ -143,6 +143,7 @@ module Bitcoin
143
143
  when PSBT_IN_TYPES[:tap_bip32_derivation]
144
144
  raise ArgumentError, 'Duplicate Key, key for tap bip32 derivation value already provided.' if input.tap_bip32_derivations[key.bth]
145
145
  raise ArgumentError, 'Size of key was not the expected size for the type tap bip32 derivation' unless key.bytesize == X_ONLY_PUBKEY_SIZE
146
+ PSBT.validate_tap_bip32_derivation!(value)
146
147
  input.tap_bip32_derivations[key.bth] = value.bth
147
148
  when PSBT_IN_TYPES[:tap_internal_key]
148
149
  raise ArgumentError, 'Size of key was not the expected size for the type tap internal key' unless key_len == 1
@@ -239,6 +240,7 @@ module Bitcoin
239
240
  # @param [String] pubkey a public key with hex format.
240
241
  # @param [String] sig a signature.
241
242
  def add_sig(pubkey, sig)
243
+ PSBT.parse_pubkey(pubkey)
242
244
  raise ArgumentError, 'The sighash in signature is invalid.' if sighash_type && sig.unpack('C*')[-1] != sighash_type
243
245
  raise ArgumentError, 'Duplicate Key, input partial signature for pubkey already provided.' if partial_sigs[pubkey]
244
246
  partial_sigs[pubkey] = sig
@@ -249,51 +251,67 @@ module Bitcoin
249
251
  # @return [Bitcoin::PSBT::Input] combined object.
250
252
  def merge(psbi)
251
253
  raise ArgumentError, 'The argument psbt must be an instance of Bitcoin::PSBT::Input.' unless psbi.is_a?(Bitcoin::PSBT::Input)
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)
254
- raise ArgumentError, 'The Partially Signed Input\'s sighash_type are different.' if sighash_type && psbi.sighash_type && sighash_type != psbi.sighash_type
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)
257
- combined = Bitcoin::PSBT::Input.new(non_witness_utxo: non_witness_utxo, witness_utxo: witness_utxo)
258
- combined.unknowns = Hash[unknowns.merge(psbi.unknowns).sort]
259
- combined.redeem_script = redeem_script
260
- combined.witness_script = witness_script
261
- combined.sighash_type = sighash_type
262
- sigs = Hash[partial_sigs.merge(psbi.partial_sigs)]
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?
265
- combined.hd_key_paths = hd_key_paths.merge(psbi.hd_key_paths)
254
+ field = ->(name) { PSBT.merge_field(send(name), psbi.send(name), "Input's #{name}") }
255
+ combined = Bitcoin::PSBT::Input.new(non_witness_utxo: field.call(:non_witness_utxo), witness_utxo: field.call(:witness_utxo))
256
+ # Fields which hold one value: present on one side, or equal on both.
257
+ %i[redeem_script witness_script sighash_type final_script_sig final_script_witness
258
+ tap_key_sig tap_internal_key tap_merkle_root].each do |name|
259
+ combined.send("#{name}=", field.call(name))
260
+ end
261
+ # Maps: the union. For a key present on both sides this input's value is kept, as Bitcoin Core's
262
+ # combiner inserts the other map into its own.
263
+ combined.partial_sigs = merge_partial_sigs(psbi.partial_sigs)
264
+ %i[hd_key_paths ripemd160_preimages sha256_preimages hash160_preimages hash256_preimages
265
+ tap_script_sigs tap_leaf_scripts tap_bip32_derivations].each do |name|
266
+ combined.send("#{name}=", psbi.send(name).merge(send(name)))
267
+ end
268
+ combined.proprietaries = PSBT.merge_proprietaries(proprietaries, psbi.proprietaries)
269
+ combined.unknowns = Hash[psbi.unknowns.merge(unknowns).sort]
266
270
  combined
267
271
  end
268
272
 
269
- # finalize input.
270
- # TODO This feature is experimental and support only multisig.
273
+ # Finalize this input, building the final scriptSig and witness from the partial signatures and scripts.
274
+ # Supported: P2PKH, P2PK, bare multisig, P2WPKH, P2WSH, P2SH wrapping any of them, and P2TR by the key
275
+ # path or a pk() or multi_a() leaf script. Anything else,
276
+ # a missing script or too few signatures raises rather than producing an unspendable input.
277
+ # @param [Integer] prevout_index The index of the output this input spends, used to look up the scriptPubKey
278
+ # in +non_witness_utxo+. Not needed when +witness_utxo+ or a redeem/witness script is present.
271
279
  # @return [Bitcoin::PSBT::Input] finalized input.
272
- def finalize!
273
- if non_witness_utxo
274
- self.final_script_sig = Bitcoin::Script.new << Bitcoin::Opcodes::OP_0 if redeem_script.multisig?
275
- partial_sigs.values.each {|sig|final_script_sig << sig}
276
- final_script_sig << redeem_script.to_hex
277
- self.partial_sigs = {}
278
- self.hd_key_paths = {}
279
- self.redeem_script = nil
280
- self.sighash_type = nil
281
- else
282
- if redeem_script
283
- self.final_script_sig = Bitcoin::Script.parse_from_payload(Bitcoin::Script.pack_pushdata(redeem_script.to_payload))
284
- self.redeem_script = nil
285
- end
286
- if witness_script
287
- self.final_script_witness = Bitcoin::ScriptWitness.new
288
- final_script_witness.stack << '' if witness_script.multisig?
289
- partial_sigs.values.each {|sig| final_script_witness.stack << sig}
290
- final_script_witness.stack << witness_script.to_payload
291
- self.witness_script = nil
280
+ # @raise [ArgumentError] If the input cannot be finalized.
281
+ def finalize!(prevout_index = nil)
282
+ return self if signed?
283
+ script_pubkey = spent_script_pubkey(prevout_index)
284
+ if script_pubkey.p2sh?
285
+ raise ArgumentError, 'The redeem script is missing.' unless redeem_script
286
+ raise ArgumentError, 'The redeem script does not match the scriptPubKey.' unless redeem_script.to_p2sh == script_pubkey
287
+ if redeem_script.witness_program?
288
+ self.final_script_witness = Bitcoin::ScriptWitness.new(witness_stack(redeem_script))
289
+ self.final_script_sig = Bitcoin::Script.new << redeem_script.to_payload
290
+ else
291
+ self.final_script_sig = Bitcoin::Script.new
292
+ satisfaction(redeem_script).each { |item| final_script_sig << item }
293
+ final_script_sig << redeem_script.to_payload
292
294
  end
293
- self.sighash_type = nil
294
- self.partial_sigs = {}
295
- self.hd_key_paths = {}
295
+ elsif script_pubkey.p2tr?
296
+ self.final_script_witness = Bitcoin::ScriptWitness.new(taproot_witness_stack)
297
+ elsif script_pubkey.witness_program?
298
+ self.final_script_witness = Bitcoin::ScriptWitness.new(witness_stack(script_pubkey))
299
+ else
300
+ self.final_script_sig = Bitcoin::Script.new
301
+ satisfaction(script_pubkey).each { |item| final_script_sig << item }
296
302
  end
303
+ # A finalized input carries nothing but the final scripts, as Bitcoin Core's finalizer leaves it.
304
+ self.partial_sigs = {}
305
+ self.hd_key_paths = {}
306
+ self.redeem_script = nil
307
+ self.witness_script = nil
308
+ self.sighash_type = nil
309
+ self.tap_key_sig = nil
310
+ self.tap_script_sigs = {}
311
+ self.tap_leaf_scripts = {}
312
+ self.tap_bip32_derivations = {}
313
+ self.tap_internal_key = nil
314
+ self.tap_merkle_root = nil
297
315
  self
298
316
  end
299
317
 
@@ -325,13 +343,146 @@ module Bitcoin
325
343
 
326
344
  private
327
345
 
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
346
+ # The union of the partial signatures of this input and +others+. Signatures for the keys of a multisig
347
+ # redeem or witness script come first in the order of the script, which keeps the serialization stable,
348
+ # and any other signature follows.
349
+ # @param [Hash] others partial signatures of the other input.
350
+ # @return [Hash]
351
+ def merge_partial_sigs(others)
352
+ sigs = others.merge(partial_sigs)
353
+ script = [redeem_script, witness_script].compact.find(&:multisig?)
354
+ return sigs unless script
355
+ ordered = {}
356
+ script.get_multisig_pubkeys.each { |pubkey| ordered[pubkey.bth] = sigs[pubkey.bth] if sigs[pubkey.bth] }
357
+ ordered.merge(sigs)
358
+ end
359
+
360
+ # The scriptPubKey this input spends.
361
+ # @param [Integer, nil] prevout_index The index of the output in +non_witness_utxo+.
362
+ # @return [Bitcoin::Script]
363
+ # @raise [ArgumentError] If it cannot be determined.
364
+ def spent_script_pubkey(prevout_index)
365
+ return witness_utxo.script_pubkey if witness_utxo
366
+ if non_witness_utxo && prevout_index
367
+ out = non_witness_utxo.out[prevout_index]
368
+ raise ArgumentError, "non_witness_utxo does not have output #{prevout_index}." unless out
369
+ return out.script_pubkey
370
+ end
371
+ # Without the utxo the scriptPubKey can still be inferred from the scripts.
372
+ return redeem_script.to_p2sh if redeem_script
373
+ return Bitcoin::Script.to_p2wsh(witness_script) if witness_script
374
+ raise ArgumentError, 'The scriptPubKey of this input cannot be determined. Set witness_utxo, or non_witness_utxo and pass the prevout index.'
375
+ end
376
+
377
+ # The witness stack satisfying the witness program +program+.
378
+ # @param [Bitcoin::Script] program A P2WPKH or P2WSH script.
379
+ # @return [Array[String]] stack items with binary format.
380
+ def witness_stack(program)
381
+ if program.p2wpkh?
382
+ satisfaction(program)
383
+ elsif program.p2wsh?
384
+ raise ArgumentError, 'The witness script is missing.' unless witness_script
385
+ raise ArgumentError, 'The witness script does not match the witness program.' unless Bitcoin::Script.to_p2wsh(witness_script) == program
386
+ satisfaction(witness_script) + [witness_script.to_payload]
387
+ else
388
+ raise ArgumentError, "Finalizing a witness program of this type is not supported: #{program}"
389
+ end
390
+ end
391
+
392
+ # The stack items which satisfy +script+, built from the partial signatures.
393
+ # @param [Bitcoin::Script] script A P2PKH, P2PK, P2WPKH or bare multisig script.
394
+ # @return [Array[String]] items with binary format, in the order they are pushed.
395
+ def satisfaction(script)
396
+ if script.p2pkh? || script.p2wpkh?
397
+ hash = (script.p2pkh? ? script.chunks[2] : script.chunks[1]).pushed_data.bth
398
+ pubkey, sig = partial_sigs.find { |pk, _| Bitcoin.hash160(pk) == hash }
399
+ raise ArgumentError, 'The partial signature for the key of this input is missing.' unless sig
400
+ [sig, pubkey.htb]
401
+ elsif script.p2pk?
402
+ sig = partial_sigs[script.chunks[0].pushed_data.bth]
403
+ raise ArgumentError, 'The partial signature for the key of this input is missing.' unless sig
404
+ [sig]
405
+ elsif script.multisig?
406
+ # Signatures are pushed in the order of the keys in the script, which is what OP_CHECKMULTISIG requires,
407
+ # and only as many as the threshold needs.
408
+ threshold = Bitcoin::Opcodes.opcode_to_small_int(script.chunks[0].opcode)
409
+ sigs = script.get_multisig_pubkeys.map { |pubkey| partial_sigs[pubkey.bth] }.compact
410
+ raise ArgumentError, "Not enough signatures: #{threshold} required, #{sigs.size} present." if sigs.size < threshold
411
+ [''] + sigs.first(threshold) # The empty item is consumed by the extra pop of OP_CHECKMULTISIG.
412
+ else
413
+ raise ArgumentError, "Finalizing a script of this type is not supported: #{script}"
414
+ end
415
+ end
416
+
417
+ # The witness stack spending a P2TR output, built as Bitcoin Core's SignTaproot does: the key path
418
+ # signature if there is one, otherwise the smallest witness among the leaf scripts whose signatures are
419
+ # all present.
420
+ # @return [Array[String]] stack items with binary format.
421
+ def taproot_witness_stack
422
+ return [tap_key_sig.htb] if tap_key_sig
423
+ stacks = tap_leaf_scripts.map do |control_block, value|
424
+ payload = value.htb
425
+ raise ArgumentError, 'Tap leaf script value must have a leaf version.' if payload.empty?
426
+ leaf_ver = payload[-1].ord # BIP-371: the script followed by its leaf version.
427
+ next unless leaf_ver == Bitcoin::TAPROOT_LEAF_TAPSCRIPT && leaf_ver == control_block.leaf_ver
428
+ script = Bitcoin::Script.parse_from_payload(payload[0...-1])
429
+ leaf_hash = Bitcoin::Taproot::LeafNode.new(script, leaf_ver).leaf_hash.bth
430
+ sigs = tapscript_satisfaction(script, leaf_hash)
431
+ sigs + [script.to_payload, control_block.to_payload] if sigs
432
+ end.compact
433
+ if stacks.empty?
434
+ raise ArgumentError, 'Neither the key path signature nor the signatures of a leaf script are present.'
435
+ end
436
+ stacks.min_by { |stack| [Bitcoin::ScriptWitness.new(stack).to_payload.bytesize, stack.last] }
437
+ end
438
+
439
+ # The stack items which satisfy the tapscript +script+, built from the tap script signatures.
440
+ # @param [Bitcoin::Script] script A pk() or multi_a() leaf script.
441
+ # @param [String] leaf_hash The leaf hash of +script+ with hex format.
442
+ # @return [Array[String], nil] items with binary format, or nil if the signatures are not all present.
443
+ def tapscript_satisfaction(script, leaf_hash)
444
+ chunks = script.chunks
445
+ # pk(): <32 byte key> OP_CHECKSIG
446
+ if chunks.size == 2 && xonly_push?(chunks[0]) && chunks[1].opcode == Bitcoin::Opcodes::OP_CHECKSIG
447
+ sig = tap_script_sigs[chunks[0].pushed_data.bth + leaf_hash]
448
+ return sig && [sig.htb]
449
+ end
450
+ # multi_a(): <key> OP_CHECKSIG (<key> OP_CHECKSIGADD)* <k> OP_NUMEQUAL
451
+ keys, threshold = multi_a_keys(chunks)
452
+ return nil unless keys
453
+ # OP_CHECKSIG takes the top item, so the signature for the first key is the last item pushed. The first
454
+ # k signatures found are used and every other key gets an empty item, as in Bitcoin Core.
455
+ good = 0
456
+ sigs = keys.reverse.map do |key|
457
+ sig = tap_script_sigs[key + leaf_hash]
458
+ next '' unless sig && good < threshold
459
+ good += 1
460
+ sig.htb
461
+ end
462
+ good == threshold ? sigs : nil
463
+ end
464
+
465
+ def xonly_push?(chunk)
466
+ chunk.pushdata? && chunk.pushed_data.bytesize == Bitcoin::X_ONLY_PUBKEY_SIZE
467
+ end
468
+
469
+ # @param [Array[String]] chunks The chunks of a script.
470
+ # @return [Array, nil] The x-only keys with hex format and the threshold of a multi_a() script, or nil.
471
+ def multi_a_keys(chunks)
472
+ return nil unless chunks.size >= 4 && chunks.size.even? && chunks[-1].opcode == Bitcoin::Opcodes::OP_NUMEQUAL
473
+ keys = []
474
+ chunks[0...-2].each_slice(2).with_index do |(key, op), i|
475
+ return nil unless xonly_push?(key) && op.opcode == (i.zero? ? Bitcoin::Opcodes::OP_CHECKSIG : Bitcoin::Opcodes::OP_CHECKSIGADD)
476
+ keys << key.pushed_data.bth
477
+ end
478
+ k = chunks[-2]
479
+ threshold = if k.pushdata?
480
+ Bitcoin::Script.decode_number(k.pushed_data.bth)
481
+ elsif (Bitcoin::Opcodes::OP_1..Bitcoin::Opcodes::OP_16).cover?(k.opcode)
482
+ Bitcoin::Opcodes.opcode_to_small_int(k.opcode)
483
+ end
484
+ return nil unless threshold && threshold.between?(1, keys.size)
485
+ [keys, threshold]
335
486
  end
336
487
 
337
488
  end
@@ -14,6 +14,8 @@ module Bitcoin
14
14
  end
15
15
 
16
16
  def self.parse_from_payload(payload)
17
+ # A fingerprint and zero or more 4 byte indexes, as Bitcoin Core requires.
18
+ raise ArgumentError, 'Invalid length for HD key path.' if payload.bytesize < 4 || (payload.bytesize - 4) % 4 != 0
17
19
  buf = StringIO.new(payload)
18
20
  self.new(fingerprint: buf.read(4).bth, key_paths: buf.read.unpack('V*'))
19
21
  end
@@ -64,6 +64,7 @@ module Bitcoin
64
64
  when PSBT_OUT_TYPES[:tap_bip32_derivation]
65
65
  raise ArgumentError, 'Duplicate Key, key for tap bip32 derivation value already provided.' if output.tap_bip32_derivations[key.bth]
66
66
  raise ArgumentError, 'Size of key was not the expected size for the type tap bip32 derivation' unless key.bytesize == X_ONLY_PUBKEY_SIZE
67
+ PSBT.validate_tap_bip32_derivation!(value)
67
68
  output.tap_bip32_derivations[key.bth] = value.bth
68
69
  else
69
70
  unknown_key = (Bitcoin.pack_var_int(key_type) + key).bth
@@ -94,13 +95,15 @@ module Bitcoin
94
95
  # @return [Bitcoin::PSBT::Output] combined object.
95
96
  def merge(psbo)
96
97
  raise ArgumentError, 'The argument psbt must be an instance of Bitcoin::PSBT::Output.' unless psbo.is_a?(Bitcoin::PSBT::Output)
97
- raise ArgumentError, 'The Partially Signed Output\'s redeem_script are different.' unless redeem_script == psbo.redeem_script
98
- raise ArgumentError, 'The Partially Signed Output\'s witness_script are different.' unless witness_script == psbo.witness_script
99
98
  combined = Bitcoin::PSBT::Output.new
100
- combined.redeem_script = redeem_script
101
- combined.witness_script = witness_script
102
- combined.unknowns = Hash[unknowns.merge(psbo.unknowns).sort]
103
- combined.hd_key_paths = hd_key_paths.merge(psbo.hd_key_paths)
99
+ %i[redeem_script witness_script tap_internal_key tap_tree].each do |name|
100
+ combined.send("#{name}=", PSBT.merge_field(send(name), psbo.send(name), "Output's #{name}"))
101
+ end
102
+ # Maps: the union, with this output's value kept for a key present on both sides.
103
+ combined.hd_key_paths = psbo.hd_key_paths.merge(hd_key_paths)
104
+ combined.tap_bip32_derivations = psbo.tap_bip32_derivations.merge(tap_bip32_derivations)
105
+ combined.proprietaries = PSBT.merge_proprietaries(proprietaries, psbo.proprietaries)
106
+ combined.unknowns = Hash[psbo.unknowns.merge(unknowns).sort]
104
107
  combined
105
108
  end
106
109
 
@@ -11,9 +11,12 @@ module Bitcoin
11
11
  # @param [String] value value with binary format.
12
12
  def initialize(key, value)
13
13
  buf = StringIO.new(key)
14
- id_len = Bitcoin.unpack_var_int_from_io(buf)
15
- @identifier = buf.read(id_len)
16
- @sub_type = Bitcoin.unpack_var_int_from_io(buf)
14
+ begin
15
+ @identifier = Bitcoin.read_exact(buf, Bitcoin.unpack_var_int_from_io(buf))
16
+ @sub_type = Bitcoin.unpack_var_int_from_io(buf)
17
+ rescue ArgumentError
18
+ raise ArgumentError, 'Invalid proprietary key.'
19
+ end
17
20
  @value = value
18
21
  end
19
22
 
@@ -49,7 +49,7 @@ module Bitcoin
49
49
  # @param [String] base64 a Partially Signed Bitcoin Transaction data with Base64 format.
50
50
  # @return [Bitcoin::PartiallySignedTx]
51
51
  def self.parse_from_base64(base64)
52
- self.parse_from_payload(Base64.decode64(base64))
52
+ self.parse_from_payload(Base64.strict_decode64(base64))
53
53
  end
54
54
 
55
55
  # parse Partially Signed Bitcoin Transaction data.
@@ -130,6 +130,7 @@ module Bitcoin
130
130
  end
131
131
 
132
132
  raise ArgumentError, 'Outputs provided does not match the number of outputs in transaction.' unless partial_tx.outputs.size == partial_tx.tx.out.size
133
+ raise ArgumentError, 'Extra data after PSBT.' unless buf.eof?
133
134
 
134
135
  partial_tx
135
136
  end
@@ -257,15 +258,19 @@ module Bitcoin
257
258
  combined.outputs[index] = o.merge(psbt.outputs[index])
258
259
  end
259
260
 
260
- combined.unknowns = Hash[unknowns.merge(psbt.unknowns).sort]
261
+ combined.xpubs = (xpubs + psbt.xpubs).uniq { |x| x.xpub.to_payload }
262
+ combined.version_number = PSBT.merge_field(version_number, psbt.version_number, "Tx's version_number")
263
+ combined.proprietaries = PSBT.merge_proprietaries(proprietaries, psbt.proprietaries)
264
+ combined.unknowns = Hash[psbt.unknowns.merge(unknowns).sort]
261
265
  combined
262
266
  end
263
267
 
264
268
  # finalize tx.
265
- # TODO This feature is experimental and support only multisig.
269
+ # Finalize every input. See Bitcoin::PSBT::Input#finalize! for the supported script types.
266
270
  # @return [Bitcoin::PSBT::Tx] finalized PSBT.
271
+ # @raise [ArgumentError] If an input cannot be finalized.
267
272
  def finalize!
268
- inputs.each {|input|input.finalize!}
273
+ inputs.each_with_index { |input, i| input.finalize!(tx.in[i].out_point.index) }
269
274
  self
270
275
  end
271
276
 
data/lib/bitcoin/psbt.rb CHANGED
@@ -54,6 +54,52 @@ module Bitcoin
54
54
 
55
55
  module_function
56
56
 
57
+ # Combine one field of two PSBT maps the way a BIP-174 combiner does: a value present on one side only is
58
+ # taken, equal values are kept, and different values are a conflict.
59
+ # @param [Object] a The value of one map, or nil.
60
+ # @param [Object] b The value of the other map, or nil.
61
+ # @param [String] name The field, for the error message, e.g. "Input's non_witness_utxo".
62
+ # @return [Object] the combined value.
63
+ # @raise [ArgumentError] If both are present and differ.
64
+ def self.merge_field(a, b, name)
65
+ return b if a.nil?
66
+ return a if b.nil?
67
+ raise ArgumentError, "The Partially Signed #{name} are different." unless a == b
68
+ a
69
+ end
70
+
71
+ # Parse the public key of a PSBT record, which is 33 bytes compressed or 65 bytes uncompressed.
72
+ # @param [String] hex The public key with hex format.
73
+ # @return [Bitcoin::Key]
74
+ # @raise [ArgumentError] If +hex+ is not a valid public key.
75
+ def self.parse_pubkey(hex)
76
+ unless hex.is_a?(String) && hex.valid_hex? && [Bitcoin::Key::COMPRESSED_PUBLIC_KEY_SIZE, Bitcoin::Key::PUBLIC_KEY_SIZE].include?(hex.size / 2)
77
+ raise ArgumentError, Errors::Messages::INVALID_PUBLIC_KEY
78
+ end
79
+ key_type = hex.size == Bitcoin::Key::PUBLIC_KEY_SIZE * 2 ? Bitcoin::Key::TYPES[:uncompressed] : Bitcoin::Key::TYPES[:compressed]
80
+ key = Bitcoin::Key.new(pubkey: hex, key_type: key_type)
81
+ raise ArgumentError, Errors::Messages::INVALID_PUBLIC_KEY unless key.fully_valid_pubkey?
82
+ key
83
+ end
84
+
85
+ # Combine two lists of proprietary records, keeping the first record for a key which appears in both.
86
+ # @return [Array[Bitcoin::PSBT::Proprietary]]
87
+ def self.merge_proprietaries(a, b)
88
+ (a + b).uniq(&:key)
89
+ end
90
+
91
+ # Check the value of a tap bip32 derivation record (BIP-371): a compact size count of leaf hashes,
92
+ # the hashes, then a fingerprint and the 4 byte indexes of the path.
93
+ # @param [String] value the value with binary format.
94
+ # @raise [ArgumentError]
95
+ def self.validate_tap_bip32_derivation!(value)
96
+ buf = StringIO.new(value)
97
+ count = Bitcoin.unpack_var_int_from_io(buf)
98
+ raise ArgumentError, 'Invalid tap bip32 derivation value.' if count.nil? || count * 32 > buf.size - buf.pos
99
+ rest = buf.size - buf.pos - count * 32
100
+ raise ArgumentError, 'Invalid length for HD key path.' if rest < 4 || (rest - 4) % 4 != 0
101
+ end
102
+
57
103
  def self.serialize_to_vector(key_type, key: nil, value: nil)
58
104
  key_len = Bitcoin.pack_var_int(key_type).bytesize
59
105
  key_len += key.bytesize if key
@@ -14,6 +14,15 @@ module Bitcoin
14
14
  # client.listunspent
15
15
  # client.getblockchaininfo
16
16
  #
17
+ # An error answered by Bitcoin Core raises Bitcoin::RPC::Error, which carries the error code:
18
+ #
19
+ # begin
20
+ # client.getblock('00')
21
+ # rescue Bitcoin::RPC::Error => e
22
+ # e.code # => -8
23
+ # e.message # => "blockhash must be of length 64 (not 2, for '00')"
24
+ # end
25
+ #
17
26
  class BitcoinCoreClient
18
27
 
19
28
  attr_reader :config
@@ -37,43 +46,75 @@ module Bitcoin
37
46
  end
38
47
  end
39
48
 
49
+ # Whether a Ruby-like method name such as get_block_hash maps to an RPC command.
50
+ def respond_to_missing?(name, include_private = false)
51
+ (name.to_s.include?('_') && respond_to?(name.to_s.delete('_').to_sym)) || super
52
+ end
53
+
40
54
  private
41
55
 
42
- def server_url
43
- url = "#{config[:schema]}://#{config[:user]}:#{config[:password]}@#{config[:host]}:#{config[:port]}"
56
+ # The URL of the RPC endpoint, without the credentials. These go in the Authorization header,
57
+ # so a password with characters such as '@', '/' or '#' cannot change the host or the path.
58
+ # @return [URI::HTTP]
59
+ def server_uri
60
+ path = '/'
44
61
  if !config[:wallet].nil? && !config[:wallet].empty?
45
- url += "/wallet/#{config[:wallet]}"
62
+ path = "/wallet/#{percent_encode(config[:wallet])}"
46
63
  end
47
- url
64
+ klass = config[:schema].to_s == 'https' ? URI::HTTPS : URI::HTTP
65
+ klass.build(host: config[:host], port: config[:port], path: path)
48
66
  end
49
67
 
68
+ # Percent-encode every character of +str+ which is not unreserved in a URL path.
69
+ def percent_encode(str)
70
+ str.to_s.b.gsub(/[^A-Za-z0-9\-._~]/) { |c| format('%%%02X', c.ord) }
71
+ end
72
+
73
+ # Send +command+ to Bitcoin Core and return the result.
74
+ # @raise [Bitcoin::RPC::Error] If Bitcoin Core answers an error, or an answer which is not the JSON-RPC
75
+ # response to this request.
50
76
  def request(command, *params)
77
+ id = SecureRandom.hex(8)
51
78
  data = {
52
79
  :method => command,
53
80
  :params => params,
54
- :id => 'jsonrpc'
81
+ :id => id
55
82
  }
56
- uri = URI.parse(server_url)
83
+ uri = server_uri
57
84
  http = Net::HTTP.new(uri.hostname, uri.port)
58
- http.use_ssl = uri.scheme === "https"
59
- request = Net::HTTP::Post.new(uri.path.empty? ? '/' : uri.path)
60
- request.basic_auth(uri.user, uri.password)
85
+ http.use_ssl = uri.scheme == 'https'
86
+ request = Net::HTTP::Post.new(uri.path)
87
+ request.basic_auth(config[:user].to_s, config[:password].to_s)
61
88
  request.content_type = 'application/json'
62
89
  request.body = data.to_json
63
90
  response = http.request(request)
91
+ http_status = response.code.to_i
64
92
  # Unescape \uXXXX with JSON.parse itself. Doing it beforehand lets a " contained in
65
93
  # a string value close that string and inject arbitrary JSON into the parsed result.
66
- json_data = JSON.parse(response.body)
94
+ begin
95
+ json_data = JSON.parse(response.body.to_s)
96
+ rescue JSON::ParserError
97
+ # Bitcoin Core answers an error as JSON, so this is for example a 401 from the wrong credentials.
98
+ raise Error.new("Bitcoin Core answered HTTP #{http_status} #{response.message} without a JSON-RPC response.", http_status: http_status)
99
+ end
100
+ unless json_data.is_a?(Hash)
101
+ raise Error.new("Bitcoin Core answered something which is not a JSON-RPC response: #{response.body}", http_status: http_status)
102
+ end
103
+ # Bitcoin Core echoes the id of the request, so a different one is the answer to another request.
104
+ if json_data.key?('id') && !json_data['id'].nil? && json_data['id'] != id
105
+ raise Error.new("The id of the JSON-RPC response #{json_data['id'].inspect} is not the id of the request #{id.inspect}.", http_status: http_status)
106
+ end
67
107
  response = convert_floats_to_strings(json_data)
68
- raise response['error'].to_json if response['error']
108
+ raise Error.from_response(response['error'], http_status: http_status) if response['error']
69
109
  response['result']
70
110
  end
71
111
 
72
112
  # Call CLI command on Ruby-like method names.
73
113
  # e.g. generate_to_address, send_to_address, get_wallet_info
74
114
  def method_missing(name, *args)
75
- if name.to_s.include?('_')
76
- send(name.to_s.gsub('_', '').to_sym, args)
115
+ command = name.to_s.delete('_').to_sym
116
+ if name.to_s.include?('_') && respond_to?(command)
117
+ send(command, *args)
77
118
  else
78
119
  super
79
120
  end
@@ -0,0 +1,43 @@
1
+ module Bitcoin
2
+ module RPC
3
+
4
+ # An error answered by Bitcoin Core to an RPC request, or an answer which is not a JSON-RPC response.
5
+ # It is a RuntimeError, as the error raised before it was, so an existing rescue still catches it.
6
+ class Error < RuntimeError
7
+
8
+ # @return [Integer, nil] The JSON-RPC error code answered by Bitcoin Core, such as -8 for an invalid
9
+ # parameter, or nil if the error is not a JSON-RPC error.
10
+ attr_reader :code
11
+ # @return [Integer, nil] The HTTP status of the response.
12
+ attr_reader :http_status
13
+ # @return [Hash, nil] The error object of the JSON-RPC response as it was answered.
14
+ attr_reader :data
15
+
16
+ # @param [String] message The error message.
17
+ # @param [Integer, nil] code The JSON-RPC error code.
18
+ # @param [Integer, nil] http_status The HTTP status of the response.
19
+ # @param [Hash, nil] data The error object of the JSON-RPC response.
20
+ def initialize(message, code: nil, http_status: nil, data: nil)
21
+ super(message)
22
+ @code = code
23
+ @http_status = http_status
24
+ @data = data
25
+ end
26
+
27
+ # Build the error for the error object +error+ of a JSON-RPC response.
28
+ # @param [Hash, Object] error The value of the error field of the response.
29
+ # @param [Integer, nil] http_status The HTTP status of the response.
30
+ # @return [Bitcoin::RPC::Error]
31
+ def self.from_response(error, http_status: nil)
32
+ if error.is_a?(Hash)
33
+ code = error['code']
34
+ code = Integer(code, exception: false) || code if code.is_a?(String)
35
+ new(error['message'].to_s, code: code, http_status: http_status, data: error)
36
+ else
37
+ new(error.to_s, http_status: http_status, data: error)
38
+ end
39
+ end
40
+ end
41
+
42
+ end
43
+ end
data/lib/bitcoin/rpc.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  module Bitcoin
2
2
  module RPC
3
3
  autoload :BitcoinCoreClient, 'bitcoin/rpc/bitcoin_core_client'
4
+ autoload :Error, 'bitcoin/rpc/error'
4
5
  end
5
6
  end