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