mirror of
https://github.com/luxfi/consensus.git
synced 2026-09-24 00:26:58 +00:00
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.
1061 lines
28 KiB
Go
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)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|