bitcoinrb 1.14.0 → 1.16.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 (101) hide show
  1. checksums.yaml +4 -4
  2. data/bitcoinrb.gemspec +5 -4
  3. data/lib/bitcoin/bip321_uri.rb +95 -32
  4. data/lib/bitcoin/bip324/ell_swift_pubkey.rb +3 -1
  5. data/lib/bitcoin/bip324/fs_chacha20.rb +3 -0
  6. data/lib/bitcoin/bip324.rb +1 -1
  7. data/lib/bitcoin/bip85_entropy.rb +79 -29
  8. data/lib/bitcoin/bit_stream.rb +2 -2
  9. data/lib/bitcoin/block.rb +46 -7
  10. data/lib/bitcoin/block_filter.rb +3 -2
  11. data/lib/bitcoin/block_header.rb +31 -8
  12. data/lib/bitcoin/bloom_filter.rb +5 -2
  13. data/lib/bitcoin/chainparams/regtest.yml +0 -1
  14. data/lib/bitcoin/chainparams/signet.yml +1 -1
  15. data/lib/bitcoin/constants.rb +13 -3
  16. data/lib/bitcoin/descriptor/combo.rb +13 -1
  17. data/lib/bitcoin/descriptor/expression.rb +35 -15
  18. data/lib/bitcoin/descriptor/multi.rb +9 -1
  19. data/lib/bitcoin/descriptor/multi_a.rb +4 -0
  20. data/lib/bitcoin/descriptor/musig.rb +1 -0
  21. data/lib/bitcoin/descriptor/pk.rb +7 -0
  22. data/lib/bitcoin/descriptor/pkh.rb +5 -0
  23. data/lib/bitcoin/descriptor/raw.rb +1 -1
  24. data/lib/bitcoin/descriptor/sh.rb +1 -0
  25. data/lib/bitcoin/descriptor/sp.rb +2 -2
  26. data/lib/bitcoin/descriptor/tr.rb +29 -3
  27. data/lib/bitcoin/descriptor/wpkh.rb +1 -0
  28. data/lib/bitcoin/descriptor/wsh.rb +1 -0
  29. data/lib/bitcoin/descriptor.rb +59 -35
  30. data/lib/bitcoin/ext/array_ext.rb +3 -0
  31. data/lib/bitcoin/ext/ecdsa.rb +7 -1
  32. data/lib/bitcoin/ext_key.rb +10 -0
  33. data/lib/bitcoin/gcs_filter.rb +21 -6
  34. data/lib/bitcoin/key.rb +76 -17
  35. data/lib/bitcoin/key_path.rb +8 -5
  36. data/lib/bitcoin/message/addr.rb +1 -0
  37. data/lib/bitcoin/message/addr_v2.rb +1 -0
  38. data/lib/bitcoin/message/base.rb +8 -1
  39. data/lib/bitcoin/message/block.rb +4 -8
  40. data/lib/bitcoin/message/block_transaction_request.rb +11 -8
  41. data/lib/bitcoin/message/block_transactions.rb +2 -1
  42. data/lib/bitcoin/message/cf_parser.rb +1 -0
  43. data/lib/bitcoin/message/cfcheckpt.rb +3 -3
  44. data/lib/bitcoin/message/cfheaders.rb +5 -4
  45. data/lib/bitcoin/message/cfilter.rb +3 -3
  46. data/lib/bitcoin/message/error.rb +3 -1
  47. data/lib/bitcoin/message/fee_filter.rb +1 -0
  48. data/lib/bitcoin/message/filter_add.rb +3 -1
  49. data/lib/bitcoin/message/filter_load.rb +4 -4
  50. data/lib/bitcoin/message/get_cfcheckpt.rb +1 -0
  51. data/lib/bitcoin/message/header_and_short_ids.rb +26 -11
  52. data/lib/bitcoin/message/headers.rb +10 -2
  53. data/lib/bitcoin/message/headers_parser.rb +4 -2
  54. data/lib/bitcoin/message/inventories_parser.rb +2 -1
  55. data/lib/bitcoin/message/inventory.rb +15 -6
  56. data/lib/bitcoin/message/merkle_block.rb +4 -4
  57. data/lib/bitcoin/message/network_addr.rb +13 -13
  58. data/lib/bitcoin/message/ping.rb +1 -0
  59. data/lib/bitcoin/message/pong.rb +1 -0
  60. data/lib/bitcoin/message/reject.rb +1 -0
  61. data/lib/bitcoin/message/send_cmpct.rb +2 -2
  62. data/lib/bitcoin/message/send_tx_rcncl.rb +1 -1
  63. data/lib/bitcoin/message/version.rb +23 -8
  64. data/lib/bitcoin/message.rb +15 -0
  65. data/lib/bitcoin/message_sign.rb +67 -15
  66. data/lib/bitcoin/mnemonic.rb +8 -0
  67. data/lib/bitcoin/out_point.rb +6 -2
  68. data/lib/bitcoin/partial_tree.rb +41 -28
  69. data/lib/bitcoin/payment_code.rb +26 -8
  70. data/lib/bitcoin/psbt/hd_key_path.rb +1 -2
  71. data/lib/bitcoin/psbt/input.rb +199 -48
  72. data/lib/bitcoin/psbt/key_origin_info.rb +2 -0
  73. data/lib/bitcoin/psbt/output.rb +9 -6
  74. data/lib/bitcoin/psbt/proprietary.rb +6 -3
  75. data/lib/bitcoin/psbt/tx.rb +9 -4
  76. data/lib/bitcoin/psbt.rb +46 -0
  77. data/lib/bitcoin/rpc/bitcoin_core_client.rb +54 -13
  78. data/lib/bitcoin/rpc/error.rb +43 -0
  79. data/lib/bitcoin/rpc.rb +1 -0
  80. data/lib/bitcoin/script/script.rb +114 -33
  81. data/lib/bitcoin/script/script_interpreter.rb +11 -6
  82. data/lib/bitcoin/script/tx_checker.rb +7 -2
  83. data/lib/bitcoin/script_witness.rb +5 -1
  84. data/lib/bitcoin/secp256k1/ruby.rb +10 -4
  85. data/lib/bitcoin/silent_payment.rb +6 -3
  86. data/lib/bitcoin/slip39/share.rb +42 -12
  87. data/lib/bitcoin/slip39/sss.rb +33 -12
  88. data/lib/bitcoin/slip39.rb +13 -5
  89. data/lib/bitcoin/taproot/control_block.rb +6 -1
  90. data/lib/bitcoin/taproot/custom_depth_builder.rb +12 -0
  91. data/lib/bitcoin/tx.rb +35 -14
  92. data/lib/bitcoin/tx_in.rb +2 -2
  93. data/lib/bitcoin/tx_out.rb +2 -2
  94. data/lib/bitcoin/util.rb +49 -28
  95. data/lib/bitcoin/validation.rb +64 -12
  96. data/lib/bitcoin/version.rb +1 -1
  97. data/lib/bitcoin/wallet/account.rb +35 -11
  98. data/lib/bitcoin/wallet/base.rb +40 -15
  99. data/lib/bitcoin/wallet/utxo.rb +22 -4
  100. data/lib/bitcoin.rb +16 -4
  101. metadata +24 -9
