Skip to content

multi: Register authenticated provisional lineage end to end - #990

Open
ellemouton wants to merge 17 commits into
reorg-batchcanon-corefrom
reorg-lineage-producers
Open

multi: Register authenticated provisional lineage end to end#990
ellemouton wants to merge 17 commits into
reorg-batchcanon-corefrom
reorg-lineage-producers

Conversation

@ellemouton

@ellemouton ellemouton commented Jul 17, 2026

Copy link
Copy Markdown
Member

What this PR does

Connects the real VTXO creation and receipt paths to the batchcanon authority
introduced by #980. Without this PR the authority exists but production paths do
not consistently feed it.

It handles three production sources:

  1. Locally received round VTXOs.
  2. OOR-received VTXOs.
  3. Explicit custom VTXOs supplied as OOR inputs.

The indexer ancestry response supplies the serialized commitment transaction,
ordered commitment prevouts, CSV-delay evidence, and the parent lineage required
to validate a received VTXO. The recipient treats that response as untrusted
and authenticates: the commitment txid, the referenced output, every commitment
input, the input prevouts and their ordering, and the relationship between the
received VTXO and the supplied batch.

Registration happens before the VTXO is saved or exposed:

sequenceDiagram
    participant R as OOR receiver
    participant I as Indexer
    participant B as batchcanon
    participant V as VTXO store

    R->>I: Request ancestry
    I-->>R: Commitment, prevouts, and lineage
    R->>R: Authenticate evidence
    R->>B: Register complete lineage
    B-->>R: Registration durable
    R->>V: Materialize and save VTXO
Loading

Once registered, the client responds to later reorgs using its local chain
watches; it does not requery the indexer.

Wallet reporting uses the same authority: a lifecycle-live VTXO whose lineage is
reorged, conflicted, unseen, or still reconciling is excluded from spendable
balance and included in temporarily_unavailable_sat. Terminally invalidated
value is excluded from both buckets.

Review focus

  • Treat the indexer response as an untrusted input — are all bindings checked?
  • Are every TxIn and ordered prevout included and authenticated?
  • Is registration durably complete before exposure?
  • Are retries idempotent across every crash boundary?
  • Does legacy or incomplete evidence fail closed?

Full design & diagrams: REORG_SAFETY_PR_REVIEW_GUIDE.md §4.3. Overall
architecture and PR stack: lumos#454.

@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@ellemouton

Copy link
Copy Markdown
Member Author

@claude review this

@claude

claude Bot commented Jul 17, 2026

Copy link
Copy Markdown

Claude finished @ellemouton's task in 4m 34s —— View job


Review: PR #990 — Register authenticated provisional lineage end to end

I reviewed the full diff against origin/reorg-batchcanon-core. This is a well-structured, defensively-written PR. The trust boundary in batchcanon.EvidenceFromAncestryPaths is the centerpiece and it is thorough — it binds every server-supplied field (tx bytes → txid, tree output value/script → tx.TxOut[idx], each input outpoint/prevout → tx.TxIn[i]) rather than trusting caller assertions, and the fail-closed rollout story (legacy indexers → empty evidence → gate keeps lineage unusable) is coherent. Test coverage is good across all five producers.

Below are observations, ordered roughly by significance. None are blockers.


1. commitment_csv_expiry_delta is trusted, not authenticated

