bsv-sdk 0.16.0 → 0.17.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.
@@ -329,6 +329,11 @@ 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
@@ -370,11 +375,11 @@ module BSV
370
375
  # or nil if the BEEF is empty or contains no raw transaction entries
371
376
  def self.from_beef(data)
372
377
  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
378
+ subject_wtxid = beef.subject_wtxid ||
379
+ beef.transactions.reverse.find(&:transaction)&.transaction&.wtxid
380
+ return nil unless subject_wtxid
376
381
 
377
- beef.find_atomic_transaction(subject_txid)
382
+ beef.find_atomic_transaction(subject_wtxid)
378
383
  end
379
384
 
380
385
  # Parse a BEEF hex string and return the subject transaction.
@@ -388,15 +393,26 @@ module BSV
388
393
 
389
394
  # --- Transaction ID ---
390
395
 
391
- # Compute the transaction ID (double-SHA-256 of the serialised tx, byte-reversed).
396
+ # Wire-order transaction ID (raw SHA-256d of the serialised tx).
397
+ #
398
+ # Used by BEEF, BUMPs, and merkle paths, which all work in wire byte order
399
+ # to match {TransactionInput#prev_wtxid}.
400
+ #
401
+ # @return [String] 32-byte transaction ID in wire byte order
402
+ def wtxid
403
+ id = BSV::Primitives::Digest.sha256d(to_binary)
404
+ BSV.logger&.debug { "[Transaction] wtxid computed (dtxid=#{id.reverse.unpack1('H*')})" }
405
+ id
406
+ end
407
+
408
+ # Display-order transaction ID (reversed from the natural SHA-256d hash).
392
409
  #
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.
410
+ # This is the conventional human-readable representation used in block
411
+ # explorers, wallets, and all user-facing contexts.
396
412
  #
397
413
  # @return [String] 32-byte transaction ID in display byte order
398
414
  def txid
399
- BSV::Primitives::Digest.sha256d(to_binary).reverse
415
+ wtxid.reverse
400
416
  end
401
417
 
402
418
  # The transaction ID as a hex string (display byte order).
@@ -406,6 +422,19 @@ module BSV
406
422
  txid.unpack1('H*')
407
423
  end
408
424
 
425
+ # Display-order transaction ID as a hex string.
426
+ #
427
+ # Mirrors the wallet gem's +DisplayTxid+ pattern. +dtxid+ always returns
428
+ # a 64-char hex string suitable for JSON and UI boundaries.
429
+ #
430
+ # @return [String] hex-encoded transaction ID (display order)
431
+ alias dtxid txid_hex
432
+
433
+ # Display-order transaction ID as a hex string (alias for {#dtxid}).
434
+ #
435
+ # @return [String] hex-encoded transaction ID (display order)
436
+ alias dtxid_hex txid_hex
437
+
409
438
  # --- Sighash (BIP-143 with FORKID) ---
410
439
 
411
440
  # Build the BIP-143 sighash preimage for an input.
@@ -456,6 +485,19 @@ module BSV
456
485
  # 10. sighash type (4 LE) — includes FORKID flag
457
486
  buf << [sighash_type].pack('V')
458
487
 
488
+ BSV.logger&.debug do
489
+ hp = buf.byteslice(4, 32).unpack1('H*')
490
+ hs = buf.byteslice(36, 32).unpack1('H*')
491
+ op = input.outpoint_binary.unpack1('H*')
492
+ sc = script_bytes.unpack1('H*')
493
+ ho = buf.byteslice(-40, 32).unpack1('H*')
494
+ "[Sighash] input=#{input_index} type=0x#{format('%02x', sighash_type)} " \
495
+ "version=#{@version} hashPrevouts=#{hp} hashSequence=#{hs} " \
496
+ "outpoint=#{op} scriptCode=#{sc[0, 40]}#{'...' if sc.length > 40} " \
497
+ "value=#{input.source_satoshis} seq=#{input.sequence} " \
498
+ "hashOutputs=#{ho} locktime=#{@lock_time}"
499
+ end
500
+
459
501
  buf
460
502
  end
461
503
 
@@ -466,7 +508,9 @@ module BSV
466
508
  # @param subscript [Script::Script, nil] override locking script for the input
467
509
  # @return [String] 32-byte sighash digest
468
510
  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))
511
+ digest = BSV::Primitives::Digest.sha256d(sighash_preimage(input_index, sighash_type, subscript: subscript))
512
+ BSV.logger&.debug { "[Sighash] digest=#{digest.unpack1('H*')}" }
513
+ digest
470
514
  end
471
515
 
472
516
  # --- Signing ---
@@ -580,17 +624,17 @@ module BSV
580
624
 
581
625
  until queue.empty?
582
626
  tx = queue.shift
583
- tx_id = tx.txid_hex
584
- next if verified[tx_id]
627
+ wtxid = tx.wtxid
628
+ next if verified[wtxid]
585
629
 
586
630
  # Merkle path short-circuit: proven transaction needs no input verification
587
631
  if tx.merkle_path
588
- unless tx.merkle_path.verify(tx_id, chain_tracker)
632
+ unless tx.merkle_path.verify(tx.txid_hex, chain_tracker)
589
633
  raise VerificationError.new(:invalid_merkle_proof,
590
- "invalid merkle proof for transaction #{tx_id}")
634
+ "invalid merkle proof for transaction #{tx.txid_hex}")
591
635
  end
