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]>
331 lines
9.9 KiB
Go
331 lines
9.9 KiB
Go
// Copyright (C) 2019-2025, Lux Industries Inc All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package chain
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/luxfi/ids"
|
|
)
|
|
|
|
// TestInitialize verifies Initialize() sets correct initial state:
|
|
// preference = genesis, numProcessing = 0, lastAccepted = genesis.
|
|
func TestInitialize(t *testing.T) {
|
|
require := require.New(t)
|
|
engine := NewTransitiveEngine()
|
|
ctx := context.Background()
|
|
|
|
params := TransitiveParams{
|
|
K: 3,
|
|
AlphaPreference: 3,
|
|
AlphaConfidence: 3,
|
|
Beta: 5,
|
|
}
|
|
|
|
genesisID := ids.GenerateTestID()
|
|
genesisHeight := uint64(0)
|
|
genesisTime := time.Now()
|
|
|
|
require.NoError(engine.Initialize(ctx, params, genesisID, genesisHeight, genesisTime))
|
|
|
|
// Verify initial state
|
|
require.Equal(genesisID, engine.preference, "preference should be genesis")
|
|
require.Equal(genesisID, engine.lastAccepted, "lastAccepted should be genesis")
|
|
require.Zero(engine.NumProcessing(), "numProcessing should be 0")
|
|
|
|
// Verify genesis block is in blocks map
|
|
genesisBlock, exists := engine.blocks[genesisID]
|
|
require.True(exists, "genesis block should be in blocks map")
|
|
require.Equal(StatusAccepted, genesisBlock.Status(), "genesis should be accepted")
|
|
|
|
// Verify genesis is finalized
|
|
require.True(engine.finalized.Contains(genesisID), "genesis should be finalized")
|
|
|
|
// Verify engine is bootstrapped
|
|
require.True(engine.bootstrapped, "engine should be bootstrapped")
|
|
|
|
// Verify params are set correctly
|
|
require.Equal(params.K, engine.params.K)
|
|
require.Equal(params.AlphaPreference, engine.params.AlphaPreference)
|
|
require.Equal(params.AlphaConfidence, engine.params.AlphaConfidence)
|
|
require.Equal(params.Beta, engine.params.Beta)
|
|
}
|
|
|
|
// TestAddToTail verifies that adding a block that extends the current preference
|
|
// UPDATES the preference to the new block.
|
|
func TestAddToTail(t *testing.T) {
|
|
require := require.New(t)
|
|
engine := NewTransitiveEngine()
|
|
ctx := context.Background()
|
|
|
|
params := TransitiveParams{
|
|
K: 1,
|
|
AlphaPreference: 1,
|
|
AlphaConfidence: 1,
|
|
Beta: 1,
|
|
}
|
|
|
|
genesisID := ids.GenerateTestID()
|
|
genesisHeight := uint64(0)
|
|
genesisTime := time.Now()
|
|
|
|
require.NoError(engine.Initialize(ctx, params, genesisID, genesisHeight, genesisTime))
|
|
|
|
// Initial preference should be genesis
|
|
require.Equal(genesisID, engine.preference)
|
|
|
|
// Add a block that extends genesis (the current preference)
|
|
block1 := &TestBlock{
|
|
IDV: ids.GenerateTestID(),
|
|
HeightV: 1,
|
|
ParentV: genesisID,
|
|
TimestampV: genesisTime.Add(time.Second),
|
|
}
|
|
require.NoError(engine.Add(block1))
|
|
|
|
// Preference should update to block1 since it extends the tail
|
|
require.Equal(block1.ID(), engine.preference, "preference should update to block1")
|
|
|
|
// Block should be in processing
|
|
require.True(engine.processing.Contains(block1.ID()))
|
|
require.Equal(1, engine.NumProcessing())
|
|
|
|
// Add another block extending block1 (the new tail)
|
|
block2 := &TestBlock{
|
|
IDV: ids.GenerateTestID(),
|
|
HeightV: 2,
|
|
ParentV: block1.ID(),
|
|
TimestampV: genesisTime.Add(2 * time.Second),
|
|
}
|
|
require.NoError(engine.Add(block2))
|
|
|
|
// Preference should update to block2
|
|
require.Equal(block2.ID(), engine.preference, "preference should update to block2")
|
|
require.Equal(2, engine.NumProcessing())
|
|
|
|
// Verify the chain: genesis -> block1 -> block2
|
|
require.Equal(genesisID, block1.ParentID())
|
|
require.Equal(block1.ID(), block2.ParentID())
|
|
}
|
|
|
|
// TestAddToNonTail verifies that adding a block that is a sibling
|
|
// (not extending current preference) does NOT change the preference.
|
|
func TestAddToNonTail(t *testing.T) {
|
|
require := require.New(t)
|
|
engine := NewTransitiveEngine()
|
|
ctx := context.Background()
|
|
|
|
params := TransitiveParams{
|
|
K: 1,
|
|
AlphaPreference: 1,
|
|
AlphaConfidence: 1,
|
|
Beta: 1,
|
|
}
|
|
|
|
genesisID := ids.GenerateTestID()
|
|
genesisHeight := uint64(0)
|
|
genesisTime := time.Now()
|
|
|
|
require.NoError(engine.Initialize(ctx, params, genesisID, genesisHeight, genesisTime))
|
|
|
|
// Add block1 extending genesis
|
|
block1 := &TestBlock{
|
|
IDV: ids.GenerateTestID(),
|
|
HeightV: 1,
|
|
ParentV: genesisID,
|
|
TimestampV: genesisTime.Add(time.Second),
|
|
}
|
|
require.NoError(engine.Add(block1))
|
|
|
|
// Preference should be block1
|
|
require.Equal(block1.ID(), engine.preference)
|
|
|
|
// Add block2 also extending genesis (sibling of block1, not extending the tail)
|
|
block2 := &TestBlock{
|
|
IDV: ids.GenerateTestID(),
|
|
HeightV: 1,
|
|
ParentV: genesisID,
|
|
TimestampV: genesisTime.Add(time.Second),
|
|
}
|
|
require.NoError(engine.Add(block2))
|
|
|
|
// Preference should still be block1 (higher height takes precedence in shouldUpdatePreference,
|
|
// but both are same height, so preference stays with block1)
|
|
require.Equal(block1.ID(), engine.preference, "preference should remain block1")
|
|
|
|
// Both blocks should be in processing
|
|
require.True(engine.processing.Contains(block1.ID()))
|
|
require.True(engine.processing.Contains(block2.ID()))
|
|
require.Equal(2, engine.NumProcessing())
|
|
|
|
// Add block3 extending block1 (the current preference tail)
|
|
block3 := &TestBlock{
|
|
IDV: ids.GenerateTestID(),
|
|
HeightV: 2,
|
|
ParentV: block1.ID(),
|
|
TimestampV: genesisTime.Add(2 * time.Second),
|
|
}
|
|
require.NoError(engine.Add(block3))
|
|
|
|
// Now preference should update to block3 (extends the tail and is higher)
|
|
require.Equal(block3.ID(), engine.preference, "preference should update to block3")
|
|
require.Equal(3, engine.NumProcessing())
|
|
|
|
// Add block4 extending block2 (not the current preference)
|
|
block4 := &TestBlock{
|
|
IDV: ids.GenerateTestID(),
|
|
HeightV: 2,
|
|
ParentV: block2.ID(),
|
|
TimestampV: genesisTime.Add(2 * time.Second),
|
|
}
|
|
require.NoError(engine.Add(block4))
|
|
|
|
// Preference should still be block3 (same height as block4, but block3 was already preferred)
|
|
require.Equal(block3.ID(), engine.preference, "preference should remain block3")
|
|
require.Equal(4, engine.NumProcessing())
|
|
}
|
|
|
|
// TestAddOnUnknownParent verifies that adding a block with an unknown/missing
|
|
// parent returns an error.
|
|
func TestAddOnUnknownParent(t *testing.T) {
|
|
require := require.New(t)
|
|
engine := NewTransitiveEngine()
|
|
ctx := context.Background()
|
|
|
|
params := TransitiveParams{
|
|
K: 1,
|
|
AlphaPreference: 1,
|
|
AlphaConfidence: 1,
|
|
Beta: 1,
|
|
}
|
|
|
|
genesisID := ids.GenerateTestID()
|
|
genesisHeight := uint64(0)
|
|
genesisTime := time.Now()
|
|
|
|
require.NoError(engine.Initialize(ctx, params, genesisID, genesisHeight, genesisTime))
|
|
|
|
// Create a block with an unknown parent ID
|
|
unknownParentID := ids.GenerateTestID()
|
|
orphanBlock := &TestBlock{
|
|
IDV: ids.GenerateTestID(),
|
|
HeightV: 1,
|
|
ParentV: unknownParentID,
|
|
TimestampV: time.Now(),
|
|
StatusV: StatusProcessing,
|
|
}
|
|
|
|
// Adding a block with unknown parent should return an error
|
|
err := engine.Add(orphanBlock)
|
|
require.Error(err, "should return error for unknown parent")
|
|
require.Contains(err.Error(), "unknown parent", "error should mention unknown parent")
|
|
|
|
// Block should NOT be added to processing set
|
|
require.False(engine.processing.Contains(orphanBlock.ID()))
|
|
require.Zero(engine.NumProcessing())
|
|
|
|
// Preference should remain genesis
|
|
require.Equal(genesisID, engine.preference)
|
|
}
|
|
|
|
// TestLastAccepted verifies that LastAccepted() correctly tracks the most
|
|
// recently accepted block through multiple accepts.
|
|
func TestLastAccepted(t *testing.T) {
|
|
require := require.New(t)
|
|
engine := NewTransitiveEngine()
|
|
ctx := context.Background()
|
|
|
|
params := TransitiveParams{
|
|
K: 1,
|
|
AlphaPreference: 1,
|
|
AlphaConfidence: 1,
|
|
Beta: 1,
|
|
}
|
|
|
|
genesisID := ids.GenerateTestID()
|
|
genesisHeight := uint64(0)
|
|
genesisTime := time.Now()
|
|
|
|
require.NoError(engine.Initialize(ctx, params, genesisID, genesisHeight, genesisTime))
|
|
|
|
// Initially, lastAccepted should be genesis
|
|
require.Equal(genesisID, engine.lastAccepted, "initially lastAccepted should be genesis")
|
|
|
|
// Add and accept block1
|
|
block1 := &TestBlock{
|
|
IDV: ids.GenerateTestID(),
|
|
HeightV: 1,
|
|
ParentV: genesisID,
|
|
TimestampV: genesisTime.Add(time.Second),
|
|
}
|
|
require.NoError(engine.Add(block1))
|
|
|
|
votes1 := NewBag[ids.ID]()
|
|
votes1.Add(block1.ID())
|
|
require.NoError(engine.RecordPoll(ctx, votes1))
|
|
|
|
// LastAccepted should now be block1
|
|
require.Equal(block1.ID(), engine.lastAccepted, "lastAccepted should be block1 after acceptance")
|
|
require.Equal(StatusAccepted, block1.Status())
|
|
require.Zero(engine.NumProcessing())
|
|
|
|
// Add and accept block2
|
|
block2 := &TestBlock{
|
|
IDV: ids.GenerateTestID(),
|
|
HeightV: 2,
|
|
ParentV: block1.ID(),
|
|
TimestampV: genesisTime.Add(2 * time.Second),
|
|
}
|
|
require.NoError(engine.Add(block2))
|
|
|
|
votes2 := NewBag[ids.ID]()
|
|
votes2.Add(block2.ID())
|
|
require.NoError(engine.RecordPoll(ctx, votes2))
|
|
|
|
// LastAccepted should now be block2
|
|
require.Equal(block2.ID(), engine.lastAccepted, "lastAccepted should be block2 after acceptance")
|
|
require.Equal(StatusAccepted, block2.Status())
|
|
require.Zero(engine.NumProcessing())
|
|
|
|
// Add competing blocks at height 3
|
|
block3a := &TestBlock{
|
|
IDV: ids.GenerateTestID(),
|
|
HeightV: 3,
|
|
ParentV: block2.ID(),
|
|
TimestampV: genesisTime.Add(3 * time.Second),
|
|
}
|
|
block3b := &TestBlock{
|
|
IDV: ids.GenerateTestID(),
|
|
HeightV: 3,
|
|
ParentV: block2.ID(),
|
|
TimestampV: genesisTime.Add(3 * time.Second),
|
|
}
|
|
|
|
require.NoError(engine.Add(block3a))
|
|
require.NoError(engine.Add(block3b))
|
|
require.Equal(2, engine.NumProcessing())
|
|
|
|
// Accept block3a
|
|
votes3a := NewBag[ids.ID]()
|
|
votes3a.Add(block3a.ID())
|
|
require.NoError(engine.RecordPoll(ctx, votes3a))
|
|
|
|
// LastAccepted should now be block3a
|
|
require.Equal(block3a.ID(), engine.lastAccepted, "lastAccepted should be block3a after acceptance")
|
|
require.Equal(StatusAccepted, block3a.Status())
|
|
|
|
// Block3b should be rejected
|
|
require.Equal(StatusRejected, block3b.Status())
|
|
require.Zero(engine.NumProcessing())
|
|
|
|
// Verify the acceptance chain: genesis -> block1 -> block2 -> block3a
|
|
require.Equal(genesisID, block1.ParentID())
|
|
require.Equal(block1.ID(), block2.ParentID())
|
|
require.Equal(block2.ID(), block3a.ParentID())
|
|
}
|