bsv-sdk 0.16.0 → 0.18.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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +53 -0
  3. data/lib/bsv/auth/certificate.rb +6 -2
  4. data/lib/bsv/auth/get_verifiable_certificates.rb +6 -6
  5. data/lib/bsv/auth/peer.rb +10 -4
  6. data/lib/bsv/auth/session_manager.rb +81 -5
  7. data/lib/bsv/identity/client.rb +5 -2
  8. data/lib/bsv/mcp/tools/broadcast_p2pkh.rb +4 -4
  9. data/lib/bsv/mcp/tools/check_balance.rb +2 -2
  10. data/lib/bsv/mcp/tools/fetch_utxos.rb +2 -2
  11. data/lib/bsv/mcp/tools/helpers.rb +2 -2
  12. data/lib/bsv/network/broadcast_error.rb +2 -0
  13. data/lib/bsv/network/broadcast_response.rb +4 -1
  14. data/lib/bsv/network/protocol.rb +56 -4
  15. data/lib/bsv/network/protocols/arc.rb +10 -6
  16. data/lib/bsv/network/protocols/chaintracks.rb +6 -2
  17. data/lib/bsv/network/protocols/jungle_bus.rb +52 -0
  18. data/lib/bsv/network/protocols/ordinals.rb +110 -8
  19. data/lib/bsv/network/protocols/taal_binary.rb +18 -4
  20. data/lib/bsv/network/protocols/woc_rest.rb +166 -85
  21. data/lib/bsv/network/protocols.rb +1 -0
  22. data/lib/bsv/network/provider.rb +36 -5
  23. data/lib/bsv/network/providers/gorilla_pool.rb +42 -20
  24. data/lib/bsv/network/providers/taal.rb +38 -15
  25. data/lib/bsv/network/providers/whats_on_chain.rb +42 -21
  26. data/lib/bsv/network/utxo.rb +8 -2
  27. data/lib/bsv/overlay/lookup_resolver.rb +5 -4
  28. data/lib/bsv/overlay/topic_broadcaster.rb +2 -2
  29. data/lib/bsv/overlay/types.rb +2 -0
  30. data/lib/bsv/primitives/hex.rb +64 -0
  31. data/lib/bsv/registry/client.rb +10 -8
  32. data/lib/bsv/registry/types.rb +2 -0
  33. data/lib/bsv/script/interpreter/interpreter.rb +7 -0
  34. data/lib/bsv/script/interpreter/operations/crypto.rb +7 -1
  35. data/lib/bsv/transaction/beef.rb +223 -147
  36. data/lib/bsv/transaction/merkle_path.rb +54 -38
  37. data/lib/bsv/transaction/transaction.rb +103 -40
  38. data/lib/bsv/transaction/transaction_input.rb +23 -18
  39. data/lib/bsv/version.rb +1 -1
  40. data/lib/bsv/wallet/interface/brc100.rb +5 -2
  41. data/lib/bsv/wallet/proto_wallet/key_deriver.rb +2 -0
  42. data/lib/bsv/wallet/proto_wallet.rb +6 -0
  43. data/lib/bsv/wire_format.rb +40 -14
  44. data/lib/bsv-sdk.rb +14 -0
  45. metadata +4 -3
@@ -21,7 +21,8 @@ module BSV
21
21
  # @!attribute [r] hash
22
22
  # @return [String, nil] 32-byte hash (nil when duplicate)
23
23
  # @!attribute [r] txid
24
- # @return [Boolean] whether this leaf is a transaction ID
24
+ # @return [Boolean] BRC-74 flag — whether this leaf represents a transaction in
25
+ # the merkle tree (the 0x02 bit in the BRC-74 serialisation). Not a txid value.
25
26
  # @!attribute [r] duplicate
26
27
  # @return [Boolean] whether this leaf duplicates its sibling
27
28
  class PathElement
@@ -29,7 +30,9 @@ module BSV
29
30
 
30
31
  # @param offset [Integer] position index within the tree level
31
32
  # @param hash [String, nil] 32-byte hash (nil when duplicate)
32
- # @param txid [Boolean] whether this leaf is a transaction ID
33
+ # @param txid [Boolean] BRC-74 flag — true when this leaf represents a transaction
34
+ # in the tree (the 0x02 bit in the BRC-74 serialisation). This is NOT a txid
35
+ # value; it is a boolean presence flag mandated by the BRC-74 specification.
33
36
  # @param duplicate [Boolean] whether this leaf duplicates its sibling
34
37
  def initialize(offset:, hash: nil, txid: false, duplicate: false)
35
38
  @offset = offset
@@ -147,25 +150,26 @@ module BSV
147
150
  # @example Convert a WoC TSC proof
148
151
  # tsc = JSON.parse(woc_response).first
