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
@@ -31,6 +31,18 @@ module Bitcoin
31
31
  raise NotImplementedError
32
32
  end
33
33
 
34
+ # Generating a control block from a nested tree is not supported yet.
35
+ # @raise [NotImplementedError]
36
+ def control_block(leaf)
37
+ raise NotImplementedError, 'CustomDepthBuilder does not support control_block yet.'
38
+ end
39
+
40
+ # Generating an inclusion proof from a nested tree is not supported yet.
41
+ # @raise [NotImplementedError]
42
+ def inclusion_proof(leaf)
43
+ raise NotImplementedError, 'CustomDepthBuilder does not support inclusion_proof yet.'
44
+ end
45
+
34
46
  private
35
47
 
36
48
  def merkle_root
data/lib/bitcoin/tx.rb CHANGED
@@ -9,7 +9,15 @@ module Bitcoin
9
9
  include Bitcoin::HexConverter
10
10
  include SilentPayment
11
11
 
12
- MAX_STANDARD_VERSION = 2
12
+ # Version 3 is TRUC (BIP-431).
13
+ MAX_STANDARD_VERSION = 3
14
+
15
+ # A tx smaller than this without the witness is not relayed, as it could be confused with a 64-byte merkle tree node.
16
+ MIN_STANDARD_TX_NONWITNESS_SIZE = 65
17
+
18
+ # The number of dust outputs a standard tx may have. Bitcoin Core relays a tx with one dust output only when
19
+ # the tx pays no fee and is spent by a child in the same package (ephemeral dust), which this class cannot see.
20
+ MAX_DUST_OUTPUTS_PER_TX = 1
13
21
 
14
22
  # The maximum weight for transactions we're willing to relay/mine
15
23
  MAX_STANDARD_TX_WEIGHT = 400000
@@ -50,18 +58,20 @@ module Bitcoin
50
58
  def self.parse_from_payload(payload, non_witness: false, strict: false)
51
59
  buf = payload.is_a?(String) ? StringIO.new(payload) : payload
52
60
  tx = new
53
- tx.version = buf.read(4).unpack1('V')
61
+ tx.version = Bitcoin.read_exact(buf, 4).unpack1('V')
54
62
 
55
63
  in_count = Bitcoin.unpack_var_int_from_io(buf)
56
64
  has_witness = false
57
65
  if in_count.zero? && !non_witness
58
66
  tx.marker = 0
59
- tx.flag = buf.read(1).unpack1('c')
67
+ tx.flag = Bitcoin.read_exact(buf, 1).unpack1('C')
60
68
  if tx.flag.zero?
61
69
  buf.pos -= 1
62
70
  else
63
71
  in_count = Bitcoin.unpack_var_int_from_io(buf)
64
- has_witness = true
72
+ # Bit 0 of the flag announces the witnesses. Bitcoin Core rejects any other bit after reading
73
+ # the inputs and outputs, see below.
74
+ has_witness = tx.flag & 1 == 1
65
75
  end
66
76
  end
67
77
 
@@ -78,10 +88,14 @@ module Bitcoin
78
88
  in_count.times do |i|
79
89
  tx.inputs[i].script_witness = Bitcoin::ScriptWitness.parse_from_payload(buf)
80
90
  end
91
+ # A tx whose witnesses are all empty is serialized without them, so this encoding has no
92
+ # txid-preserving reading and Bitcoin Core rejects it.
93
+ raise ArgumentError, 'Superfluous witness record.' unless tx.witness?
81
94
  end
95
+ raise ArgumentError, 'Unknown transaction optional data.' if tx.flag && (tx.flag & ~1) != 0
82
96
 
83
97
  raise ArgumentError, 'Transaction has unexpected data.' if strict && (buf.pos + 4) != buf.length
84
- tx.lock_time = buf.read(4).unpack1('V')
98
+ tx.lock_time = Bitcoin.read_exact(buf, 4).unpack1('V')
85
99
  tx
86
100
  end
87
101
 
@@ -111,9 +125,10 @@ module Bitcoin
111
125
 
112
126
  # get the witness commitment of coinbase tx.
113
127
  # if this tx does not coinbase or not have commitment, return nil.
128
+ # When more than one output looks like a commitment, the last one counts, as in Bitcoin Core.
114
129
  def witness_commitment
115
130
  return nil unless coinbase_tx?
116
- outputs.each do |output|
131
+ outputs.reverse_each do |output|
117
132
  commitment = output.script_pubkey.witness_commitment
118
133
  return commitment if commitment