@@ -92,15 +92,26 @@ module Bitcoin
92
92
  new << WITNESS_VERSION_V0 << redeem_script.to_sha256
93
93
  end
94
94
 
95
+ # The range of a decimal number in a script string, as ParseScript in Bitcoin Core: a number
96
+ # outside of it is not valid in a script.
97
+ MAX_SCRIPT_STRING_NUMBER = 0xffffffff
98
+
95
99
  # generate script from string.
100
+ # A number is pushed as a script number, as ParseScript in Bitcoin Core does: 0, -1 and 1 to 16
101
+ # become OP_0, OP_1NEGATE and OP_1 to OP_16, and any other number a push of its minimal encoding.
102
+ # @param [String] string script string such as the result of #to_s.
103
+ # @return [Bitcoin::Script]
104
+ # @raise [ArgumentError] If a number is out of -0xffffffff..0xffffffff or a token is not valid.
96
105
  def self.from_string(string)
97
106
  script = new
98
107
  string.split(' ').each do |v|
99
108
  opcode = Opcodes.name_to_opcode(v)
100
109
  if opcode
101
110
  script << (v =~ /^\d/ && Opcodes.small_int_to_opcode(v.ord) ? v.ord : opcode)
111
+ elsif number_token?(v)
112
+ script.send(:push_script_number, v.to_i)
102
113
  else
103
- script << (number_token?(v) ? v.to_i : v)
114
+ script << v
104
115
  end
105
116
  end
106
117
  script
@@ -112,7 +123,8 @@ module Bitcoin
112
123
  # @param [String] token a token of the script string.
113
124
  # @return [Boolean] whether +token+ should be interpreted as a script number.
114
125
  def self.number_token?(token)
115
- return false unless token =~ /^[0-9]+$/
126
+ return true if token =~ /\A-[0-9]+\z/ # A negative number cannot be pushed data.
127
+ return false unless token =~ /\A[0-9]+\z/
116
128
  token.length < 12 || token.length.odd?
117
129
  end
118
130
 
@@ -153,31 +165,31 @@ module Bitcoin
153
165
  s.chunks << opcode
154
166
  return s
155
167
  end
156
- len = case pushcode
157
- when OP_PUSHDATA1
158
- packed_size = buf.read(1)
159
- packed_size.unpack1('C')
160
- when OP_PUSHDATA2
161
- packed_size = buf.read(2)
162
- packed_size.unpack1('v')
163
- when OP_PUSHDATA4
164
- packed_size = buf.read(4)
165
- packed_size.unpack1('V')
166
- else
167
- pushcode < OP_PUSHDATA1 ? pushcode : 0
168
- end
169
- if buf.eof?
170
- s.chunks << [len].pack('C')
168
+ size_length, format = case pushcode
169
+ when OP_PUSHDATA1 then [1, 'C']
170
+ when OP_PUSHDATA2 then [2, 'v']
171
+ when OP_PUSHDATA4 then [4, 'V']
172
+ else [0, nil]
173
+ end
174
+ if size_length > 0
175
+ packed_size = buf.read(size_length)
176
+ # A length field cut short. Keep the bytes as they are so that the payload round trips,
177
+ # and leave it to the interpreter to reject the push.
178
+ if packed_size.bytesize < size_length
179
+ s.chunks << (opcode + packed_size)
180
+ return s
181
+ end
182
+ len = packed_size.unpack1(format)
171
183
  else
172
- chunk = (packed_size ? (opcode + packed_size) : (opcode)) + buf.read(len)
173
- s.chunks << chunk
184
+ len = pushcode
174
185
  end
186
+ # StringIO#read returns nil at EOF for a positive length, which is a push whose data is missing entirely.
187
+ s.chunks << (opcode + (packed_size || '') + buf.read(len).to_s)
175
188
  else
176
- if Opcodes.defined?(opcode.ord)
177
- s << opcode.ord
178
- else
179
- s.chunks << (opcode + buf.read) # If opcode is invalid, put all remaining data in last chunk.
180
- end
189
+ # Every non-push byte is an opcode of its own, defined or not, like GetOp in Bitcoin Core.
190
+ # An undefined opcode only fails the script when it is executed, so the bytes after it
191
+ # still have to be parsed for OP_ELSE and OP_ENDIF in an unexecuted branch to be found.
192
+ s.chunks << opcode
181
193
  end
182
194
  end
183
195
  s
@@ -215,8 +227,23 @@ module Bitcoin
215
227
  end
216
228
 
217
229
  # check whether standard script.
230
+ # Whether this script is a standard scriptPubKey, as IsStandard in Bitcoin Core judges it.
231
+ # An OP_RETURN script is standard up to MAX_OP_RETURN_RELAY bytes, which a tx shares among all of its
232
+ # OP_RETURN outputs. A bare multisig has at most 3 public keys.
233
+ # @return [Boolean]
218
234
  def standard?
219
- p2pkh? | p2sh? | p2wpkh? | p2wsh? | p2tr? | multisig? | standard_op_return? || p2a?
235
+ p2pkh? || p2sh? || p2wpkh? || p2wsh? || p2tr? || p2a? || witness_unknown? ||
236
+ standard_p2pk? || standard_multisig? || standard_op_return?
237
+ end
238
+
239
+ # Whether this script is a witness program of a version which is not defined yet, which Bitcoin Core relays
240
+ # so that a future soft fork can use it. A version 0 program is defined only for 20 and 32 bytes.
241
+ # @return [Boolean]
242
+ def witness_unknown?
243
+ return false unless witness_program?
244
+ version, program = witness_data
245
+ return false if version == 0
246
+ !(version == 1 && (program.bytesize == 32 || program == P2A_PROGRAM))
220
247
  end
221
248
 
222
249
  # Check whether this script is a P2PKH format script.
@@ -287,13 +314,46 @@ module Bitcoin
287
314
  sig_count <= pubkey_count
288
315
  end
289
316
 
317
+ # Whether a public key is of a valid size for its header byte, as CPubKey::ValidSize in Bitcoin Core checks it.
318
+ def self.valid_pubkey_size?(data)
319
+ return false if data.nil? || data.empty?
320
+ case data.getbyte(0)
321
+ when 0x02, 0x03 then data.bytesize == 33
322
+ when 0x04, 0x06, 0x07 then data.bytesize == 65
323
+ else false
324
+ end
325
+ end
326
+ private_class_method :valid_pubkey_size?
327
+
328
+ # Whether this script is a P2PK script which is standard.
329
+ # @return [Boolean]
330
+ def standard_p2pk?
331
+ chunks.size == 2 && chunks[0].pushdata? && chunks[1].ord == OP_CHECKSIG &&
332
+ Bitcoin::Script.send(:valid_pubkey_size?, chunks[0].pushed_data)
333
+ end
334
+
335
+ # Whether this script is a bare multisig script which is standard, m-of-n with 1 <= m <= n <= 3.
336
+ # @return [Boolean]
337
+ def standard_multisig?
338
+ return false if chunks.size < 4 || chunks.last.ord != OP_CHECKMULTISIG
339
+ m = chunks[0].opcode
340
+ n = chunks[-2].opcode
341
+ return false unless m && n && (OP_1..OP_16).cover?(m) && (OP_1..OP_16).cover?(n)
342
+ m = Opcodes.opcode_to_small_int(m)
343
+ n = Opcodes.opcode_to_small_int(n)
344
+ keys = chunks[1...-2]
345
+ return false unless keys.size == n && keys.all? { |k| k.pushdata? && Bitcoin::Script.send(:valid_pubkey_size?, k.pushed_data) }
346
+ n <= 3 && m <= n
347
+ end
348
+
290
349
  def op_return?
291
350
  chunks.size >= 1 && chunks[0].ord == OP_RETURN
292
351
  end
293
352
 
353
+ # Whether this script is an OP_RETURN script which is standard, OP_RETURN followed by pushes only.
354
+ # @return [Boolean]
294
355
  def standard_op_return?
295
- op_return? && size <= MAX_OP_RETURN_RELAY &&
296
- (chunks.size == 1 || chunks[1].opcode <= OP_16)
356
+ op_return? && size <= MAX_OP_RETURN_RELAY && chunks[1..-1].all? { |c| c.opcode.nil? || c.opcode <= OP_16 }
297
357
  end
298
358
 
299
359
  def op_return_data
@@ -334,11 +394,14 @@ module Bitcoin
334
394
  end
335
395
 
336
396
  # get witness commitment
397
+ # The witness commitment this script carries, if it is a commitment output as Bitcoin Core's
398
+ # GetWitnessCommitmentIndex recognizes it: at least 38 bytes which begin with OP_RETURN, a push of
399
+ # 0x24 bytes and the header aa21a9ed. Anything after the 32 byte commitment is ignored.
400
+ # @return [String, nil] The commitment with hex format, or nil if this is not a commitment output.
337
401
  def witness_commitment
338
- return nil if !op_return? || op_return_data.bytesize < 36
339
- buf = StringIO.new(op_return_data)
340
- return nil unless buf.read(4).bth == WITNESS_COMMITMENT_HEADER
341
- buf.read(32).bth
402
+ payload = to_payload
403
+ return nil if payload.bytesize < 38 || payload[0, 6] != "\x6a\x24".b + WITNESS_COMMITMENT_HEADER.htb
404
+ payload[6, 32].bth
342
405
  end
343
406
 
344
407
  # If this script is witness program, return its script code,
@@ -372,6 +435,24 @@ module Bitcoin
372
435
  end
373
436
  end
374
437
 
