Files
zeekay 3606f54d2c SECURITY: fix threshold sig bypass + BlockCert + min threshold
CRITICAL: VerifyQuasarSigWithContext no longer returns true for
IsThreshold sigs — verifies BLS bytes against group key.
CRITICAL: BlockCert.Verify returns false; VerifyWithKeys requires
real crypto verification.
HIGH: NewEngine/NewQuasar reject threshold < 2 (NewTestEngine/
NewTestQuasar for single-node testing).
13 security regression tests added.
2026-03-02 23:59:00 -08:00

384 lines
11 KiB
Go

// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package main
import (
"context"
"fmt"
"testing"
"time"
"github.com/luxfi/consensus"
"github.com/luxfi/consensus/core/dag"
"github.com/luxfi/consensus/protocol/wave/fpc"
"github.com/luxfi/ids"
"github.com/stretchr/testify/require"
)
// TestFPCDynamicThresholds tests FPC threshold selection
func TestFPCDynamicThresholds(t *testing.T) {
seed := fpc.DeriveEpochSeed(0, []byte("test-chain"), nil)
selector, err := fpc.NewSelector(0.5, 0.8, seed)
require.NoError(t, err)
k := 20
// Test determinism: same round = same threshold
t1 := selector.SelectThreshold(0, k)
t2 := selector.SelectThreshold(0, k)
require.Equal(t, t1, t2, "Same round must give same threshold (determinism)")
// Test variety: different rounds = different thresholds
thresholds := make(map[int]bool)
for round := uint64(0); round < 100; round++ {
threshold := selector.SelectThreshold(round, k)
// Verify threshold is in valid range
require.GreaterOrEqual(t, threshold, 10, "Threshold should be ≥ 50%% of k")
require.LessOrEqual(t, threshold, 16, "Threshold should be ≤ 80%% of k")
thresholds[threshold] = true
}
require.Greater(t, len(thresholds), 1, "Should have variety in thresholds")
t.Logf("✓ FPC generated %d unique thresholds across 100 rounds", len(thresholds))
}
// TestBlockFinalization tests complete block finalization
func TestBlockFinalization(t *testing.T) {
cfg := consensus.DefaultConfig()
chain := consensus.NewChain(cfg)
ctx := context.Background()
err := chain.Start(ctx)
require.NoError(t, err)
defer chain.Stop()
// Create block
block := &consensus.Block{
ID: consensus.ID{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32},
ParentID: consensus.GenesisID,
Height: 1,
Time: time.Now(),
Payload: []byte("Test block finalization"),
}
// Add block
err = chain.Add(ctx, block)
require.NoError(t, err, "Block should be added")
// Simulate votes
validators := make([]consensus.NodeID, 20)
for i := range validators {
validators[i] = consensus.NodeID{byte(i + 1)}
}
for _, validator := range validators {
vote := consensus.NewVote(block.ID, consensus.VotePreference, validator)
err = chain.RecordVote(ctx, vote)
require.NoError(t, err)
}
// Verify finality
status := chain.GetStatus(block.ID)
require.Equal(t, consensus.StatusAccepted, status, "Block should achieve finality")
t.Log("✓ Block achieved finality with full validator support")
}
// TestHorizonReachability tests DAG reachability algorithm
func TestHorizonReachability(t *testing.T) {
genesis := ids.Empty
b1, _ := ids.ToID([]byte("block_1_________________________"))
b2, _ := ids.ToID([]byte("block_2_________________________"))
b3, _ := ids.ToID([]byte("block_3_________________________"))
store := createMockDAG(genesis, b1, b2, b3)
// Test forward reachability
require.True(t, dag.IsReachable[ids.ID](store, genesis, b1), "Genesis should reach B1")
require.True(t, dag.IsReachable[ids.ID](store, genesis, b3), "Genesis should reach B3")
require.True(t, dag.IsReachable[ids.ID](store, b1, b3), "B1 should reach B3")
// Test backward non-reachability (DAG is directed)
require.False(t, dag.IsReachable[ids.ID](store, b3, genesis), "B3 cannot reach Genesis")
require.False(t, dag.IsReachable[ids.ID](store, b1, genesis), "B1 cannot reach Genesis")
t.Log("✓ Reachability algorithm works correctly")
}
// TestLCA tests Lowest Common Ancestor algorithm
func TestLCA(t *testing.T) {
genesis := ids.Empty
b1, _ := ids.ToID([]byte("block_1_________________________"))
b2, _ := ids.ToID([]byte("block_2_________________________"))
b3, _ := ids.ToID([]byte("block_3_________________________"))
store := createMockDAG(genesis, b1, b2, b3)
// LCA of parallel blocks should be their common parent
lca := dag.LCA[ids.ID](store, b1, b2)
require.Equal(t, genesis, lca, "LCA of B1 and B2 should be Genesis")
t.Log("✓ LCA algorithm works correctly")
}
// TestSafePrefix tests finality detection
func TestSafePrefix(t *testing.T) {
genesis := ids.Empty
b1, _ := ids.ToID([]byte("block_1_________________________"))
b2, _ := ids.ToID([]byte("block_2_________________________"))
b3, _ := ids.ToID([]byte("block_3_________________________"))
store := createMockDAG(genesis, b1, b2, b3)
// Safe prefix with B3 as frontier should include ancestors
frontier := []ids.ID{b3}
safe := dag.ComputeSafePrefix[ids.ID](store, frontier)
require.NotNil(t, safe, "Should return safe prefix")
t.Logf("✓ Safe prefix computed: %d vertices finalized", len(safe))
}
// TestChooseFrontier tests Byzantine-tolerant parent selection
func TestChooseFrontier(t *testing.T) {
// Create set of vertices
vertices := make([]ids.ID, 10)
for i := range vertices {
vertices[i], _ = ids.ToID([]byte(fmt.Sprintf("vertex_%d", i)))
}
// Choose frontier with Byzantine tolerance
chosen := dag.ChooseFrontier[ids.ID](vertices)
require.NotEmpty(t, chosen, "Should choose some vertices")
require.LessOrEqual(t, len(chosen), len(vertices), "Can't choose more than available")
// For 10 vertices, f=(10-1)/3=3, so 2f+1=7
// Should choose min(7, 10) = 7 or all if < 7
t.Logf("✓ Chose %d vertices with Byzantine tolerance", len(chosen))
}
// TestEventHorizon tests horizon advancement
func TestEventHorizon(t *testing.T) {
genesis := ids.Empty
b1, _ := ids.ToID([]byte("block_1_________________________"))
b2, _ := ids.ToID([]byte("block_2_________________________"))
b3, _ := ids.ToID([]byte("block_3_________________________"))
store := createMockDAG(genesis, b1, b2, b3)
// Create initial checkpoint
checkpoints := []dag.EventHorizon[ids.ID]{
{
Checkpoint: genesis,
Height: 0,
Validators: []string{"validator_set_0"},
Signature: []byte("corona+bls_sig_0"),
},
}
// Compute new horizon
newHorizon := dag.Horizon[ids.ID](store, checkpoints)
require.NotEqual(t, ids.ID{}, newHorizon.Checkpoint, "Should have valid checkpoint")
require.GreaterOrEqual(t, newHorizon.Height, checkpoints[0].Height, "Horizon should not regress")
t.Logf("✓ Event horizon advanced from height %d to %d", checkpoints[0].Height, newHorizon.Height)
}
// TestBeyondHorizon tests finality boundary checking
func TestBeyondHorizon(t *testing.T) {
genesis := ids.Empty
b1, _ := ids.ToID([]byte("block_1_________________________"))
b2, _ := ids.ToID([]byte("block_2_________________________"))
b3, _ := ids.ToID([]byte("block_3_________________________"))
store := createMockDAG(genesis, b1, b2, b3)
horizon := dag.EventHorizon[ids.ID]{
Checkpoint: genesis,
Height: 0,
Validators: []string{"set"},
Signature: []byte("sig"),
}
// Blocks reachable from genesis should be beyond horizon
beyond := dag.BeyondHorizon[ids.ID](store, b3, horizon)
require.True(t, beyond, "B3 should be beyond horizon (reachable from checkpoint)")
t.Log("✓ BeyondHorizon correctly identifies finalized vertices")
}
// TestQuasarIntegration tests full protocol integration
func TestQuasarIntegration(t *testing.T) {
// Create chain
cfg := consensus.DefaultConfig()
chain := consensus.NewChain(cfg)
ctx := context.Background()
err := chain.Start(ctx)
require.NoError(t, err)
defer chain.Stop()
// Add multiple blocks
blocks := make([]*consensus.Block, 5)
for i := range blocks {
blocks[i] = &consensus.Block{
ID: consensus.ID{byte(i + 1)},
ParentID: consensus.GenesisID,
Height: uint64(i + 1),
Time: time.Now(),
Payload: []byte(fmt.Sprintf("Block %d", i+1)),
}
err = chain.Add(ctx, blocks[i])
require.NoError(t, err)
}
// Vote on all blocks
validators := make([]consensus.NodeID, 20)
for i := range validators {
validators[i] = consensus.NodeID{byte(i + 1)}
}
for _, block := range blocks {
for _, validator := range validators {
vote := consensus.NewVote(block.ID, consensus.VotePreference, validator)
chain.RecordVote(ctx, vote)
}
}
// Verify all blocks finalized
finalizedCount := 0
for _, block := range blocks {
if chain.GetStatus(block.ID) == consensus.StatusAccepted {
finalizedCount++
}
}
require.Greater(t, finalizedCount, 0, "At least some blocks should finalize")
t.Logf("✓ %d/%d blocks achieved finality", finalizedCount, len(blocks))
}
// TestCertificateThresholds tests Byzantine fault tolerance math
func TestCertificateThresholds(t *testing.T) {
testCases := []struct {
n int
f int
certThreshold int
description string
}{
{20, 6, 13, "Production network (n=20, f=6)"},
{100, 33, 67, "Large network (n=100, f=33)"},
{7, 2, 5, "Small network (n=7, f=2)"},
{4, 1, 3, "Minimal BFT (n=4, f=1)"},
}
for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
params := dag.Params{N: tc.n, F: tc.f}
// Verify Byzantine tolerance: n > 3f
require.Greater(t, params.N, 3*params.F, "Must have n > 3f for BFT")
// Verify certificate threshold: 2f+1
certThreshold := 2*params.F + 1
require.Equal(t, tc.certThreshold, certThreshold)
// Verify safety: can't have both cert and skip
// (need 2f+1 for cert AND 2f+1 for skip = 4f+2 > n when n=3f+1)
require.Greater(t, 2*certThreshold, params.N,
"Certificate and skip thresholds ensure mutual exclusivity")
t.Logf("✓ n=%d, f=%d: requires %d/%d validators (%.1f%%)",
tc.n, tc.f, certThreshold, tc.n, 100.0*float64(certThreshold)/float64(tc.n))
})
}
}
// BenchmarkQuasarFullProtocol benchmarks the complete protocol
func BenchmarkQuasarFullProtocol(b *testing.B) {
cfg := consensus.DefaultConfig()
chain := consensus.NewChain(cfg)
ctx := context.Background()
chain.Start(ctx)
defer chain.Stop()
b.ResetTimer()
for i := 0; i < b.N; i++ {
block := &consensus.Block{
ID: consensus.ID{byte(i)},
ParentID: consensus.GenesisID,
Height: uint64(i + 1),
Payload: []byte("benchmark"),
}
chain.Add(ctx, block)
// Simulate votes
for j := 0; j < 20; j++ {
vote := consensus.NewVote(block.ID, consensus.VotePreference, consensus.NodeID{byte(j)})
chain.RecordVote(ctx, vote)
}
}
}
// BenchmarkFPCThresholdSelection benchmarks FPC performance
func BenchmarkFPCThresholdSelection(b *testing.B) {
seed := fpc.DeriveEpochSeed(0, []byte("bench-chain"), nil)
selector, _ := fpc.NewSelector(0.5, 0.8, seed)
k := 20
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = selector.SelectThreshold(uint64(i), k)
}
}
// BenchmarkHorizonAlgorithms benchmarks DAG algorithms
func BenchmarkHorizonAlgorithms(b *testing.B) {
genesis := ids.Empty
b1, _ := ids.ToID([]byte("block_1_________________________"))
b2, _ := ids.ToID([]byte("block_2_________________________"))
b3, _ := ids.ToID([]byte("block_3_________________________"))
store := createMockDAG(genesis, b1, b2, b3)
b.Run("IsReachable", func(b *testing.B) {
for i := 0; i < b.N; i++ {
dag.IsReachable[ids.ID](store, genesis, b3)
}
})
b.Run("LCA", func(b *testing.B) {
for i := 0; i < b.N; i++ {
dag.LCA[ids.ID](store, b1, b2)
}
})
b.Run("ComputeSafePrefix", func(b *testing.B) {
frontier := []ids.ID{b3}
for i := 0; i < b.N; i++ {
dag.ComputeSafePrefix[ids.ID](store, frontier)
}
})
b.Run("ChooseFrontier", func(b *testing.B) {
vertices := []ids.ID{b1, b2, b3}
for i := 0; i < b.N; i++ {
dag.ChooseFrontier[ids.ID](vertices)
}
})
b.Run("Horizon", func(b *testing.B) {
checkpoints := []dag.EventHorizon[ids.ID]{
{Checkpoint: genesis, Height: 0},
}
for i := 0; i < b.N; i++ {
dag.Horizon[ids.ID](store, checkpoints)
}
})
}