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
data/lib/bitcoin/key.rb CHANGED
@@ -95,7 +95,7 @@ module Bitcoin
95
95
  # @return [Bitcoin::Key]
96
96
  def self.from_point(point, compressed: true)
97
97
  pubkey = ECDSA::Format::PointOctetString.encode(point, compression: compressed).bth
98
- Bitcoin::Key.new(pubkey: pubkey, key_type: TYPES[:compressed])
98
+ Bitcoin::Key.new(pubkey: pubkey, key_type: compressed ? TYPES[:compressed] : TYPES[:uncompressed])
99
99
  end
100
100
 
101
101
  # export private key with wif format
@@ -164,6 +164,7 @@ module Bitcoin
164
164
  case algo
165
165
  when :ecdsa
166
166
  sig = ecdsa_signature_parse_der_lax(sig)
167
+ return false unless sig
167
168
  secp256k1_module.verify_sig(data, sig, pubkey)
168
169
  when :schnorr
169
170
  secp256k1_module.verify_sig(data, sig, xonly_pubkey, algo: :schnorr)
@@ -359,7 +360,11 @@ module Bitcoin
359
360
  if secp256k1_module.native?
360
361
  Bitcoin::BIP324::EllSwiftPubkey.new(secp256k1_module.ellswift_create(priv_key))
361
362
  else
362
- Bitcoin::BIP324::EllSwiftPubkey.new(Bitcoin::BIP324.xelligatorswift(xonly_pubkey))
363
+ encoded = Bitcoin::BIP324.xelligatorswift(xonly_pubkey)
364
+ # t and -t decode to the same x, and the parity of t gives the Y coordinate, as libsecp256k1 encodes it.
365
+ t = encoded[64..-1].hex
366
+ t = Bitcoin::BIP324::FIELD.mod(-t) if t.odd? != to_point.y.odd?
367
+ Bitcoin::BIP324::EllSwiftPubkey.new(encoded[0...64] + ECDSA::Format::IntegerOctetString.encode(t, 32).bth)
363
368
  end
364
369
  end
365
370
 
@@ -394,22 +399,69 @@ module Bitcoin
394
399
  MIN_PRIV_KEY_MOD_ORDER <= value && value <= MAX_PRIV_KEY_MOD_ORDER
395
400
  end
396
401
 
397
- # Supported violations include negative integers, excessive padding, garbage
398
- # at the end, and overly long length descriptors. This is safe to use in
399
- # Bitcoin because since the activation of BIP66, signatures are verified to be
400
- # strict DER before being passed to this module, and we know it supports all
401
- # violations present in the blockchain before that point.
402
+ # Parse a DER signature the way ecdsa_signature_parse_der_lax in Bitcoin Core does, and return it
403
+ # as strict DER for the backend. Supported violations include negative integers, excessive padding,
404
+ # garbage at the end, and overly long length descriptors. This is safe to use in Bitcoin because
405
+ # since the activation of BIP66, signatures are verified to be strict DER before being passed to
406
+ # this method, and it supports all violations present in the blockchain before that point.
407
+ # @param [String] sig a signature with binary format, without the sighash type.
408
+ # @return [String, nil] the signature as strict DER, or nil when it cannot be parsed or R or S is
409
+ # zero or not below the curve order, which Bitcoin Core also treats as an invalid signature.
402
410
  def ecdsa_signature_parse_der_lax(sig)