438
+ # Push +n+ as a script number. Unlike #push_int, a number which is also an opcode, e.g. 172
439
+ # (OP_CHECKSIG), is not appended as that opcode.
440
+ # @raise [ArgumentError] If +n+ is out of -0xffffffff..0xffffffff.
441
+ def push_script_number(n)
442
+ unless n.between?(-MAX_SCRIPT_STRING_NUMBER, MAX_SCRIPT_STRING_NUMBER)
443
+ raise ArgumentError, 'decimal numeric value only allowed in the range -0xFFFFFFFF...0xFFFFFFFF.'
444
+ end
445
+ if n == 0
446
+ chunks << Opcodes::OP_0.chr
447
+ elsif n == -1 || n.between?(1, 16)
448
+ chunks << Opcodes.small_int_to_opcode(n).chr
449
+ else
450
+ append_data(Script.encode_number(n))
451
+ end
452
+ self
453
+ end
454
+ private :push_script_number
455
+
375
456
  # push integer to stack.
376
457
  def push_int(n)
377
458
  begin
@@ -425,8 +506,8 @@ module Bitcoin
425
506
  if v
426
507
  v
427
508
  else
428
- data = c.pushed_data
429
- if data
509
+ if c.valid_pushdata_length?
510
+ data = c.pushed_data
430
511
  if data.bytesize <= 4
431
512
  Script.decode_number(data.bth) # for scriptnum
432
513
  else
@@ -67,7 +67,7 @@ module Bitcoin
67
67
  raise 'stack cannot be empty.' if stack.empty?
68
68
  redeem_script = Bitcoin::Script.parse_from_payload(stack.pop.htb)
69
69
  return false unless eval_script(redeem_script, :base)
70
- return set_error(SCRIPT_ERR_EVAL_FALSE) if stack.empty? || !cast_to_bool(stack.last)
70
+ return set_error(SCRIPT_ERR_EVAL_FALSE) if stack.empty? || !cast_to_bool(stack.last.htb)
71
71
 
72
72
  # P2SH witness program
73
73
  if flag?(SCRIPT_VERIFY_WITNESS) && redeem_script.witness_program?
@@ -77,6 +77,8 @@ module Bitcoin
77
77
 
78
78
  version, program = redeem_script.witness_data
79
79
  return false unless verify_witness_program(witness, version, program, true)
80
+ # Bypass the cleanstack check at the end. The actual stack is obviously not clean for witness programs.
81
+ stack.resize!(1, Script.encode_number(0))
80
82
  end
81
83
  end
82
84
 
@@ -182,7 +184,7 @@ module Bitcoin
182
184
  return false unless eval_script(script_pubkey, sig_version, opts: opts)
183
185
 
184
186
  return set_error(SCRIPT_ERR_CLEANSTACK) unless stack.size == 1
185
- return set_error(SCRIPT_ERR_EVAL_FALSE) unless cast_to_bool(stack.last)
187
+ return set_error(SCRIPT_ERR_EVAL_FALSE) unless cast_to_bool(stack.last.htb)
186
188
  true
187
189
  end
188
190
 
@@ -324,7 +326,7 @@ module Bitcoin
324
326
  3.times { stack << stack[-3] }
325
327
  when OP_IFDUP
326
328
  return set_error(SCRIPT_ERR_INVALID_STACK_OPERATION) if stack.size < 1
327
- stack << stack.last if cast_to_bool(stack.last)
329
+ stack << stack.last if cast_to_bool(stack.last.htb)
328
330
  when OP_RIPEMD160
329
331
  return set_error(SCRIPT_ERR_INVALID_STACK_OPERATION) if stack.size < 1
330
332
  stack << Digest::RMD160.hexdigest(pop_string.htb)
@@ -483,6 +485,7 @@ module Bitcoin
483
485
 
484
486
  push_int(success ? num + 1 : num)
485
487
  when OP_CHECKMULTISIG, OP_CHECKMULTISIGVERIFY
488
+ return set_error(SCRIPT_ERR_TAPSCRIPT_CHECKMULTISIG) if sig_version == :tapscript # BIP-342
486
489
  return set_error(SCRIPT_ERR_INVALID_STACK_OPERATION) if stack.size < 1
487
490
  pubkey_count = pop_int
488
491
  unless (0..MAX_PUBKEYS_PER_MULTISIG).include?(pubkey_count)
@@ -505,7 +508,8 @@ module Bitcoin
505
508
 
506
509
  sigs = pop_string(sig_count)
507
510
  sigs = [sigs] if sigs.is_a?(String)
508
-
511
+ all_sigs = sigs.dup # the loop below drops a signature once it verifies
512
+
509
513
  subscript = script.subscript(opts[:begincodehash]..-1)
510
514
 
511
515
  if sig_version == :base
@@ -532,8 +536,9 @@ module Bitcoin
532
536
  end
533
537
 
534
538
  if !success && flag?(SCRIPT_VERIFY_NULLFAIL)
535
- sigs.each do |sig|
536
- # If the operation failed, we require that all signatures must be empty vector
539
+ # If the operation failed, we require that all signatures must be empty vector, including
540
+ # the ones which did verify, as Bitcoin Core checks every signature element on the stack.
541
+ all_sigs.each do |sig|
537
542
  return set_error(SCRIPT_ERR_SIG_NULLFAIL) if sig.bytesize > 0
538
543
  end
539
544
  end
@@ -63,11 +63,16 @@ module Bitcoin
63
63
 
64
64
  begin
65
65
  sighash = tx.sighash_for_input(input_index, opts: opts, hash_type: hash_type, sig_version: sig_version, prevouts: prevouts)
66
- key = Key.from_xonly_pubkey(pubkey)
67
- key.verify(sig, sighash, algo: :schnorr)
68
66
  rescue ArgumentError
69
67
  return set_error(SCRIPT_ERR_SCHNORR_SIG_HASHTYPE)
70
68
  end
69
+ begin
70
+ key = Key.from_xonly_pubkey(pubkey)
71
+ rescue ArgumentError
72
+ # A public key which is not on the curve makes the signature fail, as XOnlyPubKey::VerifySchnorr in Bitcoin Core does.
73
+ return set_error(SCRIPT_ERR_SCHNORR_SIG)
74
+ end
75
+ key.verify(sig, sighash, algo: :schnorr)
71
76
  end
