Skip to content

unroll: gate admission on source-lineage canonicality (C8) - #820

Closed
ellemouton wants to merge 2 commits into
c7-oor-canonicality-gatefrom
c8-unroll-source-lineage-gate
Closed

unroll: gate admission on source-lineage canonicality (C8)#820
ellemouton wants to merge 2 commits into
c7-oor-canonicality-gatefrom
c8-unroll-source-lineage-gate

Conversation

@ellemouton

Copy link
Copy Markdown
Member

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. EnsureUnroll now consults the target VTXO's full source lineage (the
direct commitment txid plus every ancestor commitment txid) before spawning a
fresh child and refuses with ErrSourceLineageUnavailable when the lineage is
Invalidated.

Why it blocks only the terminal verdict

It deliberately blocks only Invalidated, not the transient LimboReorg /
LimboConflict states:

  • A reorged-out batch (no input conflict) is expected to re-confirm on its own;
    a not-yet-final conflict may still resolve in the batch's favor.
  • The critical-expiry safety net reaches this gate via a fire-and-forget
    Tell
    , so a refusal can't be observed or retried — blocking a transient,
    self-healing condition could permanently drop a needed exit.
  • An already-admitted unroll tolerates a transiently-absent parent by
    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 Invalidated there is genuinely nothing to retry (the funds are gone via
the 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.BatchCanonicality store (nil = dormant, matching C5–C7); only
fresh admissions are gated.

Follow-ups (when darepod wires the gate — deferred, gate dormant today)

  • recoverOrphanedUnrollJobs treats EnsureUnroll errors as fatal at startup;
    it should classify ErrSourceLineageUnavailable as non-fatal/deferred.
  • unrollLineageCommitmentTxids duplicates 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

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread unroll/registry.go
Comment on lines +457 to +459
if r.cfg.BatchCanonicality == nil {
return false
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
if r.cfg.BatchCanonicality == nil {
return false
}
if r.cfg.BatchCanonicality == nil || r.cfg.VTXOStore == nil {
return false
}

Comment thread unroll/registry.go
Comment on lines +461 to +468
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
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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
}

Comment thread unroll/registry.go
Comment on lines +489 to +491
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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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)

@ellemouton
ellemouton force-pushed the c7-oor-canonicality-gate branch from 73e1427 to 1872076 Compare June 29, 2026 20:10
@ellemouton
ellemouton force-pushed the c8-unroll-source-lineage-gate branch from 88e11ad to e2ecc19 Compare June 29, 2026 20:11
@ellemouton
ellemouton force-pushed the c7-oor-canonicality-gate branch from 1872076 to c2c0fa1 Compare July 1, 2026 16:16
@ellemouton
ellemouton force-pushed the c8-unroll-source-lineage-gate branch from e2ecc19 to f5ab5f3 Compare July 1, 2026 16:16
@levmi levmi added P1 Priority 1 — high reorg safety Fund-safety: stuck, lost, or mis-counted funds unroll vtxo labels Jul 6, 2026
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.
@ellemouton
ellemouton force-pushed the c7-oor-canonicality-gate branch from c2c0fa1 to 5d77198 Compare July 8, 2026 21:11
@ellemouton
ellemouton force-pushed the c8-unroll-source-lineage-gate branch from f5ab5f3 to 7e56139 Compare July 8, 2026 21:13
Squashed for the btcd v2 port. Unroll gates fresh admission on the
source VTXO's batch-lineage canonicality (blocks only Invalidated,
fail-permissive).
@ellemouton
ellemouton force-pushed the c7-oor-canonicality-gate branch from 5d77198 to 6579e95 Compare July 8, 2026 22:42
@ellemouton
ellemouton force-pushed the c8-unroll-source-lineage-gate branch from 7e56139 to 4bf4885 Compare July 8, 2026 22:43
@ellemouton

Copy link
Copy Markdown
Member Author

Superseded by #896 as part of condensing the reorg-safety client stack (epic lightninglabs/darepo#454) from 12 PRs into 3. The commits are carried over unchanged; see #896. Branch retained as a backup.

@ellemouton ellemouton closed this Jul 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P1 Priority 1 — high reorg safety Fund-safety: stuck, lost, or mis-counted funds unroll vtxo

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants