bitcoinrb 1.13.0 → 1.14.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/bitcoinrb.gemspec +2 -2
- data/lib/bitcoin/base58.rb +15 -3
- data/lib/bitcoin/bip324.rb +4 -1
- data/lib/bitcoin/block.rb +3 -3
- data/lib/bitcoin/key.rb +1 -1
- data/lib/bitcoin/message/base.rb +7 -1
- data/lib/bitcoin/message/filter_load.rb +10 -0
- data/lib/bitcoin/message/merkle_block.rb +6 -0
- data/lib/bitcoin/message.rb +3 -0
- data/lib/bitcoin/partial_tree.rb +25 -0
- data/lib/bitcoin/psbt/input.rb +7 -0
- data/lib/bitcoin/psbt/tx.rb +10 -9
- data/lib/bitcoin/rpc/bitcoin_core_client.rb +3 -2
- data/lib/bitcoin/script/script.rb +1 -1
- data/lib/bitcoin/script/script_interpreter.rb +1 -1
- data/lib/bitcoin/script/tx_checker.rb +7 -2
- data/lib/bitcoin/secp256k1/native.rb +50 -1
- data/lib/bitcoin/secp256k1/ruby.rb +115 -2
- data/lib/bitcoin/sighash_generator.rb +6 -0
- data/lib/bitcoin/silent_payment.rb +56 -90
- data/lib/bitcoin/taproot/custom_depth_builder.rb +2 -2
- data/lib/bitcoin/taproot/simple_builder.rb +9 -7
- data/lib/bitcoin/taproot.rb +6 -1
- data/lib/bitcoin/util.rb +1 -1
- data/lib/bitcoin/version.rb +1 -1
- data/lib/bitcoin/wallet/master_key.rb +83 -24
- metadata +5 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 360a1a8533df9b68228d18279909e7bc5f45d4d5679caa1a02cda75e0a9140d0
|
|
4
|
+
data.tar.gz: ff86fcec32b3fe1394eea043080aaedafee61e724ec689cde5efdd7b9ca5c307
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9aeb17cc9dc42ce33c4c31ed1f6424569d5c0f4f20627015c930d11e5ba2bf9c15f43cd20a666257caba689ffc0010b08b5855c13a3cf61a724c05fe8bcd383d
|
|
7
|
+
data.tar.gz: 14848d9c37a69ec5b0d2f993ce92c3cad3bdfc6275151789ff768ad5989d101f9c5545f1dc668d372f847b574fdc60d10e5f90b81b377b23ac20bfb3705b0a3a
|
data/bitcoinrb.gemspec
CHANGED
|
@@ -27,8 +27,8 @@ Gem::Specification.new do |spec|
|
|
|
27
27
|
spec.add_runtime_dependency 'bip-schnorr', '>= 0.7.0'
|
|
28
28
|
spec.add_runtime_dependency 'base32', '>= 0.3.4'
|
|
29
29
|
spec.add_runtime_dependency 'base64', '~> 0.2.0'
|
|
30
|
-
spec.add_runtime_dependency 'secp256k1rb', '0.
|
|
30
|
+
spec.add_runtime_dependency 'secp256k1rb', '0.8.0'
|
|
31
31
|
spec.add_runtime_dependency 'logger'
|
|
32
|
-
spec.add_runtime_dependency 'merkle', '0.
|
|
32
|
+
spec.add_runtime_dependency 'merkle', '1.0.0'
|
|
33
33
|
spec.add_runtime_dependency 'dnsruby', '1.73.0'
|
|
34
34
|
end
|
data/lib/bitcoin/base58.rb
CHANGED
|
@@ -8,6 +8,10 @@ module Bitcoin
|
|
|
8
8
|
ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
|
|
9
9
|
SIZE = ALPHABET.size
|
|
10
10
|
|
|
11
|
+
# Upper bound for #decode. Decoding is quadratic in the length of the input, and the
|
|
12
|
+
# longest value Bitcoin encodes with Base58 is an extended key, at 111 characters.
|
|
13
|
+
MAX_LENGTH = 256
|
|
14
|
+
|
|
11
15
|
# encode hex value to base58 string.
|
|
12
16
|
def encode(hex)
|
|
13
17
|
leading_zero_bytes = (hex.match(/^([0]+)/) ? $1 : '').size / 2
|
|
@@ -21,11 +25,19 @@ module Bitcoin
|
|
|
21
25
|
end
|
|
22
26
|
|
|
23
27
|
# decode base58 string to hex value.
|
|
28
|
+
# @param [String] base58_val Base58 string.
|
|
29
|
+
# @return [String] Decoded value with hex format.
|
|
30
|
+
# @raise [ArgumentError] If +base58_val+ is longer than MAX_LENGTH or holds a character
|
|
31
|
+
# which is not in the alphabet.
|
|
24
32
|
def decode(base58_val)
|
|
33
|
+
if base58_val.length > MAX_LENGTH
|
|
34
|
+
raise ArgumentError, "Base58 string must not be longer than #{MAX_LENGTH} characters."
|
|
35
|
+
end
|
|
25
36
|
int_val = 0
|
|
26
|
-
base58_val.
|
|
27
|
-
|
|
28
|
-
|
|
37
|
+
base58_val.each_char do |char|
|
|
38
|
+
char_index = ALPHABET.index(char)
|
|
39
|
+
raise ArgumentError, 'Value passed not a valid Base58 String.' if char_index.nil?
|
|
40
|
+
int_val = int_val * SIZE + char_index
|
|
29
41
|
end
|
|
30
42
|
s = int_val.to_even_length_hex
|
|
31
43
|
s = '' if s == '00'
|
data/lib/bitcoin/bip324.rb
CHANGED
|
@@ -91,13 +91,16 @@ module Bitcoin
|
|
|
91
91
|
ECDSA::Format::IntegerOctetString.encode(result, 32).bth
|
|
92
92
|
end
|
|
93
93
|
|
|
94
|
+
# Number of cases xswiftec_inv selects between. It reads c as 3 bits, c & 1, c & 2 and c & 4.
|
|
95
|
+
CASE_COUNT = 8
|
|
96
|
+
|
|
94
97
|
# Given a field element X on the curve, find (u, t) that encode them.
|
|
95
98
|
# @param [String] x coordinate with hex format.
|
|
96
99
|
# @return [String] ElligatorSwift public key with hex format.
|
|
97
100
|
def xelligatorswift(x)
|
|
98
101
|
loop do
|
|
99
102
|
u = SecureRandom.random_number(1..ECDSA::Group::Secp256k1.order).to_s(16)
|
|
100
|
-
c = SecureRandom.random_number(
|
|
103
|
+
c = SecureRandom.random_number(CASE_COUNT)
|
|
101
104
|
t = xswiftec_inv(x, u, c)
|
|
102
105
|
unless t.nil?
|
|
103
106
|
return (ECDSA::Format::IntegerOctetString.encode(u.hex, 32) +
|
data/lib/bitcoin/block.rb
CHANGED
|
@@ -28,7 +28,7 @@ module Bitcoin
|
|
|
28
28
|
header = BlockHeader.new(
|
|
29
29
|
version,
|
|
30
30
|
'00' * 32,
|
|
31
|
-
Merkle::BinaryTree.new(config: Merkle::Config.bitcoin, leaves: [coinbase.txid]).compute_root.rhex,
|
|
31
|
+
Merkle::BinaryTree.new(config: Merkle::Config.bitcoin(element_encoding: :hex), leaves: [coinbase.txid]).compute_root.rhex,
|
|
32
32
|
time,
|
|
33
33
|
bits,
|
|
34
34
|
nonce
|
|
@@ -72,7 +72,7 @@ module Bitcoin
|
|
|
72
72
|
|
|
73
73
|
# calculate merkle root from tx list.
|
|
74
74
|
def calculate_merkle_root
|
|
75
|
-
tree = Merkle::BinaryTree.new(config: Merkle::Config.bitcoin, leaves: transactions.map(&:tx_hash))
|
|
75
|
+
tree = Merkle::BinaryTree.new(config: Merkle::Config.bitcoin(element_encoding: :hex), leaves: transactions.map(&:tx_hash))
|
|
76
76
|
tree.compute_root
|
|
77
77
|
end
|
|
78
78
|
|
|
@@ -86,7 +86,7 @@ module Bitcoin
|
|
|
86
86
|
witness_hashes = [COINBASE_WTXID]
|
|
87
87
|
witness_hashes += (transactions[1..-1].map(&:witness_hash))
|
|
88
88
|
reserved_value = transactions[0].inputs[0].script_witness.stack.map(&:bth).join
|
|
89
|
-
tree = Merkle::BinaryTree.new(config: Merkle::Config.bitcoin, leaves: witness_hashes)
|
|
89
|
+
tree = Merkle::BinaryTree.new(config: Merkle::Config.bitcoin(element_encoding: :hex), leaves: witness_hashes)
|
|
90
90
|
root_hash = tree.compute_root
|
|
91
91
|
Bitcoin.double_sha256([root_hash + reserved_value].pack('H*')).bth
|
|
92
92
|
end
|
data/lib/bitcoin/key.rb
CHANGED
data/lib/bitcoin/message/base.rb
CHANGED
|
@@ -32,11 +32,17 @@ module Bitcoin
|
|
|
32
32
|
magic = buf.read(4)
|
|
33
33
|
raise ArgumentError, 'Invalid magic.' unless magic == Bitcoin.chain_params.magic_head.htb
|
|
34
34
|
command = buf.read(12).delete("\x00")
|
|
35
|
+
# The length is announced by the peer, so it is checked before it is used to read.
|
|
35
36
|
length = buf.read(4).unpack1('V')
|
|
37
|
+
if length > MAX_PROTOCOL_MESSAGE_LENGTH
|
|
38
|
+
raise ArgumentError, "Payload must not be longer than #{MAX_PROTOCOL_MESSAGE_LENGTH} bytes."
|
|
39
|
+
end
|
|
36
40
|
checksum = buf.read(4)
|
|
41
|
+
# read returns "" for a length of 0 and nil once the buffer is exhausted.
|
|
37
42
|
payload = buf.read(length)
|
|
43
|
+
raise ArgumentError, 'Payload is shorter than the announced length.' unless payload&.bytesize == length
|
|
38
44
|
raise ArgumentError, 'Checksum do not match.' unless checksum == Bitcoin.double_sha256(payload)[0...4]
|
|
39
|
-
Bitcoin::Message.decode(command, payload
|
|
45
|
+
Bitcoin::Message.decode(command, payload.bth)
|
|
40
46
|
end
|
|
41
47
|
|
|
42
48
|
end
|
|
@@ -22,8 +22,18 @@ module Bitcoin
|
|
|
22
22
|
def self.parse_from_payload(payload)
|
|
23
23
|
buf = StringIO.new(payload)
|
|
24
24
|
filter_count = Bitcoin.unpack_var_int_from_io(buf)
|
|
25
|
+
# A filterload message is received from an untrusted peer, so the limits which bound
|
|
26
|
+
# the work BloomFilter#add and #contains? perform have to be enforced here.
|
|
27
|
+
if filter_count > Bitcoin::BloomFilter::MAX_BLOOM_FILTER_SIZE
|
|
28
|
+
raise Bitcoin::Message::Error,
|
|
29
|
+
"filter size must be less than or equal to #{Bitcoin::BloomFilter::MAX_BLOOM_FILTER_SIZE}."
|
|
30
|
+
end
|
|
25
31
|
filter = buf.read(filter_count).unpack('C*')
|
|
26
32
|
func_count = buf.read(4).unpack1('V')
|
|
33
|
+
if func_count > Bitcoin::BloomFilter::MAX_HASH_FUNCS
|
|
34
|
+
raise Bitcoin::Message::Error,
|
|
35
|
+
"hash funcs must be less than or equal to #{Bitcoin::BloomFilter::MAX_HASH_FUNCS}."
|
|
36
|
+
end
|
|
27
37
|
tweak = buf.read(4).unpack1('V')
|
|
28
38
|
flag = buf.read(1).unpack1('C')
|
|
29
39
|
FilterLoad.new(Bitcoin::BloomFilter.new(filter, func_count, tweak), flag)
|
|
@@ -28,6 +28,12 @@ module Bitcoin
|
|
|
28
28
|
flag_count = Bitcoin.unpack_var_int_from_io(buf)
|
|
29
29
|
# A sequence of bits packed eight in a byte with the least significant bit first.
|
|
30
30
|
m.flags = buf.read(flag_count).bth
|
|
31
|
+
# Reject a tree an untrusted peer could use to exhaust memory or CPU in #partial_tree.
|
|
32
|
+
begin
|
|
33
|
+
Bitcoin::PartialTree.validate!(m.tx_count, m.hashes, Bitcoin.byte_to_bit(m.flags.htb))
|
|
34
|
+
rescue ArgumentError => e
|
|
35
|
+
raise Bitcoin::Message::Error, e.message
|
|
36
|
+
end
|
|
31
37
|
m
|
|
32
38
|
end
|
|
33
39
|
|
data/lib/bitcoin/message.rb
CHANGED
|
@@ -51,6 +51,9 @@ module Bitcoin
|
|
|
51
51
|
|
|
52
52
|
USER_AGENT = "/bitcoinrb:#{Bitcoin::VERSION}/"
|
|
53
53
|
|
|
54
|
+
# The maximum payload size a peer may announce in a message header.
|
|
55
|
+
MAX_PROTOCOL_MESSAGE_LENGTH = 4_000_000
|
|
56
|
+
|
|
54
57
|
SERVICE_FLAGS = {
|
|
55
58
|
none: 0,
|
|
56
59
|
network: 1 << 0, # the node is capable of serving the block chain. It is currently set by all Bitcoin Core node, and is unset by SPV clients or other peers that just want network services but don't provide them.
|
data/lib/bitcoin/partial_tree.rb
CHANGED
|
@@ -4,6 +4,10 @@ module Bitcoin
|
|
|
4
4
|
# For a complete Merkle tree implementation, migrate to the merkle gem.
|
|
5
5
|
class PartialTree
|
|
6
6
|
|
|
7
|
+
# The maximum number of transactions a block can contain, so the maximum tx_count
|
|
8
|
+
# a valid merkleblock message can commit to.
|
|
9
|
+
MAX_TX_COUNT = MAX_BLOCK_WEIGHT / MIN_TRANSACTION_WEIGHT
|
|
10
|
+
|
|
7
11
|
attr_accessor :root
|
|
8
12
|
|
|
9
13
|
def initialize(root = nil)
|
|
@@ -15,7 +19,13 @@ module Bitcoin
|
|
|
15
19
|
end
|
|
16
20
|
|
|
17
21
|
# https://bitcoin.org/en/developer-reference#creating-a-merkleblock-message
|
|
22
|
+
# @param [Integer] tx_count The number of transactions in the block.
|
|
23
|
+
# @param [Array] hashes Array of hash values with hex format.
|
|
24
|
+
# @param [String] flags The sequence of bits, a string of '0' and '1'.
|
|
25
|
+
# @return [Bitcoin::PartialTree]
|
|
26
|
+
# @raise [ArgumentError] If the given parameters are out of range or inconsistent.
|
|
18
27
|
def self.build(tx_count, hashes, flags)
|
|
28
|
+
validate!(tx_count, hashes, flags)
|
|
19
29
|
flags = flags.each_char.map(&:to_i)
|
|
20
30
|
root = build_initial_tree( Array.new(tx_count) { Node.new })
|
|
21
31
|
current_node = root
|
|
@@ -37,7 +47,22 @@ module Bitcoin
|
|
|
37
47
|
new(root)
|
|
38
48
|
end
|
|
39
49
|
|
|
50
|
+
# Validate the parameters of a partial merkle tree.
|
|
51
|
+
# A merkleblock message is received from an untrusted peer, so these bounds are required
|
|
52
|
+
# to stop it from making #build allocate an unbounded number of nodes or loop forever.
|
|
53
|
+
# See Bitcoin Core's CPartialMerkleTree::ExtractMatches.
|
|
54
|
+
# @raise [ArgumentError]
|
|
55
|
+
def self.validate!(tx_count, hashes, flags)
|
|
56
|
+
raise ArgumentError, 'tx_count must be greater than 0.' unless tx_count.is_a?(Integer) && tx_count > 0
|
|
57
|
+
raise ArgumentError, "tx_count must be less than or equal to #{MAX_TX_COUNT}." if tx_count > MAX_TX_COUNT
|
|
58
|
+
# There can never be more hashes provided than one for every txid.
|
|
59
|
+
raise ArgumentError, 'hashes must not be greater than tx_count.' if hashes.size > tx_count
|
|
60
|
+
# There must be at least one bit per node in the partial tree, and at least one node per hash.
|
|
61
|
+
raise ArgumentError, 'flags must have at least one bit per hash.' if flags.size < hashes.size
|
|
62
|
+
end
|
|
63
|
+
|
|
40
64
|
def self.build_initial_tree(nodes)
|
|
65
|
+
raise ArgumentError, 'nodes must not be empty.' if nodes.empty?
|
|
41
66
|
while nodes.size != 1
|
|
42
67
|
nodes = nodes.each_slice(2).map { |m|
|
|
43
68
|
parent = Node.new
|
data/lib/bitcoin/psbt/input.rb
CHANGED
|
@@ -192,6 +192,13 @@ module Bitcoin
|
|
|
192
192
|
payload
|
|
193
193
|
end
|
|
194
194
|
|
|
195
|
+
# Get the previous output this input spends.
|
|
196
|
+
# @param [Integer] index The index of the outpoint this input refers to.
|
|
197
|
+
# @return [Bitcoin::TxOut] The previous output, or nil if this input does not carry it.
|
|
198
|
+
def utxo(index)
|
|
199
|
+
(non_witness_utxo ? non_witness_utxo.out[index] : nil) || witness_utxo
|
|
200
|
+
end
|
|
201
|
+
|
|
195
202
|
# Check whether input's scriptPubkey is correct witness.
|
|
196
203
|
# @return [Boolean]
|
|
197
204
|
def valid_witness_input?
|
data/lib/bitcoin/psbt/tx.rb
CHANGED
|
@@ -277,16 +277,17 @@ module Bitcoin
|
|
|
277
277
|
extract_tx.in[index].script_sig = input.final_script_sig if input.final_script_sig
|
|
278
278
|
extract_tx.in[index].script_witness = input.final_script_witness if input.final_script_witness
|
|
279
279
|
end
|
|
280
|
+
# A taproot sighash commits to every prevout of the tx, see BIP-341, so they are all
|
|
281
|
+
# collected before any input is verified.
|
|
282
|
+
prevouts = extract_tx.in.each_with_index.map do |tx_in, index|
|
|
283
|
+
utxo = inputs[index].utxo(tx_in.out_point.index)
|
|
284
|
+
raise ArgumentError, "input[#{index}] does not have utxo." unless utxo
|
|
285
|
+
utxo
|
|
286
|
+
end
|
|
280
287
|
# validate signature
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
utxo = input.non_witness_utxo.out[tx_in.out_point.index]
|
|
285
|
-
raise "input[#{index}]'s signature is invalid.'" unless extract_tx.verify_input_sig(index, utxo.script_pubkey)
|
|
286
|
-
else
|
|
287
|
-
utxo = input.witness_utxo
|
|
288
|
-
raise ArgumentError, "input[#{index}] does not have utxo." unless utxo
|
|
289
|
-
raise "input[#{index}]'s signature is invalid.'" unless extract_tx.verify_input_sig(index, utxo.script_pubkey, amount: utxo.value)
|
|
288
|
+
prevouts.each_with_index do |utxo, index|
|
|
289
|
+
unless extract_tx.verify_input_sig(index, utxo.script_pubkey, amount: utxo.value, prevouts: prevouts)
|
|
290
|
+
raise "input[#{index}]'s signature is invalid.'"
|
|
290
291
|
end
|
|
291
292
|
end
|
|
292
293
|
extract_tx
|
|
@@ -61,8 +61,9 @@ module Bitcoin
|
|
|
61
61
|
request.content_type = 'application/json'
|
|
62
62
|
request.body = data.to_json
|
|
63
63
|
response = http.request(request)
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
# Unescape \uXXXX with JSON.parse itself. Doing it beforehand lets a " contained in
|
|
65
|
+
# a string value close that string and inject arbitrary JSON into the parsed result.
|
|
66
|
+
json_data = JSON.parse(response.body)
|
|
66
67
|
response = convert_floats_to_strings(json_data)
|
|
67
68
|
raise response['error'].to_json if response['error']
|
|
68
69
|
response['result']
|
|
@@ -124,7 +124,7 @@ module Bitcoin
|
|
|
124
124
|
segwit_addr = Bech32::SegwitAddr.new(addr)
|
|
125
125
|
raise ArgumentError, 'Invalid address.' unless Bitcoin.chain_params.bech32_hrp == segwit_addr.hrp
|
|
126
126
|
Bitcoin::Script.parse_from_payload(segwit_addr.to_script_pubkey.htb)
|
|
127
|
-
rescue
|
|
127
|
+
rescue StandardError => e
|
|
128
128
|
begin
|
|
129
129
|
hex, addr_version = Bitcoin.decode_base58_address(addr)
|
|
130
130
|
rescue
|
|
@@ -30,7 +30,7 @@ module Bitcoin
|
|
|
30
30
|
begin
|
|
31
31
|
key = Key.new(pubkey: pubkey, key_type: key_type, allow_hybrid: allow_hybrid)
|
|
32
32
|
key.verify(sig, sighash)
|
|
33
|
-
rescue
|
|
33
|
+
rescue StandardError
|
|
34
34
|
false
|
|
35
35
|
end
|
|
36
36
|
end
|
|
@@ -40,9 +40,14 @@ module Bitcoin
|
|
|
40
40
|
# @param [String] pubkey a public key with hex format.
|
|
41
41
|
# @param [Symbol] sig_version whether :taproot or :tapscript
|
|
42
42
|
# @return [Boolean] verification result
|
|
43
|
+
# @raise [ArgumentError] If +prevouts+ does not cover every input of the tx.
|
|
43
44
|
def check_schnorr_sig(sig, pubkey, sig_version, opts = {})
|
|
44
45
|
return false unless [:taproot, :tapscript].include?(sig_version)
|
|
45
|
-
|
|
46
|
+
# A taproot sighash commits to the amount and scriptPubkey of every prevout, so a partial
|
|
47
|
+
# set does not fail to verify, it verifies against a sighash consensus never computes.
|
|
48
|
+
# Checked here rather than left to the generator, since the rescue below would turn it
|
|
49
|
+
# into a script error.
|
|
50
|
+
raise ArgumentError, 'prevouts must be specified for all inputs.' unless prevouts.size == tx.in.size
|
|
46
51
|
|
|
47
52
|
sig = sig.htb
|
|
48
53
|
return set_error(SCRIPT_ERR_SCHNORR_SIG_SIZE) unless [64, 65].include?(sig.bytesize)
|
|
@@ -6,7 +6,8 @@ module Bitcoin
|
|
|
6
6
|
module Secp256k1
|
|
7
7
|
|
|
8
8
|
# binding for secp256k1 (https://github.com/bitcoin-core/secp256k1/)
|
|
9
|
-
# tag: v0.
|
|
9
|
+
# tag: v0.8.0, which secp256k1rb 0.8.0 binds the public API of. An older library is missing
|
|
10
|
+
# symbols the gem attaches, such as secp256k1_ec_pubkey_sort and the musig module.
|
|
10
11
|
# this is not included by default, to enable set shared object path to ENV['SECP256K1_LIB_PATH']
|
|
11
12
|
# for linux, ENV['SECP256K1_LIB_PATH'] = '/usr/local/lib/libsecp256k1.so' or '/usr/lib64/libsecp256k1.so'
|
|
12
13
|
# for mac,
|
|
@@ -98,6 +99,54 @@ module Bitcoin
|
|
|
98
99
|
raise ArgumentError, "unknown algo: #{algo}"
|
|
99
100
|
end
|
|
100
101
|
end
|
|
102
|
+
|
|
103
|
+
# Whether the loaded library supports BIP-352 silent payments.
|
|
104
|
+
# The module exists in libsecp256k1 v0.8.0 or later.
|
|
105
|
+
# @return [Boolean]
|
|
106
|
+
def sp_available?
|
|
107
|
+
silentpayments_available?
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Create the silent payment outputs for +recipients+.
|
|
111
|
+
# @param [Array] recipients An array of [scan public key, spend public key], both with hex
|
|
112
|
+
# format(33 bytes). A recipient may appear more than once.
|
|
113
|
+
# @param [String] outpoint_smallest The lexicographically smallest outpoint of the tx inputs
|
|
114
|
+
# with hex format(36 bytes).
|
|
115
|
+
# @param [Array] plain_seckeys Private keys of the non-taproot inputs with hex format.
|
|
116
|
+
# @param [Array] taproot_seckeys Private keys of the taproot inputs with hex format.
|
|
117
|
+
# @return [Array] An x-only public key with hex format for each recipient, in the same order.
|
|
118
|
+
# @raise [Secp256k1::Error] If the outputs could not be created.
|
|
119
|
+
def sp_create_outputs(recipients, outpoint_smallest, plain_seckeys: [], taproot_seckeys: [])
|
|
120
|
+
silentpayments_sender_create_outputs(
|
|
121
|
+
recipients, outpoint_smallest, plain_seckeys: plain_seckeys, taproot_seckeys: taproot_seckeys)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Create the label and the label tweak of the +m+ th label of +scan_key+.
|
|
125
|
+
# @param [String] scan_key The recipient's scan private key with hex format(32 bytes).
|
|
126
|
+
# @param [Integer] m The label index.
|
|
127
|
+
# @return [Array] The serialized label(33 bytes) and its tweak(32 bytes), both with hex format.
|
|
128
|
+
def sp_create_label(scan_key, m)
|
|
129
|
+
silentpayments_create_label(scan_key, m)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Scan +tx_outputs+ for the silent payment outputs of the recipient.
|
|
133
|
+
# @param [Array] tx_outputs The x-only public key of each taproot output with hex format.
|
|
134
|
+
# @param [String] scan_key The recipient's scan private key with hex format(32 bytes).
|
|
135
|
+
# @param [String] outpoint_smallest The lexicographically smallest outpoint of the tx inputs
|
|
136
|
+
# with hex format(36 bytes).
|
|
137
|
+
# @param [String] spend_pubkey The recipient's spend public key with hex format(33 bytes).
|
|
138
|
+
# @param [Array] plain_pubkeys Public keys of the non-taproot inputs with hex format(33 bytes).
|
|
139
|
+
# @param [Array] xonly_pubkeys X-only public keys of the taproot inputs with hex format.
|
|
140
|
+
# @param [Hash] labels A serialized label to label tweak map, both with hex format.
|
|
141
|
+
# @return [Array] A hash per found output, with the :output, :tweak and :label keys.
|
|
142
|
+
# @raise [Secp256k1::Error] If the tx is not a silent payment transaction.
|
|
143
|
+
def sp_scan_outputs(tx_outputs, scan_key, outpoint_smallest, spend_pubkey,
|
|
144
|
+
plain_pubkeys: [], xonly_pubkeys: [], labels: {})
|
|
145
|
+
summary = silentpayments_create_prevouts_summary(
|
|
146
|
+
outpoint_smallest, plain_pubkeys: plain_pubkeys, xonly_pubkeys: xonly_pubkeys)
|
|
147
|
+
silentpayments_scan_outputs(
|
|
148
|
+
tx_outputs, scan_key, summary, spend_pubkey, labels: labels.empty? ? nil : labels)
|
|
149
|
+
end
|
|
101
150
|
end
|
|
102
151
|
end
|
|
103
152
|
end
|
|
@@ -45,7 +45,7 @@ module Bitcoin
|
|
|
45
45
|
return false unless pubkey.bytesize == X_ONLY_PUBKEY_SIZE
|
|
46
46
|
begin
|
|
47
47
|
ECDSA::Format::PointOctetString.decode(pubkey, ECDSA::Group::Secp256k1)
|
|
48
|
-
rescue
|
|
48
|
+
rescue StandardError
|
|
49
49
|
return false
|
|
50
50
|
end
|
|
51
51
|
true
|
|
@@ -201,7 +201,7 @@ module Bitcoin
|
|
|
201
201
|
k = ECDSA::Format::PointOctetString.decode(repack_pubkey(pubkey), GROUP)
|
|
202
202
|
signature = ECDSA::Format::SignatureDerString.decode(sig)
|
|
203
203
|
ECDSA.valid_signature?(k, data, signature)
|
|
204
|
-
rescue
|
|
204
|
+
rescue StandardError
|
|
205
205
|
false
|
|
206
206
|
end
|
|
207
207
|
end
|
|
@@ -210,6 +210,119 @@ module Bitcoin
|
|
|
210
210
|
Schnorr.valid_sig?(data, pubkey.htb, sig)
|
|
211
211
|
end
|
|
212
212
|
|
|
213
|
+
# Whether this module supports BIP-352 silent payments.
|
|
214
|
+
# @return [Boolean]
|
|
215
|
+
def sp_available?
|
|
216
|
+
true
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Create the silent payment outputs for +recipients+.
|
|
220
|
+
# See Bitcoin::Secp256k1::Native#sp_create_outputs for the parameters.
|
|
221
|
+
# @return [Array] An x-only public key with hex format for each recipient, in the same order.
|
|
222
|
+
# @raise [ArgumentError] If the input private keys sum to zero.
|
|
223
|
+
def sp_create_outputs(recipients, outpoint_smallest, plain_seckeys: [], taproot_seckeys: [])
|
|
224
|
+
field = ECDSA::PrimeField.new(GROUP.order)
|
|
225
|
+
sum = sp_sum_seckeys(plain_seckeys, taproot_seckeys, field)
|
|
226
|
+
raise ArgumentError, 'The input private keys sum to zero.' if sum.zero?
|
|
227
|
+
agg_pubkey = (GROUP.generator.to_jacobian * sum).to_affine
|
|
228
|
+
input_hash = Bitcoin.tagged_hash('BIP0352/Inputs', outpoint_smallest.htb + agg_pubkey.to_hex.htb)
|
|
229
|
+
|
|
230
|
+
# k counts up within the group of recipients sharing a scan key, but an output keeps the
|
|
231
|
+
# position of the recipient it pays.
|
|
232
|
+
groups = {}
|
|
233
|
+
recipients.each_with_index do |(scan_pubkey, spend_pubkey), index|
|
|
234
|
+
(groups[scan_pubkey] ||= []) << [spend_pubkey, index]
|
|
235
|
+
end
|
|
236
|
+
results = Array.new(recipients.length)
|
|
237
|
+
groups.each do |scan_pubkey, spends|
|
|
238
|
+
scan_point = Bitcoin::Key.new(pubkey: scan_pubkey).to_point.to_jacobian
|
|
239
|
+
shared_secret = (scan_point * field.mod(input_hash.bti * sum)).to_affine.to_hex.htb
|
|
240
|
+
spends.each_with_index do |(spend_pubkey, index), k|
|
|
241
|
+
t_k = Bitcoin.tagged_hash('BIP0352/SharedSecret', shared_secret + [k].pack('N'))
|
|
242
|
+
spend_point = Bitcoin::Key.new(pubkey: spend_pubkey).to_point.to_jacobian
|
|
243
|
+
output = (spend_point + GROUP.generator.to_jacobian * t_k.bti).to_affine
|
|
244
|
+
results[index] = sp_xonly(output.x)
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
results
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Create the label and the label tweak of the +m+ th label of +scan_key+.
|
|
251
|
+
# See Bitcoin::Secp256k1::Native#sp_create_label for the parameters.
|
|
252
|
+
# @return [Array] The serialized label(33 bytes) and its tweak(32 bytes), both with hex format.
|
|
253
|
+
def sp_create_label(scan_key, m)
|
|
254
|
+
tweak = Bitcoin.tagged_hash('BIP0352/Label', scan_key.htb + [m].pack('N'))
|
|
255
|
+
[(GROUP.generator.to_jacobian * tweak.bti).to_affine.to_hex(true), tweak.bth]
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Scan +tx_outputs+ for the silent payment outputs of the recipient.
|
|
259
|
+
# See Bitcoin::Secp256k1::Native#sp_scan_outputs for the parameters.
|
|
260
|
+
# @return [Array] A hash per found output, with the :output, :tweak and :label keys.
|
|
261
|
+
# @raise [ArgumentError] If the input public keys sum to the point at infinity.
|
|
262
|
+
def sp_scan_outputs(tx_outputs, scan_key, outpoint_smallest, spend_pubkey,
|
|
263
|
+
plain_pubkeys: [], xonly_pubkeys: [], labels: {})
|
|
264
|
+
field = ECDSA::PrimeField.new(GROUP.order)
|
|
265
|
+
sum_pubkeys = GROUP.infinity.to_jacobian
|
|
266
|
+
plain_pubkeys.each { |p| sum_pubkeys += Bitcoin::Key.new(pubkey: p).to_point.to_jacobian }
|
|
267
|
+
xonly_pubkeys.each { |p| sum_pubkeys += Bitcoin::Key.from_xonly_pubkey(p).to_point.to_jacobian }
|
|
268
|
+
raise ArgumentError, 'The input public keys sum to the point at infinity.' if sum_pubkeys.infinity?
|
|
269
|
+
|
|
270
|
+
input_hash = Bitcoin.tagged_hash(
|
|
271
|
+
'BIP0352/Inputs', outpoint_smallest.htb + sum_pubkeys.to_affine.to_hex.htb)
|
|
272
|
+
shared_secret = (sum_pubkeys * field.mod(input_hash.bti * scan_key.to_i(16))).to_affine.to_hex.htb
|
|
273
|
+
spend_point = Bitcoin::Key.new(pubkey: spend_pubkey).to_point.to_jacobian
|
|
274
|
+
# A labeled output is P_k + label. Only the x coordinate is compared, which covers the
|
|
275
|
+
# label of either parity without negating the output.
|
|
276
|
+
label_points = labels.map do |label, tweak|
|
|
277
|
+
[label, tweak, Bitcoin::Key.new(pubkey: label).to_point.to_jacobian]
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
results = []
|
|
281
|
+
remaining = tx_outputs.map(&:downcase)
|
|
282
|
+
k = 0
|
|
283
|
+
while k < Bitcoin::SilentPayment::K_MAX
|
|
284
|
+
t_k = Bitcoin.tagged_hash('BIP0352/SharedSecret', shared_secret + [k].pack('N'))
|
|
285
|
+
p_k = GROUP.generator.to_jacobian * t_k.bti + spend_point
|
|
286
|
+
index = remaining.index(sp_xonly(p_k.to_affine.x))
|
|
287
|
+
found = if index
|
|
288
|
+
{output: remaining.delete_at(index), tweak: t_k.bth, label: nil}
|
|
289
|
+
else
|
|
290
|
+
sp_find_labeled(p_k, remaining, label_points, t_k, field)
|
|
291
|
+
end
|
|
292
|
+
break unless found
|
|
293
|
+
results << found
|
|
294
|
+
k += 1
|
|
295
|
+
end
|
|
296
|
+
results
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Sum the private keys of the inputs, negating a taproot key whose public key has odd y.
|
|
300
|
+
def sp_sum_seckeys(plain_seckeys, taproot_seckeys, field)
|
|
301
|
+
sum = plain_seckeys.inject(0) { |total, sk| field.mod(total + sk.to_i(16)) }
|
|
302
|
+
taproot_seckeys.inject(sum) do |total, sk|
|
|
303
|
+
d = sk.to_i(16)
|
|
304
|
+
d = field.mod(-d) unless (GROUP.generator.to_jacobian * d).to_affine.has_even_y?
|
|
305
|
+
field.mod(total + d)
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# Find the output +p_k+ pays through one of +label_points+, or nil.
|
|
310
|
+
def sp_find_labeled(p_k, remaining, label_points, t_k, field)
|
|
311
|
+
label_points.each do |label, tweak, point|
|
|
312
|
+
index = remaining.index(sp_xonly((p_k + point).to_affine.x))
|
|
313
|
+
next unless index
|
|
314
|
+
return {output: remaining.delete_at(index),
|
|
315
|
+
tweak: sp_xonly(field.mod(t_k.bti + tweak.to_i(16))),
|
|
316
|
+
label: label}
|
|
317
|
+
end
|
|
318
|
+
nil
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# Serialize a field element as a 32 byte value with hex format.
|
|
322
|
+
def sp_xonly(value)
|
|
323
|
+
ECDSA::Format::IntegerOctetString.encode(value, 32).bth
|
|
324
|
+
end
|
|
325
|
+
|
|
213
326
|
end
|
|
214
327
|
end
|
|
215
328
|
end
|
|
@@ -107,8 +107,14 @@ module Bitcoin
|
|
|
107
107
|
# - leaf_hash: leaf hash with binary format if sig_version is :tapscript, it required
|
|
108
108
|
# - last_code_separator_pos: the position of last code separator
|
|
109
109
|
# @return [String] signature hash with binary format.
|
|
110
|
+
# @raise [ArgumentError] If +opts[:prevouts]+ does not cover every input of the tx.
|
|
110
111
|
def generate(tx, input_index, hash_type, opts)
|
|
111
112
|
raise ArgumentError, 'Invalid sig_version was specified.' unless [:taproot, :tapscript].include?(opts[:sig_version])
|
|
113
|
+
# sha_amounts and sha_scriptpubkeys commit to every prevout, so a partial set silently
|
|
114
|
+
# produces a sighash which is not the one consensus computes for this tx.
|
|
115
|
+
unless opts[:prevouts].is_a?(Array) && opts[:prevouts].size == tx.in.size
|
|
116
|
+
raise ArgumentError, 'prevouts must be specified for all inputs.'
|
|
117
|
+
end
|
|
112
118
|
|
|
113
119
|
ext_flag = opts[:sig_version] == :taproot ? 0 : 1
|
|
114
120
|
key_version = 0
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
require 'set'
|
|
2
|
-
|
|
3
1
|
module Bitcoin
|
|
4
2
|
# BIP-352 silent payment module.
|
|
5
3
|
# @see https://github.com/bitcoin/bips/blob/master/bip-0352.mediawiki
|
|
@@ -16,7 +14,7 @@ module Bitcoin
|
|
|
16
14
|
# @param [Array<Bitcoin::Script>] prevouts An array of previous output script.
|
|
17
15
|
# @param [Array<Bitcoin::Key>] private_keys An array of Bitcoin::Key objects corresponding to each public key in prevouts.
|
|
18
16
|
# @param [Array<Bech32::SilentPaymentAddr>] recipients
|
|
19
|
-
# @return [Array<ECDSA::Point>] An array of derived points.
|
|
17
|
+
# @return [Array<ECDSA::Point>] An array of derived points, one per recipient in the same order.
|
|
20
18
|
# @raise [ArgumentError]
|
|
21
19
|
def derive_payment_points(prevouts, private_keys, recipients)
|
|
22
20
|
raise ArgumentError, "prevouts must be Array." unless prevouts.is_a? Array
|
|
@@ -24,53 +22,41 @@ module Bitcoin
|
|
|
24
22
|
raise ArgumentError, "prevouts and private_keys must be the same length." unless prevouts.length == private_keys.length
|
|
25
23
|
raise ArgumentError, "recipients must be Array." unless recipients.is_a? Array
|
|
26
24
|
|
|
27
|
-
input_pub_keys = []
|
|
28
25
|
field = ECDSA::PrimeField.new(Bitcoin::Secp256k1::GROUP.order)
|
|
26
|
+
plain_seckeys = []
|
|
27
|
+
taproot_seckeys = []
|
|
29
28
|
sum_priv_keys = 0
|
|
30
29
|
prevouts.each_with_index do |prevout, index|
|
|
31
30
|
key = private_keys[index]
|
|
32
31
|
raise ArgumentError, "private_keys element must be Bitcoin::Key." unless key.is_a? Bitcoin::Key
|
|
33
|
-
priv_key_int = key.priv_key.to_i(16)
|
|
34
32
|
public_key = extract_public_key(prevout, inputs[index])
|
|
35
33
|
next if public_key.nil?
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
34
|
+
priv_key_int = key.priv_key.to_i(16)
|
|
35
|
+
if public_key.p2tr?
|
|
36
|
+
taproot_seckeys << key.priv_key
|
|
37
|
+
priv_key_int = field.mod(-priv_key_int) unless key.to_point.has_even_y?
|
|
38
|
+
else
|
|
39
|
+
plain_seckeys << key.priv_key
|
|
40
|
+
end
|
|
41
|
+
sum_priv_keys = field.mod(sum_priv_keys + priv_key_int)
|
|
43
42
|
end
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
input_hash = Bitcoin.tagged_hash("BIP0352/Inputs", outpoint_l.htb + agg_pubkey.to_hex.htb).bth
|
|
43
|
+
return [] if plain_seckeys.empty? && taproot_seckeys.empty?
|
|
44
|
+
# The input private keys sum to zero, so the aggregate public key is the point at infinity
|
|
45
|
+
# and no shared secret exists.
|
|
46
|
+
return [] if sum_priv_keys.zero?
|
|
50
47
|
|
|
51
|
-
destinations =
|
|
52
|
-
recipients.each do |sp_addr|
|
|
48
|
+
destinations = recipients.map do |sp_addr|
|
|
53
49
|
raise ArgumentError, "recipients element must be Bech32::SilentPaymentAddr." unless sp_addr.is_a? Bech32::SilentPaymentAddr
|
|
54
|
-
|
|
55
|
-
destinations[sp_addr.scan_key] << sp_addr.spend_key
|
|
50
|
+
[sp_addr.scan_key, sp_addr.spend_key]
|
|
56
51
|
end
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
destinations.each_value do |spends|
|
|
60
|
-
raise ArgumentError, "Recipient group exceeds K_max limit (#{K_MAX})." if spends.length > K_MAX
|
|
52
|
+
destinations.group_by(&:first).each_value do |group|
|
|
53
|
+
raise ArgumentError, "Recipient group exceeds K_max limit (#{K_MAX})." if group.length > K_MAX
|
|
61
54
|
end
|
|
62
55
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
spends.each.with_index do |spend, i|
|
|
68
|
-
t_k = Bitcoin.tagged_hash('BIP0352/SharedSecret', ecdh_shared_secret + [i].pack('N'))
|
|
69
|
-
spend_key = Bitcoin::Key.new(pubkey: spend).to_point.to_jacobian
|
|
70
|
-
outputs << (spend_key + Bitcoin::Secp256k1::GROUP.generator.to_jacobian * t_k.bth.to_i(16)).to_affine
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
outputs
|
|
56
|
+
Bitcoin.secp_impl.sp_create_outputs(
|
|
57
|
+
destinations, sp_outpoint_smallest,
|
|
58
|
+
plain_seckeys: plain_seckeys, taproot_seckeys: taproot_seckeys
|
|
59
|
+
).map { |xonly| Bitcoin::Key.from_xonly_pubkey(xonly).to_point }
|
|
74
60
|
end
|
|
75
61
|
|
|
76
62
|
|
|
@@ -87,8 +73,11 @@ module Bitcoin
|
|
|
87
73
|
raise ArgumentError, "scan_private_key must be Bitcoin::Key." unless scan_private_key.is_a? Bitcoin::Key
|
|
88
74
|
raise ArgumentError, "spend_pubkey must be Bitcoin::Key." unless spend_pubkey.is_a? Bitcoin::Key
|
|
89
75
|
|
|
90
|
-
|
|
91
|
-
return []
|
|
76
|
+
taproot_outputs = outputs.select{|o| o.script_pubkey.p2tr? }
|
|
77
|
+
return [] if taproot_outputs.empty?
|
|
78
|
+
|
|
79
|
+
plain_pubkeys = []
|
|
80
|
+
xonly_pubkeys = []
|
|
92
81
|
sum_pub_keys = Bitcoin::Secp256k1::GROUP.infinity.to_jacobian
|
|
93
82
|
maximum_witness_version = Bitcoin::Opcodes.opcode_to_small_int(Bitcoin::Opcodes::OP_1)
|
|
94
83
|
prevouts.each.with_index do |prevout, index|
|
|
@@ -96,64 +85,41 @@ module Bitcoin
|
|
|
96
85
|
|
|
97
86
|
public_key = extract_public_key(prevout, inputs[index])
|
|
98
87
|
next if public_key.nil?
|
|
88
|
+
if public_key.p2tr?
|
|
89
|
+
xonly_pubkeys << public_key.xonly_pubkey
|
|
90
|
+
else
|
|
91
|
+
plain_pubkeys << public_key.pubkey
|
|
92
|
+
end
|
|
99
93
|
sum_pub_keys += public_key.to_point.to_jacobian
|
|
100
94
|
end
|
|
95
|
+
return [] if plain_pubkeys.empty? && xonly_pubkeys.empty?
|
|
96
|
+
# Not a silent payment transaction, so there is nothing to find. Checked here rather than
|
|
97
|
+
# left to the implementation, which reports it as an error.
|
|
101
98
|
return [] if sum_pub_keys.infinity?
|
|
102
99
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
label_tweak = Bitcoin.tagged_hash('BIP0352/Label', scan_private_key.priv_key.htb + [m].pack('N'))
|
|
111
|
-
label_point = Bitcoin::Secp256k1::GROUP.generator.to_jacobian * label_tweak.bti
|
|
112
|
-
[m, label_tweak, label_point]
|
|
100
|
+
impl = Bitcoin.secp_impl
|
|
101
|
+
label_values = {}
|
|
102
|
+
label_tweaks = {}
|
|
103
|
+
labels.each do |m|
|
|
104
|
+
label, tweak = impl.sp_create_label(scan_private_key.priv_key, m)
|
|
105
|
+
label_values[label] = m
|
|
106
|
+
label_tweaks[label] = tweak
|
|
113
107
|
end
|
|
114
108
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
t_k = Bitcoin.tagged_hash('BIP0352/SharedSecret', ecdh_shared_secret + [k].pack('N'))
|
|
123
|
-
p_k = Bitcoin::Secp256k1::GROUP.generator.to_jacobian * t_k.bti + spend_pubkey.to_point.to_jacobian
|
|
124
|
-
found = false
|
|
125
|
-
outputs.each do |output|
|
|
126
|
-
next unless output.script_pubkey.p2tr?
|
|
127
|
-
next if found_outputs.include?(output)
|
|
128
|
-
output_pubkey = Bitcoin::Key.from_xonly_pubkey(output.script_pubkey.witness_data[1].bth)
|
|
129
|
-
|
|
130
|
-
# Check basic match (no label)
|
|
131
|
-
if p_k.to_affine.x == output_pubkey.to_point.x
|
|
132
|
-
results << SilentPayment::Output.new(output, t_k)
|
|
133
|
-
found_outputs << output
|
|
134
|
-
k += 1
|
|
135
|
-
found = true
|
|
136
|
-
break
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
# Check labeled matches
|
|
140
|
-
label_tweaks.each do |label_value, label_tweak_scalar, label_point|
|
|
141
|
-
p_k_labeled = p_k + label_point
|
|
142
|
-
if p_k_labeled.to_affine.x == output_pubkey.to_point.x
|
|
143
|
-
# Full tweak is t_k + label_tweak (mod order)
|
|
144
|
-
full_tweak = field.mod(t_k.bti + label_tweak_scalar.bti).to_s(16).rjust(64, '0').htb
|
|
145
|
-
results << SilentPayment::Output.new(output, full_tweak, label_value)
|
|
146
|
-
found_outputs << output
|
|
147
|
-
k += 1
|
|
148
|
-
found = true
|
|
149
|
-
break
|
|
150
|
-
end
|
|
151
|
-
end
|
|
152
|
-
break if found
|
|
153
|
-
end
|
|
154
|
-
break unless found
|
|
109
|
+
tx_outputs = taproot_outputs.map{|o| o.script_pubkey.witness_data[1].bth }
|
|
110
|
+
impl.sp_scan_outputs(tx_outputs, scan_private_key.priv_key, sp_outpoint_smallest, spend_pubkey.pubkey,
|
|
111
|
+
plain_pubkeys: plain_pubkeys, xonly_pubkeys: xonly_pubkeys,
|
|
112
|
+
labels: label_tweaks).map do |found|
|
|
113
|
+
tx_out = taproot_outputs[tx_outputs.index(found[:output])]
|
|
114
|
+
SilentPayment::Output.new(tx_out, found[:tweak].htb, found[:label] && label_values[found[:label]])
|
|
155
115
|
end
|
|
156
|
-
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# The lexicographically smallest outpoint of this tx's inputs, which the input hash of
|
|
119
|
+
# BIP-352 commits to.
|
|
120
|
+
# @return [String] The outpoint with hex format(36 bytes).
|
|
121
|
+
def sp_outpoint_smallest
|
|
122
|
+
inputs.map{|i| i.out_point.to_hex }.min
|
|
157
123
|
end
|
|
158
124
|
|
|
159
125
|
# Extract public keys from +prevout+ and input.
|
|
@@ -35,14 +35,14 @@ module Bitcoin
|
|
|
35
35
|
|
|
36
36
|
def merkle_root
|
|
37
37
|
return '' if tree.empty?
|
|
38
|
-
script_tree = Merkle::CustomTree.new(config: Merkle::Config.taptree, leaves: extract_leaves(tree))
|
|
38
|
+
script_tree = Merkle::CustomTree.new(config: Merkle::Config.taptree(element_encoding: :hex), leaves: extract_leaves(tree))
|
|
39
39
|
script_tree.compute_root
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
def extract_leaves(leaves)
|
|
43
43
|
leaves.map do |leaf|
|
|
44
44
|
if leaf.is_a?(Bitcoin::Taproot::LeafNode)
|
|
45
|
-
leaf.leaf_hash
|
|
45
|
+
leaf.leaf_hash.bth
|
|
46
46
|
elsif leaf.is_a?(Array)
|
|
47
47
|
extract_leaves(leaf)
|
|
48
48
|
end
|
|
@@ -100,15 +100,17 @@ module Bitcoin
|
|
|
100
100
|
private
|
|
101
101
|
|
|
102
102
|
def script_tree
|
|
103
|
-
|
|
103
|
+
# A node holding one child hashes to that child, so the tree rejects the shape as
|
|
104
|
+
# indistinguishable from the child itself. A branch of one leaf, and a tree of one
|
|
105
|
+
# branch, are therefore that leaf and that branch rather than a node above them.
|
|
106
|
+
tree = nil
|
|
104
107
|
branches.each do |pair|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
leaves = [leaves, pair.map(&:leaf_hash)]
|
|
109
|
-
end
|
|
108
|
+
hashes = pair.map { |leaf| leaf.leaf_hash.bth }
|
|
109
|
+
node = hashes.length == 1 ? hashes.first : hashes
|
|
110
|
+
tree = tree.nil? ? node : [tree, node]
|
|
110
111
|
end
|
|
111
|
-
|
|
112
|
+
leaves = tree.is_a?(Array) ? tree : [tree]
|
|
113
|
+
Merkle::CustomTree.new(config: Merkle::Config.taptree(element_encoding: :hex), leaves: leaves)
|
|
112
114
|
end
|
|
113
115
|
|
|
114
116
|
def merkle_root
|
data/lib/bitcoin/taproot.rb
CHANGED
|
@@ -33,7 +33,12 @@ module Bitcoin
|
|
|
33
33
|
def tweak_public_key(internal_key, merkle_root)
|
|
34
34
|
t = tweak(internal_key, merkle_root)
|
|
35
35
|
key = Bitcoin::Key.new(priv_key: t.bth, key_type: Key::TYPES[:compressed])
|
|
36
|
-
|
|
36
|
+
# BIP-341 tweaks lift_x(P), the point with even y. #tweak commits to the x-only key and
|
|
37
|
+
# #tweak_private_key negates the private key to match, so the point is lifted here too.
|
|
38
|
+
# An internal key with odd y would otherwise give a Q the tweaked private key cannot spend.
|
|
39
|
+
internal_point = internal_key.to_point
|
|
40
|
+
internal_point = internal_point.negate unless internal_point.has_even_y?
|
|
41
|
+
Bitcoin::Key.from_point(key.to_point + internal_point)
|
|
37
42
|
end
|
|
38
43
|
|
|
39
44
|
# Generate tweak private key
|
data/lib/bitcoin/util.rb
CHANGED
data/lib/bitcoin/version.rb
CHANGED
|
@@ -8,15 +8,36 @@ module Bitcoin
|
|
|
8
8
|
include Bitcoin::Util
|
|
9
9
|
include Bitcoin::KeyPath
|
|
10
10
|
|
|
11
|
+
# The seed is not encrypted.
|
|
12
|
+
NOT_ENCRYPTED = 0
|
|
13
|
+
# AES-256-CBC with a key derived by PBKDF2-HMAC-SHA1. Only supported to load a wallet
|
|
14
|
+
# which was already encrypted, #encrypt always writes ENCRYPTED_V2.
|
|
15
|
+
ENCRYPTED_V1 = 1
|
|
16
|
+
# AES-256-GCM with a key derived by PBKDF2-HMAC-SHA512.
|
|
17
|
+
ENCRYPTED_V2 = 2
|
|
18
|
+
|
|
19
|
+
ENCRYPTION_VERSIONS = [ENCRYPTED_V1, ENCRYPTED_V2]
|
|
20
|
+
|
|
21
|
+
V1_KDF_ROUNDS = 2000
|
|
22
|
+
# Rounds for PBKDF2-HMAC-SHA512, which take roughly 100 ms.
|
|
23
|
+
KDF_ROUNDS = 210_000
|
|
24
|
+
|
|
25
|
+
SALT_SIZE = 16
|
|
26
|
+
GCM_IV_SIZE = 12
|
|
27
|
+
GCM_TAG_SIZE = 16
|
|
28
|
+
|
|
11
29
|
attr_reader :seed
|
|
12
30
|
attr_accessor :salt
|
|
13
31
|
attr_accessor :encrypted
|
|
32
|
+
# The encryption version of +seed+, nil unless encrypted.
|
|
33
|
+
attr_reader :encryption_version
|
|
14
34
|
attr_accessor :mnemonic # ephemeral data existing only at initialization
|
|
15
35
|
|
|
16
|
-
def initialize(seed, salt: '', encrypted: false, mnemonic: nil)
|
|
36
|
+
def initialize(seed, salt: '', encrypted: false, mnemonic: nil, encryption_version: nil)
|
|
17
37
|
@mnemonic = mnemonic
|
|
18
38
|
@seed = seed
|
|
19
39
|
@encrypted = encrypted
|
|
40
|
+
@encryption_version = encrypted ? (encryption_version || ENCRYPTED_V2) : nil
|
|
20
41
|
@salt = salt
|
|
21
42
|
end
|
|
22
43
|
|
|
@@ -42,17 +63,18 @@ module Bitcoin
|
|
|
42
63
|
# @return [Bitcoin::Wallet::MasterKey]
|
|
43
64
|
def self.parse_from_payload(payload)
|
|
44
65
|
flag, payload = unpack_var_int(payload)
|
|
45
|
-
raise 'encrypted flag is invalid.' unless [
|
|
66
|
+
raise 'encrypted flag is invalid.' unless [NOT_ENCRYPTED, *ENCRYPTION_VERSIONS].include?(flag)
|
|
46
67
|
salt, payload = unpack_var_string(payload)
|
|
47
68
|
salt = '' unless salt
|
|
48
69
|
seed, payload = unpack_var_string(payload)
|
|
49
|
-
self.new(seed.bth, salt: salt.bth,
|
|
70
|
+
self.new(seed.bth, salt: salt.bth,
|
|
71
|
+
encrypted: flag != NOT_ENCRYPTED, encryption_version: flag)
|
|
50
72
|
end
|
|
51
73
|
|
|
52
74
|
# generate payload with following format
|
|
53
|
-
# [encrypted
|
|
75
|
+
# [encryption version(not encrypted:0, v1:1, v2:2)][salt(var str)][seed(var str)]
|
|
54
76
|
def to_payload
|
|
55
|
-
flg = encrypted ?
|
|
77
|
+
flg = encrypted ? encryption_version : NOT_ENCRYPTED
|
|
56
78
|
pack_var_int(flg) << [salt, seed].map{|v|pack_var_string(v.htb)}.join
|
|
57
79
|
end
|
|
58
80
|
|
|
@@ -71,39 +93,76 @@ module Bitcoin
|
|
|
71
93
|
derived_key
|
|
72
94
|
end
|
|
73
95
|
|
|
74
|
-
#
|
|
96
|
+
# Encrypt the seed with +passphrase+.
|
|
97
|
+
# The stored seed is [IV(12 bytes)][auth tag(16 bytes)][ciphertext].
|
|
98
|
+
# @param [String] passphrase
|
|
75
99
|
def encrypt(passphrase)
|
|
76
100
|
raise 'The wallet is already encrypted.' if encrypted
|
|
77
|
-
|
|
78
|
-
|
|
101
|
+
salt = SecureRandom.bytes(SALT_SIZE)
|
|
102
|
+
iv = SecureRandom.bytes(GCM_IV_SIZE)
|
|
103
|
+
enc = OpenSSL::Cipher.new('AES-256-GCM')
|
|
79
104
|
enc.encrypt
|
|
80
|
-
enc.key
|
|
81
|
-
|
|
82
|
-
encrypted_data
|
|
83
|
-
|
|
84
|
-
@seed = encrypted_data.bth
|
|
105
|
+
enc.key = derive_key(passphrase, salt, enc.key_len)
|
|
106
|
+
enc.iv = iv
|
|
107
|
+
encrypted_data = enc.update(seed) << enc.final
|
|
108
|
+
@salt = salt.bth
|
|
109
|
+
@seed = (iv + enc.auth_tag(GCM_TAG_SIZE) + encrypted_data).bth
|
|
110
|
+
@encryption_version = ENCRYPTED_V2
|
|
85
111
|
@encrypted = true
|
|
86
112
|
end
|
|
87
113
|
|
|
88
|
-
#
|
|
114
|
+
# Decrypt the seed with +passphrase+.
|
|
115
|
+
# @param [String] passphrase
|
|
116
|
+
# @raise [ArgumentError] If +passphrase+ is wrong or the encrypted seed is corrupted.
|
|
89
117
|
def decrypt(passphrase)
|
|
90
118
|
raise 'The wallet is not encrypted.' unless encrypted
|
|
91
|
-
|
|
92
|
-
dec.decrypt
|
|
93
|
-
dec.key, dec.iv = key_iv(dec, passphrase)
|
|
94
|
-
decrypted_data = ''
|
|
95
|
-
decrypted_data << dec.update(seed.htb)
|
|
96
|
-
decrypted_data << dec.final
|
|
97
|
-
@seed = decrypted_data
|
|
119
|
+
@seed = encryption_version == ENCRYPTED_V1 ? decrypt_v1(passphrase) : decrypt_v2(passphrase)
|
|
98
120
|
@encrypted = false
|
|
121
|
+
@encryption_version = nil
|
|
99
122
|
@salt = ''
|
|
100
123
|
end
|
|
101
124
|
|
|
102
125
|
private
|
|
103
126
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
127
|
+
# Derive an encryption key from +passphrase+.
|
|
128
|
+
# @param [String] passphrase
|
|
129
|
+
# @param [String] salt Salt with binary format.
|
|
130
|
+
# @param [Integer] length Key length in bytes.
|
|
131
|
+
# @return [String] Derived key with binary format.
|
|
132
|
+
def derive_key(passphrase, salt, length)
|
|
133
|
+
OpenSSL::PKCS5.pbkdf2_hmac(passphrase, salt, KDF_ROUNDS, length, OpenSSL::Digest::SHA512.new)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Decrypt a seed stored by ENCRYPTED_V2.
|
|
137
|
+
# A wrong passphrase is rejected by the GCM authentication tag.
|
|
138
|
+
def decrypt_v2(passphrase)
|
|
139
|
+
data = seed.htb
|
|
140
|
+
raise ArgumentError, 'The encrypted seed is too short.' if data.bytesize <= GCM_IV_SIZE + GCM_TAG_SIZE
|
|
141
|
+
dec = OpenSSL::Cipher.new('AES-256-GCM')
|
|
142
|
+
dec.decrypt
|
|
143
|
+
dec.key = derive_key(passphrase, salt.htb, dec.key_len)
|
|
144
|
+
dec.iv = data[0...GCM_IV_SIZE]
|
|
145
|
+
dec.auth_tag = data[GCM_IV_SIZE...(GCM_IV_SIZE + GCM_TAG_SIZE)]
|
|
146
|
+
decrypt_with(dec, data[(GCM_IV_SIZE + GCM_TAG_SIZE)..-1])
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Decrypt a seed stored by ENCRYPTED_V1.
|
|
150
|
+
# That scheme has no authentication, so a wrong passphrase is only detected by the CBC
|
|
151
|
+
# padding and passes with a probability of about 1/256, yielding a garbage seed.
|
|
152
|
+
def decrypt_v1(passphrase)
|
|
153
|
+
dec = OpenSSL::Cipher.new('AES-256-CBC')
|
|
154
|
+
dec.decrypt
|
|
155
|
+
# The salt was passed to PBKDF2 as the hex string it is stored as, not as its bytes.
|
|
156
|
+
key_iv = OpenSSL::PKCS5.pbkdf2_hmac_sha1(passphrase, salt, V1_KDF_ROUNDS, dec.key_len + dec.iv_len)
|
|
157
|
+
dec.key = key_iv[0, dec.key_len]
|
|
158
|
+
dec.iv = key_iv[dec.key_len, dec.iv_len]
|
|
159
|
+
decrypt_with(dec, seed.htb)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def decrypt_with(dec, data)
|
|
163
|
+
dec.update(data) << dec.final
|
|
164
|
+
rescue OpenSSL::Cipher::CipherError
|
|
165
|
+
raise ArgumentError, 'Invalid passphrase.'
|
|
107
166
|
end
|
|
108
167
|
|
|
109
168
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: bitcoinrb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.14.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- azuchi
|
|
@@ -113,14 +113,14 @@ dependencies:
|
|
|
113
113
|
requirements:
|
|
114
114
|
- - '='
|
|
115
115
|
- !ruby/object:Gem::Version
|
|
116
|
-
version: 0.
|
|
116
|
+
version: 0.8.0
|
|
117
117
|
type: :runtime
|
|
118
118
|
prerelease: false
|
|
119
119
|
version_requirements: !ruby/object:Gem::Requirement
|
|
120
120
|
requirements:
|
|
121
121
|
- - '='
|
|
122
122
|
- !ruby/object:Gem::Version
|
|
123
|
-
version: 0.
|
|
123
|
+
version: 0.8.0
|
|
124
124
|
- !ruby/object:Gem::Dependency
|
|
125
125
|
name: logger
|
|
126
126
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -141,14 +141,14 @@ dependencies:
|
|
|
141
141
|
requirements:
|
|
142
142
|
- - '='
|
|
143
143
|
- !ruby/object:Gem::Version
|
|
144
|
-
version: 0.
|
|
144
|
+
version: 1.0.0
|
|
145
145
|
type: :runtime
|
|
146
146
|
prerelease: false
|
|
147
147
|
version_requirements: !ruby/object:Gem::Requirement
|
|
148
148
|
requirements:
|
|
149
149
|
- - '='
|
|
150
150
|
- !ruby/object:Gem::Version
|
|
151
|
-
version: 0.
|
|
151
|
+
version: 1.0.0
|
|
152
152
|
- !ruby/object:Gem::Dependency
|
|
153
153
|
name: dnsruby
|
|
154
154
|
requirement: !ruby/object:Gem::Requirement
|