Files
consensus/engine/chain/single_node_test.go
T
zeekay 4ae55610ef fix: --dev single-node mode P-Chain block finalization
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
2026-04-02 13:40:05 -07:00

150 lines
4.2 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 }
// TestSingleNodeSelfVote verifies that in --dev mode (K=1, single validator),
// the proposer delivers a self-vote instead of trying to poll 0 peers.
// This is the standard path for local single-validator networks.
func TestSingleNodeSelfVote(t *testing.T) {
nodeID := ids.GenerateTestNodeID()
blockID := ids.GenerateTestID()
gossiper := &testGossiper{}
var selfVoted bool
var votedBlockID ids.ID
proposer := &gossiperProposer{
gossiper: gossiper,
chainID: ids.GenerateTestID(),
networkID: ids.GenerateTestID(),
validators: &mockValidatorSampler{nodeID: nodeID},
nodeID: nodeID,
k: 1, // Single-node mode (--dev)
selfVoter: func(id ids.ID) {
selfVoted = true
votedBlockID = id
},
}
err := proposer.RequestVotes(context.Background(), VoteRequest{
BlockID: blockID,
BlockData: []byte("block data"),
})
if err != nil {
t.Fatalf("RequestVotes failed: %v", err)
}
if !selfVoted {
t.Fatal("single-node proposer did not self-vote")
}
if votedBlockID != blockID {
t.Fatalf("self-voted for wrong block: got %s, want %s", votedBlockID, blockID)
}
if gossiper.pushQueries != 0 {
t.Fatalf("single-node mode should not send push queries to network: got %d", gossiper.pushQueries)
}
}
// TestMultiNodeDoesNotSelfVote verifies that with K>1 (normal consensus),
// the standard network polling path is used — no self-voting shortcut.
func TestMultiNodeDoesNotSelfVote(t *testing.T) {
nodeID := ids.GenerateTestNodeID()
peerID := ids.GenerateTestNodeID()
blockID := ids.GenerateTestID()
gossiper := &testGossiper{}
selfVoted := false
proposer := &gossiperProposer{
gossiper: gossiper,
chainID: ids.GenerateTestID(),
networkID: ids.GenerateTestID(),
validators: &multiValidatorSampler{
nodeID: nodeID,
peers: []ids.NodeID{peerID},
},
nodeID: nodeID,
k: 3, // Multi-node mode (normal 3+ validator consensus)
selfVoter: func(_ ids.ID) {
selfVoted = true
},
}
err := proposer.RequestVotes(context.Background(), VoteRequest{
BlockID: blockID,
BlockData: []byte("block data"),
})
if err != nil {
t.Fatalf("RequestVotes failed: %v", err)
}
if selfVoted {
t.Fatal("multi-node mode should NOT self-vote — must use network consensus")
}
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) }
// TestSelfVoterNilWhenKGreaterThan1 verifies the integration layer only
// creates a selfVoter callback when K==1.
func TestSelfVoterNilWhenKGreaterThan1(t *testing.T) {
// K=1: selfVoter should be set
proposerK1 := &gossiperProposer{k: 1, selfVoter: func(_ ids.ID) {}}
if proposerK1.selfVoter == nil {
t.Fatal("K=1 should have selfVoter set")
}
// K>1: selfVoter should NOT trigger
proposerK3 := &gossiperProposer{k: 3}
if proposerK3.selfVoter != nil {
t.Fatal("K>1 should not have selfVoter set")
}
}