bitcoinrb 0.6.0 → 1.0.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 +37 -0
- data/.rspec_parallel +2 -0
- data/.ruby-version +1 -1
- data/README.md +1 -1
- data/bitcoinrb.gemspec +4 -3
- data/lib/bitcoin/constants.rb +24 -17
- data/lib/bitcoin/ext/array_ext.rb +22 -0
- data/lib/bitcoin/ext/ecdsa.rb +5 -0
- data/lib/bitcoin/ext.rb +1 -0
- data/lib/bitcoin/ext_key.rb +1 -1
- data/lib/bitcoin/key.rb +42 -2
- data/lib/bitcoin/message/addr_v2.rb +34 -0
- data/lib/bitcoin/message/base.rb +16 -0
- data/lib/bitcoin/message/network_addr.rb +141 -18
- data/lib/bitcoin/message/send_addr_v2.rb +13 -0
- data/lib/bitcoin/message/tx.rb +1 -1
- data/lib/bitcoin/message.rb +72 -0
- data/lib/bitcoin/message_sign.rb +47 -0
- data/lib/bitcoin/payments/payment.pb.rb +1 -1
- data/lib/bitcoin/psbt/input.rb +1 -1
- data/lib/bitcoin/psbt/tx.rb +10 -1
- data/lib/bitcoin/psbt.rb +8 -0
- data/lib/bitcoin/rpc/request_handler.rb +3 -3
- data/lib/bitcoin/script/script.rb +29 -12
- data/lib/bitcoin/script/script_interpreter.rb +6 -3
- data/lib/bitcoin/script/tx_checker.rb +2 -4
- data/lib/bitcoin/secp256k1/native.rb +68 -14
- data/lib/bitcoin/secp256k1/ruby.rb +31 -3
- data/lib/bitcoin/sighash_generator.rb +1 -0
- data/lib/bitcoin/taproot/leaf_node.rb +23 -0
- data/lib/bitcoin/taproot/simple_builder.rb +155 -0
- data/lib/bitcoin/taproot.rb +45 -0
- data/lib/bitcoin/tx.rb +17 -16
- data/lib/bitcoin/version.rb +1 -1
- data/lib/bitcoin.rb +3 -8
- metadata +44 -8
- data/.travis.yml +0 -13
@@ -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
|
data/lib/bitcoin/psbt/input.rb
CHANGED
@@ -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
|
data/lib/bitcoin/psbt/tx.rb
CHANGED
@@ -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.
|
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 <<
|
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 [
|
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 <<
|
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
|
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
|
-
|
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
|
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 ==
|
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 ==
|
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.
|
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.
|
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/
|
8
|
-
#
|
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
|