diff --git a/process/block/export_test.go b/process/block/export_test.go index eeb0c26246f..85210d30bcc 100644 --- a/process/block/export_test.go +++ b/process/block/export_test.go @@ -798,6 +798,11 @@ func (sp *shardProcessor) CollectExecutionResults(headerHash []byte, header data return sp.collectExecutionResults(headerHash, header, body) } +// AddExecutionResultsOnHeader - +func (sp *shardProcessor) AddExecutionResultsOnHeader(shardHeader data.HeaderHandler) error { + return sp.addExecutionResultsOnHeader(shardHeader) +} + // GetCrossShardIncomingMiniBlocksFromBody - func (sp *shardProcessor) GetCrossShardIncomingMiniBlocksFromBody(body *block.Body) []*block.MiniBlock { return sp.getCrossShardIncomingMiniBlocksFromBody(body) @@ -822,3 +827,13 @@ func GetLastExecutionResultsRootHash( func GetHaveTimeForProposal(startTime time.Time, maxDuration time.Duration) func() time.Duration { return getHaveTimeForProposal(startTime, maxDuration) } + +// ConstructPartialShardBlockProcessorForTest - +func ConstructPartialShardBlockProcessorForTest(subcomponents map[string]interface{}) (*shardProcessor, error) { + sp := &shardProcessor{} + err := factory.ConstructPartialComponentForTest(sp, subcomponents) + if err != nil { + return nil, err + } + return sp, err +} diff --git a/process/block/metablock.go b/process/block/metablock.go index 939c7c4c4ef..7aa03b30afd 100644 --- a/process/block/metablock.go +++ b/process/block/metablock.go @@ -476,11 +476,13 @@ func (mp *metaProcessor) getAllMiniBlockDstMeFromShards(metaHdr *block.MetaBlock for _, shardInfo := range metaHdr.ShardInfo { headerInfo, ok := mp.hdrsForCurrBlock.GetHeaderInfo(string(shardInfo.HeaderHash)) if !ok { - continue + return nil, fmt.Errorf("%w for shard info with hash = %s", + process.ErrMissingHeader, hex.EncodeToString(shardInfo.HeaderHash)) } shardHeader, ok := headerInfo.GetHeader().(data.ShardHeaderHandler) if !ok { - continue + return nil, fmt.Errorf("%w : for shardInfo.HeaderHash = %s", + process.ErrWrongTypeAssertion, hex.EncodeToString(shardInfo.HeaderHash)) } lastCrossNotarizedHeader, _, err := mp.blockTracker.GetLastCrossNotarizedHeader(shardInfo.ShardID) @@ -489,13 +491,16 @@ func (mp *metaProcessor) getAllMiniBlockDstMeFromShards(metaHdr *block.MetaBlock } if shardHeader.GetRound() > metaHdr.Round { - continue + return nil, fmt.Errorf("%w : for shard info with hash = %s", + process.ErrHigherRoundInBlock, hex.EncodeToString(shardInfo.HeaderHash)) } if shardHeader.GetRound() <= lastCrossNotarizedHeader.GetRound() { - continue + return nil, fmt.Errorf("%w : for shard info with hash = %s", + process.ErrLowerRoundInBlock, hex.EncodeToString(shardInfo.HeaderHash)) } if shardHeader.GetNonce() <= lastCrossNotarizedHeader.GetNonce() { - continue + return nil, fmt.Errorf("%w : for shard info with hash = %s", + process.ErrLowerNonceInBlock, hex.EncodeToString(shardInfo.HeaderHash)) } finalCrossMiniBlockHashes := mp.getFinalCrossMiniBlockHashes(shardHeader) diff --git a/process/block/metablock_test.go b/process/block/metablock_test.go index 9a108c0034d..2d33ebb1c84 100644 --- a/process/block/metablock_test.go +++ b/process/block/metablock_test.go @@ -2795,7 +2795,8 @@ func TestMetaProcessor_VerifyCrossShardMiniBlocksDstMe(t *testing.T) { assert.Nil(t, err) err = mp.VerifyCrossShardMiniBlockDstMe(hdr) - assert.Nil(t, err) + assert.Error(t, err) + assert.ErrorIs(t, err, process.ErrMissingHeader) } func TestMetaProcess_CreateNewBlockHeaderProcessHeaderExpectCheckRoundCalled(t *testing.T) { diff --git a/process/block/shardblock.go b/process/block/shardblock.go index 65826cb88ec..55bcc2ea558 100644 --- a/process/block/shardblock.go +++ b/process/block/shardblock.go @@ -1853,7 +1853,8 @@ func (sp *shardProcessor) getAllMiniBlockDstMeFromMeta(header data.ShardHeaderHa for _, metaBlockHash := range header.GetMetaBlockHashes() { headerInfo, ok := sp.hdrsForCurrBlock.GetHeaderInfo(string(metaBlockHash)) if !ok { - return nil, err + return nil, fmt.Errorf("%w for metaBlockHash %s", + process.ErrMissingHeader, hex.EncodeToString(metaBlockHash)) } err = sp.addCrossShardMiniBlocksDstMeToMap(header, metaBlockHash, headerInfo.GetHeader(), lastCrossNotarizedHeader, miniBlockMetaHashes) diff --git a/process/block/shardblockProposal_test.go b/process/block/shardblockProposal_test.go index 30f71b1483d..b97704b3a37 100644 --- a/process/block/shardblockProposal_test.go +++ b/process/block/shardblockProposal_test.go @@ -2,6 +2,9 @@ package block_test import ( "bytes" + "errors" + "fmt" + "math/big" "sync" "testing" "time" @@ -11,6 +14,11 @@ import ( "github.com/multiversx/mx-chain-core-go/data/block" "github.com/multiversx/mx-chain-core-go/data/transaction" "github.com/multiversx/mx-chain-go/common" + "github.com/multiversx/mx-chain-go/common/graceperiod" + "github.com/multiversx/mx-chain-go/config" + "github.com/multiversx/mx-chain-go/state" + + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" retriever "github.com/multiversx/mx-chain-go/dataRetriever" @@ -25,10 +33,14 @@ import ( "github.com/multiversx/mx-chain-go/testscommon" "github.com/multiversx/mx-chain-go/testscommon/cache" "github.com/multiversx/mx-chain-go/testscommon/dataRetriever" + "github.com/multiversx/mx-chain-go/testscommon/enableEpochsHandlerMock" testscommonExecutionTrack "github.com/multiversx/mx-chain-go/testscommon/executionTrack" + "github.com/multiversx/mx-chain-go/testscommon/hashingMocks" "github.com/multiversx/mx-chain-go/testscommon/mbSelection" "github.com/multiversx/mx-chain-go/testscommon/pool" "github.com/multiversx/mx-chain-go/testscommon/processMocks" + "github.com/multiversx/mx-chain-go/testscommon/round" + stateMock "github.com/multiversx/mx-chain-go/testscommon/state" statusHandlerMock "github.com/multiversx/mx-chain-go/testscommon/statusHandler" ) @@ -604,6 +616,207 @@ func TestShardProcessor_CreateBlockProposal(t *testing.T) { }) } +func Test_addExecutionResultsOnHeader(t *testing.T) { + t.Parallel() + t.Run("executionResultsTracker returns error should error", func(t *testing.T) { + t.Parallel() + + sp, _ := blproc.ConstructPartialShardBlockProcessorForTest(map[string]interface{}{ + "executionResultsTracker": &testscommonExecutionTrack.ExecutionResultsTrackerStub{ + GetPendingExecutionResultsCalled: func() ([]data.BaseExecutionResultHandler, error) { + return nil, expectedErr + }, + }, + }) + err := sp.AddExecutionResultsOnHeader(&block.HeaderV3{}) + require.Error(t, err) + require.Equal(t, expectedErr, err) + }) + t.Run("GetPrevBlockLastExecutionResult returns error should error", func(t *testing.T) { + t.Parallel() + sp, _ := blproc.ConstructPartialShardBlockProcessorForTest(map[string]interface{}{ + "executionResultsTracker": &testscommonExecutionTrack.ExecutionResultsTrackerStub{ + GetPendingExecutionResultsCalled: func() ([]data.BaseExecutionResultHandler, error) { + return []data.BaseExecutionResultHandler{ + &block.ExecutionResult{ + BaseExecutionResult: &block.BaseExecutionResult{}, + }, + }, nil + }, + }, + }) + err := sp.AddExecutionResultsOnHeader(&block.HeaderV3{}) + require.Error(t, err) + require.Equal(t, process.ErrNilBlockChain, err) + }) + t.Run("CreateDataForInclusionEstimation returns error should error", func(t *testing.T) { + t.Parallel() + sp, _ := blproc.ConstructPartialShardBlockProcessorForTest(map[string]interface{}{ + "executionResultsTracker": &testscommonExecutionTrack.ExecutionResultsTrackerStub{ + GetPendingExecutionResultsCalled: func() ([]data.BaseExecutionResultHandler, error) { + return []data.BaseExecutionResultHandler{ + &block.ExecutionResult{ + BaseExecutionResult: &block.BaseExecutionResult{}, + }, + }, nil + }, + }, + "blockChain": &testscommon.ChainHandlerStub{ + GetCurrentBlockHeaderHashCalled: func() []byte { + return []byte("hash") + }, + GetCurrentBlockHeaderCalled: func() data.HeaderHandler { + return &block.HeaderV3{ + PrevHash: []byte("prev_hash"), + } + }, + }, + }) + err := sp.AddExecutionResultsOnHeader(&block.HeaderV3{}) + require.Error(t, err) + require.Equal(t, process.ErrNilLastExecutionResultHandler, err) + }) + + t.Run("CreateLastExecutionResultInfoFromExecutionResult returns error should error", func(t *testing.T) { + t.Parallel() + + baseExecutionResults := &block.BaseExecutionResult{ + HeaderHash: []byte("hash"), + HeaderNonce: 100, + HeaderRound: 1, + RootHash: []byte("rootHash"), + } + header := &block.HeaderV3{ + PrevHash: []byte("prev_hash"), + + LastExecutionResult: &block.ExecutionResultInfo{ + NotarizedInRound: 1, + ExecutionResult: baseExecutionResults, + }, + } + + sp, _ := blproc.ConstructPartialShardBlockProcessorForTest(map[string]interface{}{ + "executionResultsTracker": &testscommonExecutionTrack.ExecutionResultsTrackerStub{ + GetPendingExecutionResultsCalled: func() ([]data.BaseExecutionResultHandler, error) { + // return one meta execution result (so Decide can include it) + meta := &block.MetaExecutionResult{ + ExecutionResult: &block.BaseMetaExecutionResult{ + BaseExecutionResult: &block.BaseExecutionResult{ + HeaderHash: []byte("hdr-hash"), + HeaderNonce: 1, + HeaderRound: 2, + RootHash: []byte("root"), + GasUsed: 100000, + }, + ValidatorStatsRootHash: []byte("vstats"), + AccumulatedFeesInEpoch: big.NewInt(123), + DevFeesInEpoch: big.NewInt(45), + }, + ReceiptsHash: []byte{}, + MiniBlockHeaders: nil, + ExecutedTxCount: 0, + } + return []data.BaseExecutionResultHandler{meta}, nil + }, + }, + "blockChain": &testscommon.ChainHandlerStub{ + GetCurrentBlockHeaderHashCalled: func() []byte { + return []byte("hash") + }, + GetCurrentBlockHeaderCalled: func() data.HeaderHandler { + return header + }, + }, + "executionResultsInclusionEstimator": &processMocks.InclusionEstimatorMock{ + DecideCalled: func(lastNotarised *estimator.LastExecutionResultForInclusion, pending []data.BaseExecutionResultHandler, currentHdrTsMs uint64) (allowed int) { + return 1 + }, + }, + "shardCoordinator": &mock.ShardCoordinatorStub{ + SelfIdCalled: func() uint32 { + return 0 + }, + }, + }) + err := sp.AddExecutionResultsOnHeader(&block.HeaderV3{Round: 3}) + require.Error(t, err) + require.Equal(t, process.ErrWrongTypeAssertion, err) + }) + t.Run("will work with valid data", func(t *testing.T) { + t.Parallel() + + baseExecutionResults := &block.BaseExecutionResult{ + HeaderHash: []byte("hash"), + HeaderNonce: 3, + HeaderRound: 1, + RootHash: []byte("rootHash"), + } + header := &block.HeaderV3{ + PrevHash: []byte("prev_hash"), + + LastExecutionResult: &block.ExecutionResultInfo{ + NotarizedInRound: 1, + ExecutionResult: baseExecutionResults, + }, + } + + genesisTimeStampMs := uint64(1000) + roundTime := uint64(100) + roundHandler := &round.RoundHandlerMock{ + GetTimeStampForRoundCalled: func(round uint64) uint64 { + return genesisTimeStampMs + round*roundTime + }, + } + defaultCfg := config.ExecutionResultInclusionEstimatorConfig{ + SafetyMargin: 110, + MaxResultsPerBlock: 10, + } + + executionResult1 := &block.ExecutionResult{ + BaseExecutionResult: &block.BaseExecutionResult{HeaderHash: []byte("hash1"), HeaderNonce: 1, HeaderRound: 2, GasUsed: 100_000_000}, + } + executionResult2 := &block.ExecutionResult{ + BaseExecutionResult: &block.BaseExecutionResult{HeaderHash: []byte("hash2"), HeaderNonce: 2, HeaderRound: 2, GasUsed: 999_000_000}, + } + sp, _ := blproc.ConstructPartialShardBlockProcessorForTest(map[string]interface{}{ + "executionResultsTracker": &testscommonExecutionTrack.ExecutionResultsTrackerStub{ + GetPendingExecutionResultsCalled: func() ([]data.BaseExecutionResultHandler, error) { + return []data.BaseExecutionResultHandler{ + executionResult1, + executionResult2, + }, nil + }, + }, + "blockChain": &testscommon.ChainHandlerStub{ + GetCurrentBlockHeaderHashCalled: func() []byte { + return []byte("hash") + }, + GetCurrentBlockHeaderCalled: func() data.HeaderHandler { + return header + }, + }, + "executionResultsInclusionEstimator": estimator.NewExecutionResultInclusionEstimator(defaultCfg, roundHandler), + "shardCoordinator": &mock.ShardCoordinatorStub{ + SelfIdCalled: func() uint32 { + return 0 + }, + }, + }) + + proposalHeader := &block.HeaderV3{Round: 3} + err := sp.AddExecutionResultsOnHeader(proposalHeader) + + // expected only first pending execution result to be added + require.NoError(t, err) + actualLastExecutionResult := proposalHeader.GetLastExecutionResultHandler().(*block.ExecutionResultInfo) + assert.NotNil(t, actualLastExecutionResult) + assert.Equal(t, actualLastExecutionResult.ExecutionResult, executionResult1.BaseExecutionResult) + assert.NotNil(t, proposalHeader.ExecutionResults) + assert.Len(t, proposalHeader.ExecutionResults, 1) + assert.Equal(t, proposalHeader.ExecutionResults[0], executionResult1) + }) +} + func TestShardProcessor_SelectIncomingMiniBlocks(t *testing.T) { t.Parallel() @@ -1082,16 +1295,280 @@ func TestShardProcessor_VerifyBlockProposal(t *testing.T) { Round: 2, MiniBlockHeaders: []block.MiniBlockHeader{}, } - err = sp.VerifyBlockProposal(header, body, haveTime) - require.Equal(t, expectedError, err) - }) - - t.Run("wait for missing data fails should error", func(t *testing.T) { - t.Parallel() - - coreComponents, dataComponents, bootstrapComponents, statusComponents := createComponentHolderMocks() - currentBlockHeader := &block.HeaderV2{ - Header: &block.Header{}, + err = sp.VerifyBlockProposal(header, body, haveTime) + require.Equal(t, expectedError, err) + }) + + t.Run("wait for missing data fails should error", func(t *testing.T) { + t.Parallel() + + coreComponents, dataComponents, bootstrapComponents, statusComponents := createComponentHolderMocks() + currentBlockHeader := &block.HeaderV2{ + Header: &block.Header{}, + } + _ = dataComponents.BlockChain.SetCurrentBlockHeaderAndRootHash(currentBlockHeader, []byte("root")) + dataComponents.BlockChain.SetCurrentBlockHeaderHash([]byte("hash")) + arguments := CreateMockArguments(coreComponents, dataComponents, bootstrapComponents, statusComponents) + arguments.ExecutionResultsVerifier = &processMocks.ExecutionResultsVerifierMock{ + VerifyHeaderExecutionResultsCalled: func(header data.HeaderHandler) error { + return nil + }, + } + arguments.ExecutionResultsInclusionEstimator = &processMocks.InclusionEstimatorMock{ + DecideCalled: func(lastNotarised *estimator.LastExecutionResultForInclusion, pending []data.BaseExecutionResultHandler, currentHdrTsMs uint64) (allowed int) { + return 0 + }, + } + + arguments.MissingDataResolver = &processMocks.MissingDataResolverMock{ + RequestMissingMetaHeadersCalled: func(shardHeader data.ShardHeaderHandler) error { + return nil + }, + WaitForMissingDataCalled: func(timeout time.Duration) error { + return expectedError + }, + } + + sp, err := blproc.NewShardProcessor(arguments) + require.Nil(t, err) + + body := &block.Body{} + + header := &block.HeaderV3{ + PrevHash: []byte("hash"), + Nonce: 1, + Round: 2, + MiniBlockHeaders: []block.MiniBlockHeader{}, + } + err = sp.VerifyBlockProposal(header, body, haveTime) + require.Equal(t, expectedError, err) + }) + + t.Run("checkEpochCorectnessCrossChain fails should error", func(t *testing.T) { + t.Parallel() + + subcomponents := createSubComponentsForVerifyProposalTest() + subcomponents["epochChangeGracePeriodHandler"] = processMocks.GracePeriodErrStub{} + subcomponents["epochStartTrigger"] = &mock.EpochStartTriggerStub{ + EpochStartRoundCalled: func() uint64 { + return 10 + }, + EpochFinalityAttestingRoundCalled: func() uint64 { + return 15 + }, + } + genesisNonce := uint64(0) + subcomponents["genesisNonce"] = genesisNonce + subcomponents["forkDetector"] = &mock.ForkDetectorMock{ + GetHighestFinalBlockNonceCalled: func() uint64 { + return genesisNonce + }, + } + sp, err := blproc.ConstructPartialShardBlockProcessorForTest(subcomponents) + require.Nil(t, err) + + body := &block.Body{} + + header := &block.HeaderV3{ + PrevHash: []byte("hash"), + Nonce: 1, + Round: 2, + MiniBlockHeaders: []block.MiniBlockHeader{}, + LastExecutionResult: &block.ExecutionResultInfo{ + ExecutionResult: &block.BaseExecutionResult{}, + }, + } + + err = sp.VerifyBlockProposal(header, body, haveTime) + require.Error(t, err) + assert.Equal(t, "epochChangeGracePeriodHandler forced error", err.Error()) + }) + t.Run("checkEpochCorectness fails should error", func(t *testing.T) { + t.Parallel() + + subcomponents := createSubComponentsForVerifyProposalTest() + + sp, err := blproc.ConstructPartialShardBlockProcessorForTest(subcomponents) + require.Nil(t, err) + + body := &block.Body{} + + header := &block.HeaderV3{ + PrevHash: []byte("hash"), + Nonce: 1, + Round: 2, + Epoch: 5, + MiniBlockHeaders: []block.MiniBlockHeader{}, + LastExecutionResult: &block.ExecutionResultInfo{ + ExecutionResult: &block.BaseExecutionResult{}, + }, + } + + err = sp.VerifyBlockProposal(header, body, haveTime) + require.Error(t, err) + assert.Contains(t, err.Error(), "epoch does not match") + }) + t.Run("checkMetaHeadersValidityAndFinalityProposal fails should error", func(t *testing.T) { + t.Parallel() + + subcomponents := createSubComponentsForVerifyProposalTest() + subcomponents["blockTracker"] = &mock.BlockTrackerMock{ + GetLastCrossNotarizedHeaderCalled: func(shardID uint32) (data.HeaderHandler, []byte, error) { + return nil, nil, expectedError + }, + } + + sp, err := blproc.ConstructPartialShardBlockProcessorForTest(subcomponents) + require.Nil(t, err) + + body := &block.Body{} + + header := &block.HeaderV3{ + PrevHash: []byte("hash"), + Nonce: 1, + Round: 2, + MiniBlockHeaders: []block.MiniBlockHeader{}, + LastExecutionResult: &block.ExecutionResultInfo{ + ExecutionResult: &block.BaseExecutionResult{}, + }, + } + + err = sp.VerifyBlockProposal(header, body, haveTime) + require.Error(t, err) + assert.Equal(t, expectedError, err) + }) + t.Run("verifyCrossShardMiniBlockDstMe fails should error", func(t *testing.T) { + t.Parallel() + + headerValidator := &processMocks.HeaderValidatorMock{ + IsHeaderConstructionValidCalled: func(currHdr, prevHdr data.HeaderHandler) error { + return nil + }, + } + subcomponents := createSubComponentsForVerifyProposalTest() + subcomponents["headerValidator"] = headerValidator + subcomponents["hasher"] = &hashingMocks.HasherMock{} + subcomponents["proofsPool"] = &dataRetriever.ProofsPoolMock{ + HasProofCalled: func(shardID uint32, headerHash []byte) bool { + return true + }, + } + + sp, err := blproc.ConstructPartialShardBlockProcessorForTest(subcomponents) + require.Nil(t, err) + + body := &block.Body{} + + metablockHashes := [][]byte{ + []byte("hash1"), + []byte("hash2"), + } + header := &block.HeaderV3{ + PrevHash: []byte("hash"), + Nonce: 1, + Round: 2, + MiniBlockHeaders: []block.MiniBlockHeader{}, + LastExecutionResult: &block.ExecutionResultInfo{ + ExecutionResult: &block.BaseExecutionResult{}, + }, + MetaBlockHashes: metablockHashes, + } + + err = sp.VerifyBlockProposal(header, body, haveTime) + + require.Error(t, err) + // stub will not return meta headers, causing type assertion to fail + expError := errors.New("wrong type assertion") + assert.Equal(t, expError, err) + }) + t.Run("verifyGasLimit fails should error", func(t *testing.T) { + t.Parallel() + + coreComponents, dataComponents, bootstrapComponents, statusComponents := createComponentHolderMocks() + + poolMock, ok := dataComponents.DataPool.(*dataRetriever.PoolsHolderStub) + require.True(t, ok) + poolMock.HeadersCalled = func() retriever.HeadersPool { + return &pool.HeadersPoolStub{ + GetHeaderByHashCalled: func(hash []byte) (data.HeaderHandler, error) { + return &block.HeaderV3{ + LastExecutionResult: &block.ExecutionResultInfo{ + ExecutionResult: &block.BaseExecutionResult{}, + }, + }, nil + }, + } + } + currentBlockHeader := &block.HeaderV3{ + LastExecutionResult: &block.ExecutionResultInfo{ + ExecutionResult: &block.BaseExecutionResult{}, + }, + } + _ = dataComponents.BlockChain.SetCurrentBlockHeaderAndRootHash(currentBlockHeader, []byte("root")) + dataComponents.BlockChain.SetCurrentBlockHeaderHash([]byte("hash")) + arguments := CreateMockArguments(coreComponents, dataComponents, bootstrapComponents, statusComponents) + arguments.ExecutionResultsVerifier = &processMocks.ExecutionResultsVerifierMock{ + VerifyHeaderExecutionResultsCalled: func(header data.HeaderHandler) error { + return nil + }, + } + arguments.ExecutionResultsInclusionEstimator = &processMocks.InclusionEstimatorMock{ + DecideCalled: func(lastNotarised *estimator.LastExecutionResultForInclusion, pending []data.BaseExecutionResultHandler, currentHdrTsMs uint64) (allowed int) { + return 0 + }, + } + arguments.GasComputation = &testscommon.GasComputationMock{ + CheckIncomingMiniBlocksCalled: func(miniBlocks []data.MiniBlockHeaderHandler, transactions map[string][]data.TransactionHandler) (int, int, error) { + return 0, 0, expectedError + }, + } + sp, err := blproc.NewShardProcessor(arguments) + require.Nil(t, err) + + body := &block.Body{} + + header := &block.HeaderV3{ + PrevHash: []byte("hash"), + Nonce: 1, + Round: 2, + MiniBlockHeaders: []block.MiniBlockHeader{}, + LastExecutionResult: &block.ExecutionResultInfo{ + ExecutionResult: &block.BaseExecutionResult{}, + }, + } + err = sp.VerifyBlockProposal(header, body, haveTime) + require.Error(t, err) + require.Equal(t, expectedErr, err) + }) + t.Run("getHeaderHash fails should error", func(t *testing.T) { + t.Parallel() + + coreComponents, dataComponents, bootstrapComponents, statusComponents := createComponentHolderMocks() + + coreComponents.IntMarsh = &mock.MarshalizerStub{ + MarshalCalled: func(obj interface{}) ([]byte, error) { + t.Log("called IntMarsh.Marshal on ", obj) + return nil, expectedErr + }, + } + + poolMock, ok := dataComponents.DataPool.(*dataRetriever.PoolsHolderStub) + require.True(t, ok) + poolMock.HeadersCalled = func() retriever.HeadersPool { + return &pool.HeadersPoolStub{ + GetHeaderByHashCalled: func(hash []byte) (data.HeaderHandler, error) { + return &block.HeaderV3{ + LastExecutionResult: &block.ExecutionResultInfo{ + ExecutionResult: &block.BaseExecutionResult{}, + }, + }, nil + }, + } + } + currentBlockHeader := &block.HeaderV3{ + LastExecutionResult: &block.ExecutionResultInfo{ + ExecutionResult: &block.BaseExecutionResult{}, + }, } _ = dataComponents.BlockChain.SetCurrentBlockHeaderAndRootHash(currentBlockHeader, []byte("root")) dataComponents.BlockChain.SetCurrentBlockHeaderHash([]byte("hash")) @@ -1107,15 +1584,6 @@ func TestShardProcessor_VerifyBlockProposal(t *testing.T) { }, } - arguments.MissingDataResolver = &processMocks.MissingDataResolverMock{ - RequestMissingMetaHeadersCalled: func(shardHeader data.ShardHeaderHandler) error { - return nil - }, - WaitForMissingDataCalled: func(timeout time.Duration) error { - return expectedError - }, - } - sp, err := blproc.NewShardProcessor(arguments) require.Nil(t, err) @@ -1126,9 +1594,13 @@ func TestShardProcessor_VerifyBlockProposal(t *testing.T) { Nonce: 1, Round: 2, MiniBlockHeaders: []block.MiniBlockHeader{}, + LastExecutionResult: &block.ExecutionResultInfo{ + ExecutionResult: &block.BaseExecutionResult{}, + }, } err = sp.VerifyBlockProposal(header, body, haveTime) - require.Equal(t, expectedError, err) + require.Error(t, err) + require.Equal(t, expectedErr, err) }) t.Run("should work", func(t *testing.T) { @@ -1355,6 +1827,55 @@ func TestShardProcessor_CheckMetaHeadersValidityAndFinalityProposal(t *testing.T require.ErrorContains(t, err, process.ErrHeaderNotFinal.Error()) }) + t.Run("GetHeaderByHash error should propagate", func(t *testing.T) { + t.Parallel() + + coreComponents, dataComponents, bootstrapComponents, statusComponents := createComponentHolderMocks() + arguments := CreateMockArguments(coreComponents, dataComponents, bootstrapComponents, statusComponents) + + metaHeader := &block.MetaBlockV3{} + + arguments.HeaderValidator = &processMocks.HeaderValidatorMock{ + IsHeaderConstructionValidCalled: func(currHdr, prevHdr data.HeaderHandler) error { + return nil + }, + } + + arguments.BlockTracker = &mock.BlockTrackerMock{ + GetLastCrossNotarizedHeaderCalled: func(shardID uint32) (data.HeaderHandler, []byte, error) { + return metaHeader, []byte("h"), nil + }, + } + + dataPool, ok := dataComponents.Datapool().(*dataRetriever.PoolsHolderStub) + require.True(t, ok) + + expectedError := errors.New("expected error from GetHeaderByHash") + dataPool.HeadersCalled = func() retriever.HeadersPool { + return &pool.HeadersPoolStub{ + GetHeaderByHashCalled: func(hash []byte) (data.HeaderHandler, error) { + return nil, expectedError + }, + } + } + dataPool.ProofsCalled = func() retriever.ProofsPool { + return &dataRetriever.ProofsPoolMock{ + HasProofCalled: func(shardID uint32, headerHash []byte) bool { + return true + }, + } + } + + sp, _ := blproc.NewShardProcessor(arguments) + + header := &block.HeaderV3{ + MetaBlockHashes: [][]byte{[]byte("hh")}, + } + err := sp.CheckMetaHeadersValidityAndFinalityProposal(header) + require.Error(t, err) + require.ErrorIs(t, err, expectedError) + }) + t.Run("should work", func(t *testing.T) { t.Parallel() @@ -2475,3 +2996,475 @@ func TestShardProcessor_OnProposedBlock(t *testing.T) { require.True(t, wasOnProposedBlockCalled) }) } + +func TestShardProcessor_collectExecutionResults(t *testing.T) { + t.Parallel() + + t.Run("with CreateReceiptsHash error should return error", func(t *testing.T) { + t.Parallel() + + txCoordinator := &testscommon.TransactionCoordinatorMock{ + CreateReceiptsHashCalled: func() ([]byte, error) { + return nil, expectedErr + }, + } + sp, err := blproc.ConstructPartialShardBlockProcessorForTest(map[string]interface{}{ + "txCoordinator": txCoordinator, + "shardCoordinator": &mock.ShardCoordinatorStub{ + SelfIdCalled: func() uint32 { + return 0 + }, + }, + }) + require.Nil(t, err) + + header := &block.HeaderV3{} + _, err = sp.CollectExecutionResults(make([]byte, 0), header, &block.Body{}) + require.Equal(t, expectedErr, err) + }) + + t.Run("with gas used exceeds gas provided should return error", func(t *testing.T) { + t.Parallel() + + subComponents, header, body := createSubComponentsForCollectExecutionResultsTest() + gasProvider := subComponents["gasConsumedProvider"].(*testscommon.GasHandlerStub) + gasProvider.TotalGasProvidedCalled = func() uint64 { + return 10 // less than gas used in test + } + sp, err := blproc.ConstructPartialShardBlockProcessorForTest(subComponents) + require.Nil(t, err) + headerHash := []byte("header hash to be tested") + _, err = sp.CollectExecutionResults(headerHash, header, body) + assert.Equal(t, process.ErrGasUsedExceedsGasProvided, err) + }) + + t.Run("with cacheExecutedMiniBlocks error should return error", func(t *testing.T) { + t.Parallel() + + expectedErr := errors.New("MarshalizerMock generic error") + subComponents, header, body := createSubComponentsForCollectExecutionResultsTest() + + expected_blocks := 10 + marshallerCalled := 0 + marshallerMock := subComponents["marshalizer"].(*mock.MarshalizerMock) + marshaller := &mock.MarshalizerStub{ + MarshalCalled: func(obj interface{}) ([]byte, error) { + // Marshaller is first called for mini block headers, then for executed mini blocks + // We want to fail on the executed mini blocks marshalling + + if marshallerCalled == expected_blocks { + marshallerMock.Fail = true + } + marshallerCalled++ + return marshallerMock.Marshal(obj) + }, + } + subComponents["marshalizer"] = marshaller + sp, err := blproc.ConstructPartialShardBlockProcessorForTest(subComponents) + require.Nil(t, err) + headerHash := []byte("header hash to be tested") + _, err = sp.CollectExecutionResults(headerHash, header, body) + assert.Error(t, err) + assert.Equal(t, expectedErr, err) + }) + + t.Run("with createMiniBlockHeaderHandlers error should return error", func(t *testing.T) { + t.Parallel() + + expectedErr := errors.New("MarshalizerMock generic error") + subComponents, header, body := createSubComponentsForCollectExecutionResultsTest() + + marshaller := subComponents["marshalizer"].(*mock.MarshalizerMock) + marshaller.Fail = true + sp, err := blproc.ConstructPartialShardBlockProcessorForTest(subComponents) + require.Nil(t, err) + headerHash := []byte("header hash to be tested") + _, err = sp.CollectExecutionResults(headerHash, header, body) + assert.Error(t, err) + assert.Equal(t, expectedErr, err) + }) + + t.Run("with cacheIntermediateTxsForHeader error should return error", func(t *testing.T) { + t.Parallel() + + expectedErr := errors.New("cacheIntermediateTxsForHeader error") + subComponents, header, body := createSubComponentsForCollectExecutionResultsTest() + + marshallerMock := subComponents["marshalizer"].(*mock.MarshalizerMock) + marshaller := &mock.MarshalizerStub{ + MarshalCalled: func(obj interface{}) ([]byte, error) { + if _, ok := obj.(map[block.Type]map[string]data.TransactionHandler); ok { + return nil, expectedErr + } else { + return marshallerMock.Marshal(obj) + } + }} + subComponents["marshalizer"] = marshaller + sp, err := blproc.ConstructPartialShardBlockProcessorForTest(subComponents) + require.Nil(t, err) + headerHash := []byte("header hash to be tested") + _, err = sp.CollectExecutionResults(headerHash, header, body) + assert.Error(t, err) + assert.Equal(t, expectedErr, err) + }) + + t.Run("should work", func(t *testing.T) { + t.Parallel() + + subComponents, header, body := createSubComponentsForCollectExecutionResultsTest() + + sp, err := blproc.ConstructPartialShardBlockProcessorForTest(subComponents) + require.Nil(t, err) + + headerHash := []byte("header hash to be tested") + result, err := sp.CollectExecutionResults(headerHash, header, body) + + require.Nil(t, err) + require.NotNil(t, result) + assert.Equal(t, uint64(1350), result.GetGasUsed(), "gas used should be set correctly") + assert.Equal(t, uint32(10), result.GetHeaderEpoch(), "epoch should be 10 as per mock header") + assert.Equal(t, headerHash, result.GetHeaderHash(), "header hash should match input") + assert.Equal(t, uint64(155), result.GetHeaderNonce(), "nonce should be 155 as per mock header") + assert.Equal(t, uint64(2067), result.GetHeaderRound(), "round should be 2067 as per mock header") + assert.Equal(t, []byte("root hash to be tested"), result.GetRootHash(), "root hash should match mock") + + realResult := result.(*block.ExecutionResult) + realResult.GetMiniBlockHeaders() + actual_miniblocks := map[block.Type]int{} + expected_miniblocks := map[block.Type]int{ + block.TxBlock: 4, + block.SmartContractResultBlock: 2, + block.ReceiptBlock: 1, + block.RewardsBlock: 1, + block.InvalidBlock: 1, + block.PeerBlock: 1, + } + for i, mbResult := range realResult.GetMiniBlockHeaders() { + switch mbResult.GetType() { + case block.TxBlock: + actual_miniblocks[block.TxBlock]++ + if mbResult.GetSenderShardID() == uint32(0) && mbResult.GetReceiverShardID() == uint32(0) { + assert.Equal(t, uint32(2), mbResult.GetTxCount(), "same-shard tx miniblock should have 2 txs as per mock") + continue + } + if mbResult.GetSenderShardID() == uint32(0) && mbResult.GetReceiverShardID() == uint32(1) { + assert.Equal(t, uint32(2), mbResult.GetTxCount(), "cross-shard tx miniblock should have 2 txs as per mock") + continue + } + if mbResult.GetSenderShardID() == uint32(1) && mbResult.GetReceiverShardID() == uint32(0) { + assert.Equal(t, uint32(1), mbResult.GetTxCount(), "cross-shard tx miniblock should have 1 tx as per mock") + continue + } + if mbResult.GetSenderShardID() == uint32(2) && mbResult.GetReceiverShardID() == uint32(0) { + assert.Equal(t, uint32(1), mbResult.GetTxCount(), "cross-shard tx miniblock should have 1 tx as per mock") + continue + } + require.Fail(t, "unexpected tx miniblock shard IDs") + + case block.SmartContractResultBlock: + actual_miniblocks[block.SmartContractResultBlock]++ + if mbResult.GetSenderShardID() == uint32(0) && mbResult.GetReceiverShardID() == uint32(1) { + assert.Equal(t, uint32(1), mbResult.GetTxCount(), "outgoing SCR miniblock should have 1 tx as per mock") + continue + } + if mbResult.GetSenderShardID() == uint32(2) && mbResult.GetReceiverShardID() == uint32(0) { + assert.Equal(t, uint32(2), mbResult.GetTxCount(), "incoming SCR miniblock should have 2 txs as per mock") + continue + } + require.Fail(t, "unexpected SCR miniblock shard IDs") + + case block.ReceiptBlock: + actual_miniblocks[block.ReceiptBlock]++ + assert.NotEqual(t, uint32(0), mbResult.GetSenderShardID(), "receipt miniblock should not be self-shard") + assert.Equal(t, uint32(1), mbResult.GetTxCount(), "receipt miniblock should have 1 tx as per mock") + case block.RewardsBlock: + actual_miniblocks[block.RewardsBlock]++ + assert.Equal(t, uint32(1), mbResult.GetTxCount(), "rewards miniblock should have 1 tx as per mock") + case block.InvalidBlock: + actual_miniblocks[block.InvalidBlock]++ + assert.Equal(t, uint32(1), mbResult.GetTxCount(), "invalid miniblock should have 1 tx as per mock") + case block.PeerBlock: + actual_miniblocks[block.PeerBlock]++ + assert.Equal(t, uint32(1), mbResult.GetTxCount(), "peer miniblock should have 1 tx as per mock") + default: + require.Fail(t, "unexpected miniblock type") + } + + fmt.Println("MiniBlockHeader Result ", i, ": type ", mbResult.GetType(), ", sender shard ", mbResult.GetSenderShardID(), ", receiver shard ", mbResult.GetReceiverShardID(), ", tx count ", mbResult.GetTxCount()) + } + assert.Equal(t, expected_miniblocks, actual_miniblocks, "miniblock counts by type should match expected") + assert.Equal(t, big.NewInt(1000), realResult.GetAccumulatedFees(), "accumulated fees should match mock") + assert.Equal(t, big.NewInt(100), realResult.GetDeveloperFees(), "developer fees should match mock") + }) +} + +func createSubComponentsForCollectExecutionResultsTest() (map[string]interface{}, data.HeaderHandler, *block.Body) { + + txCoordinator := &testscommon.TransactionCoordinatorMock{ + GetCreatedMiniBlocksFromMeCalled: func() block.MiniBlockSlice { + return block.MiniBlockSlice{ + // same-shard regular transactions + { + SenderShardID: 0, + ReceiverShardID: 0, + Type: block.TxBlock, + TxHashes: [][]byte{ + []byte("tx_self_3"), + []byte("tx_self_4"), + }, + }, + // cross-shard outgoing transactions + { + SenderShardID: 0, + ReceiverShardID: 1, + Type: block.TxBlock, + TxHashes: [][]byte{ + []byte("tx_cross_3"), + []byte("tx_cross_4"), + }, + }, + // invalid transactions + { + SenderShardID: 0, + ReceiverShardID: 0, + Type: block.InvalidBlock, + TxHashes: [][]byte{ + []byte("tx_invalid_1"), + }, + }, + } + }, + + CreatePostProcessMiniBlocksCalled: func() block.MiniBlockSlice { + return block.MiniBlockSlice{ + // self shard SCRs - should be sanitized out + { + SenderShardID: 0, + ReceiverShardID: 0, + Type: block.SmartContractResultBlock, + TxHashes: [][]byte{ + []byte("scr_self_1"), + []byte("scr_self_2"), + }, + }, + // outgoing SCRs + { + SenderShardID: 0, + ReceiverShardID: 1, + Type: block.SmartContractResultBlock, + TxHashes: [][]byte{ + []byte("scr_out_1"), + }, + }, + // self shard receipts - should be sanitized out + { + SenderShardID: 0, + ReceiverShardID: 0, + Type: block.ReceiptBlock, + TxHashes: [][]byte{ + []byte("rcpt_0"), + }, + }, + } + }, + + CreateReceiptsHashCalled: func() ([]byte, error) { + return []byte("mock_receipts_hash"), nil + }, + GetAllIntermediateTxsCalled: func() map[block.Type]map[string]data.TransactionHandler { + return map[block.Type]map[string]data.TransactionHandler{} + }, + } + + rootHash := []byte("root hash to be tested") + accounts := map[state.AccountsDbIdentifier]state.AccountsAdapter{ + 0: &stateMock.AccountsStub{ + RootHashCalled: func() ([]byte, error) { + return rootHash, nil + }, + }, + } + feeHandler := &mock.FeeAccumulatorStub{ + GetAccumulatedFeesCalled: func() *big.Int { + return big.NewInt(1000) + }, + GetDeveloperFeesCalled: func() *big.Int { + return big.NewInt(100) + }, + } + gasConsumedProvider := &testscommon.GasHandlerStub{ + TotalGasProvidedCalled: func() uint64 { + return 1500 + }, + TotalGasRefundedCalled: func() uint64 { + return 100 + }, + TotalGasPenalizedCalled: func() uint64 { + return 50 + }, + } + subComponents := map[string]interface{}{ + "txCoordinator": txCoordinator, + "shardCoordinator": &mock.ShardCoordinatorStub{ + SelfIdCalled: func() uint32 { + return 0 + }, + }, + "feeHandler": feeHandler, + "gasConsumedProvider": gasConsumedProvider, + "marshalizer": &mock.MarshalizerMock{}, + "hasher": &hashingMocks.HasherMock{}, + "enableEpochsHandler": enableEpochsHandlerMock.NewEnableEpochsHandlerStub(), + "dataPool": initDataPool(), + "accountsDB": accounts, + } + + header, body := createHeaderAndBodyForTestingProcessBlockProposal() + + return subComponents, header, body +} + +func createHeaderAndBodyForTestingProcessBlockProposal() (data.HeaderHandler, *block.Body) { + header := &block.HeaderV3{ + PrevHash: []byte("previousHash"), + Epoch: 10, + Nonce: 155, + Round: 2067, + TxCount: 13, + } + body := &block.Body{ + MiniBlocks: []*block.MiniBlock{ + //Outgoing miniblock from shard + { + SenderShardID: 0, + TxHashes: [][]byte{ + []byte("tx_self_1"), + []byte("tx_self_2"), + []byte("tx_cross_1"), + []byte("tx_cross_2"), + []byte("tx_invalid_1"), + []byte("tx_unexecutable_1"), + }, + }, + // Incoming miniblocks to shard + { + SenderShardID: 1, + ReceiverShardID: 0, + TxHashes: [][]byte{[]byte("tx_incoming_1")}, + }, + { + SenderShardID: 2, + ReceiverShardID: 0, + TxHashes: [][]byte{[]byte("tx_incoming_2")}, + }, + //Incoming SCR miniblock + { + SenderShardID: 2, + ReceiverShardID: 0, + TxHashes: [][]byte{[]byte("tx_scr_in_1"), []byte("tx_scr_in_2")}, + Type: block.SmartContractResultBlock, + }, + // Other types of miniblocks + { + Type: block.RewardsBlock, + SenderShardID: core.MetachainShardId, + ReceiverShardID: 0, + TxHashes: [][]byte{[]byte("reward1")}, + }, + { + Type: block.PeerBlock, + SenderShardID: core.MetachainShardId, + ReceiverShardID: 0, + TxHashes: [][]byte{[]byte("peer1")}, + }, + { + Type: block.ReceiptBlock, + SenderShardID: 2, + ReceiverShardID: 0, + TxHashes: [][]byte{[]byte("rcpt_1")}, + }, + }, + } + return header, body +} + +func createSubComponentsForVerifyProposalTest() map[string]interface{} { + poolMock := initDataPool() + poolMock.HeadersCalled = func() retriever.HeadersPool { + return &pool.HeadersPoolStub{ + GetHeaderByHashCalled: func(hash []byte) (data.HeaderHandler, error) { + return &block.HeaderV3{ + LastExecutionResult: &block.ExecutionResultInfo{ + ExecutionResult: &block.BaseExecutionResult{}, + }, + }, nil + }, + } + } + currentBlockHeader := &block.HeaderV3{ + PrevHash: []byte("hash"), + Nonce: 0, + Round: 1, + ShardID: 0, + LastExecutionResult: &block.ExecutionResultInfo{ + ExecutionResult: &block.BaseExecutionResult{}, + }, + } + + blkc, _ := blockchain.NewBlockChain(&statusHandlerMock.AppStatusHandlerStub{}) + _ = blkc.SetCurrentBlockHeaderAndRootHash(currentBlockHeader, []byte("root")) + blkc.SetCurrentBlockHeaderHash([]byte("hash")) + gracePeriod, _ := graceperiod.NewEpochChangeGracePeriod([]config.EpochChangeGracePeriodByEpoch{{EnableEpoch: 0, GracePeriodInRounds: 1}}) + + return map[string]interface{}{ + "blockChain": blkc, + "dataPool": poolMock, + "shardCoordinator": &mock.ShardCoordinatorStub{ + SelfIdCalled: func() uint32 { + return 0 + }, + }, + "executionResultsVerifier": &processMocks.ExecutionResultsVerifierMock{ + VerifyHeaderExecutionResultsCalled: func(header data.HeaderHandler) error { + return nil + }, + }, + "executionResultsInclusionEstimator": &processMocks.InclusionEstimatorMock{ + DecideCalled: func(lastNotarised *estimator.LastExecutionResultForInclusion, pending []data.BaseExecutionResultHandler, currentHdrTsMs uint64) (allowed int) { + return 0 + }, + }, + "missingDataResolver": &processMocks.MissingDataResolverMock{ + WaitForMissingDataCalled: func(timeout time.Duration) error { + return nil + }, + }, + "epochStartTrigger": &mock.EpochStartTriggerStub{ + EpochStartRoundCalled: func() uint64 { + return 10 + }, + EpochFinalityAttestingRoundCalled: func() uint64 { + return 5 + }, + }, + "enableEpochsHandler": enableEpochsHandlerMock.NewEnableEpochsHandlerStub(), + "epochChangeGracePeriodHandler": gracePeriod, + "blockTracker": &mock.BlockTrackerMock{ + GetLastCrossNotarizedHeaderCalled: func(shardID uint32) (data.HeaderHandler, []byte, error) { + return &block.MetaBlockV3{}, []byte("h"), nil + }, + }, + "gasComputation": &testscommon.GasComputationMock{ + CheckIncomingMiniBlocksCalled: func(miniBlocks []data.MiniBlockHeaderHandler, transactions map[string][]data.TransactionHandler) (int, int, error) { + return len(miniBlocks) - 1, 0, nil // no pending mini blocks left + }, + CheckOutgoingTransactionsCalled: func(txHashes [][]byte, transactions []data.TransactionHandler) ([][]byte, []data.MiniBlockHeaderHandler, error) { + return txHashes, []data.MiniBlockHeaderHandler{&block.MiniBlockHeader{}}, nil // one pending mini block added + }, + }, + "appStatusHandler": &statusHandlerMock.AppStatusHandlerStub{}, + "marshalizer": &mock.MarshalizerMock{}, + "roundHandler": &mock.RoundHandlerMock{}, + } + +} diff --git a/process/block/shardblock_test.go b/process/block/shardblock_test.go index 54c8c9ea5d3..57cc9afec11 100644 --- a/process/block/shardblock_test.go +++ b/process/block/shardblock_test.go @@ -22,8 +22,10 @@ import ( "github.com/multiversx/mx-chain-core-go/data/transaction" "github.com/multiversx/mx-chain-core-go/hashing" "github.com/multiversx/mx-chain-core-go/marshal" + "github.com/multiversx/mx-chain-go/common/graceperiod" "github.com/multiversx/mx-chain-go/testscommon/cache" "github.com/multiversx/mx-chain-go/testscommon/pool" + "github.com/multiversx/mx-chain-go/testscommon/processMocks" vmcommon "github.com/multiversx/mx-chain-vm-common-go" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -4340,6 +4342,99 @@ func TestShardProcessor_RestoreMetaBlockIntoPoolShouldPass(t *testing.T) { assert.Nil(t, err) } +func TestShardPreprocessor_getAllMiniBlockDstMeFromMetaGetLastCrossNotarizedHeaderErrorShouldError(t *testing.T) { + t.Parallel() + sp, err := blproc.ConstructPartialShardBlockProcessorForTest(map[string]interface{}{ + "blockTracker": &mock.BlockTrackerMock{ + GetLastCrossNotarizedHeaderCalled: func(shardID uint32) (data.HeaderHandler, []byte, error) { + return nil, nil, process.ErrNilBlockHeader + }, + }, + }) + require.Nil(t, err) + + metablockHashes := [][]byte{ + []byte("hash1"), + []byte("hash2"), + } + header := &block.HeaderV3{ + PrevHash: []byte("hash"), + Nonce: 1, + Round: 2, + MiniBlockHeaders: []block.MiniBlockHeader{}, + LastExecutionResult: &block.ExecutionResultInfo{ + ExecutionResult: &block.BaseExecutionResult{}, + }, + MetaBlockHashes: metablockHashes, + } + _, err = sp.GetAllMiniBlockDstMeFromMeta(header) + assert.Error(t, err) + assert.ErrorIs(t, err, process.ErrNilBlockHeader) +} + +func TestShardPreprocessor_getAllMiniBlockDstMeFromMetaMissingMetaHeaderShouldError(t *testing.T) { + t.Parallel() + + headerValidator := &processMocks.HeaderValidatorMock{ + IsHeaderConstructionValidCalled: func(currHdr, prevHdr data.HeaderHandler) error { + return nil + }, + } + poolMock := initDataPool() + poolMock.HeadersCalled = func() dataRetriever.HeadersPool { + return &pool.HeadersPoolStub{ + GetHeaderByHashCalled: func(hash []byte) (data.HeaderHandler, error) { + return &block.HeaderV3{ + LastExecutionResult: &block.ExecutionResultInfo{ + ExecutionResult: &block.BaseExecutionResult{}, + }, + }, nil + }, + } + } + + metablockHashes := [][]byte{ + []byte("hash1"), + []byte("hash2"), + } + header := &block.HeaderV3{ + PrevHash: []byte("hash"), + Nonce: 1, + Round: 2, + MiniBlockHeaders: []block.MiniBlockHeader{}, + LastExecutionResult: &block.ExecutionResultInfo{ + ExecutionResult: &block.BaseExecutionResult{}, + }, + MetaBlockHashes: metablockHashes, + } + + sp, err := blproc.ConstructPartialShardBlockProcessorForTest(map[string]interface{}{ + "headerValidator": headerValidator, + "hasher": &hashingMocks.HasherMock{}, + "proofsPool": &dataRetrieverMock.ProofsPoolMock{ + HasProofCalled: func(shardID uint32, headerHash []byte) bool { + return true + }, + }, + "dataPool": poolMock, + "blockTracker": &mock.BlockTrackerMock{ + GetLastCrossNotarizedHeaderCalled: func(shardID uint32) (data.HeaderHandler, []byte, error) { + return &block.MetaBlockV3{}, []byte("h"), nil + }, + }, + "hdrsForCurrBlock": &testscommon.HeadersForBlockMock{ + GetHeaderInfoCalled: func(hash string) (headerForBlock.HeaderInfo, bool) { + return nil, false + }, + }, + }) + require.Nil(t, err) + + mblocks, err := sp.GetAllMiniBlockDstMeFromMeta(header) + assert.Error(t, err) + assert.ErrorIs(t, err, process.ErrMissingHeader) + assert.Nil(t, mblocks) +} func TestShardPreprocessor_getAllMiniBlockDstMeFromMetaShouldPass(t *testing.T) { t.Parallel() @@ -4753,6 +4848,124 @@ func TestShardProcessor_updateStateStorage(t *testing.T) { assert.True(t, cancelPruneWasCalled) } +func TestShardProcessor_checkEpochCorrectnessCrossChain_gracePeriodError(t *testing.T) { + t.Parallel() + + genesisNonce := uint64(0) + gracePeriod := &processMocks.GracePeriodErrStub{} + blockchain := &testscommon.ChainHandlerStub{ + GetCurrentBlockHeaderCalled: func() data.HeaderHandler { + return &block.Header{ + Nonce: 0, + Epoch: 1, + Round: 10, + } + }, + GetGenesisHeaderCalled: func() data.HeaderHandler { + return &block.Header{ + Nonce: 0, + Epoch: 0, + Round: 10, + } + }, + GetGenesisHeaderHashCalled: func() []byte { + return []byte("genesis-hash") + }, + } + + epochStartTrigger := &mock.EpochStartTriggerStub{ + EpochFinalityAttestingRoundCalled: func() uint64 { + return 5 + }, + EpochStartRoundCalled: func() uint64 { + return 1 + }, + MetaEpochCalled: func() uint32 { + return 2 + }, + EpochCalled: func() uint32 { + return 1 + }, + } + + forkDetector := &mock.ForkDetectorMock{ + GetHighestFinalBlockNonceCalled: func() uint64 { + return genesisNonce + }, + } + sp, err := blproc.ConstructPartialShardBlockProcessorForTest(map[string]interface{}{ + "genesisNonce": genesisNonce, + "blockChain": blockchain, + "epochStartTrigger": epochStartTrigger, + "forkDetector": forkDetector, + "epochChangeGracePeriodHandler": gracePeriod, + }) + require.Nil(t, err) + + err = sp.CheckEpochCorrectnessCrossChain() + assert.NotNil(t, err) + assert.Equal(t, "epochChangeGracePeriodHandler forced error", err.Error()) + +} + +func TestShardProcessor_checkEpochCorrectnessCrossChain_FinalizedReached(t *testing.T) { + t.Parallel() + + genesisNonce := uint64(0) + gracePeriod, _ := graceperiod.NewEpochChangeGracePeriod([]config.EpochChangeGracePeriodByEpoch{{EnableEpoch: 0, GracePeriodInRounds: 1}}) + blockchain := &testscommon.ChainHandlerStub{ + GetCurrentBlockHeaderCalled: func() data.HeaderHandler { + return &block.Header{ + Nonce: 0, + Epoch: 1, + Round: 10, + } + }, + GetGenesisHeaderCalled: func() data.HeaderHandler { + return &block.Header{ + Nonce: 0, + Epoch: 0, + Round: 10, + } + }, + GetGenesisHeaderHashCalled: func() []byte { + return []byte("genesis-hash") + }, + } + + epochStartTrigger := &mock.EpochStartTriggerStub{ + EpochFinalityAttestingRoundCalled: func() uint64 { + return 5 + }, + EpochStartRoundCalled: func() uint64 { + return 1 + }, + MetaEpochCalled: func() uint32 { + return 2 + }, + EpochCalled: func() uint32 { + return 1 + }, + } + + forkDetector := &mock.ForkDetectorMock{ + GetHighestFinalBlockNonceCalled: func() uint64 { + return genesisNonce + }, + } + sp, err := blproc.ConstructPartialShardBlockProcessorForTest(map[string]interface{}{ + "genesisNonce": genesisNonce, + "blockChain": blockchain, + "epochStartTrigger": epochStartTrigger, + "forkDetector": forkDetector, + "epochChangeGracePeriodHandler": gracePeriod, + }) + require.Nil(t, err) + + err = sp.CheckEpochCorrectnessCrossChain() + assert.Nil(t, err) +} + func TestShardProcessor_checkEpochCorrectnessCrossChainNilCurrentBlock(t *testing.T) { t.Parallel() @@ -5125,6 +5338,56 @@ func TestShardProcessor_RequestMetaHeadersIfNeededShouldAddHeaderIntoTrackerPool assert.Equal(t, expectedAddedNonces, addedNonces) } +func TestShardProcessor_CheckEpochCorrectnessShouldErrorWhenHeaderEpochBehindCurrentHeader(t *testing.T) { + t.Parallel() + + header := &block.Header{ + Epoch: 1, + } + currentHeader := &block.Header{ + Epoch: 3, + } + blockChain := &testscommon.ChainHandlerStub{ + GetCurrentBlockHeaderCalled: func() data.HeaderHandler { + return currentHeader + }, + } + sp, err := blproc.ConstructPartialShardBlockProcessorForTest(map[string]interface{}{ + "blockChain": blockChain, + }) + require.Nil(t, err) + + err = sp.CheckEpochCorrectness(header) + assert.Error(t, err) + assert.Equal(t, "epoch does not match proposed header with older epoch 1 than blockchain epoch 3", err.Error()) +} + +func TestShardProcessor_CheckEpochCorrectnessShouldErrorWhenIsStartOfEpochButShouldNotBe(t *testing.T) { + t.Parallel() + + // set EpochStartMetaHash non-nil to emulate IsStartOfEpochBlock == true + header := &block.Header{ + Epoch: 3, + EpochStartMetaHash: []byte("start"), + } + currentHeader := &block.Header{ + Epoch: 3, + } + blockChain := &testscommon.ChainHandlerStub{ + GetCurrentBlockHeaderCalled: func() data.HeaderHandler { + return currentHeader + }, + } + sp, err := blproc.ConstructPartialShardBlockProcessorForTest(map[string]interface{}{ + "blockChain": blockChain, + }) + require.Nil(t, err) + + err = sp.CheckEpochCorrectness(header) + assert.Error(t, err) + assert.Equal(t, "epoch does not match proposed header with same epoch 3 as blockchain and it is of epoch start", err.Error()) +} + func TestShardProcessor_CheckEpochCorrectnessShouldRemoveAndRequestStartOfEpochMetaBlockWhenEpochDoesNotMatch(t *testing.T) { t.Parallel() @@ -5200,6 +5463,169 @@ func TestShardProcessor_CheckEpochCorrectnessShouldRemoveAndRequestStartOfEpochM assert.True(t, errors.Is(err, process.ErrEpochDoesNotMatch)) } +func TestShardProcessor_CheckEpochCorrectnessShouldErrorWhenIsHeaderOfInvalidEpoch(t *testing.T) { + t.Parallel() + + header := &block.Header{ + Epoch: 3, + } + currentHeader := &block.Header{ + Epoch: 3, + } + blockChain := &testscommon.ChainHandlerStub{ + GetCurrentBlockHeaderCalled: func() data.HeaderHandler { + return currentHeader + }, + } + epochStartTriggerStub := &mock.EpochStartTriggerStub{ + MetaEpochCalled: func() uint32 { + return 2 + }, + } + sp, err := blproc.ConstructPartialShardBlockProcessorForTest(map[string]interface{}{ + "blockChain": blockChain, + "epochStartTrigger": epochStartTriggerStub, + }) + require.Nil(t, err) + + err = sp.CheckEpochCorrectness(header) + assert.Error(t, err) + assert.Equal(t, "epoch does not match proposed header with epoch too high 3 with trigger in epoch 2", err.Error()) +} + +func TestShardProcessor_CheckEpochCorrectnessShouldErrorWhenEpochChangeGracePeriodHandlerErrors(t *testing.T) { + t.Parallel() + + header := &block.Header{ + Epoch: 3, + } + currentHeader := &block.Header{ + Epoch: 3, + } + blockChain := &testscommon.ChainHandlerStub{ + GetCurrentBlockHeaderCalled: func() data.HeaderHandler { + return currentHeader + }, + } + epochStartTriggerStub := &mock.EpochStartTriggerStub{ + MetaEpochCalled: func() uint32 { + return 3 + }, + } + enableEpochsHandler := &enableEpochsHandlerMock.EnableEpochsHandlerStub{ + IsFlagEnabledInEpochCalled: func(flag core.EnableEpochFlag, epoch uint32) bool { + return false + }, + } + sp, err := blproc.ConstructPartialShardBlockProcessorForTest(map[string]interface{}{ + "blockChain": blockChain, + "epochStartTrigger": epochStartTriggerStub, + "enableEpochsHandler": enableEpochsHandler, + "epochChangeGracePeriodHandler": &processMocks.GracePeriodErrStub{}, + }) + require.Nil(t, err) + + err = sp.CheckEpochCorrectness(header) + assert.Error(t, err) + assert.Equal(t, "epochChangeGracePeriodHandler forced error could not get grace period for epoch 3", err.Error()) +} + +func TestShardProcessor_CheckEpochCorrectnessShouldErrorWhenIsOldEpochAndShouldBeNew(t *testing.T) { + t.Parallel() + + gracePeriod, _ := graceperiod.NewEpochChangeGracePeriod([]config.EpochChangeGracePeriodByEpoch{{EnableEpoch: 0, GracePeriodInRounds: 1}}) + + header := &block.Header{ + Epoch: 3, + Round: 7, + } + currentHeader := &block.Header{ + Epoch: 3, + } + blockChain := &testscommon.ChainHandlerStub{ + GetCurrentBlockHeaderCalled: func() data.HeaderHandler { + return currentHeader + }, + } + + epochStartTriggerStub := &mock.EpochStartTriggerStub{ + MetaEpochCalled: func() uint32 { + return 4 + }, + EpochStartRoundCalled: func() uint64 { + return 5 + }, + EpochFinalityAttestingRoundCalled: func() uint64 { + return 5 + }, + IsEpochStartCalled: func() bool { + return true + }, + } + enableEpochsHandler := &enableEpochsHandlerMock.EnableEpochsHandlerStub{ + IsFlagEnabledInEpochCalled: func(flag core.EnableEpochFlag, epoch uint32) bool { + return true + }, + } + sp, err := blproc.ConstructPartialShardBlockProcessorForTest(map[string]interface{}{ + "blockChain": blockChain, + "epochStartTrigger": epochStartTriggerStub, + "enableEpochsHandler": enableEpochsHandler, + "epochChangeGracePeriodHandler": gracePeriod, + }) + require.Nil(t, err) + + err = sp.CheckEpochCorrectness(header) + assert.Error(t, err) + assert.Equal(t, "epoch does not match proposed header with epoch 3 should be in epoch 4", err.Error()) +} + +func TestShardProcessor_CheckEpochCorrectnessShouldErrorWhenIsNotEpochStartButShouldBe(t *testing.T) { + t.Parallel() + + gracePeriod, _ := graceperiod.NewEpochChangeGracePeriod([]config.EpochChangeGracePeriodByEpoch{{EnableEpoch: 0, GracePeriodInRounds: 1}}) + + header := &block.Header{ + Epoch: 4, + } + currentHeader := &block.Header{ + Epoch: 3, + } + blockChain := &testscommon.ChainHandlerStub{ + GetCurrentBlockHeaderCalled: func() data.HeaderHandler { + return currentHeader + }, + } + + epochStartTriggerStub := &mock.EpochStartTriggerStub{ + MetaEpochCalled: func() uint32 { + return 4 + }, + EpochStartRoundCalled: func() uint64 { + return 5 + }, + EpochFinalityAttestingRoundCalled: func() uint64 { + return 5 + }, + } + enableEpochsHandler := &enableEpochsHandlerMock.EnableEpochsHandlerStub{ + IsFlagEnabledInEpochCalled: func(flag core.EnableEpochFlag, epoch uint32) bool { + return false + }, + } + sp, err := blproc.ConstructPartialShardBlockProcessorForTest(map[string]interface{}{ + "blockChain": blockChain, + "epochStartTrigger": epochStartTriggerStub, + "enableEpochsHandler": enableEpochsHandler, + "epochChangeGracePeriodHandler": gracePeriod, + }) + require.Nil(t, err) + + err = sp.CheckEpochCorrectness(header) + assert.Error(t, err) + assert.Equal(t, "epoch does not match proposed header with new epoch 4 is not of type epoch start", err.Error()) +} + func TestShardProcessor_CreateNewHeaderErrWrongTypeAssertion(t *testing.T) { t.Parallel() @@ -5709,7 +6135,7 @@ func TestVerifyCrossShardMiniBlockDstMe(t *testing.T) { } err := sp.VerifyCrossShardMiniBlockDstMe(header) - require.Nil(t, err) + require.NotNil(t, err) }) t.Run("cannot find header in pool should error", func(t *testing.T) { diff --git a/process/coordinator/processProposal.go b/process/coordinator/processProposal.go index 60f0e6640ba..da05d8dd230 100644 --- a/process/coordinator/processProposal.go +++ b/process/coordinator/processProposal.go @@ -134,8 +134,8 @@ func (tc *transactionCoordinator) CreateMbsCrossShardDstMe( // if not all mini blocks were included, remove them from the miniBlocksAndHashes slice // but add them into pendingMiniBlocksAndHashes - if lastMBIndex != len(mbsSlice) { - pendingMiniBlocksAndHashes = miniBlocksAndHashes[lastMBIndex:] + if lastMBIndex < len(mbsSlice)-1 { + pendingMiniBlocksAndHashes = miniBlocksAndHashes[lastMBIndex+1:] miniBlocksAndHashes = miniBlocksAndHashes[:lastMBIndex+1] } @@ -174,17 +174,17 @@ func (tc *transactionCoordinator) SelectOutgoingTransactions() (selectedTxHashes } func (tc *transactionCoordinator) verifyCreatedMiniBlocksSanity(body *block.Body) error { - intraShardMbs := make([]*block.MiniBlock, 0) + miniblocksFromSelf := make([]*block.MiniBlock, 0) for _, mb := range body.MiniBlocks { if mb.SenderShardID == tc.shardCoordinator.SelfId() { - intraShardMbs = append(intraShardMbs, mb) + miniblocksFromSelf = append(miniblocksFromSelf, mb) } } collectedMbs := tc.GetCreatedMiniBlocksFromMe() unExecutableTransactions := tc.getUnExecutableTransactions() - allTxsInBody, err := collectTransactionsFromMiniBlocks(intraShardMbs) + allTxsInBody, err := collectTransactionsFromMiniBlocks(miniblocksFromSelf) if err != nil { return fmt.Errorf("%w: for body miniBlocks", err) } diff --git a/process/coordinator/processProposal_test.go b/process/coordinator/processProposal_test.go index a25f1f80d53..1b84362e272 100644 --- a/process/coordinator/processProposal_test.go +++ b/process/coordinator/processProposal_test.go @@ -205,6 +205,142 @@ func TestTransactionCoordinator_CreateMbsCrossShardDstMe_MiniBlockProcessing(t * require.False(t, executionPreprocessorCalled) } +func TestTransactionCoordinator_CreateMbsCrossShardDstMe_MiniBlockProcessing_WithGasComputationError(t *testing.T) { + t.Parallel() + + ph := dataRetrieverMock.NewPoolsHolderMock() + tc, err := createMockTransactionCoordinatorForProposalTests(ph) + require.Nil(t, err) + require.NotNil(t, tc) + + td := createHeaderWithMiniBlocksAndTransactions() + + // Mock mini block pool to return the test mini block + tc.miniBlockPool = &cache.CacherStub{ + PeekCalled: func(key []byte) (value interface{}, ok bool) { + if reflect.DeepEqual(key, td.mb1Info.Hash) { + return td.mb1Info.Miniblock, true + } + return nil, false + }, + } + + // Mock the block data requester to return the mini block info + tc.blockDataRequesterProposal = &preprocMocks.BlockDataRequesterStub{ + GetFinalCrossMiniBlockInfoAndRequestMissingCalled: func(header data.HeaderHandler) []*data.MiniBlockInfo { + return td.miniBlockInfos[:1] // Only first mini block + }, + } + + // Mock preprocessor to simulate no missing transactions + proposalPreprocessorCalled := false + tc.preProcProposal.txPreProcessors[block.TxBlock] = &preprocMocks.PreProcessorMock{ + GetTransactionsAndRequestMissingForMiniBlockCalled: func(miniBlock *block.MiniBlock) ([]data.TransactionHandler, int) { + proposalPreprocessorCalled = true + require.Equal(t, td.mb1Info.Miniblock, miniBlock) + return nil, 0 // No missing transactions + }, + } + + // Mock execution preprocessor to ensure it's not called + executionPreprocessorCalled := false + tc.preProcExecution.txPreProcessors[block.TxBlock] = &preprocMocks.PreProcessorMock{ + GetTransactionsAndRequestMissingForMiniBlockCalled: func(miniBlock *block.MiniBlock) ([]data.TransactionHandler, int) { + executionPreprocessorCalled = true + return nil, 0 + }, + } + + tc.gasComputation = &testscommon.GasComputationMock{ + CheckIncomingMiniBlocksCalled: func(miniBlocks []data.MiniBlockHeaderHandler, transactions map[string][]data.TransactionHandler) (int, int, error) { + return 0, 0, errors.New("gas computation error") + }, + } + + miniBlocks, _, numTxs, allAdded, err := tc.CreateMbsCrossShardDstMe(td.hdr, nil) + + require.Error(t, err) + require.Contains(t, err.Error(), "gas computation error") + require.Nil(t, miniBlocks) + require.Equal(t, uint32(0), numTxs) + require.False(t, allAdded) // No mini blocks were processed + + // Verify proposal preprocessor was used, not execution + require.True(t, proposalPreprocessorCalled) + require.False(t, executionPreprocessorCalled) +} + +func TestTransactionCoordinator_CreateMbsCrossShardDstMe_MiniBlockProcessing_WithPendingMiniBlocks(t *testing.T) { + t.Parallel() + + ph := dataRetrieverMock.NewPoolsHolderMock() + tc, err := createMockTransactionCoordinatorForProposalTests(ph) + require.Nil(t, err) + require.NotNil(t, tc) + + td := createHeaderWithMiniBlocksAndTransactions() + + // Mock mini block pool to return the test mini block + tc.miniBlockPool = &cache.CacherStub{ + PeekCalled: func(key []byte) (value interface{}, ok bool) { + if reflect.DeepEqual(key, td.mb1Info.Hash) { + return td.mb1Info.Miniblock, true + } + return td.mb2Info.Miniblock, true + }, + } + + // Mock the block data requester to return the mini block info + tc.blockDataRequesterProposal = &preprocMocks.BlockDataRequesterStub{ + GetFinalCrossMiniBlockInfoAndRequestMissingCalled: func(header data.HeaderHandler) []*data.MiniBlockInfo { + return td.miniBlockInfos + }, + } + + // Mock preprocessor to simulate no missing transactions + proposalPreprocessorCalled := false + tc.preProcProposal.txPreProcessors[block.TxBlock] = &preprocMocks.PreProcessorMock{ + GetTransactionsAndRequestMissingForMiniBlockCalled: func(miniBlock *block.MiniBlock) ([]data.TransactionHandler, int) { + proposalPreprocessorCalled = true + return nil, 0 // No missing transactions + }, + } + + // Mock execution preprocessor to ensure it's not called + executionPreprocessorCalled := false + tc.preProcExecution.txPreProcessors[block.TxBlock] = &preprocMocks.PreProcessorMock{ + GetTransactionsAndRequestMissingForMiniBlockCalled: func(miniBlock *block.MiniBlock) ([]data.TransactionHandler, int) { + executionPreprocessorCalled = true + return nil, 0 + }, + } + + tc.gasComputation = &testscommon.GasComputationMock{ + CheckIncomingMiniBlocksCalled: func(miniBlocks []data.MiniBlockHeaderHandler, transactions map[string][]data.TransactionHandler) (int, int, error) { + return 0, 1, nil // last mb added index is 0, so only first mini block is added, num pendings miniblocks is 1, so the second is pending + }, + } + + miniBlocks, pendingMiniBlocks, numTxs, allAdded, err := tc.CreateMbsCrossShardDstMe(td.hdr, nil) + + require.Nil(t, err) + + require.Equal(t, 1, len(miniBlocks)) + require.Equal(t, td.mb1Info.Miniblock, miniBlocks[0].Miniblock) + require.Equal(t, td.mb1Info.Hash, miniBlocks[0].Hash) + + require.Equal(t, 1, len(pendingMiniBlocks)) + require.Equal(t, td.mb2Info.Miniblock, pendingMiniBlocks[0].Miniblock) + require.Equal(t, td.mb2Info.Hash, pendingMiniBlocks[0].Hash) + + require.Equal(t, uint32(3), numTxs) + require.False(t, allAdded) + + // Verify proposal preprocessor was used, not execution + require.True(t, proposalPreprocessorCalled) + require.False(t, executionPreprocessorCalled) +} + func TestTransactionCoordinator_CreateMbsCrossShardDstMe_SkipShardOnMissingMiniBlock(t *testing.T) { t.Parallel() @@ -489,6 +625,41 @@ func TestTransactionCoordinator_SelectOutgoingTransactions_HandlesNilPreprocesso require.Equal(t, 0, len(txHashes)) } +func TestTransactionCoordinator_SelectOutgoingTransactions_CheckOutgoingTransactionsError(t *testing.T) { + t.Parallel() + + ph := dataRetrieverMock.NewPoolsHolderMock() + tc, err := createMockTransactionCoordinatorForProposalTests(ph) + require.Nil(t, err) + require.NotNil(t, tc) + txHashesType1 := [][]byte{[]byte("tx_hash_1"), []byte("tx_hash_2")} + txHashesType2 := [][]byte{[]byte("tx_hash_3"), []byte("tx_hash_4")} + + // add transactions to the transactions pool + cacheId := process.ShardCacherIdentifier(0, 0) + ph.Transactions().AddData(txHashesType1[0], &transaction.Transaction{SndAddr: []byte("sender1"), Nonce: 0}, 100, cacheId) + ph.Transactions().AddData(txHashesType1[1], &transaction.Transaction{SndAddr: []byte("sender1"), Nonce: 1}, 100, cacheId) + + // add transactions to the unsigned transactions pool + ph.UnsignedTransactions().AddData(txHashesType2[0], &transaction.Transaction{SndAddr: []byte("sender2"), Nonce: 0}, 100, cacheId) + ph.UnsignedTransactions().AddData(txHashesType2[1], &transaction.Transaction{SndAddr: []byte("sender3"), Nonce: 0}, 100, cacheId) + + // Add both block types to the keys + tc.preProcProposal.keysTxPreProcs = []block.Type{block.TxBlock, block.SmartContractResultBlock} + tc.gasComputation = &testscommon.GasComputationMock{ + CheckOutgoingTransactionsCalled: func(txHashes [][]byte, transactions []data.TransactionHandler) ([][]byte, []data.MiniBlockHeaderHandler, error) { + return nil, nil, errors.New("test error in CheckOutgoingTransactions") + }, + } + + require.NotPanics(t, func() { + selectedTxHashes, selectedPendingIncomingMiniBlocks := tc.SelectOutgoingTransactions() + // Function should continue and return empty slice despite error + require.Nil(t, selectedTxHashes) + require.Nil(t, selectedPendingIncomingMiniBlocks) + }) +} + func TestTransactionCoordinator_CreateMbsCrossShardDstMe_ProcessedMiniBlocksInfo(t *testing.T) { t.Parallel() @@ -599,7 +770,139 @@ func TestTransactionCoordinator_verifyCreatedMiniBlocksSanity(t *testing.T) { err = tc.verifyCreatedMiniBlocksSanity(body) require.True(t, errors.Is(err, process.ErrDuplicatedTransaction)) }) - t.Run("should work", func(t *testing.T) { + + t.Run("error adding un-executable transactions to the collected transactions - should error", func(t *testing.T) { + t.Parallel() + + ph := dataRetrieverMock.NewPoolsHolderMock() + tc, err := createMockTransactionCoordinatorForProposalTests(ph) + + tc.preProcExecution.txPreProcessors[block.TxBlock] = &preprocMocks.PreProcessorMock{ + GetUnExecutableTransactionsCalled: func() map[string]struct{} { + return map[string]struct{}{ + string([]byte("tx_unexec_hash")): {}, + } + }, + // make the same preprocessor also report the created miniblocks from me + GetCreatedMiniBlocksFromMeCalled: func() block.MiniBlockSlice { + return block.MiniBlockSlice{ + &block.MiniBlock{ + SenderShardID: 0, + ReceiverShardID: 0, + TxHashes: [][]byte{ + []byte("tx_unexec_hash"), + }, + }, + } + }, + } + require.Nil(t, err) + require.NotNil(t, tc) + + body := &block.Body{ + MiniBlocks: []*block.MiniBlock{ + { + SenderShardID: 0, + ReceiverShardID: 0, + TxHashes: [][]byte{ + []byte("tx_unexec_hash"), + }, + }, + }, + } + + err = tc.verifyCreatedMiniBlocksSanity(body) + require.True(t, errors.Is(err, process.ErrDuplicatedTransaction)) + }) + + t.Run("transactions mismatch should error", func(t *testing.T) { + t.Parallel() + + ph := dataRetrieverMock.NewPoolsHolderMock() + tc, err := createMockTransactionCoordinatorForProposalTests(ph) + require.Nil(t, err) + require.NotNil(t, tc) + + // preprocessor says we created tx 'a' + tc.preProcExecution.txPreProcessors[block.TxBlock] = &preprocMocks.PreProcessorMock{ + GetUnExecutableTransactionsCalled: func() map[string]struct{} { + return map[string]struct{}{} // no duplicates this time + }, + GetCreatedMiniBlocksFromMeCalled: func() block.MiniBlockSlice { + return block.MiniBlockSlice{ + &block.MiniBlock{ + SenderShardID: 0, + ReceiverShardID: 0, + TxHashes: [][]byte{ + []byte("tx_a"), + }, + }, + } + }, + } + + // but the block body includes tx 'b' + body := &block.Body{ + MiniBlocks: []*block.MiniBlock{ + { + SenderShardID: 0, + ReceiverShardID: 0, + TxHashes: [][]byte{ + []byte("tx_b"), + }, + }, + }, + } + + err = tc.verifyCreatedMiniBlocksSanity(body) + require.True(t, errors.Is(err, process.ErrTransactionsMismatch)) + }) + + t.Run("verifyCreatedMiniBlocksSanity should error on collectTransactionsFromMiniBlocks", func(t *testing.T) { + t.Parallel() + + ph := dataRetrieverMock.NewPoolsHolderMock() + tc, err := createMockTransactionCoordinatorForProposalTests(ph) + require.NoError(t, err) + require.NotNil(t, tc) + + tc.preProcExecution.txPreProcessors[block.TxBlock] = &preprocMocks.PreProcessorMock{ + GetCreatedMiniBlocksFromMeCalled: func() block.MiniBlockSlice { + return block.MiniBlockSlice{ + { + SenderShardID: 0, + ReceiverShardID: 0, + TxHashes: [][]byte{ + []byte("tx_dup"), + []byte("tx_dup"), // duplicate triggers collectTransactionsFromMiniBlocks error + }, + }, + } + }, + GetUnExecutableTransactionsCalled: func() map[string]struct{} { + return nil + }, + } + + body := &block.Body{ + MiniBlocks: []*block.MiniBlock{ + { + SenderShardID: 0, + ReceiverShardID: 0, + TxHashes: [][]byte{ + []byte("tx_dup"), + }, + }, + }, + } + + err = tc.verifyCreatedMiniBlocksSanity(body) + require.Error(t, err) + require.Contains(t, err.Error(), "for created miniBlocks") + require.True(t, errors.Is(err, process.ErrDuplicatedTransaction)) + }) + + t.Run("should work empty body", func(t *testing.T) { t.Parallel() ph := dataRetrieverMock.NewPoolsHolderMock() @@ -615,6 +918,55 @@ func TestTransactionCoordinator_verifyCreatedMiniBlocksSanity(t *testing.T) { err = tc.verifyCreatedMiniBlocksSanity(body) require.NoError(t, err) }) + t.Run("should work with unexecutable transactions", func(t *testing.T) { + t.Parallel() + + ph := dataRetrieverMock.NewPoolsHolderMock() + tc, err := createMockTransactionCoordinatorForProposalTests(ph) + require.NoError(t, err) + require.NotNil(t, tc) + + // first mini-block tx3 is un-executable + mb1 := &block.MiniBlock{ + SenderShardID: 0, + Type: block.TxBlock, + TxHashes: [][]byte{[]byte("tx1"), []byte("tx2"), []byte("tx_unexec")}, + } + // second mini-block incomming - will be ignored for created txs verification + mb2 := &block.MiniBlock{ + SenderShardID: 1, + ReceiverShardID: 0, + Type: block.TxBlock, + TxHashes: [][]byte{[]byte("tx4")}, + } + + body := &block.Body{ + MiniBlocks: []*block.MiniBlock{mb1, mb2}, + } + + // Preprocessor still returns all created mini-blocks + tc.preProcExecution.txPreProcessors[block.TxBlock] = &preprocMocks.PreProcessorMock{ + GetCreatedMiniBlocksFromMeCalled: func() block.MiniBlockSlice { + return block.MiniBlockSlice{ + { + SenderShardID: 0, + ReceiverShardID: 0, + Type: block.TxBlock, + TxHashes: [][]byte{[]byte("tx1"), []byte("tx2")}, + }, + } + }, + GetUnExecutableTransactionsCalled: func() map[string]struct{} { + return map[string]struct{}{ + string([]byte("tx_unexec")): {}, + } + }, + } + + err = tc.verifyCreatedMiniBlocksSanity(body) + require.NoError(t, err) + }) + } func createPreProcessorContainerWithPoolsHolder(poolsHolder dataRetriever.PoolsHolder) process.PreProcessorsContainer { diff --git a/process/coordinator/process_test.go b/process/coordinator/process_test.go index da316d2b668..44c6e436439 100644 --- a/process/coordinator/process_test.go +++ b/process/coordinator/process_test.go @@ -269,7 +269,7 @@ func createMockTransactionCoordinatorArguments() ArgTransactionCoordinator { return txHashes, nil, nil }, CheckIncomingMiniBlocksCalled: func(miniBlocks []data.MiniBlockHeaderHandler, transactions map[string][]data.TransactionHandler) (int, int, error) { - return len(miniBlocks), 0, nil + return len(miniBlocks) - 1, 0, nil }, }, } diff --git a/testscommon/factory/genericPartialComponent.go b/testscommon/factory/genericPartialComponent.go new file mode 100644 index 00000000000..20be6e9db62 --- /dev/null +++ b/testscommon/factory/genericPartialComponent.go @@ -0,0 +1,66 @@ +package factory + +import ( + "fmt" + "reflect" + "unsafe" +) + +// ConstructPartialComponentForTest initializes the given component by setting its subcomponents +func ConstructPartialComponentForTest(component interface{}, subcomponents map[string]interface{}) error { + + // initialize nil pointer fields + rv := reflect.ValueOf(component).Elem() + for i := 0; i < rv.NumField(); i++ { + field := rv.Field(i) + ft := rv.Type().Field(i) + + // handle embedded *structs that are nil + if ft.Anonymous && field.Kind() == reflect.Ptr && field.IsNil() && field.Type().Elem().Kind() == reflect.Struct { + newVal := reflect.New(field.Type().Elem()) + if !field.CanSet() { + field = reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem() + } + field.Set(newVal) + } + } + + // set subcomponents + setField := func(target any, name string, component any) error { + rv := reflect.ValueOf(target).Elem() + field := rv.FieldByName(name) + if !field.IsValid() { + return fmt.Errorf("invalid field: %s", name) + } + if !field.CanSet() { + // bypass export check (ok in tests, same package) + field = reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem() + } + + val := reflect.ValueOf(component) + switch { + case val.Type().AssignableTo(field.Type()): + field.Set(val) + case val.Type().ConvertibleTo(field.Type()): + field.Set(val.Convert(field.Type())) + case val.Kind() != reflect.Ptr && field.Kind() == reflect.Ptr && val.Type().AssignableTo(field.Type().Elem()): + ptr := reflect.New(val.Type()) + ptr.Elem().Set(val) + field.Set(ptr) + case val.Kind() != reflect.Ptr && field.Kind() == reflect.Ptr && val.Type().ConvertibleTo(field.Type().Elem()): + ptr := reflect.New(field.Type().Elem()) + ptr.Elem().Set(val.Convert(field.Type().Elem())) + field.Set(ptr) + default: + return fmt.Errorf("cannot set field %s (got %s, expected %s)", name, val.Type(), field.Type()) + } + return nil + } + + for name, subComponent := range subcomponents { + if err := setField(component, name, subComponent); err != nil { + return err + } + } + return nil +} diff --git a/testscommon/processMocks/gracePeriodErrStub.go b/testscommon/processMocks/gracePeriodErrStub.go new file mode 100644 index 00000000000..79627f99fb1 --- /dev/null +++ b/testscommon/processMocks/gracePeriodErrStub.go @@ -0,0 +1,13 @@ +package processMocks + +import "errors" + +type GracePeriodErrStub struct{} + +// GetGracePeriodForEpoch always returns an error. +func (GracePeriodErrStub) GetGracePeriodForEpoch(_ uint32) (uint32, error) { + return 0, errors.New("epochChangeGracePeriodHandler forced error") +} + +// IsInterfaceNil - +func (GracePeriodErrStub) IsInterfaceNil() bool { return false }