119
134
  end
@@ -159,10 +174,12 @@ module Bitcoin
159
174
  inputs.map { |i| i.script_witness.to_payload }.join
160
175
  end
161
176
 
162
- # check this tx is standard.
177
+ # Whether this tx is standard, as IsStandardTx in Bitcoin Core judges it, and at least
178
+ # MIN_STANDARD_TX_NONWITNESS_SIZE bytes without the witness, as the mempool of Bitcoin Core requires.
163
179
  def standard?
164
- return false if version > MAX_STANDARD_VERSION
180
+ return false if version < 1 || version > MAX_STANDARD_VERSION
165
181
  return false if weight > MAX_STANDARD_TX_WEIGHT
182
+ return false if serialize_old_format.bytesize < MIN_STANDARD_TX_NONWITNESS_SIZE
166
183
  inputs.each do |i|
167
184
  # Biggest 'standard' txin is a 15-of-15 P2SH multisig with compressed keys (remember the 520 byte limit on redeemScript size).
168
185
  # That works out to a (15*(33+1))+3=513 byte redeemScript, 513+1+15*(73+1)+3=1627
@@ -171,15 +188,19 @@ module Bitcoin
171
188
  return false if i.script_sig.size > 1650
172
189
  return false unless i.script_sig.push_only?
173
190
  end
174
- data_count = 0
191
+ # The OP_RETURN outputs share MAX_OP_RETURN_RELAY bytes, however many of them there are.
192
+ data_bytes_left = MAX_OP_RETURN_RELAY
193
+ dust_count = 0
175
194
  outputs.each do |o|
176
195
  return false unless o.script_pubkey.standard?
177
- data_count += 1 if o.script_pubkey.op_return?
178
- # TODO add non P2SH multisig relay(permitbaremultisig)
179
- return false if o.dust?
196
+ if o.script_pubkey.op_return?
197
+ return false if o.script_pubkey.size > data_bytes_left
198
+ data_bytes_left -= o.script_pubkey.size
199
+ elsif o.dust?
200
+ dust_count += 1
201
+ end
180
202
  end
181
- return false if data_count > 1
182
- true
203
+ dust_count <= MAX_DUST_OUTPUTS_PER_TX
183
204
  end
184
205
 
185
206
  # The serialized transaction size
data/lib/bitcoin/tx_in.rb CHANGED
@@ -39,9 +39,9 @@ module Bitcoin
39
39
  if sig_length == 0
40
40
  i.script_sig = Bitcoin::Script.new
41
41
  else
42
- i.script_sig = Script.parse_from_payload(buf.read(sig_length))
42
+ i.script_sig = Script.parse_from_payload(Bitcoin.read_exact(buf, sig_length))
43
43
  end
44
- i.sequence = buf.read(4).unpack1('V')
44
+ i.sequence = Bitcoin.read_exact(buf, 4).unpack1('V')
45
45
  i
46
46
  end
47
47
 
@@ -16,9 +16,9 @@ module Bitcoin
16
16
 
17
17
  def self.parse_from_payload(payload)
18
18
  buf = payload.is_a?(String) ? StringIO.new(payload) : payload
19
- value = buf.read(8).unpack1('Q<')
19
+ value = Bitcoin.read_exact(buf, 8).unpack1('Q<')
20
20
  script_size = Bitcoin.unpack_var_int_from_io(buf)
21
- new(value: value, script_pubkey: Script.parse_from_payload(buf.read(script_size)))
21
+ new(value: value, script_pubkey: Script.parse_from_payload(Bitcoin.read_exact(buf, script_size)))
22
22
  end
23
23
 
24
24
  def to_payload
data/lib/bitcoin/util.rb CHANGED
@@ -8,56 +8,75 @@ module Bitcoin
8
8
  # Bitcoin.pack_var_int(5)
9
9
  module Util
10
10
 
11
+ # Serialize +payload+ as a CompactSize length followed by its bytes.
12
+ # @param [String] payload The bytes, whatever encoding the string carries.
11
13
  def pack_var_string(payload)
12
- pack_var_int(payload.bytesize) + payload
14
+ pack_var_int(payload.bytesize) + payload.b
13
15
  end
14
16
 
15
17
  def unpack_var_string(payload)
16
18
  size, payload = unpack_var_int(payload)
17
- size && size > 0 ? payload.unpack("a#{size}a*") : ['', payload.to_s]
19
+ raise ArgumentError, 'Variable length string is truncated.' if payload.bytesize < size
20
+ [payload.byteslice(0, size), payload.byteslice(size..-1)]
18
21
  end
