bitcoinrb 1.12.1 → 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/bip321_uri.rb +3 -6
- data/lib/bitcoin/bip324/fs_chacha20.rb +1 -1
- data/lib/bitcoin/bip324/fs_chacha_poly1305.rb +1 -0
- data/lib/bitcoin/bip324.rb +6 -1
- data/lib/bitcoin/block.rb +3 -3
- data/lib/bitcoin/descriptor/checksum.rb +6 -2
- data/lib/bitcoin/ext_key.rb +8 -4
- data/lib/bitcoin/key.rb +2 -2
- data/lib/bitcoin/message/base.rb +7 -1
- data/lib/bitcoin/message/cf_parser.rb +2 -2
- data/lib/bitcoin/message/fee_filter.rb +2 -2
- data/lib/bitcoin/message/filter_load.rb +10 -0
- data/lib/bitcoin/message/header_and_short_ids.rb +4 -4
- data/lib/bitcoin/message/inventory.rb +1 -1
- data/lib/bitcoin/message/merkle_block.rb +6 -0
- data/lib/bitcoin/message/network_addr.rb +7 -3
- data/lib/bitcoin/message/ping.rb +2 -2
- data/lib/bitcoin/message/pong.rb +2 -2
- data/lib/bitcoin/message/send_cmpct.rb +2 -2
- data/lib/bitcoin/message/version.rb +3 -3
- data/lib/bitcoin/message.rb +15 -2
- data/lib/bitcoin/mnemonic.rb +9 -4
- data/lib/bitcoin/partial_tree.rb +25 -0
- data/lib/bitcoin/psbt/input.rb +58 -33
- data/lib/bitcoin/psbt/key_origin_info.rb +2 -2
- data/lib/bitcoin/psbt/output.rb +9 -5
- data/lib/bitcoin/psbt/tx.rb +22 -15
- data/lib/bitcoin/psbt.rb +1 -1
- data/lib/bitcoin/rpc/bitcoin_core_client.rb +3 -2
- data/lib/bitcoin/script/script.rb +15 -5
- 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/rfc6979.rb +6 -3
- data/lib/bitcoin/secp256k1/ruby.rb +115 -2
- data/lib/bitcoin/sighash_generator.rb +8 -2
- data/lib/bitcoin/silent_payment.rb +56 -90
- data/lib/bitcoin/taproot/control_block.rb +1 -0
- 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/tx.rb +8 -6
- data/lib/bitcoin/tx_out.rb +2 -2
- data/lib/bitcoin/util.rb +4 -4
- data/lib/bitcoin/version.rb +1 -1
- data/lib/bitcoin/wallet/account.rb +2 -2
- data/lib/bitcoin/wallet/db.rb +3 -3
- data/lib/bitcoin/wallet/master_key.rb +83 -24
- data/lib/bitcoin/wallet/utxo.rb +2 -2
- data/lib/bitcoin.rb +1 -0
- metadata +5 -5
|
@@ -68,7 +68,7 @@ module Bitcoin
|
|
|
68
68
|
hash_prevouts = Bitcoin.double_sha256(tx.inputs.map{|i|i.out_point.to_payload}.join)
|
|
69
69
|
hash_sequence = Bitcoin.double_sha256(tx.inputs.map{|i|[i.sequence].pack('V')}.join)
|
|
70
70
|
outpoint = tx.inputs[input_index].out_point.to_payload
|
|
71
|
-
amount = [amount].pack('Q')
|
|
71
|
+
amount = [amount].pack('Q<')
|
|
72
72
|
nsequence = [tx.inputs[input_index].sequence].pack('V')
|
|
73
73
|
hash_outputs = Bitcoin.double_sha256(tx.outputs.map{|o|o.to_payload}.join)
|
|
74
74
|
if output_script.p2wsh?
|
|
@@ -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
|
|
@@ -120,7 +126,7 @@ module Bitcoin
|
|
|
120
126
|
buf << [hash_type, tx.version, tx.lock_time].pack('CVV')
|
|
121
127
|
unless input_type == SIGHASH_TYPE[:anyonecanpay]
|
|
122
128
|
buf << Bitcoin.sha256(tx.in.map{|i|i.out_point.to_payload}.join) # sha_prevouts
|
|
123
|
-
buf << Bitcoin.sha256(opts[:prevouts].map(&:value).pack('Q
|
|
129
|
+
buf << Bitcoin.sha256(opts[:prevouts].map(&:value).pack('Q<*'))# sha_amounts
|
|
124
130
|
buf << Bitcoin.sha256(opts[:prevouts].map{|o|o.script_pubkey.to_payload(true)}.join) # sha_scriptpubkeys
|
|
125
131
|
buf << Bitcoin.sha256(tx.in.map(&:sequence).pack('V*')) # sha_sequences
|
|
126
132
|
end
|
|
@@ -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.
|
|
@@ -24,6 +24,7 @@ module Bitcoin
|
|
|
24
24
|
# @return [Bitcoin::Taproot::ControlBlock]
|
|
25
25
|
def self.parse_from_payload(payload)
|
|
26
26
|
raise Bitcoin::Taproot::Error, 'Invalid data length for Control Block' if payload.bytesize > TAPROOT_CONTROL_MAX_SIZE
|
|
27
|
+
raise Bitcoin::Taproot::Error, 'Invalid data length for Control Block' if payload.bytesize < TAPROOT_CONTROL_BASE_SIZE
|
|
27
28
|
raise Bitcoin::Taproot::Error, 'Invalid data length for path in Control Block' unless (payload.bytesize - TAPROOT_CONTROL_BASE_SIZE) % TAPROOT_CONTROL_NODE_SIZE == 0
|
|
28
29
|
control, internal_key, paths = payload.unpack('Ca32a*')
|
|
29
30
|
parity = control & 1
|
|
@@ -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/tx.rb
CHANGED
|
@@ -89,6 +89,10 @@ module Bitcoin
|
|
|
89
89
|
to_hex.to_i(16)
|
|
90
90
|
end
|
|
91
91
|
|
|
92
|
+
def eql?(other)
|
|
93
|
+
other.is_a?(Tx) && self == other
|
|
94
|
+
end
|
|
95
|
+
|
|
92
96
|
def tx_hash
|
|
93
97
|
Bitcoin.double_sha256(serialize_old_format).bth
|
|
94
98
|
end
|
|
@@ -228,7 +232,7 @@ module Bitcoin
|
|
|
228
232
|
# @param [Integer] input_index
|
|
229
233
|
# @param [Bitcoin::Script] script_pubkey the script pubkey for target input.
|
|
230
234
|
# @param [Integer] amount the amount of bitcoin, require for witness program only.
|
|
231
|
-
# @param [
|
|
235
|
+
# @param [Integer] flags the flags used when execute script interpreter.
|
|
232
236
|
# @param [Array[Bitcoin::TxOut]] prevouts Previous outputs referenced by all Tx inputs, required for taproot.
|
|
233
237
|
# @return [Boolean] result
|
|
234
238
|
def verify_input_sig(input_index, script_pubkey, amount: nil, flags: STANDARD_SCRIPT_VERIFY_FLAGS, prevouts: [])
|
|
@@ -236,11 +240,9 @@ module Bitcoin
|
|
|
236
240
|
has_witness = inputs[input_index].has_witness?
|
|
237
241
|
has_witness = true if script_pubkey.witness_program?
|
|
238
242
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
script_pubkey = redeem_script if redeem_script.p2wpkh?
|
|
243
|
-
end
|
|
243
|
+
# The script interpreter resolves the redeem script of P2SH, including a nested witness
|
|
244
|
+
# program, from the scriptSig, so the scriptPubkey is passed through as it is.
|
|
245
|
+
flags |= SCRIPT_VERIFY_P2SH if script_pubkey.p2sh?
|
|
244
246
|
|
|
245
247
|
if has_witness
|
|
246
248
|
verify_input_sig_for_witness(input_index, script_pubkey, amount, flags, prevouts)
|
data/lib/bitcoin/tx_out.rb
CHANGED
|
@@ -16,13 +16,13 @@ module Bitcoin
|
|
|
16
16
|
|
|
17
17
|
def self.parse_from_payload(payload)
|
|
18
18
|
buf = payload.is_a?(String) ? StringIO.new(payload) : payload
|
|
19
|
-
value = buf.read(8).unpack1('
|
|
19
|
+
value = buf.read(8).unpack1('Q<')
|
|
20
20
|
script_size = Bitcoin.unpack_var_int_from_io(buf)
|
|
21
21
|
new(value: value, script_pubkey: Script.parse_from_payload(buf.read(script_size)))
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
def to_payload
|
|
25
|
-
[value].pack('Q') << script_pubkey.to_payload(true)
|
|
25
|
+
[value].pack('Q<') << script_pubkey.to_payload(true)
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
def to_empty_payload
|
data/lib/bitcoin/util.rb
CHANGED
|
@@ -25,7 +25,7 @@ module Bitcoin
|
|
|
25
25
|
elsif i <= 0xffffffff
|
|
26
26
|
[0xfe, i].pack('CV')
|
|
27
27
|
elsif i <= 0xffffffffffffffff
|
|
28
|
-
[0xff, i].pack('CQ')
|
|
28
|
+
[0xff, i].pack('CQ<')
|
|
29
29
|
else
|
|
30
30
|
raise "int(#{i}) too large!"
|
|
31
31
|
end
|
|
@@ -39,7 +39,7 @@ module Bitcoin
|
|
|
39
39
|
when 0xfe
|
|
40
40
|
payload.unpack('xVa*')
|
|
41
41
|
when 0xff
|
|
42
|
-
payload.unpack('
|
|
42
|
+
payload.unpack('xQ<a*')
|
|
43
43
|
else
|
|
44
44
|
payload.unpack('Ca*')
|
|
45
45
|
end
|
|
@@ -54,7 +54,7 @@ module Bitcoin
|
|
|
54
54
|
when 0xfe
|
|
55
55
|
buf.read(4)&.unpack1('V')
|
|
56
56
|
when 0xff
|
|
57
|
-
buf.read(8)&.unpack1('Q')
|
|
57
|
+
buf.read(8)&.unpack1('Q<')
|
|
58
58
|
else
|
|
59
59
|
uchar
|
|
60
60
|
end
|
|
@@ -152,7 +152,7 @@ module Bitcoin
|
|
|
152
152
|
begin
|
|
153
153
|
Bitcoin::Script.parse_from_addr(addr)
|
|
154
154
|
true
|
|
155
|
-
rescue
|
|
155
|
+
rescue StandardError
|
|
156
156
|
false
|
|
157
157
|
end
|
|
158
158
|
end
|
data/lib/bitcoin/version.rb
CHANGED
|
@@ -33,7 +33,7 @@ module Bitcoin
|
|
|
33
33
|
payload = buf.read
|
|
34
34
|
name, payload = Bitcoin.unpack_var_string(payload)
|
|
35
35
|
name = name.force_encoding('utf-8')
|
|
36
|
-
purpose, index, receive_depth, change_depth, lookahead = payload.unpack('
|
|
36
|
+
purpose, index, receive_depth, change_depth, lookahead = payload.unpack('V*')
|
|
37
37
|
a = Account.new(account_key, purpose, index, name)
|
|
38
38
|
a.receive_depth = receive_depth
|
|
39
39
|
a.change_depth = change_depth
|
|
@@ -44,7 +44,7 @@ module Bitcoin
|
|
|
44
44
|
def to_payload
|
|
45
45
|
payload = account_key.to_payload
|
|
46
46
|
payload << Bitcoin.pack_var_string(name.unpack1('H*').htb)
|
|
47
|
-
payload << [purpose, index, receive_depth, change_depth, lookahead].pack('
|
|
47
|
+
payload << [purpose, index, receive_depth, change_depth, lookahead].pack('V*')
|
|
48
48
|
payload
|
|
49
49
|
end
|
|
50
50
|
|
data/lib/bitcoin/wallet/db.rb
CHANGED
|
@@ -32,7 +32,7 @@ module Bitcoin
|
|
|
32
32
|
|
|
33
33
|
def save_account(account)
|
|
34
34
|
level_db.batch do
|
|
35
|
-
id = [account.purpose, account.index].pack('
|
|
35
|
+
id = [account.purpose, account.index].pack('V*').bth
|
|
36
36
|
key = KEY_PREFIX[:account] + id
|
|
37
37
|
level_db.put(key, account.to_payload)
|
|
38
38
|
end
|
|
@@ -40,14 +40,14 @@ module Bitcoin
|
|
|
40
40
|
|
|
41
41
|
def save_key(account, purpose, index, key)
|
|
42
42
|
pubkey = key.pub
|
|
43
|
-
id = [account.purpose, account.index, purpose, index].pack('
|
|
43
|
+
id = [account.purpose, account.index, purpose, index].pack('V*').bth
|
|
44
44
|
k = KEY_PREFIX[:key] + id
|
|
45
45
|
level_db.put(k, pubkey)
|
|
46
46
|
key
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
def get_keys(account)
|
|
50
|
-
id = [account.purpose, account.index].pack('
|
|
50
|
+
id = [account.purpose, account.index].pack('V*').bth
|
|
51
51
|
from = KEY_PREFIX[:key] + id + '00000000'
|
|
52
52
|
to = KEY_PREFIX[:key] + id + 'ffffffff'
|
|
53
53
|
level_db.each(from: from, to: to).map { |k, v| v}
|
|
@@ -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
|
|
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)
|
|
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
|
data/lib/bitcoin/wallet/utxo.rb
CHANGED
|
@@ -18,7 +18,7 @@ module Bitcoin
|
|
|
18
18
|
def self.parse_from_payload(payload)
|
|
19
19
|
return nil if payload.nil?
|
|
20
20
|
|
|
21
|
-
tx_hash, index, block_height, value, payload = payload.unpack('
|
|
21
|
+
tx_hash, index, block_height, value, payload = payload.unpack('H64VVQ<a*')
|
|
22
22
|
|
|
23
23
|
buf = StringIO.new(payload)
|
|
24
24
|
script_size = Bitcoin.unpack_var_int_from_io(buf)
|
|
@@ -27,7 +27,7 @@ module Bitcoin
|
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
def to_payload
|
|
30
|
-
payload = [tx_hash, index, block_height.nil? ? 0 : block_height, value].pack('H64VVQ')
|
|
30
|
+
payload = [tx_hash, index, block_height.nil? ? 0 : block_height, value].pack('H64VVQ<')
|
|
31
31
|
s = script_pubkey.to_payload
|
|
32
32
|
payload << Bitcoin.pack_var_int(s.length) << s
|
|
33
33
|
payload
|