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
@@ -13,7 +13,7 @@ module BSV
13
13
  #
14
14
  # @example Parse a BEEF bundle and find a transaction
15
15
  # beef = BSV::Transaction::Beef.from_hex(beef_hex)
16
- # tx = beef.find_transaction(txid_bytes)
16
+ # tx = beef.find_transaction(wtxid)
17
17
  class Beef
18
18
  # @!group Version constants
19
19
 
@@ -36,44 +36,119 @@ module BSV
36
36
 
37
37
  # @!endgroup
38
38
 
39
- # A single entry in a BEEF bundle, wrapping a transaction with its format metadata.
39
+ # Abstract base class for a single entry in a BEEF bundle.
40
+ #
41
+ # Subclasses represent the three wire formats:
42
+ # - {RawTxEntry} — raw transaction without a merkle proof
43
+ # - {ProvenTxEntry} — raw transaction with an associated BUMP index
44
+ # - {TxidOnlyEntry} — transaction ID only (no raw data)
45
+ #
46
+ # @abstract Subclass and implement {#wtxid} and {#format_flag}.
40
47
  class BeefTx
41
- # @return [Integer] format flag (FORMAT_RAW_TX, FORMAT_RAW_TX_AND_BUMP, or FORMAT_TXID_ONLY)
42
- attr_reader :format
48
+ def initialize; end
49
+
50
+ # Wire-order transaction ID.
51
+ # @return [String, nil] 32-byte wtxid
52
+ # @abstract
53
+ def wtxid
54
+ raise NotImplementedError, "#{self.class}#wtxid is not implemented"
55
+ end
56
+
57
+ # Display-order transaction ID as a hex string.
58
+ #
59
+ # +dtxid+ always returns a 64-char hex string suitable for JSON
60
+ # and UI boundaries.
61
+ #
62
+ # @return [String, nil] hex-encoded transaction ID (display order)
63
+ def dtxid
64
+ wtxid&.reverse&.unpack1('H*')
65
+ end
66
+
67
+ # Wire-protocol format integer for serialisation.
68
+ # @return [Integer]
69
+ # @abstract
70
+ def format_flag
71
+ raise NotImplementedError, "#{self.class}#format_flag is not implemented"
72
+ end
73
+ end
43
74
 
44
- # @return [Transaction, nil] the transaction (nil for TXID-only entries)
75
+ # A BEEF entry containing a raw transaction without a merkle proof.
76
+ class RawTxEntry < BeefTx
77
+ # @return [Transaction] the transaction
45
78
  attr_reader :transaction
46
79
 
47
- # @return [String, nil] 32-byte txid for TXID-only entries
48
- attr_reader :known_txid
80
+ # @param transaction [Transaction] the transaction
81
+ # @raise [ArgumentError] if transaction is nil
82
+ def initialize(transaction:)
83
+ raise ArgumentError, 'RawTxEntry requires a transaction' if transaction.nil?
84
+
85
+ super()
86
+ @transaction = transaction
87
+ end
88
+
89
+ # @return [String] wire-order wtxid delegated to the transaction
90
+ def wtxid
91
+ @transaction.wtxid
92
+ end
93
+
94
+ # @return [Integer] FORMAT_RAW_TX wire-protocol flag
95
+ def format_flag
96
+ FORMAT_RAW_TX
97
+ end
98
+ end
99
+
100
+ # A BEEF entry containing a raw transaction with an associated BUMP index.
101
+ class ProvenTxEntry < BeefTx
102
+ # @return [Transaction] the transaction
103
+ attr_reader :transaction
49
104
 
50
- # @return [Integer, nil] index into the BEEF bumps array
105
+ # @return [Integer] index into the BEEF bumps array
51
106
  attr_reader :bump_index
52
107
 
53
- # @param format [Integer] format flag
54
- # @param transaction [Transaction, nil] the transaction
55
- # @param known_txid [String, nil] 32-byte txid for TXID-only entries
56
- # @param bump_index [Integer, nil] index into the bumps array
57
- # @raise [ArgumentError] if format is FORMAT_RAW_TX_AND_BUMP without a bump_index
58
- def initialize(format:, transaction: nil, known_txid: nil, bump_index: nil)
59
- raise ArgumentError, 'FORMAT_RAW_TX_AND_BUMP requires a bump_index' if format == FORMAT_RAW_TX_AND_BUMP && bump_index.nil?
108
+ # @param transaction [Transaction] the transaction
109
+ # @param bump_index [Integer] index into the bumps array
110
+ # @raise [ArgumentError] if transaction or bump_index is nil
111
+ def initialize(transaction:, bump_index:)
112
+ raise ArgumentError, 'ProvenTxEntry requires a transaction' if transaction.nil?
113
+ raise ArgumentError, 'ProvenTxEntry requires a bump_index' if bump_index.nil?
60
114
 
