Class: BSV::Transaction::Beef

Inherits:
Object
  • Object
show all
Defined in:
lib/bsv/transaction/beef.rb

Overview

Background Evaluation Extended Format (BEEF) for SPV-ready transaction bundles. Encodes one or more transactions together with their merkle proofs (BUMPs), enabling recipients to verify inclusion without querying a block explorer.

Supports BRC-62 (V1), BRC-96 (V2), and BRC-95 (Atomic BEEF) formats.

Examples:

Parse a BEEF bundle and find a transaction

beef = BSV::Transaction::Beef.from_hex(beef_hex)
tx = beef.find_transaction(wtxid)

Defined Under Namespace

Classes: BeefTx, ProvenTxEntry, RawTxEntry, TxidOnlyEntry

Version constants collapse

BEEF_V1 =

Version magic bytes as LE uint32 (matching pack(‘V’) / unpack1(‘V’)). Stream bytes: 01 00 BE EF / 02 00 BE EF / 01 01 01 01

0xEFBE0001
BEEF_V2 =

BRC-62

0xEFBE0002
ATOMIC_BEEF =

BRC-96

0x01010101

Transaction format flags collapse

FORMAT_RAW_TX =

Raw transaction without a merkle proof.

0
FORMAT_RAW_TX_AND_BUMP =

Raw transaction with an associated BUMP index.

1
FORMAT_TXID_ONLY =

Only the transaction ID (no raw data).

2

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(version: BEEF_V1, bumps: [], transactions: []) ⇒ Beef

Returns a new instance of Beef.

Parameters:

  • version (Integer) (defaults to: BEEF_V1)

    BEEF version constant (default: BEEF_V1, matching to_binary’s default for ARC compatibility; from_binary overwrites this with the parsed version)

  • bumps (Array<MerklePath>) (defaults to: [])

    merkle proofs

  • transactions (Array<BeefTx>) (defaults to: [])

    transaction entries



178
179
180
181
182
183
# File 'lib/bsv/transaction/beef.rb', line 178

def initialize(version: BEEF_V1, bumps: [], transactions: [])
  @version = version
  @bumps = bumps
  @transactions = transactions
  @subject_wtxid = nil
end

Instance Attribute Details

#bumpsArray<MerklePath> (readonly)

Returns merkle proofs (BUMPs) referenced by transactions.

Returns:

  • (Array<MerklePath>)

    merkle proofs (BUMPs) referenced by transactions



159
160
161
# File 'lib/bsv/transaction/beef.rb', line 159

def bumps
  @bumps
end

#subject_wtxidString? (readonly)

Returns 32-byte wire-order subject txid (Atomic BEEF only).

Returns:

  • (String, nil)

    32-byte wire-order subject txid (Atomic BEEF only)



165
166
167
# File 'lib/bsv/transaction/beef.rb', line 165

def subject_wtxid
  @subject_wtxid
end

#transactionsArray<BeefTx> (readonly)

Returns the transactions in dependency order.

Returns:

  • (Array<BeefTx>)

    the transactions in dependency order



162
163
164
# File 'lib/bsv/transaction/beef.rb', line 162

def transactions
  @transactions
end

#txs_not_validArray<BeefTx>? (readonly)

Returns transactions that could not be sorted due to cycles, or nil.

Populated by #sort_transactions! when cycles are detected (F5.5).

Returns:



714
715
716
# File 'lib/bsv/transaction/beef.rb', line 714

def txs_not_valid
  @txs_not_valid
end

#versionInteger

Returns BEEF version constant.

Returns:

  • (Integer)

    BEEF version constant



156
157
158
# File 'lib/bsv/transaction/beef.rb', line 156

def version
  @version
end

Class Method Details

.from_binary(data) ⇒ Beef

Deserialise a BEEF bundle from binary data.

Supports V1 (BRC-62), V2 (BRC-96), and Atomic (BRC-95) formats. After parsing, input source transactions are wired automatically.

Parameters:

  • data (String)

    raw BEEF binary

Returns:

  • (Beef)

    the parsed BEEF bundle

Raises:

  • (ArgumentError)


194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
# File 'lib/bsv/transaction/beef.rb', line 194