403
- sig_array = sig.unpack('C*')
404
- len_r = sig_array[3]
405
- r = sig_array[4...(len_r+4)].pack('C*').bth
406
- len_s = sig_array[len_r + 5]
407
- s = sig_array[(len_r + 6)...(len_r + 6 + len_s)].pack('C*').bth
408
- ECDSA::Signature.new(r.to_i(16), s.to_i(16)).to_der
411
+ bytes = sig.unpack('C*')
412
+ pos = 0
413
+ return nil if bytes[pos] != 0x30 # sequence tag
414
+ pos += 1
415
+ return nil if pos >= bytes.size
416
+ len_byte = bytes[pos] # the sequence length is skipped, however long it claims to be
417
+ pos += 1
418
+ if len_byte & 0x80 != 0
419
+ len_byte -= 0x80
420
+ return nil if len_byte > bytes.size - pos
421
+ pos += len_byte
422
+ end
423
+ r, pos = parse_lax_der_integer(bytes, pos)
424
+ return nil unless r
425
+ s, _ = parse_lax_der_integer(bytes, pos)
426
+ return nil unless s
427
+ order = ECDSA::Group::Secp256k1.order
428
+ return nil if r.zero? || s.zero? || r >= order || s >= order
429
+ ECDSA::Signature.new(r, s).to_der
430
+ end
431
+
432
+ # Parse one INTEGER of a lax DER signature starting at +pos+.
433
+ # @return [Array(Integer, Integer), nil] the value and the position after it, or nil.
434
+ def parse_lax_der_integer(bytes, pos)
435
+ return nil if pos >= bytes.size || bytes[pos] != 0x02 # integer tag
436
+ pos += 1
437
+ return nil if pos >= bytes.size
438
+ len_byte = bytes[pos]
439
+ pos += 1
440
+ if len_byte & 0x80 != 0
441
+ len_byte -= 0x80
442
+ return nil if len_byte > bytes.size - pos
443
+ while len_byte > 0 && bytes[pos] == 0 # leading zeros of the length
444
+ pos += 1
445
+ len_byte -= 1
446
+ end
447
+ return nil if len_byte >= 4
448
+ len = 0
449
+ len_byte.times do
450
+ len = (len << 8) + bytes[pos]
451
+ pos += 1
452
+ end
453
+ else
454
+ len = len_byte
455
+ end
456
+ return nil if len > bytes.size - pos
457
+ # Read as an unsigned number: a leading 0x80 bit, which strict DER would call negative, and
458
+ # leading zero padding are both accepted.
459
+ value = len.zero? ? 0 : bytes[pos, len].pack('C*').unpack1('H*').to_i(16)
460
+ [value, pos + len]
409
461
  end
410
462
 
411
463
  def valid_pubkey?
412
- !pubkey.nil? && pubkey.size > 0
464
+ !pubkey.nil? && pubkey.size > 0 && pubkey.valid_hex?
413
465
  end
414
466
 
415
467
  # check whether the signature is low-R
@@ -2,6 +2,7 @@ module Bitcoin
2
2
  module KeyPath
3
3
 
4
4
  # key path convert an array of derive number
5
+ # A hardened index is marked with a trailing ' or h, as Bitcoin Core's ParseHDKeypath reads it.
5
6
  # @param [String] path_string
6
7
  # @return [Array[Integer]] key path numbers.
7
8
  # @raise [ArgumentError] if invalid +path_string+.
@@ -14,11 +15,13 @@ module Bitcoin
14
15
  next if p == 'm'
15
16
  raise ArgumentError, "Invalid path." unless p == 'm'
16
17
  end
17
- raise ArgumentError, "Invalid path." if p.count("'") > 1 || (p.count("'") == 1 && p[-1] != "'")
18
- raise ArgumentError, "Invalid path." unless p.delete("'") =~ /^[0-9]+$/
19
- value = (p[-1] == "'" ? p.delete("'").to_i + Bitcoin::HARDENED_THRESHOLD : p.to_i)
20
- raise ArgumentError, "Invalid path." if value > 4294967295 # 4294967295 = 0xFFFFFFFF (uint32 max)
21
- value
18
+ hardened = p.end_with?("'", 'h')
19
+ number = hardened ? p[0...-1] : p
20
+ raise ArgumentError, "Invalid path." unless number =~ /\A[0-9]+\z/
21
+ value = number.to_i
22
+ # 4294967295 = 0xFFFFFFFF (uint32 max)
23
+ raise ArgumentError, "Invalid path." if value > (hardened ? Bitcoin::HARDENED_THRESHOLD - 1 : 4294967295)
24
+ hardened ? value + Bitcoin::HARDENED_THRESHOLD : value
22
25
  end[1..-1]
23
26
  end
24
27
 