61
- @format = format
115
+ super()
62
116
  @transaction = transaction
63
- @known_txid = known_txid
64
117
  @bump_index = bump_index
65
118
  end
66
119
 
67
- # The transaction ID for this entry.
68
- #
69
- # @return [String, nil] 32-byte txid in display byte order
70
- def txid
71
- case @format
72
- when FORMAT_TXID_ONLY
73
- @known_txid
74
- else
75
- @transaction&.txid
76
- end
120
+ # @return [String] wire-order wtxid delegated to the transaction
121
+ def wtxid
122
+ @transaction.wtxid
123
+ end
124
+
125
+ # @return [Integer] FORMAT_RAW_TX_AND_BUMP wire-protocol flag
126
+ def format_flag
127
+ FORMAT_RAW_TX_AND_BUMP
128
+ end
129
+ end
130
+
131
+ # A BEEF entry containing only a transaction ID (no raw data).
132
+ class TxidOnlyEntry < BeefTx
133
+ # @return [String] 32-byte wire-order wtxid
134
+ attr_reader :known_wtxid
135
+
136
+ # @param known_wtxid [String] 32-byte wire-order wtxid
137
+ # @raise [ArgumentError] if known_wtxid is invalid
138
+ def initialize(known_wtxid:)
139
+ BSV::Primitives::Hex.validate_wtxid!(known_wtxid, name: 'known_wtxid')
140
+ super()
141
+ @known_wtxid = known_wtxid
142
+ end
143
+
144
+ # @return [String] the stored wire-order wtxid
145
+ def wtxid
146
+ @known_wtxid
147
+ end
148
+
149
+ # @return [Integer] FORMAT_TXID_ONLY wire-protocol flag
150
+ def format_flag
151
+ FORMAT_TXID_ONLY
77
152
  end
78
153
  end
79
154
 
@@ -86,8 +161,15 @@ module BSV
86
161
  # @return [Array<BeefTx>] the transactions in dependency order
87
162
  attr_reader :transactions
88
163
 
89
- # @return [String, nil] 32-byte subject txid (Atomic BEEF only)
90
- attr_reader :subject_txid
164
+ # @return [String, nil] 32-byte wire-order subject txid (Atomic BEEF only)
165
+ attr_reader :subject_wtxid
166
+
167
+ # Display-order subject txid as a hex string (Atomic BEEF only).
168
+ #
169
+ # @return [String, nil] hex-encoded display-order txid, or nil
170
+ def subject_dtxid
171
+ @subject_wtxid&.reverse&.unpack1('H*')
172
+ end
91
173
 
92
174
  # @param version [Integer] BEEF version constant (default: BEEF_V1, matching to_binary's
93
175
  # default for ARC compatibility; from_binary overwrites this with the parsed version)
@@ -97,7 +179,7 @@ module BSV
97
179
  @version = version
98
180
  @bumps = bumps
99
181
  @transactions = transactions
100
- @subject_txid = nil
182
+ @subject_wtxid = nil
101
183
  end
102
184
 
103
185
  # --- Deserialisation ---
@@ -133,9 +215,9 @@ module BSV
133
215
  raise ArgumentError, "truncated Atomic BEEF: need 36 bytes at offset #{offset}, got #{remaining}"
134
216
  end
135
217
 
136
- # Atomic BEEF stores the subject txid in internal byte order (little-endian
137
- # hash order), matching JS and Go SDKs. Reverse to display order for internal use.
138
- beef.instance_variable_set(:@subject_txid, data.byteslice(offset, 32).reverse)
218
+ # Atomic BEEF stores the subject txid in wire (internal / little-endian) byte order,
219
+ # matching JS and Go SDKs. Store as-is in @subject_wtxid (wire-order).
220
+ beef.instance_variable_set(:@subject_wtxid, data.byteslice(offset, 32))
139
221
  offset += 32
140
222
  inner_version = data.byteslice(offset, 4).unpack1('V')
141
223
  offset += 4
@@ -185,7 +267,7 @@ module BSV
185
267
  # any FORMAT_TXID_ONLY entries (V1 / BRC-62 has no TXID-only format;
