bitcoinrb 1.14.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 +76 -17
  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 +67 -15
  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 +42 -12
  87. data/lib/bitcoin/slip39/sss.rb +33 -12
  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
@@ -106,46 +106,46 @@ module Bitcoin
106
106
  buf = payload.is_a?(String) ? StringIO.new(payload) : payload
107
107
  has_time = buf.size > 26
108
108
  addr = NetworkAddr.new(time: nil)
109
- addr.time = buf.read(4).unpack1('V') if has_time
110
- addr.services = buf.read(8).unpack1('Q<')
111
- addr.addr = IPAddr::new_ntoh(buf.read(16))
112
- addr.port = buf.read(2).unpack1('n')
109
+ addr.time = Bitcoin.read_exact(buf, 4).unpack1('V') if has_time
110
+ addr.services = Bitcoin.read_exact(buf, 8).unpack1('Q<')
111
+ addr.addr = IPAddr::new_ntoh(Bitcoin.read_exact(buf, 16))
112
+ addr.port = Bitcoin.read_exact(buf, 2).unpack1('n')
113
113
  addr
114
114
  end
115
115
 
116
116
  # Load addr payload with addr v2 format.
117
117
  def self.load_addr_v2_payload(payload)
118
118
  buf = payload.is_a?(String) ? StringIO.new(payload) : payload
119
- addr = NetworkAddr.new(time: buf.read(4).unpack1('V'))
119
+ addr = NetworkAddr.new(time: Bitcoin.read_exact(buf, 4).unpack1('V'))
120
120
  addr.services = Bitcoin.unpack_var_int_from_io(buf)
121
- addr.net = buf.read(1).unpack1('C')
121
+ addr.net = Bitcoin.read_exact(buf, 1).unpack1('C')
122
122
  raise Bitcoin::Message::Error, "Unknown network id: #{addr.net}" unless NETWORK_ID.value?(addr.net)
123
123
  addr_len = Bitcoin.unpack_var_int_from_io(buf)
124
124
  addr.addr = case addr.net
125
125
  when NETWORK_ID[:ipv4]
126
126
  raise Bitcoin::Message::Error, "Invalid IPv4 address." unless addr_len == 4
127
- IPAddr::new_ntoh(buf.read(addr_len))
127
+ IPAddr::new_ntoh(Bitcoin.read_exact(buf, addr_len))
128
128
  when NETWORK_ID[:ipv6]
129
129
  raise Bitcoin::Message::Error, "Invalid IPv6 address." unless addr_len == 16
130
- a = IPAddr::new_ntoh(buf.read(addr_len))
130
+ a = IPAddr::new_ntoh(Bitcoin.read_exact(buf, addr_len))
131
131
  raise Bitcoin::Message::Error, "Invalid IPv6 address." if a.ipv4_mapped?
132
132
  a
133
133
  when NETWORK_ID[:tor_v2]
134
134
  raise Bitcoin::Message::Error, "Invalid Tor v2 address." unless addr_len == 10
135
- buf.read(addr_len).bth
135
+ Bitcoin.read_exact(buf, addr_len).bth
136
136
  when NETWORK_ID[:tor_v3]
137
137
  raise Bitcoin::Message::Error, "Invalid Tor v3 address." unless addr_len == 32
138
- buf.read(addr_len).bth
138
+ Bitcoin.read_exact(buf, addr_len).bth
139
139
  when NETWORK_ID[:i2p]
140
140
  raise Bitcoin::Message::Error, "Invalid I2P address." unless addr_len == 32
141
- buf.read(addr_len).bth
141
+ Bitcoin.read_exact(buf, addr_len).bth
142
142
  when NETWORK_ID[:cjdns]
143
143
  raise Bitcoin::Message::Error, "Invalid CJDNS address." unless addr_len == 16
144
- a = IPAddr::new_ntoh(buf.read(addr_len))
144
+ a = IPAddr::new_ntoh(Bitcoin.read_exact(buf, addr_len))
145
145
  raise Bitcoin::Message::Error, "Invalid CJDNS address." unless a.to_s.start_with?('fc00:')
146
146
  a
147
147
  end
148
- addr.port = buf.read(2).unpack1('n')
148
+ addr.port = Bitcoin.read_exact(buf, 2).unpack1('n')
149
149
  addr
150
150
  end
151
151
 
@@ -14,6 +14,7 @@ module Bitcoin
14
14
  end
15
15
 
16
16
  def self.parse_from_payload(payload)