19
22
 
23
+ # Read exactly +size+ bytes from +buf+.
24
+ # @param [StringIO] buf
25
+ # @param [Integer] size
26
+ # @return [String] the bytes read.
27
+ # @raise [ArgumentError] If +buf+ ends before +size+ bytes.
28
+ def read_exact(buf, size)
29
+ data = size.zero? ? ''.b : buf.read(size)
30
+ raise ArgumentError, 'Payload is truncated.' if data.nil? || data.bytesize != size
31
+ data
32
+ end
33
+
34
+ # Encode +i+ as a CompactSize.
35
+ # @param [Integer] i
36
+ # @return [String] the CompactSize with binary format.
37
+ # @raise [ArgumentError] If +i+ is not an integer from 0 to 2**64 - 1, which have no CompactSize.
20
38
  def pack_var_int(i)
39
+ raise ArgumentError, 'CompactSize must be an integer from 0 to 2**64 - 1.' unless i.is_a?(Integer) && i.between?(0, 0xffffffffffffffff)
21
40
  if i < 0xfd
22
41
  [i].pack('C')
23
42
  elsif i <= 0xffff
24
43
  [0xfd, i].pack('Cv')
25
44
  elsif i <= 0xffffffff
26
45
  [0xfe, i].pack('CV')
27
- elsif i <= 0xffffffffffffffff
28
- [0xff, i].pack('CQ<')
29
46
  else
30
- raise "int(#{i}) too large!"
47
+ [0xff, i].pack('CQ<')
31
48
  end
32
49
  end
33
50
 
34
51
  # @return an integer for a valid payload, otherwise nil
52
+ # Decode a CompactSize at the head of +payload+.
53
+ # @return [Array(Integer, String)] the value and the rest of the payload.
54
+ # @raise [ArgumentError] If the encoding is truncated or not canonical.
35
55
  def unpack_var_int(payload)
36
- case payload.unpack1('C')
37
- when 0xfd
38
- payload.unpack('xva*')
39
- when 0xfe
40
- payload.unpack('xVa*')
41
- when 0xff
42
- payload.unpack('xQ<a*')
43
- else
44
- payload.unpack('Ca*')
45
- end
56
+ buf = StringIO.new(payload)
57
+ value = unpack_var_int_from_io(buf)
58
+ [value, buf.read.to_s]
46
59
  end
47
60
 
48
61
  # @return an integer for a valid payload, otherwise nil
62
+ # Decode a CompactSize from +buf+. As ReadCompactSize in Bitcoin Core, a value encoded with more
63
+ # bytes than it needs is rejected, so every value has exactly one encoding.
64
+ # @param [StringIO] buf
65
+ # @return [Integer]
66
+ # @raise [ArgumentError] If the encoding is truncated or not canonical.
49
67
  def unpack_var_int_from_io(buf)
50
- uchar = buf.read(1)&.unpack1('C')
51
- case uchar
52
- when 0xfd
53
- buf.read(2)&.unpack1('v')
54
- when 0xfe
55
- buf.read(4)&.unpack1('V')
56
- when 0xff
57
- buf.read(8)&.unpack1('Q<')
58
- else
59
- uchar
60
- end
68
+ first = buf.read(1)
69
+ raise ArgumentError, 'CompactSize is truncated.' if first.nil?
70
+ uchar = first.unpack1('C')
71
+ size, format, minimum = case uchar
72
+ when 0xfd then [2, 'v', 0xfd]
73
+ when 0xfe then [4, 'V', 0x10000]
74
+ when 0xff then [8, 'Q<', 0x100000000]
75
+ else return uchar
76
+ end
77
+ value = read_exact(buf, size).unpack1(format)
78
+ raise ArgumentError, 'CompactSize is not canonical.' if value < minimum
79
+ value
61
80
  end
62
81
 
63
82
  def pack_boolean(b)
@@ -66,6 +85,7 @@ module Bitcoin
66
85
 
67
86
  def unpack_boolean(payload)
68
87
  data, payload = payload.unpack('Ca*')
88
+ raise ArgumentError, 'Payload is truncated.' if data.nil?
69
89
  [(data.zero? ? false : true), payload]
70
90
  end
71
91
 
@@ -117,13 +137,14 @@ module Bitcoin
117
137
  # decode Base58 check encoding address.
118
138
  # @param [String] addr address.
119
139
  # @return [Array] hex and address version
140
+ # @raise [ArgumentError] If +addr+ is not a P2PKH or P2SH address of the current chain.
120
141
  def decode_base58_address(addr)
