mirror of
https://github.com/luxfi/consensus.git
synced 2026-09-24 00:26:58 +00:00
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]>
115 lines
3.3 KiB
Go
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) }
|