186
268
  # pass +version: BEEF_V2+ to serialise such bundles)
187
269
  def to_binary(version: BEEF_V1)
188
- if version == BEEF_V1 && @transactions.any? { |bt| bt.format == FORMAT_TXID_ONLY }
270
+ if version == BEEF_V1 && @transactions.any?(TxidOnlyEntry)
189
271
  raise ArgumentError,
190
272
  'BEEF V1 (BRC-62) does not support FORMAT_TXID_ONLY entries; pass version: BEEF_V2 to serialise this bundle'
191
273
  end
@@ -223,13 +305,13 @@ module BSV
223
305
 
224
306
  # Serialise as Atomic BEEF (BRC-95), wrapping V2 data with a subject txid.
225
307
  #
226
- # @param subject_txid [String] 32-byte subject transaction ID
308
+ # @param subject_wtxid [String] 32-byte wire-order subject transaction ID
227
309
  # @return [String] raw Atomic BEEF binary
228
- def to_atomic_binary(subject_txid)
310
+ def to_atomic_binary(subject_wtxid)
311
+ BSV::Primitives::Hex.validate_wtxid!(subject_wtxid, name: 'subject_wtxid')
229
312
  buf = [ATOMIC_BEEF].pack('V')
230
- # Write subject txid in internal byte order (reverse of display order),
231
- # matching JS and Go SDK conventions for Bitcoin binary formats.
232
- buf << subject_txid.b.reverse
313
+ # subject_wtxid is already in wire (internal) byte order write as-is.
314
+ buf << subject_wtxid.b
233
315
  # BRC-95: inner envelope is always V2
234
316
  buf << to_binary(version: BEEF_V2)
235
317
  buf
@@ -237,42 +319,46 @@ module BSV
237
319
 
238
320
  # --- Lookup ---
239
321
 
240
- # Find a transaction in the bundle by its transaction ID.
322
+ # Find a transaction in the bundle by its wire-order transaction ID.
241
323
  #
242
- # @param txid [String] 32-byte txid in display byte order
324
+ # @param wtxid [String] 32-byte wire-order wtxid
243
325
  # @return [Transaction, nil] the matching transaction, or nil
244
- def find_transaction(txid)
326
+ def find_transaction(wtxid)
327
+ BSV::Primitives::Hex.validate_wtxid!(wtxid, name: 'wtxid')
328
+ BSV.logger&.debug { "[Beef] find_transaction: #{wtxid.reverse.unpack1('H*')} in #{@transactions.length} entries" }
245
329
  @transactions.each do |beef_tx|
246
- return beef_tx.transaction if beef_tx.transaction&.txid == txid
330
+ next if beef_tx.is_a?(TxidOnlyEntry)
331
+ return beef_tx.transaction if beef_tx.wtxid == wtxid
247
332
  end
248
333
  nil
249
334
  end
250
335
 
251
- # Find the merkle path (BUMP) for a transaction by its txid.
336
+ # Find the merkle path (BUMP) for a transaction by its wire-order txid.
252
337
  #
253
338
  # First checks the transaction-table entries, then scans @bumps directly
254
- # for a BUMP whose level-0 leaves contain the txid.
339
+ # for a BUMP whose level-0 leaves contain the wtxid.
255
340
  #
256
- # @param txid [String] 32-byte txid in display byte order
341
+ # @param wtxid [String] 32-byte wire-order wtxid
257
342
  # @return [MerklePath, nil] the merkle path, or nil if not found
258
- def find_bump(txid)
343
+ def find_bump(wtxid)
344
+ BSV::Primitives::Hex.validate_wtxid!(wtxid, name: 'wtxid')
259
345
  # Check transaction-table entries first (fast path)
260
- bt = @transactions.find { |entry| entry.txid == txid && entry.format == FORMAT_RAW_TX_AND_BUMP }
261
- return bt.transaction&.merkle_path || (bt.bump_index && @bumps[bt.bump_index]) if bt
346
+ bt = @transactions.find { |entry| entry.wtxid == wtxid && entry.is_a?(ProvenTxEntry) }
347
+ return bt.transaction.merkle_path || @bumps[bt.bump_index] if bt
262
348
 
263
- # F5.8: also scan @bumps directly for a path containing the txid leaf
264
- txid_internal = txid.reverse
349
+ # F5.8: also scan @bumps directly for a path containing the wtxid leaf
265
350
  @bumps.find do |bump|