121
142
  hex = Base58.decode(addr)
122
143
  if hex.size == 50 && calc_checksum(hex[0...-8]) == hex[-8..-1]
123
- raise 'Invalid version bytes.' unless [Bitcoin.chain_params.address_version, Bitcoin.chain_params.p2sh_version].include?(hex[0..1])
144
+ raise ArgumentError, 'Invalid version bytes.' unless [Bitcoin.chain_params.address_version, Bitcoin.chain_params.p2sh_version].include?(hex[0..1])
124
145
  [hex[2...-8], hex[0..1]]
125
146
  else
126
- raise 'Invalid address.'
147
+ raise ArgumentError, 'Invalid address.'
127
148
  end
128
149
  end
129
150
 
@@ -38,8 +38,11 @@ module Bitcoin
38
38
  return state.DoS(100, reject_code: Message::Reject::CODE_INVALID, reject_reason: 'bad-cb-length')
39
39
  end
40
40
  else
41
+ # Only the null prevout (zero hash and index 0xffffffff) is rejected, as COutPoint::IsNull in
42
+ # Bitcoin Core. An index of -1 is the default of OutPoint and serializes as 0xffffffff.
43
+ null_payload = OutPoint.create_coinbase_outpoint.to_payload
41
44
  tx.inputs.each do |i|
42
- if i.out_point.nil? || !i.out_point.valid?
45
+ if i.out_point.nil? || i.out_point.to_payload == null_payload
43
46
  return state.DoS(10, reject_code: Message::Reject::CODE_INVALID, reject_reason: 'bad-txns-prevout-null')
44
47
  end
45
48
  end
@@ -47,33 +50,82 @@ module Bitcoin
47
50
  true
48
51
  end
49
52
 
50
- # check proof of work
53
+ # Check the proof of work of a block header, as CheckBlockHeader in Bitcoin Core.
54
+ # @param [Bitcoin::BlockHeader] header
55
+ # @param [Bitcoin::ValidationState] state
56
+ # @return [Boolean]
51
57
  def check_block_header(header, state)
52
- header.block_hash
53
- header.bits
54
-
58
+ unless header.valid_pow?
59
+ return state.DoS(50, reject_code: Message::Reject::CODE_INVALID, reject_reason: 'high-hash', debug_message: 'proof of work failed')
60
+ end
61
+ true
55
62
  end
56
63
 
64
+ # Check a block without any context, as CheckBlock in Bitcoin Core: the header, the merkle root,
65
+ # the size limits, the coinbase, every tx and the legacy sigop count.
66
+ # @param [Bitcoin::Block] block
67
+ # @param [Bitcoin::ValidationState] state
68
+ # @return [Boolean]
57
69
  def check_block(block, state)
58
- # check block header
59
70
  return false unless check_block_header(block.header, state)
60
71
 
61
- # check merkle root
72
+ txs = block.transactions
73
+ # A block without any tx has no merkle root to check, and fails the size limits below.
74
+ unless txs.empty?
75
+ if block.calculate_merkle_root != block.header.merkle_root
76
+ return state.DoS(100, reject_code: Message::Reject::CODE_INVALID, reject_reason: 'bad-txnmrklroot', corruption_in: true, debug_message: 'hashMerkleRoot mismatch')
77
+ end
78
+ # CVE-2012-2459
79
+ if block.merkle_root_mutated?
80
+ return state.DoS(100, reject_code: Message::Reject::CODE_INVALID, reject_reason: 'bad-txns-duplicate', corruption_in: true, debug_message: 'duplicate transaction')
81
+ end
82
+ end
83
+
84
+ # Size limits, without the witness, which has not been checked for malleability.
85
+ if txs.empty? || txs.size * WITNESS_SCALE_FACTOR > MAX_BLOCK_WEIGHT || block.stripped_size * WITNESS_SCALE_FACTOR > MAX_BLOCK_WEIGHT
86
+ return state.DoS(100, reject_code: Message::Reject::CODE_INVALID, reject_reason: 'bad-blk-length', debug_message: 'size limits failed')
87
+ end
88
+
89
+ unless txs.first.coinbase_tx?
90
+ return state.DoS(100, reject_code: Message::Reject::CODE_INVALID, reject_reason: 'bad-cb-missing', debug_message: 'first tx is not coinbase')
91
+ end
92
+ if txs[1..].any?(&:coinbase_tx?)
93
+ return state.DoS(100, reject_code: Message::Reject::CODE_INVALID, reject_reason: 'bad-cb-multiple', debug_message: 'more than one coinbase')
94
+ end
62
95
 