149
152
  # mp = BSV::Transaction::MerklePath.from_tsc(
150
- # txid: tsc['txOrId'],
153
+ # dtxid_hex: tsc['txOrId'],
151
154
  # index: tsc['index'],
152
155
  # nodes: tsc['nodes'],
153
156
  # block_height: 612_251
154
157
  # )
155
158
  # mp.compute_root_hex(tsc['txOrId']) #=> the block's merkle root
156
159
  #
157
- # @param txid [String] hex-encoded transaction ID in display byte order
160
+ # @param dtxid_hex [String] hex-encoded transaction ID in display byte order
158
161
  # @param index [Integer] the transaction's position in the block
159
162
  # @param nodes [Array<String>] sibling hashes leaf-to-root, each a 32-byte
160
163
  # hex string in display byte order, or +"*"+ for a duplicate node
161
164
  # @param block_height [Integer] the block's height (TSC carries the block
162
165
  # hash; the caller must look up the height separately)
163
166
  # @return [MerklePath] a BRC-74 merkle path equivalent to the TSC proof
164
- def self.from_tsc(txid:, index:, nodes:, block_height:)
165
- txid_bytes = [txid].pack('H*').reverse
167
+ def self.from_tsc(dtxid_hex:, index:, nodes:, block_height:)
168
+ BSV::Primitives::Hex.validate_dtxid_hex!(dtxid_hex, name: 'dtxid_hex')
169
+ wtxid = [dtxid_hex].pack('H*').reverse
166
170
 
167
171
  # Level 0 always contains the txid leaf.
168
- level0 = [PathElement.new(offset: index, hash: txid_bytes, txid: true)]
172
+ level0 = [PathElement.new(offset: index, hash: wtxid, txid: true)]
169
173
 
170
174
  # A single-tx block has no siblings — the txid IS the merkle root.
171
175
  return new(block_height: block_height, path: [level0]) if nodes.empty?
@@ -193,6 +197,7 @@ module BSV
193
197
  if node == '*'
194
198
  PathElement.new(offset: offset, duplicate: true)
195
199
  else
200
+ BSV::Primitives::Hex.validate_dtxid_hex!(node, name: "TSC merkle node at offset #{offset}")
196
201
  PathElement.new(offset: offset, hash: [node].pack('H*').reverse)
197
202
  end
198
203
  end
@@ -240,17 +245,22 @@ module BSV
240
245
 
241
246
  # Recompute the merkle root from this path and a transaction ID.
242
247
  #
243
- # @param txid [String, nil] 32-byte txid in internal byte order (auto-detected if nil)
244
- # @return [String] 32-byte merkle root in internal byte order
245
- # @raise [ArgumentError] if the txid is not found in the path
246
- def compute_root(txid = nil)
247
- txid ||= @path[0].find(&:hash)&.hash
248
- return txid if @path.length == 1 && @path[0].length == 1
248
+ # @param wtxid [String, nil] 32-byte txid in wire byte order (auto-detected if nil)
249
+ # @return [String] 32-byte merkle root in wire byte order
250
+ # @raise [ArgumentError] if the wtxid is not found in the path
251
+ def compute_root(wtxid = nil)
252
+ wtxid ||= @path[0].find(&:hash)&.hash
253
+ BSV::Primitives::Hex.validate_wtxid!(wtxid, name: 'wtxid') if wtxid
254
+ BSV.logger&.debug do
255
+ dtxid = wtxid&.reverse&.unpack1('H*')
256
+ "[MerklePath] compute_root: dtxid=#{dtxid} block_height=#{@block_height} levels=#{@path.length}"
257
+ end
258
+ return wtxid if @path.length == 1 && @path[0].length == 1
249
259
 
250
260
  indexed = build_indexed_path
251
261
 
252
- tx_leaf = @path[0].find { |l| l.hash == txid }
253
- raise ArgumentError, 'the BUMP does not contain the txid' unless tx_leaf
262
+ tx_leaf = @path[0].find { |l| l.hash == wtxid }
263
+ raise ArgumentError, 'the BUMP does not contain the given wtxid' unless tx_leaf
254
264
 
255
265
  working = tx_leaf.hash
256
266
  index = tx_leaf.offset
@@ -289,11 +299,12 @@ module BSV
289
299
 
290
300
  # Recompute the merkle root and return it as a hex string.
291
301
  #
292
- # @param txid_hex [String, nil] hex-encoded txid (display order)
302
+ # @param dtxid_hex [String, nil] hex-encoded txid (display order)
293
303
  # @return [String] hex-encoded merkle root (display order)