def self.from_binary(data)
  raise ArgumentError, "truncated BEEF: need at least 4 bytes for version, got #{data.bytesize}" if data.bytesize < 4

  offset = 0

  version = data.byteslice(offset, 4).unpack1('V')
  offset += 4

  # F5.12: reject unknown version magic bytes
  unless [BEEF_V1, BEEF_V2, ATOMIC_BEEF].include?(version)
    raise ArgumentError,
          format('unknown BEEF version 0x%<ver>08X: expected BEEF_V1 (0x%<v1>08X), ' \
                 'BEEF_V2 (0x%<v2>08X), or ATOMIC_BEEF (0x%<ab>08X)',
                 ver: version, v1: BEEF_V1, v2: BEEF_V2, ab: ATOMIC_BEEF)
  end

  beef = new(version: version)

  if version == ATOMIC_BEEF
    if data.bytesize < offset + 36
      remaining = data.bytesize - offset
      raise ArgumentError, "truncated Atomic BEEF: need 36 bytes at offset #{offset}, got #{remaining}"
    end

    # Atomic BEEF stores the subject txid in wire (internal / little-endian) byte order,
    # matching JS and Go SDKs. Store as-is in @subject_wtxid (wire-order).
    beef.instance_variable_set(:@subject_wtxid, data.byteslice(offset, 32))
    offset += 32
    inner_version = data.byteslice(offset, 4).unpack1('V')
    offset += 4

    # Validate inner version — must be V1 or V2
    unless [BEEF_V1, BEEF_V2].include?(inner_version)
      raise ArgumentError,
            format('unknown inner BEEF version 0x%<ver>08X inside Atomic BEEF: expected BEEF_V1 or BEEF_V2',
                   ver: inner_version)
    end

    beef.version = inner_version
  end

  offset = read_bumps(beef, data, offset)

  case version == ATOMIC_BEEF ? beef.version : version
  when BEEF_V2
    read_v2_transactions(beef, data, offset)
  when BEEF_V1
    read_v1_transactions(beef, data, offset)
  end

  wire_source_transactions(beef)

  beef
end

.from_hex(hex) ⇒ Beef

Deserialise a BEEF bundle from a hex string.

Parameters:

  • hex (String)

    hex-encoded BEEF data

Returns:

  • (Beef)

    the parsed BEEF bundle



253
254
255
# File 'lib/bsv/transaction/beef.rb', line 253

def self.from_hex(hex)
  from_binary(BSV::Primitives::Hex.decode(hex, name: 'BEEF hex'))
end

Instance Method Details

#find_atomic_transaction(wtxid) ⇒ Transaction?

Find a transaction and recursively wire its ancestry (source transactions and merkle paths) for atomic proof validation.

Parameters:

  • wtxid (String)

    32-byte wire-order wtxid

Returns:

  • (Transaction, nil)

    the transaction with full proof tree, or nil



373
374
375
376
377
378
379
380
# File 'lib/bsv/transaction/beef.rb', line 373

def find_atomic_transaction(wtxid)
  BSV::Primitives::Hex.validate_wtxid!(wtxid, name: 'wtxid')
  tx = find_transaction(wtxid)
  return unless tx

  wire_ancestry(tx)
  tx
end

#find_bump(wtxid) ⇒ MerklePath?

Find the merkle path (BUMP) for a transaction by its wire-order txid.

First checks the transaction-table entries, then scans @bumps directly for a BUMP whose level-0 leaves contain the wtxid.

Parameters:

  • wtxid (String)

    32-byte wire-order wtxid

Returns:

  • (MerklePath, nil)

    the merkle path, or nil if not found



343
344
345
346
347
348
349
350
351
352
353
# File 'lib/bsv/transaction/beef.rb', line 343

def find_bump(wtxid)
  BSV::Primitives::Hex.validate_wtxid!(wtxid, name: 'wtxid')
  # Check transaction-table entries first (fast path)
  bt = @transactions.find { |entry| entry.wtxid == wtxid && entry.is_a?(ProvenTxEntry) }
  return bt.transaction.merkle_path || @bumps[bt.bump_index] if bt

  # F5.8: also scan @bumps directly for a path containing the wtxid leaf
  @bumps.find do |bump|
    bump.path[0]&.any? { |leaf| leaf.hash == wtxid }
  end
end

#find_transaction(wtxid) ⇒ Transaction?

Find a transaction in the bundle by its wire-order transaction ID.

Parameters:

  • wtxid (String)

    32-byte wire-order wtxid