@@ -18,6 +18,7 @@ module Bitcoin
18
18
  def self.parse_from_payload(payload)
19
19
  buf = StringIO.new(payload)
20
20
  addr_count = Bitcoin.unpack_var_int_from_io(buf)
21
+ raise Bitcoin::Message::Error, "addr message size #{addr_count} exceeds #{MAX_ADDR_TO_SEND}." if addr_count > MAX_ADDR_TO_SEND
21
22
  addr = new
22
23
  addr_count.times do
23
24
  addr.addrs << NetworkAddr.parse_from_payload(buf)
@@ -16,6 +16,7 @@ module Bitcoin
16
16
  def self.parse_from_payload(payload)
17
17
  buf = StringIO.new(payload)
18
18
  addr_count = Bitcoin.unpack_var_int_from_io(buf)
19
+ raise Bitcoin::Message::Error, "addrv2 message size #{addr_count} exceeds #{MAX_ADDR_TO_SEND}." if addr_count > MAX_ADDR_TO_SEND
19
20
  v2 = new
20
21
  addr_count.times do
21
22
  v2.addrs << NetworkAddr.parse_from_payload(buf, type: NetworkAddr::TYPE[:addr_v2])
@@ -7,6 +7,9 @@ module Bitcoin
7
7
  include Bitcoin::Util
8
8
  extend Bitcoin::Util
9
9
 
10
+ # The size of the message header: magic, command, length and checksum.
11
+ HEADER_SIZE = 24
12
+
10
13
  # generate message header (binary format)
11
14
  # https://bitcoin.org/en/developer-reference#message-headers
12
15
  def to_pkt
@@ -28,10 +31,14 @@ module Bitcoin
28
31
  # @return [Bitcoin::Message::XXX] An instance of a class that inherits Bitcoin::Message::Base
29
32
  # @raise [ArgumentError] Occurs for data that cannot be decoded.
30
33
  def self.from_pkt(message)
34
+ raise ArgumentError, 'Message header is too short.' if message.bytesize < HEADER_SIZE
31
35
  buf = StringIO.new(message)
32
36
  magic = buf.read(4)
33
37
  raise ArgumentError, 'Invalid magic.' unless magic == Bitcoin.chain_params.magic_head.htb
34
- command = buf.read(12).delete("\x00")
38
+ # The command is padded with NUL bytes to 12. Bitcoin Core rejects a command with a non-NUL byte
39
+ # after its first NUL.
40
+ command, padding = buf.read(12).split("\x00", 2)
41
+ raise ArgumentError, 'Invalid command.' if command.nil? || padding&.delete("\x00")&.bytesize.to_i > 0
35
42
  # The length is announced by the peer, so it is checked before it is used to read.
36
43
  length = buf.read(4).unpack1('V')
37
44
  if length > MAX_PROTOCOL_MESSAGE_LENGTH
@@ -19,14 +19,10 @@ module Bitcoin
19
19
 
20
20
  def self.parse_from_payload(payload)
21
21
  buf = StringIO.new(payload)
22
- header = Bitcoin::BlockHeader.parse_from_payload(buf.read(80))
23
- transactions = []
24
- unless buf.eof?
25
- tx_count = Bitcoin.unpack_var_int_from_io(buf)
26
- tx_count.times do
27
- transactions << Bitcoin::Tx.parse_from_payload(buf)
28
- end
29
- end
22
+ header = Bitcoin::BlockHeader.parse_from_payload(Bitcoin.read_exact(buf, 80))
23
+ # A block message always carries its tx count, which may be 0 only in a block nobody can produce.
24
+ tx_count = Bitcoin.unpack_var_int_from_io(buf)
25
+ transactions = tx_count.times.map { Bitcoin::Tx.parse_from_payload(buf) }
30
26
  new(header, transactions)
31
27
  end
32
28
 
@@ -15,18 +15,21 @@ module Bitcoin
15
15
 
16
16
  def self.parse_from_payload(payload)
17
17
  buf = StringIO.new(payload)
18
- block_hash = buf.read(32).bth
18
+ block_hash = Bitcoin.read_exact(buf, 32)
19
19
  index_len = Bitcoin.unpack_var_int_from_io(buf)