17
+ raise Bitcoin::Message::Error, 'Payload must be empty or 8 bytes.' unless payload.empty? || payload.bytesize == 8
17
18
  new(payload.unpack1('Q<'))
18
19
  end
19
20
 
@@ -14,6 +14,7 @@ module Bitcoin
14
14
  end
15
15
 
16
16
  def self.parse_from_payload(payload)
17
+ raise Bitcoin::Message::Error, 'Payload must be 8 bytes.' unless payload.bytesize == 8
17
18
  new(payload.unpack1('Q<'))
18
19
  end
19
20
 
@@ -31,6 +31,7 @@ module Bitcoin
31
31
  def self.parse_from_payload(payload)
32
32
  message, payload = Bitcoin.unpack_var_string(payload)
33
33
  code, payload = payload.unpack('Ca*')
34
+ raise Bitcoin::Message::Error, 'Reject payload is too short.' if code.nil?
34
35
  reason, payload = Bitcoin.unpack_var_string(payload)
35
36
  extra = ['tx', 'block'].include?(message) ? payload.bth : payload
36
37
  new(message, code, reason, extra)
@@ -21,8 +21,8 @@ module Bitcoin
21
21
 
22
22
  def self.parse_from_payload(payload)
23
23
  buf = StringIO.new(payload)
24
- mode = buf.read(1).unpack1('c')
25
- version = buf.read(8).unpack1('Q<')
24
+ mode = Bitcoin.read_exact(buf, 1).unpack1('c')
25
+ version = Bitcoin.read_exact(buf, 8).unpack1('Q<')
26
26
  new(mode, version)
27
27
  end
28
28
 
@@ -23,7 +23,7 @@ module Bitcoin
23
23
 
24
24
  def self.parse_from_payload(payload)
25
25
  buf = StringIO.new(payload)
26
- version, salt = buf.read(16).unpack('VQ<')
26
+ version, salt = Bitcoin.read_exact(buf, 12).unpack('VQ<')
27
27
  SendTxRcncl.new(version, salt)
28
28
  end
29
29
 
@@ -31,19 +31,34 @@ module Bitcoin
31
31
  @relay = opts[:relay] || false
32
32
  end
33
33
 
34
+ # The part every version message has: version, services, timestamp and the receiving address.
35
+ MIN_PAYLOAD_SIZE = 4 + 8 + 8 + 26
36
+
37
+ # Parse a version message. As Bitcoin Core does, the sending address and nonce, the user agent, the
38
+ # start height and the relay flag are each read only when the payload still has data, so a message
39
+ # from an old protocol version which stops early is accepted with the remaining fields empty.
34
40
  def self.parse_from_payload(payload)
35
- version, services, timestamp, local_addr, remote_addr, nonce, rest = payload.unpack('VQ<Q<a26a26Q<a*')
36
- v = new
41
+ raise Bitcoin::Message::Error, 'Version payload is too short.' if payload.bytesize < MIN_PAYLOAD_SIZE
42
+ version, services, timestamp, local_addr, rest = payload.unpack('VQ<Q<a26a*')
43
+ v = new(remote_addr: NetworkAddr.new(time: nil), nonce: 0, user_agent: '', start_height: 0)
37
44
  v.version = version
38
45
  v.services = services
39
46
  v.timestamp = timestamp
40
47
  v.local_addr = NetworkAddr.parse_from_payload(local_addr)
41
- v.remote_addr = NetworkAddr.parse_from_payload(remote_addr)
42
- v.nonce = nonce
43
- user_agent, rest = unpack_var_string(rest)
44
- start_height, rest = rest.unpack('Va*')
45
- v.user_agent = user_agent
46
- v.start_height = start_height
48
+ unless rest.empty?
49
+ raise Bitcoin::Message::Error, 'Version payload is truncated.' if rest.bytesize < 26 + 8
50
+ remote_addr, nonce, rest = rest.unpack('a26Q<a*')
51
+ v.remote_addr = NetworkAddr.parse_from_payload(remote_addr)
52
+ v.nonce = nonce
53
+ end
54
+ unless rest.empty?
55
+ v.user_agent, rest = unpack_var_string(rest)
56
+ raise Bitcoin::Message::Error, "user agent size #{v.user_agent.bytesize} exceeds #{MAX_SUBVERSION_LENGTH}." if v.user_agent.bytesize > MAX_SUBVERSION_LENGTH
57
+ end
58
+ unless rest.empty?
59
+ raise Bitcoin::Message::Error, 'Version payload is truncated.' if rest.bytesize < 4
60
+ v.start_height, rest = rest.unpack('Va*')
61
+ end
47
62
  v.relay = v.unpack_relay_field(rest).first
