mirror of
https://github.com/luxfi/consensus.git
synced 2026-09-24 00:26:58 +00:00
THE BUG (CFT, not BFT): a proposer self-finalized its own block on its lone self-vote (ForceAccept) and a peer's REJECT was counted as ACCEPT for the proposer's own block (effectiveAccept = ... || IsOwnProposal). Followers fast-followed unverified gossip and courtesy-voted only back to the proposer (broken vote topology), so they could never reach threshold themselves. An equivocating proposer could finalize two valid roots at one height -> fork. THE FIX: - QuorumCert (engine/chain/quorum_cert.go): the alpha-of-K finality witness. Assembles from alpha distinct correctly-signed ACCEPT votes over the canonical position; Verify enforces threshold + strictly-increasing-distinct voters + per-vote signature via a pluggable VoteVerifier. Wire codec for gossip (quorum_cert_codec.go). Quasar bridge (quorum_cert_quasar.go): upgrades to protocol/quasar.WeightedQuorumCert with real ML-DSA records + weighted Merkle inclusion when the node supplies the PQ validator set; fails closed otherwise. PROVEN: quasar's own Verify accepts the bridged cert. - ForceAccept demoted to single-validator (K==1) only; refuses K>1 (fail-closed). - effectiveAccept REJECT->ACCEPT flip deleted. Vote signatures verified at ingestion (multi-validator): forged/unsigned/wrong-position votes dropped, so the count, the cert, and IsAccepted stay consistent and forgery-proof. - Topology rebuilt: followers broadcast SIGNED votes to ALL validators; whoever collects alpha assembles + gossips the cert; followers finalize on the verified cert (HandleIncomingCert) instead of fast-following. Liveness held: late/dropped Chits no longer freeze (cert gossip + re-request), no self-finality. - Fail-closed DEX guard (quorum_guard.go): RequireQuorumFinalityForValueDEX refuses value DEX unless Mode()==QuorumFinality (K>1 + verifier). - Start refuses K>1 without a verifier (forces the node bump to wire it). Gates (all pass, race-clean): TestConsensus_NoProposerSelfFinality, _AcceptRequiresQuorum, _EquivocatingProposerCannotFinalizeBothForks, _PartitionMinorityCannotFinalize, TestDEX_BFTQuorumFinalizesFill, _ByzantineProposerFakeFillRejected, TestLiveness_NoFreezeUnderLateChits, _DroppedProposerChitsStillFinalizeViaPeerCert, TestQuasarBridge_RealCertVerifies. 194 engine/chain tests pass.
592 lines
13 KiB
Go
592 lines
13 KiB
Go
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package chain
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/luxfi/consensus/engine/chain/chaintest"
|
|
"github.com/luxfi/ids"
|
|
)
|
|
|
|
// BenchmarkEngine encapsulates the chain engine with additional tracking for benchmarks
|
|
type BenchmarkEngine struct {
|
|
*Transitive
|
|
blocks map[ids.ID]*chaintest.TestBlock
|
|
blocksByHeight map[uint64][]*chaintest.TestBlock
|
|
votes map[ids.ID]map[ids.NodeID]bool
|
|
finalized map[ids.ID]bool
|
|
preferred ids.ID
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
// NewBenchmarkEngine creates a new benchmark engine
|
|
func NewBenchmarkEngine() *BenchmarkEngine {
|
|
return &BenchmarkEngine{
|
|
Transitive: New(),
|
|
blocks: make(map[ids.ID]*chaintest.TestBlock),
|
|
blocksByHeight: make(map[uint64][]*chaintest.TestBlock),
|
|
votes: make(map[ids.ID]map[ids.NodeID]bool),
|
|
finalized: make(map[ids.ID]bool),
|
|
preferred: chaintest.Genesis.ID(),
|
|
}
|
|
}
|
|
|
|
// AddBlock adds a block to the engine
|
|
func (e *BenchmarkEngine) AddBlock(block *chaintest.TestBlock) error {
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
|
|
e.blocks[block.ID()] = block
|
|
e.blocksByHeight[block.Height()] = append(e.blocksByHeight[block.Height()], block)
|
|
|
|
// Initialize vote tracking for this block
|
|
if e.votes[block.ID()] == nil {
|
|
e.votes[block.ID()] = make(map[ids.NodeID]bool)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// AddVote adds a vote for a block
|
|
func (e *BenchmarkEngine) AddVote(blockID ids.ID, voterID ids.NodeID) error {
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
|
|
if e.votes[blockID] == nil {
|
|
e.votes[blockID] = make(map[ids.NodeID]bool)
|
|
}
|
|
e.votes[blockID][voterID] = true
|
|
|
|
// Check finalization (simplified: 2/3 of 100 validators)
|
|
if len(e.votes[blockID]) >= 67 {
|
|
e.finalized[blockID] = true
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ProcessReorg processes a chain reorganization
|
|
func (e *BenchmarkEngine) ProcessReorg(newChainTip *chaintest.TestBlock) error {
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
|
|
// Find common ancestor
|
|
currentHeight := newChainTip.Height()
|
|
for currentHeight > 0 {
|
|
if blocks, exists := e.blocksByHeight[currentHeight]; exists {
|
|
for _, b := range blocks {
|
|
if e.finalized[b.ID()] {
|
|
// Found finalized ancestor, reorg from here
|
|
e.preferred = newChainTip.ID()
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
currentHeight--
|
|
}
|
|
|
|
e.preferred = newChainTip.ID()
|
|
return nil
|
|
}
|
|
|
|
// CheckFinalization checks if a block is finalized
|
|
func (e *BenchmarkEngine) CheckFinalization(blockID ids.ID) bool {
|
|
e.mu.RLock()
|
|
defer e.mu.RUnlock()
|
|
|
|
return e.finalized[blockID]
|
|
}
|
|
|
|
// Reset resets the engine state for benchmarking
|
|
func (e *BenchmarkEngine) Reset() {
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
|
|
e.blocks = make(map[ids.ID]*chaintest.TestBlock)
|
|
e.blocksByHeight = make(map[uint64][]*chaintest.TestBlock)
|
|
e.votes = make(map[ids.ID]map[ids.NodeID]bool)
|
|
e.finalized = make(map[ids.ID]bool)
|
|
e.preferred = chaintest.Genesis.ID()
|
|
e.bootstrapped = false
|
|
}
|
|
|
|
// Benchmark single block addition
|
|
func BenchmarkAddSingleBlock(b *testing.B) {
|
|
engine := NewBenchmarkEngine()
|
|
ctx := context.Background()
|
|
_ = engine.Start(ctx, true)
|
|
|
|
parent := chaintest.Genesis
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
block := chaintest.BuildChild(parent)
|
|
_ = engine.AddBlock(block)
|
|
parent = block
|
|
}
|
|
}
|
|
|
|
// Benchmark batch block additions
|
|
func BenchmarkAddBlockBatch100(b *testing.B) {
|
|
benchmarkAddBlockBatch(b, 100)
|
|
}
|
|
|
|
func BenchmarkAddBlockBatch1000(b *testing.B) {
|
|
benchmarkAddBlockBatch(b, 1000)
|
|
}
|
|
|
|
func BenchmarkAddBlockBatch10000(b *testing.B) {
|
|
benchmarkAddBlockBatch(b, 10000)
|
|
}
|
|
|
|
func benchmarkAddBlockBatch(b *testing.B, batchSize int) {
|
|
engine := NewBenchmarkEngine()
|
|
ctx := context.Background()
|
|
_ = engine.Start(ctx, true)
|
|
|
|
// Pre-generate blocks
|
|
blocks := make([]*chaintest.TestBlock, batchSize)
|
|
parent := chaintest.Genesis
|
|
for i := 0; i < batchSize; i++ {
|
|
blocks[i] = chaintest.BuildChild(parent)
|
|
parent = blocks[i]
|
|
}
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
engine.Reset()
|
|
_ = engine.Start(ctx, true)
|
|
|
|
for _, block := range blocks {
|
|
_ = engine.AddBlock(block)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Benchmark single vote processing
|
|
func BenchmarkProcessSingleVote(b *testing.B) {
|
|
engine := NewBenchmarkEngine()
|
|
ctx := context.Background()
|
|
_ = engine.Start(ctx, true)
|
|
|
|
block := chaintest.BuildChild(chaintest.Genesis)
|
|
_ = engine.AddBlock(block)
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
voterID := ids.GenerateTestNodeID()
|
|
_ = engine.AddVote(block.ID(), voterID)
|
|
}
|
|
}
|
|
|
|
// Benchmark batch vote processing
|
|
func BenchmarkProcessVoteBatch100(b *testing.B) {
|
|
benchmarkProcessVoteBatch(b, 100)
|
|
}
|
|
|
|
func BenchmarkProcessVoteBatch1000(b *testing.B) {
|
|
benchmarkProcessVoteBatch(b, 1000)
|
|
}
|
|
|
|
func benchmarkProcessVoteBatch(b *testing.B, voteCount int) {
|
|
engine := NewBenchmarkEngine()
|
|
ctx := context.Background()
|
|
_ = engine.Start(ctx, true)
|
|
|
|
// Create multiple blocks
|
|
blocks := make([]*chaintest.TestBlock, 10)
|
|
parent := chaintest.Genesis
|
|
for i := 0; i < 10; i++ {
|
|
blocks[i] = chaintest.BuildChild(parent)
|
|
_ = engine.AddBlock(blocks[i])
|
|
parent = blocks[i]
|
|
}
|
|
|
|
// Pre-generate voters
|
|
voters := make([]ids.NodeID, voteCount)
|
|
for i := 0; i < voteCount; i++ {
|
|
voters[i] = ids.GenerateTestNodeID()
|
|
}
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
// Reset votes
|
|
engine.votes = make(map[ids.ID]map[ids.NodeID]bool)
|
|
engine.finalized = make(map[ids.ID]bool)
|
|
|
|
// Process all votes
|
|
for _, voter := range voters {
|
|
for _, block := range blocks {
|
|
_ = engine.AddVote(block.ID(), voter)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Benchmark chain reorganization
|
|
func BenchmarkChainReorgShallow(b *testing.B) {
|
|
benchmarkChainReorg(b, 10, 5) // 10 blocks, reorg at height 5
|
|
}
|
|
|
|
func BenchmarkChainReorgDeep(b *testing.B) {
|
|
benchmarkChainReorg(b, 100, 50) // 100 blocks, reorg at height 50
|
|
}
|
|
|
|
func BenchmarkChainReorgVeryDeep(b *testing.B) {
|
|
benchmarkChainReorg(b, 1000, 500) // 1000 blocks, reorg at height 500
|
|
}
|
|
|
|
func benchmarkChainReorg(b *testing.B, chainLength int, reorgDepth int) {
|
|
engine := NewBenchmarkEngine()
|
|
ctx := context.Background()
|
|
_ = engine.Start(ctx, true)
|
|
|
|
// Build main chain
|
|
mainChain := make([]*chaintest.TestBlock, chainLength)
|
|
parent := chaintest.Genesis
|
|
for i := 0; i < chainLength; i++ {
|
|
mainChain[i] = chaintest.BuildChild(parent)
|
|
_ = engine.AddBlock(mainChain[i])
|
|
|
|
// Mark some blocks as finalized
|
|
if i < reorgDepth {
|
|
engine.finalized[mainChain[i].ID()] = true
|
|
}
|
|
parent = mainChain[i]
|
|
}
|
|
|
|
// Build alternative chain from reorg point
|
|
altParent := mainChain[reorgDepth-1]
|
|
altChain := make([]*chaintest.TestBlock, chainLength-reorgDepth)
|
|
for i := 0; i < len(altChain); i++ {
|
|
altChain[i] = chaintest.BuildChild(altParent)
|
|
altParent = altChain[i]
|
|
}
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
// Add alternative chain blocks
|
|
for _, block := range altChain {
|
|
_ = engine.AddBlock(block)
|
|
}
|
|
|
|
// Process reorganization
|
|
_ = engine.ProcessReorg(altChain[len(altChain)-1])
|
|
}
|
|
}
|
|
|
|
// Benchmark finalization checking
|
|
func BenchmarkFinalizationCheck(b *testing.B) {
|
|
engine := NewBenchmarkEngine()
|
|
ctx := context.Background()
|
|
_ = engine.Start(ctx, true)
|
|
|
|
// Create blocks with varying finalization status
|
|
blocks := make([]*chaintest.TestBlock, 1000)
|
|
parent := chaintest.Genesis
|
|
for i := 0; i < 1000; i++ {
|
|
blocks[i] = chaintest.BuildChild(parent)
|
|
_ = engine.AddBlock(blocks[i])
|
|
|
|
// Finalize every 3rd block
|
|
if i%3 == 0 {
|
|
engine.finalized[blocks[i].ID()] = true
|
|
}
|
|
parent = blocks[i]
|
|
}
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
// Check finalization status of random blocks
|
|
blockIdx := i % len(blocks)
|
|
_ = engine.CheckFinalization(blocks[blockIdx].ID())
|
|
}
|
|
}
|
|
|
|
// Benchmark concurrent block operations
|
|
func BenchmarkConcurrentBlockOperations(b *testing.B) {
|
|
engine := NewBenchmarkEngine()
|
|
ctx := context.Background()
|
|
_ = engine.Start(ctx, true)
|
|
|
|
// Number of concurrent goroutines
|
|
concurrency := 10
|
|
blocksPerRoutine := 100
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
var wg sync.WaitGroup
|
|
wg.Add(concurrency)
|
|
|
|
for j := 0; j < concurrency; j++ {
|
|
go func(routineID int) {
|
|
defer wg.Done()
|
|
|
|
parent := chaintest.Genesis
|
|
for k := 0; k < blocksPerRoutine; k++ {
|
|
block := chaintest.BuildChild(parent)
|
|
_ = engine.AddBlock(block)
|
|
|
|
// Add some votes
|
|
for v := 0; v < 5; v++ {
|
|
voterID := ids.GenerateTestNodeID()
|
|
_ = engine.AddVote(block.ID(), voterID)
|
|
}
|
|
|
|
parent = block
|
|
}
|
|
}(j)
|
|
}
|
|
|
|
wg.Wait()
|
|
engine.Reset()
|
|
_ = engine.Start(ctx, true)
|
|
}
|
|
}
|
|
|
|
// Benchmark mixed operations (realistic workload)
|
|
func BenchmarkMixedOperations(b *testing.B) {
|
|
engine := NewBenchmarkEngine()
|
|
ctx := context.Background()
|
|
_ = engine.Start(ctx, true)
|
|
|
|
// Pre-generate data
|
|
blocks := make([]*chaintest.TestBlock, 100)
|
|
parent := chaintest.Genesis
|
|
for i := 0; i < 100; i++ {
|
|
blocks[i] = chaintest.BuildChild(parent)
|
|
parent = blocks[i]
|
|
}
|
|
|
|
voters := make([]ids.NodeID, 50)
|
|
for i := 0; i < 50; i++ {
|
|
voters[i] = ids.GenerateTestNodeID()
|
|
}
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
engine.Reset()
|
|
_ = engine.Start(ctx, true)
|
|
|
|
// Simulate realistic consensus workflow
|
|
for j, block := range blocks {
|
|
// Add block
|
|
_ = engine.AddBlock(block)
|
|
|
|
// Process votes (varying number)
|
|
voteCount := 30 + (j % 40) // 30-70 votes
|
|
for v := 0; v < voteCount && v < len(voters); v++ {
|
|
_ = engine.AddVote(block.ID(), voters[v])
|
|
}
|
|
|
|
// Check finalization periodically
|
|
if j%10 == 0 {
|
|
_ = engine.CheckFinalization(block.ID())
|
|
}
|
|
|
|
// Occasional reorg (every 20 blocks)
|
|
if j%20 == 0 && j > 0 {
|
|
_ = engine.ProcessReorg(blocks[j])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Benchmark engine lifecycle (start/stop)
|
|
func BenchmarkEngineStartStop(b *testing.B) {
|
|
ctx := context.Background()
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
engine := NewBenchmarkEngine()
|
|
_ = engine.Start(ctx, true)
|
|
|
|
// Add some blocks to make it realistic
|
|
parent := chaintest.Genesis
|
|
for j := 0; j < 10; j++ {
|
|
block := chaintest.BuildChild(parent)
|
|
_ = engine.AddBlock(block)
|
|
parent = block
|
|
}
|
|
|
|
_ = engine.Stop(ctx)
|
|
}
|
|
}
|
|
|
|
// Benchmark health checks
|
|
func BenchmarkHealthCheck(b *testing.B) {
|
|
engine := NewBenchmarkEngine()
|
|
ctx := context.Background()
|
|
_ = engine.Start(ctx, true)
|
|
|
|
// Add some state
|
|
parent := chaintest.Genesis
|
|
for i := 0; i < 100; i++ {
|
|
block := chaintest.BuildChild(parent)
|
|
_ = engine.AddBlock(block)
|
|
parent = block
|
|
}
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
_, _ = engine.HealthCheck(ctx)
|
|
}
|
|
}
|
|
|
|
// Benchmark block verification (simulated cryptographic operations)
|
|
func BenchmarkBlockVerification(b *testing.B) {
|
|
ctx := context.Background()
|
|
|
|
// Pre-generate blocks with varying complexity
|
|
blocks := make([]*chaintest.TestBlock, 1000)
|
|
parent := chaintest.Genesis
|
|
for i := 0; i < 1000; i++ {
|
|
blocks[i] = chaintest.BuildChild(parent)
|
|
parent = blocks[i]
|
|
}
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
block := blocks[i%len(blocks)]
|
|
_ = block.Verify(ctx)
|
|
}
|
|
}
|
|
|
|
// Benchmark block acceptance
|
|
func BenchmarkBlockAcceptance(b *testing.B) {
|
|
ctx := context.Background()
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
block := chaintest.BuildChild(chaintest.Genesis)
|
|
_ = block.Accept(ctx)
|
|
}
|
|
}
|
|
|
|
// Benchmark block rejection
|
|
func BenchmarkBlockRejection(b *testing.B) {
|
|
ctx := context.Background()
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
block := chaintest.BuildChild(chaintest.Genesis)
|
|
_ = block.Reject(ctx)
|
|
}
|
|
}
|
|
|
|
// Benchmark memory usage with large chains
|
|
func BenchmarkMemoryUsageLargeChain(b *testing.B) {
|
|
b.ReportAllocs()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
engine := NewBenchmarkEngine()
|
|
ctx := context.Background()
|
|
_ = engine.Start(ctx, true)
|
|
|
|
// Build a large chain
|
|
parent := chaintest.Genesis
|
|
for j := 0; j < 10000; j++ {
|
|
block := chaintest.BuildChild(parent)
|
|
_ = engine.AddBlock(block)
|
|
|
|
// Add some votes to increase memory pressure
|
|
if j%100 == 0 {
|
|
for v := 0; v < 50; v++ {
|
|
voterID := ids.GenerateTestNodeID()
|
|
_ = engine.AddVote(block.ID(), voterID)
|
|
}
|
|
}
|
|
|
|
parent = block
|
|
}
|
|
}
|
|
}
|
|
|
|
// Helper function to create a benchmark table
|
|
func BenchmarkBlockAdditionTable(b *testing.B) {
|
|
sizes := []int{1, 10, 100, 1000, 10000}
|
|
|
|
for _, size := range sizes {
|
|
b.Run(fmt.Sprintf("size-%d", size), func(b *testing.B) {
|
|
benchmarkAddBlockBatch(b, size)
|
|
})
|
|
}
|
|
}
|
|
|
|
// Benchmark GetBlock operation
|
|
func BenchmarkGetBlock(b *testing.B) {
|
|
engine := newTestEngine()
|
|
ctx := context.Background()
|
|
_ = engine.Start(ctx, true)
|
|
|
|
// Pre-generate block IDs
|
|
blockIDs := make([]ids.ID, 1000)
|
|
nodeIDs := make([]ids.NodeID, 100)
|
|
for i := 0; i < 1000; i++ {
|
|
blockIDs[i] = ids.GenerateTestID()
|
|
}
|
|
for i := 0; i < 100; i++ {
|
|
nodeIDs[i] = ids.GenerateTestNodeID()
|
|
}
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
blockID := blockIDs[i%len(blockIDs)]
|
|
nodeID := nodeIDs[i%len(nodeIDs)]
|
|
_ = engine.GetBlock(ctx, nodeID, uint32(i), blockID)
|
|
}
|
|
}
|
|
|
|
// Benchmark bootstrapping process
|
|
func BenchmarkBootstrapping(b *testing.B) {
|
|
b.ReportAllocs()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
engine := newTestEngine()
|
|
ctx := context.Background()
|
|
|
|
// Start triggers bootstrapping
|
|
_ = engine.Start(ctx, true)
|
|
|
|
// Verify bootstrapped
|
|
if !engine.IsBootstrapped() {
|
|
b.Error("Engine should be bootstrapped")
|
|
}
|
|
|
|
_ = engine.Stop(ctx)
|
|
}
|
|
}
|