bitcoinrb 0.6.0 → 1.0.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.
@@ -0,0 +1,47 @@
1
+ module Bitcoin
2
+
3
+ module MessageSign
4
+
5
+ class Error < StandardError; end
6
+
7
+ module_function
8
+
9
+ # Sign a message.
10
+ # @param [Bitcoin::Key] key Private key to sign with.
11
+ # @param [String] message The message to sign.
12
+ # @return [String] Signature, base64 encoded.
13
+ def sign_message(key, message, prefix: Bitcoin.chain_params.message_magic)
14
+ digest = message_hash(message, prefix: prefix)
15
+ compact_sig = key.sign_compact(digest)
16
+ Base64.strict_encode64(compact_sig)
17
+ end
18
+
19
+ # Verify a signed message.
20
+ # @param [String] address Signer's bitcoin address, it must refer to a public key.
21
+ # @param [String] signature The signature in base64 format.
22
+ # @param [String] message The message that was signed.
23
+ # @return [Boolean] Verification result.
24
+ def verify_message(address, signature, message, prefix: Bitcoin.chain_params.message_magic)
25
+ validate_address!(address)
26
+ sig = Base64.decode64(signature)
27
+ raise ArgumentError, 'Invalid signature length' unless sig.bytesize == Bitcoin::Key::COMPACT_SIGNATURE_SIZE
28
+ digest = message_hash(message, prefix: prefix)
29
+ pubkey = Bitcoin::Key.recover_compact(digest, sig)
30
+ return false unless pubkey
31
+ pubkey.to_p2pkh == address
32
+ end
33
+
34
+ # Hashes a message for signing and verification.
35
+ def message_hash(message, prefix: Bitcoin.chain_params.message_magic)
36
+ Bitcoin.double_sha256(Bitcoin.pack_var_string(prefix) << Bitcoin.pack_var_string(message))
37
+ end
38
+
39
+ def validate_address!(address)
40
+ raise ArgumentError, 'Invalid address' unless Bitcoin.valid_address?(address)
41
+ script = Bitcoin::Script.parse_from_addr(address)
42
+ raise ArgumentError, 'Address has no key' unless script.p2pkh?
43
+ end
44
+
45
+ private_class_method :validate_address!
46
+ end
47
+ end
@@ -17,7 +17,7 @@ module Bitcoin
17
17
  end
18
18
 
19
19
  def transactions
20
- @values[:transactions].map{|raw_tx|Bitcoin::Tx.parse_from_payload(raw_tx)}
20
+ @values[:transactions].map{|raw_tx|Bitcoin::Tx.parse_from_payload(raw_tx, strict: true)}
21
21
  end
22
22
 
23
23
  end
@@ -44,7 +44,7 @@ module Bitcoin
44
44
  when PSBT_IN_TYPES[:non_witness_utxo]
45
45
  raise ArgumentError, 'Invalid non-witness utxo typed key.' unless key_len == 1
46
46
  raise ArgumentError, 'Duplicate Key, input non-witness utxo already provided.' if input.non_witness_utxo
47
- input.non_witness_utxo = Bitcoin::Tx.parse_from_payload(value)
47
+ input.non_witness_utxo = Bitcoin::Tx.parse_from_payload(value, strict: true)
48
48
  when PSBT_IN_TYPES[:witness_utxo]
49
49
  raise ArgumentError, 'Invalid input witness utxo typed key.' unless key_len == 1
50
50
  raise ArgumentError, 'Duplicate Key, input witness utxo already provided.' if input.witness_utxo
@@ -74,7 +74,7 @@ module Bitcoin
74
74
  when PSBT_GLOBAL_TYPES[:unsigned_tx]
75
75
  raise ArgumentError, 'Invalid global transaction typed key.' unless key_len == 1
76
76
  raise ArgumentError, 'Duplicate Key, unsigned tx already provided.' if partial_tx.tx
77
- partial_tx.tx = Bitcoin::Tx.parse_from_payload(value, non_witness: true)
77
+ partial_tx.tx = Bitcoin::Tx.parse_from_payload(value, non_witness: true, strict: true)
78
78
  partial_tx.tx.in.each do |tx_in|
