mirror of
https://github.com/luxfi/consensus.git
synced 2026-09-24 00:26:58 +00:00
THE LIVE n=1 FREEZE (neo's repro; my v1.35.11 unit fix passed but the live chain still froze because the earlier tests used raw New with NO proposer, so they never exercised the selfVoter path). ROOT CAUSE — a reentrant-RWMutex self-deadlock, NOT a cert issue. NewRuntime wires the single-node selfVoter (integration.go) to call engine.ReceiveVote. RequestVotes invokes that selfVoter while buildBlocksLocked HOLDS t.mu (write); ReceiveVote then does t.mu.RLock() on the SAME goroutine — sync.RWMutex is not reentrant, so it blocks forever on RLock while holding the write lock. The node logs 'single-node mode: self-voting for proposed block' and hangs there; the inline K==1 finalize (which runs AFTER RequestVotes in buildBlocksLocked) never executes → no block ever decides → EVM head frozen at 0. Stack: buildBlocksLocked → RequestVotes → selfVoter → ReceiveVote → t.mu.RLock (deadlock). FIX: the selfVoter dispatches ReceiveVote on a fresh goroutine (go engine.ReceiveVote) so it never reenters t.mu on the lock-holding goroutine. buildBlocksLocked finishes its inline finalize and releases t.mu; the async vote then delivers (redundant for the own-proposal path, and the finality trigger for the rebuild / re-poll paths that have no inline finalize). ReceiveVote is non-blocking, so the goroutine returns immediately. PROOF through the REAL runtime path (not the cert function in isolation): TestBlue_SingleValidator_DecidesThroughFullRuntimePath builds a NewRuntime with the gossiperProposer + selfVoter wired (a single-validator sampler + recordingGossiper), drives Notify, and asserts the block DECIDES + finalized height advances to 1. It HANGS (25s timeout, deadlock stack) pre-fix and PASSES post-fix. Full engine/chain suite green incl -race. v1.35.13. Co-authored-by: Hanzo Dev <[email protected]>