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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +2 -2
  3. data/.ruby-version +1 -1
  4. data/README.md +16 -5
  5. data/bitcoinrb.gemspec +2 -2
  6. data/lib/bitcoin/bip324/cipher.rb +113 -0
  7. data/lib/bitcoin/bip324/ell_swift_pubkey.rb +42 -0
  8. data/lib/bitcoin/bip324/fs_chacha20.rb +132 -0
  9. data/lib/bitcoin/bip324/fs_chacha_poly1305.rb +129 -0
  10. data/lib/bitcoin/bip324.rb +144 -0
  11. data/lib/bitcoin/descriptor/addr.rb +31 -0
  12. data/lib/bitcoin/descriptor/checksum.rb +74 -0
  13. data/lib/bitcoin/descriptor/combo.rb +30 -0
  14. data/lib/bitcoin/descriptor/expression.rb +122 -0
  15. data/lib/bitcoin/descriptor/key_expression.rb +23 -0
  16. data/lib/bitcoin/descriptor/multi.rb +49 -0
  17. data/lib/bitcoin/descriptor/multi_a.rb +43 -0
  18. data/lib/bitcoin/descriptor/pk.rb +27 -0
  19. data/lib/bitcoin/descriptor/pkh.rb +15 -0
  20. data/lib/bitcoin/descriptor/raw.rb +32 -0
  21. data/lib/bitcoin/descriptor/script_expression.rb +24 -0
  22. data/lib/bitcoin/descriptor/sh.rb +31 -0
  23. data/lib/bitcoin/descriptor/sorted_multi.rb +15 -0
  24. data/lib/bitcoin/descriptor/sorted_multi_a.rb +15 -0
  25. data/lib/bitcoin/descriptor/tr.rb +91 -0
  26. data/lib/bitcoin/descriptor/wpkh.rb +19 -0
  27. data/lib/bitcoin/descriptor/wsh.rb +30 -0
  28. data/lib/bitcoin/descriptor.rb +176 -100
  29. data/lib/bitcoin/ext/ecdsa.rb +0 -6
  30. data/lib/bitcoin/key.rb +16 -4
  31. data/lib/bitcoin/message_sign.rb +13 -8
  32. data/lib/bitcoin/script/script.rb +8 -3
  33. data/lib/bitcoin/secp256k1/native.rb +62 -6
  34. data/lib/bitcoin/secp256k1/ruby.rb +21 -4
  35. data/lib/bitcoin/taproot/custom_depth_builder.rb +64 -0
  36. data/lib/bitcoin/taproot/simple_builder.rb +1 -6
  37. data/lib/bitcoin/taproot.rb +1 -0
  38. data/lib/bitcoin/tx.rb +1 -1
  39. data/lib/bitcoin/util.rb +11 -3
  40. data/lib/bitcoin/version.rb +1 -1
  41. data/lib/bitcoin.rb +1 -0
  42. metadata +30 -7
@@ -3,145 +3,221 @@ module Bitcoin
3
3
  module Descriptor
4
4
 
5
5
  include Bitcoin::Opcodes
6
-
7
- # generate P2PK output for the given public key.
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::Script] P2PK script.
28
+ # @return [Bitcoin::Descriptor::Pk]
10
29
  def pk(key)
11
- Bitcoin::Script.new << extract_pubkey(key) << OP_CHECKSIG
30
+ Pk.new(key)
12
31
  end
13
32
 
14
- # generate P2PKH output for the given public key.
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::Script] P2PKH script.
35
+ # @return [Bitcoin::Descriptor::Pkh]
17
36
  def pkh(key)
18
- Bitcoin::Script.to_p2pkh(Bitcoin.hash160(extract_pubkey(key)))
37
+ Pkh.new(key)
19
38
  end
20
39
 
21
- # generate P2PKH output for the given public key.
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::Script] P2WPKH script.
42
+ # @return [Bitcoin::Descriptor::Wpkh]
24
43
  def wpkh(key)
