bitcoinrb 1.12.0 → 1.13.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 (67) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +1 -1
  3. data/bitcoinrb.gemspec +1 -6
  4. data/lib/bitcoin/bip321_uri.rb +3 -6
  5. data/lib/bitcoin/bip324/fs_chacha20.rb +4 -6
  6. data/lib/bitcoin/bip324/fs_chacha_poly1305.rb +20 -17
  7. data/lib/bitcoin/bip324.rb +3 -1
  8. data/lib/bitcoin/descriptor/checksum.rb +6 -2
  9. data/lib/bitcoin/descriptor/expression.rb +2 -2
  10. data/lib/bitcoin/descriptor/sp.rb +219 -0
  11. data/lib/bitcoin/descriptor.rb +20 -0
  12. data/lib/bitcoin/ext_key.rb +11 -7
  13. data/lib/bitcoin/key.rb +1 -1
  14. data/lib/bitcoin/message/cf_parser.rb +2 -2
  15. data/lib/bitcoin/message/fee_filter.rb +2 -2
  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/network_addr.rb +7 -3
  19. data/lib/bitcoin/message/ping.rb +5 -3
  20. data/lib/bitcoin/message/pong.rb +2 -2
  21. data/lib/bitcoin/message/send_cmpct.rb +2 -2
  22. data/lib/bitcoin/message/version.rb +4 -4
  23. data/lib/bitcoin/message.rb +12 -2
  24. data/lib/bitcoin/message_sign.rb +98 -38
  25. data/lib/bitcoin/mnemonic.rb +9 -4
  26. data/lib/bitcoin/psbt/input.rb +51 -33
  27. data/lib/bitcoin/psbt/key_origin_info.rb +2 -2
  28. data/lib/bitcoin/psbt/output.rb +9 -5
  29. data/lib/bitcoin/psbt/tx.rb +16 -10
  30. data/lib/bitcoin/psbt.rb +1 -1
  31. data/lib/bitcoin/rpc.rb +0 -2
  32. data/lib/bitcoin/script/script.rb +14 -4
  33. data/lib/bitcoin/secp256k1/rfc6979.rb +6 -3
  34. data/lib/bitcoin/sighash_generator.rb +2 -2
  35. data/lib/bitcoin/silent_payment/output.rb +46 -0
  36. data/lib/bitcoin/silent_payment.rb +113 -12
  37. data/lib/bitcoin/taproot/control_block.rb +1 -0
  38. data/lib/bitcoin/tx.rb +8 -6
  39. data/lib/bitcoin/tx_out.rb +2 -2
  40. data/lib/bitcoin/util.rb +5 -5
  41. data/lib/bitcoin/version.rb +1 -1
  42. data/lib/bitcoin/wallet/account.rb +2 -2
  43. data/lib/bitcoin/wallet/db.rb +3 -3
  44. data/lib/bitcoin/wallet/master_key.rb +2 -2
  45. data/lib/bitcoin/wallet/utxo.rb +2 -2
  46. data/lib/bitcoin.rb +1 -6
  47. metadata +18 -108
  48. data/exe/bitcoinrb-cli +0 -5
  49. data/exe/bitcoinrbd +0 -46
  50. data/lib/bitcoin/network/connection.rb +0 -73
  51. data/lib/bitcoin/network/message_handler.rb +0 -243
  52. data/lib/bitcoin/network/peer.rb +0 -228
  53. data/lib/bitcoin/network/peer_discovery.rb +0 -42
  54. data/lib/bitcoin/network/pool.rb +0 -135
  55. data/lib/bitcoin/network.rb +0 -13
  56. data/lib/bitcoin/node/cli.rb +0 -122
  57. data/lib/bitcoin/node/configuration.rb +0 -40
  58. data/lib/bitcoin/node/spv.rb +0 -87
  59. data/lib/bitcoin/node.rb +0 -7
  60. data/lib/bitcoin/rpc/http_server.rb +0 -84
  61. data/lib/bitcoin/rpc/request_handler.rb +0 -150
  62. data/lib/bitcoin/store/chain_entry.rb +0 -68
  63. data/lib/bitcoin/store/db/level_db.rb +0 -98
  64. data/lib/bitcoin/store/db.rb +0 -9
  65. data/lib/bitcoin/store/spv_chain.rb +0 -101
  66. data/lib/bitcoin/store/utxo_db.rb +0 -226
  67. data/lib/bitcoin/store.rb +0 -12