294
- def compute_root_hex(txid_hex = nil)
295
- txid = txid_hex ? [txid_hex].pack('H*').reverse : nil
296
- compute_root(txid).reverse.unpack1('H*')
304
+ def compute_root_hex(dtxid_hex = nil)
305
+ BSV::Primitives::Hex.validate_dtxid_hex!(dtxid_hex, name: 'compute_root_hex dtxid_hex') if dtxid_hex
306
+ wtxid = dtxid_hex ? [dtxid_hex].pack('H*').reverse : nil
307
+ compute_root(wtxid).reverse.unpack1('H*')
297
308
  end
298
309
 
299
310
  # --- Verification ---
@@ -312,22 +323,27 @@ module BSV
312
323
  # and accepts immature ones — the opposite of the intended behaviour. The correct
313
324
  # logic is: reject when `current_height - block_height < 100` (immature).
314
325
  #
315
- # @param txid_hex [String] hex-encoded transaction ID (display order)
326
+ # @param dtxid_hex [String] hex-encoded transaction ID (display order)
316
327
  # @param chain_tracker [ChainTracker] chain tracker to verify the root against
317
328
  # @return [Boolean] true if the computed root matches the block at this height
318
- def verify(txid_hex, chain_tracker)
319
- txid_bytes = [txid_hex].pack('H*').reverse
320
- txid_leaf = @path[0].find { |l| l.hash == txid_bytes }
329
+ def verify(dtxid_hex, chain_tracker)
330
+ BSV::Primitives::Hex.validate_dtxid_hex!(dtxid_hex, name: 'dtxid_hex')
331
+ wtxid = [dtxid_hex].pack('H*').reverse
332
+ tx_leaf = @path[0].find { |l| l.hash == wtxid }
321
333
 
322
334
  # Offset 0 in a block's merkle tree is always the coinbase transaction —
323
335
  # a Bitcoin protocol invariant. Apply the 100-block maturity check.
324
- if txid_leaf&.offset&.zero?
336
+ if tx_leaf&.offset&.zero?
325
337
  current = chain_tracker.current_height
326
338
  return false if current - @block_height < 100
327
339
  end
328
340
 
329
- root_hex = compute_root_hex(txid_hex)
330
- chain_tracker.valid_root_for_height?(root_hex, @block_height)
341
+ root_hex = compute_root_hex(dtxid_hex)
342
+ valid = chain_tracker.valid_root_for_height?(root_hex, @block_height)
343
+ BSV.logger&.debug do
344
+ "[MerklePath] verify: dtxid=#{dtxid_hex} height=#{@block_height} root=#{root_hex} valid=#{valid}"
345
+ end
346
+ valid
331
347
  end
332
348
 
333
349
  # --- Combine ---
@@ -445,24 +461,24 @@ module BSV
445
461
  #
446
462
  # Matches the TS SDK's +MerklePath.extract+ behaviour.
447
463
  #
448
- # @param txid_hashes [Array<String>] 32-byte txids in internal byte
464
+ # @param wtxid_hashes [Array<String>] 32-byte txids in wire byte
449
465
  # order (reverse of display order). To pass hex strings, use
450
- # +txid_hexes.map { |h| [h].pack('H*').reverse }+.
466
+ # +dtxid_hexes.map { |h| [h].pack('H*').reverse }+.
451
467
  # @return [MerklePath] a new trimmed compound path proving only the
452
468
  # requested txids
453
- # @raise [ArgumentError] if +txid_hashes+ is empty, any requested
469
+ # @raise [ArgumentError] if +wtxid_hashes+ is empty, any requested
454
470
  # txid is not present in the source path's level 0, or the
455
471
  # extracted path's root does not match the source root
456
- def extract(txid_hashes)
457
- raise ArgumentError, 'at least one txid must be provided to extract' if txid_hashes.empty?
472
+ def extract(wtxid_hashes)
473
+ raise ArgumentError, 'at least one wtxid must be provided to extract' if wtxid_hashes.empty?
458
474
 
459
475
  original_root = compute_root
460
476
  indexed = build_indexed_path
461
477
 
462
478
  # Build a level-0 hash → offset lookup
463
- txid_to_offset = {}
479
+ wtxid_to_offset = {}
464
480
  @path[0].each do |leaf|
465
- txid_to_offset[leaf.hash] = leaf.offset if leaf.hash
481
+ wtxid_to_offset[leaf.hash] = leaf.offset if leaf.hash
466
482
  end
467
483
 
468
484
  max_offset = @path[0].map(&:offset).max || 0
@@ -470,15 +486,15 @@ module BSV
470
486
 
471
487
  needed = Array.new(tree_height) { {} }
472
488
 
473
- txid_hashes.each do |txid|
474
- tx_offset = txid_to_offset[txid]
489
+ wtxid_hashes.each do |wtxid_hash|
490
+ tx_offset = wtxid_to_offset[wtxid_hash]
475
491
  if tx_offset.nil?
476
492
  raise ArgumentError,
