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

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



125
126
127
128
129
130
# File 'lib/bsv/transaction/beef.rb', line 125

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



100
101
102
# File 'lib/bsv/transaction/beef.rb', line 100

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)



106
107
108
# File 'lib/bsv/transaction/beef.rb', line 106

def subject_wtxid
  @subject_wtxid
end

#transactionsArray<BeefTx> (readonly)

Returns the transactions in dependency order.

Returns:

  • (Array<BeefTx>)

    the transactions in dependency order



103
104
105
# File 'lib/bsv/transaction/beef.rb', line 103

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:



668
669
670
# File 'lib/bsv/transaction/beef.rb', line 668

def txs_not_valid
  @txs_not_valid
end

#versionInteger

Returns BEEF version constant.

Returns:

  • (Integer)

    BEEF version constant



97
98
99
# File 'lib/bsv/transaction/beef.rb', line 97

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)


141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/bsv/transaction/beef.rb', line 141

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



200
201
202
# File 'lib/bsv/transaction/beef.rb', line 200

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



319
320
321
322
323
324
325
326
# File 'lib/bsv/transaction/beef.rb', line 319

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



289
290
291
292
293
294
295
296
297
298
299
# File 'lib/bsv/transaction/beef.rb', line 289

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.format == FORMAT_RAW_TX_AND_BUMP }
  return bt.transaction&.merkle_path || (bt.bump_index && @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



273
274
275
276
277
278
279
280
# File 'lib/bsv/transaction/beef.rb', line 273

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|
    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



305
306
307
308
309
310
311
312
# File 'lib/bsv/transaction/beef.rb', line 305

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



520
521
522
523
524
525
526
# File 'lib/bsv/transaction/beef.rb', line 520

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] = BeefTx.new(format: FORMAT_TXID_ONLY, 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)



472
473
474
475
476
477
478
479
480
481
482
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
511
512
513
514
# File 'lib/bsv/transaction/beef.rb', line 472

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.format
    when FORMAT_TXID_ONLY
      next if @transactions.any? { |bt| bt.wtxid == beef_tx.known_wtxid }

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

      if beef_tx.format == FORMAT_RAW_TX_AND_BUMP && 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 << BeefTx.new(
          format: FORMAT_RAW_TX_AND_BUMP,
          transaction: tx,
          bump_index: new_idx
        )
      else
        @transactions << BeefTx.new(format: FORMAT_RAW_TX, 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



350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
# File 'lib/bsv/transaction/beef.rb', line 350

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.format == FORMAT_RAW_TX && bt.transaction
    next unless level0_internal.include?(bt.transaction.wtxid)

    bt.transaction.merkle_path ||= bump
    @transactions[i] = BeefTx.new(
      format: FORMAT_RAW_TX_AND_BUMP,
      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



433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
# File 'lib/bsv/transaction/beef.rb', line 433

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
            BeefTx.new(format: FORMAT_RAW_TX_AND_BUMP, transaction: tx, bump_index: bump_index)
          else
            BeefTx.new(format: FORMAT_RAW_TX, 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



397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
# File 'lib/bsv/transaction/beef.rb', line 397

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)
            BeefTx.new(format: FORMAT_RAW_TX_AND_BUMP, transaction: tx, bump_index: bump_idx)
          else
            BeefTx.new(format: FORMAT_RAW_TX, 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)


618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
# File 'lib/bsv/transaction/beef.rb', line 618

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 unless bt.transaction

    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



117
118
119
# File 'lib/bsv/transaction/beef.rb', line 117

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

#subject_txidString?

Display-order subject txid as binary bytes (Atomic BEEF only).

Returns:

  • (String, nil)

    32-byte display-order txid, or nil



110
111
112
# File 'lib/bsv/transaction/beef.rb', line 110

def subject_txid
  @subject_wtxid&.reverse
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



257
258
259
260
261
262
263
264
265
# File 'lib/bsv/transaction/beef.rb', line 257

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



332
333
334
# File 'lib/bsv/transaction/beef.rb', line 332

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)



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
# File 'lib/bsv/transaction/beef.rb', line 216

def to_binary(version: BEEF_V1)
  if version == BEEF_V1 && @transactions.any? { |bt| bt.format == FORMAT_TXID_ONLY }
    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



249
250
251
# File 'lib/bsv/transaction/beef.rb', line 249

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



542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
# File 'lib/bsv/transaction/beef.rb', line 542

def valid?(allow_txid_only: false)
  # TXID-only entries are invalid unless explicitly allowed
  has_txid_only = @transactions.any? { |bt| bt.format == FORMAT_TXID_ONLY }
  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.format == FORMAT_RAW_TX_AND_BUMP

    # Must have a BUMP
    bump = bt.transaction&.merkle_path || (bt.bump_index && @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.transaction.wtxid)
    rescue ArgumentError
      return false
    end
  end

  known_wtxids = build_known_wtxids(allow_txid_only)

  pending = @transactions.select { |bt| bt.transaction && !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



597
598
599
600
601
602
603
604
605
606
607
# File 'lib/bsv/transaction/beef.rb', line 597

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