72
77
 
73
78
  def check_locktime(locktime)
@@ -14,11 +14,15 @@ module Bitcoin
14
14
  return self.new if buf.eof?
15
15
  size = Bitcoin.unpack_var_int_from_io(buf)
16
16
  stack = size.times.map do
17
- buf.read(Bitcoin.unpack_var_int_from_io(buf))
17
+ Bitcoin.read_exact(buf, Bitcoin.unpack_var_int_from_io(buf))
18
18
  end
19
19
  self.new(stack)
20
20
  end
21
21
 
22
+ def ==(other)
23
+ other.is_a?(ScriptWitness) && stack == other.stack
24
+ end
25
+
22
26
  def empty?
23
27
  stack.empty?
24
28
  end
@@ -102,14 +102,20 @@ module Bitcoin
102
102
  digest = ECDSA.normalize_digest(data, group.bit_length)
103
103
  field = ECDSA::PrimeField.new(group.order)
104
104
 
105
- unless rec & 2 == 0
106
- r = field.mod(r + group.order)
105
+ # Bit 1 of the recovery id says the x coordinate of R is r + n rather than r. It only fits in the
106
+ # field when r < p - n, and libsecp256k1 rejects the signature otherwise. r + n must not be
107
+ # reduced modulo n here, which would turn it back into r.
108
+ x = r
109
+ if rec & 2 != 0
110
+ return nil if r >= group.field.prime - group.order
111
+ x = r + group.order
107
112
  end
108
113
 
109
114
  is_odd = (rec & 1 == 1)
110
- y_coordinate = group.solve_for_y(r).find{|y| is_odd ? y.odd? : y.even?}
115
+ y_coordinate = group.solve_for_y(x).find{|y| is_odd ? y.odd? : y.even?}
116
+ return nil if y_coordinate.nil? # x is not on the curve
111
117
 
112
- p = group.new_point([r, y_coordinate])
118
+ p = group.new_point([x, y_coordinate])
113
119
 
114
120
  inv_r = field.inverse(r)
115
121
  u1 = field.mod(inv_r * digest)
@@ -134,16 +134,19 @@ module Bitcoin
134
134
  end
135
135
  end
136
136
  elsif prevout.p2sh?
137
- redeem_script = Bitcoin::Script.parse_from_payload(input.script_sig.chunks.last.pushed_data)
137
+ redeem_payload = input.script_sig.chunks.last&.pushed_data
138
+ return nil if redeem_payload.nil? # an input without a scriptSig, or whose last element is not a push
139
+ redeem_script = Bitcoin::Script.parse_from_payload(redeem_payload)
138
140
  if redeem_script.p2wpkh?
139
141
  pk = input.script_witness.stack.last
140
- return Bitcoin::Key.new(pubkey: pk.bth) if pk.bytesize == Bitcoin::Key::COMPRESSED_PUBLIC_KEY_SIZE
142
+ return Bitcoin::Key.new(pubkey: pk.bth) if pk && pk.bytesize == Bitcoin::Key::COMPRESSED_PUBLIC_KEY_SIZE
141
143
  end
142
144
  elsif prevout.p2wpkh?
143
145
  pk = input.script_witness.stack.last
144
- return Bitcoin::Key.new(pubkey: pk.bth) if pk.bytesize == Bitcoin::Key::COMPRESSED_PUBLIC_KEY_SIZE
146
+ return Bitcoin::Key.new(pubkey: pk.bth) if pk && pk.bytesize == Bitcoin::Key::COMPRESSED_PUBLIC_KEY_SIZE
145
147
  elsif prevout.p2tr?
146
148
  witness_stack = input.script_witness.stack.dup
149
+ return nil if witness_stack.empty?
147
150
  witness_stack.pop if witness_stack.last.bth.start_with?("50")
148
151
  if witness_stack.length > 1
149
152
  # script-path
@@ -5,7 +5,8 @@ module Bitcoin
5
5
  class Share
6
6
 
7
7
  attr_accessor :id # 15 bits, Integer
8
- attr_accessor :iteration_exp # 5 bits, Integer
8
+ attr_accessor :extendable # 1 bit, Boolean. The extendable backup flag.
9
+ attr_accessor :iteration_exp # 4 bits, Integer
9
10
  attr_accessor :group_index # 4 bits, Integer
10
11
  attr_accessor :group_threshold # 4 bits, Integer
11
12
  attr_accessor :group_count # 4 bits, Integer
@@ -14,6 +15,24 @@ module Bitcoin
14
15
  attr_accessor :value # 8n bits, hex string.
15
16
  attr_accessor :checksum # 30 bits, Integer
16
17
 
18
+ def initialize
19
+ @extendable = false
20
+ end
21
+
22
+ # Whether the extendable backup flag is set.
23
+ # When it is set, the identifier is not used as the salt of the master secret encryption, so sets of shares
24
+ # with different identifiers can be created for the same master secret and passphrase.
25
+ # @return [Boolean]
26
+ def extendable?
27
+ !!extendable
28
+ end
29
+
30
+ # The customization string of the RS1024 checksum for this share.
31
+ # @return [Array[Integer]]
32
+ def customization_string
33
+ extendable? ? CUSTOMIZATION_STRING_EXTENDABLE : CUSTOMIZATION_STRING_ORIG
34
+ end
35
+
17
36
  # Recover Share from the mnemonic words
18
37
  # @param [Array{String}] words the mnemonic words
19
38
  # @return [Bitcoin::SLIP39::Share] a share
@@ -26,15 +45,19 @@ module Bitcoin
26
45
  end
27
46
 
28
47
  raise ArgumentError, 'Invalid mnemonic length.' if indices.size < MIN_MNEMONIC_LENGTH_WORDS