477
- "transaction ID #{txid.reverse.unpack1('H*')} not found in the Merkle Path"
493
+ "wtxid #{wtxid_hash.reverse.unpack1('H*')} not found in the Merkle Path"
478
494
  end
479
495
 
480
- # Level 0: the txid leaf itself + its tree sibling
481
- needed[0][tx_offset] = PathElement.new(offset: tx_offset, hash: txid, txid: true)
496
+ # Level 0: the transaction leaf itself + its tree sibling
497
+ needed[0][tx_offset] = PathElement.new(offset: tx_offset, hash: wtxid_hash, txid: true)
482
498
  sib0_offset = tx_offset ^ 1
483
499
  unless needed[0].key?(sib0_offset)
484
500
  sib = offset_leaf(indexed, 0, sib0_offset)
@@ -329,16 +329,20 @@ module BSV
329
329
  ancestors = collect_ancestors
330
330
 
331
331
  bump_index_by_height = build_beef_bumps(beef, ancestors)
332
+ BSV.logger&.debug do
333
+ proven = ancestors.count(&:merkle_path)
334
+ "[Transaction] BEEF: #{ancestors.length} ancestors, #{proven} proven " \
335
+ "across #{bump_index_by_height.length} block heights"
336
+ end
332
337
 
333
338
  ancestors.each do |tx|
334
339
  entry = if tx.merkle_path
