bitcoinrb 1.12.0 → 1.13.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/.github/workflows/ruby.yml +1 -1
- data/bitcoinrb.gemspec +1 -6
- data/lib/bitcoin/bip321_uri.rb +3 -6
- data/lib/bitcoin/bip324/fs_chacha20.rb +4 -6
- data/lib/bitcoin/bip324/fs_chacha_poly1305.rb +20 -17
- data/lib/bitcoin/bip324.rb +3 -1
- data/lib/bitcoin/descriptor/checksum.rb +6 -2
- 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 +11 -7
- data/lib/bitcoin/key.rb +1 -1
- data/lib/bitcoin/message/cf_parser.rb +2 -2
- data/lib/bitcoin/message/fee_filter.rb +2 -2
- data/lib/bitcoin/message/header_and_short_ids.rb +4 -4
- data/lib/bitcoin/message/inventory.rb +1 -1
- data/lib/bitcoin/message/network_addr.rb +7 -3
- data/lib/bitcoin/message/ping.rb +5 -3
- data/lib/bitcoin/message/pong.rb +2 -2
- data/lib/bitcoin/message/send_cmpct.rb +2 -2
- data/lib/bitcoin/message/version.rb +4 -4
- data/lib/bitcoin/message.rb +12 -2
- data/lib/bitcoin/message_sign.rb +98 -38
- data/lib/bitcoin/mnemonic.rb +9 -4
- data/lib/bitcoin/psbt/input.rb +51 -33
- data/lib/bitcoin/psbt/key_origin_info.rb +2 -2
- data/lib/bitcoin/psbt/output.rb +9 -5
- data/lib/bitcoin/psbt/tx.rb +16 -10
- data/lib/bitcoin/psbt.rb +1 -1
- data/lib/bitcoin/rpc.rb +0 -2
- data/lib/bitcoin/script/script.rb +14 -4
- data/lib/bitcoin/secp256k1/rfc6979.rb +6 -3
- data/lib/bitcoin/sighash_generator.rb +2 -2
- data/lib/bitcoin/silent_payment/output.rb +46 -0
- data/lib/bitcoin/silent_payment.rb +113 -12
- data/lib/bitcoin/taproot/control_block.rb +1 -0
- data/lib/bitcoin/tx.rb +8 -6
- data/lib/bitcoin/tx_out.rb +2 -2
- data/lib/bitcoin/util.rb +5 -5
- data/lib/bitcoin/version.rb +1 -1
- data/lib/bitcoin/wallet/account.rb +2 -2
- data/lib/bitcoin/wallet/db.rb +3 -3
- data/lib/bitcoin/wallet/master_key.rb +2 -2
- data/lib/bitcoin/wallet/utxo.rb +2 -2
- data/lib/bitcoin.rb +1 -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: cc2fd227e7a25b6fa3bdd88ef6035e7b7f4a76bb1ef501285ccd0f7f5d961c4c
|
|
4
|
+
data.tar.gz: 0f2557375f3918a5416983dde696cb922be71cf7a7c762c2be2305b6e9edb3d3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9cd9ed6bfca773abf5d25228be6b2a88ef7a17c163e3ddd5bb7de9c6365261d65e51fa8c3e9a6f87304a5bfc48036f87e7b0fb99d8b910c368001f7c5aa78d81
|
|
7
|
+
data.tar.gz: bec0d5c4ab556984e2fb9fa4da85008c92fb392d4510e41901701626658f8356f20aa5b74d1837ed90c1a35efe86b52f6ea5713b90f4c2caf2112b47a42a1706
|
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
|
data/lib/bitcoin/bip321_uri.rb
CHANGED
|
@@ -105,7 +105,7 @@ module Bitcoin
|
|
|
105
105
|
req_pop = true
|
|
106
106
|
k = 'pop'
|
|
107
107
|
end
|
|
108
|
-
if %w[bc tb].include?(k.downcase)
|
|
108
|
+
if %w[bc tb bcrt].include?(k.downcase)
|
|
109
109
|
raise ArgumentError, "#{k} not allowed in current network." unless Bitcoin.chain_params.bech32_hrp == k.downcase
|
|
110
110
|
query_addrs << v
|
|
111
111
|
nil
|
|
@@ -160,12 +160,9 @@ module Bitcoin
|
|
|
160
160
|
all_params = base_params.merge other_params
|
|
161
161
|
params = all_params.map do |k, v|
|
|
162
162
|
"#{k}=#{CGI.escape(v).gsub('+', '%20')}"
|
|
163
|
-
end.join('&')
|
|
164
|
-
uri << "?#{params}" unless params.empty?
|
|
165
|
-
unless query_addrs.empty?
|
|
166
|
-
uri << '?' unless uri.include?('?')
|
|
167
|
-
uri << query_addrs.map {|addr| "#{Bitcoin.chain_params.bech32_hrp}=#{addr}"}.join('&')
|
|
168
163
|
end
|
|
164
|
+
params += query_addrs.map {|addr| "#{Bitcoin.chain_params.bech32_hrp}=#{addr}"}
|
|
165
|
+
uri << "?#{params.join('&')}" unless params.empty?
|
|
169
166
|
uri
|
|
170
167
|
end
|
|
171
168
|
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
|
|
|
@@ -103,7 +101,7 @@ module Bitcoin
|
|
|
103
101
|
|
|
104
102
|
def key_stream_bytes(n_bytes)
|
|
105
103
|
while key_stream.bytesize < n_bytes
|
|
106
|
-
nonce = [0, (chunk_counter /
|
|
104
|
+
nonce = [0, (chunk_counter / rekey_interval)].pack("VQ<")
|
|
107
105
|
self.key_stream << ChaCha20.block(key, nonce, block_counter)
|
|
108
106
|
self.block_counter += 1
|
|
109
107
|
end
|
|
@@ -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
|
|
@@ -65,6 +65,7 @@ module Bitcoin
|
|
|
65
65
|
# @return [Array] [header, plaintext]
|
|
66
66
|
def decrypt(aad, ciphertext)
|
|
67
67
|
contents = crypt(aad, ciphertext, true)
|
|
68
|
+
raise Bitcoin::BIP324::Error, 'Decryption failed.' unless contents
|
|
68
69
|
[contents[0], contents[1..-1]]
|
|
69
70
|
end
|
|
70
71
|
|
|
@@ -92,14 +93,11 @@ module Bitcoin
|
|
|
92
93
|
# Encrypt a plaintext using ChaCha20Poly1305.
|
|
93
94
|
def chacha20_poly1305_encrypt(key, nonce, aad, plaintext)
|
|
94
95
|
msg_len = plaintext.bytesize
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
ret = ret.flatten.pack('C*')
|
|
96
|
+
num_blocks = (msg_len + 63) / 64
|
|
97
|
+
keystream = num_blocks.times.map { |i| ChaCha20.block(key, nonce, i + 1) }.join
|
|
98
|
+
pt_bytes = plaintext.unpack('C*')
|
|
99
|
+
ks_bytes = keystream.unpack('C*')
|
|
100
|
+
ret = Array.new(msg_len) { |i| pt_bytes[i] ^ ks_bytes[i] }.pack('C*')
|
|
103
101
|
poly1305 = Poly1305.new(ChaCha20.block(key, nonce, 0)[0...32])
|
|
104
102
|
poly1305.add(aad, padding: true).add(ret, padding: true)
|
|
105
103
|
poly1305.add([aad.bytesize, msg_len].pack("Q<Q<"))
|
|
@@ -114,15 +112,20 @@ module Bitcoin
|
|
|
114
112
|
poly1305.add(aad, padding: true)
|
|
115
113
|
poly1305.add(ciphertext, length: msg_len, padding: true)
|
|
116
114
|
poly1305.add([aad.bytesize, msg_len].pack("Q<Q<"))
|
|
117
|
-
return nil unless ciphertext[-16..-1]
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
115
|
+
return nil unless constant_time_equal?(ciphertext[-16..-1], poly1305.tag)
|
|
116
|
+
num_blocks = (msg_len + 63) / 64
|
|
117
|
+
keystream = num_blocks.times.map { |i| ChaCha20.block(key, nonce, i + 1) }.join
|
|
118
|
+
ct_bytes = ciphertext.unpack('C*')
|
|
119
|
+
ks_bytes = keystream.unpack('C*')
|
|
120
|
+
Array.new(msg_len) { |i| ct_bytes[i] ^ ks_bytes[i] }.pack('C*')
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Constant-time comparison of two equal-length byte strings.
|
|
124
|
+
def constant_time_equal?(a, b)
|
|
125
|
+
return false unless a.bytesize == b.bytesize
|
|
126
|
+
diff = 0
|
|
127
|
+
a.bytesize.times { |i| diff |= a.getbyte(i) ^ b.getbyte(i) }
|
|
128
|
+
diff.zero?
|
|
126
129
|
end
|
|
127
130
|
end
|
|
128
131
|
end
|
data/lib/bitcoin/bip324.rb
CHANGED
|
@@ -10,7 +10,9 @@ module Bitcoin
|
|
|
10
10
|
|
|
11
11
|
autoload :EllSwiftPubkey, 'bitcoin/bip324/ell_swift_pubkey'
|
|
12
12
|
autoload :Cipher, 'bitcoin/bip324/cipher'
|
|
13
|
+
autoload :ChaCha20, 'bitcoin/bip324/fs_chacha20'
|
|
13
14
|
autoload :FSChaCha20, 'bitcoin/bip324/fs_chacha20'
|
|
15
|
+
autoload :Poly1305, 'bitcoin/bip324/fs_chacha_poly1305'
|
|
14
16
|
autoload :FSChaCha20Poly1305, 'bitcoin/bip324/fs_chacha_poly1305'
|
|
15
17
|
|
|
16
18
|
FIELD_SIZE = 2**256 - 2**32 - 977
|
|
@@ -95,7 +97,7 @@ module Bitcoin
|
|
|
95
97
|
def xelligatorswift(x)
|
|
96
98
|
loop do
|
|
97
99
|
u = SecureRandom.random_number(1..ECDSA::Group::Secp256k1.order).to_s(16)
|
|
98
|
-
c =
|
|
100
|
+
c = SecureRandom.random_number(9)
|
|
99
101
|
t = xswiftec_inv(x, u, c)
|
|
100
102
|
unless t.nil?
|
|
101
103
|
return (ECDSA::Format::IntegerOctetString.encode(u.hex, 32) +
|
|
@@ -18,7 +18,9 @@ module Bitcoin
|
|
|
18
18
|
s[-8..-1].each_char do |c|
|
|
19
19
|
return false unless CHECKSUM_CHARSET.include?(c)
|
|
20
20
|
end
|
|
21
|
-
|
|
21
|
+
expanded = descsum_expand(s[0...-9])
|
|
22
|
+
return false unless expanded
|
|
23
|
+
symbols = expanded + s[-8..-1].each_char.map{|c|CHECKSUM_CHARSET.index(c)}
|
|
22
24
|
descsum_polymod(symbols) == 1
|
|
23
25
|
end
|
|
24
26
|
|
|
@@ -26,7 +28,9 @@ module Bitcoin
|
|
|
26
28
|
# @param [String] s Descriptor string without checksum.
|
|
27
29
|
# @return [String] Descriptor string with checksum.
|
|
28
30
|
def descsum_create(s)
|
|
29
|
-
|
|
31
|
+
expanded = descsum_expand(s)
|
|
32
|
+
raise ArgumentError, 'Descriptor contains a character which is not in the input charset.' unless expanded
|
|
33
|
+
symbols = expanded + [0, 0, 0, 0, 0, 0, 0, 0]
|
|
30
34
|
checksum = descsum_polymod(symbols) ^ 1
|
|
31
35
|
result = 8.times.map do |i|
|
|
32
36
|
CHECKSUM_CHARSET[(checksum >> (5 * (7 - i))) & 31]
|
|
@@ -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,20 +108,22 @@ 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]
|
|
117
|
-
|
|
117
|
+
# A depth 1 key derives its version bytes from the purpose, see #version.
|
|
118
|
+
new_key.ver = new_key.depth == 1 ? nil : version
|
|
118
119
|
new_key
|
|
119
120
|
end
|
|
120
121
|
|
|
121
122
|
# get version bytes using serialization format
|
|
122
123
|
def version
|
|
124
|
+
return ver if ver
|
|
123
125
|
return ExtKey.version_from_purpose(number) if depth == 1
|
|
124
|
-
|
|
126
|
+
Bitcoin.chain_params.extended_privkey_version
|
|
125
127
|
end
|
|
126
128
|
|
|
127
129
|
# get key type defined by BIP-178 using version.
|
|
@@ -280,20 +282,22 @@ module Bitcoin
|
|
|
280
282
|
data = pub.htb << [number].pack('N')
|
|
281
283
|
l = Bitcoin.hmac_sha512(chain_code, data)
|
|
282
284
|
left = l[0..31].bth.to_i(16)
|
|
283
|
-
raise 'invalid key' if left >= CURVE_ORDER
|
|
285
|
+
raise 'invalid key' if left >= CURVE_ORDER || left == 0
|
|
284
286
|
l_priv = ECDSA::Format::IntegerOctetString.encode(left, 32)
|
|
285
287
|
p1 = Bitcoin::Key.new(priv_key: l_priv.bth, key_type: Bitcoin::Key::TYPES[:uncompressed]).to_point
|
|
286
288
|
p2 = Bitcoin::Key.new(pubkey: pubkey, key_type: key_type).to_point
|
|
287
289
|
new_key.pubkey = (p1 + p2).to_hex
|
|
288
290
|
new_key.chain_code = l[32..-1]
|
|
289
|
-
|
|
291
|
+
# A depth 1 key derives its version bytes from the purpose, see #version.
|
|
292
|
+
new_key.ver = new_key.depth == 1 ? nil : version
|
|
290
293
|
new_key
|
|
291
294
|
end
|
|
292
295
|
|
|
293
296
|
# get version bytes using serialization format
|
|
294
297
|
def version
|
|
298
|
+
return ver if ver
|
|
295
299
|
return ExtPubkey.version_from_purpose(number) if depth == 1
|
|
296
|
-
|
|
300
|
+
Bitcoin.chain_params.extended_pubkey_version
|
|
297
301
|
end
|
|
298
302
|
|
|
299
303
|
# get key type defined by BIP-178 using version.
|
data/lib/bitcoin/key.rb
CHANGED
|
@@ -120,7 +120,7 @@ module Bitcoin
|
|
|
120
120
|
if low_r && !sig_has_low_r?(sig)
|
|
121
121
|
counter = 1
|
|
122
122
|
until sig_has_low_r?(sig)
|
|
123
|
-
extra_entropy = [counter].pack('
|
|
123
|
+
extra_entropy = [counter].pack('V').bth.ljust(64, '0').htb
|
|
124
124
|
sig = secp256k1_module.sign_data(data, priv_key, extra_entropy)
|
|
125
125
|
counter += 1
|
|
126
126
|
end
|
|
@@ -3,12 +3,12 @@ module Bitcoin
|
|
|
3
3
|
module CFParser
|
|
4
4
|
|
|
5
5
|
def parse_from_payload(payload)
|
|
6
|
-
type, start, hash = payload.unpack('
|
|
6
|
+
type, start, hash = payload.unpack('CVH*')
|
|
7
7
|
self.new(type, start, hash)
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
def to_payload
|
|
11
|
-
[filter_type, start_height, stop_hash].pack('
|
|
11
|
+
[filter_type, start_height, stop_hash].pack('CVH*')
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
end
|
|
@@ -16,13 +16,13 @@ module Bitcoin
|
|
|
16
16
|
@nonce = nonce
|
|
17
17
|
@short_ids = short_ids
|
|
18
18
|
@prefilled_txn = prefilled_txn
|
|
19
|
-
@siphash_key = Bitcoin.sha256(header.to_payload << [nonce].pack('q
|
|
19
|
+
@siphash_key = Bitcoin.sha256(header.to_payload << [nonce].pack('q<*'))[0...16]
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def self.parse_from_payload(payload)
|
|
23
23
|
buf = StringIO.new(payload)
|
|
24
24
|
header = Bitcoin::BlockHeader.parse_from_payload(buf.read(80))
|
|
25
|
-
nonce = buf.read(8).unpack1('q
|
|
25
|
+
nonce = buf.read(8).unpack1('q<*')
|
|
26
26
|
short_ids_len = Bitcoin.unpack_var_int_from_io(buf)
|
|
27
27
|
short_ids = short_ids_len.times.map do
|
|
28
28
|
buf.read(6).reverse.bth.to_i(16)
|
|
@@ -36,9 +36,9 @@ module Bitcoin
|
|
|
36
36
|
|
|
37
37
|
def to_payload
|
|
38
38
|
p = header.to_payload
|
|
39
|
-
p << [nonce].pack('q
|
|
39
|
+
p << [nonce].pack('q<*')
|
|
40
40
|
p << Bitcoin.pack_var_int(short_ids.size)
|
|
41
|
-
p << short_ids.map{|id|sprintf('%
|
|
41
|
+
p << short_ids.map{|id|sprintf('%012x', id).htb.reverse}.join
|
|
42
42
|
p << Bitcoin.pack_var_int(prefilled_txn.size)
|
|
43
43
|
p << prefilled_txn.map(&:to_payload).join
|
|
44
44
|
p
|
|
@@ -26,7 +26,7 @@ module Bitcoin
|
|
|
26
26
|
# parse inventory payload
|
|
27
27
|
def self.parse_from_payload(payload)
|
|
28
28
|
raise Error, 'invalid inventory size.' if payload.bytesize != 36
|
|
29
|
-
identifier = payload[0..
|
|
29
|
+
identifier = payload[0..3].unpack1('V')
|
|
30
30
|
hash = payload[4..-1].bth # internal byte order
|
|
31
31
|
new(identifier, hash)
|
|
32
32
|
end
|
|
@@ -107,7 +107,7 @@ module Bitcoin
|
|
|
107
107
|
has_time = buf.size > 26
|
|
108
108
|
addr = NetworkAddr.new(time: nil)
|
|
109
109
|
addr.time = buf.read(4).unpack1('V') if has_time
|
|
110
|
-
addr.services = buf.read(8).unpack1('Q')
|
|
110
|
+
addr.services = buf.read(8).unpack1('Q<')
|
|
111
111
|
addr.addr = IPAddr::new_ntoh(buf.read(16))
|
|
112
112
|
addr.port = buf.read(2).unpack1('n')
|
|
113
113
|
addr
|
|
@@ -153,7 +153,7 @@ module Bitcoin
|
|
|
153
153
|
p = ''
|
|
154
154
|
p << [time].pack('V') unless skip_time
|
|
155
155
|
ip = addr.ipv4? ? addr.ipv4_mapped : addr
|
|
156
|
-
p << [services].pack('Q') << ip.hton << [port].pack('n')
|
|
156
|
+
p << [services].pack('Q<') << ip.hton << [port].pack('n')
|
|
157
157
|
end
|
|
158
158
|
|
|
159
159
|
def v2_payload
|
|
@@ -163,18 +163,22 @@ module Bitcoin
|
|
|
163
163
|
case net
|
|
164
164
|
when NETWORK_ID[:ipv4]
|
|
165
165
|
p << Bitcoin.pack_var_int(4)
|
|
166
|
-
p << addr.
|
|
166
|
+
p << addr.hton
|
|
167
167
|
when NETWORK_ID[:ipv6]
|
|
168
168
|
p << Bitcoin.pack_var_int(16)
|
|
169
169
|
p << addr.hton
|
|
170
170
|
when NETWORK_ID[:tor_v2]
|
|
171
171
|
p << Bitcoin.pack_var_int(10)
|
|
172
|
+
p << addr.htb
|
|
172
173
|
when NETWORK_ID[:tor_v3]
|
|
173
174
|
p << Bitcoin.pack_var_int(32)
|
|
175
|
+
p << addr.htb
|
|
174
176
|
when NETWORK_ID[:i2p]
|
|
175
177
|
p << Bitcoin.pack_var_int(32)
|
|
178
|
+
p << addr.htb
|
|
176
179
|
when NETWORK_ID[:cjdns]
|
|
177
180
|
p << Bitcoin.pack_var_int(16)
|
|
181
|
+
p << addr.hton
|
|
178
182
|
end
|
|
179
183
|
p << [port].pack('n')
|
|
180
184
|
p
|