63
- # size limits
96
+ txs.each { |tx| return false unless check_tx(tx, state) }
64
97
 
65
- # first tx is coinbase?
98
+ sigops = txs.sum { |tx| legacy_sigop_count(tx) }
99
+ if sigops * WITNESS_SCALE_FACTOR > MAX_BLOCK_SIGOPS_COST
100
+ return state.DoS(100, reject_code: Message::Reject::CODE_INVALID, reject_reason: 'bad-blk-sigops', debug_message: 'out-of-bounds SigOpCount')
101
+ end
102
+ true
103
+ end
66
104
 
67
- # check tx count
105
+ private
68
106
 
69
- # check sigop count
107
+ # Count the sigops of the scriptSigs and scriptPubkeys of +tx+, as GetLegacySigOpCount in Bitcoin Core:
108
+ # a CHECKMULTISIG counts as 20 whatever its key count.
109
+ def legacy_sigop_count(tx)
110
+ tx.inputs.sum { |i| script_sigop_count(i.script_sig) } + tx.outputs.sum { |o| script_sigop_count(o.script_pubkey) }
111
+ end
112
+
113
+ def script_sigop_count(script)
114
+ script.chunks.sum do |chunk|
115
+ next 0 if chunk.empty?
116
+ case chunk.opcode
117
+ when Opcodes::OP_CHECKSIG, Opcodes::OP_CHECKSIGVERIFY then 1
118
+ when Opcodes::OP_CHECKMULTISIG, Opcodes::OP_CHECKMULTISIGVERIFY then MAX_PUBKEYS_PER_MULTISIG
119
+ else 0
120
+ end
121
+ end
70
122
  end
71
123
 
72
124
  end
73
125
 
74
126
  class ValidationState
75
127
 
76
- MODE = {valid: 0, invlid: 1, error: 2}
128
+ MODE = {valid: 0, invalid: 1, error: 2}
77
129
 
78
130
  attr_accessor :mode
79
131
  attr_accessor :n_dos
@@ -1,3 +1,3 @@
1
1
  module Bitcoin
2
- VERSION = "1.14.0"
2
+ VERSION = "1.16.0"
3
3
  end
@@ -20,21 +20,30 @@ module Bitcoin
20
20
  validate_params!(account_key, purpose, index)
21
21
  @purpose = purpose
22
22
  @index = index
23
- @name = name
23
+ @name = normalize_name(name)
24
24
  @receive_depth = 0
25
25
  @change_depth = 0
26
26
  @lookahead = 10
27
27
  @account_key = account_key
28
28
  end
29
29
 
30
+ # Parse an account from the payload made by #to_payload.
31
+ # @param [String] payload
32
+ # @return [Bitcoin::Wallet::Account]
33
+ # @raise [ArgumentError] If the payload is truncated, has data after the account, or holds invalid values.
30
34
  def self.parse_from_payload(payload)
35
+ raise ArgumentError, 'payload must be a String.' unless payload.is_a?(String)
31
36
  buf = StringIO.new(payload)
32
- account_key = Bitcoin::ExtPubkey.parse_from_payload(buf.read(78))
33
- payload = buf.read
34
- name, payload = Bitcoin.unpack_var_string(payload)
35
- name = name.force_encoding('utf-8')
36
- purpose, index, receive_depth, change_depth, lookahead = payload.unpack('V*')
37
- a = Account.new(account_key, purpose, index, name)
37
+ account_key = Bitcoin::ExtPubkey.parse_from_payload(Bitcoin.read_exact(buf, 78))
38
+ name_size = Bitcoin.unpack_var_int_from_io(buf)
39
+ raise ArgumentError, 'Payload is truncated.' if name_size.nil?
40
+ name = Bitcoin.read_exact(buf, name_size).force_encoding('utf-8')
41
+ purpose, index, receive_depth, change_depth, lookahead = Bitcoin.read_exact(buf, 20).unpack('V5')
42
+ raise ArgumentError, 'Account payload has data after the account.' unless buf.eof?
43
+ a = Account.new(account_key, purpose, index)
44
+ # A wallet made by an older version may hold a name which is not valid UTF-8. It is kept as stored,
45
+ # so that the wallet stays usable and #to_payload writes the same bytes back.
46
+ a.instance_variable_set(:@name, name)
38
47
  a.receive_depth = receive_depth
39
48
  a.change_depth = change_depth
40
49
  a.lookahead = lookahead