@@ -14,15 +14,17 @@ module Bitcoin
14
14
  end
15
15
 
16
16
  def self.parse_from_payload(payload)
17
- new(payload.unpack1('Q'))
17
+ new(payload.unpack1('Q<'))
18
18
  end
19
19
 
20
20
  def to_payload
21
- [nonce].pack('Q')
21
+ nonce ? [nonce].pack('Q<') : ''
22
22
  end
23
23
 
24
+ # Generate pong message as a response. Return nil if the ping has no nonce
25
+ # (sent by a node before BIP-31), since such a ping does not expect a pong.
24
26
  def to_response
25
- Pong.new(nonce)
27
+ Pong.new(nonce) if nonce
26
28
  end
27
29
 
28
30
  end
@@ -14,11 +14,11 @@ module Bitcoin
14
14
  end
15
15
 
16
16
  def self.parse_from_payload(payload)
17
- new(payload.unpack1('Q'))
17
+ new(payload.unpack1('Q<'))
18
18
  end
19
19
 
20
20
  def to_payload
21
- [nonce].pack('Q')
21
+ [nonce].pack('Q<')
22
22
  end
23
23
  end
24
24
 
@@ -22,12 +22,12 @@ module Bitcoin
22
22
  def self.parse_from_payload(payload)
23
23
  buf = StringIO.new(payload)
24
24
  mode = buf.read(1).unpack1('c')
25
- version = buf.read(8).unpack1('Q')
25
+ version = buf.read(8).unpack1('Q<')
26
26
  new(mode, version)
27
27
  end
28
28
 
29
29
  def to_payload
30
- [mode, version].pack('cQ')
30
+ [mode, version].pack('cQ<')
31
31
  end
32
32
 
33
33
  def high?
@@ -32,7 +32,7 @@ module Bitcoin
32
32
  end
33
33
 
34
34
  def self.parse_from_payload(payload)
35
- version, services, timestamp, local_addr, remote_addr, nonce, rest = payload.unpack('VQQa26a26Qa*')
35
+ version, services, timestamp, local_addr, remote_addr, nonce, rest = payload.unpack('VQ<Q<a26a26Q<a*')
36
36
  v = new
37
37
  v.version = version
38
38
  v.services = services
@@ -50,10 +50,10 @@ module Bitcoin
50
50
 
51
51
  def to_payload