335
- Beef::BeefTx.new(
336
- format: Beef::FORMAT_RAW_TX_AND_BUMP,
340
+ Beef::ProvenTxEntry.new(
337
341
  transaction: tx,
338
342
  bump_index: bump_index_by_height.fetch(tx.merkle_path.block_height)
339
343
  )
340
344
  else
341
- Beef::BeefTx.new(format: Beef::FORMAT_RAW_TX, transaction: tx)
345
+ Beef::RawTxEntry.new(transaction: tx)
342
346
  end
343
347
  beef.transactions << entry
344
348
  end
@@ -357,11 +361,11 @@ module BSV
357
361
  # full ancestry wired, including late-bound BUMP attachment.
358
362
  #
359
363
  # For Atomic BEEFs (BRC-95), the subject transaction is identified by
360
- # the embedded subject_txid field. For plain BEEFs, the last transaction
361
- # with a raw tx entry is used as the subject.
364
+ # the embedded +subject_wtxid+ field. For plain BEEFs, the last
365
+ # transaction with a raw tx entry is used as the subject.
362
366
  #
363
367
  # Uses +find_atomic_transaction+ so that FORMAT_RAW_TX ancestors whose
364
- # txid appears as a leaf in a separately-stored BUMP get their
368
+ # wtxid appears as a leaf in a separately-stored BUMP get their
365
369
  # +merkle_path+ wired correctly — a gap not covered by the initial
366
370
  # +wire_source_transactions+ pass in +Beef.from_binary+.
367
371
  #
@@ -370,11 +374,11 @@ module BSV
370
374
  # or nil if the BEEF is empty or contains no raw transaction entries
371
375
  def self.from_beef(data)
372
376
  beef = Beef.from_binary(data)
373
- subject_txid = beef.subject_txid ||
374
- beef.transactions.reverse.find(&:transaction)&.transaction&.txid
375
- return nil unless subject_txid
377
+ subject_wtxid = beef.subject_wtxid ||
378
+ beef.transactions.reverse.find { |bt| !bt.is_a?(Beef::TxidOnlyEntry) }&.wtxid
379
+ return nil unless subject_wtxid
376
380
 
377
- beef.find_atomic_transaction(subject_txid)
381
+ beef.find_atomic_transaction(subject_wtxid)
378
382
  end
379
383
 
380
384
  # Parse a BEEF hex string and return the subject transaction.
@@ -388,24 +392,38 @@ module BSV
388
392
 
389
393
  # --- Transaction ID ---
390
394
 
391
- # Compute the transaction ID (double-SHA-256 of the serialised tx, byte-reversed).
395
+ # Wire-order transaction ID (raw SHA-256d of the serialised tx).
392
396
  #
393
- # Returns display byte order (reversed from the natural hash).
394
- # Compare with {TransactionInput#prev_tx_id} which stores wire byte
395
- # order (natural hash). Use +.reverse+ to convert between the two.
397
+ # Used by BEEF, BUMPs, and merkle paths, which all work in wire byte order
398
+ # to match {TransactionInput#prev_wtxid}.
396
399
  #
397
- # @return [String] 32-byte transaction ID in display byte order
398
- def txid
399
- BSV::Primitives::Digest.sha256d(to_binary).reverse
400
+ # @return [String] 32-byte transaction ID in wire byte order
401
+ def wtxid
402
+ id = BSV::Primitives::Digest.sha256d(to_binary)
403
+ BSV.logger&.debug { "[Transaction] wtxid computed (dtxid=#{id.reverse.unpack1('H*')})" }
404
+ id
400
405
  end
401
406
 
402
407
  # The transaction ID as a hex string (display byte order).
403
408
  #
404
- # @return [String] hex-encoded transaction ID
409
+ # @return [String] 64-char hex-encoded transaction ID (display order)
405
410
  def txid_hex
406
- txid.unpack1('H*')
411
+ wtxid.reverse.unpack1('H*')
407
412
  end
408
413
 
414
+ # Display-order transaction ID as a hex string.
415
+ #
416
+ # Mirrors the wallet gem's +DisplayTxid+ pattern. +dtxid+ always returns
417
+ # a 64-char hex string suitable for JSON and UI boundaries.
418
+ #
419
+ # @return [String] hex-encoded transaction ID (display order)
420
+ alias dtxid txid_hex
421
+
422
+ # Display-order transaction ID as a hex string (alias for {#dtxid}).
423
+ #
424
+ # @return [String] hex-encoded transaction ID (display order)
425
+ alias dtxid_hex txid_hex
426
+
409
427
  # --- Sighash (BIP-143 with FORKID) ---
410
428
 
411
429
  # Build the BIP-143 sighash preimage for an input.
@@ -421,6 +439,29 @@ module BSV
421
439
  raise ArgumentError, 'only SIGHASH_FORKID types are supported' unless sighash_type & Sighash::FORK_ID != 0
422
440
 
423
441
  input = @inputs[input_index]
442
+ raise ArgumentError, "no input at index #{input_index}" if input.nil?
443
+
444
+ # Resolve source data from wired source_transaction when not explicitly set.
445
+ if input.source_transaction
446
+ source_output = input.source_transaction.outputs[input.prev_tx_out_index]
447
+ if source_output
448
+ input.source_satoshis ||= source_output.satoshis
449
+ input.source_locking_script ||= source_output.locking_script
450
+ end
451
+ end
452
+
453
+ if input.source_satoshis.nil?
454
+ raise ArgumentError,
455
+ "input #{input_index} has nil source_satoshis — " \
456
+ 'set it or wire source_transaction before computing sighash'
457
+ end
458
+
459
+ unless subscript || input.source_locking_script
460
+ raise ArgumentError,
461
+ "input #{input_index} has nil source_locking_script — " \
462
+ 'set it or wire source_transaction before computing sighash'
463
+ end
464
+
424
465
  base_type = sighash_type & Sighash::MASK
425
466
  anyone = sighash_type.anybits?(Sighash::ANYONE_CAN_PAY)
426
467
 
@@ -456,6 +497,19 @@ module BSV
456
497
  # 10. sighash type (4 LE) — includes FORKID flag
457
498
  buf << [sighash_type].pack('V')
458
499
 
500
+ BSV.logger&.debug do
501
+ hp = buf.byteslice(4, 32).unpack1('H*')
502
+ hs = buf.byteslice(36, 32).unpack1('H*')
503
+ op = input.outpoint_binary.unpack1('H*')
504
+ sc = script_bytes.unpack1('H*')
505
+ ho = buf.byteslice(-40, 32).unpack1('H*')
506
+ "[Sighash] input=#{input_index} type=0x#{format('%02x', sighash_type)} " \
507
+ "version=#{@version} hashPrevouts=#{hp} hashSequence=#{hs} " \
508
+ "outpoint=#{op} scriptCode=#{sc[0, 40]}#{'...' if sc.length > 40} " \
509
+ "value=#{input.source_satoshis} seq=#{input.sequence} " \
510
+ "hashOutputs=#{ho} locktime=#{@lock_time}"
511
+ end
512
+
459
513
  buf
460
514
  end
461
515
 
@@ -466,7 +520,9 @@ module BSV
466
520
  # @param subscript [Script::Script, nil] override locking script for the input
467
521
  # @return [String] 32-byte sighash digest
468
522
  def sighash(input_index, sighash_type = Sighash::ALL_FORK_ID, subscript: nil)
469
- BSV::Primitives::Digest.sha256d(sighash_preimage(input_index, sighash_type, subscript: subscript))
523
+ digest = BSV::Primitives::Digest.sha256d(sighash_preimage(input_index, sighash_type, subscript: subscript))
524
+ BSV.logger&.debug { "[Sighash] digest=#{digest.unpack1('H*')}" }
525
+ digest
470
526
  end
471
527
 
472
528
  # --- Signing ---
@@ -580,17 +636,17 @@ module BSV
580
636
 
581
637
  until queue.empty?
582
638
  tx = queue.shift
583
- tx_id = tx.txid_hex
584
- next if verified[tx_id]
639
+ wtxid = tx.wtxid
640
+ next if verified[wtxid]
585
641
 
586
642
  # Merkle path short-circuit: proven transaction needs no input verification
587
643
  if tx.merkle_path
588
- unless tx.merkle_path.verify(tx_id, chain_tracker)
644
+ unless tx.merkle_path.verify(tx.txid_hex, chain_tracker)
589
645
  raise VerificationError.new(:invalid_merkle_proof,
590
- "invalid merkle proof for transaction #{tx_id}")
646
+ "invalid merkle proof for transaction #{tx.txid_hex}")
591
647
  end
