diff --git a/pkg/protocol/chain.go b/pkg/protocol/chain.go index 366f138a1..31c634c35 100644 --- a/pkg/protocol/chain.go +++ b/pkg/protocol/chain.go @@ -57,6 +57,9 @@ type Chain struct { // IsEvicted contains a flag that indicates whether this chain was evicted. IsEvicted reactive.Event + // shouldEvict contains a flag that indicates whether this chain should be evicted. + shouldEvict reactive.Event + // chains contains a reference to the Chains instance that this chain belongs to. chains *Chains @@ -83,6 +86,7 @@ func newChain(chains *Chains) *Chain { StartEngine: reactive.NewVariable[bool](), Engine: reactive.NewVariable[*engine.Engine](), IsEvicted: reactive.NewEvent(), + shouldEvict: reactive.NewEvent(), chains: chains, commitments: shrinkingmap.New[iotago.SlotIndex, *Commitment](), @@ -126,6 +130,10 @@ func (c *Chain) LastCommonSlot() iotago.SlotIndex { func (c *Chain) DispatchBlock(block *model.Block, src peer.ID) (dispatched bool) { if c == nil { return false + } else if c.IsEvicted.Get() { + c.LogTrace("discard for evicted chain", "commitmentID", block.ProtocolBlock().Header.SlotCommitmentID, "blockID", block.ID()) + + return true } dispatched = c.dispatchBlockToSpawnedEngine(block, src) @@ -146,7 +154,7 @@ func (c *Chain) Commitment(slot iotago.SlotIndex) (commitment *Commitment, exist case slot > forkingPoint.Slot(): return currentChain.commitments.Get(slot) default: - currentChain = c.ParentChain.Get() + currentChain = currentChain.ParentChain.Get() } } @@ -181,6 +189,7 @@ func (c *Chain) initLogger() (shutdown func()) { c.StartEngine.LogUpdates(c, log.LevelDebug, "StartEngine"), c.Engine.LogUpdates(c, log.LevelTrace, "Engine", (*engine.Engine).LogName), c.IsEvicted.LogUpdates(c, log.LevelTrace, "IsEvicted"), + c.shouldEvict.LogUpdates(c, log.LevelTrace, "shouldEvict"), c.Logger.UnsubscribeFromParentLogger, ) @@ -188,15 +197,49 @@ func (c *Chain) initLogger() (shutdown func()) { // initDerivedProperties initializes the behavior of this chain by setting up the relations between its properties. func (c *Chain) initDerivedProperties() (shutdown func()) { - return lo.Batch( + return lo.BatchReverse( c.deriveWarpSyncMode(), - c.ForkingPoint.WithValue(c.deriveParentChain), - c.ParentChain.WithNonEmptyValue(lo.Bind(c, (*Chain).deriveChildChains)), + c.shouldEvict.OnTrigger(func() { go c.IsEvicted.Trigger() }), + + lo.BatchReverse( + c.ForkingPoint.WithNonEmptyValue(func(forkingPoint *Commitment) (teardown func()) { + return lo.BatchReverse( + c.deriveParentChain(forkingPoint), + + c.ParentChain.WithValue(func(parentChain *Chain) (teardown func()) { + return lo.BatchReverse( + parentChain.deriveChildChains(c), + + c.deriveShouldEvict(forkingPoint, parentChain), + ) + }), + ) + }), + ), + c.Engine.WithNonEmptyValue(c.deriveOutOfSyncThreshold), ) } +// deriveShouldEvict defines how a chain determines whether it should be evicted (if it is not the main chain and either +// its forking point or its parent chain is evicted). +func (c *Chain) deriveShouldEvict(forkingPoint *Commitment, parentChain *Chain) (shutdown func()) { + if forkingPoint != nil && parentChain != nil { + return c.shouldEvict.DeriveValueFrom(reactive.NewDerivedVariable2(func(_, forkingPointIsEvicted bool, parentChainIsEvicted bool) bool { + return c.chains.Main.Get() != c && (forkingPointIsEvicted || parentChainIsEvicted) + }, forkingPoint.IsEvicted, parentChain.IsEvicted)) + } + + if forkingPoint != nil { + return c.shouldEvict.DeriveValueFrom(reactive.NewDerivedVariable(func(_, forkingPointIsEvicted bool) bool { + return c.chains.Main.Get() != c && forkingPointIsEvicted + }, forkingPoint.IsEvicted)) + } + + return +} + // deriveWarpSyncMode defines how a chain determines whether it is in warp sync mode or not. func (c *Chain) deriveWarpSyncMode() func() { return c.WarpSyncMode.DeriveValueFrom(reactive.NewDerivedVariable3(func(warpSyncMode bool, latestSyncedSlot iotago.SlotIndex, latestSeenSlot iotago.SlotIndex, outOfSyncThreshold iotago.SlotIndex) bool { @@ -211,12 +254,16 @@ func (c *Chain) deriveWarpSyncMode() func() { } // deriveChildChains defines how a chain determines its ChildChains (by adding each child to the set). -func (c *Chain) deriveChildChains(child *Chain) func() { - c.ChildChains.Add(child) +func (c *Chain) deriveChildChains(child *Chain) (teardown func()) { + if c != nil && c != child { + c.ChildChains.Add(child) - return func() { - c.ChildChains.Delete(child) + teardown = func() { + c.ChildChains.Delete(child) + } } + + return } // deriveParentChain defines how a chain determines its parent chain from its forking point (it inherits the Chain from @@ -261,6 +308,10 @@ func (c *Chain) addCommitment(newCommitment *Commitment) (shutdown func()) { newCommitment.IsAttested.OnTrigger(func() { c.LatestAttestedCommitment.Set(newCommitment) }), newCommitment.IsVerified.OnTrigger(func() { c.LatestProducedCommitment.Set(newCommitment) }), newCommitment.IsSynced.OnTrigger(func() { c.LatestSyncedSlot.Set(newCommitment.Slot()) }), + + func() { + c.commitments.Delete(newCommitment.Slot()) + }, ) } diff --git a/pkg/protocol/commitment.go b/pkg/protocol/commitment.go index d6455ef92..83b8bed12 100644 --- a/pkg/protocol/commitment.go +++ b/pkg/protocol/commitment.go @@ -247,10 +247,11 @@ func (c *Commitment) initDerivedProperties() (shutdown func()) { c.CumulativeWeight.Set(c.Commitment.CumulativeWeight()) } - return lo.Batch( - parent.deriveChildren(c), + parent.registerChild(c) + return lo.BatchReverse( c.deriveChain(parent), + c.deriveCumulativeAttestedWeight(parent), c.deriveIsAboveLatestVerifiedCommitment(parent), @@ -277,8 +278,9 @@ func (c *Commitment) initDerivedProperties() (shutdown func()) { ) } -// deriveChildren derives the children of this Commitment by adding the given child to the Children set. -func (c *Commitment) deriveChildren(child *Commitment) (unregisterChild func()) { +// registerChild adds the given Commitment as a child of this Commitment and sets it as the main child if it is the +// first child of this Commitment. +func (c *Commitment) registerChild(child *Commitment) { c.MainChild.Compute(func(mainChild *Commitment) *Commitment { if !c.Children.Add(child) || mainChild != nil { return mainChild @@ -286,16 +288,6 @@ func (c *Commitment) deriveChildren(child *Commitment) (unregisterChild func()) return child }) - - return func() { - c.MainChild.Compute(func(mainChild *Commitment) *Commitment { - if !c.Children.Delete(child) || child != mainChild { - return mainChild - } - - return lo.Return1(c.Children.Any()) - }) - } } // deriveChain derives the Chain of this Commitment which is either inherited from the parent if we are the main child @@ -307,9 +299,10 @@ func (c *Commitment) deriveChain(parent *Commitment) func() { return currentChain } - // if we are not the main child of our parent, we spawn a new chain + // If we are not the main child of our parent, we spawn a new chain. + // Here we basically move commitments to a new chain if there's a fork. if c != mainChild { - if currentChain == nil { + if currentChain == nil || currentChain == parentChain { currentChain = c.commitments.protocol.Chains.newChain() currentChain.ForkingPoint.Set(c) } @@ -317,9 +310,10 @@ func (c *Commitment) deriveChain(parent *Commitment) func() { return currentChain } - // if we are the main child of our parent, and our chain is not the parent chain (that we are supposed to - // inherit), then we evict our current chain (we will spawn a new one if we ever change back to not being the - // main child) + // If we are the main child of our parent, and our chain is not the parent chain, + // then we inherit the parent chain and evict the current one. + // We will spawn a new one if we ever change back to not being the main child. + // Here we basically move commitments to the parent chain. if currentChain != nil && currentChain != parentChain { currentChain.IsEvicted.Trigger() } diff --git a/pkg/protocol/commitments.go b/pkg/protocol/commitments.go index 38a2a77da..a9d0c1763 100644 --- a/pkg/protocol/commitments.go +++ b/pkg/protocol/commitments.go @@ -153,8 +153,11 @@ func (c *Commitments) publishRootCommitment(mainChain *Chain, mainEngine *engine publishedCommitment.Chain.Set(mainChain) } - // TODO: USE SET HERE (debug eviction issues) - mainChain.ForkingPoint.DefaultTo(publishedCommitment) + // Update the forking point of a chain only if the root is empty or root belongs to the main chain or the published commitment is on the main chain. + // to avoid updating ForkingPoint of the new mainChain into the past. + if c.Root.Get() == nil || c.Root.Get().Chain.Get() == mainChain || publishedCommitment.Chain.Get() == mainChain { + mainChain.ForkingPoint.Set(publishedCommitment) + } c.Root.Set(publishedCommitment) }) @@ -227,7 +230,7 @@ func (c *Commitments) publishCommitment(commitment *model.Commitment) (published func (c *Commitments) cachedRequest(commitmentID iotago.CommitmentID, requestIfMissing ...bool) *promise.Promise[*Commitment] { // handle evicted slots slotEvicted := c.protocol.EvictionEvent(commitmentID.Index()) - if slotEvicted.WasTriggered() && c.protocol.LastEvictedSlot() != 0 { + if slotEvicted.WasTriggered() { return promise.New[*Commitment]().Reject(ErrorSlotEvicted) } @@ -271,9 +274,11 @@ func (c *Commitments) initCommitment(commitment *Commitment, slotEvicted reactiv commitment.LogDebug("created", "id", commitment.ID()) // solidify the parent of the commitment - c.cachedRequest(commitment.PreviousCommitmentID(), true).OnSuccess(func(parent *Commitment) { - commitment.Parent.Set(parent) - }) + if root := c.Root.Get(); root != nil && commitment.Slot() > root.Slot() { + c.cachedRequest(commitment.PreviousCommitmentID(), true).OnSuccess(func(parent *Commitment) { + parent.IsEvicted.OnTrigger(commitment.Parent.ToggleValue(parent)) + }) + } // add commitment to the set c.Add(commitment) diff --git a/pkg/tests/confirmation_state_test.go b/pkg/tests/confirmation_state_test.go index 05e47eaa8..82bbb0e6d 100644 --- a/pkg/tests/confirmation_state_test.go +++ b/pkg/tests/confirmation_state_test.go @@ -79,7 +79,7 @@ func TestConfirmationFlags(t *testing.T) { testsuite.WithProtocolParameters(ts.API.ProtocolParameters()), testsuite.WithLatestCommitment(genesisCommitment), testsuite.WithLatestFinalizedSlot(0), - testsuite.WithChainID(genesisCommitment.MustID()), + testsuite.WithMainChainID(genesisCommitment.MustID()), testsuite.WithStorageCommitments([]*iotago.Commitment{genesisCommitment}), testsuite.WithSybilProtectionCommittee(0, expectedCommittee), testsuite.WithSybilProtectionOnlineCommittee(lo.Return1(lo.Return1(nodeA.Protocol.Engines.Main.Get().SybilProtection.SeatManager().CommitteeInSlot(1)).GetSeat(nodeA.Validator.AccountID))), diff --git a/pkg/tests/protocol_engine_rollback_test.go b/pkg/tests/protocol_engine_rollback_test.go index f3a3b1118..06daaa127 100644 --- a/pkg/tests/protocol_engine_rollback_test.go +++ b/pkg/tests/protocol_engine_rollback_test.go @@ -116,7 +116,7 @@ func TestProtocol_EngineRollbackFinalization(t *testing.T) { testsuite.WithProtocolParameters(ts.API.ProtocolParameters()), testsuite.WithLatestCommitment(genesisCommitment), testsuite.WithLatestFinalizedSlot(0), - testsuite.WithChainID(genesisCommitment.MustID()), + testsuite.WithMainChainID(genesisCommitment.MustID()), testsuite.WithStorageCommitments([]*iotago.Commitment{genesisCommitment}), testsuite.WithSybilProtectionCommittee(0, expectedCommittee), @@ -303,7 +303,7 @@ func TestProtocol_EngineRollbackNoFinalization(t *testing.T) { testsuite.WithProtocolParameters(ts.API.ProtocolParameters()), testsuite.WithLatestCommitment(genesisCommitment), testsuite.WithLatestFinalizedSlot(0), - testsuite.WithChainID(genesisCommitment.MustID()), + testsuite.WithMainChainID(genesisCommitment.MustID()), testsuite.WithStorageCommitments([]*iotago.Commitment{genesisCommitment}), testsuite.WithSybilProtectionCommittee(0, expectedCommittee), @@ -497,7 +497,7 @@ func TestProtocol_EngineRollbackNoFinalizationLastSlot(t *testing.T) { testsuite.WithProtocolParameters(ts.API.ProtocolParameters()), testsuite.WithLatestCommitment(genesisCommitment), testsuite.WithLatestFinalizedSlot(0), - testsuite.WithChainID(genesisCommitment.MustID()), + testsuite.WithMainChainID(genesisCommitment.MustID()), testsuite.WithStorageCommitments([]*iotago.Commitment{genesisCommitment}), testsuite.WithSybilProtectionCommittee(0, expectedCommittee), @@ -691,7 +691,7 @@ func TestProtocol_EngineRollbackNoFinalizationBeforePointOfNoReturn(t *testing.T testsuite.WithProtocolParameters(ts.API.ProtocolParameters()), testsuite.WithLatestCommitment(genesisCommitment), testsuite.WithLatestFinalizedSlot(0), - testsuite.WithChainID(genesisCommitment.MustID()), + testsuite.WithMainChainID(genesisCommitment.MustID()), testsuite.WithStorageCommitments([]*iotago.Commitment{genesisCommitment}), testsuite.WithSybilProtectionCommittee(0, expectedCommittee), diff --git a/pkg/tests/protocol_engine_switching_test.go b/pkg/tests/protocol_engine_switching_test.go index 71168d92d..b5b3dcd40 100644 --- a/pkg/tests/protocol_engine_switching_test.go +++ b/pkg/tests/protocol_engine_switching_test.go @@ -2,19 +2,20 @@ package tests import ( "bytes" - "context" "fmt" + "slices" "strconv" - "sync" "testing" "time" "github.com/iotaledger/hive.go/core/eventticker" "github.com/iotaledger/hive.go/ds" + "github.com/iotaledger/hive.go/ds/types" "github.com/iotaledger/hive.go/lo" "github.com/iotaledger/hive.go/runtime/module" "github.com/iotaledger/hive.go/runtime/options" "github.com/iotaledger/iota-core/pkg/core/account" + "github.com/iotaledger/iota-core/pkg/model" "github.com/iotaledger/iota-core/pkg/protocol" "github.com/iotaledger/iota-core/pkg/protocol/engine" "github.com/iotaledger/iota-core/pkg/protocol/engine/blocks" @@ -31,10 +32,16 @@ import ( ) func TestProtocol_EngineSwitching(t *testing.T) { + var ( + genesisSlot iotago.SlotIndex = 0 + minCommittableAge iotago.SlotIndex = 2 + maxCommittableAge iotago.SlotIndex = 4 + ) + ts := testsuite.NewTestSuite(t, testsuite.WithProtocolParametersOptions( iotago.WithTimeProviderOptions( - 0, + genesisSlot, testsuite.GenesisTimeWithOffsetBySlots(1000, testsuite.DefaultSlotDurationInSeconds), testsuite.DefaultSlotDurationInSeconds, 3, @@ -42,8 +49,8 @@ func TestProtocol_EngineSwitching(t *testing.T) { iotago.WithLivenessOptions( 10, 10, - 2, - 4, + minCommittableAge, + maxCommittableAge, 5, ), ), @@ -63,7 +70,7 @@ func TestProtocol_EngineSwitching(t *testing.T) { node8 := ts.AddNode("node8") ts.AddDefaultWallet(node0) - const expectedCommittedSlotAfterPartitionMerge = 18 + const expectedCommittedSlotAfterPartitionMerge = 19 nodesP1 := []*mock.Node{node0, node1, node2, node3, node4, node5} nodesP2 := []*mock.Node{node6, node7, node8} @@ -148,7 +155,7 @@ func TestProtocol_EngineSwitching(t *testing.T) { testsuite.WithProtocolParameters(ts.API.ProtocolParameters()), testsuite.WithLatestCommitment(genesisCommitment), testsuite.WithLatestFinalizedSlot(0), - testsuite.WithChainID(genesisCommitment.MustID()), + testsuite.WithMainChainID(genesisCommitment.MustID()), testsuite.WithStorageCommitments([]*iotago.Commitment{genesisCommitment}), testsuite.WithSybilProtectionCommittee(0, expectedCommittee), @@ -260,6 +267,13 @@ func TestProtocol_EngineSwitching(t *testing.T) { ts.AssertAttestationsForSlot(slot, attestationBlocks, nodesP1...) } + // Assert Protocol.Chains and Protocol.Commitments state. + ts.AssertLatestEngineCommitmentOnMainChain(nodesP1...) + ts.AssertUniqueCommitmentChain(nodesP1...) + ts.AssertCommitmentsOnChainAndChainHasCommitments(ts.CommitmentsOfMainEngine(nodesP1[0], 13, 18), ts.CommitmentOfMainEngine(nodesP1[0], 13).ID(), nodesP1...) + ts.AssertCommitmentsOnEvictedChain(ts.CommitmentsOfMainEngine(nodesP1[0], 13, 18), false, nodesP1...) + ts.AssertCommitmentsAndChainsEvicted(12, nodesP1...) + // Make sure the tips are properly set. var tipBlocks []*blocks.Block for _, node := range nodesP1[:len(nodesP1)-1] { @@ -268,6 +282,8 @@ func TestProtocol_EngineSwitching(t *testing.T) { ts.AssertStrongTips(tipBlocks, nodesP1...) } + var engineCommitmentsP2 []*model.Commitment + // Issue blocks in partition 2. { ts.IssueBlocksAtSlots("P2:", []iotago.SlotIndex{14, 15, 16, 17, 18, 19, 20}, 4, "P0:13.3", nodesP2[:len(nodesP2)-1], true, false) @@ -282,6 +298,14 @@ func TestProtocol_EngineSwitching(t *testing.T) { testsuite.WithEvictedSlot(18), ) + // Assert Protocol.Chains and Protocol.Commitments state. + engineCommitmentsP2 = ts.CommitmentsOfMainEngine(nodesP2[0], 6, 18) + ts.AssertLatestEngineCommitmentOnMainChain(nodesP2...) + ts.AssertUniqueCommitmentChain(nodesP2...) + ts.AssertCommitmentsOnChainAndChainHasCommitments(engineCommitmentsP2, ts.CommitmentOfMainEngine(nodesP1[0], 6).ID(), nodesP2...) + ts.AssertCommitmentsOnEvictedChain(engineCommitmentsP2, false, nodesP2...) + ts.AssertCommitmentsAndChainsEvicted(5, nodesP2...) + for _, slot := range []iotago.SlotIndex{12, 13, 14, 15} { var attestationBlocks []*blocks.Block for _, node := range nodesP2 { @@ -322,40 +346,25 @@ func TestProtocol_EngineSwitching(t *testing.T) { ts.AssertStrongTips(tipBlocks, nodesP2...) } - for _, node := range ts.Nodes() { - manualPOA := node.Protocol.Engines.Main.Get().SybilProtection.SeatManager().(*mock2.ManualPOA) - manualPOA.SetOnline("node0", "node1", "node2", "node3", "node4", "node6", "node7") - } // Merge the partitions { ts.MergePartitionsToMain() fmt.Println("\n=========================\nMerged network partitions\n=========================") - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) - defer cancel() - - ctxP1, ctxP1Cancel := context.WithCancel(ctx) - ctxP2, ctxP2Cancel := context.WithCancel(ctx) - - wg := &sync.WaitGroup{} - // Issue blocks on both partitions after merging the networks. - node0.Validator.IssueActivity(ctxP1, wg, 21, node0) - node1.Validator.IssueActivity(ctxP1, wg, 21, node1) - node2.Validator.IssueActivity(ctxP1, wg, 21, node2) - node3.Validator.IssueActivity(ctxP1, wg, 21, node3) - node4.Validator.IssueActivity(ctxP1, wg, 21, node4) - // node5.Validator.IssueActivity(ctxP1, wg, 21, node5) - - node6.Validator.IssueActivity(ctxP2, wg, 21, node6) - node7.Validator.IssueActivity(ctxP2, wg, 21, node7) - // node8.Validator.IssueActivity(ctxP2, wg, 21, node8) + ts.IssueBlocksAtSlots("P2-merge:", []iotago.SlotIndex{20, 21}, 4, "P2:20.3", nodesP2[:len(nodesP2)-1], true, true) // P1 finalized until slot 18. We do not expect any forks here because our CW is higher than the other partition's. ts.AssertForkDetectedCount(0, nodesP1...) // P1's chain is heavier, they should not consider switching the chain. ts.AssertCandidateEngineActivatedCount(0, nodesP1...) - ctxP2Cancel() // we can stop issuing on P2. + + for _, node := range ts.Nodes() { + manualPOA := node.Protocol.Engines.Main.Get().SybilProtection.SeatManager().(*mock2.ManualPOA) + manualPOA.SetOnline("node0", "node1", "node2", "node3", "node4", "node6", "node7") + } + + ts.IssueBlocksAtSlots("P1-merge:", []iotago.SlotIndex{20, 21, 22}, 4, "P1:20.3", nodesP1[:len(nodesP1)-1], true, true) // Nodes from P2 should switch the chain. ts.AssertForkDetectedCount(1, nodesP2...) @@ -363,9 +372,6 @@ func TestProtocol_EngineSwitching(t *testing.T) { // Here we need to let enough time pass for the nodes to sync up the candidate engines and switch them ts.AssertMainEngineSwitchedCount(1, nodesP2...) - - ctxP1Cancel() - wg.Wait() } // Make sure that nodes that switched their engine still have blocks with prefix P0 from before the fork. @@ -376,15 +382,54 @@ func TestProtocol_EngineSwitching(t *testing.T) { ts.AssertBlocksExist(ts.BlocksWithPrefix("P2"), false, ts.Nodes()...) ts.AssertEqualStoredCommitmentAtIndex(expectedCommittedSlotAfterPartitionMerge, ts.Nodes()...) + + // Assert Protocol.Chains and Protocol.Commitments state. + { + oldestNonEvictedCommitment := iotago.SlotIndex(15) + ultimateCommitmentsP2 := lo.Filter(engineCommitmentsP2, func(commitment *model.Commitment) bool { + return commitment.Slot() >= oldestNonEvictedCommitment + }) + + ts.AssertLatestFinalizedSlot(19, ts.Nodes()...) + + commitmentsMainChain := ts.CommitmentsOfMainEngine(nodesP1[0], oldestNonEvictedCommitment, expectedCommittedSlotAfterPartitionMerge) + + ts.AssertMainChain(ts.CommitmentOfMainEngine(nodesP1[0], oldestNonEvictedCommitment).ID(), ts.Nodes()...) + ts.AssertUniqueCommitmentChain(ts.Nodes()...) + ts.AssertLatestEngineCommitmentOnMainChain(ts.Nodes()...) + ts.AssertCommitmentsOnEvictedChain(ultimateCommitmentsP2, true, ts.Nodes()...) + + ts.AssertCommitmentsOnEvictedChain(commitmentsMainChain, false, ts.Nodes()...) + + ts.AssertCommitmentsOnChainAndChainHasCommitments(commitmentsMainChain, ts.CommitmentOfMainEngine(nodesP1[0], oldestNonEvictedCommitment).ID(), ts.Nodes()...) + + // Since we diverge in slot 14 the forking point of the chain is at slot 14. + commitment14P2 := lo.First(lo.Filter(engineCommitmentsP2, func(commitment *model.Commitment) bool { + return commitment.Slot() == 14 + })) + ts.AssertCommitmentsOnChain(ultimateCommitmentsP2, commitment14P2.ID(), nodesP1...) + + // Before the merge we finalize until slot 10 (root commitment=6), so the forking point of the then main chain + // is at slot 6. + ts.AssertCommitmentsOnChain(ultimateCommitmentsP2, ts.CommitmentOfMainEngine(nodesP2[0], 6).ID(), nodesP2...) + + ts.AssertCommitmentsAndChainsEvicted(oldestNonEvictedCommitment-1, ts.Nodes()...) + } } func TestProtocol_EngineSwitching_CommitteeRotation(t *testing.T) { + var ( + genesisSlot iotago.SlotIndex = 0 + minCommittableAge iotago.SlotIndex = 2 + maxCommittableAge iotago.SlotIndex = 5 + ) + ts := testsuite.NewTestSuite(t, testsuite.WithWaitFor(15*time.Second), testsuite.WithProtocolParametersOptions( iotago.WithTimeProviderOptions( - 0, + genesisSlot, testsuite.GenesisTimeWithOffsetBySlots(1000, testsuite.DefaultSlotDurationInSeconds), testsuite.DefaultSlotDurationInSeconds, 4, // 16 slots per epoch @@ -392,8 +437,8 @@ func TestProtocol_EngineSwitching_CommitteeRotation(t *testing.T) { iotago.WithLivenessOptions( 10, 10, - 2, - 5, + minCommittableAge, + maxCommittableAge, 10, ), ), @@ -405,7 +450,7 @@ func TestProtocol_EngineSwitching_CommitteeRotation(t *testing.T) { node2 := ts.AddValidatorNode("node2") node3 := ts.AddValidatorNode("node3") - const expectedCommittedSlotAfterPartitionMerge = 18 + const expectedCommittedSlotAfterPartitionMerge = 19 nodesP1 := []*mock.Node{node0, node1, node2} nodesP2 := []*mock.Node{node3} @@ -441,15 +486,14 @@ func TestProtocol_EngineSwitching_CommitteeRotation(t *testing.T) { // Verify that nodes have the expected states after startup. { - genesisCommitment := iotago.NewEmptyCommitment(ts.API) - genesisCommitment.ReferenceManaCost = ts.API.ProtocolParameters().CongestionControlParameters().MinReferenceManaCost + genesisCommitment := ts.CommitmentOfMainEngine(node0, 0) ts.AssertNodeState(ts.Nodes(), testsuite.WithSnapshotImported(true), testsuite.WithProtocolParameters(ts.API.ProtocolParameters()), - testsuite.WithLatestCommitment(genesisCommitment), + testsuite.WithLatestCommitment(genesisCommitment.Commitment()), testsuite.WithLatestFinalizedSlot(0), - testsuite.WithChainID(genesisCommitment.MustID()), - testsuite.WithStorageCommitments([]*iotago.Commitment{genesisCommitment}), + testsuite.WithMainChainID(genesisCommitment.ID()), + testsuite.WithStorageCommitments([]*iotago.Commitment{genesisCommitment.Commitment()}), testsuite.WithSybilProtectionCommittee(0, ts.AccountsOfNodes("node0", "node1", "node2", "node3")), testsuite.WithSybilProtectionOnlineCommittee(ts.SeatOfNodes(0, "node0", "node1", "node2", "node3")...), @@ -537,8 +581,18 @@ func TestProtocol_EngineSwitching_CommitteeRotation(t *testing.T) { ts.AssertBlocksExist(ts.BlocksWithPrefix("P1"), true, nodesP1...) ts.AssertBlocksExist(ts.BlocksWithPrefix("P1"), false, nodesP2...) + + // Assert Protocol.Chains and Protocol.Commitments state. + engineCommitmentsP1 := ts.CommitmentsOfMainEngine(nodesP1[0], 12, 18) + ts.AssertLatestEngineCommitmentOnMainChain(nodesP1...) + ts.AssertUniqueCommitmentChain(nodesP1...) + ts.AssertCommitmentsOnChainAndChainHasCommitments(engineCommitmentsP1, ts.CommitmentOfMainEngine(node0, 12).ID(), nodesP1...) + ts.AssertCommitmentsOnEvictedChain(engineCommitmentsP1, false, nodesP1...) + ts.AssertCommitmentsAndChainsEvicted(11, nodesP1...) } + var engineCommitmentsP2 []*model.Commitment + // Issue blocks in partition 2. { ts.IssueBlocksAtSlots("P2:", []iotago.SlotIndex{8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}, 4, "P0:7.3", nodesP2, true, false) @@ -578,6 +632,14 @@ func TestProtocol_EngineSwitching_CommitteeRotation(t *testing.T) { ts.AssertBlocksExist(ts.BlocksWithPrefix("P2"), true, nodesP2...) ts.AssertBlocksExist(ts.BlocksWithPrefix("P2"), false, nodesP1...) + + // Assert Protocol.Chains and Protocol.Commitments state. + engineCommitmentsP2 = ts.CommitmentsOfMainEngine(nodesP2[0], 0, 18) + ts.AssertLatestEngineCommitmentOnMainChain(nodesP2...) + ts.AssertUniqueCommitmentChain(nodesP2...) + ts.AssertCommitmentsOnChainAndChainHasCommitments(engineCommitmentsP2, ts.CommitmentOfMainEngine(node0, 0).ID(), nodesP2...) + ts.AssertCommitmentsOnEvictedChain(engineCommitmentsP2, false, nodesP2...) + // We only finalized until slot 4, and maxCommittableAge=5. Thus, we don't expect any evictions on chains/commmitments yet. } // Merge the partitions @@ -585,26 +647,14 @@ func TestProtocol_EngineSwitching_CommitteeRotation(t *testing.T) { ts.MergePartitionsToMain() fmt.Println("\n=========================\nMerged network partitions\n=========================") - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) - defer cancel() - - ctxP1, ctxP1Cancel := context.WithCancel(ctx) - ctxP2, ctxP2Cancel := context.WithCancel(ctx) - - wg := &sync.WaitGroup{} - - // Issue blocks on both partitions after merging the networks. - node0.Validator.IssueActivity(ctxP1, wg, 21, node0) - node1.Validator.IssueActivity(ctxP1, wg, 21, node1) - node2.Validator.IssueActivity(ctxP1, wg, 21, node2) - - node3.Validator.IssueActivity(ctxP2, wg, 21, node3) + ts.IssueBlocksAtSlots("P2-merge:", []iotago.SlotIndex{20, 21}, 4, "P2:20.3", nodesP2, true, true) // P1 finalized until slot 16. We do not expect any forks here because our CW is higher than the other partition's. ts.AssertForkDetectedCount(0, nodesP1...) // P1's chain is heavier, they should not consider switching the chain. ts.AssertCandidateEngineActivatedCount(0, nodesP1...) - ctxP2Cancel() // we can stop issuing on P2. + + ts.IssueBlocksAtSlots("P1-merge:", []iotago.SlotIndex{20, 21, 22}, 4, "P1:20.3", nodesP1, true, true) // Nodes from P2 should switch the chain. ts.AssertForkDetectedCount(1, nodesP2...) @@ -612,12 +662,6 @@ func TestProtocol_EngineSwitching_CommitteeRotation(t *testing.T) { // Here we need to let enough time pass for the nodes to sync up the candidate engines and switch them ts.AssertMainEngineSwitchedCount(1, nodesP2...) - - // Make sure that enough activity messages are issued so that a block in slot 21 gets accepted and triggers commitment of slot 18. - time.Sleep(3 * time.Second) - - ctxP1Cancel() - wg.Wait() } // Make sure that nodes that switched their engine still have blocks with prefix P0 from before the fork. @@ -650,6 +694,33 @@ func TestProtocol_EngineSwitching_CommitteeRotation(t *testing.T) { ts.AssertAttestationsForSlot(17, ts.Blocks("P1:15.3-node0", "P1:17.3-node1", "P1:17.3-node2"), nodesP1...) // Committee in epoch 2 is only node1, node2. Block(P1:15.3-node0) commits to Slot12. ts.AssertAttestationsForSlot(18, ts.Blocks("P1:18.3-node1", "P1:18.3-node2"), nodesP1...) // Committee in epoch 2 is only node1, node2. Block(P1:15.3-node0) commits to Slot12, that's why it is not carried to 18. ts.AssertAttestationsForSlot(19, ts.Blocks("P1:19.3-node1", "P1:19.3-node2"), ts.Nodes()...) // Committee in epoch 2 is only node1, node2 + + ts.AssertLatestFinalizedSlot(19, ts.Nodes()...) + + oldestNonEvictedCommitment := 19 - maxCommittableAge + commitmentsMainChain := ts.CommitmentsOfMainEngine(node0, oldestNonEvictedCommitment, expectedCommittedSlotAfterPartitionMerge) + ultimateCommitmentsP2 := lo.Filter(engineCommitmentsP2, func(commitment *model.Commitment) bool { + return commitment.Slot() >= oldestNonEvictedCommitment + }) + + // Assert Protocol.Chains and Protocol.Commitments state. + { + ts.AssertUniqueCommitmentChain(ts.Nodes()...) + ts.AssertLatestEngineCommitmentOnMainChain(ts.Nodes()...) + ts.AssertCommitmentsOnEvictedChain(ultimateCommitmentsP2, true, ts.Nodes()...) + ts.AssertCommitmentsOnEvictedChain(commitmentsMainChain, false, ts.Nodes()...) + ts.AssertCommitmentsOnChainAndChainHasCommitments(commitmentsMainChain, ts.CommitmentOfMainEngine(nodesP1[0], oldestNonEvictedCommitment).ID(), ts.Nodes()...) + + // Since we diverge in slot 8 and P1 finalized until slot 17 (root commitment=12) before the merge, + // the chain is not solidifiable and there will never be a chain created for nodes on P1. + ts.AssertCommitmentsOnChain(ultimateCommitmentsP2, iotago.EmptyCommitmentID, nodesP1...) + + // After the merge we finalize until slot 19 (root commitment=14), so the chain is evicted (we check this above) + // and therefore i + ts.AssertCommitmentsOnChain(ultimateCommitmentsP2, ts.CommitmentOfMainEngine(node3, 0).ID(), nodesP2...) + + ts.AssertCommitmentsAndChainsEvicted(oldestNonEvictedCommitment-1, ts.Nodes()...) + } } func TestProtocol_EngineSwitching_Tie(t *testing.T) { @@ -695,6 +766,7 @@ func TestProtocol_EngineSwitching_Tie(t *testing.T) { ts.AddDefaultWallet(nodes[0]) const expectedCommittedSlotAfterPartitionMerge = 18 + const forkingSlot = 14 nodeOptions := []options.Option[protocol.Protocol]{ protocol.WithSybilProtectionProvider( @@ -769,7 +841,7 @@ func TestProtocol_EngineSwitching_Tie(t *testing.T) { testsuite.WithProtocolParameters(ts.API.ProtocolParameters()), testsuite.WithLatestCommitment(genesisCommitment), testsuite.WithLatestFinalizedSlot(0), - testsuite.WithChainID(genesisCommitment.MustID()), + testsuite.WithMainChainID(genesisCommitment.MustID()), testsuite.WithStorageCommitments([]*iotago.Commitment{genesisCommitment}), testsuite.WithSybilProtectionCommittee(0, expectedCommittee), @@ -876,6 +948,14 @@ func TestProtocol_EngineSwitching_Tie(t *testing.T) { issueBlocks(0, []iotago.SlotIndex{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13}) + ts.AssertUniqueCommitmentChain(ts.Nodes()...) + ts.AssertLatestEngineCommitmentOnMainChain(ts.Nodes()...) + + commitmentsMainChain := ts.CommitmentsOfMainEngine(nodes[0], 6, 11) + ts.AssertCommitmentsOnEvictedChain(commitmentsMainChain, false, ts.Nodes()...) + ts.AssertCommitmentsOnChainAndChainHasCommitments(commitmentsMainChain, commitmentsMainChain[0].ID(), ts.Nodes()...) + ts.AssertCommitmentsAndChainsEvicted(5, ts.Nodes()...) + // Split into partitions P1, P2 and P3. ts.SplitIntoPartitions(map[string][]*mock.Node{ "P1": {nodes[0]}, @@ -906,23 +986,26 @@ func TestProtocol_EngineSwitching_Tie(t *testing.T) { issueBlocks(2, []iotago.SlotIndex{14, 15, 16, 17, 18, 19, 20}) issueBlocks(3, []iotago.SlotIndex{14, 15, 16, 17, 18, 19, 20}) - commitment140, _ := nodes[0].Protocol.Chains.Main.Get().Commitment(14) - commitment141, _ := nodes[1].Protocol.Chains.Main.Get().Commitment(14) - commitment142, _ := nodes[2].Protocol.Chains.Main.Get().Commitment(14) + ts.AssertUniqueCommitmentChain(ts.Nodes()...) + ts.AssertLatestEngineCommitmentOnMainChain(ts.Nodes()...) + + commitment140 := ts.CommitmentOfMainEngine(nodes[0], 14) + commitment141 := ts.CommitmentOfMainEngine(nodes[1], 14) + commitment142 := ts.CommitmentOfMainEngine(nodes[2], 14) var mainPartition []*mock.Node var otherPartitions []*mock.Node - switch commitmentWithLargestID(commitment140, commitment141, commitment142) { - case commitment140: - mainPartition = nodes[0:1] - otherPartitions = []*mock.Node{nodes[1], nodes[2]} - case commitment141: - mainPartition = nodes[1:2] - otherPartitions = []*mock.Node{nodes[0], nodes[2]} - case commitment142: - mainPartition = nodes[2:3] - otherPartitions = []*mock.Node{nodes[0], nodes[1]} - } + + partitionsInOrder := []*types.Tuple[int, *model.Commitment]{types.NewTuple(1, commitment140), types.NewTuple(2, commitment141), types.NewTuple(3, commitment142)} + slices.SortFunc(partitionsInOrder, func(a, b *types.Tuple[int, *model.Commitment]) int { + return bytes.Compare(lo.PanicOnErr(a.B.ID().Bytes()), lo.PanicOnErr(b.B.ID().Bytes())) + }) + + mainPartition = nodes[partitionsInOrder[2].A-1 : partitionsInOrder[2].A] + otherPartitions = []*mock.Node{nodes[partitionsInOrder[0].A-1], nodes[partitionsInOrder[1].A-1]} + + engineCommitmentsP2 := ts.CommitmentsOfMainEngine(otherPartitions[1], lastCommonSlot+1, 18) + engineCommitmentsP3 := ts.CommitmentsOfMainEngine(otherPartitions[0], lastCommonSlot+1, 18) // Merge the partitions { @@ -938,29 +1021,98 @@ func TestProtocol_EngineSwitching_Tie(t *testing.T) { ts.MergePartitionsToMain() } - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) - defer cancel() + // Make sure the nodes switch their engines. + { + ts.IssueBlocksAtSlots(fmt.Sprintf("P%d-merge:", partitionsInOrder[0].A), []iotago.SlotIndex{20}, 1, slotPrefix(partitionsInOrder[0].A, 20)+strconv.Itoa(20)+".3", nodes[partitionsInOrder[0].A-1:partitionsInOrder[0].A], true, true) + ts.AssertCommitmentsOnChainAndChainHasCommitments(engineCommitmentsP3, engineCommitmentsP3[0].ID(), mainPartition[0], otherPartitions[1]) - ctxP1, ctxP1Cancel := context.WithCancel(ctx) - ctxP2, ctxP2Cancel := context.WithCancel(ctx) - ctxP3, ctxP3Cancel := context.WithCancel(ctx) + ts.IssueBlocksAtSlots(fmt.Sprintf("P%d-merge:", partitionsInOrder[1].A), []iotago.SlotIndex{20}, 1, slotPrefix(partitionsInOrder[1].A, 20)+strconv.Itoa(20)+".3", nodes[partitionsInOrder[1].A-1:partitionsInOrder[1].A], true, true) + ts.AssertMainEngineSwitchedCount(1, otherPartitions[0]) + ts.AssertCommitmentsOnChainAndChainHasCommitments(engineCommitmentsP3, engineCommitmentsP3[0].ID(), mainPartition[0], otherPartitions[1]) + ts.AssertCommitmentsOnChainAndChainHasCommitments(engineCommitmentsP2, engineCommitmentsP2[0].ID(), mainPartition[0], otherPartitions[0]) - wg := &sync.WaitGroup{} + ts.IssueBlocksAtSlots(fmt.Sprintf("P%d-merge:", partitionsInOrder[2].A), []iotago.SlotIndex{20}, 1, slotPrefix(partitionsInOrder[2].A, 20)+strconv.Itoa(20)+".3", nodes[partitionsInOrder[2].A-1:partitionsInOrder[2].A], true, true) + ts.AssertCommitmentsOnChainAndChainHasCommitments(engineCommitmentsP3, engineCommitmentsP3[0].ID(), mainPartition[0], otherPartitions[1]) + ts.AssertCommitmentsOnChainAndChainHasCommitments(engineCommitmentsP2, engineCommitmentsP2[0].ID(), mainPartition[0], otherPartitions[0]) + ts.AssertCommitmentsOnChainAndChainHasCommitments(commitmentsMainChain, commitmentsMainChain[0].ID(), otherPartitions...) - // Issue blocks on both partitions after merging the networks. - nodes[0].Validator.IssueActivity(ctxP1, wg, 21, nodes[0]) - nodes[1].Validator.IssueActivity(ctxP2, wg, 21, nodes[1]) - nodes[2].Validator.IssueActivity(ctxP3, wg, 21, nodes[2]) + ts.AssertMainEngineSwitchedCount(0, mainPartition...) + ts.AssertMainEngineSwitchedCount(2, otherPartitions[0]) + ts.AssertMainEngineSwitchedCount(1, otherPartitions[1]) - ts.AssertMainEngineSwitchedCount(0, mainPartition...) - ts.AssertMainEngineSwitchedCount(1, otherPartitions...) + ts.AssertEqualStoredCommitmentAtIndex(expectedCommittedSlotAfterPartitionMerge, ts.Nodes()...) + } - ctxP1Cancel() - ctxP2Cancel() - ctxP3Cancel() - wg.Wait() + oldestNonEvictedCommitment := iotago.SlotIndex(6) - ts.AssertEqualStoredCommitmentAtIndex(expectedCommittedSlotAfterPartitionMerge, ts.Nodes()...) + commitmentsMainChain = ts.CommitmentsOfMainEngine(mainPartition[0], oldestNonEvictedCommitment, expectedCommittedSlotAfterPartitionMerge) + ultimateCommitmentsP2 := lo.Filter(engineCommitmentsP2, func(commitment *model.Commitment) bool { + return commitment.Slot() >= forkingSlot + }) + ultimateCommitmentsP3 := lo.Filter(engineCommitmentsP3, func(commitment *model.Commitment) bool { + return commitment.Slot() >= forkingSlot + }) + + { + ts.AssertUniqueCommitmentChain(ts.Nodes()...) + + ts.AssertMainChain(ts.CommitmentOfMainEngine(mainPartition[0], 6).ID(), mainPartition...) + ts.AssertMainChain(ts.CommitmentOfMainEngine(mainPartition[0], forkingSlot).ID(), otherPartitions...) + + ts.AssertLatestEngineCommitmentOnMainChain(ts.Nodes()...) + + // We have not evicted the slot below the forking point, so chains are not yet orphaned. + ts.AssertCommitmentsOnEvictedChain(commitmentsMainChain, false, ts.Nodes()...) + ts.AssertCommitmentsOnEvictedChain(ultimateCommitmentsP2, false, ts.Nodes()...) + ts.AssertCommitmentsOnEvictedChain(ultimateCommitmentsP3, false, ts.Nodes()...) + + // The Main partition should have all commitments on the old chain, because it did not switch chains. + ts.AssertCommitmentsOnChainAndChainHasCommitments(commitmentsMainChain, ts.CommitmentOfMainEngine(mainPartition[0], oldestNonEvictedCommitment).ID(), mainPartition...) + // Pre-fork commitments should be on the old chains on other partitions. + ts.AssertCommitmentsOnChainAndChainHasCommitments(commitmentsMainChain[:8], ts.CommitmentOfMainEngine(otherPartitions[0], oldestNonEvictedCommitment).ID(), otherPartitions...) + // Post-fork winning commitments should be on the new chains on other partitions. This chain is the new main one. + ts.AssertCommitmentsOnChainAndChainHasCommitments(commitmentsMainChain[8:], ts.CommitmentOfMainEngine(otherPartitions[0], forkingSlot).ID(), otherPartitions...) + + // P2 commitments on the main partition should be on its own chain. + ts.AssertCommitmentsOnChainAndChainHasCommitments(ultimateCommitmentsP2, ultimateCommitmentsP2[0].ID(), mainPartition...) + + // P2 commitments on P2 node should be on the old chain, that is not the main chain anymore. + ts.AssertCommitmentsOnChainAndChainHasCommitments(ultimateCommitmentsP2, ts.CommitmentOfMainEngine(otherPartitions[1], oldestNonEvictedCommitment).ID(), otherPartitions[1]) + // P2 commitments on P3 node should be on separate chain. + ts.AssertCommitmentsOnChainAndChainHasCommitments(ultimateCommitmentsP2, ultimateCommitmentsP2[0].ID(), otherPartitions[0]) + + // P3 commitments on the main partition should be on its own chain. + ts.AssertCommitmentsOnChainAndChainHasCommitments(ultimateCommitmentsP3, ultimateCommitmentsP3[0].ID(), mainPartition...) + // P3 commitments on P3 node should be on the old chain, that is not the main chain anymore. + ts.AssertCommitmentsOnChainAndChainHasCommitments(ultimateCommitmentsP3, ts.CommitmentOfMainEngine(otherPartitions[0], oldestNonEvictedCommitment).ID(), otherPartitions[0]) + // P3 commitments on P2 node should be on separate chain. + ts.AssertCommitmentsOnChainAndChainHasCommitments(ultimateCommitmentsP3, ultimateCommitmentsP3[0].ID(), otherPartitions[1]) + + ts.AssertCommitmentsAndChainsEvicted(5, ts.Nodes()...) + } + + // Finalize further slot and make sure the nodes have the same state of chains. + { + ts.IssueBlocksAtSlots("P0-merge:", []iotago.SlotIndex{20, 21, 22}, 3, slotPrefix(partitionsInOrder[len(partitionsInOrder)-1].A, 20)+strconv.Itoa(20)+".2", ts.Nodes(), true, true) + + oldestNonEvictedCommitment = 19 - maxCommittableAge + commitmentsMainChain = ts.CommitmentsOfMainEngine(mainPartition[0], oldestNonEvictedCommitment, 20) + + ts.AssertLatestFinalizedSlot(19, ts.Nodes()...) + + ts.AssertMainChain(ts.CommitmentOfMainEngine(mainPartition[0], forkingSlot+1).ID(), ts.Nodes()...) + + ts.AssertUniqueCommitmentChain(ts.Nodes()...) + ts.AssertLatestEngineCommitmentOnMainChain(ts.Nodes()...) + ts.AssertCommitmentsAndChainsEvicted(forkingSlot, ts.Nodes()...) + + ts.AssertCommitmentsOnEvictedChain(commitmentsMainChain, false, ts.Nodes()...) + ts.AssertCommitmentsOnChainAndChainHasCommitments(commitmentsMainChain, ts.CommitmentOfMainEngine(mainPartition[len(mainPartition)-1], oldestNonEvictedCommitment).ID(), mainPartition...) + + // The oldest commitment is in the slices are should already be evicted, so we only need to check the newer ones. + ts.AssertCommitmentsOnEvictedChain(ultimateCommitmentsP2[2:], true, ts.Nodes()...) + ts.AssertCommitmentsOnEvictedChain(ultimateCommitmentsP3[2:], true, ts.Nodes()...) + } } type Blocks []*blocks.Block @@ -977,8 +1129,8 @@ func slotPrefix(partition int, slot iotago.SlotIndex) string { return "P" + strconv.Itoa(partition) + ":" } -func commitmentWithLargestID(commitments ...*protocol.Commitment) *protocol.Commitment { - var largestCommitment *protocol.Commitment +func commitmentWithLargestID(commitments ...*model.Commitment) *model.Commitment { + var largestCommitment *model.Commitment for _, commitment := range commitments { if largestCommitment == nil || bytes.Compare(lo.PanicOnErr(commitment.ID().Bytes()), lo.PanicOnErr(largestCommitment.ID().Bytes())) > 0 { largestCommitment = commitment diff --git a/pkg/tests/protocol_eviction_test.go b/pkg/tests/protocol_eviction_test.go new file mode 100644 index 000000000..a5f0aac51 --- /dev/null +++ b/pkg/tests/protocol_eviction_test.go @@ -0,0 +1,190 @@ +package tests + +import ( + "strconv" + "testing" + "time" + + "github.com/fjl/memsize" + "github.com/stretchr/testify/require" + + "github.com/iotaledger/hive.go/core/eventticker" + "github.com/iotaledger/hive.go/ds" + "github.com/iotaledger/hive.go/lo" + "github.com/iotaledger/hive.go/runtime/module" + "github.com/iotaledger/hive.go/runtime/options" + "github.com/iotaledger/iota-core/pkg/protocol" + "github.com/iotaledger/iota-core/pkg/protocol/engine" + "github.com/iotaledger/iota-core/pkg/protocol/engine/blocks" + "github.com/iotaledger/iota-core/pkg/protocol/engine/syncmanager/trivialsyncmanager" + "github.com/iotaledger/iota-core/pkg/protocol/sybilprotection/seatmanager" + mock2 "github.com/iotaledger/iota-core/pkg/protocol/sybilprotection/seatmanager/mock" + "github.com/iotaledger/iota-core/pkg/protocol/sybilprotection/sybilprotectionv1" + "github.com/iotaledger/iota-core/pkg/storage" + "github.com/iotaledger/iota-core/pkg/testsuite" + "github.com/iotaledger/iota-core/pkg/testsuite/mock" + iotago "github.com/iotaledger/iota.go/v4" +) + +func TestProtocol_Eviction(t *testing.T) { + if testing.Short() { + t.Skip("Skipping test in -race mode.") + } + + var ( + genesisSlot iotago.SlotIndex = 0 + minCommittableAge iotago.SlotIndex = 2 + maxCommittableAge iotago.SlotIndex = 4 + ) + + ts := testsuite.NewTestSuite(t, + testsuite.WithProtocolParametersOptions( + iotago.WithTimeProviderOptions( + genesisSlot, + testsuite.GenesisTimeWithOffsetBySlots(1000, testsuite.DefaultSlotDurationInSeconds), + testsuite.DefaultSlotDurationInSeconds, + 3, + ), + iotago.WithLivenessOptions( + 10, + 10, + minCommittableAge, + maxCommittableAge, + 5, + ), + ), + + testsuite.WithWaitFor(15*time.Second), + ) + defer ts.Shutdown() + + node := ts.AddValidatorNode("node0") + + ts.Run(false, map[string][]options.Option[protocol.Protocol]{ + "node0": []options.Option[protocol.Protocol]{ + protocol.WithSybilProtectionProvider( + sybilprotectionv1.NewProvider( + sybilprotectionv1.WithSeatManagerProvider(module.Provide(func(e *engine.Engine) seatmanager.SeatManager { + poa := mock2.NewManualPOAProvider()(e).(*mock2.ManualPOA) + poa.AddAccount(node.Validator.AccountID, node.Name) + + onlineValidators := ds.NewSet[string]() + + e.Constructed.OnTrigger(func() { + e.Events.BlockDAG.BlockAttached.Hook(func(block *blocks.Block) { + if block.ModelBlock().ProtocolBlock().Header.IssuerID == node.Validator.AccountID && onlineValidators.Add(node.Name) { + e.LogError("node online", "name", node.Name) + poa.SetOnline(onlineValidators.ToSlice()...) + } + }) + }) + + return poa + })), + ), + ), + + protocol.WithEngineOptions( + engine.WithBlockRequesterOptions( + eventticker.RetryInterval[iotago.SlotIndex, iotago.BlockID](1*time.Second), + eventticker.RetryJitter[iotago.SlotIndex, iotago.BlockID](500*time.Millisecond), + ), + ), + + protocol.WithSyncManagerProvider( + trivialsyncmanager.NewProvider( + trivialsyncmanager.WithBootstrappedFunc(func(e *engine.Engine) bool { + return e.Notarization.IsBootstrapped() + }), + ), + ), + + protocol.WithStorageOptions( + storage.WithPruningDelay(20), + ), + }, + }) + + node.Protocol.Engines.Main.Get().SybilProtection.SeatManager().(*mock2.ManualPOA).SetOnline("node0") + + // Verify that nodes have the expected states. + { + genesisCommitment := iotago.NewEmptyCommitment(ts.API) + genesisCommitment.ReferenceManaCost = ts.API.ProtocolParameters().CongestionControlParameters().MinReferenceManaCost + ts.AssertNodeState(ts.Nodes(), + testsuite.WithSnapshotImported(true), + testsuite.WithProtocolParameters(ts.API.ProtocolParameters()), + testsuite.WithLatestCommitment(genesisCommitment), + testsuite.WithLatestFinalizedSlot(0), + testsuite.WithMainChainID(genesisCommitment.MustID()), + testsuite.WithStorageCommitments([]*iotago.Commitment{genesisCommitment}), + + testsuite.WithSybilProtectionCommittee(0, []iotago.AccountID{node.Validator.AccountID}), + testsuite.WithEvictedSlot(0), + testsuite.WithActiveRootBlocks(ts.Blocks("Genesis")), + testsuite.WithStorageRootBlocks(ts.Blocks("Genesis")), + ) + } + + issueBlocks := func(slots []iotago.SlotIndex) { + parentSlot := slots[0] - 1 + lastIssuedSlot := slots[len(slots)-1] + lastCommittedSlot := lastIssuedSlot - minCommittableAge + + initialParentsPrefix := "P0:" + strconv.Itoa(int(parentSlot)) + ".3" + if parentSlot == genesisSlot { + initialParentsPrefix = "Genesis" + } + + ts.IssueBlocksAtSlots("P0:", slots, 4, initialParentsPrefix, []*mock.Node{node}, true, true) + + cumulativeAttestations := uint64(0) + for slot := genesisSlot + maxCommittableAge; slot <= lastCommittedSlot; slot++ { + var attestationBlocks Blocks + attestationBlocks.Add(ts, node, 0, slot) + + cumulativeAttestations++ + + ts.AssertAttestationsForSlot(slot, attestationBlocks, node) + } + + ts.AssertNodeState([]*mock.Node{node}, + testsuite.WithLatestFinalizedSlot(lastCommittedSlot-1), + testsuite.WithLatestCommitmentSlotIndex(lastCommittedSlot), + testsuite.WithEqualStoredCommitmentAtIndex(lastCommittedSlot), + testsuite.WithLatestCommitmentCumulativeWeight(cumulativeAttestations), + testsuite.WithSybilProtectionCommittee(ts.API.TimeProvider().EpochFromSlot(lastCommittedSlot), []iotago.AccountID{node.Validator.AccountID}), + testsuite.WithEvictedSlot(lastCommittedSlot), + ) + + var tipBlocks Blocks + tipBlocks.Add(ts, node, 0, lastIssuedSlot) + + ts.AssertStrongTips(tipBlocks, node) + } + + lastIssuedSlot := iotago.SlotIndex(0) + + issueBlocksTill := func(slot iotago.SlotIndex) { + slotsToIssue := make([]iotago.SlotIndex, slot-lastIssuedSlot) + for currentSlot := lastIssuedSlot + 1; currentSlot <= slot; currentSlot++ { + slotsToIssue[currentSlot-lastIssuedSlot-1] = currentSlot + } + + issueBlocks(slotsToIssue) + + lastIssuedSlot = slot + } + + // issue blocks until we evict the first slot + issueBlocksTill(8) + + memConsumptionStart := memsize.Scan(node.Protocol).Total + + // issue more blocks + issueBlocksTill(100) + + memConsumptionEnd := memsize.Scan(node.Protocol).Total + + require.Less(t, float64(lo.Return1(memConsumptionEnd)), 1.05*float64(memConsumptionStart), "memory consumption should not grow by more than 5%") +} diff --git a/pkg/tests/protocol_startup_test.go b/pkg/tests/protocol_startup_test.go index c9e86a652..346452614 100644 --- a/pkg/tests/protocol_startup_test.go +++ b/pkg/tests/protocol_startup_test.go @@ -22,10 +22,13 @@ import ( ) func Test_BookInCommittedSlot(t *testing.T) { + const maxCommittableAge = iotago.SlotIndex(4) + const genesisSlot = iotago.SlotIndex(0) + ts := testsuite.NewTestSuite(t, testsuite.WithProtocolParametersOptions( iotago.WithTimeProviderOptions( - 0, + genesisSlot, testsuite.GenesisTimeWithOffsetBySlots(1000, testsuite.DefaultSlotDurationInSeconds), testsuite.DefaultSlotDurationInSeconds, 3, @@ -34,7 +37,7 @@ func Test_BookInCommittedSlot(t *testing.T) { 10, 10, 2, - 4, + maxCommittableAge, 5, ), ), @@ -64,15 +67,14 @@ func Test_BookInCommittedSlot(t *testing.T) { } // Verify that nodes have the expected states. - genesisCommitment := iotago.NewEmptyCommitment(ts.API) - genesisCommitment.ReferenceManaCost = ts.API.ProtocolParameters().CongestionControlParameters().MinReferenceManaCost + genesisCommitment := ts.CommitmentOfMainEngine(nodeA, genesisSlot) ts.AssertNodeState(ts.Nodes(), testsuite.WithSnapshotImported(true), testsuite.WithProtocolParameters(ts.API.ProtocolParameters()), - testsuite.WithLatestCommitment(genesisCommitment), + testsuite.WithLatestCommitment(genesisCommitment.Commitment()), testsuite.WithLatestFinalizedSlot(0), - testsuite.WithChainID(genesisCommitment.MustID()), - testsuite.WithStorageCommitments([]*iotago.Commitment{genesisCommitment}), + testsuite.WithMainChainID(genesisCommitment.ID()), + testsuite.WithStorageCommitments([]*iotago.Commitment{genesisCommitment.Commitment()}), testsuite.WithSybilProtectionCommittee(0, expectedCommittee), testsuite.WithSybilProtectionOnlineCommittee(expectedOnlineCommittee...), testsuite.WithEvictedSlot(0), @@ -84,7 +86,7 @@ func Test_BookInCommittedSlot(t *testing.T) { // Epoch 0: issue 4 rows per slot. { - ts.IssueBlocksAtEpoch("", 0, 4, "Genesis", ts.Nodes(), true, false) + ts.IssueBlocksAtEpoch("", 0, 4, "Genesis", ts.Nodes(), true, true) ts.AssertBlocksExist(ts.BlocksWithPrefixes("1", "2", "3", "4", "5", "6", "7"), true, ts.Nodes()...) @@ -102,7 +104,8 @@ func Test_BookInCommittedSlot(t *testing.T) { ts.AssertNodeState(ts.Nodes(), testsuite.WithSnapshotImported(true), testsuite.WithProtocolParameters(ts.API.ProtocolParameters()), - testsuite.WithChainID(genesisCommitment.MustID()), + testsuite.WithLatestFinalizedSlot(4), + testsuite.WithMainChainID(ts.CommitmentOfMainEngine(nodeA, 4-maxCommittableAge).ID()), testsuite.WithLatestCommitmentSlotIndex(5), testsuite.WithEvictedSlot(5), testsuite.WithActiveRootBlocks(expectedActiveRootBlocks), @@ -124,10 +127,13 @@ func Test_BookInCommittedSlot(t *testing.T) { } func Test_StartNodeFromSnapshotAndDisk(t *testing.T) { + const maxCommittableAge = iotago.SlotIndex(4) + const genesisSlot = iotago.SlotIndex(0) + ts := testsuite.NewTestSuite(t, testsuite.WithProtocolParametersOptions( iotago.WithTimeProviderOptions( - 0, + genesisSlot, testsuite.GenesisTimeWithOffsetBySlots(1000, testsuite.DefaultSlotDurationInSeconds), testsuite.DefaultSlotDurationInSeconds, 3, @@ -136,7 +142,7 @@ func Test_StartNodeFromSnapshotAndDisk(t *testing.T) { 10, 10, 2, - 4, + maxCommittableAge, 5, ), ), @@ -178,15 +184,14 @@ func Test_StartNodeFromSnapshotAndDisk(t *testing.T) { } // Verify that nodes have the expected states. - genesisCommitment := iotago.NewEmptyCommitment(ts.API) - genesisCommitment.ReferenceManaCost = ts.API.ProtocolParameters().CongestionControlParameters().MinReferenceManaCost + genesisCommitment := ts.CommitmentOfMainEngine(nodeA, genesisSlot) ts.AssertNodeState(ts.Nodes(), testsuite.WithSnapshotImported(true), testsuite.WithProtocolParameters(ts.API.ProtocolParameters()), - testsuite.WithLatestCommitment(genesisCommitment), + testsuite.WithLatestCommitment(genesisCommitment.Commitment()), testsuite.WithLatestFinalizedSlot(0), - testsuite.WithChainID(genesisCommitment.MustID()), - testsuite.WithStorageCommitments([]*iotago.Commitment{genesisCommitment}), + testsuite.WithMainChainID(genesisCommitment.ID()), + testsuite.WithStorageCommitments([]*iotago.Commitment{genesisCommitment.Commitment()}), testsuite.WithSybilProtectionCommittee(0, expectedCommittee), testsuite.WithSybilProtectionOnlineCommittee(expectedOnlineCommittee...), testsuite.WithEvictedSlot(0), @@ -222,7 +227,7 @@ func Test_StartNodeFromSnapshotAndDisk(t *testing.T) { ts.AssertNodeState(ts.Nodes(), testsuite.WithSnapshotImported(true), testsuite.WithProtocolParameters(ts.API.ProtocolParameters()), - testsuite.WithChainID(genesisCommitment.MustID()), + testsuite.WithMainChainID(ts.CommitmentOfMainEngine(nodeA, 4-maxCommittableAge).ID()), testsuite.WithLatestFinalizedSlot(4), testsuite.WithLatestCommitmentSlotIndex(5), testsuite.WithEqualStoredCommitmentAtIndex(5), @@ -270,7 +275,7 @@ func Test_StartNodeFromSnapshotAndDisk(t *testing.T) { ts.AssertNodeState(ts.Nodes(), testsuite.WithSnapshotImported(true), testsuite.WithProtocolParameters(ts.API.ProtocolParameters()), - testsuite.WithChainID(genesisCommitment.MustID()), + testsuite.WithMainChainID(ts.CommitmentOfMainEngine(nodeA, 11-maxCommittableAge).ID()), testsuite.WithLatestFinalizedSlot(11), testsuite.WithLatestCommitmentSlotIndex(11), testsuite.WithEqualStoredCommitmentAtIndex(11), @@ -349,12 +354,10 @@ func Test_StartNodeFromSnapshotAndDisk(t *testing.T) { ts.AssertStorageRootBlocks(expectedStorageRootBlocksFrom9, ts.Nodes("nodeD")...) } - slot7Commitment := lo.PanicOnErr(nodeA.Protocol.Engines.Main.Get().Storage.Commitments().Load(7)) - ts.AssertNodeState(ts.Nodes("nodeC-restarted", "nodeD"), testsuite.WithSnapshotImported(true), testsuite.WithProtocolParameters(ts.API.ProtocolParameters()), - testsuite.WithChainID(slot7Commitment.ID()), + testsuite.WithMainChainID(ts.CommitmentOfMainEngine(nodeA, 11-maxCommittableAge).ID()), testsuite.WithLatestFinalizedSlot(11), testsuite.WithLatestCommitmentSlotIndex(11), testsuite.WithEqualStoredCommitmentAtIndex(11), @@ -412,6 +415,7 @@ func Test_StartNodeFromSnapshotAndDisk(t *testing.T) { testsuite.WithSnapshotImported(true), testsuite.WithProtocolParameters(ts.API.ProtocolParameters()), testsuite.WithLatestFinalizedSlot(36), + testsuite.WithMainChainID(ts.CommitmentOfMainEngine(nodeA, 36-maxCommittableAge).ID()), testsuite.WithLatestCommitmentSlotIndex(37), testsuite.WithEqualStoredCommitmentAtIndex(37), testsuite.WithLatestCommitmentCumulativeWeight(68), // 2 for each slot starting from 4 diff --git a/pkg/testsuite/chainmanager.go b/pkg/testsuite/chainmanager.go deleted file mode 100644 index 18d54946a..000000000 --- a/pkg/testsuite/chainmanager.go +++ /dev/null @@ -1,28 +0,0 @@ -package testsuite - -import ( - "github.com/iotaledger/hive.go/ierrors" - "github.com/iotaledger/iota-core/pkg/testsuite/mock" -) - -func (t *TestSuite) AssertChainManagerIsSolid(nodes ...*mock.Node) { - mustNodes(nodes) - - for _, node := range nodes { - t.Eventually(func() error { - chain := node.Protocol.Chains.Main.Get() - if chain == nil { - return ierrors.Errorf("AssertChainManagerIsSolid: %s: chain is nil", node.Name) - } - - latestChainCommitment := chain.LatestCommitment.Get() - latestCommitment := node.Protocol.Engines.Main.Get().SyncManager.LatestCommitment() - - if latestCommitment.ID() != latestChainCommitment.ID() { - return ierrors.Errorf("AssertChainManagerIsSolid: %s: latest commitment is not equal, expected %s, got %s", node.Name, latestCommitment.ID(), latestChainCommitment.ID()) - } - - return nil - }) - } -} diff --git a/pkg/testsuite/chains.go b/pkg/testsuite/chains.go new file mode 100644 index 000000000..ace98cba5 --- /dev/null +++ b/pkg/testsuite/chains.go @@ -0,0 +1,245 @@ +package testsuite + +import ( + "github.com/iotaledger/hive.go/ds/shrinkingmap" + "github.com/iotaledger/hive.go/ierrors" + "github.com/iotaledger/iota-core/pkg/model" + "github.com/iotaledger/iota-core/pkg/protocol" + "github.com/iotaledger/iota-core/pkg/testsuite/mock" + iotago "github.com/iotaledger/iota.go/v4" +) + +func (t *TestSuite) AssertLatestEngineCommitmentOnMainChain(nodes ...*mock.Node) { + mustNodes(nodes) + + for _, node := range nodes { + t.Eventually(func() error { + chain := node.Protocol.Chains.Main.Get() + if chain == nil { + return ierrors.Errorf("AssertLatestEngineCommitmentOnMainChain: %s: chain is nil", node.Name) + } + + latestChainCommitment := chain.LatestCommitment.Get() + latestCommitment := node.Protocol.Engines.Main.Get().SyncManager.LatestCommitment() + + if latestCommitment.ID() != latestChainCommitment.ID() { + return ierrors.Errorf("AssertLatestEngineCommitmentOnMainChain: %s: latest commitment is not equal, expected %s, got %s", node.Name, latestCommitment.ID(), latestChainCommitment.ID()) + } + + return nil + }) + } +} + +func (t *TestSuite) AssertCommitmentsOnChainAndChainHasCommitments(expectedCommitments []*model.Commitment, chainID iotago.CommitmentID, nodes ...*mock.Node) { + mustNodes(nodes) + + for _, node := range nodes { + t.Eventually(func() error { + var selectedChain *protocol.Chain + _ = node.Protocol.Chains.Set.ForEach(func(chain *protocol.Chain) error { + if forkingPoint := chain.ForkingPoint.Get(); forkingPoint != nil && forkingPoint.ID() == chainID { + selectedChain = chain + } + + return nil + }) + + if chainID != iotago.EmptyCommitmentID && selectedChain == nil { + return ierrors.Errorf("AssertCommitmentsOnChainAndChainHasCommitments: %s: chain with forking point %s not found", node.Name, chainID) + } + + for _, expectedCommitment := range expectedCommitments { + // Check that passed commitments have the correct chain assigned. + { + protocolCommitment, err := node.Protocol.Commitments.Get(expectedCommitment.ID(), false) + if err != nil { + return ierrors.Wrapf(err, "AssertCommitmentsOnChainAndChainHasCommitments: %s: expected commitment %s on chain %s not found", node.Name, expectedCommitment.ID(), chainID) + } + + if protocolCommitment.Chain.Get() != selectedChain { + if selectedChain == nil { + return ierrors.Errorf("AssertCommitmentsOnChainAndChainHasCommitments: %s: commitment %s not on correct chain, expected nil, got %s (pointer: %p, name: %s)", node.Name, expectedCommitment.ID(), protocolCommitment.Chain.Get().ForkingPoint.Get().ID(), protocolCommitment.Chain.Get(), protocolCommitment.Chain.Get().LogName()) + } + + return ierrors.Errorf("AssertCommitmentsOnChainAndChainHasCommitments: %s: commitment %s not on correct chain, expected %s (pointer: %p, name: %s), got %s (pointer: %p, name: %s)", node.Name, expectedCommitment.ID(), chainID, selectedChain, selectedChain.LogName(), protocolCommitment.Chain.Get().ForkingPoint.Get().ID(), protocolCommitment.Chain.Get(), protocolCommitment.Chain.Get().LogName()) + } + } + + // Check that the chain has correct commitments assigned in its metadata. + if selectedChain != nil { + commitment, exists := selectedChain.Commitment(expectedCommitment.Slot()) + if !exists { + return ierrors.Errorf("AssertCommitmentsOnChainAndChainHasCommitments: %s: commitment for slot %d does not exist on the selected chain %s", node.Name, expectedCommitment.Slot(), chainID) + } + + if expectedCommitment.ID() != commitment.ID() { + return ierrors.Errorf("AssertCommitmentsOnChainAndChainHasCommitments: %s: commitment on chain does not match, expected %s, got %s", node.Name, expectedCommitment, commitment.ID()) + } + } + } + + return nil + }) + } +} + +func (t *TestSuite) AssertCommitmentsOnChain(expectedCommitments []*model.Commitment, expectedChainID iotago.CommitmentID, nodes ...*mock.Node) { + mustNodes(nodes) + + for _, node := range nodes { + t.Eventually(func() error { + for _, expectedCommitment := range expectedCommitments { + // Check that passed commitments have the correct chain assigned. + { + protocolCommitment, err := node.Protocol.Commitments.Get(expectedCommitment.ID(), false) + if err != nil { + return ierrors.Wrapf(err, "AssertCommitmentsOnChainAndChainHasCommitments: %s: expected commitment %s on chain %s not found", node.Name, expectedCommitment.ID(), expectedChainID) + } + + if protocolCommitment.Chain.Get() == nil { + if expectedChainID != iotago.EmptyCommitmentID { + return ierrors.Errorf("AssertCommitmentsOnChainAndChainHasCommitments: %s: commitment %s (name: %s) not on correct chain, expected %s, got nil", node.Name, expectedCommitment.ID(), protocolCommitment.LogName(), expectedChainID) + } + } else { + if expectedChainID != protocolCommitment.Chain.Get().ForkingPoint.Get().ID() { + return ierrors.Errorf("AssertCommitmentsOnChainAndChainHasCommitments: %s: commitment %s not on correct chain, expected %s, got %s (pointer: %p, name: %s)", node.Name, expectedCommitment.ID(), expectedChainID, protocolCommitment.Chain.Get().ForkingPoint.Get().ID(), protocolCommitment.Chain.Get(), protocolCommitment.Chain.Get().LogName()) + } + } + } + } + + return nil + }) + } +} + +func (t *TestSuite) AssertUniqueCommitmentChain(nodes ...*mock.Node) { + mustNodes(nodes) + + for _, node := range nodes { + t.Eventually(func() error { + commitmentCountPerChain := shrinkingmap.New[*protocol.Chain, *shrinkingmap.ShrinkingMap[iotago.SlotIndex, []iotago.CommitmentID]]() + _ = node.Protocol.Commitments.ForEach(func(commitment *protocol.Commitment) error { + // Orphaned commitments have chain set to nil, we want to ignore them in this check. + if commitment.Chain.Get() == nil { + return nil + } + + commitmentCountForChain, _ := commitmentCountPerChain.GetOrCreate(commitment.Chain.Get(), func() *shrinkingmap.ShrinkingMap[iotago.SlotIndex, []iotago.CommitmentID] { + return shrinkingmap.New[iotago.SlotIndex, []iotago.CommitmentID]() + }) + + commitmentCountForChain.Compute(commitment.Slot(), func(currentValue []iotago.CommitmentID, _ bool) []iotago.CommitmentID { + return append(currentValue, commitment.ID()) + }) + + return nil + }) + + incorrectCommitments := make(map[iotago.CommitmentID][]iotago.CommitmentID) + commitmentCountPerChain.ForEach(func(chain *protocol.Chain, commitmentCountForChain *shrinkingmap.ShrinkingMap[iotago.SlotIndex, []iotago.CommitmentID]) bool { + for _, commitments := range commitmentCountForChain.Values() { + if len(commitments) > 1 { + incorrectCommitments[chain.ForkingPoint.Get().ID()] = append(incorrectCommitments[chain.ForkingPoint.Get().ID()], commitments...) + } + } + + return true + }) + + if len(incorrectCommitments) > 0 { + return ierrors.Errorf("AssertUniqueCommitmentChain: %s: multiple commitments for a slot use the same chain, %s", node.Name, incorrectCommitments) + } + + return nil + }) + } +} + +func (t *TestSuite) AssertCommitmentsAndChainsEvicted(expectedEvictedSlot iotago.SlotIndex, nodes ...*mock.Node) { + mustNodes(nodes) + + for _, node := range nodes { + t.Eventually(func() error { + + seenChains := make(map[*protocol.Chain]struct{}) + if err := node.Protocol.Commitments.ForEach(func(commitment *protocol.Commitment) error { + if commitment.Chain.Get() != nil { // the chain of orphaned commitments is nil. + seenChains[commitment.Chain.Get()] = struct{}{} + } + + if expectedEvictedSlot >= commitment.Slot() { + return ierrors.Errorf("AssertCommitmentsAndChainsEvicted: %s: commitment %s not evicted", node.Name, commitment.ID()) + } + + return nil + }); err != nil { + return err + } + + if err := node.Protocol.Chains.ForEach(func(chain *protocol.Chain) error { + for i := iotago.SlotIndex(0); i <= expectedEvictedSlot; i++ { + commitment, exists := chain.Commitment(expectedEvictedSlot) + if exists { + return ierrors.Errorf("AssertCommitmentsAndChainsEvicted: %s: commitment %s on chain %s not evicted", node.Name, commitment.ID(), chain.ForkingPoint.Get().ID()) + } + } + + return nil + }); err != nil { + return err + } + + // Make sure that we don't have dangling chains. + if err := node.Protocol.Chains.Set.ForEach(func(chain *protocol.Chain) error { + if _, exists := seenChains[chain]; !exists { + return ierrors.Errorf("AssertCommitmentsAndChainsEvicted: %s: chain %s not evicted, total count of chains (from commitments)=%d, actual (in Protocol.Chains)=%d", node.Name, chain.ForkingPoint.Get().ID(), len(seenChains), node.Protocol.Chains.Set.Size()) + } + + return nil + }); err != nil { + return err + } + + return nil + }) + } +} + +func (t *TestSuite) AssertCommitmentsOnEvictedChain(expectedCommitments []*model.Commitment, expectedOrphaned bool, nodes ...*mock.Node) { + mustNodes(nodes) + + for _, node := range nodes { + t.Eventually(func() error { + for _, expectedCommitment := range expectedCommitments { + commitment, err := node.Protocol.Commitments.Get(expectedCommitment.ID(), false) + if err != nil { + return ierrors.Wrapf(err, "AssertCommitmentsOnEvictedChain: %s: expected commitment %s not found", node.Name, expectedCommitment.ID()) + } + + if chain := commitment.Chain.Get(); expectedOrphaned != (chain == nil || chain.IsEvicted.Get()) { + return ierrors.Errorf("AssertCommitmentsOnEvictedChain: %s: expected commitment %s to be on evicted chain %t, got %t", node.Name, expectedCommitment.ID(), expectedOrphaned, chain == nil || chain.IsEvicted.Get()) + } + } + + return nil + }) + } +} + +func (t *TestSuite) AssertMainChain(expectedChainID iotago.CommitmentID, nodes ...*mock.Node) { + mustNodes(nodes) + + for _, node := range nodes { + t.Eventually(func() error { + mainChainID := node.Protocol.Chains.Main.Get().ForkingPoint.Get().ID() + + if mainChainID != expectedChainID { + return ierrors.Errorf("AssertMainChain: %s: expected main chain to be %s, got %s", node.Name, expectedChainID, mainChainID) + } + + return nil + }) + } +} diff --git a/pkg/testsuite/node_state.go b/pkg/testsuite/node_state.go index aef18d144..4350d40e0 100644 --- a/pkg/testsuite/node_state.go +++ b/pkg/testsuite/node_state.go @@ -29,8 +29,8 @@ func (t *TestSuite) AssertNodeState(nodes []*mock.Node, opts ...options.Option[N if state.latestFinalizedSlot != nil { t.AssertLatestFinalizedSlot(*state.latestFinalizedSlot, nodes...) } - if state.chainID != nil { - t.AssertChainID(*state.chainID, nodes...) + if state.mainChainID != nil { + t.AssertMainChain(*state.mainChainID, nodes...) } if state.sybilProtectionCommitteeEpoch != nil && state.sybilProtectionCommittee != nil { t.AssertSybilProtectionCommittee(*state.sybilProtectionCommitteeEpoch, *state.sybilProtectionCommittee, nodes...) @@ -57,7 +57,7 @@ func (t *TestSuite) AssertNodeState(nodes []*mock.Node, opts ...options.Option[N t.AssertEvictedSlot(*state.evictedSlot, nodes...) } if state.chainManagerSolid != nil && *state.chainManagerSolid { - t.AssertChainManagerIsSolid(nodes...) + t.AssertLatestEngineCommitmentOnMainChain(nodes...) } } @@ -68,7 +68,7 @@ type NodeState struct { latestCommitmentSlot *iotago.SlotIndex latestCommitmentCumulativeWeight *uint64 latestFinalizedSlot *iotago.SlotIndex - chainID *iotago.CommitmentID + mainChainID *iotago.CommitmentID sybilProtectionCommitteeEpoch *iotago.EpochIndex sybilProtectionCommittee *[]iotago.AccountID @@ -129,9 +129,9 @@ func WithLatestFinalizedSlot(slot iotago.SlotIndex) options.Option[NodeState] { } } -func WithChainID(chainID iotago.CommitmentID) options.Option[NodeState] { +func WithMainChainID(chainID iotago.CommitmentID) options.Option[NodeState] { return func(state *NodeState) { - state.chainID = &chainID + state.mainChainID = &chainID } } diff --git a/pkg/testsuite/storage_settings.go b/pkg/testsuite/storage_settings.go index 230a85bc1..800933da9 100644 --- a/pkg/testsuite/storage_settings.go +++ b/pkg/testsuite/storage_settings.go @@ -126,19 +126,3 @@ func (t *TestSuite) AssertLatestFinalizedSlot(slot iotago.SlotIndex, nodes ...*m }) } } - -func (t *TestSuite) AssertChainID(expectedChainID iotago.CommitmentID, nodes ...*mock.Node) { - mustNodes(nodes) - - for _, node := range nodes { - t.Eventually(func() error { - actualChainID := node.Protocol.Chains.Main.Get().ForkingPoint.Get().ID() - - if expectedChainID != actualChainID { - return ierrors.Errorf("AssertChainID: %s: expected %s (index: %d), got %s (index: %d)", node.Name, expectedChainID, expectedChainID.Slot(), actualChainID, actualChainID.Slot()) - } - - return nil - }) - } -} diff --git a/pkg/testsuite/testsuite.go b/pkg/testsuite/testsuite.go index 4ac75400e..df0c31cfc 100644 --- a/pkg/testsuite/testsuite.go +++ b/pkg/testsuite/testsuite.go @@ -17,6 +17,7 @@ import ( "github.com/iotaledger/hive.go/runtime/options" "github.com/iotaledger/hive.go/runtime/syncutils" "github.com/iotaledger/iota-core/pkg/core/account" + "github.com/iotaledger/iota-core/pkg/model" "github.com/iotaledger/iota-core/pkg/protocol" "github.com/iotaledger/iota-core/pkg/protocol/engine/blocks" "github.com/iotaledger/iota-core/pkg/protocol/engine/utxoledger" @@ -99,7 +100,7 @@ func NewTestSuite(testingT *testing.T, opts ...options.Option[TestSuite]) *TestS optsTick: durationFromEnvOrDefault(2*time.Millisecond, "CI_UNIT_TESTS_TICK"), optsLogger: loggerFromEnvOrDefault("CI_UNIT_TESTS_NO_LOG", "CI_UNIT_TESTS_LOG_LEVEL"), }, opts, func(t *TestSuite) { - //fmt.Println("Setup TestSuite -", testingT.Name(), " @ ", time.Now()) + // fmt.Println("Setup TestSuite -", testingT.Name(), " @ ", time.Now()) t.ProtocolParameterOptions = append(t.ProtocolParameterOptions, iotago.WithNetworkOptions(testingT.Name(), iotago.PrefixTestnet)) t.API = iotago.V3API(iotago.NewV3SnapshotProtocolParameters(t.ProtocolParameterOptions...)) @@ -393,6 +394,23 @@ func (t *TestSuite) AddGenesisAccount(accountDetails snapshotcreator.AccountDeta t.optsAccounts = append(t.optsAccounts, accountDetails) } +func (t *TestSuite) CommitmentsOfMainEngine(node *mock.Node, start, end iotago.SlotIndex) []*model.Commitment { + var commitments []*model.Commitment + + for i := start; i <= end; i++ { + commitments = append(commitments, t.CommitmentOfMainEngine(node, i)) + } + + return commitments +} + +func (t *TestSuite) CommitmentOfMainEngine(node *mock.Node, slot iotago.SlotIndex) *model.Commitment { + commitment, err := node.Protocol.Engines.Main.Get().Storage.Commitments().Load(slot) + require.NoErrorf(t.Testing, err, "node %s: commitment for slot %d not found", node.Name, slot) + + return commitment +} + // AddGenesisWallet adds a wallet to the test suite with a block issuer in the genesis snapshot and access to the genesis seed. // If no block issuance credits are provided, the wallet will be assigned half of the maximum block issuance credits. func (t *TestSuite) AddGenesisWallet(name string, node *mock.Node, walletOpts ...options.Option[WalletOptions]) *mock.Wallet {