48
63
  v
49
64
  end
@@ -1,6 +1,21 @@
1
1
  module Bitcoin
2
2
  module Message
3
3
 
4
+ # The most addresses an addr or addrv2 message may carry (MAX_ADDR_TO_SEND in Bitcoin Core).
5
+ MAX_ADDR_TO_SEND = 1000
6
+ # The most inventories an inv, getdata or notfound message may carry (MAX_INV_SZ).
7
+ MAX_INV_SZ = 50_000
8
+ # The most hashes a block locator may carry (MAX_LOCATOR_SZ).
9
+ MAX_LOCATOR_SZ = 101
10
+ # The longest user agent of a version message (MAX_SUBVERSION_LENGTH).
11
+ MAX_SUBVERSION_LENGTH = 256
12
+ # The most filter headers a cfheaders message may carry (MAX_GETCFHEADERS_SIZE).
13
+ MAX_CFHEADERS_RESULTS = 2000
14
+ # The most transactions a compact block or its blocktxn may refer to. No block holds more, since a
15
+ # transaction serializes to at least MIN_SERIALIZABLE_TRANSACTION_WEIGHT.
16
+ MAX_COMPACT_BLOCK_TX_COUNT = MAX_BLOCK_WEIGHT / MIN_SERIALIZABLE_TRANSACTION_WEIGHT
17
+
18
+
4
19
  autoload :Base, 'bitcoin/message/base'
5
20
  autoload :Inventory, 'bitcoin/message/inventory'
6
21
  autoload :InventoriesParser, 'bitcoin/message/inventories_parser'
@@ -4,6 +4,22 @@ module Bitcoin
4
4
 
5
5
  class Error < StandardError; end
6
6
 
7
+ # A checker which enforces the BIP-322 rule that all signatures use SIGHASH_ALL, or SIGHASH_DEFAULT
8
+ # where the output type supports it. A signature with any other hash type fails, as if it did not verify.
9
+ class BIP322Checker < Bitcoin::TxChecker
10
+
11
+ def check_sig(sig, pubkey, script_code, sig_version, allow_hybrid: false)
12
+ return false unless sig.empty? || sig.htb[-1].unpack1('C') == SIGHASH_TYPE[:all]
13
+ super
14
+ end
15
+
16
+ def check_schnorr_sig(sig, pubkey, sig_version, opts = {})
17
+ bytes = sig.htb
18
+ return set_error(SCRIPT_ERR_SCHNORR_SIG_HASHTYPE) if bytes.bytesize == 65 && bytes[-1].unpack1('C') != SIGHASH_TYPE[:all]
19
+ super
20
+ end
21
+ end
22
+
7
23
  module_function
8
24
 
9
25
  FORMAT_LEGACY = :legacy
@@ -15,7 +31,9 @@ module Bitcoin
15
31
  SIGNATURE_PREFIX_FULL = 'ful'
16
32
  SIGNATURE_PREFIX_POF = 'pof'
17
33
 
18
- # BIP-322 Required rules
34
+ # BIP-322 required rules, and its upgradeable rules: a proof which relies on a reserved NOP, a witness
35
+ # version above 1, an unknown taproot leaf version, an OP_SUCCESSx or an unknown public key type is not
36
+ # valid, since such a script succeeds without any signature.
19
37
  BIP322_VERIFY_FLAGS = [
20
38
  SCRIPT_VERIFY_P2SH, SCRIPT_VERIFY_DERSIG, SCRIPT_VERIFY_STRICTENC,
21
39
  SCRIPT_VERIFY_LOW_S, SCRIPT_VERIFY_NULLFAIL, SCRIPT_VERIFY_MINIMALDATA,
@@ -23,7 +41,10 @@ module Bitcoin
23
41
  SCRIPT_VERIFY_CONST_SCRIPTCODE,
24
42
  SCRIPT_VERIFY_WITNESS, SCRIPT_VERIFY_TAPROOT,
25
43
  SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_NOPS,
26
- SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM
44
+ SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM,
45
+ SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_TAPROOT_VERSION,
46
+ SCRIPT_VERIFY_DISCOURAGE_OP_SUCCESS,
47
+ SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_PUBKEYTYPE
27
48
  ].inject(:|)
28
49
 
29
50
  # Sign a message.
@@ -33,6 +54,7 @@ module Bitcoin
33
54
  # @param [String] format Format of signature data. Default is +FORMAT_LEGACY+.