29
- raise ArgumentError, 'Invalid mnemonic checksum.' unless verify_rs1024_checksum(indices)
30
-
31
- padding_length = (RADIX_BITS * (indices.size - METADATA_LENGTH_WORDS)) % 16
32
- raise ArgumentError, 'Invalid mnemonic length.' if padding_length > 8
33
48
  data = indices.map{|i|i.to_s(2).rjust(10, '0')}.join
34
49
 
35
50
  s = self.new
36
51
  s.id = data[0...ID_LENGTH_BITS].to_i(2)
37
- s.iteration_exp = data[ID_LENGTH_BITS...(ID_LENGTH_BITS + ITERATION_EXP_LENGTH_BITS)].to_i(2)
52
+ # The flag selects the customization string, so it is read before the checksum is verified.
53
+ s.extendable = data[ID_LENGTH_BITS] == '1'
54
+ raise ArgumentError, 'Invalid mnemonic checksum.' unless verify_rs1024_checksum(indices, s.customization_string)
55
+
56
+ padding_length = (RADIX_BITS * (indices.size - METADATA_LENGTH_WORDS)) % 16
57
+ raise ArgumentError, 'Invalid mnemonic length.' if padding_length > 8
58
+
59
+ exp_start = ID_LENGTH_BITS + EXTENDABLE_FLAG_LENGTH_BITS
60
+ s.iteration_exp = data[exp_start...(exp_start + ITERATION_EXP_LENGTH_BITS)].to_i(2)
38
61
  s.group_index = data[20...24].to_i(2)
39
62
  s.group_threshold = data[24...28].to_i(2) + 1
40
63
  s.group_count = data[28...32].to_i(2) + 1
@@ -46,7 +69,7 @@ module Bitcoin
46
69
  end_index = start_index + value_length - padding_length
47
70
  padding_value = data[40...(40 + padding_length)]
48
71
  raise ArgumentError, "Invalid mnemonic. padding must only zero." unless padding_value.to_i(2) == 0
49
- s.value = data[start_index...end_index].to_i(2).to_even_length_hex
72
+ s.value = data[start_index...end_index].to_i(2).to_s(16).rjust((value_length - padding_length) / 4, '0')
50
73
  s.checksum = data[(40 + value_length)..-1].to_i(2)
51
74
  s
52
75
  end
@@ -84,7 +107,9 @@ module Bitcoin
84
107
  # @param [Boolean] include_checksum whether include checksum when creating indices.
85
108
  # @param [Array[Integer]] the array of index
86
109
  def build_word_indices(include_checksum = true)
110
+ raise ArgumentError, "Iteration exponent(#{iteration_exp}) must be between 0 and #{MAX_ITERATION_EXP}." unless (0..MAX_ITERATION_EXP).cover?(iteration_exp)
87
111
  s = id.to_bits(ID_LENGTH_BITS)
112
+ s << (extendable? ? '1' : '0')
88
113
  s << iteration_exp.to_bits(ITERATION_EXP_LENGTH_BITS)
89
114
  s << group_index.to_bits(4)
90
115
  s << (group_threshold - 1).to_bits(4)
@@ -92,8 +117,12 @@ module Bitcoin
92
117
  raise StandardError, "Group threshold(#{group_threshold}) cannot be greater than group count(#{group_count})." if group_threshold > group_count
93
118
  s << member_index.to_bits(4)
94
119
  s << (member_threshold - 1).to_bits(4)
95
- value_length = value.to_i(16).bit_length
96
- padding_length = RADIX_BITS - (value_length % RADIX_BITS)
120
+ # from_words derives the padding length from the number of value words modulo 16, which only
121
+ # matches the padding written here when the value is an even number of bytes. An odd one is
122
+ # read back as a different value instead of being rejected.
123
+ raise ArgumentError, 'The length of the share value in bytes must be an even number.' unless value.htb.bytesize.even?
124
+ value_length = value.htb.bytesize * 8
125
+ padding_length = (RADIX_BITS - (value_length % RADIX_BITS)) % RADIX_BITS
97
126
  s << value.to_i(16).to_bits(value_length + padding_length)
98
127
  s << checksum.to_bits(30) if include_checksum
99
128
  s.chars.each_slice(10).map{|index| index.join.to_i(2)}
@@ -101,16 +130,17 @@ module Bitcoin
101
130
 
102
131
  # Verify RS1024 checksum
103
132
  # @param [Array[Integer] data the array of mnemonic word index
133
+ # @param [Array[Integer]] cs the customization string
104
134
  # @return [Boolean] verify result
105
- def self.verify_rs1024_checksum(data)
106
- rs1024_polymod(CUSTOMIZATION_STRING + data) == 1
135
+ def self.verify_rs1024_checksum(data, cs)
136
+ rs1024_polymod(cs + data) == 1
107
137
  end
108
138
 
109
139
  # Create RS1024 checksum
110
140
  # @param [Array[Integer] data the array of mnemonic word index without checksum
111
141
  # @return [Array[Integer]] the array of checksum integer
112
142
  def create_rs1024_checksum(data)
113
- values = CUSTOMIZATION_STRING + data + Array.new(CHECKSUM_LENGTH_WORDS, 0)
143
+ values = customization_string + data + Array.new(CHECKSUM_LENGTH_WORDS, 0)
114
144
  polymod = Bitcoin::SLIP39::Share.rs1024_polymod(values) ^ 1
115
145
  CHECKSUM_LENGTH_WORDS.times.to_a.reverse.map {|i|(polymod >> (10 * i)) & 1023 }
116
146
  end
@@ -32,9 +32,15 @@ module Bitcoin
32
32
  # @param [Integer] exp Iteration exponent. default is 0.
33
33
  # @param [String] secret master secret with hex format.
34
34
  # @param [String] passphrase the passphrase used for encryption/decryption.
