From 88d62b4a9f71507dba4db77f19e57e67a0d3aa38 Mon Sep 17 00:00:00 2001 From: Adrian Dobrita Date: Fri, 19 Jun 2026 16:18:57 +0300 Subject: [PATCH] prefer headers with proofs --- .../metaResolversContainerFactory.go | 4 + .../shardResolversContainerFactory.go | 3 + dataRetriever/resolvers/headerResolver.go | 34 +++++- .../resolvers/headerResolver_test.go | 106 +++++++++++++++++ process/sync/baseSync.go | 41 ++++++- process/sync/metablock.go | 14 ++- process/sync/metablock_test.go | 70 ++++++++++- process/sync/shardblock_test.go | 110 +++++++++++++++++- 8 files changed, 374 insertions(+), 8 deletions(-) diff --git a/dataRetriever/factory/resolverscontainer/metaResolversContainerFactory.go b/dataRetriever/factory/resolverscontainer/metaResolversContainerFactory.go index d6b1dbe472d..681d506312e 100644 --- a/dataRetriever/factory/resolverscontainer/metaResolversContainerFactory.go +++ b/dataRetriever/factory/resolverscontainer/metaResolversContainerFactory.go @@ -4,6 +4,7 @@ import ( "github.com/multiversx/mx-chain-core-go/core" "github.com/multiversx/mx-chain-core-go/core/check" "github.com/multiversx/mx-chain-core-go/core/throttler" + "github.com/multiversx/mx-chain-go/common" "github.com/multiversx/mx-chain-go/dataRetriever" "github.com/multiversx/mx-chain-go/dataRetriever/factory/containers" @@ -11,6 +12,7 @@ import ( "github.com/multiversx/mx-chain-go/process" "github.com/multiversx/mx-chain-core-go/marshal" + "github.com/multiversx/mx-chain-go/process/factory" ) @@ -229,6 +231,7 @@ func (mrcf *metaResolversContainerFactory) createShardHeaderResolver( Throttler: mrcf.throttler, }, Headers: mrcf.dataPools.Headers(), + Proofs: mrcf.dataPools.Proofs(), HdrStorage: hdrStorer, HeadersNoncesStorage: hdrNonceStore, NonceConverter: mrcf.uint64ByteSliceConverter, @@ -292,6 +295,7 @@ func (mrcf *metaResolversContainerFactory) createMetaChainHeaderResolver( Throttler: mrcf.throttler, }, Headers: mrcf.dataPools.Headers(), + Proofs: mrcf.dataPools.Proofs(), HdrStorage: hdrStorer, HeadersNoncesStorage: hdrNonceStore, NonceConverter: mrcf.uint64ByteSliceConverter, diff --git a/dataRetriever/factory/resolverscontainer/shardResolversContainerFactory.go b/dataRetriever/factory/resolverscontainer/shardResolversContainerFactory.go index deadddea5fb..f4fbd30c1b3 100644 --- a/dataRetriever/factory/resolverscontainer/shardResolversContainerFactory.go +++ b/dataRetriever/factory/resolverscontainer/shardResolversContainerFactory.go @@ -5,6 +5,7 @@ import ( "github.com/multiversx/mx-chain-core-go/core/check" "github.com/multiversx/mx-chain-core-go/core/throttler" "github.com/multiversx/mx-chain-core-go/marshal" + "github.com/multiversx/mx-chain-go/common" "github.com/multiversx/mx-chain-go/dataRetriever" "github.com/multiversx/mx-chain-go/dataRetriever/factory/containers" @@ -180,6 +181,7 @@ func (srcf *shardResolversContainerFactory) generateHeaderResolvers() error { Throttler: srcf.throttler, }, Headers: srcf.dataPools.Headers(), + Proofs: srcf.dataPools.Proofs(), HdrStorage: hdrStorer, HeadersNoncesStorage: hdrNonceStore, NonceConverter: srcf.uint64ByteSliceConverter, @@ -233,6 +235,7 @@ func (srcf *shardResolversContainerFactory) generateMetablockHeaderResolvers() e Throttler: srcf.throttler, }, Headers: srcf.dataPools.Headers(), + Proofs: srcf.dataPools.Proofs(), HdrStorage: hdrStorer, HeadersNoncesStorage: hdrNonceStore, NonceConverter: srcf.uint64ByteSliceConverter, diff --git a/dataRetriever/resolvers/headerResolver.go b/dataRetriever/resolvers/headerResolver.go index 99eb50dbde0..3cc9a21f91f 100644 --- a/dataRetriever/resolvers/headerResolver.go +++ b/dataRetriever/resolvers/headerResolver.go @@ -1,12 +1,14 @@ package resolvers import ( + "bytes" "fmt" "runtime/debug" "sync" "github.com/multiversx/mx-chain-core-go/core" "github.com/multiversx/mx-chain-core-go/core/check" + "github.com/multiversx/mx-chain-core-go/data" "github.com/multiversx/mx-chain-core-go/data/typeConverters" logger "github.com/multiversx/mx-chain-logger-go" @@ -25,6 +27,7 @@ var _ dataRetriever.HeaderResolver = (*HeaderResolver)(nil) type ArgHeaderResolver struct { ArgBaseResolver Headers dataRetriever.HeadersPool + Proofs dataRetriever.ProofsPool HdrStorage storage.Storer HeadersNoncesStorage storage.Storer NonceConverter typeConverters.Uint64ByteSliceConverter @@ -38,6 +41,7 @@ type HeaderResolver struct { baseStorageResolver messageProcessor headers dataRetriever.HeadersPool + proofs dataRetriever.ProofsPool hdrNoncesStorage storage.Storer nonceConverter typeConverters.Uint64ByteSliceConverter mutEpochHandler sync.RWMutex @@ -58,6 +62,7 @@ func NewHeaderResolver(arg ArgHeaderResolver) (*HeaderResolver, error) { TopicResolverSender: arg.SenderResolver, }, headers: arg.Headers, + proofs: arg.Proofs, baseStorageResolver: createBaseStorageResolver(arg.HdrStorage, arg.IsFullHistoryNode), hdrNoncesStorage: arg.HeadersNoncesStorage, nonceConverter: arg.NonceConverter, @@ -82,6 +87,9 @@ func checkArgHeaderResolver(arg ArgHeaderResolver) error { if check.IfNil(arg.Headers) { return dataRetriever.ErrNilHeadersDataPool } + if check.IfNil(arg.Proofs) { + return dataRetriever.ErrNilProofsPool + } if check.IfNil(arg.HdrStorage) { return dataRetriever.ErrNilHeadersStorage } @@ -207,12 +215,13 @@ func (hdrRes *HeaderResolver) resolveHeaderFromNonce(rd *dataRetriever.RequestDa } func (hdrRes *HeaderResolver) searchInCache(nonce uint64) ([]byte, error) { - headers, _, err := hdrRes.headers.GetHeadersByNonceAndShardId(nonce, hdrRes.TargetShardID()) + headers, hashes, err := hdrRes.headers.GetHeadersByNonceAndShardId(nonce, hdrRes.TargetShardID()) if err != nil { return nil, err } - hdr := headers[len(headers)-1] + // serve the proven header, not a more recently received non-final fork at the same nonce + hdr := hdrRes.selectProvenOrLastHeader(nonce, headers, hashes) buff, err := hdrRes.marshalizer.Marshal(hdr) if err != nil { return nil, err @@ -221,6 +230,27 @@ func (hdrRes *HeaderResolver) searchInCache(nonce uint64) ([]byte, error) { return buff, nil } +func (hdrRes *HeaderResolver) selectProvenOrLastHeader( + nonce uint64, + headers []data.HeaderHandler, + hashes [][]byte, +) data.HeaderHandler { + lastHeader := headers[len(headers)-1] + + proof, err := hdrRes.proofs.GetProofByNonce(nonce, hdrRes.TargetShardID()) + if err != nil { + return lastHeader + } + + for i, hash := range hashes { + if bytes.Equal(hash, proof.GetHeaderHash()) { + return headers[i] + } + } + + return lastHeader +} + // resolveHeaderFromHash resolves a header using its key (header hash) func (hdrRes *HeaderResolver) resolveHeaderFromHash(rd *dataRetriever.RequestData) ([]byte, error) { value, err := hdrRes.headers.GetHeaderByHash(rd.Value) diff --git a/dataRetriever/resolvers/headerResolver_test.go b/dataRetriever/resolvers/headerResolver_test.go index 61aebfa2a02..f94acf36ac2 100644 --- a/dataRetriever/resolvers/headerResolver_test.go +++ b/dataRetriever/resolvers/headerResolver_test.go @@ -18,6 +18,7 @@ import ( "github.com/multiversx/mx-chain-go/dataRetriever/mock" "github.com/multiversx/mx-chain-go/dataRetriever/resolvers" "github.com/multiversx/mx-chain-go/p2p" + dataRetrieverMocks "github.com/multiversx/mx-chain-go/testscommon/dataRetriever" "github.com/multiversx/mx-chain-go/testscommon/p2pmocks" storageStubs "github.com/multiversx/mx-chain-go/testscommon/storage" ) @@ -35,6 +36,7 @@ func createMockArgHeaderResolver() resolvers.ArgHeaderResolver { return resolvers.ArgHeaderResolver{ ArgBaseResolver: createMockArgBaseResolver(), Headers: &mock.HeadersCacherStub{}, + Proofs: &dataRetrieverMocks.ProofsPoolMock{}, HdrStorage: &storageStubs.StorerStub{}, HeadersNoncesStorage: &storageStubs.StorerStub{}, NonceConverter: mock.NewNonceHashConverterMock(), @@ -66,6 +68,17 @@ func TestNewHeaderResolver_NilHeadersPoolShouldErr(t *testing.T) { assert.Nil(t, hdrRes) } +func TestNewHeaderResolver_NilProofsPoolShouldErr(t *testing.T) { + t.Parallel() + + arg := createMockArgHeaderResolver() + arg.Proofs = nil + hdrRes, err := resolvers.NewHeaderResolver(arg) + + assert.Equal(t, dataRetriever.ErrNilProofsPool, err) + assert.Nil(t, hdrRes) +} + func TestNewHeaderResolver_NilHeadersStorageShouldErr(t *testing.T) { t.Parallel() @@ -724,6 +737,99 @@ func TestHeaderResolver_ProcessReceivedMessageRequestNonceTypeNotFoundInHdrNonce assert.Len(t, msgID, 0) } +func TestHeaderResolver_ProcessReceivedMessageRequestNonceTypePrefersProvenHeaderFromPool(t *testing.T) { + t.Parallel() + + requestedNonce := uint64(67) + hashX := []byte("hashX-proven") + hashY := []byte("hashY-fork") + headerX := &block.Header{Nonce: requestedNonce, Round: 1} + headerY := &block.Header{Nonce: requestedNonce, Round: 2} + + headers := &mock.HeadersCacherStub{} + headers.GetHeaderByNonceAndShardIdCalled = func(hdrNonce uint64, shardId uint32) ([]data.HeaderHandler, [][]byte, error) { + // Y is the last-seen (more recently received) header for the nonce + return []data.HeaderHandler{headerX, headerY}, [][]byte{hashX, hashY}, nil + } + + arg := createMockArgHeaderResolver() + var sentBuff []byte + arg.SenderResolver = &mock.TopicResolverSenderStub{ + SendCalled: func(buff []byte, peer core.PeerID, source p2p.MessageHandler) error { + sentBuff = buff + return nil + }, + } + arg.Headers = headers + arg.HeadersNoncesStorage = &storageStubs.StorerStub{ + SearchFirstCalled: func(key []byte) ([]byte, error) { + return nil, errKeyNotFound // not committed yet -> serve from cache + }, + } + arg.Proofs = &dataRetrieverMocks.ProofsPoolMock{ + GetProofByNonceCalled: func(headerNonce uint64, shardID uint32) (data.HeaderProofHandler, error) { + return &block.HeaderProof{HeaderHash: hashX, HeaderNonce: headerNonce}, nil + }, + } + hdrRes, _ := resolvers.NewHeaderResolver(arg) + + _, err := hdrRes.ProcessReceivedMessage( + createRequestMsg(dataRetriever.NonceType, arg.NonceConverter.ToByteSlice(requestedNonce)), + fromConnectedPeerId, + &p2pmocks.MessengerStub{}, + ) + + assert.Nil(t, err) + expectedBuff, _ := arg.Marshaller.Marshal(headerX) + assert.Equal(t, expectedBuff, sentBuff) +} + +func TestHeaderResolver_ProcessReceivedMessageRequestNonceTypeNoProofServesLastSeenFromPool(t *testing.T) { + t.Parallel() + + requestedNonce := uint64(67) + hashX := []byte("hashX") + hashY := []byte("hashY") + headerX := &block.Header{Nonce: requestedNonce, Round: 1} + headerY := &block.Header{Nonce: requestedNonce, Round: 2} + + headers := &mock.HeadersCacherStub{} + headers.GetHeaderByNonceAndShardIdCalled = func(hdrNonce uint64, shardId uint32) ([]data.HeaderHandler, [][]byte, error) { + return []data.HeaderHandler{headerX, headerY}, [][]byte{hashX, hashY}, nil + } + + arg := createMockArgHeaderResolver() + var sentBuff []byte + arg.SenderResolver = &mock.TopicResolverSenderStub{ + SendCalled: func(buff []byte, peer core.PeerID, source p2p.MessageHandler) error { + sentBuff = buff + return nil + }, + } + arg.Headers = headers + arg.HeadersNoncesStorage = &storageStubs.StorerStub{ + SearchFirstCalled: func(key []byte) ([]byte, error) { + return nil, errKeyNotFound + }, + } + arg.Proofs = &dataRetrieverMocks.ProofsPoolMock{ + GetProofByNonceCalled: func(headerNonce uint64, shardID uint32) (data.HeaderProofHandler, error) { + return nil, errKeyNotFound // no proof known -> fall back to last-seen + }, + } + hdrRes, _ := resolvers.NewHeaderResolver(arg) + + _, err := hdrRes.ProcessReceivedMessage( + createRequestMsg(dataRetriever.NonceType, arg.NonceConverter.ToByteSlice(requestedNonce)), + fromConnectedPeerId, + &p2pmocks.MessengerStub{}, + ) + + assert.Nil(t, err) + expectedBuff, _ := arg.Marshaller.Marshal(headerY) + assert.Equal(t, expectedBuff, sentBuff) +} + func TestHeaderResolver_ProcessReceivedMessageRequestNonceTypeFoundInHdrNoncePoolCheckRetErr(t *testing.T) { t.Parallel() diff --git a/process/sync/baseSync.go b/process/sync/baseSync.go index 8e88eb84424..df7e9583868 100644 --- a/process/sync/baseSync.go +++ b/process/sync/baseSync.go @@ -804,7 +804,8 @@ func (boot *baseBootstrap) doJobOnSyncBlockFail(bodyHandler data.BodyHandler, he shouldAllowRollback := boot.shouldAllowRollback(lastCommittedBlock, lastCommittedBlockHash) shouldRollBack := isProcessWithError || isSyncWithErrorsLimitReachedInProperRound - if shouldRollBack && shouldAllowRollback { + didRollBack := shouldRollBack && shouldAllowRollback + if didRollBack { if !check.IfNil(headerHandler) { hash := boot.removeHeaderFromPools(headerHandler) boot.forkDetector.RemoveHeader(headerHandler.GetNonce(), hash) @@ -820,6 +821,38 @@ func (boot *baseBootstrap) doJobOnSyncBlockFail(bodyHandler data.BodyHandler, he boot.removeHeadersHigherThanNonceFromPool(boot.getNonceForCurrentBlock()) } } + + // stuck fetching the next header (no processing, no rollback): drop a non-final fork header whose proof + // will never arrive so it gets re-requested + if check.IfNil(headerHandler) && !didRollBack { + boot.removeBlockingUnprovenNextHeader(allowedSyncWithErrorsLimitReached) + } +} + +func (boot *baseBootstrap) removeBlockingUnprovenNextHeader(limitReached bool) { + if !limitReached { + return + } + + nonce := boot.getNonceForNextBlock() + hdr, hash, err := boot.getHeaderFromPoolWithNonce(nonce) + if err != nil { + return + } + if boot.hasProof(hash, hdr) { + return + } + + log.Debug("removeBlockingUnprovenNextHeader: removing unproven header blocking sync, will re-request", + "shard", hdr.GetShardID(), + "nonce", nonce, + "hash", hash, + ) + + boot.headers.RemoveHeaderByHash(hash) + boot.forkDetector.RemoveHeader(nonce, hash) + // reset so the re-requested header gets a full window before it could be removed in turn + boot.resetSyncedWithErrorsForNonce(nonce) } func (boot *baseBootstrap) incrementSyncedWithErrorsForNonce(nonce uint64) uint32 { @@ -831,6 +864,12 @@ func (boot *baseBootstrap) incrementSyncedWithErrorsForNonce(nonce uint64) uint3 return numSyncedWithErrors } +func (boot *baseBootstrap) resetSyncedWithErrorsForNonce(nonce uint64) { + boot.mutNonceSyncedWithErrors.Lock() + delete(boot.mapNonceSyncedWithErrors, nonce) + boot.mutNonceSyncedWithErrors.Unlock() +} + func (boot *baseBootstrap) prepareForSyncAtBoostrapIfNeeded() error { // this will be triggered only once, after a full node restart. // it is needed for the case when the node will go through bootstrap process and start diff --git a/process/sync/metablock.go b/process/sync/metablock.go index 1b5a1936ba3..1623b5e36c3 100644 --- a/process/sync/metablock.go +++ b/process/sync/metablock.go @@ -226,7 +226,7 @@ func (boot *MetaBootstrap) requestEpochStartBlockIfStuck() { targetNonce := currentNonce + 1 - header, headerHash, err := process.GetMetaHeaderFromPoolWithNonce(targetNonce, boot.headers) + header, headerHash, err := boot.getMetaHeaderForNoncePreferProven(targetNonce) if err == nil && !check.IfNil(header) { if boot.proofs.HasProof(core.MetachainShardId, headerHash) { return @@ -251,6 +251,18 @@ func (boot *MetaBootstrap) requestEpochStartBlockIfStuck() { boot.requestHandler.RequestEquivalentProofByNonce(core.MetachainShardId, targetNonce) } +func (boot *MetaBootstrap) getMetaHeaderForNoncePreferProven(nonce uint64) (data.HeaderHandler, []byte, error) { + proof, err := boot.proofs.GetProofByNonce(nonce, core.MetachainShardId) + if err == nil { + provenHeader, errHdr := boot.headers.GetHeaderByHash(proof.GetHeaderHash()) + if errHdr == nil { + return provenHeader, proof.GetHeaderHash(), nil + } + } + + return process.GetMetaHeaderFromPoolWithNonce(nonce, boot.headers) +} + func (boot *MetaBootstrap) setLastEpochStartRound() { hdr := boot.chainHandler.GetCurrentBlockHeader() if check.IfNil(hdr) || hdr.GetEpoch() < 1 { diff --git a/process/sync/metablock_test.go b/process/sync/metablock_test.go index ae6e312e53a..9405d88f84a 100644 --- a/process/sync/metablock_test.go +++ b/process/sync/metablock_test.go @@ -16,10 +16,11 @@ import ( "github.com/multiversx/mx-chain-core-go/core/check" "github.com/multiversx/mx-chain-core-go/data" "github.com/multiversx/mx-chain-core-go/data/block" - "github.com/multiversx/mx-chain-go/testscommon/processMocks" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/multiversx/mx-chain-go/testscommon/processMocks" + "github.com/multiversx/mx-chain-go/common" "github.com/multiversx/mx-chain-go/consensus/round" "github.com/multiversx/mx-chain-go/dataRetriever" @@ -206,6 +207,58 @@ func TestMetaBootstrap_RequestEpochStartBlockIfStuck(t *testing.T) { require.Zero(t, recorder.total()) }) + t.Run("stuck, proven header present alongside a more recent fork, recognizes the proof and requests nothing", func(t *testing.T) { + t.Parallel() + + hashX := []byte("proven-hash") + hashY := []byte("fork-hash") + headerX := &block.MetaBlock{Nonce: currentNonce + 1, Epoch: currentEpoch + 1} + headerY := &block.MetaBlock{Nonce: currentNonce + 1, Epoch: currentEpoch + 1} + + args, recorder := newArgs() + pools := createMockPools() + pools.HeadersCalled = func() dataRetriever.HeadersPool { + return &mock.HeadersCacherStub{ + GetHeaderByNonceAndShardIdCalled: func(hdrNonce uint64, shardId uint32) ([]data.HeaderHandler, [][]byte, error) { + if hdrNonce != currentNonce+1 { + return nil, nil, errors.New("not found") + } + // Y is the last-seen (more recently received) header for the nonce + return []data.HeaderHandler{headerX, headerY}, [][]byte{hashX, hashY}, nil + }, + GetHeaderByHashCalled: func(h []byte) (data.HeaderHandler, error) { + if bytes.Equal(h, hashX) { + return headerX, nil + } + return nil, errors.New("not found") + }, + } + } + pools.ProofsCalled = func() dataRetriever.ProofsPool { + return &dataRetrieverMock.ProofsPoolMock{ + GetProofByNonceCalled: func(headerNonce uint64, shardID uint32) (data.HeaderProofHandler, error) { + if headerNonce == currentNonce+1 { + return &block.HeaderProof{HeaderHash: hashX, HeaderNonce: headerNonce}, nil + } + return nil, errors.New("missing proof") + }, + HasProofCalled: func(shardID uint32, headerHash []byte) bool { + return bytes.Equal(headerHash, hashX) + }, + } + } + args.PoolsHolder = pools + + boot, err := sync.NewMetaBootstrap(args) + require.Nil(t, err) + + boot.SetWatchdogLastNonce(currentNonce) + boot.RequestEpochStartBlockIfStuck() + + // without proof-preference the last-seen fork Y (no proof) would be picked and its proof requested + require.Zero(t, recorder.total()) + }) + t.Run("stuck but andromeda not enabled does not request anything", func(t *testing.T) { t.Parallel() @@ -239,6 +292,7 @@ func (r *headerRequestsRecorder) total() int { } func poolsWithMetaHeader(nonce uint64, epoch uint32, hash []byte, hasProof bool) *dataRetrieverMock.PoolsHolderStub { + metaHeader := &block.MetaBlock{Nonce: nonce, Epoch: epoch} pools := createMockPools() pools.HeadersCalled = func() dataRetriever.HeadersPool { return &mock.HeadersCacherStub{ @@ -246,7 +300,13 @@ func poolsWithMetaHeader(nonce uint64, epoch uint32, hash []byte, hasProof bool) if hdrNonce != nonce { return nil, nil, errors.New("not found") } - return []data.HeaderHandler{&block.MetaBlock{Nonce: nonce, Epoch: epoch}}, [][]byte{hash}, nil + return []data.HeaderHandler{metaHeader}, [][]byte{hash}, nil + }, + GetHeaderByHashCalled: func(h []byte) (data.HeaderHandler, error) { + if bytes.Equal(h, hash) { + return metaHeader, nil + } + return nil, errors.New("not found") }, } } @@ -255,6 +315,12 @@ func poolsWithMetaHeader(nonce uint64, epoch uint32, hash []byte, hasProof bool) HasProofCalled: func(shardID uint32, headerHash []byte) bool { return hasProof }, + GetProofByNonceCalled: func(headerNonce uint64, shardID uint32) (data.HeaderProofHandler, error) { + if hasProof && headerNonce == nonce { + return &block.HeaderProof{HeaderHash: hash, HeaderNonce: nonce}, nil + } + return nil, errors.New("missing proof") + }, } } return pools diff --git a/process/sync/shardblock_test.go b/process/sync/shardblock_test.go index 6447e654ee4..8c3e4828f45 100644 --- a/process/sync/shardblock_test.go +++ b/process/sync/shardblock_test.go @@ -245,8 +245,8 @@ func createStore() *storageStubs.ChainStorerStub { } func generateTestCache() storage.Cacher { - cache, _ := storageunit.NewCache(storageunit.CacheConfig{Type: storageunit.LRUCache, Capacity: 1000, Shards: 1, SizeInBytes: 0}) - return cache + c, _ := storageunit.NewCache(storageunit.CacheConfig{Type: storageunit.LRUCache, Capacity: 1000, Shards: 1, SizeInBytes: 0}) + return c } func generateTestUnit() storage.Storer { @@ -2238,6 +2238,112 @@ func TestShardBootstrap_DoJobOnSyncBlockFailShouldNotResetProbableHighestNonceWh assert.False(t, wasCalled) } +func TestShardBootstrap_DoJobOnSyncBlockFailRemovesBlockingUnprovenHeader(t *testing.T) { + t.Parallel() + + nextNonce := uint64(2) + hashY := []byte("hashY") + headerY := &block.Header{Nonce: nextNonce, Round: 5} + + type capture struct { + removedFromPool []byte + removedFromForkDetector bool + removedNonce uint64 + } + + buildArgs := func(headerHasProof bool) (sync.ArgShardBootstrapper, *capture) { + c := &capture{} + args := CreateShardBootstrapMockArguments() + + pools := createMockPools() + pools.HeadersCalled = func() dataRetriever.HeadersPool { + return &mock.HeadersCacherStub{ + GetHeaderByNonceAndShardIdCalled: func(hdrNonce uint64, shardId uint32) ([]data.HeaderHandler, [][]byte, error) { + if hdrNonce == nextNonce { + return []data.HeaderHandler{headerY}, [][]byte{hashY}, nil + } + return nil, nil, errors.New("missing header") + }, + RemoveHeaderByHashCalled: func(headerHash []byte) { + c.removedFromPool = headerHash + }, + } + } + pools.ProofsCalled = func() dataRetriever.ProofsPool { + return &dataRetrieverMock.ProofsPoolMock{ + HasProofCalled: func(shardID uint32, headerHash []byte) bool { + return headerHasProof + }, + } + } + args.PoolsHolder = pools + + args.ChainHandler = &testscommon.ChainHandlerStub{ + GetCurrentBlockHeaderCalled: func() data.HeaderHandler { return &block.Header{Nonce: 1} }, + GetGenesisHeaderCalled: func() data.HeaderHandler { return &block.Header{} }, + } + args.ForkDetector = &mock.ForkDetectorMock{ + RemoveHeaderCalled: func(nonce uint64, hash []byte) { + c.removedFromForkDetector = true + c.removedNonce = nonce + }, + } + // not a proper round -> isolate the targeted removal from the rollback path + roundHandlerMock := &mock.RoundHandlerMock{} + roundHandlerMock.RoundIndex = 1 + args.RoundHandler = roundHandlerMock + // proofs flag active -> the cached header genuinely needs a proof + args.EnableEpochsHandler = &enableEpochsHandlerMock.EnableEpochsHandlerStub{ + IsFlagEnabledInEpochCalled: func(flag core.EnableEpochFlag, epoch uint32) bool { + return true + }, + } + + return args, c + } + + t.Run("removes the unproven blocking header and resets the error counter when the limit is reached", func(t *testing.T) { + t.Parallel() + + args, c := buildArgs(false) + bs, _ := sync.NewShardBootstrap(args) + bs.SetNumSyncedWithErrorsForNonce(nextNonce, 100) + + bs.DoJobOnSyncBlockFail(nil, nil, process.ErrTimeIsOut) + + assert.Equal(t, hashY, c.removedFromPool) + assert.True(t, c.removedFromForkDetector) + assert.Equal(t, nextNonce, c.removedNonce) + assert.Equal(t, uint32(0), bs.GetNumSyncedWithErrorsForNonce(nextNonce)) + }) + + t.Run("does not remove a header that has a proof", func(t *testing.T) { + t.Parallel() + + args, c := buildArgs(true) + bs, _ := sync.NewShardBootstrap(args) + bs.SetNumSyncedWithErrorsForNonce(nextNonce, 100) + + bs.DoJobOnSyncBlockFail(nil, nil, process.ErrTimeIsOut) + + assert.Nil(t, c.removedFromPool) + assert.False(t, c.removedFromForkDetector) + }) + + t.Run("does not remove before the error limit is reached", func(t *testing.T) { + t.Parallel() + + args, c := buildArgs(false) + bs, _ := sync.NewShardBootstrap(args) + bs.SetNumSyncedWithErrorsForNonce(nextNonce, 0) + + bs.DoJobOnSyncBlockFail(nil, nil, process.ErrTimeIsOut) + + assert.Nil(t, c.removedFromPool) + assert.False(t, c.removedFromForkDetector) + }) +} + func TestShardBootstrap_DoJobOnSyncBlockFailShouldResetProbableHighestNonce(t *testing.T) { t.Parallel()