Returns:

  • (Transaction, nil)

    the matching transaction, or nil



326
327
328
329
330
331
332
333
334
# File 'lib/bsv/transaction/beef.rb', line 326

def find_transaction(wtxid)
  BSV::Primitives::Hex.validate_wtxid!(wtxid, name: 'wtxid')
  BSV.logger&.debug { "[Beef] find_transaction: #{wtxid.reverse.unpack1('H*')} in #{@transactions.length} entries" }
  @transactions.each do |beef_tx|
    next if beef_tx.is_a?(TxidOnlyEntry)
    return beef_tx.transaction if beef_tx.wtxid == wtxid
  end
  nil
end

#find_transaction_for_signing(wtxid) ⇒ Transaction?

Find a transaction with all source_transactions wired for signing.

Parameters:

  • wtxid (String)

    32-byte wire-order wtxid

Returns:

  • (Transaction, nil)

    the transaction with wired inputs, or nil



359
360
361
362
363
364
365
366
# File 'lib/bsv/transaction/beef.rb', line 359

def find_transaction_for_signing(wtxid)
  BSV::Primitives::Hex.validate_wtxid!(wtxid, name: 'wtxid')
  tx = find_transaction(wtxid)
  return unless tx

  wire_inputs(tx)
  tx
end

#make_txid_only(wtxid) ⇒ BeefTx?

Convert a transaction entry to TXID-only format.

Parameters:

  • wtxid (String)

    32-byte wire-order wtxid

Returns:

  • (BeefTx, nil)

    the converted entry, or nil if not found



566
567
568
569
570
571
572
# File 'lib/bsv/transaction/beef.rb', line 566

def make_txid_only(wtxid)
  BSV::Primitives::Hex.validate_wtxid!(wtxid, name: 'wtxid')
  idx = @transactions.index { |bt| bt.wtxid == wtxid }
  return unless idx

  @transactions[idx] = TxidOnlyEntry.new(known_wtxid: wtxid)
end

#merge(other) ⇒ self

Merge all BUMPs and transactions from another BEEF bundle.

BUMP indices are remapped during merge. New BeefTx instances are constructed rather than sharing references with the source bundle (F5.9).

Parameters:

  • other (Beef)

    the BEEF bundle to merge from

Returns:

  • (self)

Raises:

  • (ArgumentError)

    if a transaction in other has a bump_index that does not point to any BUMP in other.bumps (i.e. the source bundle is internally inconsistent)



522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
# File 'lib/bsv/transaction/beef.rb', line 522

def merge(other)
  # Build index remap for BUMPs
  bump_remap = {}
  other.bumps.each_with_index do |bump, old_idx|
    bump_remap[old_idx] = merge_bump(bump)
  end

  # Merge transactions with remapped BUMP indices, constructing new
  # BeefTx instances rather than sharing source references (F5.9).
  other.transactions.each do |beef_tx|
    case beef_tx
    when TxidOnlyEntry
      next if @transactions.any? { |bt| bt.wtxid == beef_tx.known_wtxid }

      @transactions << TxidOnlyEntry.new(known_wtxid: beef_tx.known_wtxid)
    else
      next if @transactions.any? { |bt| bt.wtxid == beef_tx.wtxid }

      if beef_tx.is_a?(ProvenTxEntry) && beef_tx.bump_index
        new_idx = bump_remap[beef_tx.bump_index]
        if new_idx.nil?
          raise ArgumentError,
                "source BEEF has inconsistent bump_index #{beef_tx.bump_index} " \
                "(source has #{other.bumps.length} bumps); refusing to write a stale reference"
        end

        # F5.9: construct a new BeefTx with a dup'd Transaction
        # so mutations to the merged bundle don't affect the source.
        tx = beef_tx.transaction.dup
        tx.merkle_path = @bumps[new_idx]
        @transactions << ProvenTxEntry.new(transaction: tx, bump_index: new_idx)
      else
        @transactions << RawTxEntry.new(transaction: beef_tx.transaction.dup)
      end
    end
  end

  self
end

#merge_bump(merkle_path) ⇒ Integer

Add or deduplicate a merkle path (BUMP) in this BEEF bundle.