34
55
  # @param [String] prefix (Optional) Prefix used in legacy format.
35
56
  # @return [String] Signature, base64 encoded.
57
+ # @raise [ArgumentError] If +address+ is not supported, or +key+ does not correspond to it.
36
58
  def sign_message(key, message, prefix: Bitcoin.chain_params.message_magic, format: FORMAT_LEGACY, address: nil)
37
59
  validate_format!(format)
38
60
  digest = message_hash(message, prefix: prefix, legacy: format == FORMAT_LEGACY)
@@ -43,20 +65,15 @@ module Bitcoin
43
65
  else
44
66
  validate_address!(address)
45
67
  addr = Bitcoin::Script.parse_from_addr(address)
46
- sig_ver, algo = if addr.p2wpkh? || addr.p2wsh?
47
- [:witness_v0, :ecdsa]
48
- elsif addr.p2tr?
49
- [:taproot, :schnorr]
50
- else
51
-
52
- end
53
68
  tx = to_sign_tx(digest, address)
54
69
  prev_out = Bitcoin::TxOut.new(script_pubkey: addr, value: 0)
55
70
  if addr.p2tr?
56
- sighash = tx.sighash_for_input(0, addr, sig_version: :taproot, prevouts: [prev_out], hash_type: Bitcoin::SIGHASH_TYPE[:default])
57
71
  tweaked = Bitcoin::Taproot.tweak_private_key(key, '')
72
+ validate_key!(Bitcoin::Script.to_p2tr(tweaked), addr, address)
73
+ sighash = tx.sighash_for_input(0, addr, sig_version: :taproot, prevouts: [prev_out], hash_type: Bitcoin::SIGHASH_TYPE[:default])
58
74
  tx.in[0].script_witness.stack << tweaked.sign(sighash, algo: :schnorr)
59
- elsif addr.p2wpkh? || addr.p2wsh?
75
+ elsif addr.p2wpkh?
76
+ validate_key!(Bitcoin::Script.to_p2wpkh(key.hash160), addr, address)
60
77
  sighash = tx.sighash_for_input(0, addr, sig_version: :witness_v0, amount: 0, prevouts: [prev_out])
61
78
  ecdsa = key.sign(sighash, algo: :ecdsa) + [Bitcoin::SIGHASH_TYPE[:all]].pack('C')
62
79
  tx.in[0].script_witness.stack << ecdsa
@@ -75,7 +92,9 @@ module Bitcoin
75
92
  # @param [String] signature The signature in base64 format.
76
93
  # @param [String] message The message that was signed.
77
94
  # @return [Boolean] Verification result.
95
+ # @raise [ArgumentError] If +signature+ is not a String or cannot be decoded.
78
96
  def verify_message(address, signature, message, prefix: Bitcoin.chain_params.message_magic)
97
+ raise ArgumentError, 'signature must be a String.' unless signature.is_a?(String)
79
98
  addr_script = Bitcoin::Script.parse_from_addr(address)
80
99
  variant, body = case signature
81
100
  when ''
@@ -103,7 +122,8 @@ module Bitcoin
103
122
  verify_simple(addr_script, payload, message, prefix: prefix)
104
123
  when :full
105
124
  begin
106
- tx = Bitcoin::Tx.parse_from_payload(payload)
125
+ # strict rejects bytes after the tx, as Bitcoin Core's DecodeTx does.
126
+ tx = Bitcoin::Tx.parse_from_payload(payload, strict: true)
107
127
  rescue StandardError
108
128
  raise ArgumentError, 'error parsing signature as full variant'
109
129
  end
@@ -128,9 +148,26 @@ module Bitcoin
128
148
  Bitcoin::Script.parse_from_addr(address)
129
149
  end
130
150
 
151
+ # Ensure the signing key can satisfy +address+.
152
+ # A key the address does not commit to still produces a signature, and BIP-322 carries
153
+ # nothing that would report it verifies against no one.
154
+ # @param [Bitcoin::Script] derived The script the signing key produces.
155
+ # @param [Bitcoin::Script] expected The script of the address to be signed for.
156
+ # @param [String] address The address to be signed for, for the error message.
157
+ # @raise [ArgumentError] If the key does not correspond to +address+.
158
+ def validate_key!(derived, expected, address)
159
+ return if derived == expected
160
+ message = "Key does not correspond to #{address}."
161
+ if expected.p2tr?
162
+ message += ' A P2TR address holds the output key, which is the internal key this key' \
163
+ ' holds tweaked with an empty merkle root. See Bitcoin::Key#to_p2tr.'
164
+ end
165
+ raise ArgumentError, message
166
+ end
167
+
131
168
  def validate_format!(format)