35
+ # @param [Boolean] extendable the extendable backup flag. default is true, as SLIP-39 recommends.
36
+ # When it is set, the identifier is not used as the salt of the encryption, so another set of shares with
37
+ # a different identifier can be created later for the same master secret and passphrase.
38
+ # Shares with the flag set cannot be recovered by bitcoinrb 1.15.0 and earlier, nor by other implementations
39
+ # which predate the flag.
35
40
  # @return [Array[Array[Bitcoin::SLIP39::Share]]] array of group shares.
36
- def self.setup_shares(groups: [], group_threshold: nil, exp: 0, secret: nil, passphrase: '')
41
+ def self.setup_shares(groups: [], group_threshold: nil, exp: 0, secret: nil, passphrase: '', extendable: true)
37
42
  raise ArgumentError, 'Groups is empty.' if groups.empty?
43
+ raise ArgumentError, "Iteration exponent(#{exp}) must be between 0 and #{MAX_ITERATION_EXP}." unless (0..MAX_ITERATION_EXP).cover?(exp)
38
44
  raise ArgumentError, 'Group threshold must be greater than 0.' if group_threshold.nil? || group_threshold < 1
39
45
  raise ArgumentError, 'Master secret does not specified.' unless secret
40
46
  raise ArgumentError, "The length of the master secret (#{secret.htb.bytesize} bytes) must be at least #{MIN_STRENGTH_BITS / 8} bytes." if (secret.htb.bytesize * 8) < MIN_STRENGTH_BITS
@@ -48,7 +54,7 @@ module Bitcoin
48
54
  end
49
55
 
50
56
  id = SecureRandom.random_number(32767) # 32767 is max number for 15 bits.
51
- ems = encrypt(secret, passphrase, exp, id)
57
+ ems = encrypt(secret, passphrase, exp, id, extendable)
52
58
 
53
59
  group_shares = split_secret(group_threshold, groups.length, ems)
54
60
 
@@ -59,6 +65,7 @@ module Bitcoin
59
65
  shares.map do |member_index, member_share|
60
66
  share = Bitcoin::SLIP39::Share.new
61
67
  share.id = id
68
+ share.extendable = extendable
62
69
  share.iteration_exp = exp
63
70
  share.group_index = group_index
64
71
  share.group_threshold = group_threshold
@@ -81,17 +88,22 @@ module Bitcoin
81
88
  #
82
89
  # @param [Array[Bitcoin::SLIP30::Share]] shares an array of shares.
83
90
  # @param [String] passphrase the passphrase using decrypt master secret.
91
+ # @param [Boolean] legacy_salt if true, build the PBKDF2 salt with the minimum number of bytes for the
92
+ # identifier. Shares generated by bitcoinrb 1.14.0 and earlier need this to be recovered.
93
+ # It has no effect on shares whose extendable backup flag is set, since their salt does not hold the identifier.
84
94
  # @return [String] a master secret.
85
- def self.recover_secret(shares, passphrase: '')
95
+ def self.recover_secret(shares, passphrase: '', legacy_salt: false)
86
96
  raise ArgumentError, 'share is empty.' if shares.nil? || shares.empty?
87
97
  groups = {}
88
98
  id = shares[0].id
99
+ extendable = shares[0].extendable?
89
100
  exp = shares[0].iteration_exp
90
101
  group_threshold = shares.first.group_threshold
91
102
  group_count = shares.first.group_count
92
103
 
93
104
  shares.each do |share|
94
105
  raise ArgumentError, 'Invalid set of shares. All shares must have the same id.' unless id == share.id
106
+ raise ArgumentError, 'Invalid set of shares. All shares must have the same extendable backup flag.' unless extendable == share.extendable?
95
107
  raise ArgumentError, 'Invalid set of shares. All shares must have the same group threshold.' unless group_threshold == share.group_threshold
96
108
  raise ArgumentError, 'Invalid set of shares. All shares must have the same group count.' unless group_count == share.group_count
97
109
  raise ArgumentError, 'Invalid set of shares. All Shares must have the same iteration exponent.' unless exp == share.iteration_exp
@@ -127,7 +139,7 @@ module Bitcoin
127
139
  end
128
140
  end
129
141
 
130
- return decrypt(group_shares.values.first, passphrase, exp, id) if group_threshold == 1
142
+ return decrypt(group_shares.values.first, passphrase, exp, id, extendable, legacy_salt) if group_threshold == 1
131
143
 
132
144
  raise ArgumentError, "Wrong number of mnemonics. Group threshold is #{group_threshold}, but share count is #{group_shares.length}" if group_shares.length < group_threshold
133
145
 
@@ -138,7 +150,7 @@ module Bitcoin
138
150
  recover_digest = create_digest(secret, random_value)
139
151
  raise ArgumentError, 'Invalid digest of the shared secret.' unless digest == recover_digest
140
152
 
141
- decrypt(secret, passphrase, exp, id)
153
+ decrypt(secret, passphrase, exp, id, extendable, legacy_salt)
142
154
  end
143
155
 
144
156
  private
@@ -168,9 +180,11 @@ module Bitcoin
168
180
  # @param [String] passphrase the passphrase when using encrypt master secret with binary format.
169
181
  # @param [Integer] exp iteration exponent
170
182
  # @param [Integer] id identifier
171
- def self.decrypt(ems, passphrase, exp, id)
183
+ # @param [Boolean] extendable the extendable backup flag
184
+ # @param [Boolean] legacy_salt if true, build the salt with the minimum number of bytes for the identifier.
185
+ def self.decrypt(ems, passphrase, exp, id, extendable, legacy_salt = false)
172
186
  l, r = ems[0...(ems.length / 2)].htb, ems[(ems.length / 2)..-1].htb
