Files
consensus/engine
zeekayandHanzo Dev edb076bc1e fix(engine/chain): decided-height gate closes prune-then-resign double-finalization
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]>
2026-07-01 20:23:39 -07:00
..
2025-12-19 08:49:11 -08:00