Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions epochStart/metachain/trigger.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ import (
"github.com/multiversx/mx-chain-core-go/display"
"github.com/multiversx/mx-chain-core-go/hashing"
"github.com/multiversx/mx-chain-core-go/marshal"
"github.com/multiversx/mx-chain-logger-go"
"github.com/multiversx/mx-chain-go/common"
"github.com/multiversx/mx-chain-go/config"
"github.com/multiversx/mx-chain-go/dataRetriever"
"github.com/multiversx/mx-chain-go/epochStart"
"github.com/multiversx/mx-chain-go/process"
"github.com/multiversx/mx-chain-go/storage"
"github.com/multiversx/mx-chain-logger-go"
)

var log = logger.GetOrCreate("epochStart/metachain")
Expand All @@ -33,6 +33,7 @@ var _ process.EpochBootstrapper = (*trigger)(nil)
var _ closing.Closer = (*trigger)(nil)

const minimumNonceToStartEpoch = 4
const minimumBlocksPerEpoch = 2
const disabledRoundForForceEpochStart = math.MaxUint64

// ArgsNewMetaEpochStartTrigger defines struct needed to create a new start of epoch trigger
Expand Down Expand Up @@ -209,9 +210,11 @@ func (t *trigger) Update(round uint64, nonce uint64) {
}

isZeroEpochEdgeCase := nonce < minimumNonceToStartEpoch
epochStartNonce := t.epochStartMeta.GetNonce()
hasMinBlocksInEpoch := nonce >= epochStartNonce+minimumBlocksPerEpoch

Copilot AI Apr 8, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hasMinBlocksInEpoch := nonce >= epochStartNonce+minimumBlocksPerEpoch can overflow when epochStartNonce is close to math.MaxUint64, potentially making the comparison unexpectedly true/false and triggering epoch start incorrectly. Please rewrite the check to avoid uint64 addition overflow (e.g., compare using nonce >= epochStartNonce and then nonce-epochStartNonce >= minimumBlocksPerEpoch, or guard the addition with an overflow-safe check).

Suggested change
hasMinBlocksInEpoch := nonce >= epochStartNonce+minimumBlocksPerEpoch
hasMinBlocksInEpoch := nonce >= epochStartNonce && nonce-epochStartNonce >= minimumBlocksPerEpoch

Copilot uses AI. Check for mistakes.
isNormalEpochStart := t.currentRound > t.currEpochStartRound+t.roundsPerEpoch
isWithEarlyEndOfEpoch := t.currentRound >= t.nextEpochStartRound
shouldTriggerEpochStart := (isNormalEpochStart || isWithEarlyEndOfEpoch) && !isZeroEpochEdgeCase
shouldTriggerEpochStart := (isNormalEpochStart || isWithEarlyEndOfEpoch) && !isZeroEpochEdgeCase && hasMinBlocksInEpoch
if shouldTriggerEpochStart {
t.epoch += 1
t.isEpochStart = true
Expand Down
79 changes: 79 additions & 0 deletions epochStart/metachain/trigger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -462,3 +462,82 @@ func TestTrigger_RevertBehindEpochStartBlock(t *testing.T) {
ret = epochStartTrigger.IsEpochStart()
assert.False(t, ret)
}

func TestTrigger_UpdateShouldNotStartEpochWithLessThanMinimumBlocks(t *testing.T) {
t.Parallel()

epoch := uint32(0)
arguments := createMockEpochStartTriggerArguments()
arguments.Settings.RoundsPerEpoch = 2
arguments.Settings.MinRoundsBetweenEpochs = 1
arguments.Epoch = epoch
epochStartTrigger, err := NewEpochStartTrigger(arguments)
require.Nil(t, err)

epochStartNonce := uint64(100)
epochStartRound := uint64(50)

// simulate an epoch start block already processed
epochStartTrigger.SetProcessed(&block.MetaBlock{
Nonce: epochStartNonce,
Round: epochStartRound,
Epoch: epoch,
EpochStart: block.EpochStart{
LastFinalizedHeaders: []block.EpochStartShardData{{RootHash: []byte("root")}},
},
}, nil)
require.False(t, epochStartTrigger.IsEpochStart())
require.Equal(t, epoch, epochStartTrigger.Epoch())

// round condition is met but nonce is only epochStartNonce+1 (1 block in epoch)
// this should NOT trigger a new epoch because minimumBlocksPerEpoch = 2
nextRound := epochStartRound + uint64(arguments.Settings.RoundsPerEpoch) + 1
epochStartTrigger.Update(nextRound, epochStartNonce+1)
assert.False(t, epochStartTrigger.IsEpochStart(),
"epoch should not start with only 1 block in the current epoch")
assert.Equal(t, epoch, epochStartTrigger.Epoch())

// now with nonce = epochStartNonce+2 (2 blocks in epoch), it should trigger
epochStartTrigger.Update(nextRound+1, epochStartNonce+2)
assert.True(t, epochStartTrigger.IsEpochStart(),
"epoch should start once minimum blocks per epoch is reached")
assert.Equal(t, epoch+1, epochStartTrigger.Epoch())
}

func TestTrigger_ForceEpochStartShouldRespectMinimumBlocks(t *testing.T) {
t.Parallel()

epoch := uint32(0)
arguments := createMockEpochStartTriggerArguments()
arguments.Settings.RoundsPerEpoch = 200
arguments.Settings.MinRoundsBetweenEpochs = 20
arguments.Epoch = epoch
epochStartTrigger, err := NewEpochStartTrigger(arguments)
require.Nil(t, err)

epochStartNonce := uint64(500)
epochStartRound := uint64(1000)

// simulate an epoch start block already processed
epochStartTrigger.SetProcessed(&block.MetaBlock{
Nonce: epochStartNonce,
Round: epochStartRound,
Epoch: epoch,
EpochStart: block.EpochStart{
LastFinalizedHeaders: []block.EpochStartShardData{{RootHash: []byte("root")}},
},
}, nil)

// force epoch start at round 1025
epochStartTrigger.ForceEpochStart(epochStartRound + 25)

// round condition met via force, but only 1 block since epoch start
epochStartTrigger.Update(epochStartRound+25, epochStartNonce+1)
assert.False(t, epochStartTrigger.IsEpochStart(),
"forced epoch should not start with only 1 block in the current epoch")

// with 2 blocks, the forced epoch start should proceed
epochStartTrigger.Update(epochStartRound+26, epochStartNonce+2)
assert.True(t, epochStartTrigger.IsEpochStart(),
"forced epoch should start once minimum blocks per epoch is reached")
}
Loading