mirror of
https://github.com/luxfi/consensus.git
synced 2026-09-24 00:26:58 +00:00
Fixes the fresh multi-node chain double-finalization fatal (two α-of-K certs at
one height → exit(1) equivocation crash) AND the accompanying liveness stall,
without weakening any safety gate.
ROOT CAUSE (safety): the per-height non-equivocation slot was keyed on
(height, epoch) where epoch = ValidatorSetRoot(block.pChainHeight). Two honest
sibling blocks at ONE consensus height can pin DIFFERENT proposervm P-chain
heights (a bare/pre-fork block reports 0, a wrapped block reports P), so
ValidatorSetRoot(0)=Empty != ValidatorSetRoot(P). Different epoch => different
SlotKey => one honest validator signed BOTH siblings => two α-of-K certs at
height 7 => the fatal. The P-chain-height epoch is a PROPOSER-chosen axis, not a
function of the finalized value-chain prefix, so "one epoch per height" was false.
SAFETY FIX: key the slot on HEIGHT ALONE (SlotKey{Height}). One signature per
consensus height, epoch-blind. Two ⅔ certs at one height now require ≥2α−n honest
double-signers (f≥n/3) — impossible within the BFT budget. The validator-set-root
epoch binding stays in the SIGNED message + cert verification (anti cross-epoch
forgery); it is removed ONLY from the equivocation slot, where it added no safety
and only fragmented the one-signature-per-height rule. Durable guard stays
byte-compatible (reserved-zero epoch field, folds legacy snapshots on read).
ROOT CAUSE (liveness): the α-of-K cert finalized on the FIRST-seen signed votes,
independent of any converged decision, and each node bound its one signature to the
block it BUILT or FIRST-SAW. On a fresh-net storm (many validators build competing
siblings at one height) that splits the vote N ways: nothing reaches α, the net
wedges.
LIVENESS FIX: subordinate the cert to a single converged decision per height. A node
no longer votes at build/receipt; a convergence loop casts its ONE signature for the
deterministic winner (lowest signed-canonical among live siblings) once a settle
window has elapsed (proposals have gossiped in). Every honest node selects the
IDENTICAL winner, so exactly one α-of-K cert forms per height — the cert now
CERTIFIES the converged decision instead of being a parallel finality path that could
certify a conflicting sibling.
Proven on the in-process 5-node quorum engine (real gossip/vote/cert):
- TestVoteOnce_EpochBlind_OneSignaturePerHeight — the double-vote regression, closed.
- TestStorm_AllValidatorsBuild_SingleHeadPerHeight — 5 concurrent builders/height,
24 heights, single head per height, NO double-finalization.
- TestStorm_KillOneMidRun_KeepsProducing — validator killed mid-run, 4-of-5 keep
converging for 22 heights (f=1 tolerated).
Full engine/chain suite green (non-race incl storm; -race race-clean with the
timing-sensitive storm gates skipped — convergence goroutine race-checked by the
multinode suites). go vet clean.
Co-authored-by: Hanzo Dev <[email protected]>
19 lines
1007 B
Go
19 lines
1007 B
Go
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
//go:build race
|
|
|
|
package chain
|
|
|
|
// underRace is true when the test binary is built with the race detector. The race
|
|
// detector imposes a ~10x, highly variable slowdown on every memory access, which makes
|
|
// the in-process sim's gossip latency exceed any bounded convergence settle window — a
|
|
// condition that does NOT occur on a real network (where gossip is milliseconds and the
|
|
// settle is hundreds of ms). The vote-CONVERGENCE storm gates are therefore skipped under
|
|
// -race: their timing assumption (gossip < settle) is violated only by the detector's
|
|
// artificial slowdown, not by production. Their SAFETY property (no double-finalization)
|
|
// is timing-independent and still asserted in the non-race run, and the convergence
|
|
// goroutine's shared-state access is exercised race-free by the multinode proposer/
|
|
// liveness suites, which DO run under -race.
|
|
const underRace = true
|