266
- bump.path[0]&.any? { |leaf| leaf.hash == txid_internal }
351
+ bump.path[0]&.any? { |leaf| leaf.hash == wtxid }
267
352
  end
268
353
  end
269
354
 
270
355
  # Find a transaction with all source_transactions wired for signing.
271
356
  #
272
- # @param txid [String] 32-byte txid in display byte order
357
+ # @param wtxid [String] 32-byte wire-order wtxid
273
358
  # @return [Transaction, nil] the transaction with wired inputs, or nil
274
- def find_transaction_for_signing(txid)
275
- tx = find_transaction(txid)
359
+ def find_transaction_for_signing(wtxid)
360
+ BSV::Primitives::Hex.validate_wtxid!(wtxid, name: 'wtxid')
361
+ tx = find_transaction(wtxid)
276
362
  return unless tx
277
363
 
278
364
  wire_inputs(tx)
@@ -282,10 +368,11 @@ module BSV
282
368
  # Find a transaction and recursively wire its ancestry (source transactions
283
369
  # and merkle paths) for atomic proof validation.
284
370
  #
285
- # @param txid [String] 32-byte txid in display byte order
371
+ # @param wtxid [String] 32-byte wire-order wtxid
286
372
  # @return [Transaction, nil] the transaction with full proof tree, or nil
287
- def find_atomic_transaction(txid)
288
- tx = find_transaction(txid)
373
+ def find_atomic_transaction(wtxid)
374
+ BSV::Primitives::Hex.validate_wtxid!(wtxid, name: 'wtxid')
375
+ tx = find_transaction(wtxid)
289
376
  return unless tx
290
377
 
291
378
  wire_ancestry(tx)
@@ -294,10 +381,10 @@ module BSV
294
381
 
295
382
  # Serialise as Atomic BEEF (BRC-95) hex string.
296
383
  #
297
- # @param subject_txid [String] 32-byte subject transaction ID
384
+ # @param subject_wtxid [String] 32-byte wire-order subject transaction ID
298
385
  # @return [String] hex-encoded Atomic BEEF
299
- def to_atomic_hex(subject_txid)
300
- to_atomic_binary(subject_txid).unpack1('H*')
386
+ def to_atomic_hex(subject_wtxid)
387
+ to_atomic_binary(subject_wtxid).unpack1('H*')
301
388
  end
302
389
 
303
390
  # --- Merge operations ---
@@ -338,15 +425,11 @@ module BSV
338
425
  level0_leaves = bump.path[0] || []
339
426
  level0_internal = level0_leaves.map(&:hash).compact.to_set
340
427
  @transactions.each_with_index do |bt, i|
341
- next unless bt.format == FORMAT_RAW_TX && bt.transaction
342
- next unless level0_internal.include?(bt.transaction.txid.reverse)
428
+ next unless bt.is_a?(RawTxEntry)
429
+ next unless level0_internal.include?(bt.wtxid)
343
430
 
344
431
  bt.transaction.merkle_path ||= bump
345
- @transactions[i] = BeefTx.new(
346
- format: FORMAT_RAW_TX_AND_BUMP,
347
- transaction: bt.transaction,
348
- bump_index: idx
349
- )
432
+ @transactions[i] = ProvenTxEntry.new(transaction: bt.transaction, bump_index: idx)
350
433
  end
351
434
 
352
435
  idx
@@ -362,10 +445,10 @@ module BSV
362
445
  # @param tx [Transaction] the transaction to merge
363
446
  # @return [BeefTx] the (possibly existing or upgraded) BeefTx entry
364
447
  def merge_transaction(tx)
365
- txid = tx.txid
448
+ wtxid = tx.wtxid
366
449
 
367
450
  # Check for existing entry and upgrade if a stronger format is available
368
- existing_idx = @transactions.index { |bt| bt.txid == txid }
451
+ existing_idx = @transactions.index { |bt| bt.wtxid == wtxid }
369
452
  if existing_idx
370
453
  existing = @transactions[existing_idx]
371
454
  upgraded = upgrade_beef_tx(existing, tx)
@@ -381,9 +464,9 @@ module BSV
381
464
  # Merge this transaction's BUMP if it has one
382
465
  entry = if tx.merkle_path
383
466
  bump_idx = merge_bump(tx.merkle_path)
