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