Files
2026-08-18 22:35:43 -07:00

440 lines
9.9 KiB
Go

// Copyright (C) 2019-2025, Lux Industries Inc All rights reserved.
// See the file LICENSE for licensing terms.
package engine
import (
"context"
"testing"
"time"
"github.com/luxfi/consensus/types"
"github.com/luxfi/ids"
"github.com/stretchr/testify/require"
)
// TestNewChain tests the NewChain constructor
func TestNewChain(t *testing.T) {
require := require.New(t)
config := types.Config{
Alpha: 15,
}
chain := NewChain(config)
require.NotNil(chain)
require.NotNil(chain.blocks)
require.NotNil(chain.votes)
require.NotNil(chain.status)
require.Equal(types.GenesisID, chain.lastAccepted)
require.Equal(config, chain.config)
}
// TestChainAdd tests adding blocks to the chain
func TestChainAdd(t *testing.T) {
require := require.New(t)
config := types.Config{Alpha: 15}
chain := NewChain(config)
block := &types.Block{
ID: ids.GenerateTestID(),
ParentID: types.GenesisID,
Height: 1,
Time: time.Now(),
}
err := chain.Add(context.Background(), block)
require.NoError(err)
// Verify block was stored
chain.mu.RLock()
stored, exists := chain.blocks[block.ID]
status := chain.status[block.ID]
votes := chain.votes[block.ID]
chain.mu.RUnlock()
require.True(exists)
require.Equal(block, stored)
require.Equal(types.StatusProcessing, status)
require.NotNil(votes)
require.Len(votes, 0)
}
// TestChainAddMultipleBlocks tests adding multiple blocks
func TestChainAddMultipleBlocks(t *testing.T) {
require := require.New(t)
config := types.Config{Alpha: 3}
chain := NewChain(config)
// Add first block
block1 := &types.Block{
ID: ids.GenerateTestID(),
ParentID: types.GenesisID,
Height: 1,
Time: time.Now(),
}
err := chain.Add(context.Background(), block1)
require.NoError(err)
// Add second block
block2 := &types.Block{
ID: ids.GenerateTestID(),
ParentID: block1.ID,
Height: 2,
Time: time.Now(),
}
err = chain.Add(context.Background(), block2)
require.NoError(err)
require.Equal(2, len(chain.blocks))
}
// TestChainRecordVote tests recording votes
func TestChainRecordVote(t *testing.T) {
require := require.New(t)
config := types.Config{Alpha: 2}
chain := NewChain(config)
block := &types.Block{
ID: ids.GenerateTestID(),
ParentID: types.GenesisID,
Height: 1,
Time: time.Now(),
}
err := chain.Add(context.Background(), block)
require.NoError(err)
// Record first vote - should not reach quorum yet
vote1 := &types.Vote{
BlockID: block.ID,
VoteType: types.VotePreference,
Voter: ids.GenerateTestNodeID(),
}
err = chain.RecordVote(context.Background(), vote1)
require.NoError(err)
require.False(chain.IsAccepted(block.ID))
// Record second vote - should reach quorum (alpha=2)
vote2 := &types.Vote{
BlockID: block.ID,
VoteType: types.VotePreference,
Voter: ids.GenerateTestNodeID(),
}
err = chain.RecordVote(context.Background(), vote2)
require.NoError(err)
require.True(chain.IsAccepted(block.ID))
}
// TestChainRecordVoteBlockNotFound tests voting on non-existent block
func TestChainRecordVoteBlockNotFound(t *testing.T) {
require := require.New(t)
config := types.Config{Alpha: 2}
chain := NewChain(config)
vote := &types.Vote{
BlockID: ids.GenerateTestID(),
VoteType: types.VotePreference,
Voter: ids.GenerateTestNodeID(),
}
err := chain.RecordVote(context.Background(), vote)
require.Error(err)
require.Equal(types.ErrBlockNotFound, err)
}
// TestChainIsAccepted tests the IsAccepted method
func TestChainIsAccepted(t *testing.T) {
require := require.New(t)
config := types.Config{Alpha: 1}
chain := NewChain(config)
blockID := ids.GenerateTestID()
// Non-existent block should not be accepted
require.False(chain.IsAccepted(blockID))
// Add block
block := &types.Block{
ID: blockID,
ParentID: types.GenesisID,
Height: 1,
Time: time.Now(),
}
err := chain.Add(context.Background(), block)
require.NoError(err)
// Not yet accepted
require.False(chain.IsAccepted(blockID))
// Vote to accept
vote := &types.Vote{
BlockID: blockID,
VoteType: types.VotePreference,
Voter: ids.GenerateTestNodeID(),
}
err = chain.RecordVote(context.Background(), vote)
require.NoError(err)
// Now accepted
require.True(chain.IsAccepted(blockID))
}
// TestChainGetStatus tests the GetStatus method
func TestChainGetStatus(t *testing.T) {
require := require.New(t)
config := types.Config{Alpha: 1}
chain := NewChain(config)
// Unknown block
unknownID := ids.GenerateTestID()
require.Equal(types.StatusUnknown, chain.GetStatus(unknownID))
// Add block
block := &types.Block{
ID: ids.GenerateTestID(),
ParentID: types.GenesisID,
Height: 1,
Time: time.Now(),
}
err := chain.Add(context.Background(), block)
require.NoError(err)
// Processing status
require.Equal(types.StatusProcessing, chain.GetStatus(block.ID))
// Vote to accept
vote := &types.Vote{
BlockID: block.ID,
VoteType: types.VotePreference,
Voter: ids.GenerateTestNodeID(),
}
err = chain.RecordVote(context.Background(), vote)
require.NoError(err)
// Accepted status
require.Equal(types.StatusAccepted, chain.GetStatus(block.ID))
}
// TestChainStart tests the Start method
func TestChainStart(t *testing.T) {
require := require.New(t)
config := types.Config{Alpha: 15}
chain := NewChain(config)
err := chain.Start(context.Background())
require.NoError(err)
// Verify genesis block was initialized
chain.mu.RLock()
genesis, exists := chain.blocks[types.GenesisID]
status := chain.status[types.GenesisID]
lastAccepted := chain.lastAccepted
chain.mu.RUnlock()
require.True(exists)
require.NotNil(genesis)
require.Equal(uint64(0), genesis.Height)
require.Equal(types.StatusAccepted, status)
require.Equal(types.GenesisID, lastAccepted)
}
// TestChainStop tests the Stop method
func TestChainStop(t *testing.T) {
require := require.New(t)
config := types.Config{Alpha: 15}
chain := NewChain(config)
err := chain.Start(context.Background())
require.NoError(err)
err = chain.Stop()
require.NoError(err)
}
// TestChainAcceptBlock tests the acceptBlock internal method
func TestChainAcceptBlock(t *testing.T) {
require := require.New(t)
config := types.Config{Alpha: 1}
chain := NewChain(config)
err := chain.Start(context.Background())
require.NoError(err)
// Add a block at height 1
block1 := &types.Block{
ID: ids.GenerateTestID(),
ParentID: types.GenesisID,
Height: 1,
Time: time.Now(),
}
err = chain.Add(context.Background(), block1)
require.NoError(err)
// Vote to accept
vote := &types.Vote{
BlockID: block1.ID,
VoteType: types.VotePreference,
Voter: ids.GenerateTestNodeID(),
}
err = chain.RecordVote(context.Background(), vote)
require.NoError(err)
// Verify height and lastAccepted were updated
chain.mu.RLock()
height := chain.height
lastAccepted := chain.lastAccepted
chain.mu.RUnlock()
require.Equal(uint64(1), height)
require.Equal(block1.ID, lastAccepted)
// Add and accept a block at height 2
block2 := &types.Block{
ID: ids.GenerateTestID(),
ParentID: block1.ID,
Height: 2,
Time: time.Now(),
}
err = chain.Add(context.Background(), block2)
require.NoError(err)
vote2 := &types.Vote{
BlockID: block2.ID,
VoteType: types.VotePreference,
Voter: ids.GenerateTestNodeID(),
}
err = chain.RecordVote(context.Background(), vote2)
require.NoError(err)
chain.mu.RLock()
height = chain.height
lastAccepted = chain.lastAccepted
chain.mu.RUnlock()
require.Equal(uint64(2), height)
require.Equal(block2.ID, lastAccepted)
}
// TestChainAcceptBlockLowerHeight tests accepting a block with lower height
func TestChainAcceptBlockLowerHeight(t *testing.T) {
require := require.New(t)
config := types.Config{Alpha: 1}
chain := NewChain(config)
err := chain.Start(context.Background())
require.NoError(err)
// Add and accept block at height 5
block5 := &types.Block{
ID: ids.GenerateTestID(),
ParentID: types.GenesisID,
Height: 5,
Time: time.Now(),
}
err = chain.Add(context.Background(), block5)
require.NoError(err)
vote5 := &types.Vote{
BlockID: block5.ID,
VoteType: types.VotePreference,
Voter: ids.GenerateTestNodeID(),
}
err = chain.RecordVote(context.Background(), vote5)
require.NoError(err)
chain.mu.RLock()
height := chain.height
chain.mu.RUnlock()
require.Equal(uint64(5), height)
// Add and accept block at height 3 (should not update height)
block3 := &types.Block{
ID: ids.GenerateTestID(),
ParentID: types.GenesisID,
Height: 3,
Time: time.Now(),
}
err = chain.Add(context.Background(), block3)
require.NoError(err)
vote3 := &types.Vote{
BlockID: block3.ID,
VoteType: types.VotePreference,
Voter: ids.GenerateTestNodeID(),
}
err = chain.RecordVote(context.Background(), vote3)
require.NoError(err)
chain.mu.RLock()
height = chain.height
lastAccepted := chain.lastAccepted
chain.mu.RUnlock()
// Height should still be 5
require.Equal(uint64(5), height)
require.Equal(block5.ID, lastAccepted)
}
// TestDefaultConfig tests the DefaultConfig function
func TestDefaultConfig(t *testing.T) {
require := require.New(t)
config := DefaultConfig()
require.Equal(20, config.Alpha)
}
// TestChainConcurrentAccess tests concurrent access to the chain
func TestChainConcurrentAccess(t *testing.T) {
require := require.New(t)
config := types.Config{Alpha: 5}
chain := NewChain(config)
err := chain.Start(context.Background())
require.NoError(err)
// Create blocks
blocks := make([]*types.Block, 10)
for i := 0; i < 10; i++ {
blocks[i] = &types.Block{
ID: ids.GenerateTestID(),
ParentID: types.GenesisID,
Height: uint64(i + 1),
Time: time.Now(),
}
}
// Concurrent adds
done := make(chan bool)
for _, block := range blocks {
go func(b *types.Block) {
err := chain.Add(context.Background(), b)
require.NoError(err)
done <- true
}(block)
}
for i := 0; i < 10; i++ {
<-done
}
// Verify all blocks were added
chain.mu.RLock()
count := len(chain.blocks)
chain.mu.RUnlock()
// 10 blocks + genesis
require.Equal(11, count)
}