If an existing BUMP shares the same block_height and merkle root, it is combined (via MerklePath#combine) and the existing index is returned. Otherwise the BUMP is appended.

After the BUMP is stored, any existing FORMAT_RAW_TX transactions whose txid appears in the new BUMP’s level-0 leaves are retroactively upgraded to FORMAT_RAW_TX_AND_BUMP (F5.6).

Parameters:

Returns:

  • (Integer)

    the index of the (possibly merged) BUMP



404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
# File 'lib/bsv/transaction/beef.rb', line 404

def merge_bump(merkle_path)
  root = merkle_path.compute_root
  idx = nil
  @bumps.each_with_index do |existing, i|
    next unless existing.block_height == merkle_path.block_height

    next unless existing.compute_root == root

    existing.combine(merkle_path)
    idx = i
    break
  end

  if idx.nil?
    @bumps << merkle_path
    idx = @bumps.length - 1
  end

  # F5.6: retroactively link existing FORMAT_RAW_TX entries whose txid
  # appears in the new BUMP's level-0 leaves.
  bump = @bumps[idx]
  level0_leaves = bump.path[0] || []
  level0_internal = level0_leaves.map(&:hash).compact.to_set
  @transactions.each_with_index do |bt, i|
    next unless bt.is_a?(RawTxEntry)
    next unless level0_internal.include?(bt.wtxid)

    bt.transaction.merkle_path ||= bump
    @transactions[i] = ProvenTxEntry.new(transaction: bt.transaction, bump_index: idx)
  end

  idx
end

#merge_raw_tx(raw_bytes, bump_index: nil) ⇒ BeefTx

Add a transaction from raw binary data.

If the transaction already exists, upgrades weaker entries to stronger formats when a raw tx or bump_index is now available (F5.7).

Parameters:

  • raw_bytes (String)

    raw transaction binary

  • bump_index (Integer, nil) (defaults to: nil)

    optional BUMP index

Returns:

  • (BeefTx)

    the new or upgraded BeefTx entry



483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
# File 'lib/bsv/transaction/beef.rb', line 483

def merge_raw_tx(raw_bytes, bump_index: nil)
  tx = Transaction.from_binary(raw_bytes)

  if bump_index
    unless bump_index.is_a?(Integer) && bump_index >= 0 && bump_index < @bumps.length
      raise ArgumentError,
            "bump_index #{bump_index.inspect} out of range (have #{@bumps.length} bumps)"
    end

    tx.merkle_path = @bumps[bump_index]
  end

  existing_idx = @transactions.index { |bt| bt.wtxid == tx.wtxid }
  if existing_idx
    existing = @transactions[existing_idx]
    upgraded = upgrade_beef_tx(existing, tx, bump_index: bump_index)
    @transactions[existing_idx] = upgraded if upgraded
    return @transactions[existing_idx]
  end

  entry = if bump_index
            ProvenTxEntry.new(transaction: tx, bump_index: bump_index)
          else
            RawTxEntry.new(transaction: tx)
          end
  @transactions << entry
  entry
end

#merge_transaction(tx) ⇒ BeefTx

Add a transaction to this BEEF bundle.

Recursively merges the transaction’s ancestors (via source_transaction references on inputs) and their merkle paths. Duplicate transactions (same txid) are upgraded if a stronger format is now available (F5.7): TXID_ONLY → RAW_TX or RAW_TX_AND_BUMP; RAW_TX → RAW_TX_AND_BUMP.

Parameters:

Returns:

  • (BeefTx)

    the (possibly existing or upgraded) BeefTx entry



447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
# File 'lib/bsv/transaction/beef.rb', line 447

def merge_transaction(tx)
  wtxid = tx.wtxid

  # Check for existing entry and upgrade if a stronger format is available
  existing_idx = @transactions.index { |bt| bt.wtxid == wtxid }
  if existing_idx
    existing = @transactions[existing_idx]
    upgraded = upgrade_beef_tx(existing, tx)
    @transactions[existing_idx] = upgraded if upgraded
    return @transactions[existing_idx]
  end

  # Recursively merge ancestors first (dependency order)
  tx.inputs.each do |input|
    merge_transaction(input.source_transaction) if input.source_transaction
  end

  # Merge this transaction's BUMP if it has one
  entry = if tx.merkle_path
            bump_idx = merge_bump(tx.merkle_path)
            ProvenTxEntry.new(transaction: tx, bump_index: bump_idx)
          else
            RawTxEntry.new(transaction: tx)
          end
  @transactions << entry
  entry
end

#sort_transactions!self

Sort transactions in topological (dependency) order in place.

After sorting, every transaction’s input ancestors appear before it in the array. This is required for correct BEEF serialisation.

Transactions that form a cycle (i.e. cannot be topologically sorted) are moved to @txs_not_valid rather than being silently dropped (F5.5).

Returns:

  • (self)


664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
# File 'lib/bsv/transaction/beef.rb', line 664

def sort_transactions!
  return self if @transactions.length <= 1

  wtxid_index = {}
  @transactions.each_with_index { |bt, i| wtxid_index[bt.wtxid] = i }

  # Build adjacency: for each tx, which other txs must come before it?
  in_degree = Array.new(@transactions.length, 0)
  dependents = Array.new(@transactions.length) { [] }

  @transactions.each_with_index do |bt, i|
    next if bt.is_a?(TxidOnlyEntry)

    bt.transaction.inputs.each do |input|
      dep_idx = wtxid_index[input.prev_wtxid]
      next unless dep_idx

      dependents[dep_idx] << i
      in_degree[i] += 1
    end
  end

  # Kahn's algorithm
  queue = (0...@transactions.length).select { |i| in_degree[i].zero? }
  sorted = []

  until queue.empty?
    idx = queue.shift
    sorted << @transactions[idx]
    dependents[idx].each do |dep|
      in_degree[dep] -= 1
      queue << dep if in_degree[dep].zero?
    end
  end

  # F5.5: preserve unsortable (cyclic) transactions rather than silently dropping them
  if sorted.length < @transactions.length
    sorted_set = sorted.to_set(&:wtxid)
    @txs_not_valid = @transactions.reject { |bt| sorted_set.include?(bt.wtxid) }
  end

  @transactions = sorted
  self
end

#subject_dtxidString?

Display-order subject txid as a hex string (Atomic BEEF only).

Returns:

  • (String, nil)

    hex-encoded display-order txid, or nil



170
171
172
# File 'lib/bsv/transaction/beef.rb', line 170

def subject_dtxid
  @subject_wtxid&.reverse&.unpack1('H*')
end

#to_atomic_binary(subject_wtxid) ⇒ String

Serialise as Atomic BEEF (BRC-95), wrapping V2 data with a subject txid.

Parameters:

  • subject_wtxid (String)

    32-byte wire-order subject transaction ID

Returns:

  • (String)

    raw Atomic BEEF binary



310
311
312
313
314
315
316
317
318
# File 'lib/bsv/transaction/beef.rb', line 310

def to_atomic_binary(subject_wtxid)
  BSV::Primitives::Hex.validate_wtxid!(subject_wtxid, name: 'subject_wtxid')
  buf = [ATOMIC_BEEF].pack('V')
  # subject_wtxid is already in wire (internal) byte order — write as-is.
  buf << subject_wtxid.b
  # BRC-95: inner envelope is always V2
  buf << to_binary(version: BEEF_V2)
  buf
end

#to_atomic_hex(subject_wtxid) ⇒ String

Serialise as Atomic BEEF (BRC-95) hex string.

Parameters:

  • subject_wtxid (String)

    32-byte wire-order subject transaction ID

Returns:

  • (String)

    hex-encoded Atomic BEEF



386
387
388
# File 'lib/bsv/transaction/beef.rb', line 386

def to_atomic_hex(subject_wtxid)
  to_atomic_binary(subject_wtxid).unpack1('H*')
end

#to_binary(version: BEEF_V1) ⇒ String

Serialise the BEEF bundle to binary format.

Defaults to V1 (BRC-62) for compatibility with ARC and the reference TS SDK. Pass version: BEEF_V2 for BRC-96 format.

Parameters:

  • version (Integer) (defaults to: BEEF_V1)

    BEEF_V1 (default) or BEEF_V2

Returns:

  • (String)

    raw BEEF binary

Raises:

  • (ArgumentError)

    if version is BEEF_V1 and the bundle contains any FORMAT_TXID_ONLY entries (V1 / BRC-62 has no TXID-only format; pass version: BEEF_V2 to serialise such bundles)



269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
# File 'lib/bsv/transaction/beef.rb', line 269

def to_binary(version: BEEF_V1)
  if version == BEEF_V1 && @transactions.any?(TxidOnlyEntry)
    raise ArgumentError,
          'BEEF V1 (BRC-62) does not support FORMAT_TXID_ONLY entries; pass version: BEEF_V2 to serialise this bundle'
  end

  # F5.5/F5.20: ensure transactions are in dependency order before serialising
  sort_transactions!

  buf = [version].pack('V')

  buf << VarInt.encode(@bumps.length)
  @bumps.each { |bump| buf << bump.to_binary }

  buf << VarInt.encode(@transactions.length)
  @transactions.each do |beef_tx|
    if version == BEEF_V2
      write_v2_tx(buf, beef_tx)
    else
      write_v1_tx(buf, beef_tx)
    end
  end

  buf
end

#to_hexString

Serialise the BEEF bundle to a hex string.

Uses the bundle’s own @version, so a BEEF parsed from V2 round-trips to V2 hex, and a BEEF parsed from V1 (or freshly constructed via the default constructor) round-trips to V1 hex.

Returns:

  • (String)

    hex-encoded BEEF data



302
303
304
# File 'lib/bsv/transaction/beef.rb', line 302

def to_hex
  to_binary(version: @version).unpack1('H*')
end

#valid?(allow_txid_only: false) ⇒ Boolean

Check structural validity of the BEEF bundle.

A valid BEEF has every transaction either:

  • proven (has a BUMP / merkle_path), or

  • all its inputs reference transactions that are themselves valid within this bundle.

For FORMAT_RAW_TX_AND_BUMP entries, the BUMP linkage and computed root are also verified (F5.4).

Parameters:

  • allow_txid_only (Boolean) (defaults to: false)

    whether TXID-only entries count as valid (default: false)

Returns:

  • (Boolean)

    true if structurally valid



588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
# File 'lib/bsv/transaction/beef.rb', line 588

def valid?(allow_txid_only: false)
  # TXID-only entries are invalid unless explicitly allowed
  has_txid_only = @transactions.any?(TxidOnlyEntry)
  return false if has_txid_only && !allow_txid_only

  # F5.4: verify BUMP linkage and computed root for each proven transaction
  @transactions.each do |bt|
    next unless bt.is_a?(ProvenTxEntry)

    # Must have a BUMP
    bump = bt.transaction.merkle_path || @bumps[bt.bump_index]
    return false unless bump

    # The txid must appear as a leaf in the BUMP and compute a valid root
    begin
      bump.compute_root(bt.wtxid)
    rescue ArgumentError
      return false
    end
  end

  known_wtxids = build_known_wtxids(allow_txid_only)

  pending = @transactions.reject { |bt| bt.is_a?(TxidOnlyEntry) || known_wtxids.include?(bt.wtxid) }

  # Iteratively resolve: if all inputs of a tx are known, it becomes known
  changed = true
  while changed
    changed = false
    pending.reject! do |bt|
      all_inputs_known = bt.transaction.inputs.all? do |input|
        known_wtxids.include?(input.prev_wtxid)
      end
      if all_inputs_known
        known_wtxids.add(bt.wtxid)
        changed = true
      end
      all_inputs_known
    end
  end

  pending.empty?
end

#verify(chain_tracker = nil, allow_txid_only: false) ⇒ Boolean

Verify the BEEF bundle against a chain tracker (SPV).

Calls #valid? first for structural checks, then optionally verifies each BUMP’s computed merkle root against the chain tracker (F5.3).

Parameters:

  • chain_tracker (#valid_root_for_height?) (defaults to: nil)

    optional chain tracker (see ChainTracker) that responds to valid_root_for_height?(root_hex, block_height)

  • allow_txid_only (Boolean) (defaults to: false)

    passed to #valid?

Returns:

  • (Boolean)

    true if the bundle is structurally valid and, when a chain tracker is provided, all BUMP roots are confirmed by the chain



643
644
645
646
647
648
649
650
651
652
653
# File 'lib/bsv/transaction/beef.rb', line 643

def verify(chain_tracker = nil, allow_txid_only: false)
  return false unless valid?(allow_txid_only: allow_txid_only)
  return true unless chain_tracker

  @bumps.each do |bump|
    root_hex = bump.compute_root.reverse.unpack1('H*')
    return false unless chain_tracker.valid_root_for_height?(root_hex, bump.block_height)
  end

  true
end