Class: BSV::Transaction::Beef
- Inherits:
-
Object
- Object
- BSV::Transaction::Beef
- 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.
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
-
#bumps ⇒ Array<MerklePath>
readonly
Merkle proofs (BUMPs) referenced by transactions.
-
#subject_wtxid ⇒ String?
readonly
32-byte wire-order subject txid (Atomic BEEF only).
-
#transactions ⇒ Array<BeefTx>
readonly
The transactions in dependency order.
-
#txs_not_valid ⇒ Array<BeefTx>?
readonly
Returns transactions that could not be sorted due to cycles, or nil.
-
#version ⇒ Integer
BEEF version constant.
Class Method Summary collapse
-
.from_binary(data) ⇒ Beef
Deserialise a BEEF bundle from binary data.
-
.from_hex(hex) ⇒ Beef
Deserialise a BEEF bundle from a hex string.
Instance Method Summary collapse
-
#find_atomic_transaction(wtxid) ⇒ Transaction?
Find a transaction and recursively wire its ancestry (source transactions and merkle paths) for atomic proof validation.
-
#find_bump(wtxid) ⇒ MerklePath?
Find the merkle path (BUMP) for a transaction by its wire-order txid.
-
#find_transaction(wtxid) ⇒ Transaction?
Find a transaction in the bundle by its wire-order transaction ID.
-
#find_transaction_for_signing(wtxid) ⇒ Transaction?
Find a transaction with all source_transactions wired for signing.
-
#initialize(version: BEEF_V1, bumps: [], transactions: []) ⇒ Beef
constructor
A new instance of Beef.
-
#make_txid_only(wtxid) ⇒ BeefTx?
Convert a transaction entry to TXID-only format.
-
#merge(other) ⇒ self
Merge all BUMPs and transactions from another BEEF bundle.
-
#merge_bump(merkle_path) ⇒ Integer
Add or deduplicate a merkle path (BUMP) in this BEEF bundle.
-
#merge_raw_tx(raw_bytes, bump_index: nil) ⇒ BeefTx
Add a transaction from raw binary data.
-
#merge_transaction(tx) ⇒ BeefTx
Add a transaction to this BEEF bundle.
-
#sort_transactions! ⇒ self
Sort transactions in topological (dependency) order in place.
-
#subject_dtxid ⇒ String?
Display-order subject txid as a hex string (Atomic BEEF only).
-
#subject_txid ⇒ String?
Display-order subject txid as binary bytes (Atomic BEEF only).
-
#to_atomic_binary(subject_wtxid) ⇒ String
Serialise as Atomic BEEF (BRC-95), wrapping V2 data with a subject txid.
-
#to_atomic_hex(subject_wtxid) ⇒ String
Serialise as Atomic BEEF (BRC-95) hex string.
-
#to_binary(version: BEEF_V1) ⇒ String
Serialise the BEEF bundle to binary format.
-
#to_hex ⇒ String
Serialise the BEEF bundle to a hex string.
-
#valid?(allow_txid_only: false) ⇒ Boolean
Check structural validity of the BEEF bundle.
-
#verify(chain_tracker = nil, allow_txid_only: false) ⇒ Boolean
Verify the BEEF bundle against a chain tracker (SPV).
Constructor Details
#initialize(version: BEEF_V1, bumps: [], transactions: []) ⇒ Beef
Returns a new instance of Beef.
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
#bumps ⇒ Array<MerklePath> (readonly)
Returns merkle proofs (BUMPs) referenced by transactions.
100 101 102 |
# File 'lib/bsv/transaction/beef.rb', line 100 def bumps @bumps end |
#subject_wtxid ⇒ String? (readonly)
Returns 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 |
#transactions ⇒ Array<BeefTx> (readonly)
Returns the transactions in dependency order.
103 104 105 |
# File 'lib/bsv/transaction/beef.rb', line 103 def transactions @transactions end |
#txs_not_valid ⇒ Array<BeefTx>? (readonly)
Returns transactions that could not be sorted due to cycles, or nil.
Populated by #sort_transactions! when cycles are detected (F5.5).
668 669 670 |
# File 'lib/bsv/transaction/beef.rb', line 668 def txs_not_valid @txs_not_valid end |
#version ⇒ Integer
Returns 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.
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.
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.
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.
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.
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.
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.
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).
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).
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).
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.
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).
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_dtxid ⇒ String?
Display-order subject txid as a hex string (Atomic BEEF only).
117 118 119 |
# File 'lib/bsv/transaction/beef.rb', line 117 def subject_dtxid @subject_wtxid&.reverse&.unpack1('H*') end |
#subject_txid ⇒ String?
Display-order subject txid as binary bytes (Atomic BEEF only).
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.
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.
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.
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_hex ⇒ String
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.
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).
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).
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 |