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