@@ -43,7 +52,7 @@ module Bitcoin
43
52
 
44
53
  def to_payload
45
54
  payload = account_key.to_payload
46
- payload << Bitcoin.pack_var_string(name.unpack1('H*').htb)
55
+ payload << Bitcoin.pack_var_string(name.b)
47
56
  payload << [purpose, index, receive_depth, change_depth, lookahead].pack('V*')
48
57
  payload
49
58
  end
@@ -139,11 +148,26 @@ module Bitcoin
139
148
  account_key.derive(branch).derive(address_index)
140
149
  end
141
150
 
151
+ # The name is stored as its UTF-8 bytes and read back as UTF-8, so that it has to be valid UTF-8.
152
+ # @raise [ArgumentError] If +name+ is not a String which converts to valid UTF-8.
153
+ def normalize_name(name)
154
+ raise ArgumentError, 'name must be a String.' unless name.is_a?(String)
155
+ utf8 = name.encode(Encoding::UTF_8)
156
+ raise ArgumentError, 'name must be valid UTF-8.' unless utf8.valid_encoding?
157
+ utf8
158
+ rescue EncodingError
159
+ raise ArgumentError, 'name must be valid UTF-8.'
160
+ end
161
+
162
+ # @raise [ArgumentError] If a parameter is invalid or they do not match each other.
142
163
  def validate_params!(account_key, purpose, index)
143
- raise 'account_key must be an instance of Bitcoin::ExtPubkey.' unless account_key.is_a?(Bitcoin::ExtPubkey)
144
- raise 'Account key and index does not match.' unless account_key.number == (index + Bitcoin::HARDENED_THRESHOLD)
164
+ raise ArgumentError, 'account_key must be an instance of Bitcoin::ExtPubkey.' unless account_key.is_a?(Bitcoin::ExtPubkey)
165
+ # Any BIP-44 style purpose is accepted, as an older version did. Its account key has to match it below.
166
+ raise ArgumentError, 'purpose must be an integer from 0 to 2**31 - 1.' unless purpose.is_a?(Integer) && purpose.between?(0, Bitcoin::HARDENED_THRESHOLD - 1)
167
+ raise ArgumentError, 'index must be an integer from 0 to 2**31 - 1.' unless index.is_a?(Integer) && index.between?(0, Bitcoin::HARDENED_THRESHOLD - 1)
168
+ raise ArgumentError, 'Account key and index does not match.' unless account_key.number == (index + Bitcoin::HARDENED_THRESHOLD)
145
169
  version_bytes = Bitcoin::ExtPubkey.version_from_purpose(purpose + Bitcoin::HARDENED_THRESHOLD)
146
- raise 'The purpose and the account key do not match.' unless account_key.version == version_bytes
170
+ raise ArgumentError, 'The purpose and the account key do not match.' unless account_key.version == version_bytes
147
171
  end
148
172
 
149
173
  end
@@ -10,6 +10,10 @@ module Bitcoin
10
10
 
11
11
  VERSION = 1
12
12
 
13
+ # A wallet id names the directory +wallet<id>+ under the path prefix, so that it may hold only
14
+ # letters, digits, '_' and '-'. A '/' or '..' would put the wallet outside the prefix.
15
+ WALLET_ID_PATTERN = /\A[A-Za-z0-9_-]+\z/
16
+
13
17
  # get wallet dir path
14
18
  def self.default_path_prefix
15
19
  "#{Bitcoin.base_dir}/db/wallet/"
@@ -17,9 +21,10 @@ module Bitcoin
17
21
 
18
22
  # Create new wallet. If wallet already exist, throw error.
19
23
  # The wallet generates a seed using SecureRandom and store to db at initialization.
20
- # @param [String] wallet_id new wallet id.
24
+ # @param [Integer, String] wallet_id new wallet id, which consists of letters, digits, '_' and '-'.
21
25
  # @param [String] path_prefix wallet file path prefix.
22
26
  # @return [Bitcoin::Wallet::Base] the wallet
27
+ # @raise [ArgumentError] If +wallet_id+ is invalid or the wallet already exists.
23
28
  def self.create(wallet_id = 1, path_prefix = default_path_prefix, purpose = Account::PURPOSE_TYPE[:native_segwit])
24
29
  raise ArgumentError, "wallet_id : #{wallet_id} already exist." if self.exist?(wallet_id, path_prefix)
25
30
  w = self.new(wallet_id, path_prefix)
@@ -33,6 +38,7 @@ module Bitcoin
33
38
 