79
79
  raise ArgumentError, 'Unsigned tx does not have empty scriptSigs and scriptWitnesses.' if !tx_in.script_sig.empty? || !tx_in.script_witness.empty?
80
80
  end
@@ -162,6 +162,15 @@ module Bitcoin
162
162
  Base64.strict_encode64(to_payload)
163
163
  end
164
164
 
165
+ # Store the PSBT to a file.
166
+ # @param [String] path File path to store.
167
+ def to_file(path)
168
+ raise ArgumentError, 'The file already exists' if File.exist?(path)
169
+ File.open(path, 'w') do |f|
170
+ f.write(to_payload)
171
+ end
172
+ end
173
+
165
174
  # update input key-value maps.
166
175
  # @param [Bitcoin::Tx] prev_tx previous tx reference by input.
167
176
  # @param [Bitcoin::Script] redeem_script redeem script to set input.
data/lib/bitcoin/psbt.rb CHANGED
@@ -31,6 +31,14 @@ module Bitcoin
31
31
  s << Bitcoin.pack_var_int(value.bytesize) << value
32
32
  s
33
33
  end
34
+
35
+ # Load PSBT from file.
36
+ # @param [String] path File path of PSBT.
37
+ # @return [Bitcoin::PSBT::Tx] PSBT object.
38
+ def load_from_file(path)
39
+ raise ArgumentError, 'File not found' unless File.exist?(path)
40
+ Bitcoin::PSBT::Tx.parse_from_payload(File.read(path))
41
+ end
34
42
  end
35
43
 
36
44
  end
@@ -49,7 +49,7 @@ module Bitcoin
49
49
  # Returns connected peer information.
50
50
  def getpeerinfo
51
51
  node.pool.peers.map do |peer|