592
648
 
593
- verified[tx_id] = true
649
+ verified[wtxid] = true
594
650
  next
595
651
  end
596
652
 
@@ -622,13 +678,13 @@ module BSV
622
678
 
623
679
  # Enqueue source transaction for verification if not yet verified
624
680
  source_tx = input.source_transaction
625
- queue << source_tx if source_tx && !verified[source_tx.txid_hex]
681
+ queue << source_tx if source_tx && !verified[source_tx.wtxid]
626
682
  end
627
683
 
628
684
  # Output ≤ input check
629
685
  verify_output_constraint(tx)
630
686
 
631
- verified[tx_id] = true
687
+ verified[wtxid] = true
632
688
  end
633
689
 
634
690
  true
@@ -777,19 +833,19 @@ module BSV
777
833
  end
778
834
 
779
835
  def verify_input_requirements(tx, input, index)
780
- tx_id = tx.txid_hex
836
+ dtxid_hex = tx.txid_hex
781
837
  if input.unlocking_script.nil?
782
838
  raise VerificationError.new(:missing_source,
783
- "input #{index} of transaction #{tx_id} has no unlocking script")
839
+ "input #{index} of transaction #{dtxid_hex} has no unlocking script")
784
840
  end
785
841
  if input.source_locking_script.nil?
786
842
  raise VerificationError.new(:missing_source,
787
- "input #{index} of transaction #{tx_id} has no source locking script")
843
+ "input #{index} of transaction #{dtxid_hex} has no source locking script")
788
844
  end
789
845
  return unless input.source_satoshis.nil?
790
846
 
791
847
  raise VerificationError.new(:missing_source,
792
- "input #{index} of transaction #{tx_id} has no source satoshis")
848
+ "input #{index} of transaction #{dtxid_hex} has no source satoshis")
793
849
  end
794
850
 
795
851
  def verify_fee(fee_model)
@@ -843,7 +899,7 @@ module BSV
843
899
 
844
900
  # Collect this transaction and all its ancestors in dependency order
845
901
  # (ancestors first, self last). Stops recursion at transactions with
846
- # a merkle_path (proven leaves). Deduplicates by txid.
902
+ # a merkle_path (proven leaves). Deduplicates by wtxid (wire-order bytes).
847
903
  def collect_ancestors
848
904
  seen = {}
849
905
  result = []
@@ -852,18 +908,25 @@ module BSV
852
908
  end
853
909
 
854
910
  def collect_ancestors_recursive(tx, seen, result)
855
- txid = tx.txid
856
- return if seen.key?(txid)
911
+ wtxid = tx.wtxid
912
+ return if seen.key?(wtxid)
857
913
 
858
- unless tx.merkle_path
859
- tx.inputs.each do |input|
860
- next unless input.source_transaction
914
+ if tx.merkle_path
915
+ BSV.logger&.debug do
916
+ "[Transaction] ancestor: #{tx.dtxid_hex} proven at height #{tx.merkle_path.block_height} (leaf stop)"
917
+ end
918
+ else
919
+ tx.inputs.each_with_index do |input, idx|
920
+ unless input.source_transaction
921
+ BSV.logger&.debug { "[Transaction] ancestor: #{tx.dtxid_hex} input #{idx} has no source_transaction (skipped)" }
922
+ next
923
+ end
861
924
 
862
925
  collect_ancestors_recursive(input.source_transaction, seen, result)
863
926
  end
864
927
  end
865
928
 
866
- seen[txid] = true
929
+ seen[wtxid] = true
867
930
  result << tx
868
931
  end
869
932
 
@@ -886,8 +949,8 @@ module BSV
886
949
  merged = txs.first.merkle_path.dup
887
950
  txs.drop(1).each { |t| merged.combine(t.merkle_path) }
888
951
 
889
- txid_hashes = txs.map { |t| t.txid.reverse }
890
- clean = merged.extract(txid_hashes)
952
+ wtxid_hashes = txs.map(&:wtxid)
953
+ clean = merged.extract(wtxid_hashes)
891
954
 
892
955
  bump_index_by_height[height] = beef.bumps.length
893
956
  beef.bumps << clean
@@ -8,8 +8,8 @@ module BSV
8
8
  # output index (the "outpoint"), and provide an unlocking script to
9
9
  # satisfy the locking script conditions.
