unroll: gate admission on source-lineage canonicality (C8) - #820
unroll: gate admission on source-lineage canonicality (C8)#820ellemouton wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a source-lineage canonicality gate for fresh unroll admissions, preventing unrolls when a batch in the target VTXO's lineage is permanently invalidated. It also adds a comprehensive set of unit tests to verify this behavior. The review feedback highlights several opportunities for defensive programming to prevent potential nil pointer dereferences, specifically by checking for nil values of VTXOStore and the VTXO descriptor desc before accessing their fields.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| if r.cfg.BatchCanonicality == nil { | ||
| return false | ||
| } |
There was a problem hiding this comment.
Defensive programming: If r.cfg.VTXOStore is nil, calling r.cfg.VTXOStore.GetVTXO on line 461 will cause a nil pointer dereference panic. It is safer to check if VTXOStore is nil and return false (fail-permissive) early.
| if r.cfg.BatchCanonicality == nil { | |
| return false | |
| } | |
| if r.cfg.BatchCanonicality == nil || r.cfg.VTXOStore == nil { | |
| return false | |
| } |
| desc, err := r.cfg.VTXOStore.GetVTXO(ctx, outpoint) | ||
| if err != nil { | ||
| r.log.DebugS(ctx, "Unroll lineage gate: vtxo load failed, "+ | ||
| "permitting admission", err, | ||
| slog.String("outpoint", outpoint.String())) | ||
|
|
||
| return false | ||
| } |
There was a problem hiding this comment.
Defensive programming: If GetVTXO returns a nil descriptor and a nil error (which can happen with custom or buggy mock implementations), calling unrollLineageCommitmentTxids(desc) will result in a nil pointer dereference panic. Adding a check for desc == nil ensures robust execution.
| desc, err := r.cfg.VTXOStore.GetVTXO(ctx, outpoint) | |
| if err != nil { | |
| r.log.DebugS(ctx, "Unroll lineage gate: vtxo load failed, "+ | |
| "permitting admission", err, | |
| slog.String("outpoint", outpoint.String())) | |
| return false | |
| } | |
| desc, err := r.cfg.VTXOStore.GetVTXO(ctx, outpoint) | |
| if err != nil || desc == nil { | |
| r.log.DebugS(ctx, "Unroll lineage gate: vtxo load failed, "+ | |
| "permitting admission", err, | |
| slog.String("outpoint", outpoint.String())) | |
| return false | |
| } |
| func unrollLineageCommitmentTxids(desc *vtxo.Descriptor) []chainhash.Hash { | ||
| seen := make(map[chainhash.Hash]struct{}, len(desc.Ancestry)+1) | ||
| txids := make([]chainhash.Hash, 0, len(desc.Ancestry)+1) |
There was a problem hiding this comment.
Defensive programming: The helper function unrollLineageCommitmentTxids does not check if desc is nil before accessing desc.Ancestry and desc.CommitmentTxID. Adding a nil check at the beginning of the function prevents potential nil pointer dereference panics.
func unrollLineageCommitmentTxids(desc *vtxo.Descriptor) []chainhash.Hash {
if desc == nil {
return nil
}
seen := make(map[chainhash.Hash]struct{}, len(desc.Ancestry)+1)
txids := make([]chainhash.Hash, 0, len(desc.Ancestry)+1)73e1427 to
1872076
Compare
88e11ad to
e2ecc19
Compare
1872076 to
c2c0fa1
Compare
e2ecc19 to
f5ab5f3
Compare
Squashed for the btcd v2 port. OOR registers every batch parent in the received-VTXO proof lineage with the canonicality manager, and the VTXO gate combines availability across all ancestry parents (worst-state AND) for multi-parent OOR VTXOs.
c2c0fa1 to
5d77198
Compare
f5ab5f3 to
7e56139
Compare
Squashed for the btcd v2 port. Unroll gates fresh admission on the source VTXO's batch-lineage canonicality (blocks only Invalidated, fail-permissive).
5d77198 to
6579e95
Compare
7e56139 to
4bf4885
Compare
C8 — gate unroll admission on source-lineage canonicality
Part of the reorg-safety epic (lightninglabs/darepo#454). Stacked on C7 (#819).
What this does
An unroll broadcasts the VTXO's exit tree, which spends a commitment batch
output. If a batch in the VTXO's source lineage is permanently invalidated
(a consumed input was double-spent past finality), the exit tree can never
confirm.
EnsureUnrollnow consults the target VTXO's full source lineage (thedirect commitment txid plus every ancestor commitment txid) before spawning a
fresh child and refuses with
ErrSourceLineageUnavailablewhen the lineage isInvalidated.Why it blocks only the terminal verdict
It deliberately blocks only
Invalidated, not the transientLimboReorg/LimboConflictstates:a not-yet-final conflict may still resolve in the batch's favor.
Tell, so a refusal can't be observed or retried — blocking a transient,
self-healing condition could permanently drop a needed exit.
reconciling its own anchors (unroll: Reorg-safe unilateral-exit subsystem #410), so a fresh safety exit is admitted for
the same condition rather than refused.
For
Invalidatedthere is genuinely nothing to retry (the funds are gone viathe finalized conflict), so refusing — and a dropped fire-and-forget refusal —
is safe.
The gate is also fail-permissive: a descriptor-load or canonicality-lookup
error logs and admits rather than blocking an exit. Gated behind an optional
RegistryConfig.BatchCanonicalitystore (nil = dormant, matching C5–C7); onlyfresh admissions are gated.
Follow-ups (when darepod wires the gate — deferred, gate dormant today)
recoverOrphanedUnrollJobstreatsEnsureUnrollerrors as fatal at startup;it should classify
ErrSourceLineageUnavailableas non-fatal/deferred.unrollLineageCommitmentTxidsduplicates the vtxo selection-gate helper;consider exporting one shared helper.
Tests
Unit tests cover blocked-on-invalidated-ancestor, permitted transient-reorg and
canonical cases, unregistered + load-failure (permissive) cases, the dormant
no-op, and the
errors.Is-matchable wrapped sentinel.🤖 Generated with Claude Code