384
- BeefTx.new(format: FORMAT_RAW_TX_AND_BUMP, transaction: tx, bump_index: bump_idx)
467
+ ProvenTxEntry.new(transaction: tx, bump_index: bump_idx)
385
468
  else
386
- BeefTx.new(format: FORMAT_RAW_TX, transaction: tx)
469
+ RawTxEntry.new(transaction: tx)
387
470
  end
388
471
  @transactions << entry
389
472
  entry
@@ -409,7 +492,7 @@ module BSV
409
492
  tx.merkle_path = @bumps[bump_index]
410
493
  end
411
494
 
412
- existing_idx = @transactions.index { |bt| bt.txid == tx.txid }
495
+ existing_idx = @transactions.index { |bt| bt.wtxid == tx.wtxid }
413
496
  if existing_idx
414
497
  existing = @transactions[existing_idx]
415
498
  upgraded = upgrade_beef_tx(existing, tx, bump_index: bump_index)
@@ -418,9 +501,9 @@ module BSV
418
501
  end
419
502
 
420
503
  entry = if bump_index
421
- BeefTx.new(format: FORMAT_RAW_TX_AND_BUMP, transaction: tx, bump_index: bump_index)
504
+ ProvenTxEntry.new(transaction: tx, bump_index: bump_index)
422
505
  else
423
- BeefTx.new(format: FORMAT_RAW_TX, transaction: tx)
506
+ RawTxEntry.new(transaction: tx)
424
507
  end
425
508
  @transactions << entry
426
509
  entry
@@ -446,15 +529,15 @@ module BSV
446
529
  # Merge transactions with remapped BUMP indices, constructing new
447
530
  # BeefTx instances rather than sharing source references (F5.9).
448
531
  other.transactions.each do |beef_tx|
449
- case beef_tx.format
450
- when FORMAT_TXID_ONLY
451
- next if @transactions.any? { |bt| bt.txid == beef_tx.known_txid }
532
+ case beef_tx
533
+ when TxidOnlyEntry
534
+ next if @transactions.any? { |bt| bt.wtxid == beef_tx.known_wtxid }
452
535
 
453
- @transactions << BeefTx.new(format: FORMAT_TXID_ONLY, known_txid: beef_tx.known_txid)
536
+ @transactions << TxidOnlyEntry.new(known_wtxid: beef_tx.known_wtxid)
454
537
  else
455
- next if @transactions.any? { |bt| bt.txid == beef_tx.txid }
538
+ next if @transactions.any? { |bt| bt.wtxid == beef_tx.wtxid }
456
539
 
457
- if beef_tx.format == FORMAT_RAW_TX_AND_BUMP && beef_tx.bump_index
540
+ if beef_tx.is_a?(ProvenTxEntry) && beef_tx.bump_index
458
541
  new_idx = bump_remap[beef_tx.bump_index]
459
542
  if new_idx.nil?
460
543
  raise ArgumentError,
@@ -466,13 +549,9 @@ module BSV
466
549
  # so mutations to the merged bundle don't affect the source.
467
550
  tx = beef_tx.transaction.dup
468
551
  tx.merkle_path = @bumps[new_idx]
469
- @transactions << BeefTx.new(
470
- format: FORMAT_RAW_TX_AND_BUMP,
471
- transaction: tx,
472
- bump_index: new_idx
473
- )
552
+ @transactions << ProvenTxEntry.new(transaction: tx, bump_index: new_idx)
474
553
  else
475
- @transactions << BeefTx.new(format: FORMAT_RAW_TX, transaction: beef_tx.transaction.dup)
554
+ @transactions << RawTxEntry.new(transaction: beef_tx.transaction.dup)
476
555
  end
477
556
  end
478
557
  end
@@ -482,13 +561,14 @@ module BSV
482
561
 
483
562
  # Convert a transaction entry to TXID-only format.
484
563
  #
485
- # @param txid [String] 32-byte txid in display byte order
564
+ # @param wtxid [String] 32-byte wire-order wtxid
486
565
  # @return [BeefTx, nil] the converted entry, or nil if not found
487
- def make_txid_only(txid)
488
- idx = @transactions.index { |bt| bt.txid == txid }
566
+ def make_txid_only(wtxid)
567
+ BSV::Primitives::Hex.validate_wtxid!(wtxid, name: 'wtxid')
568
+ idx = @transactions.index { |bt| bt.wtxid == wtxid }
489
569
  return unless idx
490
570
 