34
39
  # load wallet with specified +wallet_id+
35
40
  # @return [Bitcoin::Wallet::Base] the wallet
41
+ # @raise [ArgumentError] If +wallet_id+ is invalid or the wallet does not exist.
36
42
  def self.load(wallet_id, path_prefix = default_path_prefix)
37
43
  raise ArgumentError, "wallet_id : #{wallet_id} dose not exist." unless self.exist?(wallet_id, path_prefix)
38
44
  self.new(wallet_id, path_prefix)
@@ -45,12 +51,14 @@ module Bitcoin
45
51
  end
46
52
 
47
53
  # get current wallet
54
+ # @return [Bitcoin::Wallet::Base] the wallet of the first wallet dir, or nil if there is none.
55
+ # Its wallet_id is an Integer if it consists of digits only, otherwise a String.
48
56
  def self.current_wallet(path_prefix = default_path_prefix)
49
- path = wallet_paths(path_prefix).first # TODO default wallet selection
50
- return nil unless path
51
- path.slice!(path_prefix + 'wallet')
52
- path.slice!('/')
53
- self.load(path.to_i, path_prefix)
57
+ # TODO default wallet selection
58
+ ids = wallet_paths(path_prefix).map { |path| File.basename(path).delete_prefix('wallet') }
59
+ id = ids.find { |i| i.match?(WALLET_ID_PATTERN) } # A dir such as "wallet/" has no wallet id.
60
+ return nil unless id
61
+ self.load(id.match?(/\A[0-9]+\z/) ? id.to_i : id, path_prefix)
54
62
  end
55
63
 
56
64
  # get account list based on BIP-44
@@ -67,10 +75,12 @@ module Bitcoin
67
75
 
68
76
  # create new account
69
77
  # @param [Integer] purpose BIP44's purpose.
70
- # @param [String] name a account name.
78
+ # @param [String] name a account name, which has to be unique in the wallet whatever the purpose.
71
79
  # @return [Bitcoin::Wallet::Account]
80
+ # @raise [ArgumentError] If an account with +name+ already exists.
72
81
  def create_account(purpose = Account::PURPOSE_TYPE[:native_segwit], name)
73
- raise ArgumentError.new('Account already exists.') if find_account(name, purpose)
82
+ # An account is looked up by its name alone, e.g. in #generate_new_address.
83
+ raise ArgumentError.new('Account already exists.') if find_account(name)
74
84
  index = accounts.size
75
85
  path = "m/#{purpose}'/#{Bitcoin.chain_params.bip44_coin_type}'/#{index}'"
76
86
  account_key = master_key.derive(path).ext_pubkey
@@ -82,18 +92,23 @@ module Bitcoin
82
92
 
83
93
  # get wallet balance.
84
94
  # @param [Bitcoin::Wallet::Account] account a account in the wallet.
95
+ # @return [Integer] the balance in satoshis.
85
96
  def get_balance(account)
86
97
  # TODO get from utxo db.
87
- 0.00000000
98
+ 0
88
99
  end
89
100
 
90
101
  # create new bitcoin address for receiving payments.
91
102
  # @param [String] account_name an account name.
103
+ # @param [Integer] purpose BIP44's purpose of the account, which is required only when a wallet
104
+ # created by an older version has accounts of the same name with different purposes.
92
105
  # @return [String] generated address.
93
- def generate_new_address(account_name)
94
- account = find_account(account_name)
95
- raise ArgumentError.new('Account does not exist.') unless account
96
- account.create_receive.addr
106
+ # @raise [ArgumentError] If no account, or more than one account, matches.
107
+ def generate_new_address(account_name, purpose = nil)
108
+ candidates = accounts(purpose).select{|a| a.name == account_name}
109
+ raise ArgumentError.new('Account does not exist.') if candidates.empty?
110
+ raise ArgumentError.new('More than one account has the name, specify the purpose.') if candidates.size > 1
111
+ candidates.first.create_receive.addr
97
112
  end
98
113
 
99
114
  # get wallet version.
@@ -119,10 +134,12 @@ module Bitcoin
119
134
  db.register_master_key(master_key)
120
135
  end
121
136
 
122
- # decrypt wallet
137
+ # decrypt wallet, and store its seed unencrypted.
123
138
  # @param [String] passphrase the wallet passphrase
139
+ # @raise [ArgumentError] If +passphrase+ is wrong.
124
140
  def decrypt(passphrase)
125
-
141
+ master_key.decrypt(passphrase)
142
+ db.register_master_key(master_key)
126
143
  end
