Files
consensus/engine
zeekayandHanzo Dev 24b2a32ba9 consensus: FIX single-validator DECIDE deadlock — selfVoter must dispatch ReceiveVote async
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]>
2026-07-02 15:00:19 -07:00
..
2025-12-19 08:49:11 -08:00