10
10
  class TransactionInput
11
- # @return [String] 32-byte transaction ID of the output being spent (internal byte order)
12
- attr_reader :prev_tx_id
11
+ # @return [String] 32-byte wire-order transaction ID of the output being spent
12
+ attr_reader :prev_wtxid
13
13
 
14
14
  # @return [Integer] index of the output within the previous transaction
15
15
  attr_reader :prev_tx_out_index
@@ -32,15 +32,17 @@ module BSV
32
32
  # @return [UnlockingScriptTemplate, nil] template for deferred signing
33
33
  attr_accessor :unlocking_script_template
34
34
 
35
- # @param prev_tx_id [String] 32-byte transaction ID (internal byte order)
35
+ # @param prev_wtxid [String] 32-byte wire-order transaction ID
36
36
  # @param prev_tx_out_index [Integer] output index in the previous transaction
37
37
  # @param unlocking_script [Script::Script, nil] unlocking script (nil if unsigned)
38
38
  # @param sequence [Integer] sequence number
39
- def initialize(prev_tx_id:, prev_tx_out_index:, unlocking_script: nil, sequence: 0xFFFFFFFF)
40
- @prev_tx_id = prev_tx_id.b
39
+ def initialize(prev_wtxid:, prev_tx_out_index:, unlocking_script: nil, sequence: 0xFFFFFFFF)
40
+ BSV::Primitives::Hex.validate_wtxid!(prev_wtxid, name: 'prev_wtxid')
41
+ @prev_wtxid = prev_wtxid.b
41
42
  @prev_tx_out_index = prev_tx_out_index
42
43
  @unlocking_script = unlocking_script
43
44
  @sequence = sequence
45
+ BSV.logger&.debug { "[TransactionInput] prev_wtxid set: #{dtxid_hex}:#{@prev_tx_out_index}" }
44
46
  end
45
47
 
46
48
  # Serialise the input to its binary wire format.
@@ -48,7 +50,7 @@ module BSV
48
50
  # @return [String] binary input (outpoint + varint + script + sequence)
49
51
  def to_binary
50
52
  script_bytes = @unlocking_script ? @unlocking_script.to_binary : ''.b
51
- @prev_tx_id +
53
+ @prev_wtxid +
52
54
  [@prev_tx_out_index].pack('V') +
53
55
  VarInt.encode(script_bytes.bytesize) +
54
56
  script_bytes +
@@ -66,7 +68,7 @@ module BSV
66
68
  "truncated input: need 36 bytes for outpoint at offset #{offset}, got #{data.bytesize - offset}"
67
69
  end
68
70
 
69
- prev_tx_id = data.byteslice(offset, 32)
71
+ prev_wtxid = data.byteslice(offset, 32)
70
72
  prev_tx_out_index = data.byteslice(offset + 32, 4).unpack1('V')
71
73
  offset += 36
72
74
 
@@ -90,7 +92,7 @@ module BSV
90
92
 
91
93
  total = 36 + vi_size + script_len + 4
