Files
consensus/config/config_test.go
zeekay a4b6022596 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

244 lines
5.6 KiB
Go

package config
import (
"fmt"
"math"
"testing"
)
// TestConsensusThresholds verifies thresholds are properly configured
func TestConsensusThresholds(t *testing.T) {
tests := []struct {
name string
params Parameters
k int
alpha int
alphaF float64
minRatio float64
}{
{
name: "DefaultParams",
params: DefaultParams(),
k: 20,
alpha: 14, // 70% of 20
alphaF: 0.69, // 69% threshold
minRatio: 0.69,
},
{
name: "MainnetParams",
params: MainnetParams(),
k: 21,
alpha: 15, // ~71% of 21
alphaF: 0.69, // 69% threshold
minRatio: 0.69,
},
{
name: "TestnetParams",
params: TestnetParams(),
k: 11,
alpha: 8, // ~73% of 11
alphaF: 0.69, // 69% threshold
minRatio: 0.69,
},
{
name: "LocalParams",
params: LocalParams(),
k: 3,
alpha: 2, // 2/3 of 3
alphaF: 0.67, // 2/3 threshold
minRatio: 0.66,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Check K value
if tt.params.K != tt.k {
t.Errorf("K = %d, want %d", tt.params.K, tt.k)
}
// Check Alpha matches expected threshold
if tt.params.Alpha != tt.alphaF {
t.Errorf("Alpha = %f, want %f", tt.params.Alpha, tt.alphaF)
}
// Check AlphaPreference matches
if tt.params.AlphaPreference != tt.alpha {
t.Errorf("AlphaPreference = %d, want %d", tt.params.AlphaPreference, tt.alpha)
}
// Verify AlphaPreference meets minimum ratio of K
minAlpha := math.Ceil(float64(tt.params.K) * tt.minRatio)
if float64(tt.params.AlphaPreference) < minAlpha {
t.Errorf("AlphaPreference %d is below %.0f%% threshold %f",
tt.params.AlphaPreference, tt.minRatio*100, minAlpha)
}
// Check that parameters are valid
if err := tt.params.Valid(); err != nil {
t.Errorf("Parameters validation failed: %v", err)
}
})
}
}
// TestByzantineTolerance31 verifies maximum Byzantine tolerance is 31%
func TestByzantineTolerance31(t *testing.T) {
tests := []struct {
byzantineWeight uint64
totalWeight uint64
canTolerate bool
}{
{31, 100, true}, // Exactly 31%
{30, 100, true}, // Below threshold
{32, 100, false}, // Above threshold
{310, 1000, true}, // 31% at scale
{311, 1000, false}, // Slightly above 31%
}
for _, tt := range tests {
result := CanTolerateFailure(tt.byzantineWeight, tt.totalWeight)
if result != tt.canTolerate {
t.Errorf("CanTolerateFailure(%d, %d) = %v, want %v",
tt.byzantineWeight, tt.totalWeight, result, tt.canTolerate)
}
}
}
// TestQuorumCalculation verifies 69% quorum calculations
func TestQuorumCalculation(t *testing.T) {
tests := []struct {
totalWeight uint64
expectedQuorum uint64
}{
{100, 69},
{1000, 690},
{10000, 6900},
{33, 23}, // Ceiling of 22.77
{67, 47}, // Ceiling of 46.23
}
for _, tt := range tests {
quorum := CalculateQuorum(tt.totalWeight)
if quorum != tt.expectedQuorum {
t.Errorf("CalculateQuorum(%d) = %d, want %d",
tt.totalWeight, quorum, tt.expectedQuorum)
}
}
}
// TestHasSuperMajority verifies super majority checks
func TestHasSuperMajority(t *testing.T) {
tests := []struct {
weight uint64
totalWeight uint64
hasMajority bool
}{
{69, 100, true}, // Exactly 69%
{70, 100, true}, // Above threshold
{68, 100, false}, // Below threshold
{690, 1000, true}, // 69% at scale
{689, 1000, false}, // Just below
}
for _, tt := range tests {
result := HasSuperMajority(tt.weight, tt.totalWeight)
if result != tt.hasMajority {
t.Errorf("HasSuperMajority(%d, %d) = %v, want %v",
tt.weight, tt.totalWeight, result, tt.hasMajority)
}
}
}
// TestAlphaForK verifies Alpha calculation for different K values
func TestAlphaForK(t *testing.T) {
tests := []struct {
k int
expectedAlpha int
}{
{20, 14}, // Ceiling of 13.8
{21, 15}, // Ceiling of 14.49
{11, 8}, // Ceiling of 7.59
{5, 4}, // Ceiling of 3.45
{100, 69}, // Exactly 69
}
for _, tt := range tests {
alpha := AlphaForK(tt.k)
if alpha != tt.expectedAlpha {
t.Errorf("AlphaForK(%d) = %d, want %d",
tt.k, alpha, tt.expectedAlpha)
}
}
}
// TestParameterValidation verifies parameter validation with 69% threshold
func TestParameterValidation(t *testing.T) {
tests := []struct {
name string
params Parameters
expectedErr error
}{
{
name: "Valid 69% threshold",
params: Parameters{
K: 20,
Alpha: 0.69,
Beta: 14,
},
expectedErr: nil,
},
{
name: "Below 2/3 threshold",
params: Parameters{
K: 20,
Alpha: 0.65, // Below minimum 2/3 threshold
Beta: 14,
},
expectedErr: ErrInvalidAlpha,
},
{
name: "Invalid K",
params: Parameters{
K: 0,
Alpha: 0.69,
Beta: 14,
},
expectedErr: ErrInvalidK,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.params.Valid()
if err != tt.expectedErr {
t.Errorf("Valid() error = %v, want %v", err, tt.expectedErr)
}
})
}
}
// BenchmarkQuorumCalculation benchmarks quorum calculations
func BenchmarkQuorumCalculation(b *testing.B) {
weights := []uint64{100, 1000, 10000, 100000, 1000000}
for _, w := range weights {
b.Run(fmt.Sprintf("weight_%d", w), func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = CalculateQuorum(w)
}
})
}
}
// BenchmarkHasSuperMajority benchmarks super majority checks
func BenchmarkHasSuperMajority(b *testing.B) {
totalWeight := uint64(1000000)
weight := uint64(690000)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = HasSuperMajority(weight, totalWeight)
}
}