20
- indexes = index_len.times.map{Bitcoin.unpack_var_int_from_io(buf)}
21
- # index data differentially encoded
20
+ # Each index takes at least one byte, so a count the payload cannot hold is rejected before
21
+ # anything is allocated for it.
22
+ raise Bitcoin::Message::Error, "Invalid getblocktxn payload. index count #{index_len} exceeds the payload." if index_len > (buf.size - buf.pos)
23
+ # index data differentially encoded. Bitcoin Core keeps every decoded index within 16 bits.
24
+ indexes = []
22
25
  offset = 0
23
- index_len.times do |i|
24
- index = indexes[i]
25
- index += offset
26
- indexes[i] = index
26
+ index_len.times do
27
+ index = Bitcoin.unpack_var_int_from_io(buf) + offset
28
+ raise Bitcoin::Message::Error, 'Invalid getblocktxn payload. index overflowed 16 bits.' if index > 0xffff
29
+ indexes << index
27
30
  offset = index + 1
28
31
  end
29
- self.new(block_hash, indexes)
32
+ self.new(block_hash.bth, indexes)
30
33
  end
31
34
 
32
35
  def to_payload
@@ -15,8 +15,9 @@ module Bitcoin
15
15
 
16
16
  def self.parse_from_payload(payload)
17
17
  buf = StringIO.new(payload)
18
- block_hash = buf.read(32).bth
18
+ block_hash = Bitcoin.read_exact(buf, 32).bth
19
19
  tx_count = Bitcoin.unpack_var_int_from_io(buf)
20
+ raise Bitcoin::Message::Error, "blocktxn tx count #{tx_count} exceeds #{MAX_COMPACT_BLOCK_TX_COUNT}." if tx_count > MAX_COMPACT_BLOCK_TX_COUNT
20
21
  txn = tx_count.times.map{Bitcoin::Tx.parse_from_payload(buf)}
21
22
  self.new(block_hash, txn)
22
23
  end
@@ -3,6 +3,7 @@ module Bitcoin
3
3
  module CFParser
4
4
 
5
5
  def parse_from_payload(payload)
6
+ raise Bitcoin::Message::Error, 'Payload must be 37 bytes.' unless payload.bytesize == 37
6
7
  type, start, hash = payload.unpack('CVH*')
7
8
  self.new(type, start, hash)
8
9
  end
@@ -19,10 +19,10 @@ module Bitcoin
19
19
 
20
20
  def self.parse_from_payload(payload)
21
21
  buf = StringIO.new(payload)
22
- type = buf.read(1).unpack1('C')
23
- hash = buf.read(32).unpack1('H*')
22
+ type = Bitcoin.read_exact(buf, 1).unpack1('C')
23
+ hash = Bitcoin.read_exact(buf, 32).unpack1('H*')
24
24
  count = Bitcoin.unpack_var_int_from_io(buf)
25
- headers = count.times.map{buf.read(32).bth}
25
+ headers = count.times.map{Bitcoin.read_exact(buf, 32).bth}
26
26
  self.new(type, hash, headers)
27
27
  end
28
28
 
@@ -21,11 +21,12 @@ module Bitcoin
21
21
 
22
22
  def self.parse_from_payload(payload)
23
23
  buf = StringIO.new(payload)
24
- type = buf.read(1).unpack1("C")
25
- hash = buf.read(32).bth
26
- header = buf.read(32).bth
24
+ type = Bitcoin.read_exact(buf, 1).unpack1("C")
25
+ hash = Bitcoin.read_exact(buf, 32).bth
26
+ header = Bitcoin.read_exact(buf, 32).bth
27
27
  count = Bitcoin.unpack_var_int_from_io(buf)
28
- hashes = count.times.map{buf.read(32).bth}
28
+ raise Bitcoin::Message::Error, "cfheaders size #{count} exceeds #{MAX_CFHEADERS_RESULTS}." if count > MAX_CFHEADERS_RESULTS
29
+ hashes = count.times.map{Bitcoin.read_exact(buf, 32).bth}
29
30
  self.new(type, hash, header, hashes)
