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.
- checksums.yaml +4 -4
- data/bitcoinrb.gemspec +5 -4
- data/lib/bitcoin/bip321_uri.rb +95 -32
- data/lib/bitcoin/bip324/ell_swift_pubkey.rb +3 -1
- data/lib/bitcoin/bip324/fs_chacha20.rb +3 -0
- data/lib/bitcoin/bip324.rb +1 -1
- data/lib/bitcoin/bip85_entropy.rb +79 -29
- data/lib/bitcoin/bit_stream.rb +2 -2
- data/lib/bitcoin/block.rb +46 -7
- data/lib/bitcoin/block_filter.rb +3 -2
- data/lib/bitcoin/block_header.rb +31 -8
- data/lib/bitcoin/bloom_filter.rb +5 -2
- data/lib/bitcoin/chainparams/regtest.yml +0 -1
- data/lib/bitcoin/chainparams/signet.yml +1 -1
- data/lib/bitcoin/constants.rb +13 -3
- data/lib/bitcoin/descriptor/combo.rb +13 -1
- data/lib/bitcoin/descriptor/expression.rb +35 -15
- data/lib/bitcoin/descriptor/multi.rb +9 -1
- data/lib/bitcoin/descriptor/multi_a.rb +4 -0
- data/lib/bitcoin/descriptor/musig.rb +1 -0
- data/lib/bitcoin/descriptor/pk.rb +7 -0
- data/lib/bitcoin/descriptor/pkh.rb +5 -0
- data/lib/bitcoin/descriptor/raw.rb +1 -1
- data/lib/bitcoin/descriptor/sh.rb +1 -0
- data/lib/bitcoin/descriptor/sp.rb +2 -2
- data/lib/bitcoin/descriptor/tr.rb +29 -3
- data/lib/bitcoin/descriptor/wpkh.rb +1 -0
- data/lib/bitcoin/descriptor/wsh.rb +1 -0
- data/lib/bitcoin/descriptor.rb +59 -35
- data/lib/bitcoin/ext/array_ext.rb +3 -0
- data/lib/bitcoin/ext/ecdsa.rb +7 -1
- data/lib/bitcoin/ext_key.rb +10 -0
- data/lib/bitcoin/gcs_filter.rb +21 -6
- data/lib/bitcoin/key.rb +66 -14
- data/lib/bitcoin/key_path.rb +8 -5
- data/lib/bitcoin/message/addr.rb +1 -0
- data/lib/bitcoin/message/addr_v2.rb +1 -0
- data/lib/bitcoin/message/base.rb +8 -1
- data/lib/bitcoin/message/block.rb +4 -8
- data/lib/bitcoin/message/block_transaction_request.rb +11 -8
- data/lib/bitcoin/message/block_transactions.rb +2 -1
- data/lib/bitcoin/message/cf_parser.rb +1 -0
- data/lib/bitcoin/message/cfcheckpt.rb +3 -3
- data/lib/bitcoin/message/cfheaders.rb +5 -4
- data/lib/bitcoin/message/cfilter.rb +3 -3
- data/lib/bitcoin/message/error.rb +3 -1
- data/lib/bitcoin/message/fee_filter.rb +1 -0
- data/lib/bitcoin/message/filter_add.rb +3 -1
- data/lib/bitcoin/message/filter_load.rb +4 -4
- data/lib/bitcoin/message/get_cfcheckpt.rb +1 -0
- data/lib/bitcoin/message/header_and_short_ids.rb +26 -11
- data/lib/bitcoin/message/headers.rb +10 -2
- data/lib/bitcoin/message/headers_parser.rb +4 -2
- data/lib/bitcoin/message/inventories_parser.rb +2 -1
- data/lib/bitcoin/message/inventory.rb +15 -6
- data/lib/bitcoin/message/merkle_block.rb +4 -4
- data/lib/bitcoin/message/network_addr.rb +13 -13
- data/lib/bitcoin/message/ping.rb +1 -0
- data/lib/bitcoin/message/pong.rb +1 -0
- data/lib/bitcoin/message/reject.rb +1 -0
- data/lib/bitcoin/message/send_cmpct.rb +2 -2
- data/lib/bitcoin/message/send_tx_rcncl.rb +1 -1
- data/lib/bitcoin/message/version.rb +23 -8
- data/lib/bitcoin/message.rb +15 -0
- data/lib/bitcoin/message_sign.rb +44 -6
- data/lib/bitcoin/mnemonic.rb +8 -0
- data/lib/bitcoin/out_point.rb +6 -2
- data/lib/bitcoin/partial_tree.rb +41 -28
- data/lib/bitcoin/payment_code.rb +26 -8
- data/lib/bitcoin/psbt/hd_key_path.rb +1 -2
- data/lib/bitcoin/psbt/input.rb +199 -48
- data/lib/bitcoin/psbt/key_origin_info.rb +2 -0
- data/lib/bitcoin/psbt/output.rb +9 -6
- data/lib/bitcoin/psbt/proprietary.rb +6 -3
- data/lib/bitcoin/psbt/tx.rb +9 -4
- data/lib/bitcoin/psbt.rb +46 -0
- data/lib/bitcoin/rpc/bitcoin_core_client.rb +54 -13
- data/lib/bitcoin/rpc/error.rb +43 -0
- data/lib/bitcoin/rpc.rb +1 -0
- data/lib/bitcoin/script/script.rb +114 -33
- data/lib/bitcoin/script/script_interpreter.rb +11 -6
- data/lib/bitcoin/script/tx_checker.rb +7 -2
- data/lib/bitcoin/script_witness.rb +5 -1
- data/lib/bitcoin/secp256k1/ruby.rb +10 -4
- data/lib/bitcoin/silent_payment.rb +6 -3
- data/lib/bitcoin/slip39/share.rb +35 -9
- data/lib/bitcoin/slip39/sss.rb +26 -11
- data/lib/bitcoin/slip39.rb +13 -5
- data/lib/bitcoin/taproot/control_block.rb +6 -1
- data/lib/bitcoin/taproot/custom_depth_builder.rb +12 -0
- data/lib/bitcoin/tx.rb +35 -14
- data/lib/bitcoin/tx_in.rb +2 -2
- data/lib/bitcoin/tx_out.rb +2 -2
- data/lib/bitcoin/util.rb +49 -28
- data/lib/bitcoin/validation.rb +64 -12
- data/lib/bitcoin/version.rb +1 -1
- data/lib/bitcoin/wallet/account.rb +35 -11
- data/lib/bitcoin/wallet/base.rb +40 -15
- data/lib/bitcoin/wallet/utxo.rb +22 -4
- data/lib/bitcoin.rb +16 -4
- metadata +24 -9
data/lib/bitcoin/psbt/input.rb
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
combined.
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
#
|
|
270
|
-
#
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
self.final_script_sig = Bitcoin::Script.
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
294
|
-
self.
|
|
295
|
-
|
|
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
|
-
#
|
|
329
|
-
#
|
|
330
|
-
#
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
data/lib/bitcoin/psbt/output.rb
CHANGED
|
@@ -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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
data/lib/bitcoin/psbt/tx.rb
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
-
#
|
|
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.
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
62
|
+
path = "/wallet/#{percent_encode(config[:wallet])}"
|
|
46
63
|
end
|
|
47
|
-
|
|
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 =>
|
|
81
|
+
:id => id
|
|
55
82
|
}
|
|
56
|
-
uri =
|
|
83
|
+
uri = server_uri
|
|
57
84
|
http = Net::HTTP.new(uri.hostname, uri.port)
|
|
58
|
-
http.use_ssl = uri.scheme
|
|
59
|
-
request = Net::HTTP::Post.new(uri.path
|
|
60
|
-
request.basic_auth(
|
|
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
|
-
|
|
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']
|
|
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
|
-
|
|
76
|
-
|
|
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
|