In batchcanon/rpc_evidence.go, every other evidence field is cross-checked against the serialized commitment tx, but CSVExpiryDelta (path.GetCommitmentCsvExpiryDelta()) is taken directly from the operator with only a > 0 check. A malicious operator can inflate/deflate it, which flows into Record.EffectiveExpiry() (ConfirmationHeight + CSVExpiryDelta). This is likely acceptable (the operator already controls tree construction, and CSV isn't derivable from the tx alone), but it's the one piece of "evidence" that isn't actually evidence. Worth a one-line comment at the trust boundary making that explicit so a future reader doesn't assume it's authenticated like the rest.

2. Asymmetric CSV validation between producer and receive paths

The receive/incoming paths enforce CSVExpiryDelta > 0 in three places (evidenceFromAncestryPath, encodeBatchEvidence/decodeBatchEvidence, validateIncomingEvidenceCoverage, registerIncomingBatchEvidence), but the round producer path in roundBatchRegistration (round/transitions.go) sets CSVExpiryDelta: int32(state.SweepDelay) with no positivity guard, and validateRegistration in batchcanon/manager.go does not check CSV either. SweepDelay is realistically always positive, so this is defensive-only, but given how consistently the receive side guards it, a matching guard (or a check inside validateRegistration) would close the gap and keep the invariant in one place. Fix this →

3. batchOutputIndex diverges from confirmationWatchScript on the out-of-range branch

batchOutputIndex (round/transitions.go) is documented as mirroring confirmationWatchScript so the persisted evidence watches the same output the chain backend does. They agree on the happy path, but when len(vtxoTrees) > 0 and the computed min index is out of range, confirmationWatchScript falls back to TxOut[0] while batchOutputIndex returns an error. So in that (shouldn't-happen) case the confirmation watch keys on output 0 while registration fails outright. The fail-closed direction is arguably safer, but the "mirrors confirmationWatchScript" comment slightly overstates the equivalence — either reconcile the fallback or note the intentional divergence.

4. Duplicated coverage-validation logic

oor.validateIncomingEvidenceCoverage (oor/incoming_batch_registration.go) and the inline coverage loop in vtxo.registerIncomingBatchEvidence (vtxo/incoming_batch_registration.go) are near-identical: per-item Validate(), CSV>0, "names commitment outside ancestry", duplicate detection, and the final len(seen) != len(ancestry) count. They live in different packages so a shared helper would need a home (perhaps batchcanon, alongside EvidenceFromAncestryPaths, taking the ancestry txid set). Not urgent, but this is exactly the kind of security-relevant check you don't want to drift between two copies.

5. Minor: emitReceived on registration failure

In vtxo/incoming_handler.go, a registration failure emits h.emitReceived(ctx, "failed") and returns an error. If the caller retries the receive, you can get failed then later received for the same transfer. Since RegisterBatch is idempotent this is only a metrics-cardinality nuance, not a correctness issue — flagging only for awareness.


Things I checked that are correct

  • OOR dependent-outpoint derivation is right. RegisterIncomingBatchEvidence builds the dependent as {Hash: chainhash.Hash(sessionID), Index: match.OutputIndex}. I confirmed SessionID is the Ark txid (see matchesIncomingVTXO comparing sessionID[:] to the candidate txid, and the events.go doc comments), and the materialized VTXO outpoint is {arkTxid, OutputIndex} — so these match. The vtxo path uses desc.Outpoint directly, which is the same value.
  • No double registration in OOR. Durable path registers in session_actor.driveOutboxEvents (outside the commit tx); the sync path registers in LocalPersistenceOutboxHandler gated on !hasActorDBTx(ctx). Mutually exclusive.
  • Registration-before-exposure ordering holds in round (before VTXOStore persist) and in vtxo (before save/notify), consistent with the fail-closed gate.
  • context.WithoutCancel in the round path correctly detaches the durable registration from request cancellation.
  • Proto changes (fields 6/7/8 on AncestryPath, new CommitmentInputEvidence) are purely additive with no field-number collisions.
  • Duplicate/conflict handling for same-commitment multi-leaf ancestry (equalBatchEvidence) is correct and tested.

Nice work — the authentication layer and the two-graph separation (actual inputs vs logical consumer edges) are clearly reasoned throughout.
· branch reorg-lineage-producers

@ellemouton

Copy link
Copy Markdown
Member Author

Review disposition for the current head: the CSV delay is intentionally operator-authored policy input because it is not derivable from the commitment transaction; all transaction-bound fields are authenticated, and the client still requires a positive delay. The output-index error path intentionally fails closed instead of copying the legacy confirmation-watch fallback. The duplicated receive-path coverage checks and failed-then-retried metric sequence are non-blocking cleanup notes; no correctness finding was left open in this PR.

@ellemouton
ellemouton force-pushed the reorg-lineage-producers branch from b1f9ab8 to 9655bde Compare July 17, 2026 23:27
@ellemouton
ellemouton force-pushed the reorg-batchcanon-core branch 2 times, most recently from 8dbd209 to c566338 Compare July 17, 2026 23:38
@ellemouton
ellemouton force-pushed the reorg-lineage-producers branch from 9655bde to 659e6aa Compare July 17, 2026 23:38
@ellemouton
ellemouton force-pushed the reorg-batchcanon-core branch from c566338 to 899c934 Compare July 18, 2026 00:00
@ellemouton
ellemouton force-pushed the reorg-lineage-producers branch from 659e6aa to b2a5d3d Compare July 18, 2026 00:00
@ellemouton
ellemouton force-pushed the reorg-batchcanon-core branch from 899c934 to 9d3b6b0 Compare July 18, 2026 00:20
@ellemouton
ellemouton force-pushed the reorg-lineage-producers branch from b2a5d3d to c121b30 Compare July 18, 2026 00:20
@ellemouton
ellemouton force-pushed the reorg-batchcanon-core branch from 9d3b6b0 to 095039e Compare July 18, 2026 00:38
@ellemouton
ellemouton force-pushed the reorg-lineage-producers branch 2 times, most recently from 49e2003 to 9d19e5d Compare July 20, 2026 16:10
@ellemouton

Copy link
Copy Markdown
Member Author

@claude review this — the current head adds canonicality-aware spendable and temporarily-unavailable balance reporting plus a real reorg/reconfirmation systest.

@claude

claude Bot commented Jul 20, 2026

Copy link
Copy Markdown

Claude finished @ellemouton's task in 2m 56s —— View job


Review: canonicality-aware balance reporting + reorg systest

Reviewed the head commits 3859c34 (proto fields) and 9d19e5d (classification + wiring + systest) against origin/reorg-batchcanon-core. This is a clean, well-tested increment. The core idea — a third balance bucket for reversible lineage limbo that is carved out of both vtxo_balance_sat and total_confirmed_sat — is implemented consistently end to end. No blockers.

What's correct

  • Spendable classification mirrors the coin-selection gate. ClassifyCanonicalityBalance (vtxo/filter.go:130) counts a VTXO as spendable iff availability.Usable(), which is exactly the predicate gateUnavailableLineage uses to admit candidates. So the reported spendable figure can't drift from what coin selection will actually let you spend — the invariant you'd most want held here.
  • No double counting. The three live buckets are disjoint by construction: ClassifyCanonicalityBalance and SumPendingBalance both key off Status, and only SumPendingBalance touches PendingForfeit/Forfeiting/Spending while classification only touches VTXOStatusLive. Exiting VTXOs come from a separate ListVTXOsByStatusLight query. Nothing overlaps.
  • total_confirmed_sat correctly excludes limbo. rpc_server.go:868 sums BoardingConfirmedSat + VtxoBalanceSat (now the spendable-only figure), matching both proto docs.
  • Terminal Invalidated contributes to neither bucket — right call; it's no longer wallet liquidity, and the case availability != batchcanon.Invalidated guard keeps it out of the temporary bucket.
  • Worst-parent semantics flow through LineageCommitmentTxIDsLineageAvailabilityCombineAvailability, and the multi-parent test (TestCanonicalityAwareVTXOBalanceUsesWorstParent) pins it: one reorged parent drags the whole VTXO into limbo.
  • Fail-closed error propagation — a broken reader surfaces an RPC Internal error rather than a misleading zero/spendable figure (TestCanonicalityAwareVTXOBalancePropagatesReaderError), consistent with the strictness rationale already documented above the boarding/VTXO fetches.
  • Nil-reader legacy path is behavior-neutral (SumSpendableBalance), and the systest asserts the real durable store across a live reorg → reconfirm cycle at all three states (Provisional → ReorgedOut → reconfirmed Provisional), using the same ListLiveVTXOs + ClassifyCanonicalityBalance path the RPC uses.
  • Proto changes are purely additive (field 6 on BalanceResponse, field 11 on GetBalanceResponse), no field-number collisions; generated .pb.go was regenerated, not hand-edited.

Observations (all non-blocking)

1. Proto docs omit the unseen case that the bucket actually includes. The temporary bucket is everything that is !Usable() && != Invalidated, which per the test (unseenBatch → 50k in unavailable) and the commit message ("reorged, conflicted, unseen, and reconciling") deliberately includes AvailabilityUnknown (unseen lineage) and LineageReconciling. But the field docs enumerate a narrower set:

  • wallet.proto:955 — "while its batch lineage is in reorg/conflict limbo or is being reconciled"
  • daemon.proto:564 — "This includes reorg/conflict limbo and lineage reconciliation"

Neither mentions unseen/unregistered lineage. Since a client rendering this figure will see freshly-created-but-unconfirmed live VTXOs land here, it's worth a word in the doc so the wire contract matches the behavior the test locks in. Fix this →

2. UX consideration for the unseen window. Following from (1): a just-received round/OOR VTXO whose commitment isn't confirmed yet is Live but AvailabilityUnknown, so it will briefly appear as temporarily_unavailable rather than as inbound-pending. That's the fail-closed-correct answer (it truly isn't spendable yet), but "temporarily unavailable" reads more alarming than "pending" for a normal first-confirmation wait. Worth a conscious confirmation that this is the intended surface, since it's the one state here that isn't actually a fault condition.

3. Minor nil-guard asymmetry. The non-nil path guards desc == nil (filter.go:117), but the nil-reader path delegates to SumSpendableBalanceFilterDescriptors, which dereferences d.Status without a nil check and would panic on a nil element. ListLiveVTXOs never yields nil entries so this isn't reachable today; flagging only because the two paths now differ on an input the new one explicitly defends against.

4. Per-RPC store fan-out. GetBalance now issues one GetBatch per (live VTXO × distinct parent) via LineageAvailability. This matches the gate's existing per-candidate pattern and is fine at current scales, but it does turn a previously in-memory sum into O(N·P) store reads on a hot read RPC. Not worth changing now — noting for when wallet VTXO counts grow.

Nice, tight change — the classification predicate reusing Usable() is exactly the right way to keep the balance figure and the admission gate from ever disagreeing.
· branch reorg-lineage-producers

@ellemouton
ellemouton force-pushed the reorg-lineage-producers branch from 9d19e5d to 97bd459 Compare July 20, 2026 16:24
@ellemouton
ellemouton force-pushed the reorg-batchcanon-core branch from 095039e to 7f636e7 Compare July 20, 2026 16:24
@ellemouton

Copy link
Copy Markdown
Member Author

@claude review this — final head is now rebased onto current main and includes canonicality-aware temporarily-unavailable balance reporting plus the real reorg systest.

@claude

claude Bot commented Jul 20, 2026

Copy link
Copy Markdown

Claude finished @ellemouton's task in 2m 8s —— View job


Review: PR #990 — final head (rebased onto main)

  • Read diff vs origin/reorg-batchcanon-core
  • Review evidence authentication (batchcanon)
  • Review producer wiring (round, oor, vtxo)
  • Review balance classification + RPC wiring (vtxo/filter.go, waved)
  • Review reorg systest
  • Post review

Re-reviewed the full stack (7 commits) against origin/reorg-batchcanon-core after the rebase onto current main. The rebase is clean — no regressions in the producer wiring or the two-graph separation that the two prior passes signed off on, and the balance-reporting increment is now folded in coherently. No blockers. Everything flagged below is non-blocking, and most of it is a status check on prior observations rather than anything new.

Rebase / correctness confirmations

  • Balance classification still mirrors the coin-selection gate. ClassifyCanonicalityBalance (vtxo/filter.go:130) counts value as spendable iff availability.Usable() — the same predicate coin selection admits on — and buckets !Usable() && != Invalidated as temporarily unavailable. The reported figure can't drift from what a spend will actually let you use. Terminal Invalidated contributes to neither bucket. ✅
  • total_confirmed_sat excludes limbo. GetBalance now sums the spendable-only VtxoBalanceSat (rpc_server.go), so both the new vtxo_temporarily_unavailable_sat (daemon field 11) and temporarily_unavailable_sat (wallet field 6) are carved out of the confirmed total. Proto changes are purely additive. ✅
  • Explicit-OOR-input gate is symmetric with wallet coin selection. requireCustomInputLineage (rpc_server.go) runs batchcanon.LineageBlocked over LineageCommitmentTxIDs(desc) before checkpoint signing, mapping Invalidated → FailedPrecondition and reorg/reconcile limbo → Unavailable, and no-ops when batchCanonStore is nil (legacy). Closes the bypass where a reorged-out VTXO could reach signing through the custom-input path. ✅
  • Registration-before-exposure ordering still holds: round registers inside InputSigSentState.ProcessEvent before the VTXOStore persist, under context.WithoutCancel, and fails the round at its pre-exposure checkpoint on registration error (round/transitions.go). oor/vtxo register before materialize/save. Fail-closed direction is preserved throughout. ✅
  • New systest exercises the real durable store. assertCanonicalityBalance (systest/batch_canonicality_gate_test.go) drives ListLiveVTXOs + ClassifyCanonicalityBalance against the real batchcanon.Reader across Provisional → ReorgedOut → reconfirmed-Provisional, asserting the exact spendable/unavailable split at each state — the same path GetBalance uses. ✅

Non-blocking observations (status of prior notes)

1. Proto docs still omit the "unseen" bucket. ClassifyCanonicalityBalance puts AvailabilityUnknown (a freshly-created, not-yet-confirmed live VTXO) into the temporary bucket, but wallet.proto:952 ("reorg/conflict limbo or is being reconciled") and daemon.proto:562 ("reorg/conflict limbo and lineage reconciliation") don't mention unseen/unregistered lineage. A client will briefly render a normal first-confirmation wait as temporarily_unavailable; worth one word so the wire contract matches the behavior the systest locks in. (Carried over from the balance-PR review; still open.) Fix this →

2. CSVExpiryDelta positivity is still only guarded on the receive path. The receive paths reject CSVExpiryDelta <= 0 (batchcanon/rpc_evidence.go:176, oor/incoming_batch_registration.go:116), but the round producer sets CSVExpiryDelta: int32(state.SweepDelay) (round/transitions.go) with no guard, and validateRegistration (batchcanon/manager.go:305) checks every field except CSV. So a zero/negative SweepDelay would register and make EffectiveExpiry() collapse to ConfirmationHeight. SweepDelay is realistically always positive — this is defensive-only, and you've already dispositioned it as intentional. Flagging only that the rebase didn't change it; a one-line CSVExpiryDelta > 0 check in validateRegistration would keep the invariant in one place for both producers.

3. Minor nil-guard asymmetry. The non-nil path guards desc == nil (vtxo/filter.go:117), but the nil-reader path delegates to SumSpendableBalance → FilterDescriptors, which dereferences d.Status without a nil check. ListLiveVTXOs never yields nil entries, so unreachable today — noting only because the two paths now differ on an input the new one explicitly defends against.

Note on validation

I could not run go build / go test in this environment (build command not permitted here), so the confirmations above are from code reading against the diff. The PR body reports make fmt-changed, make lint-changed-local, make tidy-module-check, go test ./vtxo ./waved ./oor ./round, and F2/F3/F6/F7 real-chain systests all passing.

The authentication layer and the reuse of Usable() as the single admission-and-balance predicate remain the strongest parts of this change — balance and coin selection are structurally incapable of disagreeing.
· branch reorg-lineage-producers

@ellemouton

Copy link
Copy Markdown
Member Author

@claude review this — addressed the two actionable non-blocking notes: temporary-balance docs now include unseen/unregistered lineage, and all batch registrations now enforce positive CSV expiry with regression coverage.

@ellemouton

Copy link
Copy Markdown
Member Author

Review disposition: temporary-balance wire docs and positive CSV registration validation are fixed in the current head. The nil-reader descriptor note is unreachable because ListLiveVTXOs does not return nil descriptors; no correctness finding is left open.

@claude

claude Bot commented Jul 20, 2026

Copy link
Copy Markdown

Claude finished @ellemouton's task in 2m 55s —— View job


Review: follow-up fixes (CSV positivity + temporary-balance docs)

  • Review CSV-expiry positivity commit (c4cd79b)
  • Review temporary-balance docs commit (37ab56c)
  • Confirm regression coverage
  • Post review

Reviewed the two new commits on top of the previously-signed-off stack. Both notes are addressed correctly. One small formatting issue that will fail CI.

✅ CSV-expiry positivity (c4cd79b) — correctly centralized

Putting the req.CSVExpiryDelta <= 0 check in validateRegistration (batchcanon/manager.go:317) is the right fix, and better than the per-producer guard I suggested. validateRegistration is the single chokepoint every registration flows through — both the actor path (handleRegisterBatch, manager.go:225) and the store path (record.go:65) — so this now covers the round producer (SweepDelay), the receive/OOR import paths, and any future producer with one invariant, exactly the "keep it in one place" outcome. EffectiveExpiry() can no longer collapse to ConfirmationHeight from a zero/negative delta. Regression coverage is good: the new "non-positive CSV expiry delta" table case in registration_validation_test.go:39 asserts the rejection, and the two harness defaults (manager_test.go, validRegistrationRequest) keep existing fixtures valid.

✅ Temporary-balance docs (37ab56c) — matches behavior

Both field docs now read "unseen, unregistered, in reorg/conflict limbo, or being reconciled" (wallet.proto:954, daemon.proto:562), which correctly describes what ClassifyCanonicalityBalance actually buckets (AvailabilityUnknown / LineageReconciling → temporary). The wire contract now matches the behavior the systest locks in, closing the doc gap from the prior two reviews. .pb.go was regenerated (not hand-edited), and proto and generated comments agree.


🔴 One actionable: registration_validation_test.go:138 is not gofmt-clean

The new field breaks the struct's alignment block:

BatchOutputIndex:     1,
ConfirmationPkScript: watchScript,
CSVExpiryDelta:        144,   // <- one extra space; value is shifted a column right

ConfirmationPkScript is the longest key in the group, so gofmt wants 7 spaces after CSVExpiryDelta: (aligning 144 under watchScript), but there are 8. This will fail make fmt-changed-check / the lint gate in CI. A make fmt-changed pass fixes it. Fix this →

(I couldn't run make fmt-changed-check directly in this environment — permission-gated — but the misalignment is visible in the source: 144 sits one column right of watchScript above it.)


Status of remaining prior notes (all non-blocking, unchanged)

  • Nil-guard asymmetry (vtxo/filter.go:117 guards desc == nil; the nil-reader path via SumSpendableBalance → FilterDescriptors does not) — still present, still unreachable since ListLiveVTXOs yields no nil entries. Informational only.
  • Per-RPC store fan-out in GetBalance (O(N·P) GetBatch reads) — unchanged, fine at current scales.

Everything else from the three prior passes still holds. Nice, focused follow-up — the CSV fix in particular lands the invariant in the cleanest possible spot.
· branch reorg-lineage-producers

@ellemouton
ellemouton force-pushed the reorg-lineage-producers branch from 37ab56c to 6c63040 Compare July 20, 2026 16:49
@ellemouton
ellemouton force-pushed the reorg-batchcanon-core branch from 9731c80 to e3bcacb Compare August 5, 2026 22:15
@ellemouton
ellemouton force-pushed the reorg-lineage-producers branch from b541977 to 58d2862 Compare August 5, 2026 22:18
@ellemouton
ellemouton force-pushed the reorg-batchcanon-core branch from e3bcacb to f31cccf Compare August 5, 2026 22:59
@ellemouton
ellemouton force-pushed the reorg-lineage-producers branch from 58d2862 to 2d07c20 Compare August 5, 2026 23:03
@ellemouton
ellemouton force-pushed the reorg-batchcanon-core branch from f31cccf to dbadb1b Compare August 5, 2026 23:06
ellemouton added a commit that referenced this pull request Aug 5, 2026
Adapts #990's reorg lineage/OOR-gating code to the current lint gates: drops a
stray duplicated godoc line left by a merge on LineageCommitmentTxIDs, and adds
justified funlen/nestif directives where main's growth plus the canonicality
gate pushed the incoming-VTXO dispatch and custom-input path past the
thresholds. No functional changes.
@ellemouton
ellemouton force-pushed the reorg-lineage-producers branch from 2d07c20 to 811794b Compare August 5, 2026 23:09
ellemouton added a commit that referenced this pull request Aug 6, 2026
Adapts #990's reorg lineage/OOR-gating code to the current lint gates:
drops a stray duplicated godoc line left by a merge on
LineageCommitmentTxIDs, and adds justified funlen/nestif directives
where main's growth plus the canonicality gate pushed the incoming-VTXO
dispatch and custom-input path past the thresholds. No functional
changes.
@ellemouton
ellemouton force-pushed the reorg-lineage-producers branch from 811794b to 3cd0109 Compare August 6, 2026 21:40
@litbot-9000

litbot-9000 commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

📚 Per-package doc drift

This PR's Go changes left the per-package docs stale for arkrpc, batchcanon, cmd/wavecli/waveclicommands, oor, round, rpc/wavewalletrpc, sdk/wavewalletdk, swapwallet, txconfirm, vtxo, and waved — mainly the new batch-canonicality receive/registration surface (BatchEvidence, EvidenceFromAncestryPaths, the round/OOR/incoming RegisterBatch seams, the F-H1 lineage binds), the Reader-based fail-closed admission gate now reading through Manager.GetBatch, the force-unroll and custom-OOR-input gates, the new temporarily_unavailable_sat balance bucket, and txconfirm's reorg-aware terminal-notification key.

db and systest were also in SCOPE but need no doc change: db/round_store.go only threads an already-documented column, and the systest edits are test-only.

Proposed changes — diff of the 11 CLAUDE.md files
diff --git a/arkrpc/CLAUDE.md b/arkrpc/CLAUDE.md
index 7e50d5d0..f41ddeff 100644
--- a/arkrpc/CLAUDE.md
+++ b/arkrpc/CLAUDE.md
@@ -22,12 +22,22 @@ Proto source: `arkrpc/ark.proto`, `arkrpc/indexer.proto`.
   PSBTs that `IncomingOOREvent` intentionally omits.
 - `VTXO` — Phase 2 query response from `ListVTXOsByScripts`. Carries
   authoritative lineage metadata including the structured `TreePath`.
+- `AncestryPath` — One commitment-tree fragment of a VTXO's lineage. Beyond
+  the tree path and `commitment_height` it carries the additive
+  canonicality-evidence extension: `commitment_tx` (the serialized commitment
+  transaction, whose hash must equal `commitment_txid` and the tree path's
+  batch outpoint), `commitment_inputs`, and `commitment_csv_expiry_delta` (the
+  batch sweep delay). `batchcanon.EvidenceFromAncestryPaths` authenticates
+  these into `batchcanon.BatchEvidence`.
+- `CommitmentInputEvidence` — Binds one commitment input to the full previous
+  output (`outpoint` + `prev_out`) needed to arm authenticated conflict
+  observation. One record per actual transaction input, in input order.
 
 ## Relationships
 
 - **Depends on**: `lib/tree` (for conversion utilities in `tree_path_convert.go`).
-- **Depended on by**: `indexer`, `waved`, `serverconn`, `oor` (uses generated
-  clients and conversion helpers).
+- **Depended on by**: `indexer`, `waved`, `serverconn`, `oor`, `vtxo`,
+  `batchcanon` (uses generated clients and conversion helpers).
 
 ## Invariants
 
@@ -36,3 +46,8 @@ Proto source: `arkrpc/ark.proto`, `arkrpc/indexer.proto`.
   reproduce the original tree (excluding derived `FinalKey` fields).
 - Child iteration during flattening is sorted by output index for
   deterministic serialization.
+- The `AncestryPath` canonicality-evidence fields are additive and
+  all-or-nothing per response: an indexer that predates them omits all three,
+  which clients treat as "no evidence" (rollout-compatible, lineage stays
+  unusable under the fail-closed gate). A partially-populated extension is a
+  protocol error, not a degraded response.
diff --git a/batchcanon/CLAUDE.md b/batchcanon/CLAUDE.md
index 667521d9..c9774e21 100644
--- a/batchcanon/CLAUDE.md
+++ b/batchcanon/CLAUDE.md
@@ -8,12 +8,14 @@ Client-side **batch canonicality authority** for the reorg-safety epic
 **fail-closed lineage availability** the VTXO manager's admission gate and the
 round/OOR producers consume.
 
-It owns three things: the durable, reorg-aware **data model** (`Record`,
+It owns four things: the durable, reorg-aware **data model** (`Record`,
 `ConsumedInput`, `ConsumerEdge`), the dependency-light **reducer** that derives
 canonicality `State` and `Availability` from a complete current-chain
-observation, and the **`Manager`** actor that arms reorg-aware watches, drives
+observation, the **`Manager`** actor that arms reorg-aware watches, drives
 the versioned snapshot/readiness restart barrier, and interprets chainsource
-observations into state.
+observations into state, and the **receive-path evidence adapter**
+(`BatchEvidence`, `EvidenceFromAncestryPaths`) that turns indexer ancestry into
+the authenticated registration inputs producers hand the `Manager`.
 
 Observation (chainsource) → interpretation (this package) → admission (vtxo)
 stays a strict split: chainsource reports raw reversible facts, this package
@@ -45,6 +47,32 @@ decides canonicality, and the VTXO manager remains the admission boundary.
   `PkScript` (required to arm the reorg-aware spend watch) and persisted
   `Conflicting`/`ConflictFinal` flags so restart reconciliation cannot
   transiently downgrade a persisted conflict.
+- `BatchEvidence` — the authenticated, behavior-free description of one
+  commitment tx needed to register and observe it: `BatchTxID`, `BatchTx`,
+  `BatchOutputIndex`, `ConfirmationPkScript`, `WatchHeightHint`,
+  `CSVExpiryDelta`, `ConsumedInputs`. Receive paths carry it separately from
+  the VTXOs that depend on it, so one commitment is reused across multi-parent
+  OOR lineage. `RegisterRequest(dependentVTXOs)` builds a
+  `RegisterBatchRequest` (deep-copying every byte slice) without mutating the
+  evidence; `Validate()` re-runs the manager's registration checks; `Equal()`
+  compares immutable watch evidence across duplicate ancestry fragments.
+  `EvidenceFromAncestryPaths(paths)` (`rpc_evidence.go`) builds it from
+  `arkrpc.AncestryPath`: the serialized commitment tx must hash to the claimed
+  txid, the tree path's batch outpoint/output must match the real tx output
+  byte-for-byte, and every `TxIn` must have a matching input record in order.
+  **Trust boundary**: input outpoints and ordering are authenticated against
+  the tx itself, but each prevout's value/pkScript is indexer-supplied and only
+  shape-checked. An indexer that omits the whole evidence extension yields no
+  evidence at all (older-indexer compatibility) — the fail-closed gate then
+  keeps that unregistered lineage unusable; a *partial* extension is rejected.
+- `ConsumerForfeitPersistedMsg` — inbound `Manager` message carrying the
+  `ConsumerBatch` whose `ForfeitedBy` marker just became durable. It closes an
+  ordering race: a consumer batch can reach `StateConflictFinalized` (driving
+  `restoreProvisionalConsumers`) before the forfeiture marker exists, so the
+  terminal restore's revision compare-and-swap defers and no further batchcanon
+  event is guaranteed. The VTXO actor sends this the moment `MarkForfeited`
+  succeeds so resolution can progress on the newly-durable evidence; the
+  restart-time redrive stays the backstop.
 - `ConsumerEdge` — the **logical value-lineage** edge (a VTXO consumed by a
   batch), separate from the on-chain `ConsumedInputs` graph. Carries
   `ExpectedRevision` and the full `CreatorLineage`; used by the terminal
@@ -62,20 +90,35 @@ decides canonicality, and the VTXO manager remains the admission boundary.
 - `Manager` — the actor interpreter. Arms one reorg-aware confirmation watch
   on the batch tx plus one reorg-aware spend watch per consumed input;
   registration cross-checks the serialized `BatchTx` (hash == `BatchTxID`,
-  output/pkScript bound, every `TxIn` registered) before a row can reach
-  `Ready`. `Reconcile(g)` runs the restart barrier: it opens a new observation
-  generation, re-arms watches, requires an explicit current fact per subject,
-  and only installs `Ready(g)` + derived state atomically — admission stays
-  closed until then, so a persisted conflict can never transiently look usable.
+  output/pkScript bound, every `TxIn` registered, `CSVExpiryDelta > 0`) before
+  a row can reach `Ready`. `Reconcile(g)` runs the restart barrier: it opens a
+  new observation generation, re-arms watches, requires an explicit current
+  fact per subject, and only installs `Ready(g)` + derived state atomically —
+  admission stays closed until then, so a persisted conflict can never
+  transiently look usable. The `Manager` itself implements `Reader` via
+  `GetBatch`, which overlays the in-memory watch state on the durable row under
+  the same mutex `Receive` holds: a watch that is not ready (most importantly
+  after an `ApplyObservation` write failed) forces the returned record
+  not-ready, so the gate derives `LineageReconciling` instead of trusting a
+  stale-but-`Ready` durable row. The overlay only ever downgrades
+  ready→not-ready, so it is strictly fail-closed. **Admission consumers must be
+  wired to the `Manager`, not the raw `Store`** (`waved` does this whenever the
+  gate is enabled).
 
 ## Relationships
 
 - **Depends on**: `btcd/chainhash`, `btcd/wire`, `lnd/fn/v2` only (plus
-  `baselib/actor` + `chainsource` for the `Manager`). The reducer/model
+  `baselib/actor` + `chainsource` for the `Manager`, and `arkrpc` for the
+  receive-path evidence adapter in `rpc_evidence.go`). The reducer/model
   (`state.go`, `availability.go`, `record.go`) is deliberately dependency-light
   so the **server can reuse the same reducer** (lumos#454 Server PR2).
-- **Depended on by**: `db` (concrete `Store`), `vtxo` (admission gate), and the
-  round/OOR producers (registration before exposure).
+- **Depended on by**: `db` (concrete `Store`), `vtxo` (admission gate, balance
+  classification, incoming-VTXO registration), `round` and `oor` (producer and
+  receive-path registration before exposure), `waved` (wiring, `batchRegistrar`
+  adapter, RPC-level lineage gates).
+- **Receives**: `RegisterBatchRequest` ← `round` / `oor` / `vtxo` (through
+  `waved`'s `batchRegistrar` Ask adapter); `ConsumerForfeitPersistedMsg` ←
+  `vtxo` actor after a forfeiture marker is persisted.
 
 ## Invariants
 
@@ -85,7 +128,15 @@ decides canonicality, and the VTXO manager remains the admission boundary.
   compatibility hint.
 - Registration authenticates the serialized commitment tx (hash + full `TxIn`
   set) before a record reaches `Ready`; an omitted or unauthenticated input
-  keeps the record unavailable.
+  keeps the record unavailable. `CSVExpiryDelta` must be positive — a zero
+  delta would derive an effective expiry equal to the confirmation height.
+- Evidence coverage is all-or-nothing per receive: either no ancestry path
+  carries the evidence extension (older indexer — nothing is registered and the
+  gate keeps the lineage unusable) or every path carries it. Duplicate
+  fragments naming the same commitment must agree byte-for-byte.
+- Admission reads go through `Manager.GetBatch`, never the durable `Store`
+  directly, so a failed observation write closes admission immediately instead
+  of leaving a stale `Ready` row admissible until restart.
 - Two graphs: the on-chain `ConsumedInputs` (`TxIn`) graph drives conflict
   observation; the logical `ConsumerEdge` graph drives inherited lineage and
   conditional restore.
diff --git a/cmd/wavecli/waveclicommands/CLAUDE.md b/cmd/wavecli/waveclicommands/CLAUDE.md
index 8af7d121..53fe87d7 100644
--- a/cmd/wavecli/waveclicommands/CLAUDE.md
+++ b/cmd/wavecli/waveclicommands/CLAUDE.md
@@ -40,7 +40,7 @@ unchanged).
 | `recv` | `wavewalletrpc.Recv` / `wavewalletrpc.Deposit` | Inbound. `--offchain` (default) returns a Lightning invoice; `--onchain` returns a boarding address |
 | `activity` | `wavewalletrpc.List` | Unified wallet activity view. Defaults to table output; global `--json` or `--format json` returns structured JSON. `--pending` and `--kind` narrow rows |
 | `activity inspect <id>` | `wavewalletrpc.InspectActivity` | Correlated swap/VTXO/ledger detail for one activity entry |
-| `balance` | `wavewalletrpc.Balance` | Flat balance (confirmed_sat, pending_in_sat, pending_out_sat) |
+| `balance` | `wavewalletrpc.Balance` | Flat balance (confirmed_sat, pending_in_sat, pending_out_sat, temporarily_unavailable_sat) |
 | `exit --outpoint TXID:VOUT` | `wavewalletrpc.Exit` | Queue a cooperative leave by default; unilateral unroll only fires with `--force-unroll-ack I_KNOW_WHAT_I_AM_DOING` |
 | `exit status --outpoint TXID:VOUT` | `wavewalletrpc.ExitStatus` | Query an exit/unroll job's status (proxies GetUnrollStatus) |
 | `exit summary` | `wavewalletrpc.ExitSummary` | Aggregate totals across all in-progress exits |
diff --git a/oor/CLAUDE.md b/oor/CLAUDE.md
index bb983c14..fa521aab 100644
--- a/oor/CLAUDE.md
+++ b/oor/CLAUDE.md
@@ -31,12 +31,39 @@ For field-level detail, use `go doc github.com/lightninglabs/wavelength/oor.<Sym
 - `ReceiveLimits` / `DefaultReceiveLimits` — defense-in-depth bounds on
   incoming receive (`MaxCheckpoints`, `MaxVTXOMatches`, `MaxMailboxItems`,
   `MaxMailboxScriptBytes`, `MaxConcurrentIncomingSessions`).
+- `BatchRegistrar` — narrow seam
+  (`RegisterBatch(ctx, *batchcanon.RegisterBatchRequest) error`) that durably
+  registers authenticated commitment evidence and arms its reorg-aware watches
+  before an incoming VTXO can be exposed. Threaded through
+  `OORRegistryConfig` → `SessionActorConfig` and onto
+  `LocalPersistenceOutboxHandler`.
+- `IncomingLineageVerifier` — injected F-H1 bind
+  (`func(ancestry, evidence, chainDepth, coinInputs) error`); production wires
+  `vtxo.VerifyOORAncestryLineage`. It is a function field rather than a direct
+  call so registration-logic tests can use mock (unsigned) trees with a nil
+  verifier; nil skips the binding.
+- `RegisterIncomingBatchEvidence(...)` / `BaseCoinInputs(rootCheckpoints,
+  ancestors)` (`incoming_batch_registration.go`) — register every distinct
+  commitment named by the resolved metadata (replays merge dependents
+  idempotently), and derive the committed base-VTXO outpoints the received coin
+  descends from across the full checkpoint/ark chain. `BaseCoinInputs` skips
+  prevouts that spend an ancestor package's output, so it terminates on the
+  BASE checkpoints (the real commitment-tree leaves) for single- and multi-hop
+  receives alike.
+- `IncomingVTXOMetadata.BatchEvidence []batchcanon.BatchEvidence` —
+  authenticates every distinct commitment named by `Ancestry` and supplies the
+  inputs the reorg-aware conflict watches need. Same-commitment multi-leaf
+  ancestry shares one evidence item. It is part of the durable incoming
+  snapshot (its own TLV sub-stream per item, so additive fields stay
+  replay-compatible).
 
 ## Relationships
 
 - **Depends on**: `baselib/protofsm` (FSM), `baselib/actor` (durable actor
   framework), `serverconn` (submit/finalize/query transport), `vtxo`
-  (materialization + status), `ledger` (`Sink`, accounting emission),
+  (materialization + status, `Ancestry`, the F-H1 lineage verifier),
+  `batchcanon` (`BatchEvidence`, `RegisterBatchRequest`,
+  `EvidenceFromAncestryPaths`), `ledger` (`Sink`, accounting emission),
   `timeout` (`TimeoutActor` retry scheduling), `lib/arkscript` (checkpoint
   policy, collab tapleaf), `arkrpc` (indexer response types), `lnd/input`
   (signer interface for inline Ark/checkpoint signing).
@@ -46,7 +73,9 @@ For field-level detail, use `go doc github.com/lightninglabs/wavelength/oor.<Sym
   `SendFinalizePackageRequest` / `SendIncomingAckRequest` and durable query
   requests (`QueryIncomingTransferRequest`, `QueryIncomingMetadataRequest`)
   -> `serverconn`; `MaterializeIncomingVTXOsRequest` -> wallet/VTXO store;
-  `VTXOSentMsg`/`VTXOReceivedMsg` -> `ledger` (when `LedgerSink` is set).
+  `VTXOSentMsg`/`VTXOReceivedMsg` -> `ledger` (when `LedgerSink` is set);
+  `RegisterBatchRequest` -> the `batchcanon` manager (via `BatchRegistrar`,
+  before incoming materialization is staged).
   Receives `SubmitAcceptedEvent` / `FinalizeAcceptedEvent` /
   `ResolveIncomingTransferRequest` <- `serverconn` event router;
   `StartTransferRequest` / `DriveEventRequest` / `ListSessionsRequest` <-
@@ -119,6 +148,23 @@ For field-level detail, use `go doc github.com/lightninglabs/wavelength/oor.<Sym
 - Incoming ancestor packages are capped at `maxAncestorPackages = 64`
   checkpoints, and indexer-supplied `tree_depth` is cross-checked against the
   reconstructed path via `arkrpc.ValidateAncestryPathDepth`.
+- Incoming batch registration happens BEFORE materialization and OUTSIDE the
+  actor's commit transaction: `driveOutboxEvents` registers on the
+  `MaterializeIncomingVTXOsRequest` before staging the write, and
+  `LocalPersistenceOutboxHandler` registers only when there is no actor DB
+  transaction on the context (`hasActorDBTx`) so the synchronous path does not
+  double-register. A registration failure fails the receive rather than
+  exposing a VTXO whose lineage the fail-closed gate has never seen.
+- Evidence coverage is validated per match before anything is registered:
+  every item must `Validate()`, carry a positive `CSVExpiryDelta`, name a
+  commitment that appears in the match's `Ancestry`, not duplicate another
+  item, and together cover every distinct ancestry commitment. Duplicate output
+  indices across matches are rejected, and two matches naming the same
+  commitment must supply `Equal` evidence. Completely absent evidence (older
+  indexer) registers nothing and stays rollout-compatible.
+- The F-H1 bind runs before any watch is armed and only when evidence is
+  present, so an absent extension degrades the receive instead of failing it;
+  a present-but-unbindable ancestry fails closed.
 - Server-side lineage-cap rejection surfaces as a typed `*ErrLineageTooLarge`
   via `ClassifySubmitError`, so wallet callers can switch on the cause
   without depending on the `oorpb` proto type.
diff --git a/round/CLAUDE.md b/round/CLAUDE.md
index 630d6bc8..64d48765 100644
--- a/round/CLAUDE.md
+++ b/round/CLAUDE.md
@@ -50,7 +50,9 @@ state transitions and validation rules live under [Invariants](#invariants).
 - `VTXOIntent`, `RoundVTXORequest`, `BoardingIntent`, `ClientVTXO` —
   pre-registration request / signing wrapper / boarding wrapper / full
   owned-VTXO descriptor (the latter carries `Origin`, `CommitmentTxID`,
-  `BatchExpiry`, `CreatedHeight`, and `Ancestry []types.Ancestry`).
+  `BatchExpiry`, `CreatedHeight`, `Ancestry []types.Ancestry`, and
+  `BusinessRevision`, the persisted lifecycle revision a refresh registration
+  binds `+1` of on the consumer edge).
 
 ### Persistence & Wallet Interfaces (`interfaces.go`)
 
@@ -65,6 +67,12 @@ state transitions and validation rules live under [Invariants](#invariants).
   Called at intent-build time for change/refresh outputs and inside
   `handleRegisterIntent` for entries with a non-zero `KeyLocator`.
 - `VTXOStore`, `RoundStore` — VTXO and round FSM persistence.
+- `RoundBatchRegistrar` — `RegisterBatch(ctx, *batchcanon.RegisterBatchRequest)`.
+  Installs authenticated commitment evidence (and the logical consumer edges
+  for every forfeited input) before the round exposes any VTXO derived from
+  that commitment. Wired from `RoundClientConfig.BatchRegistrar` onto every
+  `ClientEnvironment` the actor builds; `nil` (focused tests) skips
+  registration.
 
 ### Actor Layer (`actor.go`, `actor_messages.go`, `vtxo_messages.go`)
 
@@ -123,7 +131,8 @@ state transitions and validation rules live under [Invariants](#invariants).
 
 - **Depends on**: `baselib/protofsm` (FSM engine), `baselib/actor` (actor
   primitives: `ActorRef`, `ActorSystem`, `BaseMessage`), `lib/actormsg`
-  (mailbox marker interfaces), `lib/tree`, `lib/types`, `lib/arkscript`,
+  (mailbox marker interfaces), `batchcanon` (`RegisterBatchRequest`,
+  `ConsumedInput`, `ConsumerEdge`), `lib/tree`, `lib/types`, `lib/arkscript`,
   `lib/bip322` (join-round BIP-322 auth signing), `rpc/roundpb` (wire proto
   types via `FromProto`), `wallet`, `ledger` (`Sink` + `VTXOReceivedMsg` /
   `Source*` constants), `timeout`, `google/uuid`.
@@ -135,6 +144,9 @@ state transitions and validation rules live under [Invariants](#invariants).
 - **Sends → `vtxo`**: forfeit/spend/block-epoch events listed above;
   manager-level `VTXOCreatedNotification`, `VTXOTerminatedMsg`.
 - **Sends → `wallet`**: `RegisterConfirmationRequest`.
+- **Sends → `batchcanon`** (when `BatchRegistrar` is set, on the
+  `InputSigSent → Confirmed` transition, before VTXO persistence):
+  `RegisterBatchRequest`.
 - **Sends → `OwnedScriptRegistrar`** (waved adapter over the OOR
   artifact store): `RegisterOwnedScript(pkScript, ownerKey)`.
 - **Sends → `ledger`** (when `LedgerSink` is `fn.Some`), origin-routed
@@ -190,6 +202,19 @@ state transitions and validation rules live under [Invariants](#invariants).
 - Persisted VTXO ownership uses `OwnerKey` (not `SigningKey`). For
   directed sends, the sender's signing key participates in MuSig2 tree
   construction but the recipient's owner key determines ownership.
+- **Batch canonicality is registered before VTXO exposure.** On the
+  `InputSigSent → Confirmed` transition, `roundBatchRegistration` builds a
+  complete `RegisterBatchRequest` from the retained commitment PSBT — every
+  actual `TxIn` paired with its `WitnessUtxo` (a missing one fails the
+  transition), `BatchOutputIndex` mirroring `confirmationWatchScript` so the
+  persisted evidence binds the same output the chain backend watches,
+  `CSVExpiryDelta` from the round's `SweepDelay`, `WatchHeightHint` from
+  `env.StartHeight` (the round's pre-broadcast height), plus one
+  `ConsumerEdge` per forfeited VTXO carrying `BusinessRevision + 1` and that
+  VTXO's full creator lineage. Registration runs under
+  `context.WithoutCancel` and must succeed before any VTXO is persisted:
+  the admission gate is fail-closed, so a failure leaves the round at its
+  pre-exposure checkpoint and no new liquidity becomes selectable.
 - Local-balance persistence on confirmation is driven by
   `OwnedScriptChecker.IsOwnedScript(pkScript)` — `buildOwnedClientVTXOs`
   skips any VTXO whose pkScript the checker does not recognize. The
diff --git a/rpc/wavewalletrpc/CLAUDE.md b/rpc/wavewalletrpc/CLAUDE.md
index f4b679c5..969b926c 100644
--- a/rpc/wavewalletrpc/CLAUDE.md
+++ b/rpc/wavewalletrpc/CLAUDE.md
@@ -23,7 +23,7 @@ Proto source: `rpc/wavewalletrpc/wallet.proto`.
 | `Recv` | Inbound Lightning invoice (offchain receive) |
 | `List` | Unified wallet view; `ListView` selects activity/vtxos/onchain |
 | `Deposit` | Fresh boarding address (used by `recv --onchain`) |
-| `Balance` | Flat balance (confirmed / pending_in / pending_out) |
+| `Balance` | Flat balance (confirmed / pending_in / pending_out / temporarily_unavailable) |
 | `Status` | Daemon + wallet readiness summary |
 | `GetExitPlan` | Preview unilateral-exit readiness/funding for one VTXO |
 | `SweepWallet` | Preview/broadcast a backing-wallet sweep to an address |
@@ -56,6 +56,11 @@ id; unlike `List` it may leak internal correlators, so it is kept out of
 - `ExitJobStatus` — Enum collapsing the underlying unroll job phases to a
   short wallet-facing status; shared by `ExitPlanEntry`,
   `ExitStatusResponse`, and `ExitSummaryItem`.
+- `BalanceResponse.temporarily_unavailable_sat` — owned VTXO value that
+  cannot be spent while its batch lineage is unseen, unregistered, in
+  reorg/conflict limbo, or being reconciled. It is excluded from
+  `confirmed_sat` and returns there if canonicality recovers, so it is a
+  distinct bucket rather than a subset of any other field.
 - `FailureDomain` / `Reason*` constants (`failure_reasons.go`) — the
   `google.rpc.ErrorInfo` domain/reason wire contract for failed wallet
   RPCs; existing reason values MUST NOT be renamed.
diff --git a/sdk/wavewalletdk/CLAUDE.md b/sdk/wavewalletdk/CLAUDE.md
index 57da0d01..c74b10fc 100644
--- a/sdk/wavewalletdk/CLAUDE.md
+++ b/sdk/wavewalletdk/CLAUDE.md
@@ -97,7 +97,7 @@ For field-level detail, use `go doc github.com/lightninglabs/wavelength/sdk/wave
 | `GetInfo` | Daemon readiness snapshot (version, network, identity, wallet/server readiness). |
 | `CreateWallet` | Create or import the embedded wallet (auto-generates seed when mnemonic empty); proxies waverpc. |
 | `UnlockWallet` | Unlock an existing wallet; proxies waverpc. |
-| `Balance` | Flat balance (`confirmed_sat`, `pending_in_sat`, `pending_out_sat`). |
+| `Balance` | Flat balance (`ConfirmedSat`, `PendingInSat`, `PendingOutSat`, `TemporarilyUnavailableSat`, plus the credit buckets). `TemporarilyUnavailableSat` is VTXO value blocked by batch-lineage canonicality; it is excluded from `ConfirmedSat` and returns there if the lineage recovers. |
 | `Deposit` | Allocate a fresh boarding address (`recv --onchain` from CLI). |
 | `Receive` | Open a Lightning invoice receive (`recv --offchain`). Returns `{Invoice, Entry}`. |
 | `PrepareSend` | Validate + quote an outbound payment; returns a single-use `SendIntentID`. |
diff --git a/swapwallet/CLAUDE.md b/swapwallet/CLAUDE.md
index 25873a89..42054776 100644
--- a/swapwallet/CLAUDE.md
+++ b/swapwallet/CLAUDE.md
@@ -174,7 +174,11 @@ default builds avoid the swap executor's dependency graph.
   `pending_in_sat` sums `boarding_confirmed_sat +
   boarding_unconfirmed_sat + boarding_adopted_sat`, and
   `pending_out_sat` sums `boarding_pending_sweep_sat +
-  vtxo_pending_sat + vtxo_unilateral_exit_sat`. The two VTXO buckets
+  vtxo_pending_sat + vtxo_unilateral_exit_sat`, and
+  `temporarily_unavailable_sat` passes through
+  `vtxo_temporarily_unavailable_sat` (VTXO value blocked by batch-lineage
+  canonicality — already excluded from `vtxo_balance_sat`, so it is neither
+  confirmed nor pending). The two VTXO buckets
   carry value locked in an in-flight round / OOR spend and in a
   unilateral on-chain exit; folding them into `pending_out_sat` keeps
   the balance from momentarily reading zero mid-refresh, mid-spend, or
diff --git a/txconfirm/CLAUDE.md b/txconfirm/CLAUDE.md
index 1f6c1d5e..81a39101 100644
--- a/txconfirm/CLAUDE.md
+++ b/txconfirm/CLAUDE.md
@@ -144,6 +144,18 @@ For field-level detail, use `go doc github.com/lightninglabs/wavelength/txconfir
 - **Service-key symmetry**: `RegisterConfRequest` and
   `UnregisterConfRequest` both carry `PkScript` so chainsource's
   txid+script keyed service-actor lookup resolves symmetrically.
+- **Terminal notifications survive a reorg-and-reconfirmation**: the
+  idempotency key each terminal delivery dedups on (`terminalNotifyKey`) folds
+  in the tracked entry's reorg epoch (incremented in
+  `handleConfirmationReorged`) and the height of the most recent confirmation.
+  The key therefore stays stable across genuine retries of the same delivery
+  but changes after a reorg, so the re-confirmed `TxConfirmed` reaches a
+  reorg-aware consumer (e.g. an in-progress unilateral exit that rolled its
+  state back on `TxReorged`) instead of being dropped by the durable
+  subscriber's dedup as a duplicate of the pre-reorg confirmation. Height is
+  folded in as well because a restart reconstructs the entry with the epoch
+  reset to zero. An already-evicted entry keeps the zero values — a one-shot
+  terminal delivery with no reorg left to follow.
 - **Terminal eviction**: on Confirmed or Failed, the actor delivers
   terminal notifications first. If a subscriber is slow or transiently
   fails, the tracked entry is retained without a conf watch and
diff --git a/vtxo/CLAUDE.md b/vtxo/CLAUDE.md
index 46bb45fa..e744c608 100644
--- a/vtxo/CLAUDE.md
+++ b/vtxo/CLAUDE.md
@@ -30,7 +30,10 @@ when the local wallet owns the receive script.
   optional `Log`, optional `LedgerSink fn.Option[ledger.Sink]`,
   `ForfeitVTXOActorAskTimeout`, `RefreshFeeQuoter`, `FetchOperatorKey`,
   `ForfeitParticipantSigner`, `TerminalVTXOObserver`, `ExitOutcomeResolver`,
-  and `ReservationStore`. Confirmed exit-cost accounting is emitted by unroll
+  `ReservationStore`, `BatchCanonicality` (a `batchcanon.Reader` — in
+  production the `batchcanon.Manager` itself, so the fail-closed gate reads
+  through its in-memory overlay; nil leaves the gate dormant), and
+  `RedriveConsumerForfeit`. Confirmed exit-cost accounting is emitted by unroll
   after final sweep confirmation. `ForfeitVTXOActorAskTimeout`
   (default 5 s) bounds forfeit and refresh child asks so a blocked child actor
   cannot monopolize the manager until the outer RPC deadline. Zero uses the
@@ -43,7 +46,46 @@ when the local wallet owns the receive script.
   active set. `ExitOutcomeResolver` is called at startup to reconcile VTXOs
   still persisted in `VTXOStatusUnilateralExit` with their terminal job
   outcome. `ReservationStore` is used at startup to sweep orphaned Spending
-  VTXOs.
+  VTXOs. `RedriveConsumerForfeit(ctx, consumerBatch)` is forwarded to each
+  child as `VTXOActorConfig.NotifyForfeitPersisted` and fires once a forfeiture
+  marker is durable, asking the batch canonicality manager to re-resolve a
+  terminal consumer edge that deferred because it ran before the marker
+  existed. Nil disables the redrive (restart reconciliation is the backstop).
+- `LineageCommitmentTxIDs(desc)` — deduped set of commitment txids a VTXO's
+  existence depends on: its direct `CommitmentTxID` plus every distinct
+  cross-commitment ancestor batch in `Ancestry`. Exported so `waved` can apply
+  the same fail-closed gate to RPC paths that bypass the manager's selector
+  (custom OOR inputs). A nil descriptor and the zero hash yield nothing.
+- `ClassifyCanonicalityBalance(ctx, descs, reader)` (`filter.go`) — splits
+  lifecycle-live VTXOs into (spendable, temporarily-unavailable) amounts using
+  `batchcanon.LineageAvailability`. Terminally `Invalidated` lineage counts
+  toward neither bucket (it is history, not liquidity); a nil reader preserves
+  the legacy `SumSpendableBalance` behavior while the gate is disabled.
+- `VerifyReceivedVTXOBinding(ancestry, vtxoScript)` /
+  `VerifyOORAncestryLineage(ancestry, evidence, chainDepth, coinInputs)`
+  (`incoming_ancestry.go`) — cryptographic F-H1 binds run before any reorg
+  watch is armed, so a malicious indexer cannot name a real-but-decoy
+  commitment for the client to watch. Both prove each ancestry tree is a
+  genuine, operator-signed descent from its authenticated commitment output:
+  structure via `tree.Verify`, per-node `FinalKey` recomputed from its
+  cosigners + the sweep tapscript root, `PayToTaproot(key)` equal to the
+  pkScript of the output the node spends (anchored at the root by the
+  authenticated batch output, cascading down via SIGHASH_DEFAULT), then
+  `tree.VerifySigned`. They differ in anchor and terminator: the in-round bind
+  uses the tree's own batch output and requires some leaf to pay the received
+  VTXO script; the OOR bind re-anchors on caller-validated
+  `batchcanon.BatchEvidence` (a durable receive replay cannot be trusted to
+  carry its own anchor) and instead requires every coin input (checkpoint
+  prevout) to be produced by an authenticated leaf. That OOR terminator is
+  unconditional — `chainDepth` is indexer-supplied and deliberately not
+  consulted.
+- `IncomingBatchRegistrar` — narrow interface
+  (`RegisterBatch(ctx, *batchcanon.RegisterBatchRequest) error`) the
+  `IncomingVTXOHandler` uses to durably register fetched evidence and arm its
+  watches *before* the descriptor is persisted or sent to the manager.
+- `IncomingVTXOExtras` — ancestry-fetch result: `Ancestry`, `CreatedHeight`,
+  and `BatchEvidence []batchcanon.BatchEvidence` authenticating every distinct
+  commitment named by `Ancestry`.
 - `CustomForfeitInput` (`lib/actormsg`) — Describes a caller-supplied VTXO
   outside the wallet's live coin set that still needs a local actor to sign
   the exact round forfeit tx. `ActivateCustomForfeitInputsRequest` (sent by
@@ -86,11 +128,14 @@ when the local wallet owns the receive script.
 
 ## Relationships
 
-- **Depends on**: `baselib/protofsm` (FSM engine), `baselib/actor` (actor system), `lib/types` (`Ancestry`), `lib/arkscript` (taproot construction and policy helpers in `IncomingVTXOHandler`), `lib/actormsg` (admission and custom-forfeit message types), `arkrpc` (`IncomingVTXOEvent`), `chainsource` (block epochs), `coinselect` (largest-first VTXO selection), `metrics` (optional `OORTransferReceivedMsg` sink), `ledger` (`Sink` type for compatibility with manager wiring), `unroll` (via `ExitOutcomeResolver` callback wired by `waved`).
+- **Depends on**: `baselib/protofsm` (FSM engine), `baselib/actor` (actor system), `lib/types` (`Ancestry`), `lib/tree` (ancestry-tree verification in the F-H1 binds), `lib/arkscript` (taproot construction and policy helpers in `IncomingVTXOHandler`), `lib/actormsg` (admission and custom-forfeit message types), `arkrpc` (`IncomingVTXOEvent`), `batchcanon` (`Reader` admission gate, `BatchEvidence`, `RegisterBatchRequest`), `chainsource` (block epochs), `coinselect` (largest-first VTXO selection), `metrics` (optional `OORTransferReceivedMsg` sink), `ledger` (`Sink` type for compatibility with manager wiring), `unroll` (via `ExitOutcomeResolver` callback wired by `waved`).
 - **Depended on by**: `round` (triggers forfeit requests), `oor` (incoming VTXOs), `wallet` (admission gating), `db` (persistence), `waved` (wiring, owned-script adapters, incoming event route).
 - **Sends**:
   - → `round` (via manager relay): `RelayToRoundMsg` wrapping `ForfeitSignatureSubmission`
   - → `db` (via outbox): `VTXOStatusUpdate`
+  - → `batchcanon` manager: `RegisterBatchRequest` (incoming-VTXO lineage,
+    before the descriptor is persisted); `ConsumerForfeitPersistedMsg` (via
+    `NotifyForfeitPersisted`, once a forfeiture marker is durable)
   - → `vtxo` manager: `VTXOTerminatedNotification`, `RelayToRoundMsg`, `VTXOsMaterializedNotification` (from `IncomingVTXOHandler`)
   - → `ledger` actor: no direct messages; unroll emits confirmed
     `ExitCostMsg` after sweep confirmation
@@ -172,6 +217,23 @@ when the local wallet owns the receive script.
 - `IncomingVTXOHandler` only handles `VTXO_EVENT_TYPE_CREATED` events. Other event kinds, missing/short outpoints, empty pkScripts, oversized values (`> int64` or `> MaxSatoshi`), and tapscript derivation failures all return success without persisting — they cannot crash the actor or block the indexer push stream. Real DB lookup/save errors are surfaced.
 - Incoming VTXOs are saved with `Status: VTXOStatusLive` and empty `Ancestry` (the round commitment tree is not pushed alongside the event); `db.VTXOPersistenceStore.descriptorToInsertParams` accepts an empty tree-path blob to support this.
 - The `CommitmentTxID` on a materialized incoming VTXO comes from `IncomingVTXOEvent.CommitmentTxid`, which is the round commitment txid — **not** the leaf txid in the outpoint.
+- **The lineage gate covers force-unroll, not just coin selection.**
+  `handleForceUnroll` runs `batchcanon.LineageBlocked` over
+  `LineageCommitmentTxIDs(desc)` before admitting the exit, refusing both
+  objectively `Invalidated` and reversibly-limbo lineage and surfacing the
+  derived availability in the error so callers can tell them apart. Fraud
+  response routes straight into this path, so without the gate a
+  reorged-out VTXO could still enter `UnilateralExitState` and create
+  proof/sweep side effects for funding that has left the chain.
+- **Registration precedes exposure on the receive path.** The
+  `IncomingVTXOHandler` registers every distinct commitment in the fetched
+  `BatchEvidence` (and, in production, verifies the F-H1 bind) before the
+  descriptor is saved or announced. Evidence must be valid, carry a positive
+  `CSVExpiryDelta`, name only commitments present in `Descriptor.Ancestry`,
+  contain no duplicates, and cover *every* ancestry commitment; anything else
+  fails the receive rather than arming a partial watch set. Absent evidence
+  (older indexer) registers nothing and leaves the lineage unusable under the
+  fail-closed gate.
 - Per-subsystem logging: `ManagerConfig.Log` provides an optional instance logger; falls back to `build.LoggerFromContext` (no global mutable loggers).
 
 ## Deep Docs
diff --git a/waved/CLAUDE.md b/waved/CLAUDE.md
index b778f21c..ead9d21f 100644
--- a/waved/CLAUDE.md
+++ b/waved/CLAUDE.md
@@ -25,16 +25,22 @@ For field-level detail, use `go doc github.com/lightninglabs/wavelength/waved.<S
   `UnrollConfig` (unilateral-exit fee-bump cadence and cap), and
   `MaxOperatorFeeSat` (the #270 seal-time fee-cap validated in
   `Config.Validate()`).
+- `batchRegistrar` (`batch_canonicality.go`) — adapts the batch-canonicality
+  actor's Ask API to the narrow synchronous `RegisterBatch` seam that `round`
+  (`round.RoundBatchRegistrar`), `oor` (`oor.BatchRegistrar`), and the incoming
+  VTXO handler (`vtxo.IncomingBatchRegistrar`) consume. It waits until the
+  registration is durable and its watches are armed — returning earlier would
+  let a producer expose a VTXO whose lineage the fail-closed gate has not seen.
 - `WalletState` — `None` / `Locked` / `Ready` wallet lifecycle.
 - `UnrollConfig` / `OORConfig` — subsystem tunables; see `Config.Validate()`
   for the invariants each enforces.
 
 ## Relationships
 
-- **Depends on**: `baselib/actor`, `btcwbackend`, `chainbackends`,
-  `chainsource`, `lib/actormsg`, `db`, `ledger`, `round`, `txconfirm`,
-  `unroll`, `vtxo`, `wallet`, `walletcore`, `oor`, `serverconn`, `indexer`,
-  `arkrpc`, `lndbackend`, `fraud`, `gateway`, `rpc/restclient`,
+- **Depends on**: `baselib/actor`, `batchcanon`, `btcwbackend`,
+  `chainbackends`, `chainsource`, `lib/actormsg`, `db`, `ledger`, `round`,
+  `txconfirm`, `unroll`, `vtxo`, `wallet`, `walletcore`, `oor`, `serverconn`,
+  `indexer`, `arkrpc`, `lndbackend`, `fraud`, `gateway`, `rpc/restclient`,
   `vhtlcrecovery`, `vhtlcrecovery/coordinator`, `vhtlcrecovery/unrollpolicy`.
 - **Depended on by**: `cmd/waved`.
 
@@ -113,7 +119,35 @@ For field-level detail, use `go doc github.com/lightninglabs/wavelength/waved.<S
   actor repeats these checks as defense-in-depth.
 - `SendOOR` with custom inputs serializes concurrent calls on the same
   outpoints via `reserveCustomInputs`; the release function is deferred on
-  both success and failure.
+  both success and failure. Because custom inputs bypass the VTXO manager's
+  selector, `requireCustomInputLineage` re-applies the same fail-closed
+  canonicality gate before checkpoint signing: a blocked input returns
+  `codes.FailedPrecondition` when the lineage is terminally `Invalidated` and
+  `codes.Unavailable` for reversible limbo/reconciling states.
+- **Batch canonicality wiring** (`initBatchCanonicality`): the manager ref is
+  held as a full `ActorRef` (Ask, not Tell-only) so `batchRegistrar` can await
+  durable registration. When `Config.BatchCanonicalityGate` is set,
+  `s.batchCanonStore` (a `batchcanon.Reader`) is the **manager**, not the raw
+  durable store — admission then reads through `Manager.GetBatch`'s in-memory
+  overlay, so an observation whose durable write failed closes admission
+  immediately instead of staying admissible until a restart. The same ref
+  backs the VTXO manager's `RedriveConsumerForfeit` hook, which Tells
+  `batchcanon.ConsumerForfeitPersistedMsg` after a forfeiture marker persists
+  (a no-op when canonicality is disabled).
+- **F-H1 receive binds are wired at every production receive seam.**
+  `incomingAncestryFetcher` runs `vtxo.VerifyReceivedVTXOBinding` after
+  `ResolveIncomingAncestry`; a failure surfaces as a fetch failure, so the
+  handler persists the VTXO without ancestry (fail closed — no watch armed on
+  an unverified commitment, exit material restored by a later backfill). The
+  OOR registry, session actors, and `LocalPersistenceOutboxHandler` are wired
+  with `vtxo.VerifyOORAncestryLineage`. `recoveryOORHandler` wires the verifier
+  defensively even though it has no `BatchRegistrar` today, so adding one later
+  cannot silently fail open.
+- `GetBalance` splits live VTXO value via `vtxo.ClassifyCanonicalityBalance`:
+  `vtxo_balance_sat` counts only lineage-usable value and
+  `vtxo_temporarily_unavailable_sat` reports value blocked by unseen,
+  unregistered, reorg/conflict-limbo, or reconciling lineage. Terminally
+  invalidated lineage is in neither bucket.
 - `Unroll` / `GetUnrollStatus` return `codes.Unavailable` (not `Internal`)
   when the unroll subsystem refs are not yet set, so clients can retry.
 - `OORLimitsConfig.MaxMailboxScriptBytes` must be at least

How to apply

  1. Save the diff above and git apply it.

  2. Mirror each edited doc to its sibling so the pair stays byte-identical:

    for d in arkrpc batchcanon cmd/wavecli/waveclicommands oor round \
             rpc/wavewalletrpc sdk/wavewalletdk swapwallet txconfirm \
             vtxo waved; do
      cp "$d/CLAUDE.md" "$d/AGENTS.md"
    done

Or run the doc-gardening skill locally (/doc-gardening) over those packages and let it regenerate both halves of each pair.

The AGENTS.md mirrors are omitted from the diff only to keep this comment under GitHub's comment-size limit — they are byte-identical copies. Note that batchcanon/AGENTS.md had already drifted from its CLAUDE.md before this PR; the cp loop above resolves that too.

make doc-check reports one error on this runner — ./.claude-pr/CLAUDE.md exists but ./.claude-pr/AGENTS.md is missing — which comes from a gitignored CI scaffold directory, not from these changes. It reports no error for any file in this diff.

Advisory only; this check never fails CI. Posted from the workflow run on PR #990 / 928eadb.

@ellemouton
ellemouton force-pushed the reorg-batchcanon-core branch from dbadb1b to 6b3483a Compare August 10, 2026 18:05
Build complete batch evidence from the retained commitment PSBT when a
round confirms. Await durable canonicality registration before saving
any new VTXO so the fail-closed admission gate never observes
unregistered liquidity.

Refresh registrations bind each consumed VTXO to its next lifecycle
revision and complete creator lineage, allowing a terminally replaced
round to restore only the exact forfeiture it owns.
Carry the commitment transaction and every input prevout in ancestry
responses. Validate the transaction, output, and input bindings before
receive paths register canonicality watches.
Persist authenticated evidence in the durable receive event and await
canonicality registration before staging incoming VTXOs. Legacy indexers
may omit the full additive extension, while partial or conflicting
evidence fails closed.
Carry authenticated evidence through the round-receive ancestry lookup.
Await canonicality registration before saving or notifying the VTXO
manager. Missing legacy evidence remains blocked by the gate.
Custom OOR inputs bypass wallet coin selection and its fail-closed
lineage gate. Reuse the full-lineage reader before maturity checks so
reorged or invalidated inputs never reach checkpoint signing.
Expose reversible lineage limbo separately from spendable and pending
VTXO balances so clients can explain why value is temporarily unusable.
Keep reorged, conflicted, unseen, and reconciling lineage out of
the spendable figure while preserving it in a temporary-unavailability
bucket. Exercise the classification across a real reorg and
reconfirmation.
Reject malformed local or imported registration evidence before it can
produce a height-derived expiry at the confirmation block itself.
Include unseen and unregistered lineage in the balance field contract so
clients can explain the same fail-closed bucket the daemon reports.
Pass the local round start height and indexed commitment height into
batch canonicality registration. Persist the indexer hint through OOR
durable messages so delayed and replayed receive paths scan across
already-mined confirmations.
The wired VTXO admission gate reads lineage availability straight from
the durable store via LineageBlocked. When a reorg/conflict observation
could not be persisted (a transient ApplyObservation failure), the
in-memory watch was marked not-ready but the stale durable row stayed
Ready(), so the gate kept admitting a VTXO whose commitment had just
left the best chain. The overlay the code comments promised only ever
protected the unwired actor QueryLineage path.

Make the manager itself a batchcanon.Reader whose GetBatch overlays the
in-memory watch: a not-ready watch forces the returned record not-ready,
even when the durable row still reads Ready. Thread the manager (not the
raw store) into the gate. The overlay only downgrades ready->not-ready,
so it is strictly fail-closed. Add a wired-path test proving admission
closes after a failed reorg persist.
A consumer batch can reach a terminal state (ConflictFinalized) before
its ForfeitedBy(consumer) business-revision marker is durably persisted:
the round FSM registers the business revision before the outbox delivers
ForfeitConfirmedToVTXO, which is what drives MarkForfeited. When the
terminal transition wins that race, restoreProvisionalConsumers runs the
restore compare-and-swap against a VTXO not yet at the expected 
revision,
so it defers. No further batchcanon event is then guaranteed on the
consumer, leaving the consumed VTXO stranded until the next restart
redrive.

Have the VTXO actor send ConsumerForfeitPersistedMsg the moment
MarkForfeited succeeds, carrying the consuming batch. The manager
redrives that consumer's terminal lifecycle, gated on it being Ready and
terminal, so the marker's arrival becomes the evidence change that
completes the deferred restore without a restart. The redrive is wired
as an optional VTXO-manager callback, so harness paths and deployments
without batch canonicality are unaffected.
The receive paths authenticated the commitment transaction (self-hash)
and its batch output, but never proved the received VTXO actually
descends from it. Nothing tied the pre-signed tree to that authenticated
output, so a malicious or buggy indexer could name a decoy commitment
(real, deeply buried, never reorging) and have the client arm its reorg
watch there while the VTXO's true lineage silently left the chain.

Running the tree signature check alone does not close this: signatures
verify against a per-node aggregate key recomputed from the tree's own
(indexer-supplied) cosigners, so a fabricated tree signed with the
attacker's own keys passes. The load-bearing check is that each node's
aggregate key must equal the taproot key of the output it spends. At the
root that output is the authenticated commitment output, so the root key
is pinned to the real round key (unforgeable); taproot's SIGHASH_DEFAULT
commits to each tx's outputs, so the pin cascades to every leaf
(verifyAuthenticatedTree). It is key-identity-agnostic, so it never
demands the operator identity key and never false-rejects a genuine 
tree.

Two receive paths consume it:
  - In-round (waved/incoming_ancestry_fetcher): 
VerifyReceivedVTXOBinding
    runs verifyAuthenticatedTree then requires a leaf to pay the 
received
    VTXO script (an in-round VTXO IS a commitment-tree leaf).
  - OOR (oor/incoming_batch_registration): an OOR output is an ark-tx
    output, not a tree leaf, so VerifyOORAncestryLineage binds the 
coin's
    real base inputs instead. It re-anchors each tree's batch output on
    the validated evidence (OOR metadata can be a durable replay), runs
    verifyAuthenticatedTree, and requires every base coin input to be an
    authenticated ancestry leaf. BaseCoinInputs derives those by walking
    the coin's authenticated checkpoint/ark chain (root
    FinalCheckpointPSBTs + AncestorPackages) down to the checkpoints 
whose
    prevouts are the committed leaves -- uniform for single- and
    multi-hop receives. The coin inputs are trustworthy: each checkpoint
    txid is committed by the ark txid, which equals the coin's own
    outpoint/sessionID (ValidateFinalizePackage), so an attacker cannot
    substitute them. This closes the genuine-but-unrelated (decoy)
    commitment variant. The bind never keys off the indexer-supplied
    chain depth (inflatable to skip). Injected via 
IncomingLineageVerifier
    so registration-logic tests keep mock trees; also wired defensively
    into the recovery handler so a future registrar cannot bypass it.

On failure both paths fail closed (drop ancestry / fail registration),
so no watch is armed on an unverified commitment; the VTXO still 
persists
for cooperative use and its exit material is restored by a later
backfill. OOR skips the check when evidence is absent (older indexer).
When a tracked tx's confirmation is reorged out and later reconfirms,
the terminal-notification idempotency key must change so a durable
subscriber does not drop the re-confirmation as a duplicate of the
pre-reorg one. terminalNotifyKey was stable across reorg epochs, so a
reorg-aware consumer that rolled its state back on TxReorged -- e.g. an
in-progress unilateral exit waiting to re-drive -- never received the
re-confirmation and stalled indefinitely (funds safe on-chain but the
exit never swept).

Fold a per-entry reorg epoch (incremented in handleConfirmationReorged)
and the last confirmation height into terminalNotifyKey. Same-epoch,
same-height retries still dedup; a reorg-and-reconfirmation now carries
a fresh key and is delivered. Adds a regression test that fails without
the epoch increment.
Reflow lines flagged by the ll linter across the reorg
lineage-producer files. Formatting only, no behavior change.
TestGetBatchReadsUnderLock swapped mgr.cfg.Store on the test goroutine
while the actor was still processing the spend observation, which reads
cfg.Store on its own goroutine -- a data race the -race unit job caught.
Add a synchronizing Ask so the FIFO mailbox drains before the swap.
Adapts #990's reorg lineage/OOR-gating code to the current lint gates:
drops a stray duplicated godoc line left by a merge on
LineageCommitmentTxIDs, and adds justified funlen/nestif directives
where main's growth plus the canonicality gate pushed the incoming-VTXO
dispatch and custom-input path past the thresholds. No functional
changes.
@ellemouton
ellemouton force-pushed the reorg-lineage-producers branch from 3cd0109 to 928eadb Compare August 10, 2026 18:08
@litbot-9000

Copy link
Copy Markdown
Collaborator

@ellemouton, remember to re-request review from reviewers when ready

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants