diff --git a/pkg/protocol/engine/consensus/blockgadget/thresholdblockgadget/confirmation_ratification.go b/pkg/protocol/engine/consensus/blockgadget/thresholdblockgadget/confirmation_ratification.go index 7573f5a0f..1e0e80772 100644 --- a/pkg/protocol/engine/consensus/blockgadget/thresholdblockgadget/confirmation_ratification.go +++ b/pkg/protocol/engine/consensus/blockgadget/thresholdblockgadget/confirmation_ratification.go @@ -13,6 +13,7 @@ func (g *Gadget) trackConfirmationRatifierWeight(votingBlock *blocks.Block) { } ratifierBlockIndex := votingBlock.ID().Slot() + ratifierBlockEpoch := votingBlock.ProtocolBlock().API.TimeProvider().EpochFromSlot(ratifierBlockIndex) var toConfirm []*blocks.Block @@ -24,6 +25,14 @@ func (g *Gadget) trackConfirmationRatifierWeight(votingBlock *blocks.Block) { return false } + // Skip propagation if the block is not in the same epoch as the ratifier. This might delay the confirmation + // of blocks at the end of the epoch but make sure that confirmation is safe in case where the minority of voters + // got different seats for the next epoch. + blockEpoch := block.ProtocolBlock().API.TimeProvider().EpochFromSlot(block.ID().Slot()) + if ratifierBlockEpoch != blockEpoch { + return false + } + // Skip propagation if the block is already confirmed. if block.IsConfirmed() { return false diff --git a/pkg/protocol/engine/consensus/blockgadget/thresholdblockgadget/witness_weight.go b/pkg/protocol/engine/consensus/blockgadget/thresholdblockgadget/witness_weight.go index 0e1d2076a..f0b5a4453 100644 --- a/pkg/protocol/engine/consensus/blockgadget/thresholdblockgadget/witness_weight.go +++ b/pkg/protocol/engine/consensus/blockgadget/thresholdblockgadget/witness_weight.go @@ -16,6 +16,8 @@ func (g *Gadget) TrackWitnessWeight(votingBlock *blocks.Block) { return } + votingBlockEpoch := votingBlock.ProtocolBlock().API.TimeProvider().EpochFromSlot(votingBlock.ID().Slot()) + var toPreAccept []*blocks.Block toPreAcceptByID := ds.NewSet[iotago.BlockID]() @@ -32,7 +34,11 @@ func (g *Gadget) TrackWitnessWeight(votingBlock *blocks.Block) { propagateFurther = true } - if !block.IsPreConfirmed() && (shouldPreConfirm || anyChildInSet(block, toPreConfirmByID)) { + // Skip propagation of pre-confirmation if the block is not in the same epoch as the votingBlock. + // This might delay the (pre-)confirmation of blocks at the end of the epoch but make sure that (pre-)confirmation + // is safe in case where the minority of voters got different seats for the next epoch. + blockEpoch := block.ProtocolBlock().API.TimeProvider().EpochFromSlot(block.ID().Slot()) + if !block.IsPreConfirmed() && votingBlockEpoch == blockEpoch && (shouldPreConfirm || anyChildInSet(block, toPreConfirmByID)) { toPreConfirm = append([]*blocks.Block{block}, toPreConfirm...) toPreConfirmByID.Add(block.ID()) propagateFurther = true diff --git a/pkg/tests/confirmation_state_test.go b/pkg/tests/confirmation_state_test.go index 0323f23f5..05e47eaa8 100644 --- a/pkg/tests/confirmation_state_test.go +++ b/pkg/tests/confirmation_state_test.go @@ -13,6 +13,7 @@ import ( "github.com/iotaledger/iota-core/pkg/testsuite" "github.com/iotaledger/iota-core/pkg/testsuite/mock" iotago "github.com/iotaledger/iota.go/v4" + "github.com/iotaledger/iota.go/v4/api" ) func TestConfirmationFlags(t *testing.T) { @@ -245,3 +246,88 @@ func TestConfirmationFlags(t *testing.T) { ) } } + +func TestConfirmationOverEpochBoundary(t *testing.T) { + ts := testsuite.NewTestSuite(t, + testsuite.WithProtocolParametersOptions( + iotago.WithTimeProviderOptions( + 0, + testsuite.GenesisTimeWithOffsetBySlots(1000, testsuite.DefaultSlotDurationInSeconds), + testsuite.DefaultSlotDurationInSeconds, + 3, + ), + iotago.WithLivenessOptions( + 10, + 10, + 3, + 4, + 5, + ), + ), + ) + defer ts.Shutdown() + + ts.AddValidatorNode("node0") + ts.AddValidatorNode("node1") + ts.AddValidatorNode("node2") + ts.AddValidatorNode("node3") + ts.AddNode("node4") + + ts.Run(true) + + // Issue blocks up until 1 slot more than the epoch. + { + ts.IssueBlocksAtSlots("", []iotago.SlotIndex{1, 2, 3, 4, 5, 6, 7, 8, 9}, 4, "Genesis", ts.Nodes(), true, false) + + ts.AssertNodeState(ts.Nodes(), + testsuite.WithLatestFinalizedSlot(5), + testsuite.WithLatestCommitmentSlotIndex(6), + testsuite.WithEqualStoredCommitmentAtIndex(6), + testsuite.WithEvictedSlot(6), + ) + + // Verify propagation of witness weight over epoch boundaries (slot 7). + { + // We propagate witness weight for pre-acceptance and acceptance over epoch boundaries. + ts.AssertBlocksInCachePreAccepted(ts.BlocksWithPrefixes("7.0", "7.1", "7.2", "7.3"), true, ts.Nodes()...) + ts.AssertBlocksInCacheAccepted(ts.BlocksWithPrefixes("7.0", "7.1", "7.2", "7.3"), true, ts.Nodes()...) + + // We don't propagate pre-confirmation and confirmation over epoch boundaries: + // There's 4 rows in a slot, everything except the last row should be pre-confirmed. + ts.AssertBlocksInCachePreConfirmed(ts.BlocksWithPrefixes("7.0", "7.1", "7.2"), true, ts.Nodes()...) + ts.AssertBlocksInCachePreConfirmed(ts.BlocksWithPrefixes("7.3"), false, ts.Nodes()...) + // Accordingly, only the first 2 rows are confirmed. + ts.AssertBlocksInCacheConfirmed(ts.BlocksWithPrefixes("7.0", "7.1"), true, ts.Nodes()...) + ts.AssertBlocksInCacheConfirmed(ts.BlocksWithPrefixes("7.2", "7.3"), false, ts.Nodes()...) + + } + + // Slot 8 and 9 behaves normally, as they are in the new epoch. + { + ts.AssertBlocksInCacheAccepted(ts.BlocksWithPrefixes("8.0", "8.1", "8.2", "8.3", "9.0", "9.1"), true, ts.Nodes()...) + ts.AssertBlocksInCacheAccepted(ts.BlocksWithPrefixes("9.2", "9.3"), false, ts.Nodes()...) + ts.AssertBlocksInCachePreAccepted(ts.BlocksWithPrefixes("9.2"), true, ts.Nodes()...) + ts.AssertBlocksInCachePreAccepted(ts.BlocksWithPrefixes("9.3"), false, ts.Nodes()...) + + ts.AssertBlocksInCacheConfirmed(ts.BlocksWithPrefixes("8.0", "8.1", "8.2", "8.3", "9.0", "9.1"), true, ts.Nodes()...) + ts.AssertBlocksInCacheConfirmed(ts.BlocksWithPrefixes("9.2", "9.3"), false, ts.Nodes()...) + ts.AssertBlocksInCachePreConfirmed(ts.BlocksWithPrefixes("9.2"), true, ts.Nodes()...) + ts.AssertBlocksInCachePreConfirmed(ts.BlocksWithPrefixes("9.3"), false, ts.Nodes()...) + } + } + + // Issue more so that blocks at end of epoch become confirmed via finalization. + { + ts.IssueBlocksAtSlots("", []iotago.SlotIndex{10, 11, 12}, 4, "9.3", ts.Nodes(), true, false) + + ts.AssertNodeState(ts.Nodes(), + testsuite.WithLatestFinalizedSlot(8), + testsuite.WithLatestCommitmentSlotIndex(9), + testsuite.WithEqualStoredCommitmentAtIndex(9), + testsuite.WithEvictedSlot(9), + ) + + ts.AssertRetainerBlocksState(ts.BlocksWithPrefixes("7", "8"), api.BlockStateFinalized, ts.Nodes()...) + ts.AssertRetainerBlocksState(ts.BlocksWithPrefixes("9", "10", "11"), api.BlockStateConfirmed, ts.Nodes()...) + } +} diff --git a/pkg/testsuite/blocks_retainer.go b/pkg/testsuite/blocks_retainer.go new file mode 100644 index 000000000..8810f84f7 --- /dev/null +++ b/pkg/testsuite/blocks_retainer.go @@ -0,0 +1,31 @@ +package testsuite + +import ( + "github.com/iotaledger/hive.go/ierrors" + "github.com/iotaledger/iota-core/pkg/protocol/engine/blocks" + "github.com/iotaledger/iota-core/pkg/testsuite/mock" + "github.com/iotaledger/iota.go/v4/api" +) + +func (t *TestSuite) AssertRetainerBlocksState(expectedBlocks []*blocks.Block, expectedState api.BlockState, nodes ...*mock.Node) { + mustNodes(nodes) + + for _, node := range nodes { + for _, block := range expectedBlocks { + t.Eventually(func() error { + blockFromRetainer, err := node.Protocol.Engines.Main.Get().Retainer.BlockMetadata(block.ID()) + if err != nil { + return ierrors.Errorf("AssertRetainerBlocksState: %s: block %s: error when loading %s", node.Name, block.ID(), err.Error()) + } + + if expectedState != blockFromRetainer.BlockState { + return ierrors.Errorf("AssertRetainerBlocksState: %s: block %s: expected %s, got %s", node.Name, block.ID(), expectedState, blockFromRetainer.BlockState) + } + + return nil + }) + + t.AssertBlock(block, node) + } + } +}