173
- salt = get_salt(id)
187
+ salt = get_salt(id, extendable: extendable, legacy: legacy_salt)
174
188
  e = (Bitcoin::SLIP39::BASE_ITERATION_COUNT << exp) / Bitcoin::SLIP39::ROUND_COUNT
175
189
  Bitcoin::SLIP39::ROUND_COUNT.times.to_a.reverse.each do |i|
176
190
  f = OpenSSL::PKCS5.pbkdf2_hmac((i.itb + passphrase), salt + r, e, r.bytesize, 'sha256')
@@ -184,11 +198,12 @@ module Bitcoin
184
198
  # @param [String] passphrase the passphrase when using encrypt master secret with binary format.
185
199
  # @param [Integer] exp iteration exponent
186
200
  # @param [Integer] id identifier
201
+ # @param [Boolean] extendable the extendable backup flag
187
202
  # @return [String] encrypted master secret with hex format.
188
- def self.encrypt(secret, passphrase, exp, id)
203
+ def self.encrypt(secret, passphrase, exp, id, extendable)
189
204
  s = secret.htb
190
205
  l, r = s[0...(s.bytesize / 2)], s[(s.bytesize / 2)..-1]
191
- salt = get_salt(id)
206
+ salt = get_salt(id, extendable: extendable)
192
207
  e = (Bitcoin::SLIP39::BASE_ITERATION_COUNT << exp) / Bitcoin::SLIP39::ROUND_COUNT
193
208
  Bitcoin::SLIP39::ROUND_COUNT.times.to_a.each do |i|
194
209
  f = OpenSSL::PKCS5.pbkdf2_hmac((i.itb + passphrase), salt + r, e, r.bytesize, 'sha256')
@@ -206,11 +221,17 @@ module Bitcoin
206
221
  h[0...4].bth
207
222
  end
208
223
 
209
- # get salt using encryption/decryption form id.
224
+ # get salt prefix used in encryption/decryption.
210
225
  # @param [Integer] id id
226
+ # @param [Boolean] extendable the extendable backup flag. If true, the salt is empty since the identifier
227
+ # is not part of the encryption.
228
+ # @param [Boolean] legacy if true, encode the identifier with the minimum number of bytes. SLIP-39 requires
229
+ # two bytes, so a salt built this way only matches shares generated by bitcoinrb 1.14.0 and earlier.
211
230
  # @return [String] salt with binary format.
212
- def self.get_salt(id)
213
- (Bitcoin::SLIP39::CUSTOMIZATION_STRING.pack('c*') + id.itb)
231
+ def self.get_salt(id, extendable: false, legacy: false)
232
+ return '' if extendable
233
+ identifier = legacy ? id.itb : [id].pack('n')
234
+ (Bitcoin::SLIP39::CUSTOMIZATION_STRING_ORIG.pack('c*') + identifier)
214
235
  end
215
236
 
216
237
  # Split the share into +count+ with threshold +threshold+.
@@ -19,18 +19,26 @@ module Bitcoin
19
19
  RADIX = 2 ** RADIX_BITS
20
20
  # The length of the random identifier in bits.
21
21
  ID_LENGTH_BITS = 15
22
+ # The length of the extendable backup flag in bits.
23
+ EXTENDABLE_FLAG_LENGTH_BITS = 1
22
24
  # The length of the iteration exponent in bits.
23
- ITERATION_EXP_LENGTH_BITS = 5
24
- # The length of the random identifier and iteration exponent in words.
25
- ID_EXP_LENGTH_WORDS = bits_to_words(ID_LENGTH_BITS + ITERATION_EXP_LENGTH_BITS)
25
+ ITERATION_EXP_LENGTH_BITS = 4
26
+ # The maximum iteration exponent.
27
+ MAX_ITERATION_EXP = (1 << ITERATION_EXP_LENGTH_BITS) - 1
28
+ # The length of the random identifier, extendable backup flag and iteration exponent in words.
29
+ ID_EXP_LENGTH_WORDS = bits_to_words(ID_LENGTH_BITS + EXTENDABLE_FLAG_LENGTH_BITS + ITERATION_EXP_LENGTH_BITS)
26
30
  # The maximum number of shares that can be created.
27
31
  MAX_SHARE_COUNT = 16
28
32
  # The length of the RS1024 checksum in words.
29
33
  CHECKSUM_LENGTH_WORDS = 3
30
34
  # The length of the digest of the shared secret in bytes.
31
35
  DIGEST_LENGTH_BYTES = 4
32
- # The customization string used in the RS1024 checksum and in the PBKDF2 salt.
33
- CUSTOMIZATION_STRING = 'shamir'.bytes
36
+ # The customization string used in the RS1024 checksum and in the PBKDF2 salt when the extendable backup flag is not set.
37
+ CUSTOMIZATION_STRING_ORIG = 'shamir'.bytes
38
+ # The customization string used in the RS1024 checksum when the extendable backup flag is set.
39
+ CUSTOMIZATION_STRING_EXTENDABLE = 'shamir_extendable'.bytes
40
+ # Deprecated alias of CUSTOMIZATION_STRING_ORIG.
41
+ CUSTOMIZATION_STRING = CUSTOMIZATION_STRING_ORIG
34
42
  # The length of the mnemonic in words without the share value.
35
43
  METADATA_LENGTH_WORDS = ID_EXP_LENGTH_WORDS + 2 + CHECKSUM_LENGTH_WORDS
36
44
  # The minimum allowed entropy of the master secret.
@@ -41,7 +41,12 @@ module Bitcoin
41
41
  end
42
42
 
43
43
  def ==(other)
44
- to_payload == other.to_payload
44
+ other.is_a?(ControlBlock) && to_payload == other.to_payload
45
+ end
46
+ alias eql? ==
47
+
48
+ def hash
49
+ to_payload.hash
45
50
  end
46
51
  end
47
52
  end