592
636
 
593
- verified[tx_id] = true
637
+ verified[wtxid] = true
594
638
  next
595
639
  end
596
640
 
@@ -622,13 +666,13 @@ module BSV
622
666
 
623
667
  # Enqueue source transaction for verification if not yet verified
624
668
  source_tx = input.source_transaction
625
- queue << source_tx if source_tx && !verified[source_tx.txid_hex]
669
+ queue << source_tx if source_tx && !verified[source_tx.wtxid]
626
670
  end
627
671
 
628
672
  # Output ≤ input check
629
673
  verify_output_constraint(tx)
630
674
 
631
- verified[tx_id] = true
675
+ verified[wtxid] = true
632
676
  end
633
677
 
634
678
  true
@@ -777,19 +821,19 @@ module BSV
777
821
  end
778
822
 
779
823
  def verify_input_requirements(tx, input, index)
780
- tx_id = tx.txid_hex
824
+ dtxid_hex = tx.txid_hex
781
825
  if input.unlocking_script.nil?
782
826
  raise VerificationError.new(:missing_source,
783
- "input #{index} of transaction #{tx_id} has no unlocking script")
827
+ "input #{index} of transaction #{dtxid_hex} has no unlocking script")
784
828
  end
785
829
  if input.source_locking_script.nil?
786
830
  raise VerificationError.new(:missing_source,
787
- "input #{index} of transaction #{tx_id} has no source locking script")
831
+ "input #{index} of transaction #{dtxid_hex} has no source locking script")
788
832
  end
789
833
  return unless input.source_satoshis.nil?
790
834
 
791
835
  raise VerificationError.new(:missing_source,
792
- "input #{index} of transaction #{tx_id} has no source satoshis")
836
+ "input #{index} of transaction #{dtxid_hex} has no source satoshis")
793
837
  end
794
838
 
795
839
  def verify_fee(fee_model)
@@ -843,7 +887,7 @@ module BSV
843
887
 
844
888
  # Collect this transaction and all its ancestors in dependency order
845
889
  # (ancestors first, self last). Stops recursion at transactions with
846
- # a merkle_path (proven leaves). Deduplicates by txid.
890
+ # a merkle_path (proven leaves). Deduplicates by wtxid (wire-order bytes).
847
891
  def collect_ancestors
848
892
  seen = {}
849
893
  result = []
@@ -852,18 +896,25 @@ module BSV
852
896
  end
853
897
 
854
898
  def collect_ancestors_recursive(tx, seen, result)
855
- txid = tx.txid
856
- return if seen.key?(txid)
899
+ wtxid = tx.wtxid
900
+ return if seen.key?(wtxid)
857
901
 
858
- unless tx.merkle_path
859
- tx.inputs.each do |input|
860
- next unless input.source_transaction
902
+ if tx.merkle_path
903
+ BSV.logger&.debug do
904
+ "[Transaction] ancestor: #{tx.dtxid_hex} proven at height #{tx.merkle_path.block_height} (leaf stop)"
905
+ end
906
+ else
907
+ tx.inputs.each_with_index do |input, idx|
908
+ unless input.source_transaction
909
+ BSV.logger&.debug { "[Transaction] ancestor: #{tx.dtxid_hex} input #{idx} has no source_transaction (skipped)" }
910
+ next
911
+ end
861
912
 
862
913
  collect_ancestors_recursive(input.source_transaction, seen, result)
863
914
  end
864
915
  end
865
916
 
866
- seen[txid] = true
917
+ seen[wtxid] = true
867
918
  result << tx
868
919
  end
869
920
 
@@ -886,8 +937,8 @@ module BSV
886
937
  merged = txs.first.merkle_path.dup
887
938
  txs.drop(1).each { |t| merged.combine(t.merkle_path) }
888
939
 
889
- txid_hashes = txs.map { |t| t.txid.reverse }
890
- clean = merged.extract(txid_hashes)
940
+ wtxid_hashes = txs.map(&:wtxid)
941
+ clean = merged.extract(wtxid_hashes)
891
942
 
892
943
  bump_index_by_height[height] = beef.bumps.length
893
944
  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.17.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
 
data/lib/bsv-sdk.rb CHANGED
@@ -3,6 +3,20 @@
3
3
  require_relative 'bsv/version'
4
4
 
5
5
  module BSV
6
+ class << self
7
+ # Optional logger for debug-level instrumentation of txid conversions,
8
+ # BEEF wiring, and merkle path operations.
9
+ #
10
+ # No logger is configured by default — zero overhead when unused.
11
+ # Consumers opt in via:
12
+ #
13
+ # require 'logger'
14
+ # BSV.logger = Logger.new($stdout).tap { |l| l.level = Logger::DEBUG }
15
+ #
16
+ # @return [Logger, nil]
17
+ attr_accessor :logger
18
+ end
19
+
6
20
  autoload :Primitives, 'bsv/primitives'
7
21
  autoload :Script, 'bsv/script'
8
22
  autoload :Transaction, 'bsv/transaction'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bsv-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.16.0
4
+ version: 0.17.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Simon Bettison