30
31
  end
31
32
 
@@ -19,10 +19,10 @@ module Bitcoin
19
19
 
20
20
  def self.parse_from_payload(payload)
21
21
  buf = StringIO.new(payload)
22
- type = buf.read(1).unpack1("C")
23
- hash = buf.read(32).bth
22
+ type = Bitcoin.read_exact(buf, 1).unpack1("C")
23
+ hash = Bitcoin.read_exact(buf, 32).bth
24
24
  len = Bitcoin.unpack_var_int_from_io(buf)
25
- filter = buf.read(len).bth
25
+ filter = Bitcoin.read_exact(buf, len).bth
26
26
  self.new(type, hash, filter)
27
27
  end
28
28
 
@@ -2,7 +2,9 @@ module Bitcoin
2
2
  module Message
3
3
 
4
4
  # P2P message layer Error
5
- class Error < StandardError
5
+ # It is an ArgumentError, like the errors the other parsers raise, so that every failure to parse
6
+ # data from a peer can be rescued as one.
7
+ class Error < ArgumentError
6
8
 
7
9
  end
8
10
 
@@ -15,6 +15,7 @@ module Bitcoin
15
15
  end
16
16
 
17
17
  def self.parse_from_payload(payload)
18
+ raise Bitcoin::Message::Error, 'Payload must be 8 bytes.' unless payload.bytesize == 8
18
19
  new(payload.unpack1('Q<'))
19
20
  end
20
21
 
@@ -15,7 +15,9 @@ module Bitcoin
15
15
  end
16
16
 
17
17
  def self.parse_from_payload(payload)
18
- new(Bitcoin.unpack_var_string(payload).first.bth)
18
+ element = Bitcoin.unpack_var_string(payload).first
19
+ raise Bitcoin::Message::Error, "filteradd element size #{element.bytesize} exceeds #{Bitcoin::MAX_SCRIPT_ELEMENT_SIZE}." if element.bytesize > Bitcoin::MAX_SCRIPT_ELEMENT_SIZE
20
+ new(element.bth)
19
21
  end
20
22
 
21
23
  def to_payload
@@ -28,14 +28,14 @@ module Bitcoin
28
28
  raise Bitcoin::Message::Error,
29
29
  "filter size must be less than or equal to #{Bitcoin::BloomFilter::MAX_BLOOM_FILTER_SIZE}."
30
30
  end
31
- filter = buf.read(filter_count).unpack('C*')
32
- func_count = buf.read(4).unpack1('V')
31
+ filter = Bitcoin.read_exact(buf, filter_count).unpack('C*')
32
+ func_count = Bitcoin.read_exact(buf, 4).unpack1('V')
33
33
  if func_count > Bitcoin::BloomFilter::MAX_HASH_FUNCS
34
34
  raise Bitcoin::Message::Error,
35
35
  "hash funcs must be less than or equal to #{Bitcoin::BloomFilter::MAX_HASH_FUNCS}."
36
36
  end
37
- tweak = buf.read(4).unpack1('V')
38
- flag = buf.read(1).unpack1('C')
37
+ tweak = Bitcoin.read_exact(buf, 4).unpack1('V')
38
+ flag = Bitcoin.read_exact(buf, 1).unpack1('C')
39
39
  FilterLoad.new(Bitcoin::BloomFilter.new(filter, func_count, tweak), flag)
40
40
  end
41
41
 
@@ -16,6 +16,7 @@ module Bitcoin
16
16
  end
17
17
 
18
18
  def self.parse_from_payload(payload)
19
+ raise Bitcoin::Message::Error, 'Payload must be 33 bytes.' unless payload.bytesize == 33
19
20
  type, hash = payload.unpack('CH*')
20
21
  self.new(type, hash)
21
22
  end
@@ -6,9 +6,9 @@ module Bitcoin
6
6
  # https://github.com/bitcoin/bips/blob/master/bip-0152.mediawiki#HeaderAndShortIDs
7
7
  class HeaderAndShortIDs
8
8
  attr_accessor :header