132
169
  unless [FORMAT_LEGACY, FORMAT_FULL, FORMAT_SIMPLE].include?(format)
133
- raise ArgumentError "Invalid format specified."
170
+ raise ArgumentError, "Invalid format specified."
134
171
  end
135
172
  end
136
173
 
@@ -169,10 +206,23 @@ module Bitcoin
169
206
  def verify_simple(addr_script, witness_payload, message, prefix:)
170
207
  digest = message_hash(message, prefix: prefix, legacy: false)
171
208
  tx = to_sign_tx(digest, addr_script.to_addr)
172
- tx.in[0].script_witness = Bitcoin::ScriptWitness.parse_from_payload(witness_payload)
209
+ tx.in[0].script_witness = parse_simple_witness(witness_payload)
173
210
  run_interpreter(tx, addr_script)
174
211
  end
175
212
 
213
+ # Parse the witness of a simple signature, which has to be exactly one serialized witness.
214
+ # @raise [ArgumentError] If the witness is truncated or followed by other bytes.
215
+ def parse_simple_witness(payload)
216
+ buf = StringIO.new(payload)
217
+ begin
218
+ witness = Bitcoin::ScriptWitness.parse_from_payload(buf)
219
+ rescue StandardError
220
+ raise ArgumentError, 'error parsing signature as simple variant'
221
+ end
222
+ raise ArgumentError, 'error parsing signature as simple variant' unless buf.eof?
223
+ witness
224
+ end
225
+
176
226
  def verify_full(addr_script, tx, message, prefix:)
177
227
  digest = message_hash(message, prefix: prefix, legacy: false)
178
228
  to_spend = to_spend_tx(digest, addr_script.to_addr)
@@ -183,7 +233,7 @@ module Bitcoin
183
233
 
184
234
  def run_interpreter(tx, script_pubkey)
185
235
  tx_out = Bitcoin::TxOut.new(script_pubkey: script_pubkey)
186
- checker = Bitcoin::TxChecker.new(tx: tx, input_index: 0, prevouts: [tx_out])
236
+ checker = BIP322Checker.new(tx: tx, input_index: 0, prevouts: [tx_out])
187
237
  interpreter = Bitcoin::ScriptInterpreter.new(flags: BIP322_VERIFY_FLAGS, checker: checker)
188
238
  interpreter.verify_script(tx.in[0].script_sig, script_pubkey, tx.in[0].script_witness)
189
239
  end
@@ -197,10 +247,12 @@ module Bitcoin
197
247
  end
198
248
 
199
249
  private_class_method :validate_address!
250
+ private_class_method :validate_key!
200
251
  private_class_method :validate_format!
201
252
  private_class_method :validate_to_sign_tx!
202
253
  private_class_method :run_interpreter
203
254
  private_class_method :verify_simple
255
+ private_class_method :parse_simple_witness
204
256
  private_class_method :verify_full
205
257
  private_class_method :verify_legacy
206
258
  end
@@ -8,6 +8,11 @@ module Bitcoin
8
8
 
9
9
  attr_reader :language
10
10
 
11
+ # The number of words of a valid mnemonic sentence (BIP-39).
12
+ WORD_COUNTS = [12, 15, 18, 21, 24].freeze
13
+ # The number of bytes of a valid entropy (BIP-39).
14
+ ENTROPY_BYTES = [16, 20, 24, 28, 32].freeze
15
+
11
16
  def initialize(language)
12
17
  raise ArgumentError, 'specified language is not supported.' unless Mnemonic.word_lists.include?(language)
13
18
  @language = language
@@ -22,6 +27,8 @@ module Bitcoin
22
27
  # @param [Array[String]] words the array of mnemonic word.
23
28
  # @return [String] an entropy with hex format.
24
29
  def to_entropy(words)
30
+ # BIP-39: the entropy is 128 to 256 bits in steps of 32, which gives 12, 15, 18, 21 or 24 words.
31
+ raise ArgumentError, 'Invalid mnemonic length.' unless words.is_a?(Array) && WORD_COUNTS.include?(words.size)
25
32
  word_master = load_words
26
33
  mnemonic = words.map do |w|
27
34
  index = word_master.index(normalize(w).downcase)
