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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/bitcoinrb.gemspec +2 -2
  3. data/lib/bitcoin/base58.rb +15 -3
  4. data/lib/bitcoin/bip321_uri.rb +3 -6
  5. data/lib/bitcoin/bip324/fs_chacha20.rb +1 -1
  6. data/lib/bitcoin/bip324/fs_chacha_poly1305.rb +1 -0
  7. data/lib/bitcoin/bip324.rb +6 -1
  8. data/lib/bitcoin/block.rb +3 -3
  9. data/lib/bitcoin/descriptor/checksum.rb +6 -2
  10. data/lib/bitcoin/ext_key.rb +8 -4
  11. data/lib/bitcoin/key.rb +2 -2
  12. data/lib/bitcoin/message/base.rb +7 -1
  13. data/lib/bitcoin/message/cf_parser.rb +2 -2
  14. data/lib/bitcoin/message/fee_filter.rb +2 -2
  15. data/lib/bitcoin/message/filter_load.rb +10 -0
  16. data/lib/bitcoin/message/header_and_short_ids.rb +4 -4
  17. data/lib/bitcoin/message/inventory.rb +1 -1
  18. data/lib/bitcoin/message/merkle_block.rb +6 -0
  19. data/lib/bitcoin/message/network_addr.rb +7 -3
  20. data/lib/bitcoin/message/ping.rb +2 -2
  21. data/lib/bitcoin/message/pong.rb +2 -2
  22. data/lib/bitcoin/message/send_cmpct.rb +2 -2
  23. data/lib/bitcoin/message/version.rb +3 -3
  24. data/lib/bitcoin/message.rb +15 -2
  25. data/lib/bitcoin/mnemonic.rb +9 -4
  26. data/lib/bitcoin/partial_tree.rb +25 -0
  27. data/lib/bitcoin/psbt/input.rb +58 -33
  28. data/lib/bitcoin/psbt/key_origin_info.rb +2 -2
  29. data/lib/bitcoin/psbt/output.rb +9 -5
  30. data/lib/bitcoin/psbt/tx.rb +22 -15
  31. data/lib/bitcoin/psbt.rb +1 -1
  32. data/lib/bitcoin/rpc/bitcoin_core_client.rb +3 -2
  33. data/lib/bitcoin/script/script.rb +15 -5
  34. data/lib/bitcoin/script/script_interpreter.rb +1 -1
  35. data/lib/bitcoin/script/tx_checker.rb +7 -2
  36. data/lib/bitcoin/secp256k1/native.rb +50 -1
  37. data/lib/bitcoin/secp256k1/rfc6979.rb +6 -3
  38. data/lib/bitcoin/secp256k1/ruby.rb +115 -2
  39. data/lib/bitcoin/sighash_generator.rb +8 -2
  40. data/lib/bitcoin/silent_payment.rb +56 -90
  41. data/lib/bitcoin/taproot/control_block.rb +1 -0
  42. data/lib/bitcoin/taproot/custom_depth_builder.rb +2 -2
  43. data/lib/bitcoin/taproot/simple_builder.rb +9 -7
  44. data/lib/bitcoin/taproot.rb +6 -1
  45. data/lib/bitcoin/tx.rb +8 -6
  46. data/lib/bitcoin/tx_out.rb +2 -2
  47. data/lib/bitcoin/util.rb +4 -4
  48. data/lib/bitcoin/version.rb +1 -1
  49. data/lib/bitcoin/wallet/account.rb +2 -2
  50. data/lib/bitcoin/wallet/db.rb +3 -3
  51. data/lib/bitcoin/wallet/master_key.rb +83 -24
  52. data/lib/bitcoin/wallet/utxo.rb +2 -2
  53. data/lib/bitcoin.rb +1 -0
  54. 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*'))# sha_amounts
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
- private_key = if public_key.p2tr? && key.to_point.y.odd?
37
- field.mod(-priv_key_int)
38
- else
39
- priv_key_int
40
- end
41
- input_pub_keys << public_key
42
- sum_priv_keys = field.mod(sum_priv_keys + private_key)
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
- agg_pubkey = (Bitcoin::Secp256k1::GROUP.generator.to_jacobian * sum_priv_keys).to_affine
45
- return [] if agg_pubkey.infinity?
46
-
47
- outpoint_l = inputs.map{|i|i.out_point.to_hex}.sort.first
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
- destinations[sp_addr.scan_key] = [] unless destinations.has_key?(sp_addr.scan_key)
55
- destinations[sp_addr.scan_key] << sp_addr.spend_key
50
+ [sp_addr.scan_key, sp_addr.spend_key]
56
51
  end
