diff --git a/common/common.go b/common/common.go index 4568cf5baa..8c115831af 100644 --- a/common/common.go +++ b/common/common.go @@ -640,3 +640,13 @@ func GetFeePayer(tx data.TransactionHandler) []byte { return tx.GetSndAddr() } + +// IsContendedHeader returns true if rounds were skipped between the parent and the header, so a +// competing proof could exist at the header's nonce in a skipped round +func IsContendedHeader(header data.HeaderHandler, parentHeader data.HeaderHandler) bool { + if check.IfNil(header) || check.IfNil(parentHeader) { + return false + } + + return header.GetRound() > parentHeader.GetRound()+1 +} diff --git a/common/common_test.go b/common/common_test.go index 11bd2b3b97..eeef9af34b 100644 --- a/common/common_test.go +++ b/common/common_test.go @@ -1372,3 +1372,19 @@ func TestPrepareUnexecutableTxHashesKey(t *testing.T) { hash := []byte("hash") require.Equal(t, []byte("unexecutablehash"), common.PrepareUnexecutableTxHashesKey(hash)) } + +func TestIsContendedHeader(t *testing.T) { + t.Parallel() + + parent := &testscommon.HeaderHandlerStub{RoundField: 1} + + require.False(t, common.IsContendedHeader(nil, parent)) + require.False(t, common.IsContendedHeader(&testscommon.HeaderHandlerStub{RoundField: 5}, nil)) + + // consecutive round: no skipped round to hide a competitor in + require.False(t, common.IsContendedHeader(&testscommon.HeaderHandlerStub{RoundField: 2}, parent)) + + // at least one skipped round: a competing proof could exist there + require.True(t, common.IsContendedHeader(&testscommon.HeaderHandlerStub{RoundField: 3}, parent)) + require.True(t, common.IsContendedHeader(&testscommon.HeaderHandlerStub{RoundField: 10}, parent)) +} diff --git a/factory/mock/blockTrackerStub.go b/factory/mock/blockTrackerStub.go index af71c5dd1d..c91e4fb672 100644 --- a/factory/mock/blockTrackerStub.go +++ b/factory/mock/blockTrackerStub.go @@ -41,6 +41,7 @@ type BlockTrackerStub struct { ShouldAddHeaderCalled func(headerHandler data.HeaderHandler) bool ComputeOwnShardStuckCalled func(lastExecutionResultsInfo data.BaseExecutionResultHandler, currentNonce uint64) IsHeaderQuarantinedCalled func(hash []byte) bool + IsSettledCrossHeaderCalled func(header data.HeaderHandler, headerHash []byte) bool } // AddTrackedHeader - @@ -327,3 +328,11 @@ func (bts *BlockTrackerStub) Close() error { func (bts *BlockTrackerStub) IsInterfaceNil() bool { return bts == nil } + +// IsSettledCrossHeader - +func (bts *BlockTrackerStub) IsSettledCrossHeader(header data.HeaderHandler, headerHash []byte) bool { + if bts.IsSettledCrossHeaderCalled != nil { + return bts.IsSettledCrossHeaderCalled(header, headerHash) + } + return false +} diff --git a/integrationTests/mock/blockTrackerStub.go b/integrationTests/mock/blockTrackerStub.go index 792dc57240..10a8786233 100644 --- a/integrationTests/mock/blockTrackerStub.go +++ b/integrationTests/mock/blockTrackerStub.go @@ -42,6 +42,7 @@ type BlockTrackerStub struct { ShouldAddHeaderCalled func(headerHandler data.HeaderHandler) bool ComputeOwnShardStuckCalled func(lastExecutionResultsInfo data.BaseExecutionResultHandler, currentNonce uint64) IsHeaderQuarantinedCalled func(hash []byte) bool + IsSettledCrossHeaderCalled func(header data.HeaderHandler, headerHash []byte) bool } // AddTrackedHeader - @@ -327,3 +328,11 @@ func (bts *BlockTrackerStub) Close() error { func (bts *BlockTrackerStub) IsInterfaceNil() bool { return bts == nil } + +// IsSettledCrossHeader - +func (bts *BlockTrackerStub) IsSettledCrossHeader(header data.HeaderHandler, headerHash []byte) bool { + if bts.IsSettledCrossHeaderCalled != nil { + return bts.IsSettledCrossHeaderCalled(header, headerHash) + } + return false +} diff --git a/node/mock/blockTrackerStub.go b/node/mock/blockTrackerStub.go index 32993dc7f3..ab8778e6df 100644 --- a/node/mock/blockTrackerStub.go +++ b/node/mock/blockTrackerStub.go @@ -41,6 +41,7 @@ type BlockTrackerStub struct { CheckProofAgainstFinalCalled func(proof data.HeaderProofHandler) error CheckProofAgainstRoundHandlerCalled func(proof data.HeaderProofHandler) error IsHeaderQuarantinedCalled func(hash []byte) bool + IsSettledCrossHeaderCalled func(header data.HeaderHandler, headerHash []byte) bool } // CheckProofAgainstFinal - @@ -325,3 +326,11 @@ func (bts *BlockTrackerStub) Close() error { func (bts *BlockTrackerStub) IsInterfaceNil() bool { return bts == nil } + +// IsSettledCrossHeader - +func (bts *BlockTrackerStub) IsSettledCrossHeader(header data.HeaderHandler, headerHash []byte) bool { + if bts.IsSettledCrossHeaderCalled != nil { + return bts.IsSettledCrossHeaderCalled(header, headerHash) + } + return false +} diff --git a/process/block/baseProcess.go b/process/block/baseProcess.go index 993c6bde9f..a139e4c59a 100644 --- a/process/block/baseProcess.go +++ b/process/block/baseProcess.go @@ -4382,3 +4382,38 @@ func (bp *baseProcessor) pruneTrieForHeadersUnprotected( return nil } + +// isContendedUnsettledCrossHeader applies the R-CROSS referencing gate: a cross-shard header that +// skipped a round after its parent is not includable until a proofed child settles it +func (bp *baseProcessor) isContendedUnsettledCrossHeader(header data.HeaderHandler, parentHeader data.HeaderHandler, headerHash []byte) bool { + if !bp.enableEpochsHandler.IsFlagEnabled(common.SupernovaFlag) { + return false + } + if !common.IsContendedHeader(header, parentHeader) { + return false + } + + return !bp.blockTracker.IsSettledCrossHeader(header, headerHash) +} + +// checkNotContendedUnsettled errors when a referenced cross-shard header is contended and not yet +// settled; the header hash is computed only on the contended path +func (bp *baseProcessor) checkNotContendedUnsettled(header data.HeaderHandler, parentHeader data.HeaderHandler) error { + if !bp.enableEpochsHandler.IsFlagEnabled(common.SupernovaFlag) { + return nil + } + if !common.IsContendedHeader(header, parentHeader) { + return nil + } + + headerHash, err := bp.getHeaderHash(header) + if err != nil { + return err + } + + if !bp.blockTracker.IsSettledCrossHeader(header, headerHash) { + return fmt.Errorf("%w with hash %x", errIncludedContendedUnsettledHeader, headerHash) + } + + return nil +} diff --git a/process/block/errors.go b/process/block/errors.go index a2b9511ee7..d1c554d5e8 100644 --- a/process/block/errors.go +++ b/process/block/errors.go @@ -11,3 +11,5 @@ var errNilPreviousHeader = errors.New("nil previous header") var errInvalidMiniBlocks = errors.New("invalid mini blocks") var errIncludedQuarantinedHeader = errors.New("included quarantined header") + +var errIncludedContendedUnsettledHeader = errors.New("included contended header not yet settled") diff --git a/process/block/metablock.go b/process/block/metablock.go index 88f9d35685..d6d26a36cb 100644 --- a/process/block/metablock.go +++ b/process/block/metablock.go @@ -2226,6 +2226,11 @@ func (mp *metaProcessor) checkShardHeadersValidity(metaHdr data.MetaHeaderHandle if err != nil { return nil, fmt.Errorf("%w : checkShardHeadersValidity -> isHdrConstructionValid", err) } + + err = mp.checkNotContendedUnsettled(shardHdr, lastCrossNotarizedHeader[shardID]) + if err != nil { + return nil, fmt.Errorf("%w : checkShardHeadersValidity", err) + } } lastCrossNotarizedHeader[shardID] = shardHdr diff --git a/process/block/metablockProposal.go b/process/block/metablockProposal.go index 57644ca67c..ac272b353b 100644 --- a/process/block/metablockProposal.go +++ b/process/block/metablockProposal.go @@ -1209,6 +1209,10 @@ func (mp *metaProcessor) checkHeadersSequenceCorrectness(hdrsForShard []ShardHea return fmt.Errorf("%w with hash %x", errIncludedQuarantinedHeader, shardHdrInfo.Hash) } + if mp.isContendedUnsettledCrossHeader(shardHdrInfo.Header, lastNotarizedHeaderInfoForShard.Header, shardHdrInfo.Hash) { + return fmt.Errorf("%w with hash %x", errIncludedContendedUnsettledHeader, shardHdrInfo.Hash) + } + err = mp.headerValidator.IsHeaderConstructionValid(shardHdrInfo.Header, lastNotarizedHeaderInfoForShard.Header) if err != nil { return err diff --git a/process/block/metablockProposal_test.go b/process/block/metablockProposal_test.go index 7f5d605f5a..5d8a738b53 100644 --- a/process/block/metablockProposal_test.go +++ b/process/block/metablockProposal_test.go @@ -31,6 +31,7 @@ import ( "github.com/multiversx/mx-chain-go/process/mock" "github.com/multiversx/mx-chain-go/testscommon" dataRetrieverMock "github.com/multiversx/mx-chain-go/testscommon/dataRetriever" + "github.com/multiversx/mx-chain-go/testscommon/enableEpochsHandlerMock" "github.com/multiversx/mx-chain-go/testscommon/mbSelection" "github.com/multiversx/mx-chain-go/testscommon/pool" "github.com/multiversx/mx-chain-go/testscommon/processMocks" @@ -1788,7 +1789,8 @@ func Test_checkShardHeadersValidityAndFinalityProposal(t *testing.T) { dataPoolMock.SetHeadersPool(headersPoolMock) mp, err := blproc.ConstructPartialMetaBlockProcessorForTest(map[string]interface{}{ - "shardCoordinator": mock.NewOneShardCoordinatorMock(), + "shardCoordinator": mock.NewOneShardCoordinatorMock(), + "enableEpochsHandler": &enableEpochsHandlerMock.EnableEpochsHandlerStub{}, "blockTracker": &mock.BlockTrackerMock{ GetLastCrossNotarizedHeaderCalled: func(_ uint32) (data.HeaderHandler, []byte, error) { return &testscommon.HeaderHandlerStub{}, nil, nil @@ -1905,7 +1907,8 @@ func Test_checkShardHeadersValidityAndFinalityProposal(t *testing.T) { } mp, err := blproc.ConstructPartialMetaBlockProcessorForTest(map[string]interface{}{ - "shardCoordinator": mock.NewOneShardCoordinatorMock(), + "shardCoordinator": mock.NewOneShardCoordinatorMock(), + "enableEpochsHandler": &enableEpochsHandlerMock.EnableEpochsHandlerStub{}, "blockTracker": &mock.BlockTrackerMock{ GetLastCrossNotarizedHeaderCalled: func(_ uint32) (data.HeaderHandler, []byte, error) { return &testscommon.HeaderHandlerStub{}, nil, nil @@ -1950,7 +1953,8 @@ func Test_checkShardHeadersValidityAndFinalityProposal(t *testing.T) { dataPoolMock.SetHeadersPool(headersPoolMock) mp, err := blproc.ConstructPartialMetaBlockProcessorForTest(map[string]interface{}{ - "shardCoordinator": mock.NewOneShardCoordinatorMock(), + "shardCoordinator": mock.NewOneShardCoordinatorMock(), + "enableEpochsHandler": &enableEpochsHandlerMock.EnableEpochsHandlerStub{}, "blockTracker": &mock.BlockTrackerMock{ GetLastCrossNotarizedHeaderCalled: func(_ uint32) (data.HeaderHandler, []byte, error) { return &testscommon.HeaderHandlerStub{}, nil, nil @@ -3583,7 +3587,8 @@ func TestMetaProcessor_checkHeadersSequenceCorrectness(t *testing.T) { return expectedErr }, }, - "blockTracker": &integrationTestsMock.BlockTrackerStub{}, + "enableEpochsHandler": &enableEpochsHandlerMock.EnableEpochsHandlerStub{}, + "blockTracker": &integrationTestsMock.BlockTrackerStub{}, }) require.Nil(t, err) @@ -3622,6 +3627,108 @@ func TestMetaProcessor_checkHeadersSequenceCorrectness(t *testing.T) { require.ErrorContains(t, err, "included quarantined header") }) + t.Run("should return error for contended unsettled header", func(t *testing.T) { + t.Parallel() + + contendedHash := []byte("contended hash") + mp, err := blproc.ConstructPartialMetaBlockProcessorForTest(map[string]interface{}{ + "headerValidator": &processMocks.HeaderValidatorMock{ + IsHeaderConstructionValidCalled: func(currHdr, prevHdr data.HeaderHandler) error { + return nil + }, + }, + "enableEpochsHandler": &enableEpochsHandlerMock.EnableEpochsHandlerStub{ + IsFlagEnabledCalled: func(flag core.EnableEpochFlag) bool { + return flag == common.SupernovaFlag + }, + }, + "blockTracker": &integrationTestsMock.BlockTrackerStub{ + IsSettledCrossHeaderCalled: func(header data.HeaderHandler, headerHash []byte) bool { + return false + }, + }, + }) + require.Nil(t, err) + + err = mp.CheckHeadersSequenceCorrectness([]blproc.ShardHeaderInfo{ + { + // rounds 2-4 skipped after the last notarized shard header + Header: &block.Header{Nonce: 2, Round: 5}, + Hash: contendedHash, + }, + }, blproc.ShardHeaderInfo{ + Header: &block.Header{Nonce: 1, Round: 1}, + }) + require.ErrorContains(t, err, "included contended header not yet settled") + }) + + t.Run("should work for contended settled header", func(t *testing.T) { + t.Parallel() + + mp, err := blproc.ConstructPartialMetaBlockProcessorForTest(map[string]interface{}{ + "headerValidator": &processMocks.HeaderValidatorMock{ + IsHeaderConstructionValidCalled: func(currHdr, prevHdr data.HeaderHandler) error { + return nil + }, + }, + "enableEpochsHandler": &enableEpochsHandlerMock.EnableEpochsHandlerStub{ + IsFlagEnabledCalled: func(flag core.EnableEpochFlag) bool { + return flag == common.SupernovaFlag + }, + }, + "blockTracker": &integrationTestsMock.BlockTrackerStub{ + IsSettledCrossHeaderCalled: func(header data.HeaderHandler, headerHash []byte) bool { + return true + }, + }, + }) + require.Nil(t, err) + + err = mp.CheckHeadersSequenceCorrectness([]blproc.ShardHeaderInfo{ + { + Header: &block.Header{Nonce: 2, Round: 5}, + Hash: []byte("contended settled hash"), + }, + }, blproc.ShardHeaderInfo{ + Header: &block.Header{Nonce: 1, Round: 1}, + }) + require.Nil(t, err) + }) + + t.Run("should work for non-contended header without settlement lookup", func(t *testing.T) { + t.Parallel() + + mp, err := blproc.ConstructPartialMetaBlockProcessorForTest(map[string]interface{}{ + "headerValidator": &processMocks.HeaderValidatorMock{ + IsHeaderConstructionValidCalled: func(currHdr, prevHdr data.HeaderHandler) error { + return nil + }, + }, + "enableEpochsHandler": &enableEpochsHandlerMock.EnableEpochsHandlerStub{ + IsFlagEnabledCalled: func(flag core.EnableEpochFlag) bool { + return flag == common.SupernovaFlag + }, + }, + "blockTracker": &integrationTestsMock.BlockTrackerStub{ + IsSettledCrossHeaderCalled: func(header data.HeaderHandler, headerHash []byte) bool { + require.Fail(t, "settlement must not be checked on the clean path") + return false + }, + }, + }) + require.Nil(t, err) + + err = mp.CheckHeadersSequenceCorrectness([]blproc.ShardHeaderInfo{ + { + Header: &block.Header{Nonce: 2, Round: 2}, + Hash: []byte("clean hash"), + }, + }, blproc.ShardHeaderInfo{ + Header: &block.Header{Nonce: 1, Round: 1}, + }) + require.Nil(t, err) + }) + t.Run("should work", func(t *testing.T) { t.Parallel() @@ -3636,7 +3743,8 @@ func TestMetaProcessor_checkHeadersSequenceCorrectness(t *testing.T) { return nil }, }, - "blockTracker": &integrationTestsMock.BlockTrackerStub{}, + "enableEpochsHandler": &enableEpochsHandlerMock.EnableEpochsHandlerStub{}, + "blockTracker": &integrationTestsMock.BlockTrackerStub{}, }) require.Nil(t, err) diff --git a/process/block/metablock_test.go b/process/block/metablock_test.go index 9dfd9bc05c..98bccf9d35 100644 --- a/process/block/metablock_test.go +++ b/process/block/metablock_test.go @@ -5778,3 +5778,95 @@ func TestMetaProcessor_VerifyShardDataAgainstHeadersFlagGating(t *testing.T) { require.ErrorIs(t, mp.VerifyShardDataAgainstHeaders(metaHdr), process.ErrHeaderShardDataMismatch) }) } + +func TestMetaProcessor_CheckShardHeadersValidityContendedGate(t *testing.T) { + t.Parallel() + + buildProcessorAndHeader := func(settled bool) (interface { + CheckShardHeadersValidity(header *block.MetaBlock) (map[uint32]data.HeaderHandler, error) + }, *block.MetaBlock) { + pool := dataRetrieverMock.NewPoolsHolderMock() + noOfShards := uint32(5) + coreComponents, dataComponents, bootstrapComponents, statusComponents := createMockComponentHolders() + coreComponents.Hash = &hashingMocks.HasherMock{} + coreComponents.EnableEpochsHandlerField = &enableEpochsHandlerMock.EnableEpochsHandlerStub{ + IsFlagEnabledCalled: func(flag core.EnableEpochFlag) bool { + return flag == common.SupernovaFlag + }, + } + dataComponents.DataPool = pool + dataComponents.Storage = initStore() + bootstrapComponents.Coordinator = mock.NewMultiShardsCoordinatorMock(noOfShards) + arguments := createMockMetaArguments(coreComponents, dataComponents, bootstrapComponents, statusComponents) + + startHeaders := createGenesisBlocks(bootstrapComponents.ShardCoordinator()) + blockTracker := mock.NewBlockTrackerMock(bootstrapComponents.ShardCoordinator(), startHeaders) + blockTracker.IsSettledCrossHeaderCalled = func(header data.HeaderHandler, headerHash []byte) bool { + return settled + } + arguments.BlockTracker = blockTracker + + argsHeaderValidator := processBlock.ArgsHeaderValidator{ + Hasher: coreComponents.Hash, + Marshalizer: coreComponents.InternalMarshalizer(), + EnableEpochsHandler: coreComponents.EnableEpochsHandler(), + } + arguments.HeaderValidator, _ = processBlock.NewHeaderValidator(argsHeaderValidator) + + mp, _ := processBlock.NewMetaProcessor(arguments) + + prevRandSeed := []byte("prevrand") + currRandSeed := []byte("currrand") + notarizedHdrs := mp.NotarizedHdrs() + setLastNotarizedHdr(noOfShards, 9, 44, prevRandSeed, notarizedHdrs, arguments.BlockTracker) + + // rounds 10-11 skipped after the last notarized shard header (round 9) -> contended + prevHash, _ := mp.ComputeHeaderHash(mp.LastNotarizedHdrForShard(0).(*block.Header)) + contendedHdr := &block.Header{ + Round: 12, + Nonce: 45, + ShardID: 0, + PrevRandSeed: prevRandSeed, + RandSeed: currRandSeed, + PrevHash: prevHash, + RootHash: []byte("rootHash"), + AccumulatedFees: big.NewInt(0), + DeveloperFees: big.NewInt(0), + } + contendedHash, _ := mp.ComputeHeaderHash(contendedHdr) + pool.Headers().AddHeader(contendedHash, contendedHdr) + + metaHdr := &block.MetaBlock{Round: 20} + metaHdr.ShardInfo = []block.ShardData{{ + ShardID: 0, + HeaderHash: contendedHash, + Round: contendedHdr.Round, + Nonce: contendedHdr.Nonce, + PrevRandSeed: contendedHdr.PrevRandSeed, + PrevHash: contendedHdr.PrevHash, + AccumulatedFees: big.NewInt(0), + DeveloperFees: big.NewInt(0), + TxCount: contendedHdr.TxCount, + }} + + arguments.HeadersForBlock.AddHeaderUsedInBlock(string(contendedHash), contendedHdr) + + return mp, metaHdr + } + + t.Run("contended unsettled referenced shard header should error", func(t *testing.T) { + t.Parallel() + + mp, metaHdr := buildProcessorAndHeader(false) + _, err := mp.CheckShardHeadersValidity(metaHdr) + assert.ErrorContains(t, err, "included contended header not yet settled") + }) + + t.Run("contended settled referenced shard header should pass", func(t *testing.T) { + t.Parallel() + + mp, metaHdr := buildProcessorAndHeader(true) + _, err := mp.CheckShardHeadersValidity(metaHdr) + assert.Nil(t, err) + }) +} diff --git a/process/block/shardblock.go b/process/block/shardblock.go index 6d9f824325..4aa0b0cbd0 100644 --- a/process/block/shardblock.go +++ b/process/block/shardblock.go @@ -542,6 +542,11 @@ func (sp *shardProcessor) checkMetaHeadersValidityAndFinality() error { return fmt.Errorf("%w : checkMetaHeadersValidityAndFinality -> isHdrConstructionValid", err) } + err = sp.checkNotContendedUnsettled(metaHdr, lastCrossNotarizedHeader) + if err != nil { + return fmt.Errorf("%w : checkMetaHeadersValidityAndFinality", err) + } + lastCrossNotarizedHeader = metaHdr } diff --git a/process/block/shardblockProposal.go b/process/block/shardblockProposal.go index 848bade305..ae49f81f4a 100644 --- a/process/block/shardblockProposal.go +++ b/process/block/shardblockProposal.go @@ -827,6 +827,10 @@ func (sp *shardProcessor) checkMetaHeadersValidityAndFinalityProposal(header dat return fmt.Errorf("%w with hash %x", errIncludedQuarantinedHeader, usedMetaHashes[idx]) } + if sp.isContendedUnsettledCrossHeader(metaHeader, lastCrossNotarizedHeader, usedMetaHashes[idx]) { + return fmt.Errorf("%w with hash %x", errIncludedContendedUnsettledHeader, usedMetaHashes[idx]) + } + err = sp.headerValidator.IsHeaderConstructionValid(metaHeader, lastCrossNotarizedHeader) if err != nil { return fmt.Errorf("%w : checkMetaHeadersValidityAndFinalityProposal -> isHdrConstructionValid", err) diff --git a/process/block/shardblockProposal_test.go b/process/block/shardblockProposal_test.go index 8db03867c6..74c9692207 100644 --- a/process/block/shardblockProposal_test.go +++ b/process/block/shardblockProposal_test.go @@ -2782,6 +2782,158 @@ func TestShardProcessor_CheckMetaHeadersValidityAndFinalityProposal(t *testing.T require.ErrorContains(t, err, "included quarantined header") }) + t.Run("contended unsettled meta header should error", func(t *testing.T) { + t.Parallel() + + coreComponents, dataComponents, bootstrapComponents, statusComponents := createComponentHolderMocks() + coreComponents.EnableEpochsHandlerField = &enableEpochsHandlerMock.EnableEpochsHandlerStub{ + IsFlagEnabledCalled: func(flag core.EnableEpochFlag) bool { + return flag == common.SupernovaFlag + }, + } + arguments := CreateMockArguments(coreComponents, dataComponents, bootstrapComponents, statusComponents) + + lastNotarizedMeta := &block.MetaBlockV3{Round: 1, Nonce: 1} + arguments.BlockTracker = &mock.BlockTrackerMock{ + GetLastCrossNotarizedHeaderCalled: func(shardID uint32) (data.HeaderHandler, []byte, error) { + return lastNotarizedMeta, []byte("h"), nil + }, + IsSettledCrossHeaderCalled: func(header data.HeaderHandler, headerHash []byte) bool { + return false + }, + } + arguments.HeaderValidator = &processMocks.HeaderValidatorMock{ + IsHeaderConstructionValidCalled: func(currHdr, prevHdr data.HeaderHandler) error { + require.Fail(t, "should not have been called for a contended unsettled header") + return nil + }, + } + + dataPool, ok := dataComponents.Datapool().(*dataRetriever.PoolsHolderStub) + require.True(t, ok) + dataPool.HeadersCalled = func() retriever.HeadersPool { + return &pool.HeadersPoolStub{ + GetHeaderByHashCalled: func(hash []byte) (data.HeaderHandler, error) { + // rounds 2-4 skipped after the last notarized meta header + return &block.MetaBlockV3{Round: 5, Nonce: 2}, nil + }, + } + } + + sp, _ := blproc.NewShardProcessor(arguments) + + header := &block.HeaderV3{ + MetaBlockHashes: [][]byte{[]byte("hh")}, + } + err := sp.CheckMetaHeadersValidityAndFinalityProposal(header) + require.ErrorContains(t, err, "included contended header not yet settled") + }) + + t.Run("contended settled meta header should pass the gate", func(t *testing.T) { + t.Parallel() + + coreComponents, dataComponents, bootstrapComponents, statusComponents := createComponentHolderMocks() + coreComponents.EnableEpochsHandlerField = &enableEpochsHandlerMock.EnableEpochsHandlerStub{ + IsFlagEnabledCalled: func(flag core.EnableEpochFlag) bool { + return flag == common.SupernovaFlag + }, + } + arguments := CreateMockArguments(coreComponents, dataComponents, bootstrapComponents, statusComponents) + + lastNotarizedMeta := &block.MetaBlockV3{Round: 1, Nonce: 1} + arguments.BlockTracker = &mock.BlockTrackerMock{ + GetLastCrossNotarizedHeaderCalled: func(shardID uint32) (data.HeaderHandler, []byte, error) { + return lastNotarizedMeta, []byte("h"), nil + }, + IsSettledCrossHeaderCalled: func(header data.HeaderHandler, headerHash []byte) bool { + return true + }, + } + arguments.HeaderValidator = &processMocks.HeaderValidatorMock{ + IsHeaderConstructionValidCalled: func(currHdr, prevHdr data.HeaderHandler) error { + return nil + }, + } + + dataPool, ok := dataComponents.Datapool().(*dataRetriever.PoolsHolderStub) + require.True(t, ok) + dataPool.HeadersCalled = func() retriever.HeadersPool { + return &pool.HeadersPoolStub{ + GetHeaderByHashCalled: func(hash []byte) (data.HeaderHandler, error) { + return &block.MetaBlockV3{Round: 5, Nonce: 2}, nil + }, + } + } + 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.Nil(t, err) + }) + + t.Run("non-contended meta header skips the settlement lookup", func(t *testing.T) { + t.Parallel() + + coreComponents, dataComponents, bootstrapComponents, statusComponents := createComponentHolderMocks() + coreComponents.EnableEpochsHandlerField = &enableEpochsHandlerMock.EnableEpochsHandlerStub{ + IsFlagEnabledCalled: func(flag core.EnableEpochFlag) bool { + return flag == common.SupernovaFlag + }, + } + arguments := CreateMockArguments(coreComponents, dataComponents, bootstrapComponents, statusComponents) + + lastNotarizedMeta := &block.MetaBlockV3{Round: 1, Nonce: 1} + arguments.BlockTracker = &mock.BlockTrackerMock{ + GetLastCrossNotarizedHeaderCalled: func(shardID uint32) (data.HeaderHandler, []byte, error) { + return lastNotarizedMeta, []byte("h"), nil + }, + IsSettledCrossHeaderCalled: func(header data.HeaderHandler, headerHash []byte) bool { + require.Fail(t, "settlement must not be checked on the clean path") + return false + }, + } + arguments.HeaderValidator = &processMocks.HeaderValidatorMock{ + IsHeaderConstructionValidCalled: func(currHdr, prevHdr data.HeaderHandler) error { + return nil + }, + } + + dataPool, ok := dataComponents.Datapool().(*dataRetriever.PoolsHolderStub) + require.True(t, ok) + dataPool.HeadersCalled = func() retriever.HeadersPool { + return &pool.HeadersPoolStub{ + GetHeaderByHashCalled: func(hash []byte) (data.HeaderHandler, error) { + return &block.MetaBlockV3{Round: 2, Nonce: 2}, nil + }, + } + } + 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.Nil(t, err) + }) + t.Run("invalid header should error", func(t *testing.T) { t.Parallel() diff --git a/process/block/shardblock_test.go b/process/block/shardblock_test.go index 8039a955cf..37c858247d 100644 --- a/process/block/shardblock_test.go +++ b/process/block/shardblock_test.go @@ -2863,28 +2863,28 @@ func TestShardProcessor_MarshalizedDataToBroadcast_WithSupernova(t *testing.T) { } executionResults := []*block.ExecutionResult{ - &block.ExecutionResult{ + { MiniBlockHeaders: []block.MiniBlockHeader{ - block.MiniBlockHeader{ + { Hash: []byte("mbHash1"), ReceiverShardID: 1, SenderShardID: 0, }, - block.MiniBlockHeader{ + { Hash: []byte("mbHash2"), ReceiverShardID: 1, SenderShardID: 0, }, }, }, - &block.ExecutionResult{ + { MiniBlockHeaders: []block.MiniBlockHeader{ - block.MiniBlockHeader{ + { Hash: []byte("mbHash3"), ReceiverShardID: 1, SenderShardID: 0, }, - block.MiniBlockHeader{ + { Hash: []byte("mbHash4"), ReceiverShardID: 1, SenderShardID: 0, @@ -5285,7 +5285,7 @@ func TestShardProcessor_checkEpochCorrectnessCrossChain_gracePeriodError(t *test genesisNonce := uint64(0) gracePeriod := &processMocks.GracePeriodErrStub{} - blockchain := &testscommon.ChainHandlerStub{ + bc := &testscommon.ChainHandlerStub{ GetCurrentBlockHeaderCalled: func() data.HeaderHandler { return &block.Header{ Nonce: 0, @@ -5327,7 +5327,7 @@ func TestShardProcessor_checkEpochCorrectnessCrossChain_gracePeriodError(t *test } sp, err := blproc.ConstructPartialShardBlockProcessorForTest(map[string]interface{}{ "genesisNonce": genesisNonce, - "blockChain": blockchain, + "blockChain": bc, "epochStartTrigger": epochStartTrigger, "forkDetector": forkDetector, "epochChangeGracePeriodHandler": gracePeriod, @@ -5345,7 +5345,7 @@ func TestShardProcessor_checkEpochCorrectnessCrossChain_FinalizedReached(t *test genesisNonce := uint64(0) gracePeriod, _ := graceperiod.NewEpochChangeGracePeriod([]config.EpochChangeGracePeriodByEpoch{{EnableEpoch: 0, GracePeriodInRounds: 1}}) - blockchain := &testscommon.ChainHandlerStub{ + bc := &testscommon.ChainHandlerStub{ GetCurrentBlockHeaderCalled: func() data.HeaderHandler { return &block.Header{ Nonce: 0, @@ -5387,7 +5387,7 @@ func TestShardProcessor_checkEpochCorrectnessCrossChain_FinalizedReached(t *test } sp, err := blproc.ConstructPartialShardBlockProcessorForTest(map[string]interface{}{ "genesisNonce": genesisNonce, - "blockChain": blockchain, + "blockChain": bc, "epochStartTrigger": epochStartTrigger, "forkDetector": forkDetector, "epochChangeGracePeriodHandler": gracePeriod, @@ -8157,3 +8157,80 @@ func TestShardProcessor_EnsureEpochStartInfoAvailable(t *testing.T) { require.ErrorIs(t, err, process.ErrEpochStartInfoNotAvailable) }) } + +func TestShardProcessor_CheckMetaHeadersValidityAndFinalityContendedGate(t *testing.T) { + t.Parallel() + + hasher := &hashingMocks.HasherMock{} + marshalizer := &mock.MarshalizerMock{} + + buildProcessor := func(settled bool) interface{ CheckMetaHeadersValidityAndFinality() error } { + tdp := dataRetrieverMock.NewPoolsHolderMock() + shardCoordinator := mock.NewMultiShardsCoordinatorMock(3) + genesisBlocks := createGenesisBlocks(shardCoordinator) + + prevMeta := genesisBlocks[core.MetachainShardId] + prevHash, _ := core.CalculateHash(marshalizer, hasher, prevMeta) + // rounds 1-4 skipped after the genesis meta block -> contended + meta1 := &block.MetaBlock{ + Nonce: 1, + Round: 5, + PrevHash: prevHash, + PrevRandSeed: prevMeta.GetRandSeed(), + ShardInfo: make([]block.ShardData, 0), + } + metaBytes, _ := marshalizer.Marshal(meta1) + metaHash1 := hasher.Compute(string(metaBytes)) + tdp.Headers().AddHeader(metaHash1, meta1) + + prevHash, _ = core.CalculateHash(marshalizer, hasher, meta1) + meta2 := &block.MetaBlock{ + Nonce: 2, + Round: 6, + PrevHash: prevHash, + ShardInfo: make([]block.ShardData, 0), + } + metaBytes, _ = marshalizer.Marshal(meta2) + metaHash2 := hasher.Compute(string(metaBytes)) + tdp.Headers().AddHeader(metaHash2, meta2) + + coreComponents, dataComponents, bootstrapComponents, statusComponents := createComponentHolderMocks() + coreComponents.Hash = hasher + coreComponents.IntMarsh = marshalizer + coreComponents.EnableEpochsHandlerField = &enableEpochsHandlerMock.EnableEpochsHandlerStub{ + IsFlagEnabledCalled: func(flag core.EnableEpochFlag) bool { + return flag == common.SupernovaFlag + }, + } + dataComponents.DataPool = tdp + arguments := CreateMockArguments(coreComponents, dataComponents, bootstrapComponents, statusComponents) + + blockTracker := mock.NewBlockTrackerMock(bootstrapComponents.ShardCoordinator(), genesisBlocks) + blockTracker.IsSettledCrossHeaderCalled = func(header data.HeaderHandler, headerHash []byte) bool { + return settled + } + arguments.BlockTracker = blockTracker + + arguments.HeadersForBlock.AddHeaderUsedInBlock(string(metaHash1), meta1) + arguments.HeadersForBlock.AddHeaderNotUsedInBlock(string(metaHash2), meta2) + + sp, _ := blproc.NewShardProcessor(arguments) + return sp + } + + t.Run("contended unsettled referenced meta header should error", func(t *testing.T) { + t.Parallel() + + sp := buildProcessor(false) + err := sp.CheckMetaHeadersValidityAndFinality() + assert.ErrorContains(t, err, "included contended header not yet settled") + }) + + t.Run("contended settled referenced meta header should pass", func(t *testing.T) { + t.Parallel() + + sp := buildProcessor(true) + err := sp.CheckMetaHeadersValidityAndFinality() + assert.Nil(t, err) + }) +} diff --git a/process/interface.go b/process/interface.go index ebd3c394b0..2e9aadf752 100644 --- a/process/interface.go +++ b/process/interface.go @@ -1051,6 +1051,7 @@ type BlockTracker interface { ComputeOwnShardStuck(lastExecutionResultsInfo data.BaseExecutionResultHandler, currentNonce uint64) IsOwnShardStuck() bool IsHeaderQuarantined(hash []byte) bool + IsSettledCrossHeader(header data.HeaderHandler, headerHash []byte) bool Close() error IsInterfaceNil() bool } diff --git a/process/mock/blockTrackerHandlerMock.go b/process/mock/blockTrackerHandlerMock.go index 968e500864..85684f9312 100644 --- a/process/mock/blockTrackerHandlerMock.go +++ b/process/mock/blockTrackerHandlerMock.go @@ -2,6 +2,7 @@ package mock import ( "github.com/multiversx/mx-chain-core-go/data" + "github.com/multiversx/mx-chain-go/process/track" ) @@ -13,6 +14,7 @@ type BlockTrackerHandlerMock struct { SortHeadersFromNonceCalled func(shardID uint32, nonce uint64) ([]data.HeaderHandler, [][]byte) AddHeaderFromPoolCalled func(shardID uint32, nonce uint64) IsHeaderQuarantinedCalled func(hash []byte) bool + IsSettledCrossHeaderCalled func(header data.HeaderHandler, headerHash []byte) bool } // GetSelfHeaders - @@ -69,3 +71,11 @@ func (bthm *BlockTrackerHandlerMock) IsHeaderQuarantined(hash []byte) bool { func (bthm *BlockTrackerHandlerMock) IsInterfaceNil() bool { return bthm == nil } + +// IsSettledCrossHeader - +func (bthm *BlockTrackerHandlerMock) IsSettledCrossHeader(header data.HeaderHandler, headerHash []byte) bool { + if bthm.IsSettledCrossHeaderCalled != nil { + return bthm.IsSettledCrossHeaderCalled(header, headerHash) + } + return false +} diff --git a/process/mock/blockTrackerMock.go b/process/mock/blockTrackerMock.go index 37bfdd6d0e..84e00d8a35 100644 --- a/process/mock/blockTrackerMock.go +++ b/process/mock/blockTrackerMock.go @@ -55,6 +55,7 @@ type BlockTrackerMock struct { ShouldAddHeaderCalled func(headerHandler data.HeaderHandler) bool ComputeOwnShardStuckCalled func(lastExecutionResultsInfo data.BaseExecutionResultHandler, currentNonce uint64) IsHeaderQuarantinedCalled func(hash []byte) bool + IsSettledCrossHeaderCalled func(header data.HeaderHandler, headerHash []byte) bool shardCoordinator sharding.Coordinator @@ -579,3 +580,11 @@ func (btm *BlockTrackerMock) Close() error { func (btm *BlockTrackerMock) IsInterfaceNil() bool { return btm == nil } + +// IsSettledCrossHeader - +func (btm *BlockTrackerMock) IsSettledCrossHeader(header data.HeaderHandler, headerHash []byte) bool { + if btm.IsSettledCrossHeaderCalled != nil { + return btm.IsSettledCrossHeaderCalled(header, headerHash) + } + return false +} diff --git a/process/track/blockProcessor.go b/process/track/blockProcessor.go index b8baacc9e2..222f66a208 100644 --- a/process/track/blockProcessor.go +++ b/process/track/blockProcessor.go @@ -302,6 +302,11 @@ func (bp *blockProcessor) getNextHeader( continue } + if bp.isContendedUnsettledCrossHeader(currHeader, prevHeader, sortedHeadersHashes[i]) { + log.Trace("getNextHeader: skipping contended unsettled cross header", "hash", sortedHeadersHashes[i]) + continue + } + err := bp.headerValidator.IsHeaderConstructionValid(currHeader, prevHeader) if err != nil { continue @@ -318,6 +323,22 @@ func (bp *blockProcessor) getNextHeader( } } +// isContendedUnsettledCrossHeader applies the R-CROSS referencing gate: a cross-shard header that +// skipped a round after its parent is not includable until a proofed child settles it +func (bp *blockProcessor) isContendedUnsettledCrossHeader(header data.HeaderHandler, parentHeader data.HeaderHandler, headerHash []byte) bool { + if header.GetShardID() == bp.shardCoordinator.SelfId() { + return false + } + if !bp.enableEpochsHandler.IsFlagEnabled(common.SupernovaFlag) { + return false + } + if !common.IsContendedHeader(header, parentHeader) { + return false + } + + return !bp.blockTracker.IsSettledCrossHeader(header, headerHash) +} + func (bp *blockProcessor) checkHeaderFinality( header data.HeaderHandler, sortedHeaders []data.HeaderHandler, diff --git a/process/track/blockProcessor_test.go b/process/track/blockProcessor_test.go index cdfbfb55d8..445e78e2dc 100644 --- a/process/track/blockProcessor_test.go +++ b/process/track/blockProcessor_test.go @@ -9,6 +9,8 @@ import ( "github.com/multiversx/mx-chain-core-go/core" + "github.com/multiversx/mx-chain-go/common" + "github.com/multiversx/mx-chain-go/testscommon" "github.com/multiversx/mx-chain-go/testscommon/dataRetriever" "github.com/multiversx/mx-chain-go/testscommon/enableEpochsHandlerMock" @@ -1263,3 +1265,140 @@ func TestShouldProcessReceivedHeader_ShouldWork(t *testing.T) { assert.False(t, bp.ShouldProcessReceivedHeader(&dataBlock.MetaBlock{Nonce: 10})) assert.True(t, bp.ShouldProcessReceivedHeader(&dataBlock.MetaBlock{Nonce: 11})) } + +func TestBlockProcessorComputeLongestChain_ContendedUnsettledCrossHeader(t *testing.T) { + t.Parallel() + + hasherMock := &hashingMocks.HasherMock{} + marshallerMock := &mock.MarshalizerMock{} + + // cross-shard chain (shard 1, self is shard 0): header2 skipped rounds 2-4 after header1 + header1 := &dataBlock.Header{ShardID: 1, Round: 1, Nonce: 1} + h1Bytes, _ := marshallerMock.Marshal(header1) + headerHash1 := hasherMock.Compute(string(h1Bytes)) + + header2 := &dataBlock.Header{ShardID: 1, Round: 5, Nonce: 2, PrevHash: headerHash1} + h2Bytes, _ := marshallerMock.Marshal(header2) + headerHash2 := hasherMock.Compute(string(h2Bytes)) + + header3 := &dataBlock.Header{ShardID: 1, Round: 6, Nonce: 3, PrevHash: headerHash2} + h3Bytes, _ := marshallerMock.Marshal(header3) + headerHash3 := hasherMock.Compute(string(h3Bytes)) + + newBlockProcessor := func(settled func(header data.HeaderHandler, headerHash []byte) bool) interface { + ComputeLongestChain(shardID uint32, header data.HeaderHandler) ([]data.HeaderHandler, [][]byte) + } { + blockProcessorArguments := CreateBlockProcessorMockArguments() + blockProcessorArguments.EnableEpochsHandler = &enableEpochsHandlerMock.EnableEpochsHandlerStub{ + IsFlagEnabledCalled: func(flag core.EnableEpochFlag) bool { + return flag == common.SupernovaFlag + }, + } + blockProcessorArguments.BlockTracker = &mock.BlockTrackerHandlerMock{ + SortHeadersFromNonceCalled: func(shardID uint32, nonce uint64) ([]data.HeaderHandler, [][]byte) { + return []data.HeaderHandler{header2, header3}, [][]byte{headerHash2, headerHash3} + }, + IsSettledCrossHeaderCalled: settled, + } + + bp, _ := track.NewBlockProcessor(blockProcessorArguments) + return bp + } + + t.Run("contended unsettled cross header is excluded", func(t *testing.T) { + t.Parallel() + + bp := newBlockProcessor(func(header data.HeaderHandler, headerHash []byte) bool { + return false + }) + + headers, hashes := bp.ComputeLongestChain(1, header1) + + require.Equal(t, 0, len(headers)) + require.Equal(t, 0, len(hashes)) + }) + + t.Run("contended settled cross header is included", func(t *testing.T) { + t.Parallel() + + bp := newBlockProcessor(func(header data.HeaderHandler, headerHash []byte) bool { + return bytes.Equal(headerHash, headerHash2) + }) + + headers, hashes := bp.ComputeLongestChain(1, header1) + + require.Equal(t, 1, len(headers)) + assert.Equal(t, header2, headers[0]) + assert.Equal(t, headerHash2, hashes[0]) + }) + + t.Run("non-contended cross header is included without settlement lookup", func(t *testing.T) { + t.Parallel() + + header2Clean := &dataBlock.Header{ShardID: 1, Round: 2, Nonce: 2, PrevHash: headerHash1} + h2CleanBytes, _ := marshallerMock.Marshal(header2Clean) + headerHash2Clean := hasherMock.Compute(string(h2CleanBytes)) + header3Clean := &dataBlock.Header{ShardID: 1, Round: 3, Nonce: 3, PrevHash: headerHash2Clean} + h3CleanBytes, _ := marshallerMock.Marshal(header3Clean) + headerHash3Clean := hasherMock.Compute(string(h3CleanBytes)) + + blockProcessorArguments := CreateBlockProcessorMockArguments() + blockProcessorArguments.EnableEpochsHandler = &enableEpochsHandlerMock.EnableEpochsHandlerStub{ + IsFlagEnabledCalled: func(flag core.EnableEpochFlag) bool { + return flag == common.SupernovaFlag + }, + } + blockProcessorArguments.BlockTracker = &mock.BlockTrackerHandlerMock{ + SortHeadersFromNonceCalled: func(shardID uint32, nonce uint64) ([]data.HeaderHandler, [][]byte) { + return []data.HeaderHandler{header2Clean, header3Clean}, [][]byte{headerHash2Clean, headerHash3Clean} + }, + IsSettledCrossHeaderCalled: func(header data.HeaderHandler, headerHash []byte) bool { + require.Fail(t, "settlement must not be checked on the clean path") + return false + }, + } + + bp, _ := track.NewBlockProcessor(blockProcessorArguments) + headers, hashes := bp.ComputeLongestChain(1, header1) + + require.Equal(t, 1, len(headers)) + assert.Equal(t, header2Clean, headers[0]) + assert.Equal(t, headerHash2Clean, hashes[0]) + }) + + t.Run("contended header of the own shard is not gated", func(t *testing.T) { + t.Parallel() + + selfHeader1 := &dataBlock.Header{ShardID: 0, Round: 1, Nonce: 1} + selfH1Bytes, _ := marshallerMock.Marshal(selfHeader1) + selfHeaderHash1 := hasherMock.Compute(string(selfH1Bytes)) + selfHeader2 := &dataBlock.Header{ShardID: 0, Round: 5, Nonce: 2, PrevHash: selfHeaderHash1} + selfH2Bytes, _ := marshallerMock.Marshal(selfHeader2) + selfHeaderHash2 := hasherMock.Compute(string(selfH2Bytes)) + selfHeader3 := &dataBlock.Header{ShardID: 0, Round: 6, Nonce: 3, PrevHash: selfHeaderHash2} + selfH3Bytes, _ := marshallerMock.Marshal(selfHeader3) + selfHeaderHash3 := hasherMock.Compute(string(selfH3Bytes)) + + blockProcessorArguments := CreateBlockProcessorMockArguments() + blockProcessorArguments.EnableEpochsHandler = &enableEpochsHandlerMock.EnableEpochsHandlerStub{ + IsFlagEnabledCalled: func(flag core.EnableEpochFlag) bool { + return flag == common.SupernovaFlag + }, + } + blockProcessorArguments.BlockTracker = &mock.BlockTrackerHandlerMock{ + SortHeadersFromNonceCalled: func(shardID uint32, nonce uint64) ([]data.HeaderHandler, [][]byte) { + return []data.HeaderHandler{selfHeader2, selfHeader3}, [][]byte{selfHeaderHash2, selfHeaderHash3} + }, + IsSettledCrossHeaderCalled: func(header data.HeaderHandler, headerHash []byte) bool { + require.Fail(t, "settlement must not be checked for own shard headers") + return false + }, + } + + bp, _ := track.NewBlockProcessor(blockProcessorArguments) + headers, _ := bp.ComputeLongestChain(0, selfHeader1) + + require.Equal(t, 1, len(headers)) + assert.Equal(t, selfHeader2, headers[0]) + }) +} diff --git a/process/track/contendedHeadersProofPuller.go b/process/track/contendedHeadersProofPuller.go index db65da34cb..7c70f10e25 100644 --- a/process/track/contendedHeadersProofPuller.go +++ b/process/track/contendedHeadersProofPuller.go @@ -23,9 +23,10 @@ type proofPullState struct { } type headCandidate struct { - shardID uint32 - head data.HeaderHandler - parent data.HeaderHandler + shardID uint32 + head data.HeaderHandler + headHash []byte + parent data.HeaderHandler } func (bbt *baseBlockTrack) pullProofsForContendedHeadsLoop(ctx context.Context) { @@ -108,14 +109,11 @@ func (bbt *baseBlockTrack) getContendedUnsettledHeads() map[uint32]data.HeaderHa } } - isContended := candidate.head.GetRound() > parent.GetRound()+1 - if !isContended { + if !common.IsContendedHeader(candidate.head, parent) { continue } - _, err := bbt.proofsPool.GetProofByNonce(candidate.head.GetNonce()+1, candidate.shardID) - isSettledByChildProof := err == nil - if isSettledByChildProof { + if bbt.IsSettledCrossHeader(candidate.head, candidate.headHash) { continue } @@ -133,11 +131,12 @@ func (bbt *baseBlockTrack) getTrackedHeadCandidates() []headCandidate { candidates := make([]headCandidate, 0, len(bbt.headers)) for shardID, headersForShard := range bbt.headers { - head := getHighestNonceLowestRoundHeader(headersForShard) - if check.IfNil(head) || head.GetNonce() == 0 { + headInfo := getHighestNonceLowestRoundHeader(headersForShard) + if headInfo == nil || headInfo.Header.GetNonce() == 0 { continue } + head := headInfo.Header var parent data.HeaderHandler for _, headerInfo := range headersForShard[head.GetNonce()-1] { if string(headerInfo.Hash) == string(head.GetPrevHash()) { @@ -146,14 +145,14 @@ func (bbt *baseBlockTrack) getTrackedHeadCandidates() []headCandidate { } } - candidates = append(candidates, headCandidate{shardID: shardID, head: head, parent: parent}) + candidates = append(candidates, headCandidate{shardID: shardID, head: head, headHash: headInfo.Hash, parent: parent}) } return candidates } -func getHighestNonceLowestRoundHeader(headersForShard map[uint64][]*HeaderInfo) data.HeaderHandler { - var head data.HeaderHandler +func getHighestNonceLowestRoundHeader(headersForShard map[uint64][]*HeaderInfo) *HeaderInfo { + var head *HeaderInfo highestNonce := uint64(0) for nonce, headersInfo := range headersForShard { if nonce < highestNonce { @@ -165,12 +164,12 @@ func getHighestNonceLowestRoundHeader(headersForShard map[uint64][]*HeaderInfo) continue } - keepCurrentHead := nonce == highestNonce && !check.IfNil(head) && headerInfo.Header.GetRound() >= head.GetRound() + keepCurrentHead := nonce == highestNonce && head != nil && headerInfo.Header.GetRound() >= head.Header.GetRound() if keepCurrentHead { continue } - head = headerInfo.Header + head = headerInfo highestNonce = nonce } } diff --git a/process/track/contendedHeadersProofPuller_test.go b/process/track/contendedHeadersProofPuller_test.go index 9c88c6beaf..41f4f99140 100644 --- a/process/track/contendedHeadersProofPuller_test.go +++ b/process/track/contendedHeadersProofPuller_test.go @@ -105,8 +105,8 @@ func TestBaseBlockTrack_PullProofsForContendedHeads(t *testing.T) { sbt.PullProofsForContendedHeads() require.Equal(t, 3, len(*requests)) - // settled by child proof: no more requests; the round is advanced before AddProof, which - // dispatches the tracker's receivedProof subscriber on another goroutine + // settled by a proofed child extending the head: no more requests; the round is advanced + // before AddProof, which dispatches the tracker's receivedProof subscriber on another goroutine roundHandler.RoundIndex = 17 _ = args.PoolsHolder.Proofs().AddProof(&block.HeaderProof{ HeaderHash: []byte("childHash"), @@ -114,6 +114,9 @@ func TestBaseBlockTrack_PullProofsForContendedHeads(t *testing.T) { HeaderRound: 6, HeaderShardId: 0, }) + // the child is known from the headers pool only, so the contended header stays the tracked head + child := &block.Header{Nonce: 3, Round: 6, PrevHash: []byte("headHash")} + args.PoolsHolder.Headers().AddHeader([]byte("childHash"), child) sbt.PullProofsForContendedHeads() require.Equal(t, 3, len(*requests)) }) diff --git a/process/track/crossHeaderSettlement.go b/process/track/crossHeaderSettlement.go new file mode 100644 index 0000000000..cca6ed254e --- /dev/null +++ b/process/track/crossHeaderSettlement.go @@ -0,0 +1,50 @@ +package track + +import ( + "bytes" + + "github.com/multiversx/mx-chain-core-go/core/check" + "github.com/multiversx/mx-chain-core-go/data" +) + +// IsSettledCrossHeader returns true if the header can no longer change: a proofed child extending +// it is known locally, from the tracked headers or from the headers pool +func (bbt *baseBlockTrack) IsSettledCrossHeader(header data.HeaderHandler, headerHash []byte) bool { + if check.IfNil(header) || len(headerHash) == 0 { + return false + } + + shardID := header.GetShardID() + childNonce := header.GetNonce() + 1 + + trackedChildren, trackedChildrenHashes := bbt.GetTrackedHeadersWithNonce(shardID, childNonce) + if bbt.holdProofedChild(trackedChildren, trackedChildrenHashes, headerHash, shardID) { + return true + } + + pooledChildren, pooledChildrenHashes, err := bbt.headersPool.GetHeadersByNonceAndShardId(childNonce, shardID) + if err != nil { + return false + } + + return bbt.holdProofedChild(pooledChildren, pooledChildrenHashes, headerHash, shardID) +} + +func (bbt *baseBlockTrack) holdProofedChild( + children []data.HeaderHandler, + childrenHashes [][]byte, + parentHash []byte, + shardID uint32, +) bool { + for i, child := range children { + if check.IfNil(child) || !bytes.Equal(child.GetPrevHash(), parentHash) { + continue + } + + if bbt.proofsPool.HasProof(shardID, childrenHashes[i]) { + return true + } + } + + return false +} diff --git a/process/track/crossHeaderSettlement_test.go b/process/track/crossHeaderSettlement_test.go new file mode 100644 index 0000000000..dd2365e7df --- /dev/null +++ b/process/track/crossHeaderSettlement_test.go @@ -0,0 +1,88 @@ +package track_test + +import ( + "testing" + + "github.com/multiversx/mx-chain-core-go/data/block" + "github.com/stretchr/testify/require" + + "github.com/multiversx/mx-chain-go/process/track" +) + +func TestBaseBlockTrack_IsSettledCrossHeader(t *testing.T) { + t.Parallel() + + headerHash := []byte("headerHash") + header := &block.Header{Nonce: 5, Round: 10} + + t.Run("nil header or empty hash returns false", func(t *testing.T) { + t.Parallel() + + args, _, _ := createProofPullTrackerScaffold(true) + sbt, err := track.NewShardBlockTrack(args) + require.Nil(t, err) + _ = sbt.Close() + + require.False(t, sbt.IsSettledCrossHeader(nil, headerHash)) + require.False(t, sbt.IsSettledCrossHeader(header, nil)) + }) + + t.Run("no child known returns false", func(t *testing.T) { + t.Parallel() + + args, _, _ := createProofPullTrackerScaffold(true) + sbt, err := track.NewShardBlockTrack(args) + require.Nil(t, err) + _ = sbt.Close() + + require.False(t, sbt.IsSettledCrossHeader(header, headerHash)) + }) + + t.Run("tracked proofed child extending the header settles it", func(t *testing.T) { + t.Parallel() + + args, _, _ := createProofPullTrackerScaffold(true) + sbt, err := track.NewShardBlockTrack(args) + require.Nil(t, err) + _ = sbt.Close() + + child := &block.Header{Nonce: 6, Round: 11, PrevHash: headerHash} + sbt.AddTrackedHeader(child, []byte("childHash")) + + // no proof for the child yet + require.False(t, sbt.IsSettledCrossHeader(header, headerHash)) + + _ = args.PoolsHolder.Proofs().AddProof(&block.HeaderProof{HeaderHash: []byte("childHash"), HeaderNonce: 6, HeaderRound: 11}) + require.True(t, sbt.IsSettledCrossHeader(header, headerHash)) + }) + + t.Run("proofed child of a sibling does not settle the header", func(t *testing.T) { + t.Parallel() + + args, _, _ := createProofPullTrackerScaffold(true) + sbt, err := track.NewShardBlockTrack(args) + require.Nil(t, err) + _ = sbt.Close() + + siblingChild := &block.Header{Nonce: 6, Round: 11, PrevHash: []byte("siblingHash")} + sbt.AddTrackedHeader(siblingChild, []byte("siblingChildHash")) + _ = args.PoolsHolder.Proofs().AddProof(&block.HeaderProof{HeaderHash: []byte("siblingChildHash"), HeaderNonce: 6, HeaderRound: 11}) + + require.False(t, sbt.IsSettledCrossHeader(header, headerHash)) + }) + + t.Run("proofed child known only from the headers pool settles it", func(t *testing.T) { + t.Parallel() + + args, _, _ := createProofPullTrackerScaffold(true) + sbt, err := track.NewShardBlockTrack(args) + require.Nil(t, err) + _ = sbt.Close() + + child := &block.Header{Nonce: 6, Round: 11, PrevHash: headerHash} + args.PoolsHolder.Headers().AddHeader([]byte("childHash"), child) + _ = args.PoolsHolder.Proofs().AddProof(&block.HeaderProof{HeaderHash: []byte("childHash"), HeaderNonce: 6, HeaderRound: 11}) + + require.True(t, sbt.IsSettledCrossHeader(header, headerHash)) + }) +} diff --git a/process/track/interface.go b/process/track/interface.go index 7e4e93dd01..2d07f60295 100644 --- a/process/track/interface.go +++ b/process/track/interface.go @@ -39,6 +39,7 @@ type blockTrackerHandler interface { SortHeadersFromNonce(shardID uint32, nonce uint64) ([]data.HeaderHandler, [][]byte) AddHeaderFromPool(shardID uint32, nonce uint64) IsHeaderQuarantined(hash []byte) bool + IsSettledCrossHeader(header data.HeaderHandler, headerHash []byte) bool IsInterfaceNil() bool }