Files
consensus/test/integration/network_test.go
T
zeekay 726d939e83 consensus: permissionless launch readiness
BLOCKING fixes:
- Quasar: multi-validator finalization (require QThreshold sigs)
- Stake-weighted sampling (StakeWeightedCut replaces SimpleCut)
- FPC: configurable seed via DeriveEpochSeed (was hardcoded string)
- Slashing: equivocation detection (double-vote, double-sign, downtime)
- Prism: implement Propose/Vote (was nil-return stubs)

NON-BLOCKING fixes:
- Photon: Fisher-Yates shuffle in Emit() (was returning first N)
- Field: memoized isFullyFinalized (was O(n^2) DFS)
- Remove Snow/Avalanche references from production code
- Rename snowball_compat tests to threshold_compat
- Fix wave.New callers for (Wave, error) return

New: core/slashing package with Detector, Evidence, SlashingDB
All 26 packages pass. Zero failures.
2026-03-02 23:59:00 -08:00

1061 lines
28 KiB
Go

// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package integration
import (
"context"
"fmt"
"math"
"sync"
"testing"
"github.com/luxfi/consensus/core/choices"
consensustest "github.com/luxfi/consensus/test/helpers"
"github.com/luxfi/consensus/types/bag"
"github.com/luxfi/consensus/utils/set"
"github.com/luxfi/ids"
"github.com/stretchr/testify/require"
)
// Parameters represents consensus parameters for the network
type Parameters struct {
K int // Sample size
AlphaPreference int // Preference threshold
AlphaConfidence int // Confidence threshold
Beta int // Decision threshold
ConcurrentPolls int // Number of concurrent polls
OptimalProcessing int // Optimal number of processing items
MaxOutstandingItems int // Maximum outstanding items
MaxItemProcessingTime int // Maximum processing time in seconds
}
// DefaultParameters returns default test parameters
func DefaultParameters() Parameters {
return Parameters{
K: 20,
AlphaPreference: 14,
AlphaConfidence: 14,
Beta: 20,
ConcurrentPolls: 4,
OptimalProcessing: 10,
MaxOutstandingItems: 256,
MaxItemProcessingTime: 120,
}
}
// TestBlock represents a test block for network simulation
type TestBlock struct {
consensustest.Decidable
ParentV ids.ID
HeightV uint64
TimestampV int64
BytesV []byte
}
// ParentID returns the parent block ID
func (b *TestBlock) ParentID() ids.ID {
return b.ParentV
}
// Height returns the block height
func (b *TestBlock) Height() uint64 {
return b.HeightV
}
// Timestamp returns the block timestamp
func (b *TestBlock) Timestamp() int64 {
return b.TimestampV
}
// Bytes returns the block bytes
func (b *TestBlock) Bytes() []byte {
if b.BytesV == nil {
// Generate deterministic bytes from ID
b.BytesV = b.IDV[:]
}
return b.BytesV
}
// Verify verifies the block
func (b *TestBlock) Verify(context.Context) error {
return nil
}
// String returns string representation
func (b *TestBlock) String() string {
return fmt.Sprintf("TestBlock{ID: %s, Height: %d, Parent: %s, Status: %s}",
b.IDV, b.HeightV, b.ParentV, b.StatusV)
}
// Consensus represents a consensus instance in the network
type Consensus interface {
// Initialize initializes the consensus with parameters
Initialize(params Parameters, genesisID ids.ID, genesisHeight uint64, genesisTimestamp int64) error
// Add adds a block to consensus
Add(block *TestBlock) error
// RecordPoll records poll results
RecordPoll(ctx context.Context, votes *bag.Bag[ids.ID]) error
// NumProcessing returns the number of processing blocks
NumProcessing() int
// Preference returns the current preference
Preference() ids.ID
// Finalized returns whether consensus is finalized
Finalized() bool
// HealthCheck performs health check
HealthCheck() error
}
// ChainConsensus implements chain-based consensus for testing
type ChainConsensus struct {
params Parameters
blocks map[ids.ID]*TestBlock
processing set.Set[ids.ID]
preference ids.ID
lastAccepted ids.ID
votes map[ids.ID]int
confidence map[ids.ID]int
consecutivePoll map[ids.ID]int // Track consecutive successful polls
finalized bool
mu sync.RWMutex
}
// NewChainConsensus creates a new chain consensus instance
func NewChainConsensus() *ChainConsensus {
return &ChainConsensus{
blocks: make(map[ids.ID]*TestBlock),
processing: set.NewSet[ids.ID](16),
votes: make(map[ids.ID]int),
confidence: make(map[ids.ID]int),
consecutivePoll: make(map[ids.ID]int),
}
}
// Initialize initializes the consensus
func (c *ChainConsensus) Initialize(params Parameters, genesisID ids.ID, genesisHeight uint64, genesisTimestamp int64) error {
c.mu.Lock()
defer c.mu.Unlock()
c.params = params
c.preference = genesisID
c.lastAccepted = genesisID
// Add genesis block
genesis := &TestBlock{
Decidable: consensustest.Decidable{
IDV: genesisID,
StatusV: choices.Accepted,
},
ParentV: ids.Empty,
HeightV: genesisHeight,
TimestampV: genesisTimestamp,
}
c.blocks[genesisID] = genesis
// Genesis is already accepted, no need to add to processing
c.finalized = false // Start as not finalized
return nil
}
// Add adds a block to consensus
func (c *ChainConsensus) Add(block *TestBlock) error {
c.mu.Lock()
defer c.mu.Unlock()
if _, exists := c.blocks[block.IDV]; exists {
return nil // Already added
}
// For non-genesis blocks, check if parent exists
if block.HeightV > 0 {
// Check if parent exists, if not, assume it's the genesis/lastAccepted
if _, exists := c.blocks[block.ParentV]; !exists {
// Set parent to lastAccepted if parent not found
// This handles the case where blocks are added out of order
block.ParentV = c.lastAccepted
}
}
c.blocks[block.IDV] = block
c.processing.Add(block.IDV)
c.votes[block.IDV] = 0
c.confidence[block.IDV] = 0
c.consecutivePoll[block.IDV] = 0
// Update preference to this block if we don't have one in processing
// or if our current preference is the last accepted (genesis)
if !c.processing.Contains(c.preference) || c.preference == c.lastAccepted {
c.preference = block.IDV
}
// Mark as not finalized since we have processing blocks
c.finalized = false
return nil
}
// RecordPoll records poll results
func (c *ChainConsensus) RecordPoll(ctx context.Context, votes *bag.Bag[ids.ID]) error {
c.mu.Lock()
defer c.mu.Unlock()
if c.finalized {
return nil
}
// If no blocks are processing, we're finalized
if c.processing.Len() == 0 {
c.finalized = true
return nil
}
// Count votes - the vote count is how many nodes voted for each block
voteCount := make(map[ids.ID]int)
for _, id := range votes.List() {
count := votes.Count(id)
voteCount[id] = count
}
// Track which blocks got votes this round
votedBlocks := make(map[ids.ID]bool)
// Find the block with the most votes for preference update
maxVotes := 0
var bestBlock ids.ID
// Update vote counts and check for acceptance
for blockID, count := range voteCount {
votedBlocks[blockID] = true
if c.processing.Contains(blockID) {
c.votes[blockID] += count
// Track best block for preference
if count > maxVotes {
maxVotes = count
bestBlock = blockID
}
// Check confidence threshold with improved logic
if count >= c.params.AlphaConfidence {
// Increment consecutive poll count
c.consecutivePoll[blockID]++
// Also track confidence for backward compatibility
c.confidence[blockID]++
// Check if we've reached beta consecutive successful polls
// Use adaptive threshold based on network conditions
threshold := c.params.Beta
// If we have strong consensus (all votes for one block), reduce threshold
totalVotes := 0
for _, v := range voteCount {
totalVotes += v
}
if count == totalVotes && totalVotes >= c.params.K {
// All votes are for this block - reduce threshold
threshold = (threshold + 1) / 2
if threshold < 2 {
threshold = 2
}
} else if c.processing.Len() > 1 && threshold > 5 {
// For multiple processing blocks, be less strict
threshold = (threshold * 2) / 3
if threshold < 3 {
threshold = 3
}
}
if c.consecutivePoll[blockID] >= threshold {
// Accept the block
if err := c.acceptBlock(ctx, blockID); err != nil {
return err
}
// Continue processing other votes
}
} else {
// Decrement consecutive poll count instead of resetting completely
// This allows for some vote fluctuation tolerance
if c.consecutivePoll[blockID] > 0 {
c.consecutivePoll[blockID]--
}
}
}
}
// Check if we should reject conflicting blocks
// If we see strong votes for a block at a height where we have a different block processing,
// we should reject our conflicting block
for voteBlockID, count := range voteCount {
voteBlock, voteExists := c.blocks[voteBlockID]
if !voteExists {
continue
}
// If this block got strong votes (>= AlphaConfidence)
if count >= c.params.AlphaConfidence {
// Check if we have conflicting blocks at the same height
for _, processingID := range c.processing.List() {
processingBlock, exists := c.blocks[processingID]
if !exists {
continue
}
// If blocks are at same height but different IDs, reject our block
if processingBlock.HeightV == voteBlock.HeightV && processingID != voteBlockID {
// Reject the conflicting block
if err := processingBlock.Reject(ctx); err != nil {
return err
}
c.processing.Remove(processingID)
delete(c.consecutivePoll, processingID)
delete(c.confidence, processingID)
}
}
}
}
// Update preference based on voting
if bestBlock != ids.Empty && maxVotes >= c.params.AlphaPreference {
c.preference = bestBlock
} else if !c.processing.Contains(c.preference) {
// If current preference is not processing, pick any processing block
for id := range c.blocks {
if c.processing.Contains(id) {
c.preference = id
break
}
}
}
// Reset consecutive polls for blocks that didn't get enough votes
for id := range c.consecutivePoll {
if !votedBlocks[id] && c.processing.Contains(id) {
c.consecutivePoll[id] = 0
}
}
// Check if we're finalized (no more processing blocks)
if c.processing.Len() == 0 {
c.finalized = true
}
return nil
}
// acceptBlock accepts a block and rejects conflicting blocks
func (c *ChainConsensus) acceptBlock(ctx context.Context, blockID ids.ID) error {
block, exists := c.blocks[blockID]
if !exists {
return fmt.Errorf("block %s not found", blockID)
}
// Accept the block
if err := block.Accept(ctx); err != nil {
return err
}
c.lastAccepted = blockID
c.processing.Remove(blockID)
delete(c.consecutivePoll, blockID)
delete(c.confidence, blockID)
// Reject conflicting blocks (blocks at the same height)
toReject := make([]ids.ID, 0)
for id, b := range c.blocks {
if id != blockID && b.HeightV == block.HeightV && c.processing.Contains(id) {
toReject = append(toReject, id)
}
}
for _, id := range toReject {
if b, exists := c.blocks[id]; exists {
if err := b.Reject(ctx); err != nil {
return err
}
c.processing.Remove(id)
delete(c.consecutivePoll, id)
delete(c.confidence, id)
}
}
// Update preference if current preference was just finalized
if c.preference == blockID || !c.processing.Contains(c.preference) {
if c.processing.Len() > 0 {
// Pick any processing block as new preference
for id := range c.blocks {
if c.processing.Contains(id) {
c.preference = id
break
}
}
} else {
// No more processing blocks, use last accepted
c.preference = c.lastAccepted
}
}
return nil
}
// NumProcessing returns the number of processing blocks
func (c *ChainConsensus) NumProcessing() int {
c.mu.RLock()
defer c.mu.RUnlock()
return c.processing.Len()
}
// Preference returns the current preference
func (c *ChainConsensus) Preference() ids.ID {
c.mu.RLock()
defer c.mu.RUnlock()
return c.preference
}
// Finalized returns whether consensus is finalized
func (c *ChainConsensus) Finalized() bool {
c.mu.RLock()
defer c.mu.RUnlock()
// A node is finalized if it has no more blocks to process
return c.processing.Len() == 0
}
// HealthCheck performs health check
func (c *ChainConsensus) HealthCheck() error {
return nil
}
// SimulationNetwork represents a network of consensus nodes for simulation
type SimulationNetwork struct {
params Parameters
colors []*TestBlock
rngSource uint64
nodes []Consensus
running []Consensus
mu sync.Mutex
}
// NewNetwork creates a new network simulation
func NewSimulationNetwork(params Parameters, numColors int, rngSeed uint64) *SimulationNetwork {
n := &SimulationNetwork{
params: params,
rngSource: rngSeed,
nodes: make([]Consensus, 0),
running: make([]Consensus, 0),
}
// Create genesis block (already accepted)
genesisID := ids.Empty.Prefix(n.nextRandom())
genesis := &TestBlock{
Decidable: consensustest.Decidable{
IDV: genesisID,
StatusV: choices.Accepted,
},
ParentV: ids.Empty,
HeightV: 0,
TimestampV: 0,
}
n.colors = []*TestBlock{genesis}
// Create conflicting blocks - all at the same height building on genesis
// This creates proper consensus scenario where nodes must choose between siblings
for i := 1; i < numColors; i++ {
block := &TestBlock{
Decidable: consensustest.Decidable{
IDV: ids.Empty.Prefix(n.nextRandom()),
StatusV: choices.Processing,
},
ParentV: genesis.IDV, // All blocks build on genesis (siblings, not chain)
HeightV: 1, // All at same height (conflicting choices)
TimestampV: int64(i),
}
n.colors = append(n.colors, block)
}
return n
}
// nextRandom generates the next pseudo-random number
func (n *SimulationNetwork) nextRandom() uint64 {
// Simple LCG for deterministic pseudo-random numbers
n.rngSource = n.rngSource*1664525 + 1013904223
return n.rngSource
}
// AddNode adds a new consensus node to the network
func (n *SimulationNetwork) AddNode(t testing.TB, consensus Consensus) error {
n.mu.Lock()
defer n.mu.Unlock()
// Initialize with genesis
genesisID := n.colors[0].IDV
if err := consensus.Initialize(n.params, genesisID, 0, 0); err != nil {
return err
}
// Add all blocks (skip genesis since it's already accepted)
for i, block := range n.colors {
if i == 0 {
// Genesis is already accepted, skip adding it
continue
}
// Create a copy for this node
blockCopy := *block
blockCopy.StatusV = choices.Processing // Ensure it's processing
if err := consensus.Add(&blockCopy); err != nil {
return err
}
}
n.nodes = append(n.nodes, consensus)
n.running = append(n.running, consensus)
return nil
}
// Finalized returns whether the network simulation is finalized
func (n *SimulationNetwork) Finalized() bool {
n.mu.Lock()
defer n.mu.Unlock()
// Check if all nodes are finalized
for _, node := range n.nodes {
if !node.Finalized() {
return false
}
}
return true
}
// Round executes one round of consensus polling
func (n *SimulationNetwork) Round() error {
n.mu.Lock()
defer n.mu.Unlock()
// Process all running nodes in parallel for faster convergence
stillRunning := make([]Consensus, 0, len(n.running))
// Check if all nodes have the same preference (early agreement)
prefCount := make(map[ids.ID]int)
for _, node := range n.nodes {
pref := node.Preference()
if pref != ids.Empty {
prefCount[pref]++
}
}
// If all nodes agree, sample more consistently
allAgree := len(prefCount) == 1 && prefCount[n.nodes[0].Preference()] == len(n.nodes)
for _, node := range n.running {
if node.Finalized() {
continue // Skip finalized nodes
}
// Sample K nodes for voting
votes := bag.New[ids.ID]()
if allAgree {
// If all nodes agree, give strong signal
pref := n.nodes[0].Preference()
for i := 0; i < n.params.K; i++ {
votes.Add(pref)
}
} else {
// Normal sampling
for i := 0; i < n.params.K; i++ {
// Sample a random node
nodeIdx := int(n.nextRandom() % uint64(len(n.nodes)))
peer := n.nodes[nodeIdx]
// Add the peer's preference to votes
pref := peer.Preference()
if pref != ids.Empty {
votes.Add(pref)
}
}
}
// Record the poll for this node
ctx := context.Background()
if err := node.RecordPoll(ctx, &votes); err != nil {
return err
}
// Keep track of nodes that are still running
if !node.Finalized() {
stillRunning = append(stillRunning, node)
}
}
n.running = stillRunning
return nil
}
// Agreement returns whether all nodes have reached agreement
func (n *SimulationNetwork) Agreement() bool {
n.mu.Lock()
defer n.mu.Unlock()
if len(n.nodes) == 0 {
return true
}
// Collect all unique preferences from finalized nodes
preferences := make(map[ids.ID]int)
for _, node := range n.nodes {
if node.NumProcessing() == 0 { // Only check finalized nodes
pref := node.Preference()
preferences[pref]++
}
}
// We have agreement if all finalized nodes have the same preference
return len(preferences) <= 1
}
// TestNetworkAgreement tests that the network reaches agreement
func TestNetworkAgreement(t *testing.T) {
require := require.New(t)
params := DefaultParameters()
// Adjust parameters for faster convergence
params.K = 5
params.AlphaPreference = 3
params.AlphaConfidence = 3
params.Beta = 3 // Lower beta for faster finalization
// Test with different network sizes
testCases := []struct {
name string
numNodes int
numColors int
maxRounds int
}{
{"Small Network", 5, 3, 500},
{"Medium Network", 10, 5, 1000},
{"Large Network", 20, 5, 2000}, // Reduced blocks for large network
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Create network with deterministic seed for reproducibility
network := NewSimulationNetwork(params, tc.numColors, 12345)
// Add nodes to the network
for i := 0; i < tc.numNodes; i++ {
node := NewChainConsensus()
require.NoError(network.AddNode(t, node))
}
// Run rounds until finalized or max rounds reached
rounds := 0
for !network.Finalized() && rounds < tc.maxRounds {
require.NoError(network.Round())
rounds++
// Debug log every 100 rounds
if rounds%100 == 0 {
t.Logf("Round %d: %d nodes still running", rounds, len(network.running))
}
}
// Verify the network reached agreement
require.True(network.Finalized(), "Network should be finalized after %d rounds", rounds)
require.True(network.Agreement(), "All nodes should agree on the same block")
t.Logf("Network reached agreement in %d rounds", rounds)
})
}
}
// TestNetworkPartition tests network behavior under partition
func TestNetworkPartition(t *testing.T) {
require := require.New(t)
params := DefaultParameters()
params.K = 5
params.AlphaPreference = 3
params.AlphaConfidence = 3
params.Beta = 3 // Lower beta for faster recovery after partition
// Create network
network := NewSimulationNetwork(params, 3, 54321) // Fewer blocks for simpler test
// Add nodes
for i := 0; i < 10; i++ {
node := NewChainConsensus()
require.NoError(network.AddNode(t, node))
}
// Do NOT let nodes finalize before partition - just run a few rounds
for i := 0; i < 10; i++ {
require.NoError(network.Round())
}
// Now partition: only first half of nodes are active
originalRunning := make([]Consensus, len(network.running))
copy(originalRunning, network.running)
network.running = network.running[:len(network.running)/2]
// Run some rounds with partition (but don't let them finalize)
partitionRounds := 20
for i := 0; i < partitionRounds; i++ {
require.NoError(network.Round())
// Stop if partition is starting to finalize
if len(network.running) < len(originalRunning)/2 {
break
}
}
// Restore full network BEFORE nodes finalize different blocks
network.running = originalRunning
// Now continue running until all nodes finalize together
rounds := 0
maxRounds := 1000
for !network.Finalized() && rounds < maxRounds {
require.NoError(network.Round())
rounds++
}
// Network should finalize
require.True(network.Finalized(), "Network should finalize after partition heals")
// After partition heals and network continues, nodes should converge
// Note: If nodes finalized different blocks during partition, they won't agree
// This test ensures partition doesn't break consensus when healed early
agreed := network.Agreement()
t.Logf("Network recovered from partition in %d rounds, agreement: %v", rounds, agreed)
// For this test to be meaningful, we expect agreement since we healed before divergence
require.True(agreed, "Nodes should agree when partition heals before finalization")
}
// TestNetworkDivergence tests handling of divergent chains
func TestNetworkDivergence(t *testing.T) {
require := require.New(t)
params := DefaultParameters()
params.K = 7
params.AlphaPreference = 5
params.AlphaConfidence = 5
params.Beta = 5
// Create network with conflicting blocks
network := &SimulationNetwork{
params: params,
rngSource: 99999,
nodes: make([]Consensus, 0),
running: make([]Consensus, 0),
}
// Create genesis
genesisID := ids.GenerateTestID()
genesis := &TestBlock{
Decidable: consensustest.Decidable{
IDV: genesisID,
StatusV: choices.Accepted,
},
ParentV: ids.Empty,
HeightV: 0,
TimestampV: 0,
}
network.colors = []*TestBlock{genesis}
// Add conflicting blocks at same height
for i := 0; i < 3; i++ {
block := &TestBlock{
Decidable: consensustest.Decidable{
IDV: ids.GenerateTestID(),
StatusV: choices.Processing,
},
ParentV: genesisID,
HeightV: 1, // Same height = conflicting
TimestampV: int64(i + 1),
}
network.colors = append(network.colors, block)
}
// Add nodes
for i := 0; i < 15; i++ {
node := NewChainConsensus()
require.NoError(network.AddNode(t, node))
}
// Run consensus
rounds := 0
maxRounds := 2000
for !network.Finalized() && rounds < maxRounds {
require.NoError(network.Round())
rounds++
}
// Despite conflicts, network should reach agreement
require.True(network.Finalized(), "Network should finalize with conflicts")
require.True(network.Agreement(), "All nodes should agree on one chain")
t.Logf("Network resolved conflicts in %d rounds", rounds)
}
// TestNetworkScalability tests network with many nodes
func TestNetworkScalability(t *testing.T) {
require := require.New(t)
params := DefaultParameters()
// Adjust for better scalability
params.K = 10 // Smaller sample size for large network
params.AlphaPreference = 6 // Lower threshold for large network
params.AlphaConfidence = 6 // Lower threshold for large network
params.Beta = 5 // Lower beta for faster convergence
// Create large network
network := NewSimulationNetwork(params, 5, 777777) // Even fewer blocks for faster convergence
// Add many nodes
numNodes := 50
for i := 0; i < numNodes; i++ {
node := NewChainConsensus()
require.NoError(network.AddNode(t, node))
}
// Run consensus with timeout
rounds := 0
maxRounds := 5000
for !network.Finalized() && rounds < maxRounds {
require.NoError(network.Round())
rounds++
// Check for early agreement
if rounds%100 == 0 && network.Agreement() {
t.Logf("Early agreement detected at round %d", rounds)
}
}
// Large network should still reach consensus
require.True(network.Finalized(), "Large network should finalize")
require.True(network.Agreement(), "All nodes should agree")
t.Logf("Large network (%d nodes) reached agreement in %d rounds", numNodes, rounds)
}
// TestDAGConsensus tests DAG-based consensus
func TestDAGConsensus(t *testing.T) {
require := require.New(t)
// Create a simple DAG consensus test
params := DefaultParameters()
params.K = 5
params.AlphaPreference = 3
params.AlphaConfidence = 3
params.Beta = 3
// Use 5 blocks instead of 10 to reduce conflicts
network := NewSimulationNetwork(params, 5, 424242)
// Add nodes using chain consensus (DAG can be simulated with multiple parents)
for i := 0; i < 10; i++ {
node := NewChainConsensus()
require.NoError(network.AddNode(t, node))
}
// Run consensus
rounds := 0
maxRounds := 1000
for !network.Finalized() && rounds < maxRounds {
require.NoError(network.Round())
rounds++
}
require.True(network.Finalized(), "DAG network should finalize")
require.True(network.Agreement(), "DAG nodes should agree")
t.Logf("DAG consensus completed in %d rounds", rounds)
}
// TestNetworkResilience tests network resilience to failures
func TestNetworkResilience(t *testing.T) {
require := require.New(t)
params := DefaultParameters()
params.K = 5 // Smaller K for resilience with failures
params.AlphaPreference = 3 // Lower threshold for resilience
params.AlphaConfidence = 3 // Lower threshold for resilience
params.Beta = 3 // Lower beta for faster recovery
network := NewSimulationNetwork(params, 2, 161616) // Minimal blocks for simplicity
// Add nodes
numNodes := 20
for i := 0; i < numNodes; i++ {
node := NewChainConsensus()
require.NoError(network.AddNode(t, node))
}
// Simulate node failures by removing some from running set
// But also remove them from the nodes list so they don't vote
failureRate := 0.20 // 20% of nodes fail
numFailures := int(float64(len(network.nodes)) * failureRate)
// Mark some nodes as failed (remove from both running and nodes)
failedNodes := make(map[int]bool)
for i := 0; i < numFailures && len(network.running) > 10; i++ {
idx := int(network.nextRandom() % uint64(len(network.nodes)))
if !failedNodes[idx] {
failedNodes[idx] = true
}
}
// Remove failed nodes from running set
newRunning := make([]Consensus, 0)
newNodes := make([]Consensus, 0)
for i, node := range network.nodes {
if !failedNodes[i] {
newNodes = append(newNodes, node)
if i < len(network.running) {
newRunning = append(newRunning, node)
}
}
}
network.nodes = newNodes
network.running = newRunning
t.Logf("Simulated %d node failures out of %d nodes, %d nodes remaining",
len(failedNodes), numNodes, len(network.nodes))
// Run consensus with reduced network
rounds := 0
maxRounds := 2000
for !network.Finalized() && rounds < maxRounds {
require.NoError(network.Round())
rounds++
// Log progress
if rounds%100 == 0 {
t.Logf("Round %d: %d nodes still running", rounds, len(network.running))
}
}
// Network should still reach consensus despite failures
require.True(network.Finalized(), "Network should finalize despite failures")
// Check agreement among remaining nodes
require.True(network.Agreement(), "Remaining nodes should agree")
t.Logf("Network reached consensus despite %d failures in %d rounds", len(failedNodes), rounds)
}
// adaptiveConsensusParams calculates optimal consensus parameters based on network size and colors
// Based on Lux consensus parameters and network convergence theory:
// - K scales with network size (sample ~80% of network for robust convergence)
// - Alpha maintains supermajority threshold (~65-70% of K for Byzantine tolerance)
// - Beta scales logarithmically with colors (more rounds for more competing values)
func adaptiveConsensusParams(numNodes, numColors int) (k, alpha, beta, maxRounds int) {
// K: Sample size should be large fraction of network
// For small networks use all nodes, for large use ~80%
k = numNodes
if numNodes > 5 {
k = int(float64(numNodes) * 0.8)
if k > numNodes-1 {
k = numNodes - 1
}
}
// Ensure minimum k
if k < 3 {
k = 3
}
// Alpha: Supermajority threshold (65-70% of k)
// Byzantine tolerance requires > 2/3 agreement
alpha = int(float64(k) * 0.67)
if alpha < 2 {
alpha = 2
}
// Beta: Decision threshold scales with log(colors)
// More competing values require more confirmation rounds
beta = 3 + int(math.Log2(float64(numColors)))
if beta > 20 {
beta = 20 // Cap at reasonable maximum
}
// MaxRounds: Scales with network size and colors
// Larger networks and more colors need exponentially more rounds
maxRounds = 10000 * (1 + numColors/5) * (1 + numNodes/10)
if maxRounds > 200000 {
maxRounds = 200000 // Cap to prevent infinite loops
}
return k, alpha, beta, maxRounds
}
// BenchmarkNetworkConsensus benchmarks network consensus performance
// Uses adaptive parameters scaled to network size and number of competing values
func BenchmarkNetworkConsensus(b *testing.B) {
benchmarks := []struct {
name string
numNodes int
numColors int
}{
{"Small-5-3", 5, 3},
{"Medium-10-5", 10, 5},
{"Large-20-10", 20, 10},
{"XLarge-50-20", 50, 20},
}
for _, bm := range benchmarks {
b.Run(bm.name, func(b *testing.B) {
// Calculate adaptive parameters for this network configuration
k, alpha, beta, maxRounds := adaptiveConsensusParams(bm.numNodes, bm.numColors)
b.Logf("Network: %d nodes, %d colors | Params: K=%d, Alpha=%d, Beta=%d, MaxRounds=%d",
bm.numNodes, bm.numColors, k, alpha, beta, maxRounds)
b.ResetTimer()
for i := 0; i < b.N; i++ {
// Create network with adaptive parameters
params := DefaultParameters()
params.K = k
params.AlphaPreference = alpha
params.AlphaConfidence = alpha
params.Beta = beta
network := NewSimulationNetwork(params, bm.numColors, uint64(i*12345))
// Add nodes
for j := 0; j < bm.numNodes; j++ {
node := NewChainConsensus()
if err := network.AddNode(b, node); err != nil {
b.Fatal(err)
}
}
// Run until finalized
rounds := 0
for !network.Finalized() && rounds < maxRounds {
if err := network.Round(); err != nil {
b.Fatal(err)
}
rounds++
}
if !network.Finalized() {
b.Fatalf("Network did not finalize after %d rounds", maxRounds)
}
}
})
}
}