92
94
  input = new(
93
- prev_tx_id: prev_tx_id,
95
+ prev_wtxid: prev_wtxid,
94
96
  prev_tx_out_index: prev_tx_out_index,
95
97
  unlocking_script: unlocking_script,
96
98
  sequence: sequence
@@ -98,26 +100,29 @@ module BSV
98
100
  [input, total]
99
101
  end
100
102
 
101
- # Convert a hex transaction ID to internal byte order (reversed).
103
+ # Convert a display-order hex transaction ID to wire-order bytes.
102
104
  #
103
105
  # @param hex [String] hex-encoded transaction ID (display order)
104
- # @return [String] 32-byte transaction ID in internal byte order
105
- def self.txid_from_hex(hex)
106
- [hex].pack('H*').reverse
106
+ # @return [String] 32-byte transaction ID in wire byte order
107
+ def self.wtxid_from_hex(hex)
108
+ BSV::Primitives::Hex.validate_dtxid_hex!(hex, name: 'wtxid_from_hex input')
109
+ wtxid = [hex].pack('H*').reverse
110
+ BSV.logger&.debug { "[TransactionInput] wtxid_from_hex: #{hex} -> #{wtxid.bytesize}B wire-order" }
111
+ wtxid
107
112
  end
108
113
 
109
- # Serialise the outpoint (prev_tx_id + output index) as binary.
114
+ # Serialise the outpoint (prev_wtxid + output index) as binary.
110
115
  #
111
116
  # @return [String] 36-byte outpoint
112
117
  def outpoint_binary
113
- @prev_tx_id + [@prev_tx_out_index].pack('V')
118
+ @prev_wtxid + [@prev_tx_out_index].pack('V')
114
119
  end
115
120
 
116
- # The previous transaction ID in hex (display order).
121
+ # The previous transaction ID in display-order hex.
117
122
  #
118
- # @return [String] hex-encoded transaction ID
119
- def txid_hex
120
- @prev_tx_id.reverse.unpack1('H*')
123
+ # @return [String] hex-encoded transaction ID (display order)
124
+ def dtxid_hex
125
+ @prev_wtxid.reverse.unpack1('H*')
121
126
  end
122
127
  end
123
128
  end
data/lib/bsv/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BSV
4
- VERSION = '0.16.0'
4
+ VERSION = '0.18.0'
5
5
  end
@@ -38,10 +38,12 @@ module BSV
38
38
  # - :basket [String] optional basket name for UTXO tracking
39
39
  # - :custom_instructions [String] application-specific context
40
40
  # - :tags [Array<String>] output tags for filtering
41
- # @return [Hash] :txid, :tx, :no_send_change, :send_with_results, :signable_transaction
41
+ # @return [Hash] BRC-100 spec-mandated keys: :txid (display-order hex), :tx, :no_send_change,
42
+ # :send_with_results, :signable_transaction
42
43
  def create_action(description:, input_beef: nil, inputs: nil, outputs: nil,
43
44
  lock_time: nil, version: nil, labels: nil,
44
45
  sign_and_process: true, accept_delayed_broadcast: true,
46
+ # BRC-100 spec-mandated parameter names — display-order hex txids
45
47
  trust_self: nil, known_txids: nil, return_txid_only: false,
46
48
  no_send: false, no_send_change: nil, send_with: nil,
47
49
  randomize_outputs: true, originator: nil)
@@ -53,7 +55,8 @@ module BSV
53
55
  # @param spends [Hash{Integer => Hash}] input index => { unlocking_script:, sequence_number: }
54
56
  # @param reference [String] reference returned by {#create_action}
55
57
  def sign_action(spends:, reference:,
56
- accept_delayed_broadcast: true, return_txid_only: false,
58
+ accept_delayed_broadcast: true,
59
+ return_txid_only: false, # BRC-100 spec-mandated parameter name
57
60
  no_send: false, send_with: nil, originator: nil)
58
61
  raise NotImplementedError
59
62
  end
@@ -45,6 +45,7 @@ module BSV
45
45
  Validators.validate_protocol_id!(protocol_id)
46
46
  Validators.validate_key_id!(key_id)
47
47
  invoice = compute_invoice_number(protocol_id, key_id)
48
+ BSV.logger&.debug { "[KeyDeriver] derive_public_key: invoice=#{invoice.inspect} for_self=#{for_self}" }
48
49
  counterparty_pub = resolve_counterparty(counterparty)
49
50
 
50
51
  if for_self
@@ -64,6 +65,7 @@ module BSV
64
65
  Validators.validate_protocol_id!(protocol_id)
65
66
  Validators.validate_key_id!(key_id)
66
67
  invoice = compute_invoice_number(protocol_id, key_id)
68
+ BSV.logger&.debug { "[KeyDeriver] derive_private_key: invoice=#{invoice.inspect}" }
67
69
  counterparty_pub = resolve_counterparty(counterparty)
68
70
  @root_key.derive_child(counterparty_pub, invoice)
69
71
  end
@@ -127,6 +127,7 @@ module BSV
127
127
  counterparty: nil, privileged: false, privileged_reason: nil,
128
128
  seek_permission: true, originator: nil)
129
129
  counterparty ||= 'anyone'
130
+ BSV.logger&.debug { "[ProtoWallet] create_signature: protocol=#{protocol_id} key_id=#{key_id.inspect} counterparty=#{counterparty}" }
130
131
  priv_key = @key_deriver.derive_private_key(protocol_id, key_id, counterparty)
131
132
 
132
133
  hash = if hash_to_directly_sign
@@ -156,6 +157,10 @@ module BSV
156
157
  privileged: false, privileged_reason: nil,
157
158
  seek_permission: true, originator: nil)
158
159
  counterparty ||= 'self'
160
+ BSV.logger&.debug do
161
+ "[ProtoWallet] verify_signature: protocol=#{protocol_id} key_id=#{key_id.inspect} " \
162
+ "counterparty=#{counterparty} for_self=#{for_self}"
163
+ end
159
164
 
160
165
  pub_key = @key_deriver.derive_public_key(
161
166
  protocol_id, key_id, counterparty, for_self: for_self
@@ -169,6 +174,7 @@ module BSV
169
174
 
170
175
  sig = BSV::Primitives::Signature.from_der(bytes_to_string(signature))
171
176
  valid = pub_key.verify(hash, sig)
177
+ BSV.logger&.debug { "[ProtoWallet] verify_signature result=#{valid}" }
172
178
 
173
179
  raise InvalidSignatureError unless valid
174
180