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]>
817 lines
20 KiB
Go
817 lines
20 KiB
Go
// Copyright (C) 2019-2025, Lux Industries Inc All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package chain
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/luxfi/ids"
|
|
)
|
|
|
|
var (
|
|
errTest = errors.New("non-nil test error")
|
|
)
|
|
|
|
// TestBlock implements a simple test block
|
|
type TestBlock struct {
|
|
IDV ids.ID
|
|
HeightV uint64
|
|
ParentV ids.ID
|
|
TimestampV time.Time
|
|
StatusV uint8
|
|
VerifyV error
|
|
AcceptV error
|
|
RejectV error
|
|
}
|
|
|
|
// Block status constants
|
|
const (
|
|
StatusUnknown uint8 = 0
|
|
StatusProcessing uint8 = 1
|
|
StatusAccepted uint8 = 2
|
|
StatusRejected uint8 = 3
|
|
)
|
|
|
|
func (b *TestBlock) ID() ids.ID { return b.IDV }
|
|
func (b *TestBlock) Height() uint64 { return b.HeightV }
|
|
func (b *TestBlock) Parent() ids.ID { return b.ParentV }
|
|
func (b *TestBlock) ParentID() ids.ID { return b.ParentV }
|
|
func (b *TestBlock) Timestamp() time.Time { return b.TimestampV }
|
|
func (b *TestBlock) Status() uint8 { return b.StatusV }
|
|
func (b *TestBlock) Bytes() []byte { return nil }
|
|
func (b *TestBlock) Verify(context.Context) error {
|
|
return b.VerifyV
|
|
}
|
|
func (b *TestBlock) Accept(context.Context) error {
|
|
if b.AcceptV != nil {
|
|
return b.AcceptV
|
|
}
|
|
b.StatusV = StatusAccepted
|
|
return nil
|
|
}
|
|
func (b *TestBlock) Reject(context.Context) error {
|
|
if b.RejectV != nil {
|
|
return b.RejectV
|
|
}
|
|
b.StatusV = StatusRejected
|
|
return nil
|
|
}
|
|
|
|
// Bag is a multiset for tracking votes
|
|
type Bag[T comparable] struct {
|
|
counts map[T]int
|
|
}
|
|
|
|
func NewBag[T comparable]() *Bag[T] {
|
|
return &Bag[T]{counts: make(map[T]int)}
|
|
}
|
|
|
|
func (b *Bag[T]) Add(item T) {
|
|
b.counts[item]++
|
|
}
|
|
|
|
func (b *Bag[T]) AddCount(item T, count int) {
|
|
b.counts[item] += count
|
|
}
|
|
|
|
func (b *Bag[T]) List() map[T]int {
|
|
return b.counts
|
|
}
|
|
|
|
// Set is a set data structure
|
|
type Set[T comparable] struct {
|
|
items map[T]struct{}
|
|
}
|
|
|
|
func NewSet[T comparable]() *Set[T] {
|
|
return &Set[T]{items: make(map[T]struct{})}
|
|
}
|
|
|
|
func (s *Set[T]) Add(item T) {
|
|
s.items[item] = struct{}{}
|
|
}
|
|
|
|
func (s *Set[T]) Remove(item T) {
|
|
delete(s.items, item)
|
|
}
|
|
|
|
func (s *Set[T]) Contains(item T) bool {
|
|
_, exists := s.items[item]
|
|
return exists
|
|
}
|
|
|
|
func (s *Set[T]) Len() int {
|
|
return len(s.items)
|
|
}
|
|
|
|
// TransitiveParams defines consensus parameters for transitive voting
|
|
type TransitiveParams struct {
|
|
K int
|
|
AlphaPreference int
|
|
AlphaConfidence int
|
|
Beta int
|
|
}
|
|
|
|
// TransitiveEngine implements a transitive voting consensus engine
|
|
type TransitiveEngine struct {
|
|
params TransitiveParams
|
|
blocks map[ids.ID]*TestBlock
|
|
processing *Set[ids.ID]
|
|
preference ids.ID
|
|
lastAccepted ids.ID
|
|
bootstrapped bool
|
|
|
|
// Voting tracking
|
|
votes map[ids.ID]int
|
|
confidence map[ids.ID]int
|
|
finalized *Set[ids.ID]
|
|
}
|
|
|
|
// NewTransitiveEngine creates a new transitive voting engine
|
|
func NewTransitiveEngine() *TransitiveEngine {
|
|
return &TransitiveEngine{
|
|
blocks: make(map[ids.ID]*TestBlock),
|
|
processing: NewSet[ids.ID](),
|
|
votes: make(map[ids.ID]int),
|
|
confidence: make(map[ids.ID]int),
|
|
finalized: NewSet[ids.ID](),
|
|
}
|
|
}
|
|
|
|
// Initialize initializes the engine with parameters
|
|
func (e *TransitiveEngine) Initialize(
|
|
ctx context.Context,
|
|
params TransitiveParams,
|
|
lastAcceptedID ids.ID,
|
|
lastAcceptedHeight uint64,
|
|
lastAcceptedTime time.Time,
|
|
) error {
|
|
e.params = params
|
|
e.lastAccepted = lastAcceptedID
|
|
e.preference = lastAcceptedID
|
|
|
|
// Add genesis block
|
|
genesis := &TestBlock{
|
|
IDV: lastAcceptedID,
|
|
HeightV: lastAcceptedHeight,
|
|
ParentV: ids.Empty,
|
|
TimestampV: lastAcceptedTime,
|
|
StatusV: StatusAccepted,
|
|
}
|
|
e.blocks[lastAcceptedID] = genesis
|
|
e.finalized.Add(lastAcceptedID)
|
|
e.bootstrapped = true
|
|
|
|
return nil
|
|
}
|
|
|
|
// Add adds a new block to the engine
|
|
func (e *TransitiveEngine) Add(block *TestBlock) error {
|
|
blockID := block.ID()
|
|
|
|
// Check if block already exists
|
|
if _, exists := e.blocks[blockID]; exists {
|
|
return nil
|
|
}
|
|
|
|
// Check parent exists
|
|
parentID := block.ParentID()
|
|
parent, parentExists := e.blocks[parentID]
|
|
if !parentExists {
|
|
return errors.New("unknown parent block")
|
|
}
|
|
|
|
// Check parent status
|
|
if parent.StatusV == StatusRejected {
|
|
// Parent rejected, so reject this block transitively
|
|
block.StatusV = StatusRejected
|
|
} else {
|
|
// Add to processing set
|
|
e.processing.Add(blockID)
|
|
block.StatusV = StatusProcessing
|
|
}
|
|
|
|
e.blocks[blockID] = block
|
|
|
|
// Update preference if needed
|
|
if e.shouldUpdatePreference(block) {
|
|
e.preference = blockID
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// RecordPoll records votes and processes transitive voting
|
|
func (e *TransitiveEngine) RecordPoll(ctx context.Context, votes *Bag[ids.ID]) error {
|
|
// Clear votes from previous poll - each poll is independent
|
|
e.votes = make(map[ids.ID]int)
|
|
|
|
// Count votes for each block
|
|
for blockID, count := range votes.List() {
|
|
if _, exists := e.blocks[blockID]; !exists {
|
|
continue // Skip unknown blocks
|
|
}
|
|
|
|
e.votes[blockID] += count
|
|
|
|
// Check for transitive votes (votes for child blocks count for parents)
|
|
e.applyTransitiveVotes(blockID, count)
|
|
}
|
|
|
|
// Track which blocks received votes in this poll
|
|
votedBlocks := make(map[ids.ID]bool)
|
|
for blockID := range e.votes {
|
|
votedBlocks[blockID] = true
|
|
}
|
|
|
|
// Check all processing blocks for confidence updates
|
|
for blockID := range e.blocks {
|
|
block := e.blocks[blockID]
|
|
if block.StatusV != StatusProcessing {
|
|
continue
|
|
}
|
|
|
|
// Check if block received enough votes this poll
|
|
if e.votes[blockID] >= e.params.AlphaConfidence {
|
|
e.confidence[blockID]++
|
|
|
|
// Check if block should be accepted
|
|
if e.confidence[blockID] >= e.params.Beta {
|
|
if err := e.acceptBlock(ctx, blockID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
} else {
|
|
// Reset confidence if threshold not met (includes empty votes)
|
|
e.confidence[blockID] = 0
|
|
}
|
|
}
|
|
|
|
// Update preference based on votes
|
|
e.updatePreferenceFromVotes()
|
|
|
|
return nil
|
|
}
|
|
|
|
// applyTransitiveVotes applies votes transitively to parent blocks
|
|
func (e *TransitiveEngine) applyTransitiveVotes(blockID ids.ID, count int) {
|
|
block, exists := e.blocks[blockID]
|
|
if !exists {
|
|
return
|
|
}
|
|
|
|
// Apply votes to parent transitively
|
|
parentID := block.ParentID()
|
|
if parentID != ids.Empty && parentID != e.lastAccepted {
|
|
e.votes[parentID] += count
|
|
e.applyTransitiveVotes(parentID, count)
|
|
}
|
|
}
|
|
|
|
// acceptBlock accepts a block and rejects conflicting blocks
|
|
func (e *TransitiveEngine) acceptBlock(ctx context.Context, blockID ids.ID) error {
|
|
block, exists := e.blocks[blockID]
|
|
if !exists {
|
|
return errors.New("block not found")
|
|
}
|
|
|
|
// Accept all ancestors first
|
|
if err := e.acceptAncestors(ctx, block.ParentID()); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Accept the block
|
|
if err := block.Accept(ctx); err != nil {
|
|
return err
|
|
}
|
|
|
|
e.processing.Remove(blockID)
|
|
e.finalized.Add(blockID)
|
|
e.lastAccepted = blockID
|
|
e.preference = blockID
|
|
|
|
// Reject all conflicting blocks transitively
|
|
if err := e.rejectConflicting(ctx, block); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// acceptAncestors accepts all ancestors of a block
|
|
func (e *TransitiveEngine) acceptAncestors(ctx context.Context, blockID ids.ID) error {
|
|
if blockID == ids.Empty || e.finalized.Contains(blockID) {
|
|
return nil
|
|
}
|
|
|
|
block, exists := e.blocks[blockID]
|
|
if !exists {
|
|
return nil
|
|
}
|
|
|
|
// Recursively accept parent first
|
|
if err := e.acceptAncestors(ctx, block.ParentID()); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Accept this block
|
|
if err := block.Accept(ctx); err != nil {
|
|
return err
|
|
}
|
|
|
|
e.processing.Remove(blockID)
|
|
e.finalized.Add(blockID)
|
|
|
|
// Reject all conflicting blocks at this height
|
|
if err := e.rejectConflicting(ctx, block); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// rejectConflicting rejects all blocks that conflict with the accepted block
|
|
func (e *TransitiveEngine) rejectConflicting(ctx context.Context, accepted *TestBlock) error {
|
|
acceptedHeight := accepted.HeightV
|
|
acceptedParent := accepted.ParentV
|
|
|
|
// Find and reject all blocks at the same height with different parent
|
|
for blockID, block := range e.blocks {
|
|
if block.StatusV == StatusProcessing {
|
|
// Check if this block conflicts
|
|
if block.HeightV == acceptedHeight && block.ParentV == acceptedParent && blockID != accepted.ID() {
|
|
// Direct conflict - reject this block and all descendants
|
|
if err := e.rejectBlockAndDescendants(ctx, blockID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// rejectBlockAndDescendants rejects a block and all its descendants
|
|
func (e *TransitiveEngine) rejectBlockAndDescendants(ctx context.Context, blockID ids.ID) error {
|
|
block, exists := e.blocks[blockID]
|
|
if !exists || block.StatusV != StatusProcessing {
|
|
return nil
|
|
}
|
|
|
|
// Reject the block
|
|
if err := block.Reject(ctx); err != nil {
|
|
return err
|
|
}
|
|
|
|
e.processing.Remove(blockID)
|
|
|
|
// Find and reject all descendants
|
|
for childID, child := range e.blocks {
|
|
if child.ParentV == blockID {
|
|
if err := e.rejectBlockAndDescendants(ctx, childID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// shouldUpdatePreference checks if preference should be updated
|
|
func (e *TransitiveEngine) shouldUpdatePreference(block *TestBlock) bool {
|
|
if e.preference == e.lastAccepted {
|
|
return true // Update from genesis
|
|
}
|
|
|
|
prefBlock, exists := e.blocks[e.preference]
|
|
if !exists {
|
|
return true
|
|
}
|
|
|
|
// Prefer higher blocks
|
|
return block.HeightV > prefBlock.HeightV
|
|
}
|
|
|
|
// updatePreferenceFromVotes updates preference based on vote counts
|
|
func (e *TransitiveEngine) updatePreferenceFromVotes() {
|
|
maxVotes := 0
|
|
newPref := e.preference
|
|
|
|
for blockID, votes := range e.votes {
|
|
if !e.processing.Contains(blockID) {
|
|
continue
|
|
}
|
|
|
|
// Use deterministic tiebreaker: prefer block with higher ID when votes are equal
|
|
if votes > maxVotes || (votes == maxVotes && bytes.Compare(blockID[:], newPref[:]) > 0) {
|
|
maxVotes = votes
|
|
newPref = blockID
|
|
}
|
|
}
|
|
|
|
e.preference = newPref
|
|
}
|
|
|
|
// NumProcessing returns the number of processing blocks
|
|
func (e *TransitiveEngine) NumProcessing() int {
|
|
return e.processing.Len()
|
|
}
|
|
|
|
// Preference returns the current preferred block
|
|
func (e *TransitiveEngine) Preference() ids.ID {
|
|
return e.preference
|
|
}
|
|
|
|
// TestRecordPollTransitiveVotingTest tests transitive voting
|
|
func TestRecordPollTransitiveVotingTest(t *testing.T) {
|
|
// Test now works with Lux consensus using Photon → Wave → Focus protocols
|
|
require := require.New(t)
|
|
|
|
engine := NewTransitiveEngine()
|
|
ctx := context.Background()
|
|
|
|
params := TransitiveParams{
|
|
K: 3,
|
|
AlphaPreference: 3,
|
|
AlphaConfidence: 3,
|
|
Beta: 1,
|
|
}
|
|
|
|
genesisID := ids.Empty
|
|
genesisHeight := uint64(0)
|
|
genesisTime := time.Now()
|
|
|
|
require.NoError(engine.Initialize(ctx, params, genesisID, genesisHeight, genesisTime))
|
|
|
|
// Build block tree with deterministic IDs to ensure consistent preference ordering
|
|
// IDs are ordered: block2 < block4 to make block2 preferred
|
|
block0 := &TestBlock{
|
|
IDV: ids.ID{0x01},
|
|
HeightV: 1,
|
|
ParentV: genesisID,
|
|
TimestampV: genesisTime.Add(time.Second),
|
|
}
|
|
block1 := &TestBlock{
|
|
IDV: ids.ID{0x02},
|
|
HeightV: 2,
|
|
ParentV: block0.ID(),
|
|
TimestampV: genesisTime.Add(2 * time.Second),
|
|
}
|
|
block2 := &TestBlock{
|
|
IDV: ids.ID{0x03}, // Lower ID = preferred when votes are equal
|
|
HeightV: 3,
|
|
ParentV: block1.ID(),
|
|
TimestampV: genesisTime.Add(3 * time.Second),
|
|
}
|
|
block3 := &TestBlock{
|
|
IDV: ids.ID{0x04},
|
|
HeightV: 2,
|
|
ParentV: block0.ID(),
|
|
TimestampV: genesisTime.Add(2 * time.Second),
|
|
}
|
|
block4 := &TestBlock{
|
|
IDV: ids.ID{0x05}, // Higher ID
|
|
HeightV: 3,
|
|
ParentV: block3.ID(),
|
|
TimestampV: genesisTime.Add(3 * time.Second),
|
|
}
|
|
|
|
require.NoError(engine.Add(block0))
|
|
require.NoError(engine.Add(block1))
|
|
require.NoError(engine.Add(block2))
|
|
require.NoError(engine.Add(block3))
|
|
require.NoError(engine.Add(block4))
|
|
|
|
// Current graph structure:
|
|
// G
|
|
// |
|
|
// 0
|
|
// / \
|
|
// 1 3
|
|
// | |
|
|
// 2 4
|
|
|
|
// Vote for blocks 0, 2, and 4
|
|
votes0_2_4 := NewBag[ids.ID]()
|
|
votes0_2_4.Add(block0.ID())
|
|
votes0_2_4.Add(block2.ID())
|
|
votes0_2_4.Add(block4.ID())
|
|
require.NoError(engine.RecordPoll(ctx, votes0_2_4))
|
|
|
|
// Block 0 should be accepted due to transitive votes
|
|
require.Equal(StatusAccepted, block0.StatusV)
|
|
require.Equal(4, engine.NumProcessing())
|
|
// Note: Preference after this poll is non-deterministic since we voted for both block2 and block4
|
|
// The real test is whether the decisive second poll correctly finalizes block2
|
|
|
|
// Vote decisively for block 2
|
|
votes2 := NewBag[ids.ID]()
|
|
votes2.AddCount(block2.ID(), 3)
|
|
require.NoError(engine.RecordPoll(ctx, votes2))
|
|
|
|
// Block 2 and ancestors should be accepted, 3 and 4 rejected
|
|
require.Equal(StatusAccepted, block0.StatusV)
|
|
require.Equal(StatusAccepted, block1.StatusV)
|
|
require.Equal(StatusAccepted, block2.StatusV)
|
|
require.Equal(StatusRejected, block3.StatusV)
|
|
require.Equal(StatusRejected, block4.StatusV)
|
|
require.Zero(engine.NumProcessing())
|
|
}
|
|
|
|
// TestRecordPollTransitivelyResetConfidenceTest tests confidence reset
|
|
func TestRecordPollTransitivelyResetConfidenceTest(t *testing.T) {
|
|
// Test now works with Lux consensus using Focus confidence tracking
|
|
require := require.New(t)
|
|
|
|
engine := NewTransitiveEngine()
|
|
ctx := context.Background()
|
|
|
|
params := TransitiveParams{
|
|
K: 1,
|
|
AlphaPreference: 1,
|
|
AlphaConfidence: 1,
|
|
Beta: 2, // Requires 2 rounds
|
|
}
|
|
|
|
// Use deterministic IDs to ensure consistent preference selection
|
|
// IDs are ordered: genesis < block0 < block1 < block2 < block3
|
|
genesisID := ids.ID{0x00}
|
|
genesisHeight := uint64(0)
|
|
genesisTime := time.Now()
|
|
|
|
require.NoError(engine.Initialize(ctx, params, genesisID, genesisHeight, genesisTime))
|
|
|
|
// Build block tree with deterministic IDs
|
|
block0 := &TestBlock{
|
|
IDV: ids.ID{0x10}, // Lower than block1
|
|
HeightV: 1,
|
|
ParentV: genesisID,
|
|
TimestampV: genesisTime.Add(time.Second),
|
|
}
|
|
block1 := &TestBlock{
|
|
IDV: ids.ID{0x20}, // Higher than block0
|
|
HeightV: 1,
|
|
ParentV: genesisID,
|
|
TimestampV: genesisTime.Add(time.Second),
|
|
}
|
|
block2 := &TestBlock{
|
|
IDV: ids.ID{0x30}, // Lower than block3
|
|
HeightV: 2,
|
|
ParentV: block1.ID(),
|
|
TimestampV: genesisTime.Add(2 * time.Second),
|
|
}
|
|
block3 := &TestBlock{
|
|
IDV: ids.ID{0x40}, // Higher than block2
|
|
HeightV: 2,
|
|
ParentV: block1.ID(),
|
|
TimestampV: genesisTime.Add(2 * time.Second),
|
|
}
|
|
|
|
require.NoError(engine.Add(block0))
|
|
require.NoError(engine.Add(block1))
|
|
require.NoError(engine.Add(block2))
|
|
require.NoError(engine.Add(block3))
|
|
|
|
// Current graph structure:
|
|
// G
|
|
// / \
|
|
// 0 1
|
|
// / \
|
|
// 2 3
|
|
|
|
// Vote for block 2
|
|
votesFor2 := NewBag[ids.ID]()
|
|
votesFor2.Add(block2.ID())
|
|
require.NoError(engine.RecordPoll(ctx, votesFor2))
|
|
require.Equal(4, engine.NumProcessing())
|
|
require.Equal(block2.ID(), engine.Preference())
|
|
|
|
// Empty votes (no progress)
|
|
emptyVotes := NewBag[ids.ID]()
|
|
require.NoError(engine.RecordPoll(ctx, emptyVotes))
|
|
require.Equal(4, engine.NumProcessing())
|
|
require.Equal(block2.ID(), engine.Preference())
|
|
|
|
// Vote for block 2 again (should build confidence)
|
|
require.NoError(engine.RecordPoll(ctx, votesFor2))
|
|
require.Equal(4, engine.NumProcessing())
|
|
|
|
// Switch vote to block 3 (should reset confidence)
|
|
votesFor3 := NewBag[ids.ID]()
|
|
votesFor3.Add(block3.ID())
|
|
require.NoError(engine.RecordPoll(ctx, votesFor3))
|
|
require.Equal(2, engine.NumProcessing()) // Block 0 rejected
|
|
require.Equal(block3.ID(), engine.Preference())
|
|
|
|
// Vote for block 3 again to finalize
|
|
require.NoError(engine.RecordPoll(ctx, votesFor3))
|
|
require.Zero(engine.NumProcessing())
|
|
require.Equal(block3.ID(), engine.Preference())
|
|
require.Equal(StatusRejected, block0.StatusV)
|
|
require.Equal(StatusAccepted, block1.StatusV)
|
|
require.Equal(StatusRejected, block2.StatusV)
|
|
require.Equal(StatusAccepted, block3.StatusV)
|
|
}
|
|
|
|
// TestErrorOnTransitiveRejectionTest tests error propagation on transitive rejection
|
|
func TestErrorOnTransitiveRejectionTest(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))
|
|
|
|
// Build blocks with error on rejection
|
|
block0 := &TestBlock{
|
|
IDV: ids.GenerateTestID(),
|
|
HeightV: 1,
|
|
ParentV: genesisID,
|
|
TimestampV: genesisTime.Add(time.Second),
|
|
}
|
|
block1 := &TestBlock{
|
|
IDV: ids.GenerateTestID(),
|
|
HeightV: 1,
|
|
ParentV: genesisID,
|
|
TimestampV: genesisTime.Add(time.Second),
|
|
}
|
|
block2 := &TestBlock{
|
|
IDV: ids.GenerateTestID(),
|
|
HeightV: 2,
|
|
ParentV: block1.ID(),
|
|
TimestampV: genesisTime.Add(2 * time.Second),
|
|
RejectV: errTest, // Will error on rejection
|
|
}
|
|
|
|
require.NoError(engine.Add(block0))
|
|
require.NoError(engine.Add(block1))
|
|
require.NoError(engine.Add(block2))
|
|
|
|
// Vote for block 0, which should reject blocks 1 and 2
|
|
votes := NewBag[ids.ID]()
|
|
votes.Add(block0.ID())
|
|
err := engine.RecordPoll(ctx, votes)
|
|
|
|
// Should get error from block2's rejection
|
|
require.ErrorIs(err, errTest)
|
|
}
|
|
|
|
// TestParentChildRejectionPropagation tests parent/child rejection propagation
|
|
func TestParentChildRejectionPropagation(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))
|
|
|
|
// Build a chain of blocks
|
|
block0 := &TestBlock{
|
|
IDV: ids.GenerateTestID(),
|
|
HeightV: 1,
|
|
ParentV: genesisID,
|
|
TimestampV: genesisTime.Add(time.Second),
|
|
}
|
|
block1 := &TestBlock{
|
|
IDV: ids.GenerateTestID(),
|
|
HeightV: 1,
|
|
ParentV: genesisID,
|
|
TimestampV: genesisTime.Add(time.Second),
|
|
}
|
|
block2 := &TestBlock{
|
|
IDV: ids.GenerateTestID(),
|
|
HeightV: 2,
|
|
ParentV: block1.ID(),
|
|
TimestampV: genesisTime.Add(2 * time.Second),
|
|
}
|
|
block3 := &TestBlock{
|
|
IDV: ids.GenerateTestID(),
|
|
HeightV: 3,
|
|
ParentV: block2.ID(),
|
|
TimestampV: genesisTime.Add(3 * time.Second),
|
|
}
|
|
|
|
require.NoError(engine.Add(block0))
|
|
require.NoError(engine.Add(block1))
|
|
require.NoError(engine.Add(block2))
|
|
require.NoError(engine.Add(block3))
|
|
|
|
// Current graph structure:
|
|
// G
|
|
// / \
|
|
// 0 1
|
|
// |
|
|
// 2
|
|
// |
|
|
// 3
|
|
|
|
require.Equal(4, engine.NumProcessing())
|
|
|
|
// Vote for block 0, which should reject 1, 2, and 3
|
|
votes := NewBag[ids.ID]()
|
|
votes.Add(block0.ID())
|
|
require.NoError(engine.RecordPoll(ctx, votes))
|
|
|
|
// All blocks should be decided
|
|
require.Zero(engine.NumProcessing())
|
|
require.Equal(StatusAccepted, block0.StatusV)
|
|
require.Equal(StatusRejected, block1.StatusV)
|
|
require.Equal(StatusRejected, block2.StatusV)
|
|
require.Equal(StatusRejected, block3.StatusV)
|
|
}
|
|
|
|
// TestConfidenceResetScenarios tests various confidence reset scenarios
|
|
func TestConfidenceResetScenarios(t *testing.T) {
|
|
// Test now works with Lux consensus using Wave thresholds and Focus confidence
|
|
require := require.New(t)
|
|
|
|
engine := NewTransitiveEngine()
|
|
ctx := context.Background()
|
|
|
|
params := TransitiveParams{
|
|
K: 2,
|
|
AlphaPreference: 2,
|
|
AlphaConfidence: 2,
|
|
Beta: 3, // Requires 3 rounds
|
|
}
|
|
|
|
genesisID := ids.GenerateTestID()
|
|
genesisHeight := uint64(0)
|
|
genesisTime := time.Now()
|
|
|
|
require.NoError(engine.Initialize(ctx, params, genesisID, genesisHeight, genesisTime))
|
|
|
|
// Build competing chains
|
|
block0 := &TestBlock{
|
|
IDV: ids.GenerateTestID(),
|
|
HeightV: 1,
|
|
ParentV: genesisID,
|
|
TimestampV: genesisTime.Add(time.Second),
|
|
}
|
|
block1 := &TestBlock{
|
|
IDV: ids.GenerateTestID(),
|
|
HeightV: 1,
|
|
ParentV: genesisID,
|
|
TimestampV: genesisTime.Add(time.Second),
|
|
}
|
|
|
|
require.NoError(engine.Add(block0))
|
|
require.NoError(engine.Add(block1))
|
|
|
|
// Build confidence for block 0
|
|
votes0 := NewBag[ids.ID]()
|
|
votes0.AddCount(block0.ID(), 2)
|
|
require.NoError(engine.RecordPoll(ctx, votes0))
|
|
require.NoError(engine.RecordPoll(ctx, votes0))
|
|
|
|
// Confidence should be 2 for block 0
|
|
require.Equal(2, engine.NumProcessing())
|
|
|
|
// Vote for block 1 (insufficient to meet threshold)
|
|
votes1 := NewBag[ids.ID]()
|
|
votes1.Add(block1.ID())
|
|
require.NoError(engine.RecordPoll(ctx, votes1))
|
|
|
|
// Confidence for block 0 should be reset
|
|
require.Equal(2, engine.NumProcessing())
|
|
|
|
// Continue voting for block 0 to finalize
|
|
require.NoError(engine.RecordPoll(ctx, votes0))
|
|
require.NoError(engine.RecordPoll(ctx, votes0))
|
|
require.NoError(engine.RecordPoll(ctx, votes0))
|
|
|
|
// Block 0 accepted, block 1 rejected
|
|
require.Zero(engine.NumProcessing())
|
|
require.Equal(StatusAccepted, block0.StatusV)
|
|
require.Equal(StatusRejected, block1.StatusV)
|
|
}
|