Files
consensus/engine/chain/comprehensive_test.go
zeekayandHanzo Dev e808906131 the corporate name is Lux Industries Inc
Lux Partners Limited is not the entity behind this work. Every place the
old name stood — copyright lines, site footers, brand config, legal and
compliance text — now names the company that actually holds it.

lux.fund is the exception and is not renamed to match: that site belongs
to Lux Assets Management LLC, a different company, and it says so.

Two kinds of line were left for a person rather than rewritten. A dated
news item recording that Lux Partners Limited was established in the Isle
of Man is a historical fact, and renaming it would falsify the record. And
a sentence asserting that the company is domiciled in the Isle of Man is a
claim about a jurisdiction, not a name — an Inc or an LLC is not an Isle
of Man company, so swapping the name there would assert something untrue.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-28 12:58:35 -07:00

1267 lines
33 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright (C) 2019-2025, Lux Industries Inc All rights reserved.
// See the file LICENSE for licensing terms.
package chain
import (
"context"
"errors"
"sync"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/luxfi/consensus/config"
"github.com/luxfi/consensus/core"
"github.com/luxfi/consensus/engine/chain/block"
"github.com/luxfi/ids"
)
// =============================================================================
// ChainConsensus Tests - AddBlock, ProcessVote, Poll, IsAccepted, IsRejected,
// Preference, GetBlock
// =============================================================================
// TestChainConsensusAddBlock tests the AddBlock method of ChainConsensus
func TestChainConsensusAddBlock(t *testing.T) {
require := require.New(t)
ctx := context.Background()
consensus := NewChainConsensus(3, 2, 2)
// Create a genesis-like block
genesisID := ids.GenerateTestID()
block := &Block{
id: genesisID,
parentID: ids.Empty,
height: 0,
timestamp: time.Now().UnixNano(),
data: []byte("genesis"),
}
// Add block should succeed
err := consensus.AddBlock(ctx, block)
require.NoError(err)
// Block should be in the blocks map
storedBlock, exists := consensus.GetBlock(genesisID)
require.True(exists)
require.Equal(genesisID, storedBlock.id)
// Block should be a tip
require.Contains(consensus.tips, genesisID)
// Lux consensus should be initialized
require.NotNil(storedBlock.driver)
}
// TestChainConsensusAddBlockDuplicate tests that adding duplicate blocks fails
func TestChainConsensusAddBlockDuplicate(t *testing.T) {
require := require.New(t)
ctx := context.Background()
consensus := NewChainConsensus(3, 2, 2)
blockID := ids.GenerateTestID()
block := &Block{
id: blockID,
parentID: ids.Empty,
height: 0,
timestamp: time.Now().UnixNano(),
}
// First add should succeed
err := consensus.AddBlock(ctx, block)
require.NoError(err)
// Second add should fail
err = consensus.AddBlock(ctx, block)
require.Error(err)
require.Contains(err.Error(), "block already exists")
}
// TestChainConsensusAddBlockUpdatesTips tests that adding child blocks updates tips
func TestChainConsensusAddBlockUpdatesTips(t *testing.T) {
require := require.New(t)
ctx := context.Background()
consensus := NewChainConsensus(3, 2, 2)
// Add genesis block
genesisID := ids.GenerateTestID()
genesis := &Block{
id: genesisID,
parentID: ids.Empty,
height: 0,
timestamp: time.Now().UnixNano(),
}
require.NoError(consensus.AddBlock(ctx, genesis))
require.Contains(consensus.tips, genesisID)
// Add child block
childID := ids.GenerateTestID()
child := &Block{
id: childID,
parentID: genesisID,
height: 1,
timestamp: time.Now().UnixNano(),
}
require.NoError(consensus.AddBlock(ctx, child))
// Genesis should no longer be a tip
require.NotContains(consensus.tips, genesisID)
// Child should be a tip
require.Contains(consensus.tips, childID)
}
// TestChainConsensusProcessVote tests the ProcessVote method
func TestChainConsensusProcessVote(t *testing.T) {
require := require.New(t)
ctx := context.Background()
consensus := NewChainConsensus(3, 2, 2)
// Add a block
blockID := ids.GenerateTestID()
block := &Block{
id: blockID,
parentID: ids.Empty,
height: 0,
timestamp: time.Now().UnixNano(),
}
require.NoError(consensus.AddBlock(ctx, block))
// Process accept vote
err := consensus.ProcessVote(ctx, blockID, ids.GenerateTestNodeID(), true)
require.NoError(err)
}
// TestChainConsensusProcessVoteUnknownBlock tests voting for unknown block
func TestChainConsensusProcessVoteUnknownBlock(t *testing.T) {
require := require.New(t)
ctx := context.Background()
consensus := NewChainConsensus(3, 2, 2)
// Process vote for non-existent block
unknownID := ids.GenerateTestID()
err := consensus.ProcessVote(ctx, unknownID, ids.GenerateTestNodeID(), true)
require.Error(err)
require.Contains(err.Error(), "block not found")
}
// TestChainConsensusProcessVoteNilConsensus tests voting when consensus not initialized
func TestChainConsensusProcessVoteNilConsensus(t *testing.T) {
require := require.New(t)
ctx := context.Background()
consensus := NewChainConsensus(3, 2, 2)
// Manually add block without initializing driver
blockID := ids.GenerateTestID()
block := &Block{
id: blockID,
parentID: ids.Empty,
height: 0,
driver: nil, // intentionally nil
}
consensus.blocks[blockID] = block
// Process vote should fail
err := consensus.ProcessVote(ctx, blockID, ids.GenerateTestNodeID(), true)
require.Error(err)
require.Contains(err.Error(), "block not initialized for consensus")
}
// TestChainConsensusPoll tests the Poll method
func TestChainConsensusPoll(t *testing.T) {
require := require.New(t)
ctx := context.Background()
consensus := NewChainConsensus(1, 1, 1)
// Add a block
blockID := ids.GenerateTestID()
block := &Block{
id: blockID,
parentID: ids.Empty,
height: 0,
timestamp: time.Now().UnixNano(),
}
require.NoError(consensus.AddBlock(ctx, block))
// Poll with votes
responses := map[ids.ID]int{
blockID: 5,
}
err := consensus.Poll(ctx, responses)
require.NoError(err)
}
// TestChainConsensusPollSkipsUnknownBlocks tests that Poll ignores unknown blocks
func TestChainConsensusPollSkipsUnknownBlocks(t *testing.T) {
require := require.New(t)
ctx := context.Background()
consensus := NewChainConsensus(3, 2, 2)
// Poll with votes for non-existent blocks
unknownID := ids.GenerateTestID()
responses := map[ids.ID]int{
unknownID: 5,
}
err := consensus.Poll(ctx, responses)
require.NoError(err) // Should not error, just skip
}
// TestChainConsensusPollFinalization tests block finalization through polling
func TestChainConsensusPollFinalization(t *testing.T) {
require := require.New(t)
ctx := context.Background()
// Beta=1 for quick finalization, K=1, Alpha=1 means 1 vote is enough
consensus := NewChainConsensus(1, 1, 1)
// Add a block
blockID := ids.GenerateTestID()
block := &Block{
id: blockID,
parentID: ids.Empty,
height: 0,
timestamp: time.Now().UnixNano(),
}
require.NoError(consensus.AddBlock(ctx, block))
// Initial state
require.False(consensus.IsAccepted(blockID))
// The α-count path (ProcessVote/Poll) is LIVENESS only: it marks the block worth a
// finalize ATTEMPT (IsAccepted) but does NOT advance finalized history — finality is
// committed by FinalizeBranch (the cert path). Decomplected: count ≠ finalize.
for i := 0; i < 10; i++ {
require.NoError(consensus.ProcessVote(ctx, blockID, ids.GenerateTestNodeID(), true))
}
responses := map[ids.ID]int{blockID: 10}
for i := 0; i < 5; i++ {
require.NoError(consensus.Poll(ctx, responses))
}
// Liveness reached, but finalized history is still empty (a cert is required).
require.True(consensus.IsAccepted(blockID))
_, set := consensus.GetFinalizedHeight()
require.False(set, "the count path must not advance finalized history")
// FinalizeBranch (the cert path) commits finality and advances the tip.
_, err := consensus.FinalizeBranch(blockID, 0, ids.Empty)
require.NoError(err)
require.Equal(blockID, consensus.GetFinalizedTip())
}
// TestChainConsensusIsAccepted tests the IsAccepted method
func TestChainConsensusIsAccepted(t *testing.T) {
require := require.New(t)
consensus := NewChainConsensus(3, 2, 2)
// Unknown block should not be accepted
unknownID := ids.GenerateTestID()
require.False(consensus.IsAccepted(unknownID))
// Add and mark block as accepted
blockID := ids.GenerateTestID()
block := &Block{
id: blockID,
accepted: true,
}
consensus.blocks[blockID] = block
require.True(consensus.IsAccepted(blockID))
}
// TestChainConsensusIsRejected tests the IsRejected method
func TestChainConsensusIsRejected(t *testing.T) {
require := require.New(t)
consensus := NewChainConsensus(3, 2, 2)
// Unknown block should not be rejected
unknownID := ids.GenerateTestID()
require.False(consensus.IsRejected(unknownID))
// Add and mark block as rejected
blockID := ids.GenerateTestID()
block := &Block{
id: blockID,
rejected: true,
}
consensus.blocks[blockID] = block
require.True(consensus.IsRejected(blockID))
}
// TestChainConsensusPreference tests the Preference method
func TestChainConsensusPreference(t *testing.T) {
require := require.New(t)
ctx := context.Background()
consensus := NewChainConsensus(3, 2, 2)
// Empty consensus should return Empty preference
require.Equal(ids.Empty, consensus.Preference())
// Add a block as tip
blockID := ids.GenerateTestID()
block := &Block{
id: blockID,
parentID: ids.Empty,
height: 0,
timestamp: time.Now().UnixNano(),
}
require.NoError(consensus.AddBlock(ctx, block))
// Should return the tip
pref := consensus.Preference()
require.Equal(blockID, pref)
// Finalizing the block makes it the finalized tip; Preference surfaces it.
_, err := consensus.FinalizeBranch(blockID, 0, ids.Empty)
require.NoError(err)
require.Equal(blockID, consensus.Preference())
}
// TestChainConsensusPreferenceFinalizedTip tests that finalized tip takes precedence
func TestChainConsensusPreferenceFinalizedTip(t *testing.T) {
require := require.New(t)
ctx := context.Background()
consensus := NewChainConsensus(3, 2, 2)
// Add multiple tips
block1ID := ids.GenerateTestID()
block1 := &Block{
id: block1ID,
parentID: ids.Empty,
height: 0,
timestamp: time.Now().UnixNano(),
}
require.NoError(consensus.AddBlock(ctx, block1))
block2ID := ids.GenerateTestID()
block2 := &Block{
id: block2ID,
parentID: ids.Empty,
height: 0,
timestamp: time.Now().UnixNano(),
}
require.NoError(consensus.AddBlock(ctx, block2))
// Finalizing block1 makes it the finalized tip; Preference surfaces it.
_, err := consensus.FinalizeBranch(block1ID, 0, ids.Empty)
require.NoError(err)
// Preference should be the finalized tip
require.Equal(block1ID, consensus.Preference())
}
// TestChainConsensusGetBlock tests the GetBlock method
func TestChainConsensusGetBlock(t *testing.T) {
require := require.New(t)
ctx := context.Background()
consensus := NewChainConsensus(3, 2, 2)
// Non-existent block
unknownID := ids.GenerateTestID()
block, exists := consensus.GetBlock(unknownID)
require.False(exists)
require.Nil(block)
// Add a block
blockID := ids.GenerateTestID()
expectedBlock := &Block{
id: blockID,
parentID: ids.Empty,
height: 0,
timestamp: time.Now().UnixNano(),
}
require.NoError(consensus.AddBlock(ctx, expectedBlock))
// Get the block
block, exists = consensus.GetBlock(blockID)
require.True(exists)
require.NotNil(block)
require.Equal(blockID, block.id)
}
// TestChainConsensusStats tests the Stats method with various states
func TestChainConsensusStats(t *testing.T) {
require := require.New(t)
consensus := NewChainConsensus(3, 2, 2)
// Add blocks with different states
acceptedID := ids.GenerateTestID()
consensus.blocks[acceptedID] = &Block{id: acceptedID, accepted: true}
rejectedID := ids.GenerateTestID()
consensus.blocks[rejectedID] = &Block{id: rejectedID, rejected: true}
pendingID := ids.GenerateTestID()
consensus.blocks[pendingID] = &Block{id: pendingID}
consensus.tips[pendingID] = true
_, err := consensus.FinalizeBranch(acceptedID, 0, ids.Empty)
require.NoError(err)
stats := consensus.Stats()
require.Equal(3, stats["total_blocks"])
require.Equal(1, stats["accepted"])
require.Equal(1, stats["rejected"])
require.Equal(1, stats["pending"])
require.Equal(1, stats["tips"])
require.Equal(acceptedID.String(), stats["finalized_tip"])
}
// =============================================================================
// Transitive Engine Tests - AddBlock, ProcessVote, Poll, IsAccepted, Preference,
// SetVM, Notify, buildBlocksLocked, PendingBuildBlocks
// =============================================================================
// TestTransitiveAddBlock tests the Transitive AddBlock method
func TestTransitiveAddBlock(t *testing.T) {
require := require.New(t)
ctx := context.Background()
engine := newTestEngine()
require.NoError(engine.Start(ctx, true))
defer engine.Stop(ctx)
// Create a block
blockID := ids.GenerateTestID()
block := &Block{
id: blockID,
parentID: ids.Empty,
height: 0,
timestamp: time.Now().UnixNano(),
}
// Add block
err := engine.AddBlock(ctx, block)
require.NoError(err)
}
// TestTransitiveProcessVote tests the Transitive ProcessVote method
func TestTransitiveProcessVote(t *testing.T) {
require := require.New(t)
ctx := context.Background()
engine := newTestEngine()
require.NoError(engine.Start(ctx, true))
defer engine.Stop(ctx)
// Add a block first
blockID := ids.GenerateTestID()
block := &Block{
id: blockID,
parentID: ids.Empty,
height: 0,
timestamp: time.Now().UnixNano(),
}
require.NoError(engine.AddBlock(ctx, block))
// Process vote
err := engine.ProcessVote(ctx, blockID, ids.GenerateTestNodeID(), true)
require.NoError(err)
}
// TestTransitivePoll tests the Transitive Poll method
func TestTransitivePoll(t *testing.T) {
require := require.New(t)
ctx := context.Background()
engine := newTestEngine()
require.NoError(engine.Start(ctx, true))
defer engine.Stop(ctx)
// Add a block first
blockID := ids.GenerateTestID()
block := &Block{
id: blockID,
parentID: ids.Empty,
height: 0,
timestamp: time.Now().UnixNano(),
}
require.NoError(engine.AddBlock(ctx, block))
// Poll with responses
responses := map[ids.ID]int{
blockID: 5,
}
err := engine.Poll(ctx, responses)
require.NoError(err)
}
// TestTransitiveIsAccepted tests the Transitive IsAccepted method
func TestTransitiveIsAccepted(t *testing.T) {
require := require.New(t)
ctx := context.Background()
engine := newTestEngine()
require.NoError(engine.Start(ctx, true))
defer engine.Stop(ctx)
// Unknown block should not be accepted
unknownID := ids.GenerateTestID()
require.False(engine.IsAccepted(unknownID))
}
// TestTransitivePreference tests the Transitive Preference method
func TestTransitivePreference(t *testing.T) {
require := require.New(t)
ctx := context.Background()
engine := newTestEngine()
require.NoError(engine.Start(ctx, true))
defer engine.Stop(ctx)
// Initially should return empty preference
pref := engine.Preference()
require.Equal(ids.Empty, pref)
}
// TestTransitiveSetVM tests the SetVM method
func TestTransitiveSetVM(t *testing.T) {
require := require.New(t)
engine := newTestEngine()
// Initially no VM
require.Nil(engine.vm)
// Create a mock VM
mockVM := &mockBlockBuilder{}
// Set VM
engine.SetVM(mockVM)
// VM should be set
require.NotNil(engine.vm)
require.Equal(mockVM, engine.vm)
}
// TestTransitiveNotifyPendingTxs tests Notify with PendingTxs message
func TestTransitiveNotifyPendingTxs(t *testing.T) {
require := require.New(t)
ctx := context.Background()
engine := newTestEngine()
require.NoError(engine.Start(ctx, true))
defer engine.Stop(ctx)
// Initially no pending builds
require.Zero(engine.PendingBuildBlocks())
// Send PendingTxs notification without VM
msg := Message{Type: PendingTxs}
err := engine.Notify(ctx, msg)
require.NoError(err)
// Without VM, pending builds accumulate (buildBlocksLocked returns early)
require.Equal(1, engine.PendingBuildBlocks())
}
// TestTransitiveNotifyPendingTxsWithVM tests Notify with PendingTxs and a VM set
func TestTransitiveNotifyPendingTxsWithVM(t *testing.T) {
require := require.New(t)
ctx := context.Background()
engine := newTestEngine()
require.NoError(engine.Start(ctx, true))
defer engine.Stop(ctx)
// Set a mock VM that succeeds
mockVM := &mockBlockBuilder{
buildFunc: func(ctx context.Context) (block.Block, error) {
return &testBlock{id: ids.GenerateTestID(), height: 1}, nil
},
}
engine.SetVM(mockVM)
// Send PendingTxs notification
msg := Message{Type: PendingTxs}
err := engine.Notify(ctx, msg)
require.NoError(err)
// Pending builds should be consumed
require.Zero(engine.PendingBuildBlocks())
}
// TestTransitiveNotifyPendingTxsWithVMError tests Notify when VM returns error
func TestTransitiveNotifyPendingTxsWithVMError(t *testing.T) {
require := require.New(t)
ctx := context.Background()
engine := newTestEngine()
require.NoError(engine.Start(ctx, true))
defer engine.Stop(ctx)
// Set a mock VM that returns error
mockVM := &mockBlockBuilder{
buildFunc: func(ctx context.Context) (block.Block, error) {
return nil, errors.New("no transactions")
},
}
engine.SetVM(mockVM)
// Send PendingTxs notification
msg := Message{Type: PendingTxs}
err := engine.Notify(ctx, msg)
require.NoError(err) // Errors are swallowed, not fatal
// The failed build CONSUMES its demand (no spin): a non-leader's BuildBlock
// fails every slot until its window opens, so holding the demand was an
// unbounded hot loop. Liveness comes from fresh triggers, not held demand.
require.Equal(0, engine.PendingBuildBlocks())
}
// TestTransitiveNotifyStateSyncDone tests Notify with StateSyncDone message
func TestTransitiveNotifyStateSyncDone(t *testing.T) {
require := require.New(t)
ctx := context.Background()
engine := newTestEngine()
require.NoError(engine.Start(ctx, true))
defer engine.Stop(ctx)
// Send StateSyncDone notification
msg := Message{Type: StateSyncDone}
err := engine.Notify(ctx, msg)
require.NoError(err)
}
// TestTransitiveNotifyUnknownType tests Notify with unknown message type
func TestTransitiveNotifyUnknownType(t *testing.T) {
require := require.New(t)
ctx := context.Background()
engine := newTestEngine()
require.NoError(engine.Start(ctx, true))
defer engine.Stop(ctx)
// Send unknown notification type
msg := Message{Type: 999}
err := engine.Notify(ctx, msg)
require.NoError(err) // Unknown types are ignored
}
// TestTransitivePendingBuildBlocks tests the PendingBuildBlocks getter
func TestTransitivePendingBuildBlocks(t *testing.T) {
require := require.New(t)
engine := newTestEngine()
// Initially zero
require.Zero(engine.PendingBuildBlocks())
// Manually set pending
engine.mu.Lock()
engine.pendingBuildBlocks = 5
engine.mu.Unlock()
require.Equal(5, engine.PendingBuildBlocks())
}
// TestTransitiveBuildBlocksLockedNilVM tests buildBlocksLocked with nil VM
func TestTransitiveBuildBlocksLockedNilVM(t *testing.T) {
require := require.New(t)
ctx := context.Background()
engine := newTestEngine()
// Set pending builds
engine.pendingBuildBlocks = 3
// buildBlocksLocked expects caller to hold mu.Lock
engine.mu.Lock()
err := engine.buildBlocksLocked(ctx)
engine.mu.Unlock()
require.NoError(err)
// Pending builds should remain unchanged
require.Equal(3, engine.pendingBuildBlocks)
}
// TestTransitiveBuildBlocksLockedMultiple tests building multiple blocks
func TestTransitiveBuildBlocksLockedMultiple(t *testing.T) {
require := require.New(t)
ctx := context.Background()
engine := newTestEngine()
// Track build calls
buildCount := 0
mockVM := &mockBlockBuilder{
buildFunc: func(ctx context.Context) (block.Block, error) {
buildCount++
return &testBlock{id: ids.GenerateTestID(), height: uint64(buildCount)}, nil
},
}
engine.vm = mockVM
engine.pendingBuildBlocks = 3
// buildBlocksLocked expects caller to hold mu.Lock
engine.mu.Lock()
err := engine.buildBlocksLocked(ctx)
engine.mu.Unlock()
require.NoError(err)
require.Equal(3, buildCount)
require.Zero(engine.pendingBuildBlocks)
}
// TestTransitiveBuildBlocksLockedPartialFailure tests partial build failures
func TestTransitiveBuildBlocksLockedPartialFailure(t *testing.T) {
require := require.New(t)
ctx := context.Background()
engine := newTestEngine()
// Fail after first build
buildCount := 0
mockVM := &mockBlockBuilder{
buildFunc: func(ctx context.Context) (block.Block, error) {
buildCount++
if buildCount > 1 {
return nil, errors.New("no more transactions")
}
return &testBlock{id: ids.GenerateTestID(), height: uint64(buildCount)}, nil
},
}
engine.vm = mockVM
engine.pendingBuildBlocks = 3
// buildBlocksLocked expects caller to hold mu.Lock
engine.mu.Lock()
err := engine.buildBlocksLocked(ctx)
engine.mu.Unlock()
require.NoError(err)
require.Equal(2, buildCount) // Called twice (success, then fail)
}
// =============================================================================
// NewWithParams Tests
// =============================================================================
// TestNewWithParams tests creating engine with custom parameters
func TestNewWithParams(t *testing.T) {
require := require.New(t)
params := config.Parameters{
K: 5,
AlphaPreference: 4,
AlphaConfidence: 4,
Beta: 10,
}
engine := newTestEngineParams(params)
require.NotNil(engine)
require.NotNil(engine.consensus)
require.Equal(params.K, engine.params.K)
require.Equal(params.AlphaPreference, engine.params.AlphaPreference)
require.Equal(params.Beta, engine.params.Beta)
require.False(engine.IsBootstrapped())
}
// =============================================================================
// Concurrent Access Tests
// =============================================================================
// TestChainConsensusConcurrentAddBlock tests concurrent block additions
func TestChainConsensusConcurrentAddBlock(t *testing.T) {
require := require.New(t)
ctx := context.Background()
consensus := NewChainConsensus(3, 2, 2)
// Add blocks concurrently
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func(idx int) {
defer wg.Done()
blockID := ids.GenerateTestID()
block := &Block{
id: blockID,
parentID: ids.Empty,
height: uint64(idx),
timestamp: time.Now().UnixNano(),
}
_ = consensus.AddBlock(ctx, block)
}(i)
}
wg.Wait()
// Should have added blocks (some may fail due to duplicates)
stats := consensus.Stats()
require.Greater(stats["total_blocks"].(int), 0)
}
// TestChainConsensusConcurrentPoll tests concurrent polling
func TestChainConsensusConcurrentPoll(t *testing.T) {
require := require.New(t)
ctx := context.Background()
consensus := NewChainConsensus(3, 2, 2)
// Add a block
blockID := ids.GenerateTestID()
block := &Block{
id: blockID,
parentID: ids.Empty,
height: 0,
timestamp: time.Now().UnixNano(),
}
require.NoError(consensus.AddBlock(ctx, block))
// Poll concurrently
var wg sync.WaitGroup
for i := 0; i < 50; i++ {
wg.Add(1)
go func() {
defer wg.Done()
responses := map[ids.ID]int{blockID: 5}
_ = consensus.Poll(ctx, responses)
}()
}
wg.Wait()
// Should not panic or deadlock
}
// TestChainConsensusConcurrentStats tests concurrent Stats access
func TestChainConsensusConcurrentStats(t *testing.T) {
consensus := NewChainConsensus(3, 2, 2)
// Add some blocks
for i := 0; i < 10; i++ {
blockID := ids.GenerateTestID()
consensus.blocks[blockID] = &Block{
id: blockID,
accepted: i%3 == 0,
rejected: i%3 == 1,
}
}
// Access Stats concurrently
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
_ = consensus.Stats()
}()
}
wg.Wait()
}
// TestTransitiveConcurrentNotify tests concurrent Notify calls
func TestTransitiveConcurrentNotify(t *testing.T) {
require := require.New(t)
ctx := context.Background()
engine := newTestEngine()
require.NoError(engine.Start(ctx, true))
defer engine.Stop(ctx)
// Set a simple VM
engine.SetVM(&mockBlockBuilder{
buildFunc: func(ctx context.Context) (block.Block, error) {
return &testBlock{id: ids.GenerateTestID(), height: 1}, nil
},
})
// Send notifications concurrently
var wg sync.WaitGroup
for i := 0; i < 50; i++ {
wg.Add(1)
go func() {
defer wg.Done()
msg := Message{Type: PendingTxs}
_ = engine.Notify(ctx, msg)
}()
}
wg.Wait()
}
// =============================================================================
// Edge Cases
// =============================================================================
// TestChainConsensusEmptyPoll tests polling with empty responses
func TestChainConsensusEmptyPoll(t *testing.T) {
require := require.New(t)
ctx := context.Background()
consensus := NewChainConsensus(3, 2, 2)
// Poll with empty responses
responses := map[ids.ID]int{}
err := consensus.Poll(ctx, responses)
require.NoError(err)
}
// TestChainConsensusPollNilConsensus tests polling block with nil consensus
func TestChainConsensusPollNilConsensus(t *testing.T) {
require := require.New(t)
ctx := context.Background()
consensus := NewChainConsensus(3, 2, 2)
// Add block without consensus
blockID := ids.GenerateTestID()
block := &Block{
id: blockID,
driver: nil,
}
consensus.blocks[blockID] = block
// Poll should handle nil consensus gracefully
responses := map[ids.ID]int{blockID: 5}
err := consensus.Poll(ctx, responses)
require.NoError(err)
}
// TestTransitiveStartStop tests multiple start/stop cycles
func TestTransitiveStartStop(t *testing.T) {
require := require.New(t)
ctx := context.Background()
engine := newTestEngine()
// Multiple start/stop cycles
for i := 0; i < 5; i++ {
err := engine.Start(ctx, true)
require.NoError(err)
require.True(engine.IsBootstrapped())
err = engine.Stop(ctx)
require.NoError(err)
require.False(engine.IsBootstrapped())
}
}
// TestTransitiveStopWithoutStart tests stopping engine that was never started
func TestTransitiveStopWithoutStart(t *testing.T) {
require := require.New(t)
ctx := context.Background()
engine := newTestEngine()
// Stop should be safe even without start
err := engine.Stop(ctx)
require.NoError(err)
}
// TestTransitiveHealthCheckBeforeStart tests health check before start
func TestTransitiveHealthCheckBeforeStart(t *testing.T) {
require := require.New(t)
ctx := context.Background()
engine := newTestEngine()
// Health check should work even before start
health, err := engine.HealthCheck(ctx)
require.NoError(err)
require.NotNil(health)
m, ok := health.(map[string]interface{})
require.True(ok)
require.False(m["bootstrapped"].(bool))
}
// TestTransitiveHealthCheckAfterStart tests health check after start
func TestTransitiveHealthCheckAfterStart(t *testing.T) {
require := require.New(t)
ctx := context.Background()
engine := newTestEngine()
require.NoError(engine.Start(ctx, true))
defer engine.Stop(ctx)
health, err := engine.HealthCheck(ctx)
require.NoError(err)
m, ok := health.(map[string]interface{})
require.True(ok)
require.True(m["bootstrapped"].(bool))
}
// TestChainConsensusPreferenceEmptyTips tests Preference with no tips
func TestChainConsensusPreferenceEmptyTips(t *testing.T) {
require := require.New(t)
consensus := NewChainConsensus(3, 2, 2)
// Clear tips; a fresh consensus has no finalized ledger, so Preference is Empty.
consensus.tips = make(map[ids.ID]bool)
// Should return Empty
require.Equal(ids.Empty, consensus.Preference())
}
// TestChainConsensusBlockWithEmptyParent tests adding block with Empty parent
func TestChainConsensusBlockWithEmptyParent(t *testing.T) {
require := require.New(t)
ctx := context.Background()
consensus := NewChainConsensus(3, 2, 2)
// Block with Empty parent
blockID := ids.GenerateTestID()
block := &Block{
id: blockID,
parentID: ids.Empty,
height: 0,
}
err := consensus.AddBlock(ctx, block)
require.NoError(err)
// Should be a tip
require.Contains(consensus.tips, blockID)
}
// =============================================================================
// Mock Types
// =============================================================================
// testBlock implements block.Block for comprehensive testing
type testBlock struct {
id ids.ID
parentID ids.ID
height uint64
timestamp time.Time
status uint8
bytes []byte
}
func (b *testBlock) ID() ids.ID { return b.id }
func (b *testBlock) Parent() ids.ID { return b.parentID }
func (b *testBlock) ParentID() ids.ID { return b.parentID }
func (b *testBlock) Height() uint64 { return b.height }
func (b *testBlock) Timestamp() time.Time { return b.timestamp }
func (b *testBlock) Status() uint8 { return b.status }
func (b *testBlock) Verify(context.Context) error { return nil }
func (b *testBlock) Accept(context.Context) error { return nil }
func (b *testBlock) Reject(context.Context) error { return nil }
func (b *testBlock) Bytes() []byte { return b.bytes }
// mockBlockBuilder implements BlockBuilder for testing
type mockBlockBuilder struct {
buildFunc func(ctx context.Context) (block.Block, error)
blocks map[ids.ID]block.Block
lastAcceptedID ids.ID
}
func (m *mockBlockBuilder) BuildBlock(ctx context.Context) (block.Block, error) {
if m.buildFunc != nil {
return m.buildFunc(ctx)
}
return nil, nil
}
func (m *mockBlockBuilder) GetBlock(ctx context.Context, id ids.ID) (block.Block, error) {
if m.blocks != nil {
if blk, ok := m.blocks[id]; ok {
return blk, nil
}
}
return nil, errors.New("block not found")
}
func (m *mockBlockBuilder) ParseBlock(ctx context.Context, bytes []byte) (block.Block, error) {
return &testBlock{bytes: bytes}, nil
}
func (m *mockBlockBuilder) LastAccepted(ctx context.Context) (ids.ID, error) {
return m.lastAcceptedID, nil
}
func (m *mockBlockBuilder) SetPreference(ctx context.Context, id ids.ID) error {
// Mock implementation - just record the preferred block
m.lastAcceptedID = id
return nil
}
// =============================================================================
// Integration Tests
// =============================================================================
// TestFullConsensusFlow tests a complete consensus flow from block addition to finalization
func TestFullConsensusFlow(t *testing.T) {
require := require.New(t)
ctx := context.Background()
// Create engine with minimal params for quick finalization
params := config.Parameters{
K: 1,
AlphaPreference: 1,
AlphaConfidence: 1,
Beta: 1,
}
engine := newTestEngineParams(params)
require.NoError(engine.Start(ctx, true))
defer engine.Stop(ctx)
// Add genesis block
genesisID := ids.GenerateTestID()
genesis := &Block{
id: genesisID,
parentID: ids.Empty,
height: 0,
timestamp: time.Now().UnixNano(),
}
require.NoError(engine.AddBlock(ctx, genesis))
// Add child block
childID := ids.GenerateTestID()
child := &Block{
id: childID,
parentID: genesisID,
height: 1,
timestamp: time.Now().UnixNano(),
}
require.NoError(engine.AddBlock(ctx, child))
// Poll to build consensus
responses := map[ids.ID]int{
childID: 5,
}
for i := 0; i < 5; i++ {
err := engine.Poll(ctx, responses)
require.NoError(err)
}
// Health check
health, err := engine.HealthCheck(ctx)
require.NoError(err)
require.NotNil(health)
// Stop engine
require.NoError(engine.Stop(ctx))
require.False(engine.IsBootstrapped())
}
// TestConsensusWithMultipleBranches tests consensus with competing branches
func TestConsensusWithMultipleBranches(t *testing.T) {
require := require.New(t)
ctx := context.Background()
consensus := NewChainConsensus(3, 2, 2)
// Create genesis
genesisID := ids.GenerateTestID()
genesis := &Block{
id: genesisID,
parentID: ids.Empty,
height: 0,
timestamp: time.Now().UnixNano(),
}
require.NoError(consensus.AddBlock(ctx, genesis))
// Create two competing branches from genesis
branch1ID := ids.GenerateTestID()
branch1 := &Block{
id: branch1ID,
parentID: genesisID,
height: 1,
timestamp: time.Now().UnixNano(),
}
require.NoError(consensus.AddBlock(ctx, branch1))
branch2ID := ids.GenerateTestID()
branch2 := &Block{
id: branch2ID,
parentID: genesisID,
height: 1,
timestamp: time.Now().UnixNano(),
}
require.NoError(consensus.AddBlock(ctx, branch2))
// Both branches should be tips (since genesis had its tip status removed)
require.Contains(consensus.tips, branch1ID)
require.Contains(consensus.tips, branch2ID)
// Genesis should no longer be a tip
require.NotContains(consensus.tips, genesisID)
// Stats should show all blocks
stats := consensus.Stats()
require.Equal(3, stats["total_blocks"].(int))
require.Equal(2, stats["tips"].(int))
}
// TestNotifyMultiplePendingTxs tests multiple PendingTxs notifications
func TestNotifyMultiplePendingTxs(t *testing.T) {
require := require.New(t)
ctx := context.Background()
engine := newTestEngine()
require.NoError(engine.Start(ctx, true))
defer engine.Stop(ctx)
buildCount := 0
mockVM := &mockBlockBuilder{
buildFunc: func(ctx context.Context) (block.Block, error) {
buildCount++
return &testBlock{id: ids.GenerateTestID(), height: uint64(buildCount)}, nil
},
}
engine.SetVM(mockVM)
// Send multiple PendingTxs notifications
for i := 0; i < 5; i++ {
msg := Message{Type: PendingTxs}
err := engine.Notify(ctx, msg)
require.NoError(err)
}
// Should have built 5 blocks
require.Equal(5, buildCount)
}
// TestMessageTypeConstants verifies message type re-exports
func TestMessageTypeConstants(t *testing.T) {
require := require.New(t)
// Verify message types match core definitions
require.Equal(core.PendingTxs, PendingTxs)
require.Equal(core.StateSyncDone, StateSyncDone)
}
// TestProcessVoteRejectPath tests ProcessVote with reject=false
func TestProcessVoteRejectPath(t *testing.T) {
require := require.New(t)
ctx := context.Background()
consensus := NewChainConsensus(3, 2, 2)
// Add a block
blockID := ids.GenerateTestID()
block := &Block{
id: blockID,
parentID: ids.Empty,
height: 0,
timestamp: time.Now().UnixNano(),
}
require.NoError(consensus.AddBlock(ctx, block))
// Process reject vote (accept=false)
err := consensus.ProcessVote(ctx, blockID, ids.GenerateTestNodeID(), false)
require.NoError(err)
// Should not record a vote when accept=false
}