mirror of
https://github.com/luxfi/consensus.git
synced 2026-09-24 00:26:58 +00:00
The fresh-net double-finalization fatal that survived the epoch-blind height-only
slot fix was a guard-LIFECYCLE defect, not a keying one:
pruneCommittedSlotsBelow used k.Height <= height (INCLUSIVE), deleting the
just-finalized height's equivocation slot. A losing sibling at that height with
a DIFFERENT outer parent than the winner (bare vs proposervm-wrapped envelope) is
NOT in losingSubtrees(winner) — losingSubtrees only rejects the other children of
the winner's parent — so it stays tracked and undecided after the winner finalizes.
With the slot deleted, the convergence pass re-offered the height and
reserveSlotForSign found an EMPTY slot, admitting this node's SECOND signature at
the height. Enough nodes doing this assembled a second alpha-of-K cert at one
height -> fatal EQUIVOCATION -> os.Exit(1) fleet-wide (live devnet artifact: the
conflicting cert's canonical == its envelope, i.e. exactly the bare sibling).
Fix (avalanchego-grounded, two layers):
1. DECIDED-HEIGHT GATE (load-bearing, durable): reserveSlotForSign refuses any
height <= GetFinalizedHeight(). A decided height's blocks are permanently
unsignable, exactly as topological.go's acceptPreferredChild+rejectTransitively
makes a decided height's siblings unreachable and drops their votes. The
certified frontier is monotonic, regression-guarded, and reconstructed from the
VM's last-accepted head on restart, so the refusal spans a crash.
2. PRUNE STRICTLY BELOW (k.Height < height): retain the finalized tip's slot as an
in-memory belt, mirroring ava keeping the last accepted block in its tree. The
tip's slot is reclaimed only when the next height finalizes, by when the gate
covers it durably.
3. Convergence pass skips slots at/below the finalized frontier (never busy-replay
a decided, permanently-refused height).
Tests: new TestFinalizeThenResign_DecidedHeightIsUnsignable drives the real finalizer
functions (FinalizeBranch + pruneCommittedSlotsBelow) and proves a sibling at a
decided height is refused both while the tip slot is retained AND after it is pruned
by a higher finalize (isolating the durable gate). Fails on pre-fix code, passes on
fixed. Two RED probe tests updated from the old inclusive-prune 'reopen' invariant to
the corrected strictly-below + decided-height-gate invariant. Full engine/chain suite
green, incl. storm (24 heights, 5 concurrent builders, no double-finalization) and
f=1 kill-one liveness; -race clean.
Co-authored-by: Hanzo Dev <[email protected]>