bitcoinrb 1.15.0 → 1.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. checksums.yaml +4 -4
  2. data/bitcoinrb.gemspec +5 -4
  3. data/lib/bitcoin/bip321_uri.rb +95 -32
  4. data/lib/bitcoin/bip324/ell_swift_pubkey.rb +3 -1
  5. data/lib/bitcoin/bip324/fs_chacha20.rb +3 -0
  6. data/lib/bitcoin/bip324.rb +1 -1
  7. data/lib/bitcoin/bip85_entropy.rb +79 -29
  8. data/lib/bitcoin/bit_stream.rb +2 -2
  9. data/lib/bitcoin/block.rb +46 -7
  10. data/lib/bitcoin/block_filter.rb +3 -2
  11. data/lib/bitcoin/block_header.rb +31 -8
  12. data/lib/bitcoin/bloom_filter.rb +5 -2
  13. data/lib/bitcoin/chainparams/regtest.yml +0 -1
  14. data/lib/bitcoin/chainparams/signet.yml +1 -1
  15. data/lib/bitcoin/constants.rb +13 -3
  16. data/lib/bitcoin/descriptor/combo.rb +13 -1
  17. data/lib/bitcoin/descriptor/expression.rb +35 -15
  18. data/lib/bitcoin/descriptor/multi.rb +9 -1
  19. data/lib/bitcoin/descriptor/multi_a.rb +4 -0
  20. data/lib/bitcoin/descriptor/musig.rb +1 -0
  21. data/lib/bitcoin/descriptor/pk.rb +7 -0
  22. data/lib/bitcoin/descriptor/pkh.rb +5 -0
  23. data/lib/bitcoin/descriptor/raw.rb +1 -1
  24. data/lib/bitcoin/descriptor/sh.rb +1 -0
  25. data/lib/bitcoin/descriptor/sp.rb +2 -2
  26. data/lib/bitcoin/descriptor/tr.rb +29 -3
  27. data/lib/bitcoin/descriptor/wpkh.rb +1 -0
  28. data/lib/bitcoin/descriptor/wsh.rb +1 -0
  29. data/lib/bitcoin/descriptor.rb +59 -35
  30. data/lib/bitcoin/ext/array_ext.rb +3 -0
  31. data/lib/bitcoin/ext/ecdsa.rb +7 -1
  32. data/lib/bitcoin/ext_key.rb +10 -0
  33. data/lib/bitcoin/gcs_filter.rb +21 -6
  34. data/lib/bitcoin/key.rb +66 -14
  35. data/lib/bitcoin/key_path.rb +8 -5
  36. data/lib/bitcoin/message/addr.rb +1 -0
  37. data/lib/bitcoin/message/addr_v2.rb +1 -0
  38. data/lib/bitcoin/message/base.rb +8 -1
  39. data/lib/bitcoin/message/block.rb +4 -8
  40. data/lib/bitcoin/message/block_transaction_request.rb +11 -8
  41. data/lib/bitcoin/message/block_transactions.rb +2 -1
  42. data/lib/bitcoin/message/cf_parser.rb +1 -0
  43. data/lib/bitcoin/message/cfcheckpt.rb +3 -3
  44. data/lib/bitcoin/message/cfheaders.rb +5 -4
  45. data/lib/bitcoin/message/cfilter.rb +3 -3
  46. data/lib/bitcoin/message/error.rb +3 -1
  47. data/lib/bitcoin/message/fee_filter.rb +1 -0
  48. data/lib/bitcoin/message/filter_add.rb +3 -1
  49. data/lib/bitcoin/message/filter_load.rb +4 -4
  50. data/lib/bitcoin/message/get_cfcheckpt.rb +1 -0
  51. data/lib/bitcoin/message/header_and_short_ids.rb +26 -11
  52. data/lib/bitcoin/message/headers.rb +10 -2
  53. data/lib/bitcoin/message/headers_parser.rb +4 -2
  54. data/lib/bitcoin/message/inventories_parser.rb +2 -1
  55. data/lib/bitcoin/message/inventory.rb +15 -6
  56. data/lib/bitcoin/message/merkle_block.rb +4 -4
  57. data/lib/bitcoin/message/network_addr.rb +13 -13
  58. data/lib/bitcoin/message/ping.rb +1 -0
  59. data/lib/bitcoin/message/pong.rb +1 -0
  60. data/lib/bitcoin/message/reject.rb +1 -0
  61. data/lib/bitcoin/message/send_cmpct.rb +2 -2
  62. data/lib/bitcoin/message/send_tx_rcncl.rb +1 -1
  63. data/lib/bitcoin/message/version.rb +23 -8
  64. data/lib/bitcoin/message.rb +15 -0
  65. data/lib/bitcoin/message_sign.rb +44 -6
  66. data/lib/bitcoin/mnemonic.rb +8 -0
  67. data/lib/bitcoin/out_point.rb +6 -2
  68. data/lib/bitcoin/partial_tree.rb +41 -28
  69. data/lib/bitcoin/payment_code.rb +26 -8
  70. data/lib/bitcoin/psbt/hd_key_path.rb +1 -2
  71. data/lib/bitcoin/psbt/input.rb +199 -48
  72. data/lib/bitcoin/psbt/key_origin_info.rb +2 -0
  73. data/lib/bitcoin/psbt/output.rb +9 -6
  74. data/lib/bitcoin/psbt/proprietary.rb +6 -3
  75. data/lib/bitcoin/psbt/tx.rb +9 -4
  76. data/lib/bitcoin/psbt.rb +46 -0
  77. data/lib/bitcoin/rpc/bitcoin_core_client.rb +54 -13
  78. data/lib/bitcoin/rpc/error.rb +43 -0
  79. data/lib/bitcoin/rpc.rb +1 -0
  80. data/lib/bitcoin/script/script.rb +114 -33
  81. data/lib/bitcoin/script/script_interpreter.rb +11 -6
  82. data/lib/bitcoin/script/tx_checker.rb +7 -2
  83. data/lib/bitcoin/script_witness.rb +5 -1
  84. data/lib/bitcoin/secp256k1/ruby.rb +10 -4
  85. data/lib/bitcoin/silent_payment.rb +6 -3
  86. data/lib/bitcoin/slip39/share.rb +35 -9
  87. data/lib/bitcoin/slip39/sss.rb +26 -11
  88. data/lib/bitcoin/slip39.rb +13 -5
  89. data/lib/bitcoin/taproot/control_block.rb +6 -1
  90. data/lib/bitcoin/taproot/custom_depth_builder.rb +12 -0
  91. data/lib/bitcoin/tx.rb +35 -14
  92. data/lib/bitcoin/tx_in.rb +2 -2
  93. data/lib/bitcoin/tx_out.rb +2 -2
  94. data/lib/bitcoin/util.rb +49 -28
  95. data/lib/bitcoin/validation.rb +64 -12
  96. data/lib/bitcoin/version.rb +1 -1
  97. data/lib/bitcoin/wallet/account.rb +35 -11
  98. data/lib/bitcoin/wallet/base.rb +40 -15
  99. data/lib/bitcoin/wallet/utxo.rb +22 -4
  100. data/lib/bitcoin.rb +16 -4
  101. metadata +24 -9
@@ -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.
@@ -71,7 +92,9 @@ module Bitcoin
71
92
  # @param [String] signature The signature in base64 format.
72
93
  # @param [String] message The message that was signed.
73
94
  # @return [Boolean] Verification result.
95
+ # @raise [ArgumentError] If +signature+ is not a String or cannot be decoded.
74
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)
75
98
  addr_script = Bitcoin::Script.parse_from_addr(address)
76
99
  variant, body = case signature
77
100
  when ''
@@ -99,7 +122,8 @@ module Bitcoin
99
122
  verify_simple(addr_script, payload, message, prefix: prefix)
100
123
  when :full
101
124
  begin
102
- 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)
103
127
  rescue StandardError
104
128
  raise ArgumentError, 'error parsing signature as full variant'
105
129
  end
@@ -143,7 +167,7 @@ module Bitcoin
143
167
 
144
168
  def validate_format!(format)
145
169
  unless [FORMAT_LEGACY, FORMAT_FULL, FORMAT_SIMPLE].include?(format)
146
- raise ArgumentError "Invalid format specified."
170
+ raise ArgumentError, "Invalid format specified."
147
171
  end
148
172
  end
149
173
 
@@ -182,10 +206,23 @@ module Bitcoin
182
206
  def verify_simple(addr_script, witness_payload, message, prefix:)
183
207
  digest = message_hash(message, prefix: prefix, legacy: false)
184
208
  tx = to_sign_tx(digest, addr_script.to_addr)
185
- tx.in[0].script_witness = Bitcoin::ScriptWitness.parse_from_payload(witness_payload)
209
+ tx.in[0].script_witness = parse_simple_witness(witness_payload)
186
210
  run_interpreter(tx, addr_script)
187
211
  end
188
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
+
189
226
  def verify_full(addr_script, tx, message, prefix:)
190
227
  digest = message_hash(message, prefix: prefix, legacy: false)
191
228
  to_spend = to_spend_tx(digest, addr_script.to_addr)
@@ -196,7 +233,7 @@ module Bitcoin
196
233
 
197
234
  def run_interpreter(tx, script_pubkey)
198
235
  tx_out = Bitcoin::TxOut.new(script_pubkey: script_pubkey)
199
- checker = Bitcoin::TxChecker.new(tx: tx, input_index: 0, prevouts: [tx_out])
236
+ checker = BIP322Checker.new(tx: tx, input_index: 0, prevouts: [tx_out])
200
237
  interpreter = Bitcoin::ScriptInterpreter.new(flags: BIP322_VERIFY_FLAGS, checker: checker)
201
238
  interpreter.verify_script(tx.in[0].script_sig, script_pubkey, tx.in[0].script_witness)
202
239
  end
@@ -215,6 +252,7 @@ module Bitcoin
215
252
  private_class_method :validate_to_sign_tx!
216
253
  private_class_method :run_interpreter
217
254
  private_class_method :verify_simple
255
+ private_class_method :parse_simple_witness
218
256
  private_class_method :verify_full
219
257
  private_class_method :verify_legacy
220
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