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.
- 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
|