bitcoinrb 1.4.0 → 1.6.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 +2 -2
- data/.ruby-version +1 -1
- data/README.md +16 -5
- data/bitcoinrb.gemspec +2 -2
- data/lib/bitcoin/bip324/cipher.rb +113 -0
- data/lib/bitcoin/bip324/ell_swift_pubkey.rb +42 -0
- data/lib/bitcoin/bip324/fs_chacha20.rb +132 -0
- data/lib/bitcoin/bip324/fs_chacha_poly1305.rb +129 -0
- data/lib/bitcoin/bip324.rb +144 -0
- data/lib/bitcoin/descriptor/addr.rb +31 -0
- data/lib/bitcoin/descriptor/checksum.rb +74 -0
- data/lib/bitcoin/descriptor/combo.rb +30 -0
- data/lib/bitcoin/descriptor/expression.rb +122 -0
- data/lib/bitcoin/descriptor/key_expression.rb +23 -0
- data/lib/bitcoin/descriptor/multi.rb +49 -0
- data/lib/bitcoin/descriptor/multi_a.rb +43 -0
- data/lib/bitcoin/descriptor/pk.rb +27 -0
- data/lib/bitcoin/descriptor/pkh.rb +15 -0
- data/lib/bitcoin/descriptor/raw.rb +32 -0
- data/lib/bitcoin/descriptor/script_expression.rb +24 -0
- data/lib/bitcoin/descriptor/sh.rb +31 -0
- data/lib/bitcoin/descriptor/sorted_multi.rb +15 -0
- data/lib/bitcoin/descriptor/sorted_multi_a.rb +15 -0
- data/lib/bitcoin/descriptor/tr.rb +91 -0
- data/lib/bitcoin/descriptor/wpkh.rb +19 -0
- data/lib/bitcoin/descriptor/wsh.rb +30 -0
- data/lib/bitcoin/descriptor.rb +176 -100
- data/lib/bitcoin/ext/ecdsa.rb +0 -6
- data/lib/bitcoin/key.rb +16 -4
- data/lib/bitcoin/message_sign.rb +13 -8
- data/lib/bitcoin/script/script.rb +8 -3
- data/lib/bitcoin/secp256k1/native.rb +62 -6
- data/lib/bitcoin/secp256k1/ruby.rb +21 -4
- data/lib/bitcoin/taproot/custom_depth_builder.rb +64 -0
- data/lib/bitcoin/taproot/simple_builder.rb +1 -6
- data/lib/bitcoin/taproot.rb +1 -0
- data/lib/bitcoin/tx.rb +1 -1
- data/lib/bitcoin/util.rb +11 -3
- data/lib/bitcoin/version.rb +1 -1
- data/lib/bitcoin.rb +1 -0
- metadata +30 -7
data/lib/bitcoin/descriptor.rb
CHANGED
@@ -3,145 +3,221 @@ module Bitcoin
|
|
3
3
|
module Descriptor
|
4
4
|
|
5
5
|
include Bitcoin::Opcodes
|
6
|
-
|
7
|
-
|
6
|
+
autoload :Expression, 'bitcoin/descriptor/expression'
|
7
|
+
autoload :KeyExpression, 'bitcoin/descriptor/key_expression'
|
8
|
+
autoload :ScriptExpression, 'bitcoin/descriptor/script_expression'
|
9
|
+
autoload :Pk, 'bitcoin/descriptor/pk'
|
10
|
+
autoload :Pkh, 'bitcoin/descriptor/pkh'
|
11
|
+
autoload :Wpkh, 'bitcoin/descriptor/wpkh'
|
12
|
+
autoload :Sh, 'bitcoin/descriptor/sh'
|
13
|
+
autoload :Wsh, 'bitcoin/descriptor/wsh'
|
14
|
+
autoload :Combo, 'bitcoin/descriptor/combo'
|
15
|
+
autoload :Multi, 'bitcoin/descriptor/multi'
|
16
|
+
autoload :SortedMulti, 'bitcoin/descriptor/sorted_multi'
|
17
|
+
autoload :Raw, 'bitcoin/descriptor/raw'
|
18
|
+
autoload :Addr, 'bitcoin/descriptor/addr'
|
19
|
+
autoload :Tr, 'bitcoin/descriptor/tr'
|
20
|
+
autoload :MultiA, 'bitcoin/descriptor/multi_a'
|
21
|
+
autoload :SortedMultiA, 'bitcoin/descriptor/sorted_multi_a'
|
22
|
+
autoload :Checksum, 'bitcoin/descriptor/checksum'
|
23
|
+
|
24
|
+
module_function
|
25
|
+
|
26
|
+
# Generate P2PK output for the given public key.
|
8
27
|
# @param [String] key private key or public key with hex format
|
9
|
-
# @return [Bitcoin::
|
28
|
+
# @return [Bitcoin::Descriptor::Pk]
|
10
29
|
def pk(key)
|
11
|
-
|
30
|
+
Pk.new(key)
|
12
31
|
end
|
13
32
|
|
14
|
-
#
|
33
|
+
# Generate P2PKH output for the given public key.
|
15
34
|
# @param [String] key private key or public key with hex format.
|
16
|
-
# @return [Bitcoin::
|
35
|
+
# @return [Bitcoin::Descriptor::Pkh]
|
17
36
|
def pkh(key)
|
18
|
-
|
37
|
+
Pkh.new(key)
|
19
38
|
end
|
20
39
|
|
21
|
-
#
|
40
|
+
# Generate P2PKH output for the given public key.
|
22
41
|
# @param [String] key private key or public key with hex format.
|
23
|
-
# @return [Bitcoin::
|
42
|
+
# @return [Bitcoin::Descriptor::Wpkh]
|
24
43
|
def wpkh(key)
|
25
|
-
|
26
|
-
raise ArgumentError, "Uncompressed key are not allowed." unless compressed_key?(pubkey)
|
27
|
-
Bitcoin::Script.to_p2wpkh(Bitcoin.hash160(pubkey))
|
44
|
+
Wpkh.new(key)
|
28
45
|
end
|
29
46
|
|
30
|
-
#
|
31
|
-
# @param [
|
32
|
-
# @return [Bitcoin::
|
33
|
-
def sh(
|
34
|
-
|
35
|
-
raise ArgumentError, "P2SH script is too large, 547 bytes is larger than #{Bitcoin::MAX_SCRIPT_ELEMENT_SIZE} bytes." if script.htb.bytesize > Bitcoin::MAX_SCRIPT_ELEMENT_SIZE
|
36
|
-
Bitcoin::Script.to_p2sh(Bitcoin.hash160(script))
|
47
|
+
# Generate P2SH embed the argument.
|
48
|
+
# @param [Bitcoin::Descriptor::Base] exp script expression to be embed.
|
49
|
+
# @return [Bitcoin::Descriptor::Sh]
|
50
|
+
def sh(exp)
|
51
|
+
Sh.new(exp)
|
37
52
|
end
|
38
53
|
|
39
|
-
#
|
40
|
-
# @param [
|
41
|
-
# @return [Bitcoin::
|
42
|
-
def wsh(
|
43
|
-
|
44
|
-
raise ArgumentError, "P2SH script is too large, 547 bytes is larger than #{Bitcoin::MAX_SCRIPT_ELEMENT_SIZE} bytes." if script.to_payload.bytesize > Bitcoin::MAX_SCRIPT_ELEMENT_SIZE
|
45
|
-
raise ArgumentError, "Uncompressed key are not allowed." if script.get_pubkeys.any?{|p|!compressed_key?(p)}
|
46
|
-
Bitcoin::Script.to_p2wsh(script)
|
54
|
+
# Generate P2WSH embed the argument.
|
55
|
+
# @param [Bitcoin::Descriptor::Expression] exp script expression to be embed.
|
56
|
+
# @return [Bitcoin::Descriptor::Wsh]
|
57
|
+
def wsh(exp)
|
58
|
+
Wsh.new(exp)
|
47
59
|
end
|
48
60
|
|
49
|
-
#
|
61
|
+
# An alias for the collection of `pk(KEY)` and `pkh(KEY)`.
|
50
62
|
# If the key is compressed, it also includes `wpkh(KEY)` and `sh(wpkh(KEY))`.
|
51
63
|
# @param [String] key private key or public key with hex format.
|
52
|
-
# @return [
|
64
|
+
# @return [Bitcoin::Descriptor::Combo]
|
53
65
|
def combo(key)
|
54
|
-
|
55
|
-
pubkey = extract_pubkey(key)
|
56
|
-
if compressed_key?(pubkey)
|
57
|
-
result << wpkh(key)
|
58
|
-
result << sh(result.last)
|
59
|
-
end
|
60
|
-
result
|
66
|
+
Combo.new(key)
|
61
67
|
end
|
62
68
|
|
63
|
-
#
|
69
|
+
# Generate multisig output for given keys.
|
64
70
|
# @param [Integer] threshold the threshold of multisig.
|
65
71
|
# @param [Array[String]] keys an array of keys.
|
66
|
-
# @return [Bitcoin::
|
67
|
-
def multi(threshold, *keys
|
68
|
-
|
69
|
-
raise ArgumentError, 'Multisig threshold cannot be 0, must be at least 1.' unless threshold > 0
|
70
|
-
raise ArgumentError, 'Multisig threshold cannot be larger than the number of keys.' if threshold > keys.size
|
71
|
-
raise ArgumentError, 'Multisig must have between 1 and 16 keys, inclusive.' if keys.size > 16
|
72
|
-
pubkeys = keys.map{|key| extract_pubkey(key) }
|
73
|
-
Bitcoin::Script.to_multisig_script(threshold, pubkeys, sort: sort)
|
72
|
+
# @return [Bitcoin::Descriptor::Multi] multisig script.
|
73
|
+
def multi(threshold, *keys)
|
74
|
+
Multi.new(threshold, keys)
|
74
75
|
end
|
75
76
|
|
76
|
-
#
|
77
|
+
# Generate sorted multisig output for given keys.
|
77
78
|
# @param [Integer] threshold the threshold of multisig.
|
78
79
|
# @param [Array[String]] keys an array of keys.
|
79
|
-
# @return [Bitcoin::
|
80
|
+
# @return [Bitcoin::Descriptor::SortedMulti]
|
80
81
|
def sortedmulti(threshold, *keys)
|
81
|
-
|
82
|
-
end
|
83
|
-
|
84
|
-
private
|
85
|
-
|
86
|
-
# extract public key from KEY format.
|
87
|
-
# @param [String] key KEY string.
|
88
|
-
# @return [String] public key.
|
89
|
-
def extract_pubkey(key)
|
90
|
-
if key.start_with?('[') # BIP32 fingerprint
|
91
|
-
raise ArgumentError, 'Invalid key origin.' if key.count('[') > 1 || key.count(']') > 1
|
92
|
-
info = key[1...key.index(']')] # TODO
|
93
|
-
fingerprint, *paths = info.split('/')
|
94
|
-
raise ArgumentError, 'Fingerprint is not hex.' unless fingerprint.valid_hex?
|
95
|
-
raise ArgumentError, 'Fingerprint is not 4 bytes.' unless fingerprint.size == 8
|
96
|
-
key = key[(key.index(']') + 1)..-1]
|
97
|
-
else
|
98
|
-
raise ArgumentError, 'Invalid key origin.' if key.include?(']')
|
99
|
-
end
|
82
|
+
SortedMulti.new(threshold, keys)
|
83
|
+
end
|
100
84
|
|
101
|
-
|
102
|
-
|
85
|
+
# Generate raw output script about +hex+.
|
86
|
+
# @param [String] hex Hex string of bitcoin script.
|
87
|
+
# @return [Bitcoin::Descriptor::Raw]
|
88
|
+
def raw(hex)
|
89
|
+
Raw.new(hex)
|
90
|
+
end
|
103
91
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
92
|
+
# Generate raw output script about +hex+.
|
93
|
+
# @param [String] addr Bitcoin address.
|
94
|
+
# @return [Bitcoin::Descriptor::Addr]
|
95
|
+
def addr(addr)
|
96
|
+
Addr.new(addr)
|
97
|
+
end
|
98
|
+
|
99
|
+
# Generate taproot output script descriptor.
|
100
|
+
# @param [String] key
|
101
|
+
# @param [String] tree
|
102
|
+
# @return [Bitcoin::Descriptor::Tr]
|
103
|
+
def tr(key, tree = nil)
|
104
|
+
Tr.new(key, tree)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Generate tapscript multisig output for given keys.
|
108
|
+
# @param [Integer] threshold the threshold of multisig.
|
109
|
+
# @param [Array[String]] keys an array of keys.
|
110
|
+
# @return [Bitcoin::Descriptor::MultiA] multisig script.
|
111
|
+
def multi_a(threshold, *keys)
|
112
|
+
MultiA.new(threshold, keys)
|
113
|
+
end
|
114
|
+
|
115
|
+
# Generate tapscript sorted multisig output for given keys.
|
116
|
+
# @param [Integer] threshold the threshold of multisig.
|
117
|
+
# @param [Array[String]] keys an array of keys.
|
118
|
+
# @return [Bitcoin::Descriptor::SortedMulti]
|
119
|
+
def sortedmulti_a(threshold, *keys)
|
120
|
+
SortedMultiA.new(threshold, keys)
|
121
|
+
end
|
122
|
+
|
123
|
+
# Parse descriptor string.
|
124
|
+
# @param [String] string Descriptor string.
|
125
|
+
# @return [Bitcoin::Descriptor::Expression]
|
126
|
+
def parse(string, top_level = true)
|
127
|
+
validate_checksum!(string)
|
128
|
+
content, _ = string.split('#')
|
129
|
+
exp, args_str = content.match(/(\w+)\((.+)\)/).captures
|
130
|
+
case exp
|
131
|
+
when 'pk'
|
132
|
+
pk(args_str)
|
133
|
+
when 'pkh'
|
134
|
+
pkh(args_str)
|
135
|
+
when 'wpkh'
|
136
|
+
wpkh(args_str)
|
137
|
+
when 'sh'
|
138
|
+
sh(parse(args_str, false))
|
139
|
+
when 'wsh'
|
140
|
+
wsh(parse(args_str, false))
|
141
|
+
when 'combo'
|
142
|
+
combo(args_str)
|
143
|
+
when 'multi', 'sortedmulti', 'multi_a', 'sortedmulti_a'
|
144
|
+
args = args_str.split(',')
|
145
|
+
threshold = args[0].to_i
|
146
|
+
keys = args[1..-1]
|
147
|
+
case exp
|
148
|
+
when 'multi'
|
149
|
+
multi(threshold, *keys)
|
150
|
+
when 'sortedmulti'
|
151
|
+
sortedmulti(threshold, *keys)
|
152
|
+
when 'multi_a'
|
153
|
+
raise ArgumentError, "Can only have multi_a/sortedmulti_a inside tr()." if top_level
|
154
|
+
multi_a(threshold, *keys)
|
155
|
+
when 'sortedmulti_a'
|
156
|
+
raise ArgumentError, "Can only have multi_a/sortedmulti_a inside tr()." if top_level
|
157
|
+
sortedmulti_a(threshold, *keys)
|
116
158
|
end
|
159
|
+
when 'raw'
|
160
|
+
raw(args_str)
|
161
|
+
when 'addr'
|
162
|
+
addr(args_str)
|
163
|
+
when 'tr'
|
164
|
+
key, rest = args_str.split(',', 2)
|
165
|
+
if rest.nil?
|
166
|
+
tr(key)
|
167
|
+
elsif rest.start_with?('{')
|
168
|
+
tr(key, parse_nested_string(rest))
|
169
|
+
else
|
170
|
+
tr(key, parse(rest, false))
|
171
|
+
end
|
172
|
+
else
|
173
|
+
raise ArgumentError, "Parse failed: #{string}"
|
117
174
|
end
|
118
|
-
key = key.is_a?(Bitcoin::Key) ? key : key.key
|
119
|
-
raise ArgumentError, Errors::Messages::INVALID_PUBLIC_KEY unless key.fully_valid_pubkey?
|
120
|
-
key.pubkey
|
121
175
|
end
|
122
176
|
|
123
|
-
|
124
|
-
|
177
|
+
# Validate descriptor checksum.
|
178
|
+
# @raise [ArgumentError] If +descriptor+ has invalid checksum.
|
179
|
+
def validate_checksum!(descriptor)
|
180
|
+
return unless descriptor.include?("#")
|
181
|
+
content, *checksums = descriptor.split("#")
|
182
|
+
raise ArgumentError, "Multiple '#' symbols." if checksums.length > 1
|
183
|
+
checksum = checksums.first
|
184
|
+
len = checksum.nil? ? 0 : checksum.length
|
185
|
+
raise ArgumentError, "Expected 8 character checksum, not #{len} characters." unless len == 8
|
186
|
+
_, calc_checksum = Checksum.descsum_create(content).split('#')
|
187
|
+
unless calc_checksum == checksum
|
188
|
+
raise ArgumentError, "Provided checksum '#{checksum}' does not match computed checksum '#{calc_checksum}'."
|
189
|
+
end
|
125
190
|
end
|
126
191
|
|
127
|
-
def
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
192
|
+
def parse_nested_string(string)
|
193
|
+
return nil if string.nil?
|
194
|
+
stack = []
|
195
|
+
current = []
|
196
|
+
buffer = ""
|
197
|
+
string.each_char do |c|
|
198
|
+
case c
|
199
|
+
when '{'
|
200
|
+
stack << current
|
201
|
+
current = []
|
202
|
+
when '}'
|
203
|
+
unless buffer.empty?
|
204
|
+
current << parse(buffer, false)
|
205
|
+
buffer = ""
|
206
|
+
end
|
207
|
+
nested = current
|
208
|
+
current = stack.pop
|
209
|
+
current << nested
|
210
|
+
when ','
|
211
|
+
unless buffer.empty?
|
212
|
+
current << parse(buffer, false)
|
213
|
+
buffer = ""
|
214
|
+
end
|
136
215
|
else
|
137
|
-
|
138
|
-
raise ArgumentError, 'Key path value is out of range.' if path.to_i >= Bitcoin::HARDENED_THRESHOLD
|
139
|
-
key = key.derive(path.to_i)
|
216
|
+
buffer << c
|
140
217
|
end
|
141
218
|
end
|
142
|
-
|
219
|
+
current << parse(buffer, false) unless buffer.empty?
|
220
|
+
current.first
|
143
221
|
end
|
144
|
-
|
145
222
|
end
|
146
|
-
|
147
223
|
end
|
data/lib/bitcoin/ext/ecdsa.rb
CHANGED
@@ -10,12 +10,6 @@ class ::ECDSA::Signature
|
|
10
10
|
end
|
11
11
|
end
|
12
12
|
|
13
|
-
class ::ECDSA::Point
|
14
|
-
def to_hex(compression = true)
|
15
|
-
ECDSA::Format::PointOctetString.encode(self, compression: compression).bth
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
13
|
module ::ECDSA::Format::PointOctetString
|
20
14
|
|
21
15
|
class << self
|
data/lib/bitcoin/key.rb
CHANGED
@@ -83,7 +83,7 @@ module Bitcoin
|
|
83
83
|
# @return [Bitcoin::Key] key object has public key.
|
84
84
|
def self.from_xonly_pubkey(xonly_pubkey)
|
85
85
|
raise ArgumentError, "xonly_pubkey must be #{X_ONLY_PUBKEY_SIZE} bytes" unless xonly_pubkey.htb.bytesize == X_ONLY_PUBKEY_SIZE
|
86
|
-
Bitcoin::Key.new(pubkey: "02#{xonly_pubkey}", key_type: TYPES[:
|
86
|
+
Bitcoin::Key.new(pubkey: "02#{xonly_pubkey}", key_type: TYPES[:p2tr])
|
87
87
|
end
|
88
88
|
|
89
89
|
# Generate from public key point.
|
@@ -146,9 +146,9 @@ module Bitcoin
|
|
146
146
|
# @return [Bitcoin::Key] Recovered public key.
|
147
147
|
def self.recover_compact(data, signature)
|
148
148
|
rec_id = signature.unpack1('C')
|
149
|
-
rec = rec_id - Bitcoin::Key::COMPACT_SIG_HEADER_BYTE
|
150
|
-
raise ArgumentError, 'Invalid signature parameter' if rec < 0 || rec >
|
151
|
-
|
149
|
+
rec = (rec_id - Bitcoin::Key::COMPACT_SIG_HEADER_BYTE) & 3
|
150
|
+
raise ArgumentError, 'Invalid signature parameter' if rec < 0 || rec > 3
|
151
|
+
|
152
152
|
compressed = (rec_id - Bitcoin::Key::COMPACT_SIG_HEADER_BYTE) & 4 != 0
|
153
153
|
Bitcoin.secp_impl.recover_compact(data, signature, rec, compressed)
|
154
154
|
end
|
@@ -344,6 +344,18 @@ module Bitcoin
|
|
344
344
|
valid_pubkey? && secp256k1_module.parse_ec_pubkey?(pubkey, allow_hybrid)
|
345
345
|
end
|
346
346
|
|
347
|
+
# Create an ellswift-encoded public key for this key, with specified entropy.
|
348
|
+
# @return [Bitcoin::BIP324::EllSwiftPubkey]
|
349
|
+
# @raise ArgumentError If ent32 does not 32 bytes.
|
350
|
+
def create_ell_pubkey
|
351
|
+
raise ArgumentError, "private_key required." unless priv_key
|
352
|
+
if secp256k1_module.is_a?(Bitcoin::Secp256k1::Native)
|
353
|
+
Bitcoin::BIP324::EllSwiftPubkey.new(secp256k1_module.ellswift_create(priv_key))
|
354
|
+
else
|
355
|
+
Bitcoin::BIP324::EllSwiftPubkey.new(Bitcoin::BIP324.xelligatorswift(xonly_pubkey))
|
356
|
+
end
|
357
|
+
end
|
358
|
+
|
347
359
|
private
|
348
360
|
|
349
361
|
def self.compare_big_endian(c1, c2)
|
data/lib/bitcoin/message_sign.rb
CHANGED
@@ -50,18 +50,22 @@ module Bitcoin
|
|
50
50
|
# @param [String] message The message that was signed.
|
51
51
|
# @return [Boolean] Verification result.
|
52
52
|
def verify_message(address, signature, message, prefix: Bitcoin.chain_params.message_magic)
|
53
|
-
|
53
|
+
addr_script = Bitcoin::Script.parse_from_addr(address)
|
54
54
|
begin
|
55
55
|
sig = Base64.strict_decode64(signature)
|
56
56
|
rescue ArgumentError
|
57
57
|
raise ArgumentError, 'Invalid signature'
|
58
58
|
end
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
59
|
+
if addr_script.p2pkh?
|
60
|
+
begin
|
61
|
+
# Legacy verification
|
62
|
+
pubkey = Bitcoin::Key.recover_compact(message_hash(message, prefix: prefix, legacy: true), sig)
|
63
|
+
return false unless pubkey
|
64
|
+
pubkey.to_p2pkh == address
|
65
|
+
rescue RuntimeError
|
66
|
+
return false
|
67
|
+
end
|
68
|
+
elsif addr_script.witness_program?
|
65
69
|
# BIP322 verification
|
66
70
|
tx = to_sign_tx(message_hash(message, prefix: prefix, legacy: false), address)
|
67
71
|
tx.in[0].script_witness = Bitcoin::ScriptWitness.parse_from_payload(sig)
|
@@ -69,6 +73,8 @@ module Bitcoin
|
|
69
73
|
tx_out = Bitcoin::TxOut.new(script_pubkey: script_pubkey)
|
70
74
|
interpreter = Bitcoin::ScriptInterpreter.new(checker: Bitcoin::TxChecker.new(tx: tx, input_index: 0, prevouts: [tx_out]))
|
71
75
|
interpreter.verify_script(Bitcoin::Script.new, script_pubkey, tx.in[0].script_witness)
|
76
|
+
else
|
77
|
+
raise ArgumentError, "This address unsupported."
|
72
78
|
end
|
73
79
|
end
|
74
80
|
|
@@ -82,7 +88,6 @@ module Bitcoin
|
|
82
88
|
end
|
83
89
|
|
84
90
|
def validate_address!(address)
|
85
|
-
raise ArgumentError, 'Invalid address' unless Bitcoin.valid_address?(address)
|
86
91
|
script = Bitcoin::Script.parse_from_addr(address)
|
87
92
|
raise ArgumentError, 'This address unsupported' if script.p2sh? || script.p2wsh?
|
88
93
|
end
|
@@ -55,7 +55,9 @@ module Bitcoin
|
|
55
55
|
|
56
56
|
# generate p2sh script with this as a redeem script
|
57
57
|
# @return [Script] P2SH script
|
58
|
+
# @raise [RuntimeError] If the script size exceeds 520 bytes
|
58
59
|
def to_p2sh
|
60
|
+
raise RuntimeError, "P2SH redeem script must be 520 bytes or less." if size > Bitcoin::MAX_SCRIPT_ELEMENT_SIZE
|
59
61
|
Script.to_p2sh(to_hash160)
|
60
62
|
end
|
61
63
|
|
@@ -74,9 +76,11 @@ module Bitcoin
|
|
74
76
|
end
|
75
77
|
|
76
78
|
# generate p2wsh script for +redeem_script+
|
77
|
-
# @param [Script] redeem_script target redeem script
|
78
|
-
# @
|
79
|
+
# @param [Bitcoin::Script] redeem_script target redeem script
|
80
|
+
# @return [Bitcoin::Script] p2wsh script
|
81
|
+
# @raise [ArgumentError] If the script size exceeds 10,000 bytes
|
79
82
|
def self.to_p2wsh(redeem_script)
|
83
|
+
raise ArgumentError, 'P2WSH witness script must be 10,000 bytes or less.' if redeem_script.size > Bitcoin::MAX_SCRIPT_SIZE
|
80
84
|
new << WITNESS_VERSION_V0 << redeem_script.to_sha256
|
81
85
|
end
|
82
86
|
|
@@ -146,7 +150,7 @@ module Bitcoin
|
|
146
150
|
end
|
147
151
|
if buf.eof?
|
148
152
|
s.chunks << [len].pack('C')
|
149
|
-
else
|
153
|
+
else
|
150
154
|
chunk = (packed_size ? (opcode + packed_size) : (opcode)) + buf.read(len)
|
151
155
|
s.chunks << chunk
|
152
156
|
end
|
@@ -555,6 +559,7 @@ module Bitcoin
|
|
555
559
|
return 'multisig' if multisig?
|
556
560
|
return 'witness_v0_keyhash' if p2wpkh?
|
557
561
|
return 'witness_v0_scripthash' if p2wsh?
|
562
|
+
return 'witness_v1_taproot' if p2tr?
|
558
563
|
'nonstandard'
|
559
564
|
end
|
560
565
|
|
@@ -5,9 +5,9 @@ module Bitcoin
|
|
5
5
|
module Secp256k1
|
6
6
|
|
7
7
|
# binding for secp256k1 (https://github.com/bitcoin-core/secp256k1/)
|
8
|
-
#
|
8
|
+
# tag: v0.4.0
|
9
9
|
# this is not included by default, to enable set shared object path to ENV['SECP256K1_LIB_PATH']
|
10
|
-
# for linux, ENV['SECP256K1_LIB_PATH'] = '/usr/local/lib/libsecp256k1.so'
|
10
|
+
# for linux, ENV['SECP256K1_LIB_PATH'] = '/usr/local/lib/libsecp256k1.so' or '/usr/lib64/libsecp256k1.so'
|
11
11
|
# for mac,
|
12
12
|
module Native
|
13
13
|
include ::FFI::Library
|
@@ -50,14 +50,20 @@ module Bitcoin
|
|
50
50
|
attach_function(:secp256k1_ecdsa_signature_parse_der, [:pointer, :pointer, :pointer, :size_t], :int)
|
51
51
|
attach_function(:secp256k1_ecdsa_signature_normalize, [:pointer, :pointer, :pointer], :int)
|
52
52
|
attach_function(:secp256k1_ecdsa_verify, [:pointer, :pointer, :pointer, :pointer], :int)
|
53
|
-
attach_function(:
|
54
|
-
attach_function(:secp256k1_schnorrsig_verify, [:pointer, :pointer, :pointer, :pointer], :int)
|
53
|
+
attach_function(:secp256k1_schnorrsig_sign32, [:pointer, :pointer, :pointer, :pointer, :pointer], :int)
|
54
|
+
attach_function(:secp256k1_schnorrsig_verify, [:pointer, :pointer, :pointer, :size_t, :pointer], :int)
|
55
55
|
attach_function(:secp256k1_keypair_create, [:pointer, :pointer, :pointer], :int)
|
56
56
|
attach_function(:secp256k1_xonly_pubkey_parse, [:pointer, :pointer, :pointer], :int)
|
57
57
|
attach_function(:secp256k1_ecdsa_sign_recoverable, [:pointer, :pointer, :pointer, :pointer, :pointer, :pointer], :int)
|
58
58
|
attach_function(:secp256k1_ecdsa_recoverable_signature_serialize_compact, [:pointer, :pointer, :pointer, :pointer], :int)
|
59
59
|
attach_function(:secp256k1_ecdsa_recover, [:pointer, :pointer, :pointer, :pointer], :int)
|
60
60
|
attach_function(:secp256k1_ecdsa_recoverable_signature_parse_compact, [:pointer, :pointer, :pointer, :int], :int)
|
61
|
+
attach_function(:secp256k1_ellswift_decode, [:pointer, :pointer, :pointer], :int)
|
62
|
+
attach_function(:secp256k1_ellswift_create, [:pointer, :pointer, :pointer, :pointer], :int)
|
63
|
+
# Define function pointer
|
64
|
+
callback(:secp256k1_ellswift_xdh_hash_function, [:pointer, :pointer, :pointer, :pointer, :pointer], :int)
|
65
|
+
attach_variable(:secp256k1_ellswift_xdh_hash_function_bip324, :secp256k1_ellswift_xdh_hash_function)
|
66
|
+
attach_function(:secp256k1_ellswift_xdh, [:pointer, :pointer, :pointer, :pointer, :pointer, :int, :pointer, :pointer], :int)
|
61
67
|
end
|
62
68
|
|
63
69
|
def with_context(flags: (SECP256K1_CONTEXT_VERIFY | SECP256K1_CONTEXT_SIGN))
|
@@ -224,6 +230,56 @@ module Bitcoin
|
|
224
230
|
true
|
225
231
|
end
|
226
232
|
|
233
|
+
# Decode ellswift public key.
|
234
|
+
# @param [String] ell_key ElligatorSwift key with binary format.
|
235
|
+
# @return [String] Decoded public key with hex format
|
236
|
+
def ellswift_decode(ell_key)
|
237
|
+
with_context do |context|
|
238
|
+
ell64 = FFI::MemoryPointer.new(:uchar, ell_key.bytesize).put_bytes(0, ell_key)
|
239
|
+
internal = FFI::MemoryPointer.new(:uchar, 64)
|
240
|
+
result = secp256k1_ellswift_decode(context, internal, ell64)
|
241
|
+
raise ArgumentError, 'Decode failed.' unless result == 1
|
242
|
+
serialize_pubkey_internal(context, internal, true)
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
# Compute an ElligatorSwift public key for a secret key.
|
247
|
+
# @param [String] priv_key private key with hex format
|
248
|
+
# @return [String] ElligatorSwift public key with hex format.
|
249
|
+
def ellswift_create(priv_key)
|
250
|
+
with_context(flags: SECP256K1_CONTEXT_SIGN) do |context|
|
251
|
+
ell64 = FFI::MemoryPointer.new(:uchar, 64)
|
252
|
+
seckey32 = FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, priv_key.htb)
|
253
|
+
result = secp256k1_ellswift_create(context, ell64, seckey32, nil)
|
254
|
+
raise ArgumentError, 'Failed to create ElligatorSwift public key.' unless result == 1
|
255
|
+
ell64.read_string(64).bth
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
# Compute X coordinate of shared ECDH point between elswift pubkey and privkey.
|
260
|
+
# @param [Bitcoin::BIP324::EllSwiftPubkey] their_ell_pubkey Their EllSwift public key.
|
261
|
+
# @param [Bitcoin::BIP324::EllSwiftPubkey] our_ell_pubkey Our EllSwift public key.
|
262
|
+
# @param [String] priv_key private key with hex format.
|
263
|
+
# @param [Boolean] initiating Whether your initiator or not.
|
264
|
+
# @return [String] x coordinate with hex format.
|
265
|
+
def ellswift_ecdh_xonly(their_ell_pubkey, our_ell_pubkey, priv_key, initiating)
|
266
|
+
with_context(flags: SECP256K1_CONTEXT_SIGN) do |context|
|
267
|
+
output = FFI::MemoryPointer.new(:uchar, 32)
|
268
|
+
our_ell_ptr = FFI::MemoryPointer.new(:uchar, 64).put_bytes(0, our_ell_pubkey.key)
|
269
|
+
their_ell_ptr = FFI::MemoryPointer.new(:uchar, 64).put_bytes(0, their_ell_pubkey.key)
|
270
|
+
seckey32 = FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, priv_key.htb)
|
271
|
+
hashfp = secp256k1_ellswift_xdh_hash_function_bip324
|
272
|
+
result = secp256k1_ellswift_xdh(context, output,
|
273
|
+
initiating ? our_ell_ptr : their_ell_ptr,
|
274
|
+
initiating ? their_ell_ptr : our_ell_ptr,
|
275
|
+
seckey32,
|
276
|
+
initiating ? 0 : 1,
|
277
|
+
hashfp, nil)
|
278
|
+
raise ArgumentError, "secret was invalid or hashfp returned 0" unless result == 1
|
279
|
+
output.read_string(32).bth
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
227
283
|
private
|
228
284
|
|
229
285
|
# Calculate full public key(64 bytes) from public key(32 bytes).
|
@@ -280,7 +336,7 @@ module Bitcoin
|
|
280
336
|
signature = FFI::MemoryPointer.new(:uchar, 64)
|
281
337
|
msg32 = FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, data)
|
282
338
|
aux_rand = FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, aux_rand) if aux_rand
|
283
|
-
raise 'Failed to generate schnorr signature.' unless
|
339
|
+
raise 'Failed to generate schnorr signature.' unless secp256k1_schnorrsig_sign32(context, signature, msg32, keypair, aux_rand) == 1
|
284
340
|
signature.read_string(64)
|
285
341
|
end
|
286
342
|
end
|
@@ -316,7 +372,7 @@ module Bitcoin
|
|
316
372
|
xonly_pubkey = FFI::MemoryPointer.new(:uchar, pubkey.bytesize).put_bytes(0, pubkey)
|
317
373
|
signature = FFI::MemoryPointer.new(:uchar, sig.bytesize).put_bytes(0, sig)
|
318
374
|
msg32 = FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, data)
|
319
|
-
result = secp256k1_schnorrsig_verify(context, signature, msg32, xonly_pubkey)
|
375
|
+
result = secp256k1_schnorrsig_verify(context, signature, msg32, 32, xonly_pubkey)
|
320
376
|
result == 1
|
321
377
|
end
|
322
378
|
end
|
@@ -77,13 +77,30 @@ module Bitcoin
|
|
77
77
|
# @param [Boolean] compressed whether compressed public key or not.
|
78
78
|
# @return [Bitcoin::Key] Recovered public key.
|
79
79
|
def recover_compact(data, signature, rec, compressed)
|
80
|
+
group = Bitcoin::Secp256k1::GROUP
|
80
81
|
r = ECDSA::Format::IntegerOctetString.decode(signature[1...33])
|
81
82
|
s = ECDSA::Format::IntegerOctetString.decode(signature[33..-1])
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
83
|
+
return nil if r.zero?
|
84
|
+
return nil if s.zero?
|
85
|
+
|
86
|
+
digest = ECDSA.normalize_digest(data, group.bit_length)
|
87
|
+
field = ECDSA::PrimeField.new(group.order)
|
88
|
+
|
89
|
+
unless rec & 2 == 0
|
90
|
+
r = field.mod(r + group.order)
|
86
91
|
end
|
92
|
+
|
93
|
+
is_odd = (rec & 1 == 1)
|
94
|
+
y_coordinate = group.solve_for_y(r).find{|y| is_odd ? y.odd? : y.even?}
|
95
|
+
|
96
|
+
p = group.new_point([r, y_coordinate])
|
97
|
+
|
98
|
+
inv_r = field.inverse(r)
|
99
|
+
u1 = field.mod(inv_r * digest)
|
100
|
+
u2 = field.mod(inv_r * s)
|
101
|
+
q = p * u2 + (group.new_point(u1)).negate
|
102
|
+
return nil if q.infinity?
|
103
|
+
Bitcoin::Key.from_point(q, compressed: compressed)
|
87
104
|
end
|
88
105
|
|
89
106
|
# verify signature using public key
|