57
-
58
- # Check K_max limit: fail if any group exceeds the limit
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
- outputs = []
64
- destinations.each do |scan_key, spends|
65
- scan_key = Bitcoin::Key.new(pubkey: scan_key).to_point.to_jacobian
66
- ecdh_shared_secret = (scan_key * field.mod(input_hash.to_i(16) * sum_priv_keys)).to_affine.to_hex.htb
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
- has_taproot = !outputs.find{|o| o.script_pubkey.p2tr? }.nil?
91
- return [] unless has_taproot
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
- field = ECDSA::PrimeField.new(Bitcoin::Secp256k1::GROUP.order)
104
- outpoint_l = inputs.map{|i|i.out_point.to_hex}.sort.first
105
- input_hash = Bitcoin.tagged_hash("BIP0352/Inputs", outpoint_l.htb + sum_pub_keys.to_affine.to_hex.htb).bth
106
- ecdh_shared_secret = (sum_pub_keys * field.mod(input_hash.to_i(16) * scan_private_key.priv_key.to_i(16))).to_affine.to_hex.htb
107
-
108
- # Pre-compute label tweak points with their label values and scalar tweaks
109
- label_tweaks = labels.map do |m|
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
- k = 0
116
- results = []
117
- found_outputs = Set.new
118
- loop do
119
- # Stop scanning if K_max limit is reached
120
- break if k == K_MAX
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
- results
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
- leaves = []
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
- if leaves.empty? || leaves.length == 1
106
- leaves << pair.map(&:leaf_hash)
107
- elsif leaves.length == 2
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
- Merkle::CustomTree.new(config: Merkle::Config.taptree, leaves: leaves)
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
@@ -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
- Bitcoin::Key.from_point(key.to_point + internal_key.to_point)
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 [Array] flags the flags used when execute script interpreter.
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
- if script_pubkey.p2sh?
240
- flags << SCRIPT_VERIFY_P2SH
241
- redeem_script = Script.parse_from_payload(script_sig.chunks.last)
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)
@@ -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('q')
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('xQa*')
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 Exception
155
+ rescue StandardError
156
156
  false
157
157
  end
158
158
  end
@@ -1,3 +1,3 @@
1
1
  module Bitcoin
2
- VERSION = "1.12.1"
2
+ VERSION = "1.14.0"
3
3
  end
@@ -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('I*')
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('I*')
47
+ payload << [purpose, index, receive_depth, change_depth, lookahead].pack('V*')
48
48
  payload
49
49
  end
50
50
 
@@ -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('I*').bth
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('I*').bth
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('I*').bth
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 [0, 1].include?(flag)
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, encrypted: flag == 1)
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(false:0, true:1)][salt(var str)][seed(var str)]
75
+ # [encryption version(not encrypted:0, v1:1, v2:2)][salt(var str)][seed(var str)]
54
76
  def to_payload
55
- flg = encrypted ? 1 : 0
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
- # encrypt seed
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
- @salt = SecureRandom.hex(16)
78
- enc = OpenSSL::Cipher.new('AES-256-CBC')
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, enc.iv = key_iv(enc, passphrase)
81
- encrypted_data = ''
82
- encrypted_data << enc.update(seed)
83
- encrypted_data << enc.final
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
- # decrypt seed
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
- dec = OpenSSL::Cipher.new('AES-256-CBC')
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
- def key_iv(enc, passphrase)
105
- key_iv = OpenSSL::PKCS5.pbkdf2_hmac_sha1(passphrase, salt, 2000, enc.key_len + enc.iv_len)
106
- [key_iv[0, enc.key_len], key_iv[enc.key_len, enc.iv_len]]
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
@@ -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('H64VVQa*')
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
data/lib/bitcoin.rb CHANGED
@@ -4,6 +4,7 @@
4
4
  require 'bitcoin/version'
5
5
  require 'schnorr'
6
6
  require 'securerandom'
7
+ require 'stringio'
7
8
  require 'json'
8
9
  require 'bech32'
9
10
  require 'base64'