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)

Direct Known Subclasses

BeefParty

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



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

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



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

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)



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

def subject_wtxid
  @subject_wtxid
end

#transactionsArray<BeefTx> (readonly)

Returns the transactions in dependency order.

Returns:

  • (Array<BeefTx>)

    the transactions in dependency order



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

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:



867
868
869
# File 'lib/bsv/transaction/beef.rb', line 867

def txs_not_valid
  @txs_not_valid
end

#versionInteger

Returns BEEF version constant.

Returns:

  • (Integer)

    BEEF version constant



154
155
156
# File 'lib/bsv/transaction/beef.rb', line 154

def version
  @version
end

Class Method Details

.from_binary(data) ⇒ Transaction::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:

Raises:

  • (ArgumentError)


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

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) ⇒ Transaction::Beef

Deserialise a BEEF bundle from a hex string.

Parameters:

  • hex (String)

    hex-encoded BEEF data

Returns:



251
252
253
# File 'lib/bsv/transaction/beef.rb', line 251

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

Instance Method Details

#cloneTransaction::Beef

Return a shallow copy of this Transaction::Beef.

Both @bumps and @transactions arrays are duplicated (new arrays), but the BeefTx and MerklePath objects they contain are shared references. This mirrors the TS SDK’s clone contract: entries are effectively immutable once added to a bundle, so shallow semantics are correct. If a deeper copy is ever required, add a separate deep_dup rather than changing this method’s contract.

@subject_wtxid (Atomic BEEF) and @txs_not_valid (cyclic-graph metadata) are preserved on the copy.

Returns:



420
421
422
423
424
425
426
427
428
429
430
431
# File 'lib/bsv/transaction/beef.rb', line 420

def clone
  # Use super (Object#clone) rather than self.class.new so subclasses
  # (e.g. Transaction::BeefParty) with different initialize signatures
  # work correctly. Object#clone does a shallow ivar copy without
  # invoking initialize; we then dup the two arrays so the copy's
  # contents can mutate independently.
  c = super
  c.instance_variable_set(:@bumps, @bumps.dup)
  c.instance_variable_set(:@transactions, @transactions.dup)
  c.instance_variable_set(:@txs_not_valid, @txs_not_valid&.dup)
  c
end

#find_atomic_transaction(wtxid) ⇒ Transaction::Tx?

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:



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

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



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

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::Tx?

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

Parameters:

  • wtxid (String)

    32-byte wire-order wtxid

Returns:



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

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::Tx?

Find a transaction with all source_transactions wired for signing.

Parameters:

  • wtxid (String)

    32-byte wire-order wtxid

Returns:



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

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



719
720
721
722
723
724
725
# File 'lib/bsv/transaction/beef.rb', line 719

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:

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)



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
708
709
710
711
712
713
# File 'lib/bsv/transaction/beef.rb', line 675

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



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
585
586
587
588
589
# File 'lib/bsv/transaction/beef.rb', line 557

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



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

def merge_raw_tx(raw_bytes, bump_index: nil)
  tx = Tx.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



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

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

#merge_txid_only(wtxid) ⇒ BeefTx

Add a TXID-only entry for wtxid if no entry exists yet.

If an entry already exists (in any format), the call is a no-op —TXID-only is the weakest format, so an existing stronger entry is kept.

Parameters:

  • wtxid (String)

    32-byte wire-order binary txid

Returns:

  • (BeefTx)

    the existing or newly added entry



397
398
399
400
401
402
403
404
405
# File 'lib/bsv/transaction/beef.rb', line 397

def merge_txid_only(wtxid)
  BSV::Primitives::Hex.validate_wtxid!(wtxid, name: 'wtxid')
  existing = @transactions.find { |bt| bt.wtxid == wtxid }
  return existing if existing

  entry = TxidOnlyEntry.new(known_wtxid: wtxid)
  @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)


817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
# File 'lib/bsv/transaction/beef.rb', line 817

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



168
169
170
# File 'lib/bsv/transaction/beef.rb', line 168

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



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

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



384
385
386
# File 'lib/bsv/transaction/beef.rb', line 384

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)



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

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



300
301
302
# File 'lib/bsv/transaction/beef.rb', line 300

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

