mirror of
https://github.com/luxfi/consensus.git
synced 2026-09-24 00:26:58 +00:00
Rip the July-2026 view-change/lock accretion: the round-scoped prevote/POL/ precommit/lock liveness layer bolted on top of the alpha-of-K cert. avalanchego/snow has ZERO of it; it was added 2026-07-02 (c7b2f0081) to fix a competing-fork storm whose real root (empty validator set + warpConfig VM-init bug) is already fixed. Deleted whole: round_view.go (541), lock_migration.go (406) + the braid-only tests (round_view*, lock_migration_test, red_stale_lock_probe, multinode_splitlock, storm_convergence, idle_storm, vc_churn_storm, multinode_live_rebase, competing_fork_deadlock). Excised the braid threads from the shared files, KEEPING the safety primitives: - engine.go: dropped views/prevoteSigs/recoveredLocks/lockRounds; reserveSlotForSign is now a pure per-height non-equivocation refusal (a value/witness, never a lock). - integration.go: removed runViewChangePass/stepViewChange/maybeRebaseStaleLock/ castPrecommitForCanonical/HandleIncomingPrevote/viewCommittee/BroadcastPrevote; RunSettlePass keeps only the emitConvergedVote settle path. Master toggle t.params.ViewChange no longer gates anything. - vote_guard.go: reverted the durable codec v3->v2 (dropped the lock-round field); the per-height non-equivocation guard + decided-floor are UNCHANGED (clean v2, reserved field written all-zero; v1/v2 snapshots still decode). - reconcile.go: phantom-floor recovery kept, decoupled from the braid maps (writeReconciledStateLocked now has one caller — DRY). KEPT (naming traps, NOT braid): guard.go (ConsensusMode), lock_invariant.go (engineMutex = sync mutex, not a block-lock), topology.go cert-rebase. Package + full engine/chain suite green (the proven alpha-of-K cert-decider intact). NOT YET SHIPPABLE — this is the foundation, not the tag. B1 (braid deleted, cert-decider) reintroduces the competing-fork asymmetric-partition deadlock the braid was fixing: emitConvergedVote converges a split only when the settle window exceeds gossip latency, so an asymmetric partition that outlasts it leaves honest nodes bound to different siblings with no way to switch (the testnet 415->416 freeze). Phase B makes Nova (Photon->Wave->Focus/FPC) the SOLE decider — fluid preference until beta, so a healed split re-samples and converges — which is the correct fix and restores fork convergence. Tag v1.36.0 only after Nova is green on the full matrix. Co-authored-by: Hanzo Dev <[email protected]>
65 lines
2.5 KiB
Go
65 lines
2.5 KiB
Go
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
|
||
// See the file LICENSE for licensing terms.
|
||
|
||
package chain
|
||
|
||
import (
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/luxfi/consensus/config"
|
||
"github.com/luxfi/ids"
|
||
)
|
||
|
||
// storm_helpers_test.go — the shared STORM-INERT harness. Competing siblings must
|
||
// converge to ONE finalized head per height with NO lock / view-change machinery,
|
||
// purely by Nova sampling + the deterministic lowest-canonical settle. Shared by the
|
||
// storm-inert, committee-scaling, competing-fork, and mainnet-committee gates.
|
||
|
||
// stormParams5 is the 5-validator BFT param set for the storm gate: K=5, α=4, with a
|
||
// round budget large enough that the convergence settle window (RoundTO/2 = 500ms)
|
||
// comfortably exceeds sibling-gossip latency — even under the pathological slowdown of
|
||
// the `-race` detector — so every honest node sees the full sibling set before it binds
|
||
// its one signature.
|
||
func stormParams5() config.Parameters {
|
||
p := prodParams5()
|
||
p.RoundTO = 1 * time.Second // settle = RoundTO/2 = 500ms — comfortably exceeds real gossip latency
|
||
return p
|
||
}
|
||
|
||
// stormTO is a generous per-height convergence ceiling for the storm tests. It is only a
|
||
// CEILING — stormAwaitSingleHead returns the instant the net converges, so a healthy run
|
||
// is unaffected; the headroom exists so the gate does not flake under the heavy CPU
|
||
// contention of the full `go test` package run.
|
||
const stormTO = 30 * time.Second
|
||
|
||
// stormAwaitSingleHead waits until every UP node has finalized the SAME single block at
|
||
// height h (emergent convergence), and returns that head. Fails on a fork (two distinct
|
||
// finalized heads at one height) immediately.
|
||
func stormAwaitSingleHead(t *testing.T, net *simNet, h uint64) ids.ID {
|
||
t.Helper()
|
||
deadline := time.Now().Add(stormTO)
|
||
for time.Now().Before(deadline) {
|
||
heads := net.headsAtHeight(h)
|
||
if len(heads) > 1 {
|
||
t.Fatalf("DOUBLE-FINALIZATION at height %d: distinct finalized heads %v — two finality "+
|
||
"attestations at one height (the safety violation Nova single-accept must make impossible)", h, heads)
|
||
}
|
||
if len(heads) == 1 {
|
||
var head ids.ID
|
||
count := 0
|
||
for id, c := range heads {
|
||
head, count = id, c
|
||
}
|
||
if count >= net.upCount() {
|
||
return head
|
||
}
|
||
}
|
||
time.Sleep(15 * time.Millisecond)
|
||
}
|
||
t.Fatalf("LIVENESS STALL at height %d: the up validators did not converge on a single finalized head "+
|
||
"within %s (heads=%v, up=%d). A fresh-net storm must converge to one block per height.",
|
||
h, stormTO, net.headsAtHeight(h), net.upCount())
|
||
return ids.Empty
|
||
}
|