@@ -39,6 +46,7 @@ module Bitcoin
39
46
  # @return [Array] the array of mnemonic word.
40
47
  def to_mnemonic(entropy)
41
48
  raise ArgumentError, 'entropy is empty.' if entropy.nil? || entropy.empty?
49
+ raise ArgumentError, 'Invalid entropy length.' unless ENTROPY_BYTES.include?(entropy.htb.bytesize)
42
50
  e = entropy.htb.unpack1('B*')
43
51
  seed = e + checksum(e)
44
52
  mnemonic_index = seed.chars.each_slice(11).map{|i|i.join.to_i(2)}
@@ -25,7 +25,7 @@ module Bitcoin
25
25
  # @return [Bitcoin::OutPoint]
26
26
  def self.parse_from_payload(payload)
27
27
  buf = payload.is_a?(String) ? StringIO.new(payload) : payload
28
- hash, index = buf.read(36).unpack('a32V')
28
+ hash, index = Bitcoin.read_exact(buf, 36).unpack('a32V')
29
29
  OutPoint.new(hash.bth, index)
30
30
  end
31
31
 
@@ -33,7 +33,11 @@ module Bitcoin
33
33
  tx_hash == COINBASE_HASH && index == COINBASE_INDEX
34
34
  end
35
35
 
36
+ # @raise [ArgumentError] If the index does not fit in 4 bytes. -1, the default, is written as 0xffffffff.
36
37
  def to_payload
38
+ unless index.is_a?(Integer) && index.between?(-1, COINBASE_INDEX)
39
+ raise ArgumentError, 'index must be an integer from 0 to 0xffffffff.'
40
+ end
37
41
  [tx_hash.htb, index].pack('a32V')
38
42
  end
39
43
 
@@ -42,7 +46,7 @@ module Bitcoin
42
46
  end
43
47
 
44
48
  def valid?
45
- index >= 0 && (!coinbase? && tx_hash != COINBASE_HASH)
49
+ index.is_a?(Integer) && index.between?(0, COINBASE_INDEX) && (!coinbase? && tx_hash != COINBASE_HASH)
46
50
  end
47
51
 
48
52
  # convert hash to txid
@@ -19,6 +19,9 @@ module Bitcoin
19
19
  end
20
20
 
21
21
  # https://bitcoin.org/en/developer-reference#creating-a-merkleblock-message
22
+ # The tree is read depth first the way Bitcoin Core's CPartialMerkleTree::TraverseAndExtract does, and
23
+ # like Core it is rejected when the flags or the hashes run out, when a hash is left over, or when a flag
24
+ # bit outside the padding of the last byte is left over.
22
25
  # @param [Integer] tx_count The number of transactions in the block.
23
26
  # @param [Array] hashes Array of hash values with hex format.
24
27
  # @param [String] flags The sequence of bits, a string of '0' and '1'.
@@ -26,24 +29,12 @@ module Bitcoin
26
29
  # @raise [ArgumentError] If the given parameters are out of range or inconsistent.
27
30
  def self.build(tx_count, hashes, flags)
28
31
  validate!(tx_count, hashes, flags)
29
- flags = flags.each_char.map(&:to_i)
32
+ bits = flags.each_char.map(&:to_i)
30
33
  root = build_initial_tree( Array.new(tx_count) { Node.new })
31
- current_node = root
32
- hash_index = 0
33
- flags.each do |f|
34
- current_node.flag = f
35
- if f.zero? || current_node.leaf?
36
- current_node.value = hashes[hash_index]
37
- hash_index += 1
38
- end
39
- current_node = current_node.next_partial
40
- if hash_index == hashes.size
41
- if current_node&.leaf?
42
- current_node.value = hashes.last
43
- end
44
- break
45
- end
46
- end
34
+ used = { bits: 0, hashes: 0 }
35
+ traverse(root, bits, hashes, used)
36
+ raise ArgumentError, 'Invalid partial tree. Not every hash was used.' unless used[:hashes] == hashes.size
37
+ raise ArgumentError, 'Invalid partial tree. Not every flag bit was used.' unless (used[:bits] + 7) / 8 == (bits.size + 7) / 8
47
38
  new(root)
48
39
  end
49
40
 
@@ -61,13 +52,36 @@ module Bitcoin
61
52
  raise ArgumentError, 'flags must have at least one bit per hash.' if flags.size < hashes.size
62
53
  end
63
54
 
