bitcoinrb 1.4.0 → 1.6.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|