127
144
 
128
145
  # wallet information
@@ -140,16 +157,24 @@ module Bitcoin
140
157
  private
141
158
 
142
159
  def initialize(wallet_id, path_prefix)
160
+ self.class.validate_wallet_id!(wallet_id)
143
161
  @path = "#{path_prefix}wallet#{wallet_id}/"
144
162
  @db = Bitcoin::Wallet::DB.new(@path)
145
163
  @wallet_id = wallet_id
146
164
  end
147
165
 
148
166
  def self.exist?(wallet_id, path_prefix)
167
+ validate_wallet_id!(wallet_id)
149
168
  path = "#{path_prefix}wallet#{wallet_id}"
150
169
  Dir.exist?(path)
151
170
  end
152
171
 
172
+ def self.validate_wallet_id!(wallet_id)
173
+ valid = (wallet_id.is_a?(Integer) && wallet_id >= 0) ||
174
+ (wallet_id.is_a?(String) && wallet_id.match?(WALLET_ID_PATTERN))
175
+ raise ArgumentError, "wallet_id must be a non-negative integer or a string of letters, digits, '_' and '-'." unless valid
176
+ end
177
+
153
178
  # find account using +account_name+
154
179
  def find_account(account_name, purpose = nil)
155
180
  accounts(purpose).find{|a| a.name == account_name}
@@ -7,7 +7,23 @@ module Bitcoin
7
7
  attr_reader :value
8
8
  attr_reader :script_pubkey
9
9
 
10
+ # @param [String] tx_hash The transaction hash with hex format.
11
+ # @param [Integer] index The index of the output, 0 to 2**32 - 1.
12
+ # @param [Integer] value The value in satoshi, 0 to 2**64 - 1.
13
+ # @param [Bitcoin::Script] script_pubkey The scriptPubKey of the output.
14
+ # @param [Integer, nil] block_height The height of the block which holds the tx, 1 to 2**32 - 1, or nil
15
+ # while the tx is unconfirmed.
16
+ # @raise [ArgumentError] If a value is out of range, since the serialization would silently change it.
10
17
  def initialize(tx_hash, index, value, script_pubkey, block_height = nil)
18
+ unless tx_hash.is_a?(String) && tx_hash.valid_hex? && tx_hash.length == 64
19
+ raise ArgumentError, 'tx_hash must be a 32 byte hash with hex format.'
20
+ end
21
+ raise ArgumentError, 'index must be an integer from 0 to 2**32 - 1.' unless index.is_a?(Integer) && index.between?(0, 0xffffffff)
22
+ raise ArgumentError, 'value must be an integer from 0 to 2**64 - 1.' unless value.is_a?(Integer) && value.between?(0, 0xffffffffffffffff)
23
+ raise ArgumentError, 'script_pubkey must be a Bitcoin::Script.' unless script_pubkey.is_a?(Bitcoin::Script)
24
+ unless block_height.nil? || (block_height.is_a?(Integer) && block_height.between?(1, 0xffffffff))
25
+ raise ArgumentError, 'block_height must be nil or an integer from 1 to 2**32 - 1.'
26
+ end
11
27
  @tx_hash = tx_hash
12
28
  @index = index
13
29
  @block_height = block_height
@@ -15,14 +31,16 @@ module Bitcoin
15
31
  @script_pubkey = script_pubkey
16
32
  end
17
33
 
34
+ # @param [String] payload The serialization made by #to_payload.
35
+ # @return [Bitcoin::Wallet::Utxo, nil] nil if +payload+ is nil.
36
+ # @raise [ArgumentError] If +payload+ is truncated or has bytes after the script.
18
37
  def self.parse_from_payload(payload)
19
38
  return nil if payload.nil?
20
-
21
- tx_hash, index, block_height, value, payload = payload.unpack('H64VVQ<a*')
22
-
23
39
  buf = StringIO.new(payload)
40
+ tx_hash, index, block_height, value = Bitcoin.read_exact(buf, 48).unpack('H64VVQ<')
24
41
  script_size = Bitcoin.unpack_var_int_from_io(buf)
25
- script_pubkey = Bitcoin::Script.parse_from_payload(buf.read(script_size));
42
+ script_pubkey = Bitcoin::Script.parse_from_payload(Bitcoin.read_exact(buf, script_size))
43
+ raise ArgumentError, 'Utxo payload has data after the script.' unless buf.eof?
26
44
  new(tx_hash, index, value, script_pubkey, block_height == 0 ? nil : block_height )
27
45
  end
28
46