Files
consensus/engine/chain/single_node_test.go
T
zeekayandHanzo Dev 0e115304bb consensus: DECOMPLECT single-validator finalize — remove selfVoter shortcut (one path, ava-aligned)
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]>
2026-07-02 15:29:14 -07:00

115 lines
3.3 KiB
Go

package chain
import (
"context"
"testing"
"github.com/luxfi/ids"
)
// mockValidatorSampler returns the node itself as the only validator.
type mockValidatorSampler struct {
nodeID ids.NodeID
}
func (m *mockValidatorSampler) Sample(_ ids.ID, k int) ([]ids.NodeID, error) {
result := make([]ids.NodeID, 0, k)
for i := 0; i < k; i++ {
result = append(result, m.nodeID)
}
return result, nil
}
func (m *mockValidatorSampler) Count(_ ids.ID) int { return 1 }
// testGossiper implements Gossiper for testing.
type testGossiper struct {
pushQueries int
}
func (g *testGossiper) GossipPut(_ ids.ID, _ ids.ID, _ []byte) int { return 0 }
func (g *testGossiper) SendPushQuery(_ ids.ID, _ ids.ID, _ []byte, _ []ids.NodeID) int {
g.pushQueries++
return 0
}
func (g *testGossiper) SendPullQuery(_ ids.ID, _ ids.ID, _ ids.ID, _ []ids.NodeID) int { return 0 }
func (g *testGossiper) SendVote(_ ids.ID, _ ids.NodeID, _ ids.ID) error { return nil }
// TestSingleNode_PollsNobody verifies the DECOMPLECTED single-validator path: a K==1 proposer
// polls NOBODY (no peers) and does NOT self-vote — there is no self-voter shortcut. Finality
// comes solely from the inline build-path finalizer (proven in singlenode_decide_test.go).
// RequestVotes returns cleanly and sends zero network queries.
func TestSingleNode_PollsNobody(t *testing.T) {
nodeID := ids.GenerateTestNodeID()
blockID := ids.GenerateTestID()
gossiper := &testGossiper{}
proposer := &gossiperProposer{
gossiper: gossiper,
chainID: ids.GenerateTestID(),
networkID: ids.GenerateTestID(),
validators: &mockValidatorSampler{nodeID: nodeID},
nodeID: nodeID,
k: 1, // single validator
}
if err := proposer.RequestVotes(context.Background(), VoteRequest{
BlockID: blockID,
BlockData: []byte("block data"),
}); err != nil {
t.Fatalf("RequestVotes failed: %v", err)
}
if gossiper.pushQueries != 0 {
t.Fatalf("K==1 must poll nobody (no peers, no self-vote shortcut): got %d push queries", gossiper.pushQueries)
}
}
// TestMultiNode_PollsNetwork verifies that with K>1 the proposer polls the network (no
// self-voting shortcut ever existed for multi-validator, and none exists now).
func TestMultiNode_PollsNetwork(t *testing.T) {
nodeID := ids.GenerateTestNodeID()
peerID := ids.GenerateTestNodeID()
blockID := ids.GenerateTestID()
gossiper := &testGossiper{}
proposer := &gossiperProposer{
gossiper: gossiper,
chainID: ids.GenerateTestID(),
networkID: ids.GenerateTestID(),
validators: &multiValidatorSampler{
nodeID: nodeID,
peers: []ids.NodeID{peerID},
},
nodeID: nodeID,
k: 3, // multi-validator
}
if err := proposer.RequestVotes(context.Background(), VoteRequest{
BlockID: blockID,
BlockData: []byte("block data"),
}); err != nil {
t.Fatalf("RequestVotes failed: %v", err)
}
if gossiper.pushQueries != 1 {
t.Fatalf("multi-node mode should send push query via network: got %d", gossiper.pushQueries)
}
}
// multiValidatorSampler returns self + peers.
type multiValidatorSampler struct {
nodeID ids.NodeID
peers []ids.NodeID
}
func (m *multiValidatorSampler) Sample(_ ids.ID, k int) ([]ids.NodeID, error) {
all := append([]ids.NodeID{m.nodeID}, m.peers...)
if k > len(all) {
k = len(all)
}
return all[:k], nil
}
func (m *multiValidatorSampler) Count(_ ids.ID) int { return 1 + len(m.peers) }