bitcoinrb 1.12.0 → 1.12.1
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/.github/workflows/ruby.yml +1 -1
- data/bitcoinrb.gemspec +1 -6
- data/lib/bitcoin/bip324/fs_chacha20.rb +3 -5
- data/lib/bitcoin/bip324/fs_chacha_poly1305.rb +19 -17
- data/lib/bitcoin/bip324.rb +1 -1
- data/lib/bitcoin/descriptor/expression.rb +2 -2
- data/lib/bitcoin/descriptor/sp.rb +219 -0
- data/lib/bitcoin/descriptor.rb +20 -0
- data/lib/bitcoin/ext_key.rb +3 -3
- data/lib/bitcoin/message/ping.rb +4 -2
- data/lib/bitcoin/message/version.rb +1 -1
- data/lib/bitcoin/message_sign.rb +98 -38
- data/lib/bitcoin/rpc.rb +0 -2
- data/lib/bitcoin/silent_payment/output.rb +46 -0
- data/lib/bitcoin/silent_payment.rb +113 -12
- data/lib/bitcoin/util.rb +2 -2
- data/lib/bitcoin/version.rb +1 -1
- data/lib/bitcoin.rb +0 -6
- metadata +18 -108
- data/exe/bitcoinrb-cli +0 -5
- data/exe/bitcoinrbd +0 -46
- data/lib/bitcoin/network/connection.rb +0 -73
- data/lib/bitcoin/network/message_handler.rb +0 -243
- data/lib/bitcoin/network/peer.rb +0 -228
- data/lib/bitcoin/network/peer_discovery.rb +0 -42
- data/lib/bitcoin/network/pool.rb +0 -135
- data/lib/bitcoin/network.rb +0 -13
- data/lib/bitcoin/node/cli.rb +0 -122
- data/lib/bitcoin/node/configuration.rb +0 -40
- data/lib/bitcoin/node/spv.rb +0 -87
- data/lib/bitcoin/node.rb +0 -7
- data/lib/bitcoin/rpc/http_server.rb +0 -84
- data/lib/bitcoin/rpc/request_handler.rb +0 -150
- data/lib/bitcoin/store/chain_entry.rb +0 -68
- data/lib/bitcoin/store/db/level_db.rb +0 -98
- data/lib/bitcoin/store/db.rb +0 -9
- data/lib/bitcoin/store/spv_chain.rb +0 -101
- data/lib/bitcoin/store/utxo_db.rb +0 -226
- data/lib/bitcoin/store.rb +0 -12
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e60d83b48a9f7189cd07c75ee61749ab50a83895f86dd97c7d7fbbd568234067
|
|
4
|
+
data.tar.gz: 04e39f6f592379617273944009f78d224cf181f1877de15a285665e9b25602b3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 13046ad4ba44662c2689d13dec7d51bc88e2d80cccaedc0048ee7484136e5cf04600161b5b7ae99faf4e1d7081fb845b403ebc3defb9cf92e46847e500befd65
|
|
7
|
+
data.tar.gz: 12c68844e79ca382dc774d6d4d6cdad8bbd33064a315098e0a31eecb88ac1369c2bd4ff6b39da49d3d5b2eae9750beff11e5761dbbdbe6221c3ef1100cd8cde2
|
data/.github/workflows/ruby.yml
CHANGED
data/bitcoinrb.gemspec
CHANGED
|
@@ -21,19 +21,14 @@ Gem::Specification.new do |spec|
|
|
|
21
21
|
spec.require_paths = ["lib"]
|
|
22
22
|
|
|
23
23
|
spec.add_runtime_dependency 'ecdsa_ext', '~> 0.5.1'
|
|
24
|
-
spec.add_runtime_dependency 'eventmachine'
|
|
25
24
|
spec.add_runtime_dependency 'murmurhash3', '~> 0.1.7'
|
|
26
25
|
spec.add_runtime_dependency 'bech32', '>= 1.5.0'
|
|
27
|
-
spec.add_runtime_dependency 'daemon-spawn'
|
|
28
|
-
spec.add_runtime_dependency 'thor'
|
|
29
|
-
spec.add_runtime_dependency 'eventmachine_httpserver'
|
|
30
|
-
spec.add_runtime_dependency 'iniparse'
|
|
31
26
|
spec.add_runtime_dependency 'siphash'
|
|
32
27
|
spec.add_runtime_dependency 'bip-schnorr', '>= 0.7.0'
|
|
33
28
|
spec.add_runtime_dependency 'base32', '>= 0.3.4'
|
|
34
29
|
spec.add_runtime_dependency 'base64', '~> 0.2.0'
|
|
35
|
-
spec.add_runtime_dependency 'observer', '~> 0.1.2'
|
|
36
30
|
spec.add_runtime_dependency 'secp256k1rb', '0.1.1'
|
|
37
31
|
spec.add_runtime_dependency 'logger'
|
|
38
32
|
spec.add_runtime_dependency 'merkle', '0.3.0'
|
|
33
|
+
spec.add_runtime_dependency 'dnsruby', '1.73.0'
|
|
39
34
|
end
|
|
@@ -16,8 +16,6 @@ module Bitcoin
|
|
|
16
16
|
# @param [Integer] bits
|
|
17
17
|
# @return [Integer]
|
|
18
18
|
def rotl32(v, bits)
|
|
19
|
-
raise Bitcoin::BIP324::Error, "v must be integer" unless v.is_a?(Integer)
|
|
20
|
-
raise Bitcoin::BIP324::Error, "bits must be integer" unless bits.is_a?(Integer)
|
|
21
19
|
((v << bits) & 0xffffffff) | (v >> (32 - bits))
|
|
22
20
|
end
|
|
23
21
|
|
|
@@ -117,9 +115,9 @@ module Bitcoin
|
|
|
117
115
|
# @return [String]
|
|
118
116
|
def crypt(chunk)
|
|
119
117
|
ks = key_stream_bytes(chunk.bytesize)
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
118
|
+
chunk_bytes = chunk.unpack("C*")
|
|
119
|
+
ks_bytes = ks.unpack("C*")
|
|
120
|
+
ret = chunk_bytes.each_with_index.map { |c, i| c ^ ks_bytes[i] }.pack("C*")
|
|
123
121
|
if (self.chunk_counter + 1) % rekey_interval == 0
|
|
124
122
|
self.key = key_stream_bytes(32)
|
|
125
123
|
self.block_counter = 0
|
|
@@ -92,14 +92,11 @@ module Bitcoin
|
|
|
92
92
|
# Encrypt a plaintext using ChaCha20Poly1305.
|
|
93
93
|
def chacha20_poly1305_encrypt(key, nonce, aad, plaintext)
|
|
94
94
|
msg_len = plaintext.bytesize
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
ret = ret.flatten.pack('C*')
|
|
95
|
+
num_blocks = (msg_len + 63) / 64
|
|
96
|
+
keystream = num_blocks.times.map { |i| ChaCha20.block(key, nonce, i + 1) }.join
|
|
97
|
+
pt_bytes = plaintext.unpack('C*')
|
|
98
|
+
ks_bytes = keystream.unpack('C*')
|
|
99
|
+
ret = Array.new(msg_len) { |i| pt_bytes[i] ^ ks_bytes[i] }.pack('C*')
|
|
103
100
|
poly1305 = Poly1305.new(ChaCha20.block(key, nonce, 0)[0...32])
|
|
104
101
|
poly1305.add(aad, padding: true).add(ret, padding: true)
|
|
105
102
|
poly1305.add([aad.bytesize, msg_len].pack("Q<Q<"))
|
|
@@ -114,15 +111,20 @@ module Bitcoin
|
|
|
114
111
|
poly1305.add(aad, padding: true)
|
|
115
112
|
poly1305.add(ciphertext, length: msg_len, padding: true)
|
|
116
113
|
poly1305.add([aad.bytesize, msg_len].pack("Q<Q<"))
|
|
117
|
-
return nil unless ciphertext[-16..-1]
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
114
|
+
return nil unless constant_time_equal?(ciphertext[-16..-1], poly1305.tag)
|
|
115
|
+
num_blocks = (msg_len + 63) / 64
|
|
116
|
+
keystream = num_blocks.times.map { |i| ChaCha20.block(key, nonce, i + 1) }.join
|
|
117
|
+
ct_bytes = ciphertext.unpack('C*')
|
|
118
|
+
ks_bytes = keystream.unpack('C*')
|
|
119
|
+
Array.new(msg_len) { |i| ct_bytes[i] ^ ks_bytes[i] }.pack('C*')
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Constant-time comparison of two equal-length byte strings.
|
|
123
|
+
def constant_time_equal?(a, b)
|
|
124
|
+
return false unless a.bytesize == b.bytesize
|
|
125
|
+
diff = 0
|
|
126
|
+
a.bytesize.times { |i| diff |= a.getbyte(i) ^ b.getbyte(i) }
|
|
127
|
+
diff.zero?
|
|
126
128
|
end
|
|
127
129
|
end
|
|
128
130
|
end
|
data/lib/bitcoin/bip324.rb
CHANGED
|
@@ -95,7 +95,7 @@ module Bitcoin
|
|
|
95
95
|
def xelligatorswift(x)
|
|
96
96
|
loop do
|
|
97
97
|
u = SecureRandom.random_number(1..ECDSA::Group::Secp256k1.order).to_s(16)
|
|
98
|
-
c =
|
|
98
|
+
c = SecureRandom.random_number(9)
|
|
99
99
|
t = xswiftec_inv(x, u, c)
|
|
100
100
|
unless t.nil?
|
|
101
101
|
return (ECDSA::Format::IntegerOctetString.encode(u.hex, 32) +
|
|
@@ -67,10 +67,10 @@ module Bitcoin
|
|
|
67
67
|
|
|
68
68
|
raise ArgumentError, "No key provided." unless key
|
|
69
69
|
|
|
70
|
-
if key.start_with?('xprv')
|
|
70
|
+
if key.start_with?('xprv') || key.start_with?('tprv')
|
|
71
71
|
key = Bitcoin::ExtKey.from_base58(key)
|
|
72
72
|
key = derive_path(key, paths) if paths
|
|
73
|
-
elsif key.start_with?('xpub')
|
|
73
|
+
elsif key.start_with?('xpub') || key.start_with?('tpub')
|
|
74
74
|
key = Bitcoin::ExtPubkey.from_base58(key)
|
|
75
75
|
key = derive_path(key, paths) if paths
|
|
76
76
|
else
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
module Bitcoin
|
|
2
|
+
module Descriptor
|
|
3
|
+
# sp() expression for Silent Payment descriptor.
|
|
4
|
+
# Supports both single-argument (spscan/spspend) and two-argument forms.
|
|
5
|
+
# @see https://github.com/bitcoin/bips/blob/master/bip-0392.mediawiki
|
|
6
|
+
class Sp < Expression
|
|
7
|
+
|
|
8
|
+
# HRP for spscan/spspend encoding
|
|
9
|
+
SPSCAN_HRP = 'spscan'
|
|
10
|
+
SPSPEND_HRP = 'spspend'
|
|
11
|
+
TSPSCAN_HRP = 'tspscan'
|
|
12
|
+
TSPSPEND_HRP = 'tspspend'
|
|
13
|
+
|
|
14
|
+
attr_reader :key_arg
|
|
15
|
+
attr_reader :scan_key
|
|
16
|
+
attr_reader :spend_key
|
|
17
|
+
|
|
18
|
+
# Constructor.
|
|
19
|
+
# @param [String] key_or_scan_key Either a spscan/spspend encoded key, or the scan key for two-argument form.
|
|
20
|
+
# @param [String, Expression, nil] spend_key The spend key (for two-argument form) or nil for single-argument form.
|
|
21
|
+
# @raise [ArgumentError] If keys are invalid.
|
|
22
|
+
def initialize(key_or_scan_key, spend_key = nil)
|
|
23
|
+
raise ArgumentError, "key_or_scan_key must be a String." unless key_or_scan_key.is_a?(String)
|
|
24
|
+
unless spend_key.nil? || spend_key.is_a?(String) || spend_key.is_a?(Expression)
|
|
25
|
+
raise ArgumentError, "spend_key must be a String, Expression, or nil."
|
|
26
|
+
end
|
|
27
|
+
if spend_key.nil?
|
|
28
|
+
# Single argument form: spscan or spspend encoded key
|
|
29
|
+
@key_arg = key_or_scan_key
|
|
30
|
+
parse_encoded_key(key_or_scan_key)
|
|
31
|
+
else
|
|
32
|
+
# Two argument form: separate scan and spend keys
|
|
33
|
+
@key_arg = nil
|
|
34
|
+
@scan_key = key_or_scan_key
|
|
35
|
+
@spend_key = spend_key
|
|
36
|
+
end
|
|
37
|
+
validate_keys!
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def type
|
|
41
|
+
:sp
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def top_level?
|
|
45
|
+
true
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def args
|
|
49
|
+
if key_arg
|
|
50
|
+
# Single argument form
|
|
51
|
+
key_arg
|
|
52
|
+
else
|
|
53
|
+
# Two argument form
|
|
54
|
+
spend_arg = spend_key.is_a?(Expression) ? spend_key.to_s : spend_key
|
|
55
|
+
"#{scan_key},#{spend_arg}"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Check if this is a single-argument form (spscan/spspend).
|
|
60
|
+
# @return [Boolean]
|
|
61
|
+
def single_key?
|
|
62
|
+
!key_arg.nil?
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Check if this descriptor has spend private key (spspend form).
|
|
66
|
+
# @return [Boolean]
|
|
67
|
+
def has_spend_private_key?
|
|
68
|
+
return @has_spend_private_key if defined?(@has_spend_private_key)
|
|
69
|
+
if single_key?
|
|
70
|
+
@has_spend_private_key
|
|
71
|
+
else
|
|
72
|
+
# Check if spend_key is a private key
|
|
73
|
+
return false if spend_key.is_a?(Expression)
|
|
74
|
+
begin
|
|
75
|
+
key = extract_pubkey(spend_key)
|
|
76
|
+
!key.priv_key.nil?
|
|
77
|
+
rescue
|
|
78
|
+
false
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Generate silent payment address.
|
|
84
|
+
# @return [Bech32::SilentPaymentAddr] Silent payment address.
|
|
85
|
+
def to_addr
|
|
86
|
+
Bech32::SilentPaymentAddr.new(
|
|
87
|
+
address_hrp,
|
|
88
|
+
0,
|
|
89
|
+
extracted_scan_pubkey,
|
|
90
|
+
extracted_spend_pubkey
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Silent payment descriptors do not produce a single script.
|
|
95
|
+
# Use to_address to get the silent payment address.
|
|
96
|
+
# @raise [RuntimeError]
|
|
97
|
+
def to_script
|
|
98
|
+
raise RuntimeError, "sp() descriptor does not produce a fixed script. Use to_address instead."
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
# Parse spscan or spspend encoded key.
|
|
104
|
+
# @param [String] encoded_key The encoded key string.
|
|
105
|
+
# @raise [ArgumentError] If the key format is invalid.
|
|
106
|
+
def parse_encoded_key(encoded_key)
|
|
107
|
+
# Handle key origin (e.g., [fingerprint/path]key)
|
|
108
|
+
key_str = encoded_key
|
|
109
|
+
if key_str.start_with?('[')
|
|
110
|
+
key_str = key_str[(key_str.index(']') + 1)..-1]
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Decode Bech32m (max_length increased for longer spscan/spspend strings)
|
|
114
|
+
hrp, data, spec = Bech32.decode(key_str, 200)
|
|
115
|
+
raise ArgumentError, "Invalid spscan/spspend encoding." unless hrp
|
|
116
|
+
raise ArgumentError, "spscan/spspend must use Bech32m encoding." unless spec == Bech32::Encoding::BECH32M
|
|
117
|
+
|
|
118
|
+
# Validate HRP
|
|
119
|
+
valid_hrps = [SPSCAN_HRP, SPSPEND_HRP, TSPSCAN_HRP, TSPSPEND_HRP]
|
|
120
|
+
raise ArgumentError, "Invalid HRP: #{hrp}. Expected one of: #{valid_hrps.join(', ')}" unless valid_hrps.include?(hrp)
|
|
121
|
+
|
|
122
|
+
# Check version (first data element should be 0 for version 0)
|
|
123
|
+
raise ArgumentError, "Invalid version." unless data[0] == 0
|
|
124
|
+
|
|
125
|
+
# Convert from 5-bit to 8-bit
|
|
126
|
+
payload = Bech32.convert_bits(data[1..-1], 5, 8, false)
|
|
127
|
+
payload_bytes = payload.pack('C*')
|
|
128
|
+
|
|
129
|
+
is_spspend = [SPSPEND_HRP, TSPSPEND_HRP].include?(hrp)
|
|
130
|
+
|
|
131
|
+
if is_spspend
|
|
132
|
+
# spspend: 32 bytes scan + 32 bytes spend
|
|
133
|
+
raise ArgumentError, "Invalid spspend payload length." unless payload_bytes.bytesize == 64
|
|
134
|
+
scan_priv = payload_bytes[0, 32].bth
|
|
135
|
+
spend_priv = payload_bytes[32, 32].bth
|
|
136
|
+
@scan_key = scan_priv
|
|
137
|
+
@spend_key = spend_priv
|
|
138
|
+
@has_spend_private_key = true
|
|
139
|
+
else
|
|
140
|
+
# spscan: 32 bytes scan + 33 bytes spend pubkey
|
|
141
|
+
raise ArgumentError, "Invalid spscan payload length." unless payload_bytes.bytesize == 65
|
|
142
|
+
scan_priv = payload_bytes[0, 32].bth
|
|
143
|
+
spend_pub = payload_bytes[32, 33].bth
|
|
144
|
+
@scan_key = scan_priv
|
|
145
|
+
@spend_key = spend_pub
|
|
146
|
+
@has_spend_private_key = false
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def validate_keys!
|
|
151
|
+
# Scan key must be a private key
|
|
152
|
+
scan = extracted_scan_key
|
|
153
|
+
raise ArgumentError, "Scan key must be a private key." unless scan.priv_key
|
|
154
|
+
|
|
155
|
+
# Validate spend key
|
|
156
|
+
spend = extracted_spend_key
|
|
157
|
+
raise ArgumentError, "Uncompressed keys are not allowed." unless spend.compressed?
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Get the extracted scan key.
|
|
161
|
+
# @return [Bitcoin::Key] Extracted scan key.
|
|
162
|
+
def extracted_scan_key
|
|
163
|
+
if single_key?
|
|
164
|
+
# scan_key is already a hex private key from spscan/spspend
|
|
165
|
+
Bitcoin::Key.new(priv_key: scan_key)
|
|
166
|
+
else
|
|
167
|
+
extract_pubkey(scan_key)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Get the extracted scan public key (compressed, 33 bytes hex).
|
|
172
|
+
# @return [String] Compressed public key hex.
|
|
173
|
+
def extracted_scan_pubkey
|
|
174
|
+
extracted_scan_key.pubkey
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Get the extracted spend key.
|
|
178
|
+
# @return [Bitcoin::Key] Extracted spend key.
|
|
179
|
+
def extracted_spend_key
|
|
180
|
+
if spend_key.is_a?(MuSig)
|
|
181
|
+
# MuSig returns x-only pubkey, convert to full pubkey
|
|
182
|
+
Bitcoin::Key.from_xonly_pubkey(spend_key.to_hex)
|
|
183
|
+
elsif spend_key.is_a?(Expression)
|
|
184
|
+
spend_key.extracted_key
|
|
185
|
+
elsif single_key? && has_spend_private_key?
|
|
186
|
+
# spspend: spend_key is hex private key
|
|
187
|
+
Bitcoin::Key.new(priv_key: spend_key)
|
|
188
|
+
elsif single_key?
|
|
189
|
+
# spscan: spend_key is hex public key
|
|
190
|
+
Bitcoin::Key.new(pubkey: spend_key)
|
|
191
|
+
else
|
|
192
|
+
extract_pubkey(spend_key)
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Get the extracted spend public key (compressed, 33 bytes hex).
|
|
197
|
+
# @return [String] Compressed public key hex.
|
|
198
|
+
def extracted_spend_pubkey
|
|
199
|
+
if spend_key.is_a?(MuSig)
|
|
200
|
+
# MuSig aggregated key as compressed pubkey (even y-coordinate assumed)
|
|
201
|
+
'02' + spend_key.to_hex
|
|
202
|
+
else
|
|
203
|
+
extracted_spend_key.pubkey
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Get HRP for silent payment address based on network.
|
|
208
|
+
# @return [String] HRP (sp for mainnet, tsp for testnet/signet).
|
|
209
|
+
def address_hrp
|
|
210
|
+
case Bitcoin.chain_params.network
|
|
211
|
+
when 'mainnet'
|
|
212
|
+
Bech32::SilentPaymentAddr::HRP_MAINNET
|
|
213
|
+
else
|
|
214
|
+
Bech32::SilentPaymentAddr::HRP_TESTNET
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
data/lib/bitcoin/descriptor.rb
CHANGED
|
@@ -22,6 +22,7 @@ module Bitcoin
|
|
|
22
22
|
autoload :RawTr, 'bitcoin/descriptor/raw_tr'
|
|
23
23
|
autoload :Checksum, 'bitcoin/descriptor/checksum'
|
|
24
24
|
autoload :MuSig, 'bitcoin/descriptor/musig'
|
|
25
|
+
autoload :Sp, 'bitcoin/descriptor/sp'
|
|
25
26
|
|
|
26
27
|
module_function
|
|
27
28
|
|
|
@@ -137,6 +138,15 @@ module Bitcoin
|
|
|
137
138
|
MuSig.new(keys, path)
|
|
138
139
|
end
|
|
139
140
|
|
|
141
|
+
# Generate sp() descriptor for Silent Payment.
|
|
142
|
+
# @param [String] key_or_scan_key Either a spscan/spspend encoded key (single-argument form),
|
|
143
|
+
# or the scan key for two-argument form.
|
|
144
|
+
# @param [String, Expression, nil] spend_key The spend key (for two-argument form) or nil for single-argument form.
|
|
145
|
+
# @return [Bitcoin::Descriptor::Sp]
|
|
146
|
+
def sp(key_or_scan_key, spend_key = nil)
|
|
147
|
+
Sp.new(key_or_scan_key, spend_key)
|
|
148
|
+
end
|
|
149
|
+
|
|
140
150
|
# Parse descriptor string.
|
|
141
151
|
# @param [String] string Descriptor string.
|
|
142
152
|
# @return [Bitcoin::Descriptor::Expression]
|
|
@@ -202,6 +212,16 @@ module Bitcoin
|
|
|
202
212
|
when 'musig'
|
|
203
213
|
keys = args_str.split(',')
|
|
204
214
|
path.empty? ? musig(*keys) : musig(*keys, path: path)
|
|
215
|
+
when 'sp'
|
|
216
|
+
scan_key, spend_key_str = split_two(args_str)
|
|
217
|
+
if spend_key_str.nil?
|
|
218
|
+
# Single argument form: spscan or spspend encoded key
|
|
219
|
+
sp(scan_key)
|
|
220
|
+
else
|
|
221
|
+
# Two argument form: separate scan and spend keys
|
|
222
|
+
spend_key = spend_key_str.include?('(') ? parse(spend_key_str, false) : spend_key_str
|
|
223
|
+
sp(scan_key, spend_key)
|
|
224
|
+
end
|
|
205
225
|
else
|
|
206
226
|
raise ArgumentError, "Parse failed: #{string}"
|
|
207
227
|
end
|
data/lib/bitcoin/ext_key.rb
CHANGED
|
@@ -108,9 +108,9 @@ module Bitcoin
|
|
|
108
108
|
end
|
|
109
109
|
l = Bitcoin.hmac_sha512(chain_code, data)
|
|
110
110
|
left = l[0..31].bth.to_i(16)
|
|
111
|
-
raise 'invalid key' if left >= CURVE_ORDER
|
|
111
|
+
raise 'invalid key' if left >= CURVE_ORDER || left == 0
|
|
112
112
|
child_priv = (left + key.priv_key.to_i(16)) % CURVE_ORDER
|
|
113
|
-
raise 'invalid key
|
|
113
|
+
raise 'invalid key' if child_priv == 0
|
|
114
114
|
child_priv = ECDSA::Format::IntegerOctetString.encode(child_priv, 32)
|
|
115
115
|
new_key.key = Bitcoin::Key.new(priv_key: child_priv.bth, key_type: key_type)
|
|
116
116
|
new_key.chain_code = l[32..-1]
|
|
@@ -280,7 +280,7 @@ module Bitcoin
|
|
|
280
280
|
data = pub.htb << [number].pack('N')
|
|
281
281
|
l = Bitcoin.hmac_sha512(chain_code, data)
|
|
282
282
|
left = l[0..31].bth.to_i(16)
|
|
283
|
-
raise 'invalid key' if left >= CURVE_ORDER
|
|
283
|
+
raise 'invalid key' if left >= CURVE_ORDER || left == 0
|
|
284
284
|
l_priv = ECDSA::Format::IntegerOctetString.encode(left, 32)
|
|
285
285
|
p1 = Bitcoin::Key.new(priv_key: l_priv.bth, key_type: Bitcoin::Key::TYPES[:uncompressed]).to_point
|
|
286
286
|
p2 = Bitcoin::Key.new(pubkey: pubkey, key_type: key_type).to_point
|
data/lib/bitcoin/message/ping.rb
CHANGED
|
@@ -18,11 +18,13 @@ module Bitcoin
|
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def to_payload
|
|
21
|
-
[nonce].pack('Q')
|
|
21
|
+
nonce ? [nonce].pack('Q') : ''
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
+
# Generate pong message as a response. Return nil if the ping has no nonce
|
|
25
|
+
# (sent by a node before BIP-31), since such a ping does not expect a pong.
|
|
24
26
|
def to_response
|
|
25
|
-
Pong.new(nonce)
|
|
27
|
+
Pong.new(nonce) if nonce
|
|
26
28
|
end
|
|
27
29
|
|
|
28
30
|
end
|
|
@@ -61,7 +61,7 @@ module Bitcoin
|
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
def unpack_relay_field(payload)
|
|
64
|
-
( version >= 70001 && payload ) ? unpack_boolean(payload) : [ true, nil ]
|
|
64
|
+
( version >= 70001 && payload && !payload.empty? ) ? unpack_boolean(payload) : [ true, nil ]
|
|
65
65
|
end
|
|
66
66
|
|
|
67
67
|
# Check whether +service_flag+ support this version.
|
data/lib/bitcoin/message_sign.rb
CHANGED
|
@@ -10,6 +10,22 @@ module Bitcoin
|
|
|
10
10
|
FORMAT_SIMPLE = :simple
|
|
11
11
|
FORMAT_FULL = :full
|
|
12
12
|
|
|
13
|
+
# Prefix
|
|
14
|
+
SIGNATURE_PREFIX_SIMPLE = 'smp'
|
|
15
|
+
SIGNATURE_PREFIX_FULL = 'ful'
|
|
16
|
+
SIGNATURE_PREFIX_POF = 'pof'
|
|
17
|
+
|
|
18
|
+
# BIP-322 Required rules
|
|
19
|
+
BIP322_VERIFY_FLAGS = [
|
|
20
|
+
SCRIPT_VERIFY_P2SH, SCRIPT_VERIFY_DERSIG, SCRIPT_VERIFY_STRICTENC,
|
|
21
|
+
SCRIPT_VERIFY_LOW_S, SCRIPT_VERIFY_NULLFAIL, SCRIPT_VERIFY_MINIMALDATA,
|
|
22
|
+
SCRIPT_VERIFY_CLEANSTACK, SCRIPT_VERIFY_MINIMALIF,
|
|
23
|
+
SCRIPT_VERIFY_CONST_SCRIPTCODE,
|
|
24
|
+
SCRIPT_VERIFY_WITNESS, SCRIPT_VERIFY_TAPROOT,
|
|
25
|
+
SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_NOPS,
|
|
26
|
+
SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM
|
|
27
|
+
].inject(:|)
|
|
28
|
+
|
|
13
29
|
# Sign a message.
|
|
14
30
|
# @param [Bitcoin::Key] key Private key to sign.
|
|
15
31
|
# @param [String] message The message to be signed.
|
|
@@ -20,28 +36,38 @@ module Bitcoin
|
|
|
20
36
|
def sign_message(key, message, prefix: Bitcoin.chain_params.message_magic, format: FORMAT_LEGACY, address: nil)
|
|
21
37
|
validate_format!(format)
|
|
22
38
|
digest = message_hash(message, prefix: prefix, legacy: format == FORMAT_LEGACY)
|
|
39
|
+
prefix_marker = ''
|
|
23
40
|
sig = case format
|
|
24
41
|
when FORMAT_LEGACY
|
|
25
42
|
key.sign_compact(digest)
|
|
26
43
|
else
|
|
27
44
|
validate_address!(address)
|
|
28
45
|
addr = Bitcoin::Script.parse_from_addr(address)
|
|
29
|
-
sig_ver, algo = if addr.p2wpkh?
|
|
46
|
+
sig_ver, algo = if addr.p2wpkh? || addr.p2wsh?
|
|
30
47
|
[:witness_v0, :ecdsa]
|
|
31
48
|
elsif addr.p2tr?
|
|
32
49
|
[:taproot, :schnorr]
|
|
33
50
|
else
|
|
34
|
-
|
|
51
|
+
|
|
35
52
|
end
|
|
36
53
|
tx = to_sign_tx(digest, address)
|
|
37
|
-
prev_out = Bitcoin::TxOut.new(script_pubkey: addr)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
54
|
+
prev_out = Bitcoin::TxOut.new(script_pubkey: addr, value: 0)
|
|
55
|
+
if addr.p2tr?
|
|
56
|
+
sighash = tx.sighash_for_input(0, addr, sig_version: :taproot, prevouts: [prev_out], hash_type: Bitcoin::SIGHASH_TYPE[:default])
|
|
57
|
+
tweaked = Bitcoin::Taproot.tweak_private_key(key, '')
|
|
58
|
+
tx.in[0].script_witness.stack << tweaked.sign(sighash, algo: :schnorr)
|
|
59
|
+
elsif addr.p2wpkh? || addr.p2wsh?
|
|
60
|
+
sighash = tx.sighash_for_input(0, addr, sig_version: :witness_v0, amount: 0, prevouts: [prev_out])
|
|
61
|
+
ecdsa = key.sign(sighash, algo: :ecdsa) + [Bitcoin::SIGHASH_TYPE[:all]].pack('C')
|
|
62
|
+
tx.in[0].script_witness.stack << ecdsa
|
|
63
|
+
tx.in[0].script_witness.stack << key.pubkey.htb
|
|
64
|
+
else
|
|
65
|
+
raise ArgumentError, "#{address} dose not supported."
|
|
66
|
+
end
|
|
67
|
+
prefix_marker = format == FORMAT_SIMPLE ? SIGNATURE_PREFIX_SIMPLE : SIGNATURE_PREFIX_FULL
|
|
42
68
|
format == FORMAT_SIMPLE ? tx.in[0].script_witness.to_payload : tx.to_payload
|
|
43
69
|
end
|
|
44
|
-
Base64.strict_encode64(sig)
|
|
70
|
+
prefix_marker + Base64.strict_encode64(sig)
|
|
45
71
|
end
|
|
46
72
|
|
|
47
73
|
# Verify a signed message.
|
|
@@ -51,41 +77,41 @@ module Bitcoin
|
|
|
51
77
|
# @return [Boolean] Verification result.
|
|
52
78
|
def verify_message(address, signature, message, prefix: Bitcoin.chain_params.message_magic)
|
|
53
79
|
addr_script = Bitcoin::Script.parse_from_addr(address)
|
|
80
|
+
variant, body = case signature
|
|
81
|
+
when ''
|
|
82
|
+
raise ArgumentError, 'signature too short'
|
|
83
|
+
when /\A#{SIGNATURE_PREFIX_SIMPLE}/
|
|
84
|
+
[:simple, signature[3..]]
|
|
85
|
+
when /\A#{SIGNATURE_PREFIX_FULL}/
|
|
86
|
+
[:full, signature[3..]]
|
|
87
|
+
when /\A#{SIGNATURE_PREFIX_POF}/
|
|
88
|
+
[:pof, signature[3..]]
|
|
89
|
+
else
|
|
90
|
+
[:fallback, signature]
|
|
91
|
+
end
|
|
54
92
|
begin
|
|
55
|
-
|
|
93
|
+
payload = Base64.strict_decode64(body)
|
|
56
94
|
rescue ArgumentError
|
|
57
95
|
raise ArgumentError, 'Invalid signature'
|
|
58
96
|
end
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
end
|
|
68
|
-
elsif addr_script.witness_program?
|
|
69
|
-
# BIP322 verification
|
|
70
|
-
digest = message_hash(message, prefix: prefix, legacy: false)
|
|
97
|
+
|
|
98
|
+
case variant
|
|
99
|
+
when :simple, :fallback
|
|
100
|
+
return verify_legacy(address, payload, message, prefix: prefix) if addr_script.p2pkh?
|
|
101
|
+
raise ArgumentError, 'simple format not supported for this address' unless
|
|
102
|
+
addr_script.p2wpkh? || addr_script.p2wsh? || addr_script.p2tr?
|
|
103
|
+
verify_simple(addr_script, payload, message, prefix: prefix)
|
|
104
|
+
when :full
|
|
71
105
|
begin
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
to_spend = to_spend_tx(digest, address)
|
|
76
|
-
return false unless tx.in[0].out_point.tx_hash == to_spend.tx_hash
|
|
77
|
-
rescue Exception
|
|
78
|
-
# Simple
|
|
79
|
-
tx = to_sign_tx(digest, address)
|
|
80
|
-
tx.in[0].script_witness = Bitcoin::ScriptWitness.parse_from_payload(sig)
|
|
106
|
+
tx = Bitcoin::Tx.parse_from_payload(payload)
|
|
107
|
+
rescue StandardError
|
|
108
|
+
raise ArgumentError, 'error parsing signature as full variant'
|
|
81
109
|
end
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
interpreter = Bitcoin::ScriptInterpreter.new(flags: flags, checker: Bitcoin::TxChecker.new(tx: tx, input_index: 0, prevouts: [tx_out]))
|
|
86
|
-
interpreter.verify_script(Bitcoin::Script.new, script_pubkey, tx.in[0].script_witness)
|
|
110
|
+
verify_full(addr_script, tx, message, prefix: prefix)
|
|
111
|
+
when :pof
|
|
112
|
+
raise NotImplementedError, 'proof of funds variant is not supported'
|
|
87
113
|
else
|
|
88
|
-
raise ArgumentError, "
|
|
114
|
+
raise ArgumentError, "unknown signature variant: #{variant.inspect}"
|
|
89
115
|
end
|
|
90
116
|
end
|
|
91
117
|
|
|
@@ -99,8 +125,7 @@ module Bitcoin
|
|
|
99
125
|
end
|
|
100
126
|
|
|
101
127
|
def validate_address!(address)
|
|
102
|
-
|
|
103
|
-
raise ArgumentError, 'This address unsupported' if script.p2sh? || script.p2wsh?
|
|
128
|
+
Bitcoin::Script.parse_from_addr(address)
|
|
104
129
|
end
|
|
105
130
|
|
|
106
131
|
def validate_format!(format)
|
|
@@ -110,6 +135,7 @@ module Bitcoin
|
|
|
110
135
|
end
|
|
111
136
|
|
|
112
137
|
def validate_to_sign_tx!(tx)
|
|
138
|
+
raise ArgumentError, "Invalid version." unless [0, 2].include?(tx.version)
|
|
113
139
|
raise ArgumentError, "Multiple inputs (proof of funds) are not supported." unless tx.in.length == 1
|
|
114
140
|
raise ArgumentError, "vin[0].prevout.n must be 0." unless tx.in[0].out_point.index == 0
|
|
115
141
|
raise ArgumentError, "Multiple outputs are not supported." unless tx.out.length == 1
|
|
@@ -140,8 +166,42 @@ module Bitcoin
|
|
|
140
166
|
tx
|
|
141
167
|
end
|
|
142
168
|
|
|
169
|
+
def verify_simple(addr_script, witness_payload, message, prefix:)
|
|
170
|
+
digest = message_hash(message, prefix: prefix, legacy: false)
|
|
171
|
+
tx = to_sign_tx(digest, addr_script.to_addr)
|
|
172
|
+
tx.in[0].script_witness = Bitcoin::ScriptWitness.parse_from_payload(witness_payload)
|
|
173
|
+
run_interpreter(tx, addr_script)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def verify_full(addr_script, tx, message, prefix:)
|
|
177
|
+
digest = message_hash(message, prefix: prefix, legacy: false)
|
|
178
|
+
to_spend = to_spend_tx(digest, addr_script.to_addr)
|
|
179
|
+
validate_to_sign_tx!(tx)
|
|
180
|
+
return false unless tx.in[0].out_point.tx_hash == to_spend.tx_hash
|
|
181
|
+
run_interpreter(tx, addr_script)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def run_interpreter(tx, script_pubkey)
|
|
185
|
+
tx_out = Bitcoin::TxOut.new(script_pubkey: script_pubkey)
|
|
186
|
+
checker = Bitcoin::TxChecker.new(tx: tx, input_index: 0, prevouts: [tx_out])
|
|
187
|
+
interpreter = Bitcoin::ScriptInterpreter.new(flags: BIP322_VERIFY_FLAGS, checker: checker)
|
|
188
|
+
interpreter.verify_script(tx.in[0].script_sig, script_pubkey, tx.in[0].script_witness)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def verify_legacy(address, sig_bytes, message, prefix:)
|
|
192
|
+
pubkey = Bitcoin::Key.recover_compact(message_hash(message, prefix: prefix, legacy: true), sig_bytes)
|
|
193
|
+
return false unless pubkey
|
|
194
|
+
pubkey.to_p2pkh == address
|
|
195
|
+
rescue StandardError
|
|
196
|
+
false
|
|
197
|
+
end
|
|
198
|
+
|
|
143
199
|
private_class_method :validate_address!
|
|
144
200
|
private_class_method :validate_format!
|
|
145
201
|
private_class_method :validate_to_sign_tx!
|
|
202
|
+
private_class_method :run_interpreter
|
|
203
|
+
private_class_method :verify_simple
|
|
204
|
+
private_class_method :verify_full
|
|
205
|
+
private_class_method :verify_legacy
|
|
146
206
|
end
|
|
147
207
|
end
|