Skip to content

fix: persist BEEF ancestry through the wallet lifecycle#473

Merged
sgbett merged 7 commits intomasterfrom
feat/466-beef-ancestry-persistence
Apr 16, 2026
Merged

fix: persist BEEF ancestry through the wallet lifecycle#473
sgbett merged 7 commits intomasterfrom
feat/466-beef-ancestry-persistence

Conversation

@sgbett
Copy link
Copy Markdown
Owner

@sgbett sgbett commented Apr 16, 2026

Summary

Fixes the architectural gap that caused x402-rack phantom-payment failures and the same class of bug reported upstream in wallet-toolbox#149. Closes #466.

The real bug (after investigation)

Not what the HLR initially described. The storage side of internalize_action was fine. The failures came from two places:

  1. Transaction#to_ef too strict — required explicit source_satoshis/source_locking_script on every input, rather than deriving from wired source_transaction. This caused silent fallback to plain hex, which ARC rejects for tx with unconfirmed parents. Ruby was stricter than TS.
  2. Transaction.from_beef didn't wire ancestry via find_atomic_transaction — late-bound BUMPs on FORMAT_RAW_TX ancestors weren't attached.

Fix

  • to_ef now derives source data from wired source_transaction when unset (matches TS writeEF). Non-mutating — source fields on input objects remain untouched.
  • from_beef uses Beef#find_atomic_transaction so ancestry is wired recursively including late-bound BUMPs.
  • Regression spec confirms internalize_action persists all ancestors (was already correct — spec cements the guarantee).
  • Removed redundant store_transaction call in internalize_action (duplicated work done by store_proofs_from_beef).
  • End-to-end round-trip spec proves the full fix, including a sanity-check that strips ancestry and asserts to_ef_hex raises.

Consumer impact

Consumers doing Transaction.from_beef(bytes) then ARC#broadcast(tx) — or receiving a BEEF via internalize_action and later spending via create_action — now work correctly. Previously these silently failed or produced invalid EF.

Test plan

  • 6 round-trip examples pass (sanity check included — strips ancestry and expects failure)
  • to_ef fallback covered with EF-specific specs
  • from_beef ancestry wiring covered with new specs
  • internalize_action ancestor persistence covered with regression specs
  • Full suite: bsv-sdk (3459 examples), bsv-wallet (1257 examples), bsv-wallet-postgres (174 pending — Postgres unavailable in this env), bsv-attest (30 examples) all pass
  • RuboCop clean (346 files inspected, no offences)

Closes #466, #467, #468, #469, #470, #471, #472

🤖 Generated with Claude Code

sgbett and others added 7 commits April 16, 2026 20:56
Transaction#to_ef now falls back to source_transaction.outputs[prev_tx_out_index]
when source_satoshis or source_locking_script are not explicitly set on an input.
Derivation is non-mutating — calling to_ef twice produces identical bytes and
input attributes remain unchanged. Raises ArgumentError only when neither
explicit fields nor a wired source_transaction are available.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…regression spec (#469)

`store_proofs_from_beef` already calls `@storage.store_transaction` for every
tx in the BEEF (including the subject), making the explicit call on the line
below it redundant. Remove the duplicate and add a regression spec with 9
examples covering 3-generation ancestry, TXID_ONLY entries, and idempotency
to lock in the persistence guarantee.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
)

Cover plain BEEF, Atomic BEEF (subject_txid), 2-generation ancestry,
late-bound BUMP attachment via merge_bump, and empty BEEF → nil cases.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CHANGELOG entry under Unreleased covers the to_ef source-data derivation
fix (#467) and the from_beef find_atomic_transaction rewrite (#468), with
consumer impact and cross-reference to wallet-toolbox#149. Transaction
guide gains a "Extracting a transaction from BEEF" subsection showing the
from_beef → to_ef_hex broadcast flow. Wallet guide notes the internalize →
create → broadcast round-trip coverage added in Task 3/4.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Exercises the full internalize → create_action → broadcast lifecycle with a
2-generation BEEF (grandparent confirmed, parent raw-only). The mocked ARC
calls to_ef_hex on the broadcast tx — raises if ancestry is not wired,
proving both Task 1 (to_ef fallback) and Task 2 (from_beef wiring) are needed.

Covers: happy path with full ancestry, unproven parent edge case, and a
regression-defence scenario that strips ancestry to confirm the test fails
without the fixes in place.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the [PR #N] placeholder with the actual PR URL now that
#473 exists. Remove the placeholder footer instruction.

Refs #466, #472
@sgbett sgbett merged commit b1e1fdf into master Apr 16, 2026
3 of 9 checks passed
sgbett added a commit that referenced this pull request Apr 16, 2026
The previous spec invoked merge_bump after merge_transaction, which
triggers F5.6's retroactive upgrade. The ancestor then landed on-wire
as FORMAT_RAW_TX_AND_BUMP and the parser populated merkle_path at read
time — before find_atomic_transaction ever ran. The spec passed but
would still have passed with the pre-#468 from_beef implementation
(bare subject extraction without find_atomic_transaction routing).

Rewrite to bypass merge_bump: push the BUMP directly into beef.bumps
so the ancestor stays FORMAT_RAW_TX on-wire. Add a sanity assertion
that the serialised ancestor.format is FORMAT_RAW_TX, so a future
change that reintroduces the F5.6 path silently would fail the spec.

Verified the spec now fails when from_beef is reverted to the old
`beef.transactions.reverse.find(&:transaction)&.transaction` shape.

Ref: remote review on PR #473, bug_001_1.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@sgbett sgbett deleted the feat/466-beef-ancestry-persistence branch April 21, 2026 01:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[HLR] Persist BEEF ancestry through the wallet lifecycle

1 participant