55
+ # Read the flag bit of +node+ and, for a node which is neither a leaf nor the parent of a match, the
56
+ # hash which stands for it. The right child of the last node of an odd level repeats the left one and
57
+ # has no flag or hash of its own.
58
+ # @raise [ArgumentError] If the flags or the hashes run out.
59
+ def self.traverse(node, bits, hashes, used)
60
+ raise ArgumentError, 'Invalid partial tree. The flags ran out.' if used[:bits] >= bits.size
61
+ node.flag = bits[used[:bits]]
62
+ used[:bits] += 1
63
+ if node.flag.zero? || node.leaf?
64
+ raise ArgumentError, 'Invalid partial tree. The hashes ran out.' if used[:hashes] >= hashes.size
65
+ node.value = hashes[used[:hashes]]
66
+ used[:hashes] += 1
67
+ else
68
+ traverse(node.left, bits, hashes, used)
69
+ if node.right.duplicated
70
+ node.right.value = node.left.value
71
+ else
72
+ traverse(node.right, bits, hashes, used)
73
+ end
74
+ end
75
+ end
76
+ private_class_method :traverse
77
+
64
78
  def self.build_initial_tree(nodes)
65
79
  raise ArgumentError, 'nodes must not be empty.' if nodes.empty?
66
80
  while nodes.size != 1
67
81
  nodes = nodes.each_slice(2).map { |m|
68
82
  parent = Node.new
69
83
  parent.left = m[0]
70
- parent.right = m[1] ? m[1] : m[0].dup
84
+ parent.right = m[1] ? m[1] : m[0].dup.tap { |n| n.duplicated = true }
71
85
  parent
72
86
  }
73
87
  end
@@ -86,9 +100,13 @@ module Bitcoin
86
100
  attr_accessor :parent
87
101
  attr_accessor :left
88
102
  attr_accessor :right
103
+ # Whether this node stands in for a missing right child, as the last node of a level with an odd
104
+ # count is paired with a copy of itself. Such a copy is equal to its sibling by construction.
105
+ attr_accessor :duplicated
89
106
 
90
107
  def initialize(value = nil)
91
108
  @value = value
109
+ @duplicated = false
92
110
  end
93
111
 
94
112
  def left=(node)
@@ -103,8 +121,11 @@ module Bitcoin
103
121
 
104
122
  def value
105
123
  return @value if @value
106
- self.right = left.dup unless right
107
- Bitcoin.double_sha256([left.value + right.value].pack('H*')).bth
124
+ left_value, right_value = left.value, right.value
125
+ # Two identical branches mean the hashes under them are not unique, which is how the duplicated
126
+ # tx of CVE-2012-2459 hides in a merkleblock. Bitcoin Core marks such a tree as bad.
127
+ raise ArgumentError, 'Invalid partial tree. The left and right branches are identical.' if !right.duplicated && left_value == right_value
128
+ Bitcoin.double_sha256([left_value + right_value].pack('H*')).bth
108
129
  end
109
130
 
110
131
  def root?
@@ -119,14 +140,6 @@ module Bitcoin
119
140
  !flag.nil?
120
141
  end
121
142
 
122
- def next_partial
123
- return nil if root? && (flag.zero? || (left.nil? && right.nil?) || (left.partial? && right.partial?))
124
- return parent.next_partial if flag.zero? || leaf?
125
- return left unless left.partial?
126
- self.right = left.dup unless right
127
- right.partial? ? parent.next_partial : right
128
- end
129
-
130
143
  # calculate the depth of this node in the tree.
131
144
  def depth
132
145
  d = 0
@@ -7,10 +7,15 @@ module Bitcoin
7
7
 
8
8
  attr_accessor :x_value
9
9
  attr_accessor :sign
10
+ attr_accessor :features_bits # the features byte with hex format. Only bit 0 may be set.
11
+ attr_accessor :reserve_field # the 13 reserved bytes with hex format, zero unless a future version says otherwise.
10
12
 
11
13
  VERSION_BYTE = '47'
12
14
  SUPPORT_VERSIONS = ['01']
13
15
  SUPPORT_SIGNS = ['02', '03']
16
+ PAYLOAD_SIZE = 80 # version, features, sign, x value, chain code and the reserved bytes.
17
+ # The Base58Check form: the 0x47 version byte, the payload and the checksum.
18
+ BASE58_PAYLOAD_SIZE = 1 + PAYLOAD_SIZE + 4
14
19
 
15
20
  def initialize
16
21
  @version = '01'
@@ -26,6 +31,9 @@ module Bitcoin
26
31
 
27
32
  payment_code = PaymentCode.new
