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.
- checksums.yaml +4 -4
- data/bitcoinrb.gemspec +5 -4
- data/lib/bitcoin/bip321_uri.rb +95 -32
- data/lib/bitcoin/bip324/ell_swift_pubkey.rb +3 -1
- data/lib/bitcoin/bip324/fs_chacha20.rb +3 -0
- data/lib/bitcoin/bip324.rb +1 -1
- data/lib/bitcoin/bip85_entropy.rb +79 -29
- data/lib/bitcoin/bit_stream.rb +2 -2
- data/lib/bitcoin/block.rb +46 -7
- data/lib/bitcoin/block_filter.rb +3 -2
- data/lib/bitcoin/block_header.rb +31 -8
- data/lib/bitcoin/bloom_filter.rb +5 -2
- data/lib/bitcoin/chainparams/regtest.yml +0 -1
- data/lib/bitcoin/chainparams/signet.yml +1 -1
- data/lib/bitcoin/constants.rb +13 -3
- data/lib/bitcoin/descriptor/combo.rb +13 -1
- data/lib/bitcoin/descriptor/expression.rb +35 -15
- data/lib/bitcoin/descriptor/multi.rb +9 -1
- data/lib/bitcoin/descriptor/multi_a.rb +4 -0
- data/lib/bitcoin/descriptor/musig.rb +1 -0
- data/lib/bitcoin/descriptor/pk.rb +7 -0
- data/lib/bitcoin/descriptor/pkh.rb +5 -0
- data/lib/bitcoin/descriptor/raw.rb +1 -1
- data/lib/bitcoin/descriptor/sh.rb +1 -0
- data/lib/bitcoin/descriptor/sp.rb +2 -2
- data/lib/bitcoin/descriptor/tr.rb +29 -3
- data/lib/bitcoin/descriptor/wpkh.rb +1 -0
- data/lib/bitcoin/descriptor/wsh.rb +1 -0
- data/lib/bitcoin/descriptor.rb +59 -35
- data/lib/bitcoin/ext/array_ext.rb +3 -0
- data/lib/bitcoin/ext/ecdsa.rb +7 -1
- data/lib/bitcoin/ext_key.rb +10 -0
- data/lib/bitcoin/gcs_filter.rb +21 -6
- data/lib/bitcoin/key.rb +66 -14
- data/lib/bitcoin/key_path.rb +8 -5
- data/lib/bitcoin/message/addr.rb +1 -0
- data/lib/bitcoin/message/addr_v2.rb +1 -0
- data/lib/bitcoin/message/base.rb +8 -1
- data/lib/bitcoin/message/block.rb +4 -8
- data/lib/bitcoin/message/block_transaction_request.rb +11 -8
- data/lib/bitcoin/message/block_transactions.rb +2 -1
- data/lib/bitcoin/message/cf_parser.rb +1 -0
- data/lib/bitcoin/message/cfcheckpt.rb +3 -3
- data/lib/bitcoin/message/cfheaders.rb +5 -4
- data/lib/bitcoin/message/cfilter.rb +3 -3
- data/lib/bitcoin/message/error.rb +3 -1
- data/lib/bitcoin/message/fee_filter.rb +1 -0
- data/lib/bitcoin/message/filter_add.rb +3 -1
- data/lib/bitcoin/message/filter_load.rb +4 -4
- data/lib/bitcoin/message/get_cfcheckpt.rb +1 -0
- data/lib/bitcoin/message/header_and_short_ids.rb +26 -11
- data/lib/bitcoin/message/headers.rb +10 -2
- data/lib/bitcoin/message/headers_parser.rb +4 -2
- data/lib/bitcoin/message/inventories_parser.rb +2 -1
- data/lib/bitcoin/message/inventory.rb +15 -6
- data/lib/bitcoin/message/merkle_block.rb +4 -4
- data/lib/bitcoin/message/network_addr.rb +13 -13
- data/lib/bitcoin/message/ping.rb +1 -0
- data/lib/bitcoin/message/pong.rb +1 -0
- data/lib/bitcoin/message/reject.rb +1 -0
- data/lib/bitcoin/message/send_cmpct.rb +2 -2
- data/lib/bitcoin/message/send_tx_rcncl.rb +1 -1
- data/lib/bitcoin/message/version.rb +23 -8
- data/lib/bitcoin/message.rb +15 -0
- data/lib/bitcoin/message_sign.rb +44 -6
- data/lib/bitcoin/mnemonic.rb +8 -0
- data/lib/bitcoin/out_point.rb +6 -2
- data/lib/bitcoin/partial_tree.rb +41 -28
- data/lib/bitcoin/payment_code.rb +26 -8
- data/lib/bitcoin/psbt/hd_key_path.rb +1 -2
- data/lib/bitcoin/psbt/input.rb +199 -48
- data/lib/bitcoin/psbt/key_origin_info.rb +2 -0
- data/lib/bitcoin/psbt/output.rb +9 -6
- data/lib/bitcoin/psbt/proprietary.rb +6 -3
- data/lib/bitcoin/psbt/tx.rb +9 -4
- data/lib/bitcoin/psbt.rb +46 -0
- data/lib/bitcoin/rpc/bitcoin_core_client.rb +54 -13
- data/lib/bitcoin/rpc/error.rb +43 -0
- data/lib/bitcoin/rpc.rb +1 -0
- data/lib/bitcoin/script/script.rb +114 -33
- data/lib/bitcoin/script/script_interpreter.rb +11 -6
- data/lib/bitcoin/script/tx_checker.rb +7 -2
- data/lib/bitcoin/script_witness.rb +5 -1
- data/lib/bitcoin/secp256k1/ruby.rb +10 -4
- data/lib/bitcoin/silent_payment.rb +6 -3
- data/lib/bitcoin/slip39/share.rb +35 -9
- data/lib/bitcoin/slip39/sss.rb +26 -11
- data/lib/bitcoin/slip39.rb +13 -5
- data/lib/bitcoin/taproot/control_block.rb +6 -1
- data/lib/bitcoin/taproot/custom_depth_builder.rb +12 -0
- data/lib/bitcoin/tx.rb +35 -14
- data/lib/bitcoin/tx_in.rb +2 -2
- data/lib/bitcoin/tx_out.rb +2 -2
- data/lib/bitcoin/util.rb +49 -28
- data/lib/bitcoin/validation.rb +64 -12
- data/lib/bitcoin/version.rb +1 -1
- data/lib/bitcoin/wallet/account.rb +35 -11
- data/lib/bitcoin/wallet/base.rb +40 -15
- data/lib/bitcoin/wallet/utxo.rb +22 -4
- data/lib/bitcoin.rb +16 -4
- metadata +24 -9
data/lib/bitcoin/tx.rb
CHANGED
|
@@ -9,7 +9,15 @@ module Bitcoin
|
|
|
9
9
|
include Bitcoin::HexConverter
|
|
10
10
|
include SilentPayment
|
|
11
11
|
|
|
12
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
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(
|
|
42
|
+
i.script_sig = Script.parse_from_payload(Bitcoin.read_exact(buf, sig_length))
|
|
43
43
|
end
|
|
44
|
-
i.sequence =
|
|
44
|
+
i.sequence = Bitcoin.read_exact(buf, 4).unpack1('V')
|
|
45
45
|
i
|
|
46
46
|
end
|
|
47
47
|
|
data/lib/bitcoin/tx_out.rb
CHANGED
|
@@ -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 =
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
data/lib/bitcoin/validation.rb
CHANGED
|
@@ -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? ||
|
|
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
|
-
#
|
|
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.
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
96
|
+
txs.each { |tx| return false unless check_tx(tx, state) }
|
|
64
97
|
|
|
65
|
-
|
|
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
|
-
|
|
105
|
+
private
|
|
68
106
|
|
|
69
|
-
|
|
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,
|
|
128
|
+
MODE = {valid: 0, invalid: 1, error: 2}
|
|
77
129
|
|
|
78
130
|
attr_accessor :mode
|
|
79
131
|
attr_accessor :n_dos
|
data/lib/bitcoin/version.rb
CHANGED
|
@@ -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(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
name =
|
|
36
|
-
purpose, index, receive_depth, change_depth, lookahead =
|
|
37
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
data/lib/bitcoin/wallet/base.rb
CHANGED
|
@@ -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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
self.load(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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}
|
data/lib/bitcoin/wallet/utxo.rb
CHANGED
|
@@ -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(
|
|
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
|
|