#trim_known_wtxids(known_wtxids) ⇒ Transaction::Beef

Return a new Transaction::Beef with TXID-only entries removed for any wtxid in known_wtxids.

RAW_TX and RAW_TX_AND_BUMP entries are always retained, even when their wtxid appears in known_wtxids. Only TxidOnlyEntry records are candidates for removal (they carry no proof data the recipient needs).

After dropping TXID-only entries, any BUMP that is no longer referenced by any remaining ProvenTxEntry is removed, and all bump_index fields are renumbered to match the new bumps array (mirrors TS trimKnownTxids at Beef.ts:861-914).

Does not mutate self. Starts from a #clone so the caller’s state is preserved.

Parameters:

  • known_wtxids (Array<String>)

    binary wtxids the recipient already has

Returns:



450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
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
# File 'lib/bsv/transaction/beef.rb', line 450

def trim_known_wtxids(known_wtxids)
  known_set = known_wtxids.to_set

  trimmed = clone
  trimmed.instance_variable_set(
    :@transactions,
    trimmed.transactions.reject { |bt| bt.is_a?(TxidOnlyEntry) && known_set.include?(bt.wtxid) }
  )

  # Find which bump indices are still referenced
  referenced = Set.new
  trimmed.transactions.each do |bt|
    referenced.add(bt.bump_index) if bt.is_a?(ProvenTxEntry)
  end

  # If all bumps are still referenced, nothing more to do
  return trimmed if referenced.size == trimmed.bumps.length

  # Build old → new index map for surviving bumps
  index_map = {}
  new_idx = 0
  trimmed.bumps.each_with_index do |_, old_idx|
    if referenced.include?(old_idx)
      index_map[old_idx] = new_idx
      new_idx += 1
    end
  end

  # Drop unreferenced bumps
  trimmed.instance_variable_set(
    :@bumps,
    trimmed.bumps.each_with_index.filter_map { |bump, i| bump if referenced.include?(i) }
  )

  # Renumber bump_index on all ProvenTxEntry records
  trimmed.instance_variable_set(
    :@transactions,
    trimmed.transactions.map do |bt|
      next bt unless bt.is_a?(ProvenTxEntry)

      new_bump_idx = index_map.fetch(bt.bump_index)
      ProvenTxEntry.new(transaction: bt.transaction, bump_index: new_bump_idx).tap do |e|
        e.transaction.merkle_path = trimmed.bumps[new_bump_idx]
      end
    end
  )

  trimmed
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



741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
# File 'lib/bsv/transaction/beef.rb', line 741

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

#valid_wtxidsArray<String>

Return the wtxids of transactions that are “valid” in this bundle.

A transaction is valid when it either:

  • has a merkle proof (is a ProvenTxEntry), or

  • all of its inputs chain back to proven transactions within the bundle.

TXID-only entries are excluded. Cyclic transactions (from @txs_not_valid) are also excluded.

Used by BSV::Transaction::BeefParty to record which wtxids a party gains knowledge of after a #merge_beef_from_party call.

Returns:

  • (Array<String>)

    binary wire-order wtxids



513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
# File 'lib/bsv/transaction/beef.rb', line 513

def valid_wtxids
  invalid = @txs_not_valid || Set.new
  known   = Set.new

  # Seed with proven entries (excluding any marked cyclic/unsortable)
  @transactions.each do |bt|
    known.add(bt.wtxid) if bt.is_a?(ProvenTxEntry) && !invalid.include?(bt.wtxid)
  end

  # Iteratively resolve unproven entries whose inputs are all known.
  # Skip @txs_not_valid entries — by definition they can't be ordered
  # for validation, so a counterparty receiving them can't validate either.
  changed = true
  while changed
    changed = false
    @transactions.each do |bt|
      next if bt.is_a?(TxidOnlyEntry) || known.include?(bt.wtxid)
      next if invalid.include?(bt.wtxid)
      next unless bt.respond_to?(:transaction)

      if bt.transaction.inputs.all? { |inp| known.include?(inp.prev_wtxid) }
        known.add(bt.wtxid)
        changed = true
      end
    end
  end

  known.to_a
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



796
797
798
799
800
801
802
803
804
805
806
# File 'lib/bsv/transaction/beef.rb', line 796

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