9
- attr_accessor :nonce
9
+ attr_accessor :nonce # 64 bit unsigned integer
10
10
  attr_accessor :short_ids
11
- attr_accessor :prefilled_txn
11
+ attr_accessor :prefilled_txn # Array of PrefilledTx, whose index is the position in the block.
12
12
  attr_accessor :siphash_key
13
13
 
14
14
  def initialize(header, nonce, short_ids = [], prefilled_txn = [])
@@ -16,31 +16,46 @@ module Bitcoin
16
16
  @nonce = nonce
17
17
  @short_ids = short_ids
18
18
  @prefilled_txn = prefilled_txn
19
- @siphash_key = Bitcoin.sha256(header.to_payload << [nonce].pack('q<*'))[0...16]
19
+ @siphash_key = Bitcoin.sha256(header.to_payload << [nonce].pack('Q<'))[0...16]
20
20
  end
21
21
 
22
22
  def self.parse_from_payload(payload)
23
23
  buf = StringIO.new(payload)
24
- header = Bitcoin::BlockHeader.parse_from_payload(buf.read(80))
25
- nonce = buf.read(8).unpack1('q<*')
24
+ header = Bitcoin::BlockHeader.parse_from_payload(Bitcoin.read_exact(buf, 80))
25
+ nonce = Bitcoin.read_exact(buf, 8).unpack1('Q<')
26
26
  short_ids_len = Bitcoin.unpack_var_int_from_io(buf)
27
+ raise Bitcoin::Message::Error, "cmpctblock tx count #{short_ids_len} exceeds #{MAX_COMPACT_BLOCK_TX_COUNT}." if short_ids_len > MAX_COMPACT_BLOCK_TX_COUNT
27
28
  short_ids = short_ids_len.times.map do
28
- buf.read(6).reverse.bth.to_i(16)
29
+ Bitcoin.read_exact(buf, 6).reverse.bth.to_i(16)
29
30
  end
30
31
  prefilled_txn_len = Bitcoin.unpack_var_int_from_io(buf)
32
+ if short_ids_len + prefilled_txn_len > MAX_COMPACT_BLOCK_TX_COUNT
33
+ raise Bitcoin::Message::Error, "cmpctblock tx count #{short_ids_len + prefilled_txn_len} exceeds #{MAX_COMPACT_BLOCK_TX_COUNT}."
34
+ end
35
+ # The index of a prefilled tx is differentially encoded. Bitcoin Core keeps every decoded index within 16 bits.
36
+ offset = 0
31
37
  prefilled_txn = prefilled_txn_len.times.map do
32
- PrefilledTx.parse_from_io(buf)
38
+ prefilled = PrefilledTx.parse_from_io(buf)
39
+ prefilled.index += offset
40
+ raise Bitcoin::Message::Error, 'Invalid cmpctblock payload. prefilled tx index overflowed 16 bits.' if prefilled.index > 0xffff
41
+ offset = prefilled.index + 1
42
+ prefilled
33
43
  end
34
44
  self.new(header, nonce, short_ids, prefilled_txn)
35
45
  end
36
46
 
37
47
  def to_payload
38
48
  p = header.to_payload
39
- p << [nonce].pack('q<*')
49
+ p << [nonce].pack('Q<')
40
50
  p << Bitcoin.pack_var_int(short_ids.size)
41
51
  p << short_ids.map{|id|sprintf('%012x', id).htb.reverse}.join
42
52
  p << Bitcoin.pack_var_int(prefilled_txn.size)
43
- p << prefilled_txn.map(&:to_payload).join
53
+ prev = -1
54
+ prefilled_txn.each do |prefilled|
55
+ raise ArgumentError, 'prefilled tx indexes must be in ascending order.' unless prefilled.index > prev
56
+ p << PrefilledTx.new(prefilled.index - prev - 1, prefilled.tx).to_payload
57
+ prev = prefilled.index
58
+ end
44
59
  p
45
60
  end
46
61
 
@@ -48,8 +63,8 @@ module Bitcoin
48
63
  # @param [String] txid a transaction id
