diff --git a/factory/mock/forkDetectorMock.go b/factory/mock/forkDetectorMock.go index 217ee15e14..0a07c8e7fe 100644 --- a/factory/mock/forkDetectorMock.go +++ b/factory/mock/forkDetectorMock.go @@ -10,6 +10,7 @@ import ( type ForkDetectorMock struct { AddHeaderCalled func(header data.HeaderHandler, hash []byte, state process.BlockHeaderState, selfNotarizedHeaders []data.HeaderHandler, selfNotarizedHeadersHashes [][]byte) error RemoveHeaderCalled func(nonce uint64, hash []byte) + RemoveCommittedHeaderCalled func(nonce uint64, hash []byte) CheckForkCalled func() *process.ForkInfo GetHighestFinalBlockNonceCalled func() uint64 GetHighestFinalBlockHashCalled func() []byte @@ -46,6 +47,13 @@ func (fdm *ForkDetectorMock) RemoveHeader(nonce uint64, hash []byte) { } } +// RemoveCommittedHeader - +func (fdm *ForkDetectorMock) RemoveCommittedHeader(nonce uint64, hash []byte) { + if fdm.RemoveCommittedHeaderCalled != nil { + fdm.RemoveCommittedHeaderCalled(nonce, hash) + } +} + // CheckFork is a mock implementation for CheckFork func (fdm *ForkDetectorMock) CheckFork() *process.ForkInfo { if fdm.CheckForkCalled != nil { diff --git a/integrationTests/mock/forkDetectorStub.go b/integrationTests/mock/forkDetectorStub.go index dba71b1dd3..25878d673a 100644 --- a/integrationTests/mock/forkDetectorStub.go +++ b/integrationTests/mock/forkDetectorStub.go @@ -10,6 +10,7 @@ import ( type ForkDetectorStub struct { AddHeaderCalled func(header data.HeaderHandler, hash []byte, state process.BlockHeaderState, selfNotarizedHeaders []data.HeaderHandler, selfNotarizedHeadersHashes [][]byte) error RemoveHeaderCalled func(nonce uint64, hash []byte) + RemoveCommittedHeaderCalled func(nonce uint64, hash []byte) CheckForkCalled func() *process.ForkInfo GetHighestFinalBlockNonceCalled func() uint64 GetHighestFinalBlockHashCalled func() []byte @@ -52,6 +53,13 @@ func (fdm *ForkDetectorStub) RemoveHeader(nonce uint64, hash []byte) { } } +// RemoveCommittedHeader - +func (fdm *ForkDetectorStub) RemoveCommittedHeader(nonce uint64, hash []byte) { + if fdm.RemoveCommittedHeaderCalled != nil { + fdm.RemoveCommittedHeaderCalled(nonce, hash) + } +} + // CheckFork is a mock implementation for CheckFork func (fdm *ForkDetectorStub) CheckFork() *process.ForkInfo { if fdm.CheckForkCalled != nil { diff --git a/node/mock/forkDetectorMock.go b/node/mock/forkDetectorMock.go index 7458887b48..992986e903 100644 --- a/node/mock/forkDetectorMock.go +++ b/node/mock/forkDetectorMock.go @@ -10,6 +10,7 @@ import ( type ForkDetectorMock struct { AddHeaderCalled func(header data.HeaderHandler, hash []byte, state process.BlockHeaderState, selfNotarizedHeaders []data.HeaderHandler, selfNotarizedHeadersHashes [][]byte) error RemoveHeaderCalled func(nonce uint64, hash []byte) + RemoveCommittedHeaderCalled func(nonce uint64, hash []byte) CheckForkCalled func() *process.ForkInfo GetHighestFinalBlockNonceCalled func() uint64 GetHighestFinalBlockHashCalled func() []byte @@ -39,6 +40,11 @@ func (fdm *ForkDetectorMock) RemoveHeader(nonce uint64, hash []byte) { fdm.RemoveHeaderCalled(nonce, hash) } +// RemoveCommittedHeader - +func (fdm *ForkDetectorMock) RemoveCommittedHeader(nonce uint64, hash []byte) { + fdm.RemoveCommittedHeaderCalled(nonce, hash) +} + // CheckFork is a mock implementation for CheckFork func (fdm *ForkDetectorMock) CheckFork() *process.ForkInfo { return fdm.CheckForkCalled() diff --git a/process/interface.go b/process/interface.go index 2e9aadf752..5c3149f844 100644 --- a/process/interface.go +++ b/process/interface.go @@ -468,6 +468,7 @@ type Bootstrapper interface { type ForkDetector interface { AddHeader(header data.HeaderHandler, headerHash []byte, state BlockHeaderState, selfNotarizedHeaders []data.HeaderHandler, selfNotarizedHeadersHashes [][]byte) error RemoveHeader(nonce uint64, hash []byte) + RemoveCommittedHeader(nonce uint64, hash []byte) CheckFork() *ForkInfo GetHighestFinalBlockNonce() uint64 GetHighestFinalBlockHash() []byte diff --git a/process/mock/forkDetectorMock.go b/process/mock/forkDetectorMock.go index 65f75c8376..07d2725e49 100644 --- a/process/mock/forkDetectorMock.go +++ b/process/mock/forkDetectorMock.go @@ -10,6 +10,7 @@ import ( type ForkDetectorMock struct { AddHeaderCalled func(header data.HeaderHandler, hash []byte, state process.BlockHeaderState, selfNotarizedHeaders []data.HeaderHandler, selfNotarizedHeadersHashes [][]byte) error RemoveHeaderCalled func(nonce uint64, hash []byte) + RemoveCommittedHeaderCalled func(nonce uint64, hash []byte) CheckForkCalled func() *process.ForkInfo GetHighestFinalBlockNonceCalled func() uint64 GetHighestFinalBlockHashCalled func() []byte @@ -45,6 +46,13 @@ func (fdm *ForkDetectorMock) RemoveHeader(nonce uint64, hash []byte) { } } +// RemoveCommittedHeader - +func (fdm *ForkDetectorMock) RemoveCommittedHeader(nonce uint64, hash []byte) { + if fdm.RemoveCommittedHeaderCalled != nil { + fdm.RemoveCommittedHeaderCalled(nonce, hash) + } +} + // CheckFork - func (fdm *ForkDetectorMock) CheckFork() *process.ForkInfo { if fdm.CheckForkCalled != nil { diff --git a/process/sync/baseForkDetector.go b/process/sync/baseForkDetector.go index 49f4207d87..a5dfdbdcc1 100644 --- a/process/sync/baseForkDetector.go +++ b/process/sync/baseForkDetector.go @@ -279,6 +279,53 @@ func (bfd *baseForkDetector) RemoveHeader(nonce uint64, hash []byte) { "final checkpoint nonce", bfd.finalCheckpoint().nonce) } +// RemoveCommittedHeader removes a reverted committed header together with its checkpoint, proof +// included, so a same-nonce sibling can be adopted; it never removes at or below the final checkpoint +func (bfd *baseForkDetector) RemoveCommittedHeader(nonce uint64, hash []byte) { + finalCheckpointNonce := bfd.finalCheckpoint().nonce + if nonce <= finalCheckpointNonce { + log.Warn("baseForkDetector.RemoveCommittedHeader: refusing removal at or below the final checkpoint", + "nonce", nonce, + "hash", hash, + "final checkpoint nonce", finalCheckpointNonce) + return + } + + bfd.removeCheckpointWithNonce(nonce) + + preservedHdrsInfo := make([]*headerInfo, 0) + + bfd.mutHeaders.Lock() + + hdrsInfo := bfd.headers[nonce] + for _, hdrInfo := range hdrsInfo { + if hdrInfo.state != process.BHNotarized && bytes.Equal(hash, hdrInfo.hash) { + continue + } + + preservedHdrsInfo = append(preservedHdrsInfo, hdrInfo) + } + + if len(preservedHdrsInfo) == 0 { + delete(bfd.headers, nonce) + } else { + bfd.headers[nonce] = preservedHdrsInfo + } + + bfd.mutHeaders.Unlock() + + bfd.forkDetector.computeFinalCheckpoint() + + probableHighestNonce := bfd.computeProbableHighestNonce() + bfd.setProbableHighestNonce(probableHighestNonce) + + log.Debug("forkDetector.RemoveCommittedHeader", + "nonce", nonce, + "hash", hash, + "probable highest nonce", probableHighestNonce, + "final checkpoint nonce", bfd.finalCheckpoint().nonce) +} + func (bfd *baseForkDetector) removeCheckpointWithNonce(nonce uint64) { bfd.mutFork.Lock() preservedCheckpoint := make([]*checkpointInfo, 0) diff --git a/process/sync/baseForkDetector_test.go b/process/sync/baseForkDetector_test.go index 67219db7de..158535b59f 100644 --- a/process/sync/baseForkDetector_test.go +++ b/process/sync/baseForkDetector_test.go @@ -1768,3 +1768,61 @@ func TestBaseForkDetector_ReceivedProofForBlockHeaderShouldSetProof(t *testing.T assert.Equal(t, []byte("hash0"), hdrInfos[1].Hash()) assert.Equal(t, true, hdrInfos[1].HasProof()) } + +func TestBaseForkDetector_RemoveCommittedHeader(t *testing.T) { + t.Parallel() + + sfd, _ := sync.NewShardForkDetector( + &mock.RoundHandlerMock{RoundIndex: 5}, + &testscommon.TimeCacheStub{}, + &mock.BlockTrackerMock{}, + 0, + 0, + &enableEpochsHandlerMock.EnableEpochsHandlerStub{ + IsFlagEnabledInEpochCalled: func(flag core.EnableEpochFlag, epoch uint32) bool { + return flag == common.AndromedaFlag || flag == common.SupernovaFlag + }, + }, + &testscommon.EnableRoundsHandlerStub{}, + &dataRetriever.ProofsPoolMock{ + HasProofCalled: func(shardID uint32, headerHash []byte) bool { + return true + }, + }, + &chainParameters.ChainParametersHandlerStub{}, + testscommon.GetDefaultProcessConfigsHandler(), + 0, + ) + + hash1, hash2, competitorHash := []byte("hash1"), []byte("hash2"), []byte("competitorHash") + hdr1 := &block.Header{Nonce: 1, Round: 1, PubKeysBitmap: []byte("X")} + contendedHdr2 := &block.Header{Nonce: 2, Round: 4, PrevHash: hash1, PubKeysBitmap: []byte("X")} + + _ = sfd.AddHeader(hdr1, hash1, process.BHProcessed, nil, nil) + _ = sfd.AddHeader(contendedHdr2, hash2, process.BHProcessed, nil, nil) + sfd.ReceivedProof(&block.HeaderProof{ + HeaderHash: competitorHash, + HeaderNonce: 2, + HeaderRound: 3, + HeaderShardId: 0, + }) + + // RemoveHeader refuses proofed headers, the committed one included + sfd.RemoveHeader(2, hash2) + assert.Len(t, sfd.GetHeaders(2), 2) + assert.Equal(t, uint64(2), sfd.LastCheckpointNonce()) + + // the deliberate switch removal drops the committed header and its checkpoint despite the proof + sfd.RemoveCommittedHeader(2, hash2) + hdrInfos := sfd.GetHeaders(2) + assert.Len(t, hdrInfos, 1) + assert.Equal(t, competitorHash, hdrInfos[0].Hash()) + assert.Equal(t, uint64(1), sfd.LastCheckpointNonce()) + assert.Equal(t, uint64(1), sfd.FinalCheckpointNonce()) + + // removal at or below the final checkpoint is refused + sfd.RemoveCommittedHeader(1, hash1) + assert.Len(t, sfd.GetHeaders(1), 1) + assert.Equal(t, uint64(1), sfd.LastCheckpointNonce()) + assert.Equal(t, uint64(1), sfd.FinalCheckpointNonce()) +} diff --git a/process/sync/baseSync.go b/process/sync/baseSync.go index 862a321021..1f5735c152 100644 --- a/process/sync/baseSync.go +++ b/process/sync/baseSync.go @@ -1685,7 +1685,8 @@ func (boot *baseBootstrap) rollBack(revertUsingForkNonce bool) error { var currBody data.BodyHandler defer func() { - if !roleBackOneBlockExecuted { + isHeaderV3 := !check.IfNil(currHeader) && currHeader.IsHeaderV3() + if !roleBackOneBlockExecuted && !isHeaderV3 { err = boot.scheduledTxsExecutionHandler.RollBackToBlock(currHeaderHash) if err != nil { rootHash := boot.chainHandler.GetGenesisHeader().GetRootHash() @@ -1712,7 +1713,9 @@ func (boot *baseBootstrap) rollBack(revertUsingForkNonce bool) error { } allowRollBack := boot.shouldAllowRollback(currHeader, currHeaderHash) - if !revertUsingForkNonce && !allowRollBack { + // a header v3 switch must never cross the final checkpoint, not even fork-driven + isRollBackDenied := !allowRollBack && (!revertUsingForkNonce || currHeader.IsHeaderV3()) + if isRollBackDenied { return ErrRollBackBehindFinalHeader } @@ -1735,13 +1738,22 @@ func (boot *baseBootstrap) rollBack(revertUsingForkNonce bool) error { "nonce", boot.forkDetector.GetHighestFinalBlockNonce(), ) - currBody, err = boot.rollBackOneBlock( - currHeaderHash, - currHeader, - prevHeaderHash, - prevHeader, - ) - roleBackOneBlockExecuted = true + if currHeader.IsHeaderV3() { + currBody, err = boot.rollBackOneBlockV3( + currHeaderHash, + currHeader, + prevHeaderHash, + prevHeader, + ) + } else { + currBody, err = boot.rollBackOneBlock( + currHeaderHash, + currHeader, + prevHeaderHash, + prevHeader, + ) + roleBackOneBlockExecuted = true + } if err != nil { return err } @@ -1765,15 +1777,17 @@ func (boot *baseBootstrap) rollBack(revertUsingForkNonce bool) error { return err } - err = boot.scheduledTxsExecutionHandler.RollBackToBlock(prevHeaderHash) - if err != nil { - scheduledInfo := &process.ScheduledInfo{ - RootHash: prevHeader.GetRootHash(), - IntermediateTxs: make(map[block.Type][]data.TransactionHandler), - GasAndFees: process.GetZeroGasAndFees(), - MiniBlocks: make(block.MiniBlockSlice, 0), + if !currHeader.IsHeaderV3() { + err = boot.scheduledTxsExecutionHandler.RollBackToBlock(prevHeaderHash) + if err != nil { + scheduledInfo := &process.ScheduledInfo{ + RootHash: prevHeader.GetRootHash(), + IntermediateTxs: make(map[block.Type][]data.TransactionHandler), + GasAndFees: process.GetZeroGasAndFees(), + MiniBlocks: make(block.MiniBlockSlice, 0), + } + boot.scheduledTxsExecutionHandler.SetScheduledInfo(scheduledInfo) } - boot.scheduledTxsExecutionHandler.SetScheduledInfo(scheduledInfo) } err = boot.outportHandler.RevertIndexedBlock(&outportcore.HeaderDataWithBody{ @@ -1803,9 +1817,12 @@ func (boot *baseBootstrap) rollBack(revertUsingForkNonce bool) error { } func (boot *baseBootstrap) shouldAllowRollback(currHeader data.HeaderHandler, currHeaderHash []byte) bool { - if check.IfNil(currHeader) || currHeader.IsHeaderV3() { + if check.IfNil(currHeader) { return false } + if currHeader.IsHeaderV3() { + return boot.shouldAllowRollbackV3(currHeader) + } finalBlockNonce := boot.forkDetector.GetHighestFinalBlockNonce() finalBlockHash := boot.forkDetector.GetHighestFinalBlockHash() @@ -1831,6 +1848,21 @@ func (boot *baseBootstrap) shouldAllowRollback(currHeader data.HeaderHandler, cu return allowRollBack } +// shouldAllowRollbackV3 allows replacing a committed block only while it is not final (R-SWITCH); +// the state is never reverted through tries, the adopted sibling re-executes asynchronously +func (boot *baseBootstrap) shouldAllowRollbackV3(currHeader data.HeaderHandler) bool { + finalBlockNonce := boot.forkDetector.GetHighestFinalBlockNonce() + allowRollBack := currHeader.GetNonce() > finalBlockNonce + + log.Debug("baseBootstrap.shouldAllowRollbackV3", + "nonce", currHeader.GetNonce(), + "final block nonce", finalBlockNonce, + "allowRollBack", allowRollBack, + ) + + return allowRollBack +} + func (boot *baseBootstrap) canRollbackBlock(currHeader data.HeaderHandler) bool { firstCommittedNonce := boot.blockProcessor.NonceOfFirstCommittedBlock() @@ -1889,6 +1921,51 @@ func (boot *baseBootstrap) rollBackOneBlock( return currBlockBody, nil } +// rollBackOneBlockV3 reverts a committed, not yet final header so a same-nonce sibling can be +// adopted; the trie state is not reverted, the sibling's execution results are produced async +func (boot *baseBootstrap) rollBackOneBlockV3( + currHeaderHash []byte, + currHeader data.HeaderHandler, + prevHeaderHash []byte, + prevHeader data.HeaderHandler, +) (data.BodyHandler, error) { + err := boot.chainHandler.SetCurrentBlockHeaderAndHash(prevHeaderHash, prevHeader) + if err != nil { + return nil, err + } + + defer func() { + if err != nil { + errNotCritical := boot.chainHandler.SetCurrentBlockHeaderAndHash(currHeaderHash, currHeader) + if errNotCritical != nil { + log.Warn("rollBackOneBlockV3: cannot restore current block info", "error", errNotCritical) + } + } + }() + + err = boot.executionManager.RemoveAtNonceAndHigher(currHeader.GetNonce()) + if err != nil { + return nil, err + } + + currBlockBody, errNotCritical := boot.blockBootstrapper.getBlockBody(currHeader) + if errNotCritical != nil { + log.Debug("rollBackOneBlockV3 getBlockBody error", "error", errNotCritical) + } + + err = boot.blockProcessor.RestoreBlockIntoPools(currHeader, currBlockBody) + if err != nil { + return nil, err + } + + hash := boot.removeHeaderFromPools(currHeader) + boot.forkDetector.RemoveCommittedHeader(currHeader.GetNonce(), hash) + nonceToByteSlice := boot.uint64Converter.ToByteSlice(currHeader.GetNonce()) + _ = boot.headerNonceHashStore.Remove(nonceToByteSlice) + + return currBlockBody, nil +} + func (boot *baseBootstrap) getRootHashFromBlock(hdr data.HeaderHandler, hdrHash []byte) []byte { hdrRootHash := hdr.GetRootHash() scheduledHdrRootHash, err := boot.scheduledTxsExecutionHandler.GetScheduledRootHashForHeader(hdrHash) diff --git a/process/sync/baseSync_test.go b/process/sync/baseSync_test.go index 5d56d5aeda..605f7523ce 100644 --- a/process/sync/baseSync_test.go +++ b/process/sync/baseSync_test.go @@ -15,6 +15,9 @@ import ( "github.com/multiversx/mx-chain-core-go/data/smartContractResult" "github.com/multiversx/mx-chain-core-go/data/transaction" "github.com/multiversx/mx-chain-core-go/marshal" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/multiversx/mx-chain-go/common" "github.com/multiversx/mx-chain-go/dataRetriever" "github.com/multiversx/mx-chain-go/process" @@ -30,8 +33,6 @@ import ( "github.com/multiversx/mx-chain-go/testscommon/processMocks" statusHandlerMock "github.com/multiversx/mx-chain-go/testscommon/statusHandler" storageStubs "github.com/multiversx/mx-chain-go/testscommon/storage" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestBaseBootstrap_SyncBlocksShouldNotCallSyncIfNotConnectedToTheNetwork(t *testing.T) { @@ -678,7 +679,7 @@ func TestBaseSync_shouldAllowRollback(t *testing.T) { require.False(t, boot.shouldAllowRollback(header, notFinalBlockHash)) }) - t.Run("should not allow rollback of a header v3", func(t *testing.T) { + t.Run("should allow rollback of a header v3 only above the final nonce", func(t *testing.T) { header := &testscommon.HeaderHandlerStub{ GetNonceCalled: func() uint64 { return 11 @@ -687,6 +688,17 @@ func TestBaseSync_shouldAllowRollback(t *testing.T) { return true }, } + require.True(t, boot.shouldAllowRollback(header, finalBlockHash)) + + header.GetNonceCalled = func() uint64 { + return 10 + } + require.False(t, boot.shouldAllowRollback(header, finalBlockHash)) + require.False(t, boot.shouldAllowRollback(header, notFinalBlockHash)) + + header.GetNonceCalled = func() uint64 { + return 9 + } require.False(t, boot.shouldAllowRollback(header, finalBlockHash)) }) } diff --git a/process/sync/shardblock_test.go b/process/sync/shardblock_test.go index f91884a271..ef7a7d8eff 100644 --- a/process/sync/shardblock_test.go +++ b/process/sync/shardblock_test.go @@ -17,6 +17,7 @@ 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" + outportcore "github.com/multiversx/mx-chain-core-go/data/outport" "github.com/multiversx/mx-chain-core-go/marshal" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -3968,3 +3969,153 @@ func TestShardBootstrap_GetNextHeaderWithCompetingProofsUsesLowestRound(t *testi require.Equal(t, hashLowRound, hash) require.Equal(t, headerLowRound, header) } + +type revertedBlocksCapture struct { + *outport.OutportStub + revertedHashes [][]byte +} + +func (capture *revertedBlocksCapture) RevertIndexedBlock(headerData *outportcore.HeaderDataWithBody) error { + capture.revertedHashes = append(capture.revertedHashes, headerData.HeaderHash) + return nil +} + +func TestBootstrap_RollBackV3(t *testing.T) { + t.Parallel() + + marshaller := &mock.MarshalizerMock{} + prevHdrHash := []byte("prev header hash") + currHdrHash := []byte("curr header hash") + + newV3Header := func(nonce uint64, round uint64, prevHash []byte) *block.HeaderV3 { + return &block.HeaderV3{ + Nonce: nonce, + Round: round, + PrevHash: prevHash, + LastExecutionResult: &block.ExecutionResultInfo{ + ExecutionResult: &block.BaseExecutionResult{HeaderNonce: nonce - 2, HeaderHash: []byte("execHash")}, + }, + } + } + prevHdr := newV3Header(7, 9, []byte("older hash")) + // committed contended head at nonce 8, not final: the switch candidate + currHdr := newV3Header(8, 12, prevHdrHash) + prevHdrBytes, _ := marshaller.Marshal(prevHdr) + + buildBootstrapper := func(finalNonce uint64) ( + *sync.ShardBootstrap, + *testscommon.ChainHandlerStub, + *revertedBlocksCapture, + map[string]uint64, + map[string]bool, + ) { + removedAtNonce := make(map[string]uint64) + calledFlags := make(map[string]bool) + + args := CreateShardBootstrapMockArguments() + args.Marshalizer = marshaller + args.Hasher = &mock.HasherStub{ + ComputeCalled: func(s string) []byte { + return currHdrHash + }, + } + + blkc := &testscommon.ChainHandlerStub{} + var currentHeader data.HeaderHandler = currHdr + currentHeaderHash := currHdrHash + blkc.GetCurrentBlockHeaderCalled = func() data.HeaderHandler { + return currentHeader + } + blkc.GetCurrentBlockHeaderHashCalled = func() []byte { + return currentHeaderHash + } + blkc.SetCurrentBlockHeaderAndHashCalled = func(headerHash []byte, header data.HeaderHandler) error { + currentHeader = header + currentHeaderHash = headerHash + return nil + } + args.ChainHandler = blkc + + args.Store = &storageStubs.ChainStorerStub{ + GetStorerCalled: func(unitType dataRetriever.UnitType) (storage.Storer, error) { + return &storageStubs.StorerStub{ + GetCalled: func(key []byte) ([]byte, error) { + return prevHdrBytes, nil + }, + RemoveCalled: func(key []byte) error { + calledFlags["nonceHashStoreRemove"] = true + return nil + }, + }, nil + }, + } + args.ForkDetector = &mock.ForkDetectorMock{ + GetHighestFinalBlockNonceCalled: func() uint64 { + return finalNonce + }, + RemoveCommittedHeaderCalled: func(nonce uint64, hash []byte) { + if nonce == currHdr.GetNonce() && bytes.Equal(hash, currHdrHash) { + calledFlags["removeCommittedHeader"] = true + } + }, + } + args.ExecutionManager = &processMocks.ExecutionManagerMock{ + RemoveAtNonceAndHigherCalled: func(nonce uint64) error { + removedAtNonce["executionManager"] = nonce + return nil + }, + } + args.BlockProcessor = &testscommon.BlockProcessorStub{ + RestoreBlockIntoPoolsCalled: func(header data.HeaderHandler, body data.BodyHandler) error { + removedAtNonce["restoredIntoPools"] = header.GetNonce() + return nil + }, + } + args.ScheduledTxsExecutionHandler = &testscommon.ScheduledTxsExecutionStub{ + RollBackToBlockCalled: func(headerHash []byte) error { + calledFlags["scheduledRollBack"] = true + return nil + }, + } + outportCapture := &revertedBlocksCapture{OutportStub: &outport.OutportStub{}} + args.OutportHandler = outportCapture + + bs, err := sync.NewShardBootstrap(args) + require.Nil(t, err) + bs.SetForkNonce(currHdr.GetNonce()) + + return bs, blkc, outportCapture, removedAtNonce, calledFlags + } + + t.Run("reverts the committed non-final head without touching tries or scheduled state", func(t *testing.T) { + t.Parallel() + + bs, blkc, outportCapture, removedAtNonce, calledFlags := buildBootstrapper(5) + + err := bs.RollBack(true) + require.Nil(t, err) + + require.Equal(t, prevHdr.GetNonce(), blkc.GetCurrentBlockHeader().GetNonce()) + require.Equal(t, prevHdrHash, blkc.GetCurrentBlockHeaderHash()) + require.Equal(t, currHdr.GetNonce(), removedAtNonce["executionManager"]) + require.Equal(t, currHdr.GetNonce(), removedAtNonce["restoredIntoPools"]) + require.True(t, calledFlags["removeCommittedHeader"]) + require.True(t, calledFlags["nonceHashStoreRemove"]) + require.False(t, calledFlags["scheduledRollBack"]) + require.Equal(t, [][]byte{currHdrHash}, outportCapture.revertedHashes) + }) + + t.Run("never crosses the final checkpoint, fork-driven included", func(t *testing.T) { + t.Parallel() + + bs, blkc, outportCapture, removedAtNonce, calledFlags := buildBootstrapper(currHdr.GetNonce()) + + err := bs.RollBack(true) + require.Equal(t, sync.ErrRollBackBehindFinalHeader, err) + + require.Equal(t, currHdr.GetNonce(), blkc.GetCurrentBlockHeader().GetNonce()) + require.Empty(t, removedAtNonce) + require.Empty(t, calledFlags) + require.Empty(t, outportCapture.revertedHashes) + }) +} diff --git a/testscommon/processMocks/forkDetectorStub.go b/testscommon/processMocks/forkDetectorStub.go index a6e4f2e262..0c6aa9ff55 100644 --- a/testscommon/processMocks/forkDetectorStub.go +++ b/testscommon/processMocks/forkDetectorStub.go @@ -10,6 +10,7 @@ import ( type ForkDetectorStub struct { AddHeaderCalled func(header data.HeaderHandler, hash []byte, state process.BlockHeaderState, selfNotarizedHeaders []data.HeaderHandler, selfNotarizedHeadersHashes [][]byte) error RemoveHeaderCalled func(nonce uint64, hash []byte) + RemoveCommittedHeaderCalled func(nonce uint64, hash []byte) CheckForkCalled func() *process.ForkInfo GetHighestFinalBlockNonceCalled func() uint64 GetHighestFinalBlockHashCalled func() []byte @@ -42,6 +43,11 @@ func (fdm *ForkDetectorStub) RemoveHeader(nonce uint64, hash []byte) { fdm.RemoveHeaderCalled(nonce, hash) } +// RemoveCommittedHeader - +func (fdm *ForkDetectorStub) RemoveCommittedHeader(nonce uint64, hash []byte) { + fdm.RemoveCommittedHeaderCalled(nonce, hash) +} + // CheckFork - func (fdm *ForkDetectorStub) CheckFork() *process.ForkInfo { return fdm.CheckForkCalled()