52
- local_addr = "#{peer.remote_version.remote_addr.ip}:18333"
52
+ local_addr = "#{peer.remote_version.remote_addr.addr_string}:18333"
53
53
  {
54
54
  id: peer.id,
55
55
  addr: "#{peer.host}:#{peer.port}",
@@ -75,7 +75,7 @@ module Bitcoin
75
75
 
76
76
  # broadcast transaction
77
77
  def sendrawtransaction(hex_tx)
78
- tx = Bitcoin::Tx.parse_from_payload(hex_tx.htb)
78
+ tx = Bitcoin::Tx.parse_from_payload(hex_tx.htb, strict: true)
79
79
  # TODO check wether tx is valid
80
80
  node.broadcast(tx)
81
81
  tx.txid
@@ -84,7 +84,7 @@ module Bitcoin
84
84
  # decode tx data.
85
85
  def decoderawtransaction(hex_tx)
86
86
  begin
87
- Bitcoin::Tx.parse_from_payload(hex_tx.htb).to_h
87
+ Bitcoin::Tx.parse_from_payload(hex_tx.htb, strict: true ).to_h
88
88
  rescue Exception
89
89
  raise ArgumentError.new('TX decode failed')
90
90
  end
@@ -21,7 +21,7 @@ module Bitcoin
21
21
 
22
22
  # generate P2WPKH script
23
23
  def self.to_p2wpkh(pubkey_hash)
24
- new << WITNESS_VERSION << pubkey_hash
24
+ new << WITNESS_VERSION_V0 << pubkey_hash
25
25
  end
26
26
 
27
27
  # generate m of n multisig p2sh script
@@ -52,7 +52,7 @@ module Bitcoin
52
52
  end
53
53
 
54
54
  # generate m of n multisig script
55
- # @param [String] m the number of signatures required for multisig
55
+ # @param [Integer] m the number of signatures required for multisig
56
56
  # @param [Array] pubkeys array of public keys that compose multisig
57
57
  # @return [Script] multisig script.
58
58
  def self.to_multisig_script(m, pubkeys, sort: false)
@@ -64,7 +64,7 @@ module Bitcoin
64
64
  # @param [Script] redeem_script target redeem script
65
65
  # @param [Script] p2wsh script
66
66
  def self.to_p2wsh(redeem_script)
67
- new << WITNESS_VERSION << redeem_script.to_sha256
67
+ new << WITNESS_VERSION_V0 << redeem_script.to_sha256
68
68
  end
69
69
 
70
70
  # generate script from string.
@@ -87,17 +87,21 @@ module Bitcoin
87
87
  def self.parse_from_addr(addr)
88
88
  begin
89
89
  segwit_addr = Bech32::SegwitAddr.new(addr)
90
- raise 'Invalid hrp.' unless Bitcoin.chain_params.bech32_hrp == segwit_addr.hrp
90
+ raise ArgumentError, 'Invalid address.' unless Bitcoin.chain_params.bech32_hrp == segwit_addr.hrp
91
91
  Bitcoin::Script.parse_from_payload(segwit_addr.to_script_pubkey.htb)
92
92
  rescue Exception => e
93
+ begin
93
94
  hex, addr_version = Bitcoin.decode_base58_address(addr)
95
+ rescue
96
+ raise ArgumentError, 'Invalid address.'
97
+ end
94
98
  case addr_version
95
99
  when Bitcoin.chain_params.address_version
96
100
  Bitcoin::Script.to_p2pkh(hex)
97
101
  when Bitcoin.chain_params.p2sh_version
98
102
  Bitcoin::Script.to_p2sh(hex)
99
103
  else
100
- throw e
104
+ raise ArgumentError, 'Invalid address.'
101
105
  end
102
106
  end
103
107
  end
@@ -145,7 +149,7 @@ module Bitcoin
145
149
  end
146
150
 
147
151
  # Output script payload.
148
- # @param [Boolean] length_prefixed Flag whether the length of the pyrode should be given at the beginning.(default: false)
152
+ # @param [Boolean] length_prefixed Flag whether the length of the payload should be given at the beginning.(default: false)
149
153
  # @return [String] payload
150
154
  def to_payload(length_prefixed = false)
151
155
  p = chunks.join
@@ -177,27 +181,40 @@ module Bitcoin
177
181
 
178
182
  # check whether standard script.
179
183
  def standard?
180
- p2pkh? | p2sh? | p2wpkh? | p2wsh? | multisig? | standard_op_return?
184
+ p2pkh? | p2sh? | p2wpkh? | p2wsh? | p2tr? | multisig? | standard_op_return?
181
185
  end
182
186
 
183
- # whether this script is a P2PKH format script.
187
+ # Check whether this script is a P2PKH format script.
188
+ # @return [Boolean] if P2PKH return true, otherwise false
184
189
  def p2pkh?
185
190
  return false unless chunks.size == 5
186
191
  [OP_DUP, OP_HASH160, OP_EQUALVERIFY, OP_CHECKSIG] ==
187
192
  (chunks[0..1]+ chunks[3..4]).map(&:ord) && chunks[2].bytesize == 21
188
193
  end
189
194
 
190
- # whether this script is a P2WPKH format script.
195
+ # Check whether this script is a P2WPKH format script.
196
+ # @return [Boolean] if P2WPKH return true, otherwise false
191
197
  def p2wpkh?
192
198
  return false unless chunks.size == 2
193
- chunks[0].ord == WITNESS_VERSION && chunks[1].bytesize == 21
199
+ chunks[0].ord == WITNESS_VERSION_V0 && chunks[1].bytesize == 21
194
200
  end
195
201
 
202
+ # Check whether this script is a P2WPSH format script.
203
+ # @return [Boolean] if P2WPSH return true, otherwise false
196
204
  def p2wsh?
197
205
  return false unless chunks.size == 2
198
- chunks[0].ord == WITNESS_VERSION && chunks[1].bytesize == 33
206
+ chunks[0].ord == WITNESS_VERSION_V0 && chunks[1].bytesize == 33
207
+ end
208
+
209
+ # Check whether this script is a P2TR format script.
210
+ # @return [Boolean] if P2TR return true, otherwise false
211
+ def p2tr?
212
+ return false unless chunks.size == 2
213
+ chunks[0].ord == WITNESS_VERSION_V1 && chunks[1].bytesize == 33
199
214
  end
200
215
 
216
+ # Check whether this script is a P2SH format script.
217
+ # @return [Boolean] if P2SH return true, otherwise false
201
218
  def p2sh?
202
219
  return false unless chunks.size == 3
203
220
  OP_HASH160 == chunks[0].ord && OP_EQUAL == chunks[2].ord && chunks[1].bytesize == 21
@@ -498,7 +515,7 @@ module Bitcoin
498
515
  end
499
516
 
500
517
  def ==(other)
501
- return false unless other
518
+ return false unless other.is_a?(Script)
502
519
  chunks == other.chunks
503
520
  end
504
521
 
@@ -3,6 +3,7 @@ module Bitcoin
3
3
  class ScriptInterpreter
4
4
 
5
5
  include Bitcoin::Opcodes
6
+ using Bitcoin::Ext::ArrayExt
6
7
 
7
8
  attr_reader :stack
8
9
  attr_reader :debug
@@ -55,6 +56,8 @@ module Bitcoin
55
56
  version, program = script_pubkey.witness_data
56
57
  stack_copy = stack.dup
57
58
  return false unless verify_witness_program(witness, version, program, false)
59
+ # Bypass the cleanstack check at the end. The actual stack is obviously not clean for witness programs.
60
+ stack.resize!(1, Script.encode_number(0))
58
61
  end
59
62
 
60
63
  # Additional validation for spend-to-script-hash transactions
@@ -160,9 +163,9 @@ module Bitcoin
160
163
  end
161
164
  return set_error(SCRIPT_ERR_STACK_SIZE) if stack.size > MAX_STACK_SIZE
162
165
  need_evaluate = true
166
+ elsif flag?(SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_TAPROOT_VERSION)
167
+ return set_error(SCRIPT_ERR_DISCOURAGE_UPGRADABLE_TAPROOT_VERSION)
163
168
  end
164
-
165
- return set_error(SCRIPT_ERR_DISCOURAGE_UPGRADABLE_TAPROOT_VERSION) if flag?(SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_TAPROOT_VERSION)
166
169
  return true unless need_evaluate
167
170
  end
168
171
  elsif flag?(SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM)
@@ -697,7 +700,7 @@ module Bitcoin
697
700
  begin
698
701
  path_len = (control.bytesize - TAPROOT_CONTROL_BASE_SIZE) / TAPROOT_CONTROL_NODE_SIZE
699
702
  xonly_pubkey = control[1...TAPROOT_CONTROL_BASE_SIZE]
700
- p = Bitcoin::Key.new(pubkey: "02#{xonly_pubkey.bth}", key_type: Key::TYPES[:compressed])
703
+ p = Bitcoin::Key.from_xonly_pubkey(xonly_pubkey.bth)
701
704
  k = leaf_hash
702
705
  path_len.times do |i|
703
706
  pos = (TAPROOT_CONTROL_BASE_SIZE + TAPROOT_CONTROL_NODE_SIZE * i)
@@ -56,11 +56,9 @@ module Bitcoin
56
56
 
57
57
  return set_error(SCRIPT_ERR_SCHNORR_SIG_HASHTYPE) unless (hash_type <= 0x03 || (hash_type >= 0x81 && hash_type <= 0x83))
58
58
 
59
- opts[:prevouts] = prevouts
60
-
61
59
  begin
62
- sighash = tx.sighash_for_input(input_index, opts: opts, hash_type: hash_type, sig_version: sig_version)
63
- key = Key.new(pubkey: "02#{pubkey}", key_type: Key::TYPES[:compressed])
60
+ sighash = tx.sighash_for_input(input_index, opts: opts, hash_type: hash_type, sig_version: sig_version, prevouts: prevouts)
61
+ key = Key.from_xonly_pubkey(pubkey)
64
62
  key.verify(sig, sighash, algo: :schnorr)
65
63
  rescue ArgumentError
66
64
  return set_error(SCRIPT_ERR_SCHNORR_SIG_HASHTYPE)
@@ -4,8 +4,8 @@
4
4
  module Bitcoin
5
5
  module Secp256k1
6
6
 
7
- # binding for secp256k1 (https://github.com/bitcoin/bitcoin/tree/v0.14.2/src/secp256k1)
8
- # tag: v0.14.2
7
+ # binding for secp256k1 (https://github.com/bitcoin-core/secp256k1/)
8
+ # commit: efad3506a8937162e8010f5839fdf3771dfcf516
9
9
  # this is not included by default, to enable set shared object path to ENV['SECP256K1_LIB_PATH']
10
10
  # for linux, ENV['SECP256K1_LIB_PATH'] = '/usr/local/lib/libsecp256k1.so'
11
11
  # for mac,
@@ -54,6 +54,10 @@ module Bitcoin
54
54
  attach_function(:secp256k1_schnorrsig_verify, [:pointer, :pointer, :pointer, :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
+ attach_function(:secp256k1_ecdsa_sign_recoverable, [:pointer, :pointer, :pointer, :pointer, :pointer, :pointer], :int)
58
+ attach_function(:secp256k1_ecdsa_recoverable_signature_serialize_compact, [:pointer, :pointer, :pointer, :pointer], :int)
59
+ attach_function(:secp256k1_ecdsa_recover, [:pointer, :pointer, :pointer, :pointer], :int)
60
+ attach_function(:secp256k1_ecdsa_recoverable_signature_parse_compact, [:pointer, :pointer, :pointer, :int], :int)
57
61
  end
58
62
 
59
63
  def with_context(flags: (SECP256K1_CONTEXT_VERIFY | SECP256K1_CONTEXT_SIGN))
@@ -116,6 +120,52 @@ module Bitcoin
116
120
  end
117
121
  end
118
122
 
123
+ # Sign data with compact format.
124
+ # @param [String] data a data to be signed with binary format
125
+ # @param [String] privkey a private key using sign with hex format
126
+ # @return [Array[signature, recovery id]]
127
+ def sign_compact(data, privkey)
128
+ with_context do |context|
129
+ sig = FFI::MemoryPointer.new(:uchar, 65)
130
+ hash =FFI::MemoryPointer.new(:uchar, data.bytesize).put_bytes(0, data)
131
+ priv_key = privkey.htb
132
+ sec_key = FFI::MemoryPointer.new(:uchar, priv_key.bytesize).put_bytes(0, priv_key)
133
+ result = secp256k1_ecdsa_sign_recoverable(context, sig, hash, sec_key, nil, nil)
134
+ raise 'secp256k1_ecdsa_sign_recoverable failed.' if result == 0
135
+
136
+ output = FFI::MemoryPointer.new(:uchar, 64)
137
+ rec = FFI::MemoryPointer.new(:uint64)
138
+ result = secp256k1_ecdsa_recoverable_signature_serialize_compact(context, output, rec, sig)
139
+ raise 'secp256k1_ecdsa_recoverable_signature_serialize_compact failed.' unless result == 1
140
+
141
+ raw_sig = output.read_string(64)
142
+ [ECDSA::Signature.new(raw_sig[0...32].bti, raw_sig[32..-1].bti), rec.read(:int)]
143
+ end
144
+ end
145
+
146
+ # Recover public key from compact signature.
147
+ # @param [String] data message digest using signature.
148
+ # @param [String] signature signature with binary format.
149
+ # @param [Integer] rec recovery id.
150
+ # @param [Boolean] compressed whether compressed public key or not.
151
+ # @return [Bitcoin::Key] Recovered public key.
152
+ def recover_compact(data, signature, rec, compressed)
153
+ with_context do |context|
154
+ sig = FFI::MemoryPointer.new(:uchar, 65)
155
+ input = FFI::MemoryPointer.new(:uchar, 64).put_bytes(0, signature[1..-1])
156
+ result = secp256k1_ecdsa_recoverable_signature_parse_compact(context, sig, input, rec)
157
+ raise 'secp256k1_ecdsa_recoverable_signature_parse_compact failed.' unless result == 1
158
+
159
+ pubkey = FFI::MemoryPointer.new(:uchar, 64)
160
+ msg = FFI::MemoryPointer.new(:uchar, data.bytesize).put_bytes(0, data)
161
+ result = secp256k1_ecdsa_recover(context, pubkey, sig, msg)
162
+ raise 'secp256k1_ecdsa_recover failed.' unless result == 1
163
+
164
+ pubkey = serialize_pubkey_internal(context, pubkey.read_string(64), compressed)
165
+ Bitcoin::Key.new(pubkey: pubkey, compressed: compressed)
166
+ end
167
+ end
168
+
119
169
  # verify signature
120
170
  # @param [String] data a data with binary format.
121
171
  # @param [String] sig signature data with binary format
@@ -194,18 +244,7 @@ module Bitcoin
194
244
  internal_pubkey = FFI::MemoryPointer.new(:uchar, 64)
195
245
  result = secp256k1_ec_pubkey_create(context, internal_pubkey, privkey.htb)
196
246
  raise 'error creating pubkey' unless result
197
-
198
- pubkey = FFI::MemoryPointer.new(:uchar, 65)
199
- pubkey_len = FFI::MemoryPointer.new(:uint64)
200
- result = if compressed
201
- pubkey_len.put_uint64(0, 33)
202
- secp256k1_ec_pubkey_serialize(context, pubkey, pubkey_len, internal_pubkey, SECP256K1_EC_COMPRESSED)
203
- else
204
- pubkey_len.put_uint64(0, 65)
205
- secp256k1_ec_pubkey_serialize(context, pubkey, pubkey_len, internal_pubkey, SECP256K1_EC_UNCOMPRESSED)
206
- end
207
- raise 'error serialize pubkey' unless result || pubkey_len.read_uint64 > 0
208
- pubkey.read_string(pubkey_len.read_uint64).bth
247
+ serialize_pubkey_internal(context, internal_pubkey, compressed)
209
248
  end
210
249
 
211
250
  def sign_ecdsa(data, privkey, extra_entropy)
@@ -282,6 +321,21 @@ module Bitcoin
282
321
  end
283
322
  end
284
323
 
324
+ # Serialize public key.
325
+ def serialize_pubkey_internal(context, pubkey_input, compressed)
326
+ pubkey = FFI::MemoryPointer.new(:uchar, 65)
327
+ pubkey_len = FFI::MemoryPointer.new(:uint64)
328
+ result = if compressed
329
+ pubkey_len.put_uint64(0, 33)
330
+ secp256k1_ec_pubkey_serialize(context, pubkey, pubkey_len, pubkey_input, SECP256K1_EC_COMPRESSED)
331
+ else
332
+ pubkey_len.put_uint64(0, 65)
333
+ secp256k1_ec_pubkey_serialize(context, pubkey, pubkey_len, pubkey_input, SECP256K1_EC_UNCOMPRESSED)
334
+ end
335
+ raise 'error serialize pubkey' unless result || pubkey_len.read_uint64 > 0
336
+ pubkey.read_string(pubkey_len.read_uint64).bth
337
+ end
338
+
285
339
  end
286
340
  end
287
341
  end
@@ -43,14 +43,14 @@ module Bitcoin
43
43
 
44
44
  # sign data.
45
45
  # @param [String] data a data to be signed with binary format
46
- # @param [String] privkey a private key using sign
46
+ # @param [String] privkey a private key using sign with hex format
47
47
  # @param [String] extra_entropy a extra entropy with binary format for rfc6979
48
48
  # @param [Symbol] algo signature algorithm. ecdsa(default) or schnorr.
49
49
  # @return [String] signature data with binary format
50
50
  def sign_data(data, privkey, extra_entropy = nil, algo: :ecdsa)
51
51
  case algo
52
52
  when :ecdsa
53
- sign_ecdsa(data, privkey, extra_entropy)
53
+ sign_ecdsa(data, privkey, extra_entropy)&.first
54
54
  when :schnorr
55
55
  sign_schnorr(data, privkey, extra_entropy)
56
56
  else
@@ -58,6 +58,31 @@ module Bitcoin
58
58
  end
59
59
  end
60
60
 
61
+ # Sign data with compact format.
62
+ # @param [String] data a data to be signed with binary format
63
+ # @param [String] privkey a private key using sign with hex format
64
+ # @return [Array[signature, recovery id]]
65
+ def sign_compact(data, privkey)
66
+ sig, rec = sign_ecdsa(data, privkey, nil)
67
+ [ECDSA::Format::SignatureDerString.decode(sig), rec]
68
+ end
69
+
70
+ # Recover public key from compact signature.
71
+ # @param [String] data message digest using signature.
72
+ # @param [String] signature signature with binary format.
73
+ # @param [Integer] rec recovery id.
74
+ # @param [Boolean] compressed whether compressed public key or not.
75
+ # @return [Bitcoin::Key] Recovered public key.
76
+ def recover_compact(data, signature, rec, compressed)
77
+ r = ECDSA::Format::IntegerOctetString.decode(signature[1...33])
78
+ s = ECDSA::Format::IntegerOctetString.decode(signature[33..-1])
79
+ ECDSA.recover_public_key(Bitcoin::Secp256k1::GROUP, data, ECDSA::Signature.new(r, s)).each do |p|
80
+ if p.y & 1 == rec
81
+ return Bitcoin::Key.from_point(p, compressed: compressed)
82
+ end
83
+ end
84
+ end
85
+
61
86
  # verify signature using public key
62
87
  # @param [String] data a SHA-256 message digest with binary format
63
88
  # @param [String] sig a signature for +data+ with binary format
@@ -113,11 +138,14 @@ module Bitcoin
113
138
  r = point_field.mod(r_point.x)
114
139
  return nil if r.zero?
115
140
 
141
+ rec = r_point.y & 1
142
+
116
143
  e = ECDSA.normalize_digest(data, GROUP.bit_length)
117
144
  s = point_field.mod(point_field.inverse(nonce) * (e + r * private_key))
118
145
 
119
146
  if s > (GROUP.order / 2) # convert low-s
120
147
  s = GROUP.order - s
148
+ rec ^= 1
121
149
  end
122
150
 
123
151
  return nil if s.zero?
@@ -125,7 +153,7 @@ module Bitcoin
125
153
  signature = ECDSA::Signature.new(r, s).to_der
126
154
  public_key = Bitcoin::Key.new(priv_key: privkey.bth).pubkey
127
155
  raise 'Creation of signature failed.' unless Bitcoin::Secp256k1::Ruby.verify_sig(data, signature, public_key)
128
- signature
156
+ [signature, rec]
129
157
  end
130
158
 
131
159
  def sign_schnorr(data, privkey, aux_rand)
@@ -62,6 +62,7 @@ module Bitcoin
62
62
 
63
63
  def generate(tx, input_index, hash_type, opts)
64
64
  amount = opts[:amount]
65
+ raise ArgumentError, 'segwit sighash requires amount.' unless amount
65
66
  output_script = opts[:script_code]
66
67
  skip_separator_index = opts[:skip_separator_index]
67
68
  hash_prevouts = Bitcoin.double_sha256(tx.inputs.map{|i|i.out_point.to_payload}.join)
@@ -0,0 +1,23 @@
1
+ module Bitcoin
2
+ module Taproot
3
+ class LeafNode
4
+
5
+ attr_reader :script, :leaf_ver
6
+
7
+ # Initialize
8
+ # @param [Bitcoin::Script] script Locking script
9
+ # @param [Integer] leaf_ver The leaf version of this script.
10
+ def initialize(script, leaf_ver = Bitcoin::TAPROOT_LEAF_TAPSCRIPT)
11
+ raise Taproot::Error, 'script must be Bitcoin::Script object' unless script.is_a?(Bitcoin::Script)
12
+ @script = script
13
+ @leaf_ver = leaf_ver
14
+ end
15
+
16
+ # Calculate leaf hash.
17
+ # @return [String] leaf hash.
18
+ def leaf_hash
19
+ @hash_value ||= Bitcoin.tagged_hash('TapLeaf', [leaf_ver].pack('C') + Bitcoin.pack_var_string(script.to_payload))
20
+ end
21
+ end
22
+ end
23
+ end