49
64
  # @return [Integer] 6 bytes short transaction id.
50
65
  def short_id(txid)
51
- hash = SipHash.digest(siphash_key, txid.htb.reverse).to_even_length_hex
52
- [hash].pack('H*')[2...8].bth.to_i(16)
66
+ # The low 6 bytes of the 64 bit SipHash output, which may have leading zero bytes.
67
+ SipHash.digest(siphash_key, txid.htb.reverse) & 0xffff_ffff_ffff
53
68
  end
54
69
 
55
70
  end
@@ -7,6 +7,10 @@ module Bitcoin
7
7
 
8
8
  COMMAND = 'headers'
9
9
 
10
+ # The most headers one message may carry. Bitcoin Core sends this many for one getheaders and
11
+ # rejects a message with more (MAX_HEADERS_RESULTS).
12
+ MAX_HEADERS_RESULTS = 2000
13
+
10
14
  # Array[Bitcoin::BlockHeader]
11
15
  attr_accessor :headers
12
16
 
@@ -14,13 +18,17 @@ module Bitcoin
14
18
  @headers = headers
15
19
  end
16
20
 
21
+ # @raise [Bitcoin::Message::Error] If the payload is truncated or carries more headers than allowed.
17
22
  def self.parse_from_payload(payload)
18
23
  buf = StringIO.new(payload)
19
24
  header_count = Bitcoin.unpack_var_int_from_io(buf)
25
+ raise Bitcoin::Message::Error, "Invalid headers payload. header count #{header_count} exceeds #{MAX_HEADERS_RESULTS}." if header_count > MAX_HEADERS_RESULTS
20
26
  h = new
21
27
  header_count.times do
22
- h.headers << Bitcoin::BlockHeader.parse_from_payload(buf.read(80))
23
- buf.read(1) # read tx count 0x00 (headers message doesn't include any tx.)
28
+ h.headers << Bitcoin::BlockHeader.parse_from_payload(Bitcoin.read_exact(buf, 80))
29
+ # The tx count is a CompactSize, which Bitcoin Core reads and ignores since a headers message
30
+ # carries no tx. Reading a single byte here misaligned every header behind a longer encoding.
31
+ Bitcoin.unpack_var_int_from_io(buf)
24
32
  end
25
33
  h
26
34
  end
@@ -5,14 +5,16 @@ module Bitcoin
5
5
  module HeadersParser
6
6
 
7
7
  def parse_from_payload(payload)
8
+ raise Bitcoin::Message::Error, 'Payload is too short.' if payload.bytesize < 4
8
9
  ver, payload = payload.unpack('Va*')
9
10
  size, payload = Bitcoin.unpack_var_int(payload)
11
+ raise Bitcoin::Message::Error, "#{self::COMMAND} locator size #{size} exceeds #{MAX_LOCATOR_SZ}." if size > MAX_LOCATOR_SZ
10
12
  hashes = []
11
13
  buf = StringIO.new(payload)
12
14
  size.times do
13
- hashes << buf.read(32).bth
15
+ hashes << Bitcoin.read_exact(buf, 32).bth
14
16
  end
15
- new(ver, hashes, buf.read(32).bth)
17
+ new(ver, hashes, Bitcoin.read_exact(buf, 32).bth)
16
18
  end
17
19
 
18
20
  def to_payload
@@ -6,10 +6,11 @@ module Bitcoin
6
6
 
7
7
  def parse_from_payload(payload)
8
8
  size, payload = Bitcoin.unpack_var_int(payload)
9
+ raise Bitcoin::Message::Error, "#{self::COMMAND} message size #{size} exceeds #{MAX_INV_SZ}." if size > MAX_INV_SZ
9
10
  buf = StringIO.new(payload)
10
11
  i = new
11
12
  size.times do
12
- i.inventories << Inventory.parse_from_payload(buf.read(36))
13
+ i.inventories << Inventory.parse_from_payload(Bitcoin.read_exact(buf, 36))
13
14
  end
14
15
  i
15
16
  end
@@ -10,6 +10,7 @@ module Bitcoin
10
10
  MSG_BLOCK = 2
