Owner mandate: decomplect the n=1 finalize, do it right, one path. Supersedes the
async band-aid (v1.35.13) with the structural fix.
The single-node selfVoter shortcut (integration.go) was the COMPLECTING element: for
K==1 it called engine.ReceiveVote from inside RequestVotes while buildBlocksLocked held
t.mu, self-deadlocking on the non-reentrant t.mu.RLock (the live n=1 freeze — node
hangs at 'single-node mode: self-voting'), and delivered a redundant vote that could sit
uncounted in handleVote's not-yet-tracked buffer. REMOVED entirely (field, wiring,
RequestVotes branch). A K==1 chain now finalizes SOLELY through the inline build-path
finalizer (buildBlocksLocked → buildSingleValidatorCertLocked → acceptWithCertCore) —
one finalize path, matching avalanchego's 'a lone validator's own decision finalizes
locally.' buildBlocksLocked skips RequestVotes for K==1 (no peers); RequestVotes returns
early when the sample resolves to no peers (true no-op, not a query to nobody).
Also (RED decentralization follow-up): gate reclampCommitteeLocked AND the
buildSingleValidatorCertLocked count>1 synthesize-refusal on a RESOLVED set
(GetFinalizedHeight set = ≥1 finalized block), so a fresh single-validator chain that
transiently over-reports its validator count cannot wedge its own first block.
PROOF through the REAL runtime path: TestBlue_SingleValidator_DecidesThroughFullRuntimePath
now drives NewRuntime (real gossiperProposer) 0→1→2 and asserts VM.Accept + height
advances — HANGS pre-decomplect (deadlock, 25s timeout), PASSES post. params_test /
single_node_test converted to the decomplected invariant (K==1 polls nobody; K>1 polls
network). Full engine/chain suite green incl -race. v1.35.14.
Co-authored-by: Hanzo Dev <[email protected]>
Three fixes for single-node consensus (--dev mode, K=1):
1. Self-voting: when K=1 and all sampled validators are self, deliver
a self-vote via callback instead of polling 0 peers.
2. Block acceptance: call Verify() before Accept() — PlatformVM requires
block state prepared during Verify(). Without it, Accept() fails
silently due to missing block state.
3. DrainAccepted: after each VM notification, check for blocks that
consensus accepted and call their Verify+Accept pipeline.
Multi-node consensus (K>1) is unchanged — self-voting only activates
when K=1 and no external validators exist in the sample.
Tests:
- TestSingleNodeSelfVote: K=1 self-votes, no network queries
- TestMultiNodeDoesNotSelfVote: K=3 uses network, no self-vote
- TestSelfVoterNilWhenKGreaterThan1: callback only for K=1