diff --git a/process/block/export_test.go b/process/block/export_test.go index 943b41f0b3..92f5779a79 100644 --- a/process/block/export_test.go +++ b/process/block/export_test.go @@ -909,6 +909,11 @@ func (sp *shardProcessor) CheckEpochStartInfoAvailableIfNeeded(header data.Shard return sp.checkEpochStartInfoAvailableIfNeeded(header) } +// EnsureEpochStartInfoAvailable - +func (sp *shardProcessor) EnsureEpochStartInfoAvailable(header data.ShardHeaderHandler, haveTime func() time.Duration) error { + return sp.ensureEpochStartInfoAvailable(header, haveTime) +} + // HeadersPool - func (sp *shardProcessor) HeadersPool() dataRetriever.HeadersPool { return sp.dataPool.Headers() diff --git a/process/block/shardblock.go b/process/block/shardblock.go index 28c29c5eee..3cb1a2cfef 100644 --- a/process/block/shardblock.go +++ b/process/block/shardblock.go @@ -301,6 +301,36 @@ func (sp *shardProcessor) checkEpochStartInfoAvailableIfNeeded(header data.Shard return nil } +func (sp *shardProcessor) ensureEpochStartInfoAvailable(header data.ShardHeaderHandler, haveTime func() time.Duration) error { + err := sp.checkEpochStartInfoAvailableIfNeeded(header) + if err == nil { + return nil + } + + log.Warn("epoch start info missing at execution, requesting", + "epochStartMetaHash", header.GetEpochStartMetaHash(), + "error", err, + ) + + requestCount := 0 + for haveTime() > 0 { + requestCount++ + if requestCount%5 == 1 { + go sp.requestHandler.RequestMetaHeader(header.GetEpochStartMetaHash()) + sp.requestEpochStartProofIfNeeded(header.GetEpochStartMetaHash(), header.GetEpoch()) + } + + time.Sleep(timeBetweenCheckForEpochStart) + + err = sp.checkEpochStartInfoAvailableIfNeeded(header) + if err == nil { + return nil + } + } + + return err +} + func (sp *shardProcessor) requestEpochStartInfo(header data.ShardHeaderHandler, haveTime func() time.Duration) error { if !sp.shouldEpochStartInfoBeAvailable(header) { return nil diff --git a/process/block/shardblockProposal.go b/process/block/shardblockProposal.go index 50cba308a5..bca30fd28c 100644 --- a/process/block/shardblockProposal.go +++ b/process/block/shardblockProposal.go @@ -338,8 +338,7 @@ func (sp *shardProcessor) ProcessBlockProposal( return nil, err } - // TODO: improvement - add also a request if it is missing as a fallback, although it should not be missing at this point - err = sp.checkEpochStartInfoAvailableIfNeeded(header) + err = sp.ensureEpochStartInfoAvailable(header, haveTime) if err != nil { return nil, err } diff --git a/process/block/shardblock_test.go b/process/block/shardblock_test.go index 8a1398b164..8039a955cf 100644 --- a/process/block/shardblock_test.go +++ b/process/block/shardblock_test.go @@ -7814,3 +7814,346 @@ func TestShardProcessor_CancelPruneForDismissedExecutionResults(t *testing.T) { require.Equal(t, 4, cancelPruneCalls) }) } + +func TestShardProcessor_EnsureEpochStartInfoAvailable(t *testing.T) { + t.Parallel() + + headerHash := []byte("epochStartMetaHash") + headerEpoch := uint32(10) + + t.Run("not start of epoch block, should return nil", func(t *testing.T) { + t.Parallel() + + arguments := CreateMockArguments(createComponentHolderMocks()) + sp, _ := blproc.NewShardProcessor(arguments) + + header := &testscommon.HeaderHandlerStub{ + IsStartOfEpochBlockCalled: func() bool { + return false + }, + } + err := sp.EnsureEpochStartInfoAvailable(header, haveTime) + require.Nil(t, err) + }) + + t.Run("epoch start, header and proof available, should return nil", func(t *testing.T) { + t.Parallel() + + arguments := CreateMockArguments(createComponentHolderMocks()) + arguments.EpochStartTrigger = &mock.EpochStartTriggerStub{ + MetaEpochCalled: func() uint32 { + return 9 + }, + IsEpochStartCalled: func() bool { + return false + }, + } + sp, _ := blproc.NewShardProcessor(arguments) + sp.HeadersPool().AddHeader(headerHash, &block.MetaBlock{}) + sp.ProofsPool().AddProof(&block.HeaderProof{ + HeaderHash: headerHash, + HeaderShardId: core.MetachainShardId, + }) + + header := &block.HeaderV3{ + Epoch: headerEpoch, + EpochStartMetaHash: headerHash, + } + err := sp.EnsureEpochStartInfoAvailable(header, haveTime) + require.Nil(t, err) + }) + + t.Run("missing header, arrives within budget", func(t *testing.T) { + t.Parallel() + + poolCallCount := 0 + headersPool := &pool.HeadersPoolStub{ + GetHeaderByHashCalled: func(hash []byte) (data.HeaderHandler, error) { + poolCallCount++ + if poolCallCount <= 2 { + return nil, expectedError + } + return &block.MetaBlock{}, nil + }, + } + poolsHolder := &dataRetrieverMock.PoolsHolderStub{ + HeadersCalled: func() dataRetriever.HeadersPool { + return headersPool + }, + TransactionsCalled: func() dataRetriever.ShardedDataCacherNotifier { + return &testscommon.ShardedDataStub{} + }, + ProofsCalled: func() dataRetriever.ProofsPool { + return &dataRetrieverMock.ProofsPoolMock{ + HasProofCalled: func(shardID uint32, headerHash []byte) bool { + return true + }, + } + }, + DirectSentTransactionsCalled: func() storage.Cacher { + return cache.NewCacherStub() + }, + } + + requestMetaHeaderCt := atomicCore.Counter{} + arguments := CreateMockArguments(createComponentHolderMocks()) + arguments.EpochStartTrigger = &mock.EpochStartTriggerStub{ + MetaEpochCalled: func() uint32 { + return 9 + }, + IsEpochStartCalled: func() bool { + return false + }, + } + arguments.DataComponents = &mock.DataComponentsMock{ + Storage: &storageStubs.ChainStorerStub{}, + DataPool: poolsHolder, + BlockChain: &testscommon.ChainHandlerStub{ + GetGenesisHeaderCalled: func() data.HeaderHandler { + return &block.Header{} + }, + }, + } + arguments.RequestHandler = &testscommon.RequestHandlerStub{ + RequestMetaHeaderCalled: func(hash []byte) { + requestMetaHeaderCt.Increment() + }, + } + + sp, _ := blproc.NewShardProcessor(arguments) + + header := &block.HeaderV3{ + Epoch: headerEpoch, + EpochStartMetaHash: headerHash, + } + err := sp.EnsureEpochStartInfoAvailable(header, haveTime) + require.Nil(t, err) + require.GreaterOrEqual(t, requestMetaHeaderCt.Get(), int64(1)) + require.Equal(t, 3, poolCallCount) + }) + + t.Run("missing header, budget exhausted", func(t *testing.T) { + t.Parallel() + + headersPool := &pool.HeadersPoolStub{ + GetHeaderByHashCalled: func(hash []byte) (data.HeaderHandler, error) { + return nil, expectedError + }, + } + poolsHolder := &dataRetrieverMock.PoolsHolderStub{ + HeadersCalled: func() dataRetriever.HeadersPool { + return headersPool + }, + TransactionsCalled: func() dataRetriever.ShardedDataCacherNotifier { + return &testscommon.ShardedDataStub{} + }, + ProofsCalled: func() dataRetriever.ProofsPool { + return &dataRetrieverMock.ProofsPoolMock{ + HasProofCalled: func(shardID uint32, headerHash []byte) bool { + return true + }, + } + }, + DirectSentTransactionsCalled: func() storage.Cacher { + return cache.NewCacherStub() + }, + } + + arguments := CreateMockArguments(createComponentHolderMocks()) + arguments.EpochStartTrigger = &mock.EpochStartTriggerStub{ + MetaEpochCalled: func() uint32 { + return 9 + }, + IsEpochStartCalled: func() bool { + return false + }, + } + arguments.DataComponents = &mock.DataComponentsMock{ + Storage: &storageStubs.ChainStorerStub{}, + DataPool: poolsHolder, + BlockChain: &testscommon.ChainHandlerStub{ + GetGenesisHeaderCalled: func() data.HeaderHandler { + return &block.Header{} + }, + }, + } + + requestMetaHeaderCt := atomicCore.Counter{} + arguments.RequestHandler = &testscommon.RequestHandlerStub{ + RequestMetaHeaderCalled: func(hash []byte) { + requestMetaHeaderCt.Increment() + }, + } + + sp, _ := blproc.NewShardProcessor(arguments) + + haveTimeCallCount := 0 + noTimeFunc := func() time.Duration { + haveTimeCallCount++ + if haveTimeCallCount == 1 { + return time.Millisecond + } + return -time.Millisecond + } + + header := &block.HeaderV3{ + Epoch: headerEpoch, + EpochStartMetaHash: headerHash, + } + err := sp.EnsureEpochStartInfoAvailable(header, noTimeFunc) + require.ErrorIs(t, err, process.ErrEpochStartInfoNotAvailable) + require.GreaterOrEqual(t, requestMetaHeaderCt.Get(), int64(1)) + }) + + t.Run("missing proof with andromeda, arrives within budget", func(t *testing.T) { + t.Parallel() + + coreComponents, dataComponents, bootstrapComponents, statusComponents := createComponentHolderMocks() + coreComponents.EnableEpochsHandlerField = &enableEpochsHandlerMock.EnableEpochsHandlerStub{ + IsFlagEnabledInEpochCalled: func(flag core.EnableEpochFlag, epoch uint32) bool { + return flag == common.AndromedaFlag + }, + } + + hasProofCallCount := 0 + headersPool := &pool.HeadersPoolStub{ + GetHeaderByHashCalled: func(hash []byte) (data.HeaderHandler, error) { + return &block.MetaBlock{}, nil + }, + } + proofsPool := &dataRetrieverMock.ProofsPoolMock{ + HasProofCalled: func(shardID uint32, headerHash []byte) bool { + hasProofCallCount++ + return hasProofCallCount > 2 + }, + } + poolsHolder := &dataRetrieverMock.PoolsHolderStub{ + HeadersCalled: func() dataRetriever.HeadersPool { + return headersPool + }, + TransactionsCalled: func() dataRetriever.ShardedDataCacherNotifier { + return &testscommon.ShardedDataStub{} + }, + ProofsCalled: func() dataRetriever.ProofsPool { + return proofsPool + }, + DirectSentTransactionsCalled: func() storage.Cacher { + return cache.NewCacherStub() + }, + } + + requestProofCt := atomicCore.Counter{} + bootstrapComponents.Coordinator = mock.NewOneShardCoordinatorMock() + arguments := CreateMockArguments(coreComponents, dataComponents, bootstrapComponents, statusComponents) + arguments.EpochStartTrigger = &mock.EpochStartTriggerStub{ + MetaEpochCalled: func() uint32 { + return 9 + }, + IsEpochStartCalled: func() bool { + return false + }, + } + arguments.DataComponents = &mock.DataComponentsMock{ + Storage: &storageStubs.ChainStorerStub{}, + DataPool: poolsHolder, + BlockChain: &testscommon.ChainHandlerStub{ + GetGenesisHeaderCalled: func() data.HeaderHandler { + return &block.Header{} + }, + }, + } + arguments.RequestHandler = &testscommon.RequestHandlerStub{ + RequestEquivalentProofByHashCalled: func(shardID uint32, hash []byte) { + requestProofCt.Increment() + }, + } + + sp, _ := blproc.NewShardProcessor(arguments) + + header := &block.Header{ + Epoch: headerEpoch, + EpochStartMetaHash: headerHash, + } + err := sp.EnsureEpochStartInfoAvailable(header, haveTime) + require.Nil(t, err) + require.GreaterOrEqual(t, requestProofCt.Get(), int64(1)) + }) + + t.Run("missing proof with andromeda, budget exhausted", func(t *testing.T) { + t.Parallel() + + coreComponents, dataComponents, bootstrapComponents, statusComponents := createComponentHolderMocks() + coreComponents.EnableEpochsHandlerField = &enableEpochsHandlerMock.EnableEpochsHandlerStub{ + IsFlagEnabledInEpochCalled: func(flag core.EnableEpochFlag, epoch uint32) bool { + return flag == common.AndromedaFlag + }, + } + + headersPool := &pool.HeadersPoolStub{ + GetHeaderByHashCalled: func(hash []byte) (data.HeaderHandler, error) { + return &block.MetaBlock{}, nil + }, + } + proofsPool := &dataRetrieverMock.ProofsPoolMock{ + HasProofCalled: func(shardID uint32, headerHash []byte) bool { + return false + }, + } + poolsHolder := &dataRetrieverMock.PoolsHolderStub{ + HeadersCalled: func() dataRetriever.HeadersPool { + return headersPool + }, + TransactionsCalled: func() dataRetriever.ShardedDataCacherNotifier { + return &testscommon.ShardedDataStub{} + }, + ProofsCalled: func() dataRetriever.ProofsPool { + return proofsPool + }, + DirectSentTransactionsCalled: func() storage.Cacher { + return cache.NewCacherStub() + }, + } + + bootstrapComponents.Coordinator = mock.NewOneShardCoordinatorMock() + arguments := CreateMockArguments(coreComponents, dataComponents, bootstrapComponents, statusComponents) + arguments.EpochStartTrigger = &mock.EpochStartTriggerStub{ + MetaEpochCalled: func() uint32 { + return 9 + }, + IsEpochStartCalled: func() bool { + return false + }, + } + arguments.DataComponents = &mock.DataComponentsMock{ + Storage: &storageStubs.ChainStorerStub{}, + DataPool: poolsHolder, + BlockChain: &testscommon.ChainHandlerStub{ + GetGenesisHeaderCalled: func() data.HeaderHandler { + return &block.Header{} + }, + }, + } + arguments.RequestHandler = &testscommon.RequestHandlerStub{ + RequestEquivalentProofByHashCalled: func(shardID uint32, hash []byte) { + }, + } + + sp, _ := blproc.NewShardProcessor(arguments) + + haveTimeCallCount := 0 + noTimeFunc := func() time.Duration { + haveTimeCallCount++ + if haveTimeCallCount == 1 { + return time.Millisecond + } + return -time.Millisecond + } + + header := &block.Header{ + Epoch: headerEpoch, + EpochStartMetaHash: headerHash, + } + err := sp.EnsureEpochStartInfoAvailable(header, noTimeFunc) + require.ErrorIs(t, err, process.ErrEpochStartInfoNotAvailable) + }) +}