11
11
  MSG_FILTERED_BLOCK = 3
12
12
  MSG_CMPCT_BLOCK = 4
13
+ MSG_WTX = 5 # BIP-339, the hash is a wtxid.
13
14
  MSG_WITNESS_TX = SEGWIT_FLAG | MSG_TX
14
15
  MSG_WITNESS_BLOCK = SEGWIT_FLAG | MSG_BLOCK
15
16
  MSG_FILTERED_WITNESS_BLOCK = SEGWIT_FLAG | MSG_FILTERED_BLOCK
@@ -17,8 +18,17 @@ module Bitcoin
17
18
  attr_accessor :identifier
18
19
  attr_accessor :hash
19
20
 
21
+ KNOWN_IDENTIFIERS = [MSG_TX, MSG_BLOCK, MSG_FILTERED_BLOCK, MSG_CMPCT_BLOCK, MSG_WTX, MSG_WITNESS_TX,
22
+ MSG_WITNESS_BLOCK, MSG_FILTERED_WITNESS_BLOCK].freeze
23
+
24
+ # A type which is not known is kept as it is, and is left for the receiver to ignore, as Bitcoin Core does.
25
+ # @param [Integer] identifier The type of the inventory, a 32 bit unsigned integer.
26
+ # @param [String] hash The hash with hex format.
27
+ # @raise [Bitcoin::Message::Error] If +identifier+ is not a 32 bit unsigned integer.
20
28
  def initialize(identifier, hash)
21
- raise Error, "invalid type identifier specified. identifier = #{identifier}" unless valid_identifier?(identifier)
29
+ unless identifier.is_a?(Integer) && identifier.between?(0, 0xffffffff)
30
+ raise Error, "invalid type identifier specified. identifier = #{identifier}"
31
+ end
22
32
  @identifier = identifier
23
33
  @hash = hash
24
34
  end
@@ -39,11 +49,10 @@ module Bitcoin
39
49
  [MSG_BLOCK, MSG_WITNESS_BLOCK, MSG_FILTERED_WITNESS_BLOCK].include?(identifier)
40
50
  end
41
51
 
42
- private
43
-
44
- def valid_identifier?(identifier)
45
- [MSG_TX, MSG_BLOCK, MSG_FILTERED_BLOCK, MSG_CMPCT_BLOCK, MSG_WITNESS_TX,
46
- MSG_WITNESS_BLOCK, MSG_FILTERED_WITNESS_BLOCK].include?(identifier)
52
+ # Whether the type of this inventory is one this library knows.
53
+ # @return [Boolean]
54
+ def known?
55
+ KNOWN_IDENTIFIERS.include?(identifier)
47
56
  end
48
57
 
49
58
  end
@@ -19,15 +19,15 @@ module Bitcoin
19
19
  def self.parse_from_payload(payload)
20
20
  m = new
21
21
  buf = StringIO.new(payload)
22
- m.header = Bitcoin::BlockHeader.parse_from_payload(buf.read(80))
23
- m.tx_count = buf.read(4).unpack1('V')
22
+ m.header = Bitcoin::BlockHeader.parse_from_payload(Bitcoin.read_exact(buf, 80))
23
+ m.tx_count = Bitcoin.read_exact(buf, 4).unpack1('V')
24
24
  hash_count = Bitcoin.unpack_var_int_from_io(buf)
25
25
  hash_count.times do
26
- m.hashes << buf.read(32).bth
26
+ m.hashes << Bitcoin.read_exact(buf, 32).bth
27
27
  end
28
28
  flag_count = Bitcoin.unpack_var_int_from_io(buf)
29
29
  # A sequence of bits packed eight in a byte with the least significant bit first.
30
- m.flags = buf.read(flag_count).bth
30
+ m.flags = Bitcoin.read_exact(buf, flag_count).bth
31
31
  # Reject a tree an untrusted peer could use to exhaust memory or CPU in #partial_tree.
32
32
  begin
33
33
  Bitcoin::PartialTree.validate!(m.tx_count, m.hashes, Bitcoin.byte_to_bit(m.flags.htb))
@@ -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