25
- pubkey = extract_pubkey(key)
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
- # generate P2SH embed the argument.
31
- # @param [String or Script] script script to be embed.
32
- # @return [Bitcoin::Script] P2SH script.
33
- def sh(script)
34
- script = script.to_hex if script.is_a?(Bitcoin::Script)
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
- # generate P2WSH embed the argument.
40
- # @param [String or Script] script script to be embed.
41
- # @return [Bitcoin::Script] P2WSH script.
42
- def wsh(script)
43
- script = Bitcoin::Script(script.htb) if script.is_a?(String)
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
- # an alias for the collection of `pk(KEY)` and `pkh(KEY)`.
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 [Array[Bitcoin::Script]]
64
+ # @return [Bitcoin::Descriptor::Combo]
53
65
  def combo(key)
54
- result = [pk(key), pkh(key)]
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
- # generate multisig output for given keys.
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::Script] multisig script.
67
- def multi(threshold, *keys, sort: false)
68
- raise ArgumentError, 'Multisig threshold is not valid.' unless threshold.is_a?(Integer)
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
- # generate sorted multisig output for given keys.
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::Script] multisig script.
80
+ # @return [Bitcoin::Descriptor::SortedMulti]
80
81
  def sortedmulti(threshold, *keys)
81
- multi(threshold, *keys, sort: true)
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
- # check BIP32 derivation path
102
- key, *paths = key.split('/')
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
- if key.start_with?('xprv')
105
- key = Bitcoin::ExtKey.from_base58(key)
106
- key = derive_path(key, paths, true) if paths
107
- elsif key.start_with?('xpub')
108
- key = Bitcoin::ExtPubkey.from_base58(key)
109
- key = derive_path(key, paths, false) if paths
110
- else
111
- begin
112
- key = Bitcoin::Key.from_wif(key)
113
- rescue ArgumentError
114
- key_type = compressed_key?(key) ? Bitcoin::Key::TYPES[:compressed] : Bitcoin::Key::TYPES[:uncompressed]
115
- key = Bitcoin::Key.new(pubkey: key, key_type: key_type)
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
- def compressed_key?(key)
124
- %w(02 03).include?(key[0..1]) && [key].pack("H*").bytesize == 33
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 derive_path(key, paths, is_private)
128
- paths.each do |path|
129
- raise ArgumentError, 'xpub can not derive hardened key.' if !is_private && path.end_with?("'")
130
- if is_private
131
- hardened = path.end_with?("'")
132
- path = hardened ? path[0..-2] : path
133
- raise ArgumentError, 'Key path value is not a valid value.' unless path =~ /^[0-9]+$/
134
- raise ArgumentError, 'Key path value is out of range.' if !hardened && path.to_i >= Bitcoin::HARDENED_THRESHOLD
135
- key = key.derive(path.to_i, hardened)
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
- raise ArgumentError, 'Key path value is not a valid value.' unless path =~ /^[0-9]+$/
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
- key
219
+ current << parse(buffer, false) unless buffer.empty?
220
+ current.first
143
221
  end
144
-
145
222
  end
146
-
147
223
  end
@@ -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[:compressed])
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 > 15
151
- rec = rec & 3
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)
@@ -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
- validate_address!(address)
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
- begin
60
- # Legacy verification
61
- pubkey = Bitcoin::Key.recover_compact(message_hash(message, prefix: prefix, legacy: true), sig)
62
- return false unless pubkey
63
- pubkey.to_p2pkh == address
64
- rescue ArgumentError
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
- # @param [Script] p2wsh script
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 buf.eof?
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
- # commit: efad3506a8937162e8010f5839fdf3771dfcf516
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(:secp256k1_schnorrsig_sign, [:pointer, :pointer, :pointer, :pointer, :pointer, :pointer], :int)
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 secp256k1_schnorrsig_sign(context, signature, msg32, keypair, nil, aux_rand) == 1
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
- ECDSA.recover_public_key(Bitcoin::Secp256k1::GROUP, data, ECDSA::Signature.new(r, s)).each do |p|
83
- if p.y & 1 == rec
84
- return Bitcoin::Key.from_point(p, compressed: compressed)
85
- end
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