491
- @transactions[idx] = BeefTx.new(format: FORMAT_TXID_ONLY, known_txid: txid)
571
+ @transactions[idx] = TxidOnlyEntry.new(known_wtxid: wtxid)
492
572
  end
493
573
 
494
574
  # --- Validation ---
@@ -507,28 +587,28 @@ module BSV
507
587
  # @return [Boolean] true if structurally valid
508
588
  def valid?(allow_txid_only: false)
509
589
  # TXID-only entries are invalid unless explicitly allowed
510
- has_txid_only = @transactions.any? { |bt| bt.format == FORMAT_TXID_ONLY }
590
+ has_txid_only = @transactions.any?(TxidOnlyEntry)
511
591
  return false if has_txid_only && !allow_txid_only
512
592
 
513
593
  # F5.4: verify BUMP linkage and computed root for each proven transaction
514
594
  @transactions.each do |bt|
515
- next unless bt.format == FORMAT_RAW_TX_AND_BUMP
595
+ next unless bt.is_a?(ProvenTxEntry)
516
596
 
517
597
  # Must have a BUMP
518
- bump = bt.transaction&.merkle_path || (bt.bump_index && @bumps[bt.bump_index])
598
+ bump = bt.transaction.merkle_path || @bumps[bt.bump_index]
519
599
  return false unless bump
520
600
 
521
601
  # The txid must appear as a leaf in the BUMP and compute a valid root
522
602
  begin
523
- bump.compute_root(bt.transaction.txid.reverse)
603
+ bump.compute_root(bt.wtxid)
524
604
  rescue ArgumentError
525
605
  return false
526
606
  end
527
607
  end
528
608
 
529
- known_txids = build_known_txids(allow_txid_only)
609
+ known_wtxids = build_known_wtxids(allow_txid_only)
530
610
 
531
- pending = @transactions.select { |bt| bt.transaction && !known_txids.include?(bt.txid) }
611
+ pending = @transactions.reject { |bt| bt.is_a?(TxidOnlyEntry) || known_wtxids.include?(bt.wtxid) }
532
612
 
533
613
  # Iteratively resolve: if all inputs of a tx are known, it becomes known
534
614
  changed = true
@@ -536,10 +616,10 @@ module BSV
536
616
  changed = false
537
617
  pending.reject! do |bt|
538
618
  all_inputs_known = bt.transaction.inputs.all? do |input|
539
- known_txids.include?(input.prev_tx_id.reverse)
619
+ known_wtxids.include?(input.prev_wtxid)
540
620
  end
541
621
  if all_inputs_known
542
- known_txids.add(bt.txid)
622
+ known_wtxids.add(bt.wtxid)
543
623
  changed = true
544
624
  end
545
625
  all_inputs_known
@@ -584,18 +664,18 @@ module BSV
584
664
  def sort_transactions!
585
665
  return self if @transactions.length <= 1
586
666
 
587
- txid_index = {}
588
- @transactions.each_with_index { |bt, i| txid_index[bt.txid] = i }
667
+ wtxid_index = {}
668
+ @transactions.each_with_index { |bt, i| wtxid_index[bt.wtxid] = i }
589
669
 
590
670
  # Build adjacency: for each tx, which other txs must come before it?
591
671
  in_degree = Array.new(@transactions.length, 0)
592
672
  dependents = Array.new(@transactions.length) { [] }
593
673
 
594
674
  @transactions.each_with_index do |bt, i|
595
- next unless bt.transaction
675
+ next if bt.is_a?(TxidOnlyEntry)
596
676
 
597
677
  bt.transaction.inputs.each do |input|
598
- dep_idx = txid_index[input.prev_tx_id.reverse]
678
+ dep_idx = wtxid_index[input.prev_wtxid]
599
679
  next unless dep_idx
600
680
 
601
681
  dependents[dep_idx] << i
@@ -618,8 +698,8 @@ module BSV
618
698
 
619
699
  # F5.5: preserve unsortable (cyclic) transactions rather than silently dropping them
620
700
  if sorted.length < @transactions.length
621
- sorted_set = sorted.to_set(&:txid)
622
- @txs_not_valid = @transactions.reject { |bt| sorted_set.include?(bt.txid) }
701
+ sorted_set = sorted.to_set(&:wtxid)
702
+ @txs_not_valid = @transactions.reject { |bt| sorted_set.include?(bt.wtxid) }
623
703
  end
624
704
 
625
705
  @transactions = sorted
@@ -661,27 +741,24 @@ module BSV
661
741
 
662
742
  case format