28
33
  payment_code.depth = master_ext_key.depth
34
+ payment_code.number = master_ext_key.number
35
+ payment_code.parent_fingerprint = master_ext_key.parent_fingerprint
36
+ payment_code.ver = master_ext_key.ver
29
37
  payment_code.key = master_ext_key.key
30
38
  payment_code.sign = compressed_pubkey[0..1]
31
39
  payment_code.x_value = compressed_pubkey[2..-1]
@@ -50,28 +58,38 @@ module Bitcoin
50
58
  end
51
59
 
52
60
  # decode base58 encoded payment code
61
+ # The serialization is the 85 bytes of BIP-47: the 0x47 version byte, the 80 byte payload and the
62
+ # checksum. The features byte may only have bit 0 set, and the reserved bytes are kept as they are.
53
63
  # @params [String] base58_payment_code base58 encoded payment code
64
+ # @return [String] the 80 byte payload with binary format.
65
+ # @raise [ArgumentError] If +base58_payment_code+ is not a payment code.
54
66
  def self.from_base58(base58_payment_code)
67
+ raise ArgumentError, 'payment code must be string.' unless base58_payment_code.is_a?(String)
55
68
  hex = Bitcoin::Base58.decode(base58_payment_code)
69
+ raise ArgumentError, 'invalid payment code length' unless hex.length == BASE58_PAYLOAD_SIZE * 2
70
+ raise ArgumentError, 'invalid version byte' unless hex[0..1] == VERSION_BYTE
71
+ payment_code = hex[0...-8]
72
+ raise ArgumentError, Errors::Messages::INVALID_CHECKSUM unless Bitcoin.calc_checksum(payment_code) == hex[-8..-1]
73
+
56
74
  version = hex[2..3]
75
+ features = hex[4..5]
57
76
  sign = hex[6..7]
58
- public_key = hex[8..71]
59
- payment_code = hex[0...-8]
77
+ x_value = hex[8..71]
78
+ chain_code_hex = hex[72..135]
79
+ reserved = hex[136..161]
60
80
 
61
- raise ArgumentError, 'invalid version byte' unless hex[0..1] == VERSION_BYTE
62
81
  raise ArgumentError, 'invalid version' unless PaymentCode.support_version?(version)
82
+ raise ArgumentError, 'invalid features' unless (features.to_i(16) & 0xfe).zero?
63
83
  raise ArgumentError, 'invalid sign' unless PaymentCode.support_sign?(sign)
64
- raise ArgumentError, Errors::Messages::INVALID_PUBLIC_KEY unless Bitcoin::Key.new(pubkey: sign + public_key).fully_valid_pubkey?
65
- raise ArgumentError, Errors::Messages::INVALID_CHECKSUM unless Bitcoin.calc_checksum(payment_code) == hex[-8..-1]
66
-
67
- x_value = payment_code[8..71]
68
- chain_code_hex = payment_code[72..135]
84
+ raise ArgumentError, Errors::Messages::INVALID_PUBLIC_KEY unless Bitcoin::Key.new(pubkey: sign + x_value).fully_valid_pubkey?
69
85
 
70
86
  payment_code_pubkey = PaymentCode.new
71
87
  payment_code_pubkey.depth = 3
88
+ payment_code_pubkey.features_bits = features
72
89
  payment_code_pubkey.sign = sign
73
90
  payment_code_pubkey.x_value = x_value
74
91
  payment_code_pubkey.chain_code = [chain_code_hex].pack('H*')
92
+ payment_code_pubkey.reserve_field = reserved
75
93
 
76
94
  payment_code_pubkey.to_payload
77
95
  end
@@ -11,8 +11,7 @@ module Bitcoin
11
11
  def initialize(pubkey, info)
12
12
  pubkey = pubkey.encoding == Encoding::ASCII_8BIT ? pubkey : pubkey.htb
13
13
  raise ArgumentError, 'Size of key was not the expected size for the type BIP32 keypath.' unless [Bitcoin::Key::PUBLIC_KEY_SIZE, Bitcoin::Key::COMPRESSED_PUBLIC_KEY_SIZE].include?(pubkey.bytesize)
14
- pubkey = Bitcoin::Key.new(pubkey: pubkey.bth)
15
- raise ArgumentError, Errors::Messages::INVALID_PUBLIC_KEY unless pubkey.fully_valid_pubkey?
14
+ pubkey = PSBT.parse_pubkey(pubkey.bth)
16
15
  @pubkey = pubkey.pubkey
17
16
  @info = info
18
17
  end