Files
consensus/engine
zeekayandHanzo Dev e06cea0db2 engine/chain: LIVE stale-alias view-change lock rebase (F1 / mainnet 1085761 freeze)
The v1.35.36 cert-receive canonical fix let luxd-4 past the 1085755 storm wall,
but it then froze at round-height 1085761: a pre-fix durable v3 view-change lock
stored by an OUTER proposervm wrapper id (committedSlot written when canonical=
outer) is re-seeded by viewForLocked as lockBlock, and prevoteTarget compares that
stale alias against the inner-canonical winner by RAW id. The equality fails, the
node prevotes its stale lock forever, no POL for the winner forms → SAFE but
FROZEN (lockIsStaleAlias=true, lockRound=0, prevoteTarget=stale, winner=inner).

The boot-time operator migration (lock_migration.go) already repairs this durably,
but it is one-shot and env-gated. This adds the LIVE, per-step, in-memory analogue
so a running fleet self-heals without operator action:

  - round_view.go rebaseLockAlias: a PURE, mechanical alias swap. Re-names an
    existing lock onto `canonical`, preserving the locked VALUE (same inner block)
    and the lock ROUND. Touches no tally/round/precommit record, so every other
    round-machine invariant is trivially preserved. It RESTORES the machine's
    contract that lockBlock is always an inner canonical.

  - integration.go maybeRebaseStaleLock: at the top of each view-change step, if
    the height's view is locked on a NON-winner id, resolve that lock's inner
    canonical (vmCanonicalResolver — tracked canonicalRep, then the durable VM
    store) with NO engine lock held (preserving runViewChangePass's mu->slotMu
    order), and rebase onto the winner IFF the resolved inner EQUALS winnerCanon.

SAFETY (no liveness bought with a fork): we only ever move a lock to the value the
whole fleet is already converging on. A lock resolving to a DIFFERENT inner (real
fork) or unresolvable is left untouched — the node stays fail-closed frozen. A
fresh in-session lock is already canonical, resolves to itself, and only rebases
when it equals the winner (a no-op) — an honest same-session precommit is never
re-pointed. lockRound is preserved, so the unlock bar for a genuinely conflicting
value is not lowered. The rebase target is the deterministic winner canonical
(unique per (height,parent-group) via convergedWinnerAtHeightLocked), and
recordOwnPrecommit still refuses a conflicting precommit at a round, so no node can
be made to POL two distinct inner ids at one height; 2α−n>f (n=5,α=4: 3>1) is
unchanged. Scope is exactly the v3 soft-lock case (lockRounds); legacy hard locks
(recoveredLocks, no round) are never round-seeded into a view, so they stay the
operator migration's domain — the live gate and reserveSlotForSign agree on which
locks are re-pointable.

Tests (fail-on-old / pass-on-new):
  - round_view: TestRoundView_RebaseLockAlias (pure alias swap, round preserved,
    prevoteTarget flips suppression->winner, guards never fabricate a lock).
  - RED TestRedLiveRebase_SafetyGate: same-inner rebased live (no migration);
    divergent-inner REFUSED (no fork); unresolvable left untouched.
  - Multinode TestMultiNode_LiveStaleLockRebase_ConvergesNoFork: the n=5 α=4
    3/2 stale-outer split-lock freezes without a resolver (control) and CONVERGES
    to a single head via the live rebase with no boot migration (fix).

Full engine/chain suite green (356s); lock/view-change/fork/committee/incident
subset green under -race (139s).

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-09 12:01:09 -07:00
..
2025-12-19 08:49:11 -08:00