663
743
  when FORMAT_TXID_ONLY
664
- # Wire stores txid in internal (little-endian) byte order;
665
- # reverse to display order so BeefTx#txid is consistent with
666
- # Transaction#txid across all format types.
744
+ # Wire stores txid in internal (little-endian / wire) byte order;
745
+ # store as-is in known_wtxid so it matches Transaction#wtxid.
667
746
  raise ArgumentError, 'truncated BEEF: not enough bytes for TXID_ONLY entry' if offset + 32 > data.bytesize
668
747
 
669
- known_txid = data.byteslice(offset, 32).reverse
748
+ known_wtxid = data.byteslice(offset, 32)
670
749
  offset += 32
671
- beef.transactions << BeefTx.new(format: FORMAT_TXID_ONLY, known_txid: known_txid)
750
+ beef.transactions << TxidOnlyEntry.new(known_wtxid: known_wtxid)
672
751
  when FORMAT_RAW_TX_AND_BUMP
673
752
  bump_index, vi_size = VarInt.decode(data, offset)
674
753
  offset += vi_size
675
754
  tx, consumed = Transaction.from_binary_with_offset(data, offset)
676
755
  offset += consumed
677
756
  tx.merkle_path = beef.bumps[bump_index] if bump_index < beef.bumps.length
678
- beef.transactions << BeefTx.new(
679
- format: FORMAT_RAW_TX_AND_BUMP, transaction: tx, bump_index: bump_index
680
- )
757
+ beef.transactions << ProvenTxEntry.new(transaction: tx, bump_index: bump_index)
681
758
  when FORMAT_RAW_TX
682
759
  tx, consumed = Transaction.from_binary_with_offset(data, offset)
683
760
  offset += consumed
684
- beef.transactions << BeefTx.new(format: FORMAT_RAW_TX, transaction: tx)
761
+ beef.transactions << RawTxEntry.new(transaction: tx)
685
762
  end
686
763
  end
687
764
 
@@ -700,14 +777,12 @@ module BSV
700
777
  offset += 1
701
778
 
702
779
  if has_bump.zero?
703
- beef.transactions << BeefTx.new(format: FORMAT_RAW_TX, transaction: tx)
780
+ beef.transactions << RawTxEntry.new(transaction: tx)
704
781
  else
705
782
  bump_index, vi_size = VarInt.decode(data, offset)
706
783
  offset += vi_size
707
784
  tx.merkle_path = beef.bumps[bump_index] if bump_index < beef.bumps.length
708
- beef.transactions << BeefTx.new(
709
- format: FORMAT_RAW_TX_AND_BUMP, transaction: tx, bump_index: bump_index
710
- )
785
+ beef.transactions << ProvenTxEntry.new(transaction: tx, bump_index: bump_index)
711
786
  end
712
787
  end
713
788
 
@@ -717,16 +792,22 @@ module BSV
717
792
  def wire_source_transactions(beef)
718
793
  tx_map = {}
719
794
  beef.transactions.each do |beef_tx|
720
- next unless beef_tx.transaction
795
+ next if beef_tx.is_a?(TxidOnlyEntry)
721
796
 
722
- # Wire inputs to ancestors already in the map (BEEF is dependency-ordered)
797
+ # Wire inputs to ancestors already in the map (BEEF is dependency-ordered).
798
+ # Both prev_wtxid and wtxid are wire-order — no conversion needed.
723
799
  beef_tx.transaction.inputs.each do |input|
724
- # prev_tx_id is wire byte order; txid keys are display byte order (reversed)
725
- source = tx_map[input.prev_tx_id.reverse]
726
- input.source_transaction = source if source
800
+ source = tx_map[input.prev_wtxid]
801
+ next unless source
802
+
803
+ input.source_transaction = source
804
+ BSV.logger&.debug do
805
+ "[Beef] wired input #{input.prev_wtxid.reverse.unpack1('H*')}:#{input.prev_tx_out_index} " \
806
+ "-> source #{source.wtxid.reverse.unpack1('H*')}"
807
+ end
727
808
  end
728
809
 
729
- tx_map[beef_tx.transaction.txid] = beef_tx.transaction
810
+ tx_map[beef_tx.wtxid] = beef_tx.transaction
730
811
  end
731
812
  end
732
813
  end
@@ -751,33 +832,28 @@ module BSV
751
832
  effective_bump_idx = bump_index
752
833
  effective_bump_idx = merge_bump(tx.merkle_path) if effective_bump_idx.nil? && tx&.merkle_path