52
52
  [
53
- [version, services, timestamp].pack('VQQ'),
53
+ [version, services, timestamp].pack('VQ<Q<'),
54
54
  local_addr.to_payload(true),
55
55
  remote_addr.to_payload(true),
56
- [nonce].pack('Q'),
56
+ [nonce].pack('Q<'),
57
57
  pack_var_string(user_agent),
58
58
  [start_height].pack('V'),
59
59
  pack_boolean(relay)
@@ -61,7 +61,7 @@ module Bitcoin
61
61
  end
62
62
 
63
63
  def unpack_relay_field(payload)
64
- ( version >= 70001 && payload ) ? unpack_boolean(payload) : [ true, nil ]
64
+ ( version >= 70001 && payload && !payload.empty? ) ? unpack_boolean(payload) : [ true, nil ]
65
65
  end
66
66
 
67
67
  # Check whether +service_flag+ support this version.
@@ -1,8 +1,6 @@
1
1
  module Bitcoin
2
2
  module Message
3
3
 
4
- class Error < StandardError; end
5
-
6
4
  autoload :Base, 'bitcoin/message/base'
7
5
  autoload :Inventory, 'bitcoin/message/inventory'
8
6
  autoload :InventoriesParser, 'bitcoin/message/inventories_parser'
@@ -107,6 +105,8 @@ module Bitcoin
107
105
  Pong.parse_from_payload(payload)
108
106
  when GetHeaders::COMMAND
109
107
  GetHeaders.parse_from_payload(payload)
108
+ when GetBlocks::COMMAND
109
+ GetBlocks.parse_from_payload(payload)
110
110
  when Headers::COMMAND
111
111
  Headers.parse_from_payload(payload)
112
112
  when Block::COMMAND
@@ -125,8 +125,18 @@ module Bitcoin
125
125
  Inv.parse_from_payload(payload)
126
126
  when MerkleBlock::COMMAND
127
127
  MerkleBlock.parse_from_payload(payload)
128
+ when FilterLoad::COMMAND
129
+ FilterLoad.parse_from_payload(payload)
130
+ when FilterAdd::COMMAND
131
+ FilterAdd.parse_from_payload(payload)
132
+ when FilterClear::COMMAND
133
+ FilterClear.new
128
134
  when CmpctBlock::COMMAND
129
135
  CmpctBlock.parse_from_payload(payload)
136
+ when GetBlockTxn::COMMAND
137
+ GetBlockTxn.parse_from_payload(payload)
138
+ when BlockTxn::COMMAND
139
+ BlockTxn.parse_from_payload(payload)
130
140
  when GetData::COMMAND
131
141
  GetData.parse_from_payload(payload)
132
142
  when GetCFHeaders::COMMAND
@@ -10,6 +10,22 @@ module Bitcoin
10
10
  FORMAT_SIMPLE = :simple
11
11
  FORMAT_FULL = :full
12
12
 
13
+ # Prefix
14
+ SIGNATURE_PREFIX_SIMPLE = 'smp'
15
+ SIGNATURE_PREFIX_FULL = 'ful'
16
+ SIGNATURE_PREFIX_POF = 'pof'
17
+
18
+ # BIP-322 Required rules
19
+ BIP322_VERIFY_FLAGS = [
20
+ SCRIPT_VERIFY_P2SH, SCRIPT_VERIFY_DERSIG, SCRIPT_VERIFY_STRICTENC,
21
+ SCRIPT_VERIFY_LOW_S, SCRIPT_VERIFY_NULLFAIL, SCRIPT_VERIFY_MINIMALDATA,
22
+ SCRIPT_VERIFY_CLEANSTACK, SCRIPT_VERIFY_MINIMALIF,
23
+ SCRIPT_VERIFY_CONST_SCRIPTCODE,
24
+ SCRIPT_VERIFY_WITNESS, SCRIPT_VERIFY_TAPROOT,
25
+ SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_NOPS,
26
+ SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM
27
+ ].inject(:|)
28
+
13
29
  # Sign a message.
14
30
  # @param [Bitcoin::Key] key Private key to sign.
15
31
  # @param [String] message The message to be signed.
@@ -20,28 +36,38 @@ module Bitcoin
20
36
  def sign_message(key, message, prefix: Bitcoin.chain_params.message_magic, format: FORMAT_LEGACY, address: nil)
21
37
  validate_format!(format)
22
38
  digest = message_hash(message, prefix: prefix, legacy: format == FORMAT_LEGACY)
39
+ prefix_marker = ''
23
40
  sig = case format
24
41
  when FORMAT_LEGACY
25
42
  key.sign_compact(digest)
26
43
  else
27
44
  validate_address!(address)
28
45
  addr = Bitcoin::Script.parse_from_addr(address)
29
- sig_ver, algo = if addr.p2wpkh?
46
+ sig_ver, algo = if addr.p2wpkh? || addr.p2wsh?
30
47
  [:witness_v0, :ecdsa]
31
48
  elsif addr.p2tr?
32
49
  [:taproot, :schnorr]
33
50
  else
34
- raise ArgumentError "#{address} dose not supported."
51
+
35
52
  end
36
53
  tx = to_sign_tx(digest, address)
37
- prev_out = Bitcoin::TxOut.new(script_pubkey: addr)
38
- sighash = tx.sighash_for_input(0, addr, sig_version: sig_ver, amount: 0, prevouts: [prev_out])
39
- sig = key.sign(sighash, algo: algo) + [Bitcoin::SIGHASH_TYPE[:all]].pack('C')
40
- tx.in[0].script_witness.stack << sig
41
- tx.in[0].script_witness.stack << key.pubkey.htb
54
+ prev_out = Bitcoin::TxOut.new(script_pubkey: addr, value: 0)
55
+ if addr.p2tr?
56
+ sighash = tx.sighash_for_input(0, addr, sig_version: :taproot, prevouts: [prev_out], hash_type: Bitcoin::SIGHASH_TYPE[:default])
57
+ tweaked = Bitcoin::Taproot.tweak_private_key(key, '')
58
+ tx.in[0].script_witness.stack << tweaked.sign(sighash, algo: :schnorr)
59
+ elsif addr.p2wpkh? || addr.p2wsh?
60
+ sighash = tx.sighash_for_input(0, addr, sig_version: :witness_v0, amount: 0, prevouts: [prev_out])
61
+ ecdsa = key.sign(sighash, algo: :ecdsa) + [Bitcoin::SIGHASH_TYPE[:all]].pack('C')
62
+ tx.in[0].script_witness.stack << ecdsa
63
+ tx.in[0].script_witness.stack << key.pubkey.htb
64
+ else
65
+ raise ArgumentError, "#{address} dose not supported."
66
+ end
67
+ prefix_marker = format == FORMAT_SIMPLE ? SIGNATURE_PREFIX_SIMPLE : SIGNATURE_PREFIX_FULL
42
68
  format == FORMAT_SIMPLE ? tx.in[0].script_witness.to_payload : tx.to_payload
43
69
  end
44
- Base64.strict_encode64(sig)
70
+ prefix_marker + Base64.strict_encode64(sig)
45
71
  end
46
72
 
47
73
  # Verify a signed message.
@@ -51,41 +77,41 @@ module Bitcoin
51
77
  # @return [Boolean] Verification result.
52
78
  def verify_message(address, signature, message, prefix: Bitcoin.chain_params.message_magic)
53
79
  addr_script = Bitcoin::Script.parse_from_addr(address)
80
+ variant, body = case signature
81
+ when ''
82
+ raise ArgumentError, 'signature too short'
83
+ when /\A#{SIGNATURE_PREFIX_SIMPLE}/
84
+ [:simple, signature[3..]]
85
+ when /\A#{SIGNATURE_PREFIX_FULL}/
86
+ [:full, signature[3..]]
87
+ when /\A#{SIGNATURE_PREFIX_POF}/
88
+ [:pof, signature[3..]]
89
+ else
90
+ [:fallback, signature]
91
+ end
54
92
  begin
55
- sig = Base64.strict_decode64(signature)
93
+ payload = Base64.strict_decode64(body)
56
94
  rescue ArgumentError
57
95
  raise ArgumentError, 'Invalid signature'
58
96
  end
59
- if addr_script.p2pkh?
60
- begin
61
- # Legacy verification
62
- pubkey = Bitcoin::Key.recover_compact(message_hash(message, prefix: prefix, legacy: true), sig)
63
- return false unless pubkey
64
- pubkey.to_p2pkh == address
65
- rescue Exception
66
- false
67
- end
68
- elsif addr_script.witness_program?
69
- # BIP322 verification
70
- digest = message_hash(message, prefix: prefix, legacy: false)
97
+
98
+ case variant
99
+ when :simple, :fallback
100
+ return verify_legacy(address, payload, message, prefix: prefix) if addr_script.p2pkh?
101
+ raise ArgumentError, 'simple format not supported for this address' unless
102
+ addr_script.p2wpkh? || addr_script.p2wsh? || addr_script.p2tr?
103
+ verify_simple(addr_script, payload, message, prefix: prefix)
104
+ when :full
71
105
  begin
72
- # Full
73
- tx = Bitcoin::Tx.parse_from_payload(sig)
74
- validate_to_sign_tx!(tx)
75
- to_spend = to_spend_tx(digest, address)
76
- return false unless tx.in[0].out_point.tx_hash == to_spend.tx_hash
77
- rescue Exception
78
- # Simple
79
- tx = to_sign_tx(digest, address)
80
- tx.in[0].script_witness = Bitcoin::ScriptWitness.parse_from_payload(sig)
106
+ tx = Bitcoin::Tx.parse_from_payload(payload)
107
+ rescue StandardError
108
+ raise ArgumentError, 'error parsing signature as full variant'
81
109
  end
82
- script_pubkey = Bitcoin::Script.parse_from_addr(address)
83
- tx_out = Bitcoin::TxOut.new(script_pubkey: script_pubkey)
84
- flags = Bitcoin::STANDARD_SCRIPT_VERIFY_FLAGS
85
- interpreter = Bitcoin::ScriptInterpreter.new(flags: flags, checker: Bitcoin::TxChecker.new(tx: tx, input_index: 0, prevouts: [tx_out]))
86
- interpreter.verify_script(Bitcoin::Script.new, script_pubkey, tx.in[0].script_witness)
110
+ verify_full(addr_script, tx, message, prefix: prefix)
111
+ when :pof
112
+ raise NotImplementedError, 'proof of funds variant is not supported'
87
113
  else
88
- raise ArgumentError, "This address unsupported."
114
+ raise ArgumentError, "unknown signature variant: #{variant.inspect}"
89
115
  end
90
116
  end
91
117
 
@@ -99,8 +125,7 @@ module Bitcoin
99
125
  end
100
126
 
101
127
  def validate_address!(address)
102
- script = Bitcoin::Script.parse_from_addr(address)
103
- raise ArgumentError, 'This address unsupported' if script.p2sh? || script.p2wsh?
128
+ Bitcoin::Script.parse_from_addr(address)
104
129
  end
105
130
 
106
131
  def validate_format!(format)
@@ -110,6 +135,7 @@ module Bitcoin
110
135
  end
111
136
 
112
137
  def validate_to_sign_tx!(tx)
138
+ raise ArgumentError, "Invalid version." unless [0, 2].include?(tx.version)
113
139
  raise ArgumentError, "Multiple inputs (proof of funds) are not supported." unless tx.in.length == 1
114
140
  raise ArgumentError, "vin[0].prevout.n must be 0." unless tx.in[0].out_point.index == 0
115
141
  raise ArgumentError, "Multiple outputs are not supported." unless tx.out.length == 1
@@ -140,8 +166,42 @@ module Bitcoin
140
166
  tx
141
167
  end
142
168
 
169
+ def verify_simple(addr_script, witness_payload, message, prefix:)
170
+ digest = message_hash(message, prefix: prefix, legacy: false)
171
+ tx = to_sign_tx(digest, addr_script.to_addr)
172
+ tx.in[0].script_witness = Bitcoin::ScriptWitness.parse_from_payload(witness_payload)
173
+ run_interpreter(tx, addr_script)
174
+ end
175
+
176
+ def verify_full(addr_script, tx, message, prefix:)
177
+ digest = message_hash(message, prefix: prefix, legacy: false)
178
+ to_spend = to_spend_tx(digest, addr_script.to_addr)
179
+ validate_to_sign_tx!(tx)
180
+ return false unless tx.in[0].out_point.tx_hash == to_spend.tx_hash
181
+ run_interpreter(tx, addr_script)
182
+ end
183
+
184
+ def run_interpreter(tx, script_pubkey)
185
+ tx_out = Bitcoin::TxOut.new(script_pubkey: script_pubkey)
186
+ checker = Bitcoin::TxChecker.new(tx: tx, input_index: 0, prevouts: [tx_out])
187
+ interpreter = Bitcoin::ScriptInterpreter.new(flags: BIP322_VERIFY_FLAGS, checker: checker)
188
+ interpreter.verify_script(tx.in[0].script_sig, script_pubkey, tx.in[0].script_witness)
189
+ end
190
+
191
+ def verify_legacy(address, sig_bytes, message, prefix:)
192
+ pubkey = Bitcoin::Key.recover_compact(message_hash(message, prefix: prefix, legacy: true), sig_bytes)
193
+ return false unless pubkey
194
+ pubkey.to_p2pkh == address
195
+ rescue StandardError
196
+ false
197
+ end
198
+
143
199
  private_class_method :validate_address!
144
200
  private_class_method :validate_format!
145
201
  private_class_method :validate_to_sign_tx!
202
+ private_class_method :run_interpreter
203
+ private_class_method :verify_simple
204
+ private_class_method :verify_full
205
+ private_class_method :verify_legacy
146
206
  end
147
207
  end
@@ -24,7 +24,7 @@ module Bitcoin
24
24
  def to_entropy(words)
25
25
  word_master = load_words
26
26
  mnemonic = words.map do |w|
27
- index = word_master.index(w.downcase)
27
+ index = word_master.index(normalize(w).downcase)
28
28
  raise IndexError, 'word not found in words list.' unless index
29
29
  index.to_s(2).rjust(11, '0')
30
30
  end.join
@@ -53,8 +53,8 @@ module Bitcoin
53
53
  # @return [String] seed
54
54
  def to_seed(mnemonic, passphrase: '')
55
55
  to_entropy(mnemonic)
56
- OpenSSL::PKCS5.pbkdf2_hmac(mnemonic.join(' ').downcase,
57
- 'mnemonic' + passphrase, 2048, 64, OpenSSL::Digest::SHA512.new).bth
56
+ OpenSSL::PKCS5.pbkdf2_hmac(normalize(mnemonic.join(' ')).downcase,
57
+ 'mnemonic' + normalize(passphrase), 2048, 64, OpenSSL::Digest::SHA512.new).bth
58
58
  end
59
59
 
60
60
  # calculate entropy checksum
@@ -69,7 +69,12 @@ module Bitcoin
69
69
 
70
70
  # load word list contents
71
71
  def load_words
72
- File.readlines("#{WORD_DIR}/#{language}.txt").map(&:strip)
72
+ File.readlines("#{WORD_DIR}/#{language}.txt", encoding: Encoding::UTF_8).map{|w| normalize(w.strip)}
73
+ end
74
+
75
+ # BIP-39 requires that mnemonic sentences and passphrases are normalized using NFKD.
76
+ def normalize(str)
77
+ str.unicode_normalize(:nfkd)
73
78
  end
74
79
 
75
80
  end
@@ -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
@@ -191,10 +195,13 @@ module Bitcoin
191
195
  # Check whether input's scriptPubkey is correct witness.
192
196
  # @return [Boolean]
193
197
  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
198
+ return false unless witness_utxo
199
+ return true if witness_utxo.script_pubkey.p2wpkh? # P2WPKH
200
+ # P2WSH. The scriptPubkey commits to the witness script directly, so a redeem script is not used.
201
+ return true if witness_utxo.script_pubkey.p2wsh? && witness_script &&
202
+ Bitcoin::Script.to_p2wsh(witness_script) == witness_utxo.script_pubkey
196
203
  # segwit nested in P2SH
197
- if witness_utxo&.script_pubkey.p2sh? && redeem_script&.witness_program? && redeem_script.to_p2sh == witness_utxo&.script_pubkey
204
+ if witness_utxo.script_pubkey.p2sh? && redeem_script&.witness_program? && redeem_script.to_p2sh == witness_utxo.script_pubkey
198
205
  return true if redeem_script.p2wpkh?# nested p2wpkh
199
206
  return true if witness_script&.to_sha256 == redeem_script.witness_data[1].bth # nested p2wsh
200
207
  end
@@ -235,19 +242,19 @@ module Bitcoin
235
242
  # @return [Bitcoin::PSBT::Input] combined object.
236
243
  def merge(psbi)
237
244
  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
245
+ raise ArgumentError, 'The Partially Signed Input\'s non_witness_utxo are different.' unless same_field?(non_witness_utxo, psbi.non_witness_utxo)
246
+ raise ArgumentError, 'The Partially Signed Input\'s witness_utxo are different.' unless same_field?(witness_utxo, psbi.witness_utxo)
240
247
  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
248
+ raise ArgumentError, 'The Partially Signed Input\'s redeem_script are different.' unless same_field?(redeem_script, psbi.redeem_script)
249
+ raise ArgumentError, 'The Partially Signed Input\'s witness_script are different.' unless same_field?(witness_script, psbi.witness_script)
243
250
  combined = Bitcoin::PSBT::Input.new(non_witness_utxo: non_witness_utxo, witness_utxo: witness_utxo)
244
251
  combined.unknowns = Hash[unknowns.merge(psbi.unknowns).sort]
245
252
  combined.redeem_script = redeem_script
246
253
  combined.witness_script = witness_script
247
254
  combined.sighash_type = sighash_type
248
255
  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?
256
+ redeem_script.get_multisig_pubkeys.each{|pubkey|combined.partial_sigs[pubkey.bth] = sigs[pubkey.bth] if sigs[pubkey.bth]} if redeem_script&.multisig?
257
+ witness_script.get_multisig_pubkeys.each{|pubkey|combined.partial_sigs[pubkey.bth] = sigs[pubkey.bth] if sigs[pubkey.bth]} if witness_script&.multisig?
251
258
  combined.hd_key_paths = hd_key_paths.merge(psbi.hd_key_paths)
252
259
  combined
253
260
  end
@@ -288,7 +295,7 @@ module Bitcoin
288
295
  h[:non_witness_utxo] = non_witness_utxo.to_h if non_witness_utxo
289
296
  h[:witness_utxo] = witness_utxo.to_h if witness_utxo
290
297
  h[:redeem_script] = redeem_script.to_h if redeem_script
291
- h[:witness_script] = witness_script.to_h if redeem_script
298
+ h[:witness_script] = witness_script.to_h if witness_script
292
299
  h[:final_script_sig] = final_script_sig.to_h if final_script_sig
293
300
  h[:final_script_witness] = final_script_witness.to_h if final_script_witness
294
301
  h[:bip32_derivs] = hd_key_paths.values.map(&:to_h) unless hd_key_paths.empty?
@@ -309,6 +316,17 @@ module Bitcoin
309
316
  h
310
317
  end
311
318
 
319
+ private
320
+
321
+ # Whether +a+ and +b+ are the same field value. Unlike ==, this does not raise
322
+ # NoMethodError when only one of them is nil.
323
+ # @return [Boolean]
324
+ def same_field?(a, b)
325
+ return true if a.nil? && b.nil?
326
+ return false if a.nil? || b.nil?
327
+ a == b
328
+ end
329
+
312
330
  end
313
331
 
314
332
  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,21 @@ 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
275
280
  # validate signature
276
- tx.in.each_with_index do |tx_in, index|
281
+ extract_tx.in.each_with_index do |tx_in, index|
277
282
  input = inputs[index]
278
283
  if input.non_witness_utxo
279
284
  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)
285
+ raise "input[#{index}]'s signature is invalid.'" unless extract_tx.verify_input_sig(index, utxo.script_pubkey)
281
286
  else
282
287
  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
+ raise ArgumentError, "input[#{index}] does not have utxo." unless utxo
289
+ raise "input[#{index}]'s signature is invalid.'" unless extract_tx.verify_input_sig(index, utxo.script_pubkey, amount: utxo.value)
284
290
  end
285
291
  end
286
292
  extract_tx