753
834
 
754
- case existing.format
755
- when FORMAT_TXID_ONLY
835
+ case existing
836
+ when TxidOnlyEntry
756
837
  if effective_bump_idx
757
- BeefTx.new(format: FORMAT_RAW_TX_AND_BUMP, transaction: tx, bump_index: effective_bump_idx)
838
+ ProvenTxEntry.new(transaction: tx, bump_index: effective_bump_idx)
758
839
  elsif tx
759
- BeefTx.new(format: FORMAT_RAW_TX, transaction: tx)
840
+ RawTxEntry.new(transaction: tx)
760
841
  end
761
- when FORMAT_RAW_TX
842
+ when RawTxEntry
762
843
  if effective_bump_idx
763
844
  tx_to_use = tx || existing.transaction
764
845
  tx_to_use.merkle_path ||= @bumps[effective_bump_idx]
765
- BeefTx.new(format: FORMAT_RAW_TX_AND_BUMP, transaction: tx_to_use, bump_index: effective_bump_idx)
846
+ ProvenTxEntry.new(transaction: tx_to_use, bump_index: effective_bump_idx)
766
847
  end
767
848
  end
768
- # FORMAT_RAW_TX_AND_BUMP is already the strongest — no upgrade needed
849
+ # ProvenTxEntry is already the strongest — no upgrade needed
769
850
  end
770
851
 
771
- # Build a set of txids that are "known" (proven or txid-only).
772
- def build_known_txids(allow_txid_only)
852
+ # Build a set of wire-order wtxids that are "known" (proven or txid-only).
853
+ def build_known_wtxids(allow_txid_only)
773
854
  known = Set.new
774
855
  @transactions.each do |bt|
775
- case bt.format
776
- when FORMAT_RAW_TX_AND_BUMP
777
- known.add(bt.txid)
778
- when FORMAT_TXID_ONLY
779
- known.add(bt.txid) if allow_txid_only
780
- end
856
+ known.add(bt.wtxid) if bt.is_a?(ProvenTxEntry) || (bt.is_a?(TxidOnlyEntry) && allow_txid_only)
781
857
  end
782
858
  known
783
859
  end
@@ -787,7 +863,7 @@ module BSV
787
863
  tx.inputs.each do |input|
788
864
  next if input.source_transaction
789
865
 
790
- source = find_transaction(input.prev_tx_id.reverse)
866
+ source = find_transaction(input.prev_wtxid)
791
867
  input.source_transaction = source if source
792
868
  end
793
869
  end
@@ -799,7 +875,7 @@ module BSV
799
875
  next unless input.source_transaction
800
876
 
801
877
  source = input.source_transaction
802
- source.merkle_path ||= find_bump(source.txid)
878
+ source.merkle_path ||= find_bump(source.wtxid)
803
879
  wire_ancestry(source)
804
880
  end
805
881
  end
@@ -807,7 +883,7 @@ module BSV
807
883
  # V1 (BRC-62): raw_tx + has_bump(byte) [+ bump_index(varint)]
808
884
  def write_v1_tx(buf, beef_tx)
809
885
  buf << beef_tx.transaction.to_binary
810
- if beef_tx.format == FORMAT_RAW_TX_AND_BUMP
886
+ if beef_tx.is_a?(ProvenTxEntry)
811
887
  buf << [1].pack('C')
812
888
  buf << VarInt.encode(beef_tx.bump_index)
813
889
  else
@@ -817,12 +893,12 @@ module BSV
817
893
 
818
894
  # V2 (BRC-96): format_byte [+ bump_index(varint)] + raw_tx
819
895
  def write_v2_tx(buf, beef_tx)
820
- case beef_tx.format
821
- when FORMAT_TXID_ONLY
896
+ case beef_tx
897
+ when TxidOnlyEntry
822
898
  buf << [FORMAT_TXID_ONLY].pack('C')
823
- # Reverse display-order txid back to wire (internal) byte order.
824
- buf << beef_tx.known_txid.reverse
825
- when FORMAT_RAW_TX_AND_BUMP
899
+ # known_wtxid is already wire (internal) byte order.
900
+ buf << beef_tx.known_wtxid
901
+ when ProvenTxEntry
826
902
  buf << [FORMAT_RAW_TX_AND_BUMP].pack('C')
827
903
  buf << VarInt.encode(beef_tx.bump_index)
828
904
  buf << beef_tx.transaction.to_binary