Repository: kaspanet/kaspad
Pull request #2114 (2022-07-08 14:25:06)

Crucial fix for the UTXO difference mechanism 

codecov[bot]: # [Codecov](https://codecov.io/gh/kaspanet/kaspad/pull/2114?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kaspanet) Report
> Merging [#2114](https://codecov.io/gh/kaspanet/kaspad/pull/2114?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kaspanet) (a620442) into [dev](https://codecov.io/gh/kaspanet/kaspad/commit/b9093d59eb83d8f97c34962d6ab9f0720f5364db?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kaspanet) (b9093d5) will **decrease** coverage by `0.01%`.
> The diff coverage is `61.48%`.

> :exclamation: Current head a620442 differs from pull request most recent head 09186a4. Consider uploading reports for the commit 09186a4 to get more accurate results

```diff
@@            Coverage Diff             @@
##              dev    #2114      +/-   ##
==========================================
- Coverage   59.19%   59.17%   -0.02%     
==========================================
  Files         678      678              
  Lines       32831    32871      +40     
==========================================
+ Hits        19434    19452      +18     
- Misses      10596    10604       +8     
- Partials     2801     2815      +14     
```


| [Impacted Files](https://codecov.io/gh/kaspanet/kaspad/pull/2114?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kaspanet) | Coverage Δ | |
|---|---|---|
| [domain/migrate.go](https://codecov.io/gh/kaspanet/kaspad/pull/2114/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kaspanet#diff-ZG9tYWluL21pZ3JhdGUuZ28=) | `0.00% <0.00%> (ø)` | |
| [app/protocol/flows/v5/blockrelay/ibd.go](https://codecov.io/gh/kaspanet/kaspad/pull/2114/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kaspanet#diff-YXBwL3Byb3RvY29sL2Zsb3dzL3Y1L2Jsb2NrcmVsYXkvaWJkLmdv) | `45.43% <43.75%> (-0.64%)` | :arrow_down: |
| [...nsensus/processes/consensusstatemanager/resolve.go](https://codecov.io/gh/kaspanet/kaspad/pull/2114/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kaspanet#diff-ZG9tYWluL2NvbnNlbnN1cy9wcm9jZXNzZXMvY29uc2Vuc3Vzc3RhdGVtYW5hZ2VyL3Jlc29sdmUuZ28=) | `58.62% <64.55%> (-1.38%)` | :arrow_down: |
| [domain/consensus/consensus.go](https://codecov.io/gh/kaspanet/kaspad/pull/2114/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kaspanet#diff-ZG9tYWluL2NvbnNlbnN1cy9jb25zZW5zdXMuZ28=) | `67.53% <70.37%> (-0.25%)` | :arrow_down: |
| [...sses/consensusstatemanager/resolve\_block\_status.go](https://codecov.io/gh/kaspanet/kaspad/pull/2114/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kaspanet#diff-ZG9tYWluL2NvbnNlbnN1cy9wcm9jZXNzZXMvY29uc2Vuc3Vzc3RhdGVtYW5hZ2VyL3Jlc29sdmVfYmxvY2tfc3RhdHVzLmdv) | `70.65% <100.00%> (ø)` | |
| [...cesses/consensusstatemanager/reverse\_utxo\_diffs.go](https://codecov.io/gh/kaspanet/kaspad/pull/2114/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kaspanet#diff-ZG9tYWluL2NvbnNlbnN1cy9wcm9jZXNzZXMvY29uc2Vuc3Vzc3RhdGVtYW5hZ2VyL3JldmVyc2VfdXR4b19kaWZmcy5nbw==) | `67.44% <100.00%> (ø)` | |
| [.../processes/consensusstatemanager/update\_virtual.go](https://codecov.io/gh/kaspanet/kaspad/pull/2114/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kaspanet#diff-ZG9tYWluL2NvbnNlbnN1cy9wcm9jZXNzZXMvY29uc2Vuc3Vzc3RhdGVtYW5hZ2VyL3VwZGF0ZV92aXJ0dWFsLmdv) | `67.08% <100.00%> (ø)` | |
| [...tructure/network/rpcclient/rpc\_on\_chain\_changed.go](https://codecov.io/gh/kaspanet/kaspad/pull/2114/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kaspanet#diff-aW5mcmFzdHJ1Y3R1cmUvbmV0d29yay9ycGNjbGllbnQvcnBjX29uX2NoYWluX2NoYW5nZWQuZ28=) | `52.38% <0.00%> (-14.29%)` | :arrow_down: |
| [app/protocol/flows/v5/blockrelay/block\_locator.go](https://codecov.io/gh/kaspanet/kaspad/pull/2114/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kaspanet#diff-YXBwL3Byb3RvY29sL2Zsb3dzL3Y1L2Jsb2NrcmVsYXkvYmxvY2tfbG9jYXRvci5nbw==) | `52.94% <0.00%> (-11.77%)` | :arrow_down: |
| [...k/netadapter/server/grpcserver/connection\_loops.go](https://codecov.io/gh/kaspanet/kaspad/pull/2114/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kaspanet#diff-aW5mcmFzdHJ1Y3R1cmUvbmV0d29yay9uZXRhZGFwdGVyL3NlcnZlci9ncnBjc2VydmVyL2Nvbm5lY3Rpb25fbG9vcHMuZ28=) | `56.89% <0.00%> (-5.18%)` | :arrow_down: |
| ... and [3 more](https://codecov.io/gh/kaspanet/kaspad/pull/2114/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kaspanet) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/kaspanet/kaspad/pull/2114?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kaspanet).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kaspanet)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/kaspanet/kaspad/pull/2114?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kaspanet). Last update [b9093d5...09186a4](https://codecov.io/gh/kaspanet/kaspad/pull/2114?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kaspanet). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kaspanet).


Patch:
diff --git a/app/protocol/flows/v5/blockrelay/ibd.go b/app/protocol/flows/v5/blockrelay/ibd.go
index e1e68e4d..fd5fcdcc 100644
--- a/app/protocol/flows/v5/blockrelay/ibd.go
+++ b/app/protocol/flows/v5/blockrelay/ibd.go
@@ -686,37 +686,28 @@ func (flow *handleIBDFlow) banIfBlockIsHeaderOnly(block *externalapi.DomainBlock
 }
 
 func (flow *handleIBDFlow) resolveVirtual(estimatedVirtualDAAScoreTarget uint64) error {
-	virtualDAAScoreStart, err := flow.Domain().Consensus().GetVirtualDAAScore()
+	err := flow.Domain().Consensus().ResolveVirtual(func(virtualDAAScoreStart uint64, virtualDAAScore uint64) {
+		var percents int
+		if estimatedVirtualDAAScoreTarget-virtualDAAScoreStart <= 0 {
+			percents = 100
+		} else {
+			percents = int(float64(virtualDAAScore-virtualDAAScoreStart) / float64(estimatedVirtualDAAScoreTarget-virtualDAAScoreStart) * 100)
+		}
+		if percents < 0 {
+			percents = 0
+		} else if percents > 100 {
+			percents = 100
+		}
+		log.Infof("Resolving virtual. Estimated progress: %d%%", percents)
+	})
 	if err != nil {
 		return err
 	}
 
-	for i := 0; ; i++ {
-		if i%10 == 0 {
-			virtualDAAScore, err := flow.Domain().Consensus().GetVirtualDAAScore()
-			if err != nil {
-				return err
-			}
-			var percents int
-			if estimatedVirtualDAAScoreTarget-virtualDAAScoreStart <= 0 {
-				percents = 100
-			} else {
-				percents = int(float64(virtualDAAScore-virtualDAAScoreStart) / float64(estimatedVirtualDAAScoreTarget-virtualDAAScoreStart) * 100)
-			}
-			log.Infof("Resolving virtual. Estimated progress: %d%%", percents)
-		}
-		isCompletelyResolved, err := flow.Domain().Consensus().ResolveVirtual()
-		if err != nil {
-			return err
-		}
-
-		if isCompletelyResolved {
-			log.Infof("Resolved virtual")
-			err = flow.OnNewBlockTemplate()
-			if err != nil {
-				return err
-			}
-			return nil
-		}
+	log.Infof("Resolved virtual")
+	err = flow.OnNewBlockTemplate()
+	if err != nil {
+		return err
 	}
+	return nil
 }
diff --git a/domain/consensus/consensus.go b/domain/consensus/consensus.go
index aaddf88e..8177389b 100644
--- a/domain/consensus/consensus.go
+++ b/domain/consensus/consensus.go
@@ -194,11 +194,33 @@ func (s *consensus) BuildBlockTemplate(coinbaseData *externalapi.DomainCoinbaseD
 
 // ValidateAndInsertBlock validates the given block and, if valid, applies it
 // to the current state
-func (s *consensus) ValidateAndInsertBlock(block *externalapi.DomainBlock, shouldValidateAgainstUTXO bool) error {
+func (s *consensus) ValidateAndInsertBlock(block *externalapi.DomainBlock, updateVirtual bool) error {
+	if updateVirtual {
+		s.lock.Lock()
+		if s.virtualNotUpdated {
+			s.lock.Unlock()
+			err := s.ResolveVirtual(nil)
+			if err != nil {
+				return err
+			}
+			return s.validateAndInsertBlockWithLock(block, updateVirtual)
+		}
+		defer s.lock.Unlock()
+		_, err := s.validateAndInsertBlockNoLock(block, updateVirtual)
+		if err != nil {
+			return err
+		}
+		return nil
+	}
+
+	return s.validateAndInsertBlockWithLock(block, updateVirtual)
+}
+
+func (s *consensus) validateAndInsertBlockWithLock(block *externalapi.DomainBlock, updateVirtual bool) error {
 	s.lock.Lock()
 	defer s.lock.Unlock()
 
-	_, err := s.validateAndInsertBlockNoLock(block, shouldValidateAgainstUTXO)
+	_, err := s.validateAndInsertBlockNoLock(block, updateVirtual)
 	if err != nil {
 		return err
 	}
@@ -206,19 +228,6 @@ func (s *consensus) ValidateAndInsertBlock(block *externalapi.DomainBlock, shoul
 }
 
 func (s *consensus) validateAndInsertBlockNoLock(block *externalapi.DomainBlock, updateVirtual bool) (*externalapi.VirtualChangeSet, error) {
-	// If virtual is in non-updated state, and the caller requests updating virtual -- then we must first
-	// resolve virtual so that the new block can be fully processed properly
-	if updateVirtual && s.virtualNotUpdated {
-		for s.virtualNotUpdated {
-			// We use 10000 << finality interval. See comment in `ResolveVirtual`.
-			// We give up responsiveness of consensus in this rare case.
-			_, err := s.resolveVirtualNoLock(10000) // Note `s.virtualNotUpdated` is updated within the call
-			if err != nil {
-				return nil, err
-			}
-		}
-	}
-
 	virtualChangeSet, blockStatus, err := s.blockProcessor.ValidateAndInsertBlock(block, updateVirtual)
 	if err != nil {
 		return nil, err
@@ -257,7 +266,7 @@ func (s *consensus) sendBlockAddedEvent(block *externalapi.DomainBlock, blockSta
 }
 
 func (s *consensus) sendVirtualChangedEvent(virtualChangeSet *externalapi.VirtualChangeSet, wasVirtualUpdated bool) error {
-	if !wasVirtualUpdated || s.consensusEventsChan == nil {
+	if !wasVirtualUpdated || s.consensusEventsChan == nil || virtualChangeSet == nil {
 		return nil
 	}
 
@@ -888,41 +897,68 @@ func (s *consensus) PopulateMass(transaction *externalapi.DomainTransaction) {
 	s.transactionValidator.PopulateMass(transaction)
 }
 
-func (s *consensus) ResolveVirtual() (bool, error) {
+func (s *consensus) ResolveVirtual(progressReportCallback func(uint64, uint64)) error {
+	virtualDAAScoreStart, err := s.GetVirtualDAAScore()
+	if err != nil {
+		return err
+	}
+
+	for i := 0; ; i++ {
+		if i%10 == 0 && progressReportCallback != nil {
+			virtualDAAScore, err := s.GetVirtualDAAScore()
+			if err != nil {
+				return err
+			}
+			progressReportCallback(virtualDAAScoreStart, virtualDAAScore)
+		}
+
+		// In order to prevent a situation that the consensus lock is held for too much time, we
+		// release the lock each time we resolve 100 blocks.
+		// Note: maxBlocksToResolve should be smaller than `params.FinalityDuration` in order to avoid a situation
+		// where UpdatePruningPointByVirtual skips a pruning point.
+		_, isCompletelyResolved, err := s.resolveVirtualChunkWithLock(100)
+		if err != nil {
+			return err
+		}
+		if isCompletelyResolved {
+			break
+		}
+	}
+
+	return nil
+}
+
+func (s *consensus) resolveVirtualChunkWithLock(maxBlocksToResolve uint64) (*externalapi.VirtualChangeSet, bool, error) {
 	s.lock.Lock()
 	defer s.lock.Unlock()
 
-	// In order to prevent a situation that the consensus lock is held for too much time, we
-	// release the lock each time resolve 100 blocks.
-	// Note: maxBlocksToResolve should be smaller than finality interval in order to avoid a situation
-	// where UpdatePruningPointByVirtual skips a pruning point.
-	return s.resolveVirtualNoLock(100)
+	return s.resolveVirtualChunkNoLock(maxBlocksToResolve)
 }
 
-func (s *consensus) resolveVirtualNoLock(maxBlocksToResolve uint64) (bool, error) {
+func (s *consensus) resolveVirtualChunkNoLock(maxBlocksToResolve uint64) (*externalapi.VirtualChangeSet, bool, error) {
 	virtualChangeSet, isCompletelyResolved, err := s.consensusStateManager.ResolveVirtual(maxBlocksToResolve)
 	if err != nil {
-		return false, err
+		return nil, false, err
 	}
 	s.virtualNotUpdated = !isCompletelyResolved
 
 	stagingArea := model.NewStagingArea()
 	err = s.pruningManager.UpdatePruningPointByVirtual(stagingArea)
 	if err != nil {
-		return false, err
+		return nil, false, err
 	}
 
 	err = staging.CommitAllChanges(s.databaseContext, stagingArea)
 	if err != nil {
-		return false, err
+		return nil, false, err
 	}
 
 	err = s.sendVirtualChangedEvent(virtualChangeSet, true)
 	if err != nil {
-		return false, err
+		return nil, false, err
 	}
 
-	return isCompletelyResolved, nil
+	return virtualChangeSet, isCompletelyResolved, nil
 }
 
 func (s *consensus) BuildPruningPointProof() (*externalapi.PruningPointProof, error) {
diff --git a/domain/consensus/factory.go b/domain/consensus/factory.go
index f5167fc7..3b92ecb9 100644
--- a/domain/consensus/factory.go
+++ b/domain/consensus/factory.go
@@ -515,6 +515,7 @@ func (f *factory) NewConsensus(config *Config, db infrastructuredatabase.Databas
 		blocksWithTrustedDataDAAWindowStore: daaWindowStore,
 
 		consensusEventsChan: consensusEventsChan,
+		virtualNotUpdated:   true,
 	}
 
 	if isOldReachabilityInitialized {
diff --git a/domain/consensus/model/externalapi/consensus.go b/domain/consensus/model/externalapi/consensus.go
index adaa81bc..d7aadc20 100644
--- a/domain/consensus/model/externalapi/consensus.go
+++ b/domain/consensus/model/externalapi/consensus.go
@@ -5,7 +5,7 @@ type Consensus interface {
 	Init(skipAddingGenesis bool) error
 	BuildBlock(coinbaseData *DomainCoinbaseData, transactions []*DomainTransaction) (*DomainBlock, error)
 	BuildBlockTemplate(coinbaseData *DomainCoinbaseData, transactions []*DomainTransaction) (*DomainBlockTemplate, error)
-	ValidateAndInsertBlock(block *DomainBlock, shouldValidateAgainstUTXO bool) error
+	ValidateAndInsertBlock(block *DomainBlock, updateVirtual bool) error
 	ValidateAndInsertBlockWithTrustedData(block *BlockWithTrustedData, validateUTXO bool) error
 	ValidateTransactionAndPopulateWithConsensusData(transaction *DomainTransaction) error
 	ImportPruningPoints(pruningPoints []BlockHeader) error
@@ -48,7 +48,7 @@ type Consensus interface {
 	Anticone(blockHash *DomainHash) ([]*DomainHash, error)
 	EstimateNetworkHashesPerSecond(startHash *DomainHash, windowSize int) (uint64, error)
 	PopulateMass(transaction *DomainTransaction)
-	ResolveVirtual() (bool, error)
+	ResolveVirtual(progressReportCallback func(uint64, uint64)) error
 	BlockDAAWindowHashes(blockHash *DomainHash) ([]*DomainHash, error)
 	TrustedDataDataDAAHeader(trustedBlockHash, daaBlockHash *DomainHash, daaBlockWindowIndex uint64) (*TrustedDataDataDAAHeader, error)
 	TrustedBlockAssociatedGHOSTDAGDataBlockHashes(blockHash *DomainHash) ([]*DomainHash, error)
diff --git a/domain/consensus/processes/consensusstatemanager/resolve.go b/domain/consensus/processes/consensusstatemanager/resolve.go
index 3a362488..85528c9b 100644
--- a/domain/consensus/processes/consensusstatemanager/resolve.go
+++ b/domain/consensus/processes/consensusstatemanager/resolve.go
@@ -5,22 +5,22 @@ import (
 	"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
 	"github.com/kaspanet/kaspad/infrastructure/logger"
 	"github.com/kaspanet/kaspad/util/staging"
+	"github.com/pkg/errors"
 	"sort"
 )
 
-func (csm *consensusStateManager) ResolveVirtual(maxBlocksToResolve uint64) (*externalapi.VirtualChangeSet, bool, error) {
-	onEnd := logger.LogAndMeasureExecutionTime(log, "csm.ResolveVirtual")
-	defer onEnd()
-
-	readStagingArea := model.NewStagingArea()
-	tips, err := csm.consensusStateStore.Tips(readStagingArea, csm.databaseContext)
+// tipsInDecreasingGHOSTDAGParentSelectionOrder returns the current DAG tips in decreasing parent selection order.
+// This means that the first tip in the resulting list would be the GHOSTDAG selected parent, and if removed from the list,
+// the second tip would be the selected parent, and so on.
+func (csm *consensusStateManager) tipsInDecreasingGHOSTDAGParentSelectionOrder(stagingArea *model.StagingArea) ([]*externalapi.DomainHash, error) {
+	tips, err := csm.consensusStateStore.Tips(stagingArea, csm.databaseContext)
 	if err != nil {
-		return nil, false, err
+		return nil, err
 	}
 
 	var sortErr error
 	sort.Slice(tips, func(i, j int) bool {
-		selectedParent, err := csm.ghostdagManager.ChooseSelectedParent(readStagingArea, tips[i], tips[j])
+		selectedParent, err := csm.ghostdagManager.ChooseSelectedParent(stagingArea, tips[i], tips[j])
 		if err != nil {
 			sortErr = err
 			return false
@@ -29,16 +29,22 @@ func (csm *consensusStateManager) ResolveVirtual(maxBlocksToResolve uint64) (*ex
 		return selectedParent.Equal(tips[i])
 	})
 	if sortErr != nil {
-		return nil, false, sortErr
+		return nil, sortErr
 	}
+	return tips, nil
+}
 
-	var selectedTip *externalapi.DomainHash
-	isCompletelyResolved := true
-	for _, tip := range tips {
+func (csm *consensusStateManager) findNextPendingTip(stagingArea *model.StagingArea) (*externalapi.DomainHash, externalapi.BlockStatus, error) {
+	orderedTips, err := csm.tipsInDecreasingGHOSTDAGParentSelectionOrder(stagingArea)
+	if err != nil {
+		return nil, externalapi.StatusInvalid, err
+	}
+
+	for _, tip := range orderedTips {
 		log.Debugf("Resolving tip %s", tip)
-		isViolatingFinality, shouldNotify, err := csm.isViolatingFinality(readStagingArea, tip)
+		isViolatingFinality, shouldNotify, err := csm.isViolatingFinality(stagingArea, tip)
 		if err != nil {
-			return nil, false, err
+			return nil, externalapi.StatusInvalid, err
 		}
 
 		if isViolatingFinality {
@@ -49,55 +55,147 @@ func (csm *consensusStateManager) ResolveVirtual(maxBlocksToResolve uint64) (*ex
 			continue
 		}
 
-		resolveStagingArea := model.NewStagingArea()
-		unverifiedBlocks, err := csm.getUnverifiedChainBlocks(resolveStagingArea, tip)
+		status, err := csm.blockStatusStore.Get(csm.databaseContext, stagingArea, tip)
 		if err != nil {
-			return nil, false, err
+			return nil, externalapi.StatusInvalid, err
 		}
+		if status == externalapi.StatusUTXOValid || status == externalapi.StatusUTXOPendingVerification {
+			return tip, status, nil
+		}
+	}
+
+	return nil, externalapi.StatusInvalid, nil
+}
+
+// getGHOSTDAGLowerTips returns the set of tips which are lower in GHOSTDAG parent selection order than `pendingTip`. i.e.,
+// they can be added to virtual parents but `pendingTip` will remain the virtual selected parent
+func (csm *consensusStateManager) getGHOSTDAGLowerTips(stagingArea *model.StagingArea, pendingTip *externalapi.DomainHash) ([]*externalapi.DomainHash, error) {
+	tips, err := csm.consensusStateStore.Tips(stagingArea, csm.databaseContext)
+	if err != nil {
+		return nil, err
+	}
 
-		resolveTip := tip
-		hasMoreUnverifiedThanMax := maxBlocksToResolve != 0 && uint64(len(unverifiedBlocks)) > maxBlocksToResolve
-		if hasMoreUnverifiedThanMax {
-			resolveTip = unverifiedBlocks[uint64(len(unverifiedBlocks))-maxBlocksToResolve]
-			log.Debugf("Has more than %d blocks to resolve. Changing the resolve tip to %s", maxBlocksToResolve, resolveTip)
+	lowerTips := []*externalapi.DomainHash{pendingTip}
+	for _, tip := range tips {
+		if tip.Equal(pendingTip) {
+			continue
+		}
+		selectedParent, err := csm.ghostdagManager.ChooseSelectedParent(stagingArea, tip, pendingTip)
+		if err != nil {
+			return nil, err
+		}
+		if selectedParent.Equal(pendingTip) {
+			lowerTips = append(lowerTips, tip)
 		}
+	}
+	return lowerTips, nil
+}
+
+func (csm *consensusStateManager) ResolveVirtual(maxBlocksToResolve uint64) (*externalapi.VirtualChangeSet, bool, error) {
+	onEnd := logger.LogAndMeasureExecutionTime(log, "csm.ResolveVirtual")
+	defer onEnd()
+
+	// We use a read-only staging area for some read-only actions, to avoid
+	// confusion with the resolve/updateVirtual staging areas below
+	readStagingArea := model.NewStagingArea()
+
+	pendingTip, pendingTipStatus, err := csm.findNextPendingTip(readStagingArea)
+	if err != nil {
+		return nil, false, err
+	}
+
+	if pendingTip == nil {
+		log.Warnf("None of the DAG tips are valid")
+		return nil, true, nil
+	}
 
-		blockStatus, reversalData, err := csm.resolveBlockStatus(resolveStagingArea, resolveTip, true)
+	previousVirtualSelectedParent, err := csm.virtualSelectedParent(readStagingArea)
+	if err != nil {
+		return nil, false, err
+	}
+
+	if pendingTipStatus == externalapi.StatusUTXOValid && previousVirtualSelectedParent.Equal(pendingTip) {
+		return nil, true, nil
+	}
+
+	// Resolve a chunk from the pending chain
+	resolveStagingArea := model.NewStagingArea()
+	unverifiedBlocks, err := csm.getUnverifiedChainBlocks(resolveStagingArea, pendingTip)
+	if err != nil {
+		return nil, false, err
+	}
+
+	// Initially set the resolve processing point to the pending tip
+	processingPoint := pendingTip
+
+	// Too many blocks to verify, so we only process a chunk and return
+	if maxBlocksToResolve != 0 && uint64(len(unverifiedBlocks)) > maxBlocksToResolve {
+		processingPointIndex := uint64(len(unverifiedBlocks)) - maxBlocksToResolve
+		processingPoint = unverifiedBlocks[processingPointIndex]
+		isNewVirtualSelectedParent, err := csm.isNewSelectedTip(readStagingArea, processingPoint, previousVirtualSelectedParent)
 		if err != nil {
 			return nil, false, err
 		}
 
-		if blockStatus == externalapi.StatusUTXOValid {
-			selectedTip = resolveTip
-			isCompletelyResolved = !hasMoreUnverifiedThanMax
-
-			err = staging.CommitAllChanges(csm.databaseContext, resolveStagingArea)
+		// We must find a processing point which wins previous virtual selected parent
+		// even if we process more than `maxBlocksToResolve` for that.
+		// Otherwise, internal UTXO diff logic gets all messed up
+		for !isNewVirtualSelectedParent {
+			if processingPointIndex == 0 {
+				return nil, false, errors.Errorf(
+					"Expecting the pending tip %s to overcome the previous selected parent %s", pendingTip, previousVirtualSelectedParent)
+			}
+			processingPointIndex--
+			processingPoint = unverifiedBlocks[processingPointIndex]
+			isNewVirtualSelectedParent, err = csm.isNewSelectedTip(readStagingArea, processingPoint, previousVirtualSelectedParent)
 			if err != nil {
 				return nil, false, err
 			}
-
-			if reversalData != nil {
-				err = csm.ReverseUTXODiffs(resolveTip, reversalData)
-				if err != nil {
-					return nil, false, err
-				}
-			}
-			break
 		}
+		log.Debugf("Has more than %d blocks to resolve. Setting the resolve processing point to %s", maxBlocksToResolve, processingPoint)
 	}
 
-	if selectedTip == nil {
-		log.Warnf("Non of the DAG tips are valid")
-		return nil, true, nil
-	}
-
-	oldVirtualGHOSTDAGData, err := csm.ghostdagDataStore.Get(csm.databaseContext, readStagingArea, model.VirtualBlockHash, false)
+	processingPointStatus, reversalData, err := csm.resolveBlockStatus(
+		resolveStagingArea, processingPoint, true)
 	if err != nil {
 		return nil, false, err
 	}
 
+	if processingPointStatus == externalapi.StatusUTXOValid {
+		err = staging.CommitAllChanges(csm.databaseContext, resolveStagingArea)
+		if err != nil {
+			return nil, false, err
+		}
+
+		if reversalData != nil {
+			err = csm.ReverseUTXODiffs(processingPoint, reversalData)
+			if err != nil {
+				return nil, false, err
+			}
+		}
+	}
+
+	isActualTip := processingPoint.Equal(pendingTip)
+	isCompletelyResolved := isActualTip && processingPointStatus == externalapi.StatusUTXOValid
+
 	updateVirtualStagingArea := model.NewStagingArea()
-	virtualUTXODiff, err := csm.updateVirtualWithParents(updateVirtualStagingArea, []*externalapi.DomainHash{selectedTip})
+
+	virtualParents := []*externalapi.DomainHash{processingPoint}
+	// If `isCompletelyResolved`, set virtual correctly with all tips which have less blue work than pending
+	if isCompletelyResolved {
+		lowerTips, err := csm.getGHOSTDAGLowerTips(readStagingArea, pendingTip)
+		if err != nil {
+			return nil, false, err
+		}
+		log.Debugf("Picking virtual parents from relevant tips len: %d", len(lowerTips))
+
+		virtualParents, err = csm.pickVirtualParents(readStagingArea, lowerTips)
+		if err != nil {
+			return nil, false, err
+		}
+		log.Debugf("Picked virtual parents: %s", virtualParents)
+	}
+	virtualUTXODiff, err := csm.updateVirtualWithParents(updateVirtualStagingArea, virtualParents)
 	if err != nil {
 		return nil, false, err
 	}
@@ -108,12 +206,12 @@ func (csm *consensusStateManager) ResolveVirtual(maxBlocksToResolve uint64) (*ex
 	}
 
 	selectedParentChainChanges, err := csm.dagTraversalManager.
-		CalculateChainPath(readStagingArea, oldVirtualGHOSTDAGData.SelectedParent(), selectedTip)
+		CalculateChainPath(updateVirtualStagingArea, previousVirtualSelectedParent, processingPoint)
 	if err != nil {
 		return nil, false, err
 	}
 
-	virtualParents, err := csm.dagTopologyManager.Parents(readStagingArea, model.VirtualBlockHash)
+	virtualParentsOutcome, err := csm.dagTopologyManager.Parents(updateVirtualStagingArea, model.VirtualBlockHash)
 	if err != nil {
 		return nil, false, err
 	}
@@ -121,6 +219,6 @@ func (csm *consensusStateManager) ResolveVirtual(maxBlocksToResolve uint64) (*ex
 	return &externalapi.VirtualChangeSet{
 		VirtualSelectedParentChainChanges: selectedParentChainChanges,
 		VirtualUTXODiff:                   virtualUTXODiff,
-		VirtualParents:                    virtualParents,
+		VirtualParents:                    virtualParentsOutcome,
 	}, isCompletelyResolved, nil
 }
diff --git a/domain/consensus/processes/consensusstatemanager/resolve_block_status.go b/domain/consensus/processes/consensusstatemanager/resolve_block_status.go
index 860a9447..631cbeee 100644
--- a/domain/consensus/processes/consensusstatemanager/resolve_block_status.go
+++ b/domain/consensus/processes/consensusstatemanager/resolve_block_status.go
@@ -233,7 +233,7 @@ func (csm *consensusStateManager) resolveSingleBlockStatus(stagingArea *model.St
 		return externalapi.StatusUTXOValid, nil, nil
 	}
 
-	oldSelectedTip, err := csm.selectedTip(stagingArea)
+	oldSelectedTip, err := csm.virtualSelectedParent(stagingArea)
 	if err != nil {
 		return 0, nil, err
 	}
@@ -298,7 +298,7 @@ func (csm *consensusStateManager) isNewSelectedTip(stagingArea *model.StagingAre
 	return blockHash.Equal(newSelectedTip), nil
 }
 
-func (csm *consensusStateManager) selectedTip(stagingArea *model.StagingArea) (*externalapi.DomainHash, error) {
+func (csm *consensusStateManager) virtualSelectedParent(stagingArea *model.StagingArea) (*externalapi.DomainHash, error) {
 	virtualGHOSTDAGData, err := csm.ghostdagDataStore.Get(csm.databaseContext, stagingArea, model.VirtualBlockHash, false)
 	if err != nil {
 		return nil, err
diff --git a/domain/consensus/processes/consensusstatemanager/reverse_utxo_diffs.go b/domain/consensus/processes/consensusstatemanager/reverse_utxo_diffs.go
index b8f388d2..49d2b24b 100644
--- a/domain/consensus/processes/consensusstatemanager/reverse_utxo_diffs.go
+++ b/domain/consensus/processes/consensusstatemanager/reverse_utxo_diffs.go
@@ -56,12 +56,6 @@ func (csm *consensusStateManager) ReverseUTXODiffs(tipHash *externalapi.DomainHa
 			return err
 		}
 
-		// We stop reversing when current's UTXODiffChild is not current's SelectedParent
-		if !currentBlockGHOSTDAGData.SelectedParent().Equal(currentBlockUTXODiffChild) {
-			log.Debugf("Block %s's UTXODiffChild is not it's selected parent - finish reversing", currentBlock)
-			break
-		}
-
 		currentUTXODiff := previousUTXODiff.Reversed()
 
 		// retrieve current utxoDiff for Bi, to be used by next block
@@ -75,6 +69,12 @@ func (csm *consensusStateManager) ReverseUTXODiffs(tipHash *externalapi.DomainHa
 			return err
 		}
 
+		// We stop reversing when current's UTXODiffChild is not current's SelectedParent
+		if !currentBlockGHOSTDAGData.SelectedParent().Equal(currentBlockUTXODiffChild) {
+			log.Debugf("Block %s's UTXODiffChild is not it's selected parent - finish reversing", currentBlock)
+			break
+		}
+
 		previousBlock = currentBlock
 		previousBlockGHOSTDAGData = currentBlockGHOSTDAGData
 
diff --git a/domain/consensus/processes/consensusstatemanager/update_virtual.go b/domain/consensus/processes/consensusstatemanager/update_virtual.go
index 8cb63c75..9b38bc94 100644
--- a/domain/consensus/processes/consensusstatemanager/update_virtual.go
+++ b/domain/consensus/processes/consensusstatemanager/update_virtual.go
@@ -110,7 +110,7 @@ func (csm *consensusStateManager) updateSelectedTipUTXODiff(
 	onEnd := logger.LogAndMeasureExecutionTime(log, "updateSelectedTipUTXODiff")
 	defer onEnd()
 
-	selectedTip, err := csm.selectedTip(stagingArea)
+	selectedTip, err := csm.virtualSelectedParent(stagingArea)
 	if err != nil {
 		return err
 	}
diff --git a/domain/migrate.go b/domain/migrate.go
index c1804862..ef75a045 100644
--- a/domain/migrate.go
+++ b/domain/migrate.go
@@ -214,34 +214,19 @@ func syncConsensuses(syncer, syncee externalapi.Consensus) error {
 		return err
 	}
 
-	virtualDAAScoreStart, err := syncee.GetVirtualDAAScore()
+	err = syncer.ResolveVirtual(func(virtualDAAScoreStart uint64, virtualDAAScore uint64) {
+		if estimatedVirtualDAAScoreTarget-virtualDAAScoreStart <= 0 {
+			percents = 100
+		} else {
+			percents = int(float64(virtualDAAScore-virtualDAAScoreStart) / float64(estimatedVirtualDAAScoreTarget-virtualDAAScoreStart) * 100)
+		}
+		log.Infof("Resolving virtual. Estimated progress: %d%%", percents)
+	})
 	if err != nil {
 		return err
 	}
 
-	percents = 0
-	for i := 0; ; i++ {
-		if i%10 == 0 {
-			virtualDAAScore, err := syncee.GetVirtualDAAScore()
-			if err != nil {
-				return err
-			}
-			newPercents := int(float64(virtualDAAScore-virtualDAAScoreStart) / float64(estimatedVirtualDAAScoreTarget-virtualDAAScoreStart) * 100)
-			if newPercents > percents {
-				percents = newPercents
-				log.Infof("Resolving virtual. Estimated progress: %d%%", percents)
-			}
-		}
-		isCompletelyResolved, err := syncee.ResolveVirtual()
-		if err != nil {
-			return err
-		}
-
-		if isCompletelyResolved {
-			log.Infof("Resolved virtual")
-			break
-		}
-	}
+	log.Infof("Resolved virtual")
 
 	return nil
 }


Test patch:
diff --git a/domain/consensus/finality_test.go b/domain/consensus/finality_test.go
index 5e4e9824..cf764428 100644
--- a/domain/consensus/finality_test.go
+++ b/domain/consensus/finality_test.go
@@ -589,18 +589,13 @@ func TestFinalityResolveVirtual(t *testing.T) {
 			}
 		}
 
-		for i := 0; ; i++ {
-			isCompletelyResolved, err := tc.ResolveVirtual()
-			if err != nil {
-				panic(err)
-			}
-
-			if isCompletelyResolved {
-				t.Log("Resolved virtual")
-				break
-			}
+		err = tc.ResolveVirtual(nil)
+		if err != nil {
+			panic(err)
 		}
 
+		t.Log("Resolved virtual")
+
 		sideChainTipGHOSTDAGData, err = tc.GHOSTDAGDataStore().Get(tc.DatabaseContext(), stagingArea, sideChainTipHash, false)
 		if err != nil {
 			panic(err)
diff --git a/domain/consensus/model/testapi/test_consensus.go b/domain/consensus/model/testapi/test_consensus.go
index 828cabd1..f7be5940 100644
--- a/domain/consensus/model/testapi/test_consensus.go
+++ b/domain/consensus/model/testapi/test_consensus.go
@@ -49,7 +49,7 @@ type TestConsensus interface {
 		*externalapi.VirtualChangeSet, error)
 	UpdatePruningPointByVirtual() error
 
-	ResolveVirtualWithMaxParam(maxBlocksToResolve uint64) (bool, error)
+	ResolveVirtualWithMaxParam(maxBlocksToResolve uint64) (*externalapi.VirtualChangeSet, bool, error)
 
 	MineJSON(r io.Reader, blockType MineJSONBlockType) (tips []*externalapi.DomainHash, err error)
 	ToJSON(w io.Writer) error
diff --git a/domain/consensus/processes/consensusstatemanager/resolve_virtual_test.go b/domain/consensus/processes/consensusstatemanager/resolve_virtual_test.go
index 4af0c337..5257c45b 100644
--- a/domain/consensus/processes/consensusstatemanager/resolve_virtual_test.go
+++ b/domain/consensus/processes/consensusstatemanager/resolve_virtual_test.go
@@ -1,6 +1,9 @@
 package consensusstatemanager_test
 
 import (
+	"fmt"
+	"github.com/kaspanet/kaspad/domain/consensus/model"
+	"github.com/kaspanet/kaspad/domain/consensus/model/testapi"
 	"github.com/kaspanet/kaspad/domain/consensus/utils/consensushashing"
 	"testing"
 
@@ -21,11 +24,14 @@ func TestAddBlockBetweenResolveVirtualCalls(t *testing.T) {
 		}
 		defer teardown(false)
 
+		hashes := []*externalapi.DomainHash{consensusConfig.GenesisHash}
+
 		// Create a chain of blocks
 		const initialChainLength = 10
 		previousBlockHash := consensusConfig.GenesisHash
 		for i := 0; i < initialChainLength; i++ {
 			previousBlockHash, _, err = tc.AddBlock([]*externalapi.DomainHash{previousBlockHash}, nil, nil)
+			hashes = append(hashes, previousBlockHash)
 			if err != nil {
 				t.Fatalf("Error mining block no. %d in initial chain: %+v", i, err)
 			}
@@ -40,6 +46,7 @@ func TestAddBlockBetweenResolveVirtualCalls(t *testing.T) {
 				t.Fatalf("Error mining block no. %d in re-org chain: %+v", i, err)
 			}
 			previousBlockHash = consensushashing.BlockHash(previousBlock)
+			hashes = append(hashes, previousBlockHash)
 
 			// Do not UTXO validate in order to resolve virtual later
 			err = tc.ValidateAndInsertBlock(previousBlock, false)
@@ -49,7 +56,7 @@ func TestAddBlockBetweenResolveVirtualCalls(t *testing.T) {
 		}
 
 		// Resolve one step
-		_, err = tc.ResolveVirtualWithMaxParam(2)
+		_, _, err = tc.ResolveVirtualWithMaxParam(2)
 		if err != nil {
 			t.Fatalf("Error resolving virtual in re-org chain: %+v", err)
 		}
@@ -68,7 +75,7 @@ func TestAddBlockBetweenResolveVirtualCalls(t *testing.T) {
 		}
 
 		// Resolve one more step
-		isCompletelyResolved, err := tc.ResolveVirtualWithMaxParam(2)
+		_, isCompletelyResolved, err := tc.ResolveVirtualWithMaxParam(2)
 		if err != nil {
 			t.Fatalf("Error resolving virtual in re-org chain: %+v", err)
 		}
@@ -78,14 +85,17 @@ func TestAddBlockBetweenResolveVirtualCalls(t *testing.T) {
 		if err != nil {
 			t.Fatalf("Error mining block during virtual resolution of reorg: %+v", err)
 		}
+		hashes = append(hashes, consensushashing.BlockHash(blockTemplate.Block))
 
 		// Complete resolving virtual
 		for !isCompletelyResolved {
-			isCompletelyResolved, err = tc.ResolveVirtualWithMaxParam(2)
+			_, isCompletelyResolved, err = tc.ResolveVirtualWithMaxParam(2)
 			if err != nil {
 				t.Fatalf("Error resolving virtual in re-org chain: %+v", err)
 			}
 		}
+
+		verifyUtxoDiffPaths(t, tc, hashes)
 	})
 }
 
@@ -100,11 +110,14 @@ func TestAddGenesisChildAfterOneResolveVirtualCall(t *testing.T) {
 		}
 		defer teardown(false)
 
+		hashes := []*externalapi.DomainHash{consensusConfig.GenesisHash}
+
 		// Create a chain of blocks
 		const initialChainLength = 6
 		previousBlockHash := consensusConfig.GenesisHash
 		for i := 0; i < initialChainLength; i++ {
 			previousBlockHash, _, err = tc.AddBlock([]*externalapi.DomainHash{previousBlockHash}, nil, nil)
+			hashes = append(hashes, previousBlockHash)
 			if err != nil {
 				t.Fatalf("Error mining block no. %d in initial chain: %+v", i, err)
 			}
@@ -119,6 +132,7 @@ func TestAddGenesisChildAfterOneResolveVirtualCall(t *testing.T) {
 				t.Fatalf("Error mining block no. %d in re-org chain: %+v", i, err)
 			}
 			previousBlockHash = consensushashing.BlockHash(previousBlock)
+			hashes = append(hashes, previousBlockHash)
 
 			// Do not UTXO validate in order to resolve virtual later
 			err = tc.ValidateAndInsertBlock(previousBlock, false)
@@ -128,7 +142,7 @@ func TestAddGenesisChildAfterOneResolveVirtualCall(t *testing.T) {
 		}
 
 		// Resolve one step
-		isCompletelyResolved, err := tc.ResolveVirtualWithMaxParam(2)
+		_, isCompletelyResolved, err := tc.ResolveVirtualWithMaxParam(2)
 		if err != nil {
 			t.Fatalf("Error resolving virtual in re-org chain: %+v", err)
 		}
@@ -140,11 +154,13 @@ func TestAddGenesisChildAfterOneResolveVirtualCall(t *testing.T) {
 
 		// Complete resolving virtual
 		for !isCompletelyResolved {
-			isCompletelyResolved, err = tc.ResolveVirtualWithMaxParam(2)
+			_, isCompletelyResolved, err = tc.ResolveVirtualWithMaxParam(2)
 			if err != nil {
 				t.Fatalf("Error resolving virtual in re-org chain: %+v", err)
 			}
 		}
+
+		verifyUtxoDiffPaths(t, tc, hashes)
 	})
 }
 
@@ -159,11 +175,14 @@ func TestAddGenesisChildAfterTwoResolveVirtualCalls(t *testing.T) {
 		}
 		defer teardown(false)
 
+		hashes := []*externalapi.DomainHash{consensusConfig.GenesisHash}
+
 		// Create a chain of blocks
 		const initialChainLength = 6
 		previousBlockHash := consensusConfig.GenesisHash
 		for i := 0; i < initialChainLength; i++ {
 			previousBlockHash, _, err = tc.AddBlock([]*externalapi.DomainHash{previousBlockHash}, nil, nil)
+			hashes = append(hashes, previousBlockHash)
 			if err != nil {
 				t.Fatalf("Error mining block no. %d in initial chain: %+v", i, err)
 			}
@@ -178,6 +197,7 @@ func TestAddGenesisChildAfterTwoResolveVirtualCalls(t *testing.T) {
 				t.Fatalf("Error mining block no. %d in re-org chain: %+v", i, err)
 			}
 			previousBlockHash = consensushashing.BlockHash(previousBlock)
+			hashes = append(hashes, previousBlockHash)
 
 			// Do not UTXO validate in order to resolve virtual later
 			err = tc.ValidateAndInsertBlock(previousBlock, false)
@@ -187,13 +207,13 @@ func TestAddGenesisChildAfterTwoResolveVirtualCalls(t *testing.T) {
 		}
 
 		// Resolve one step
-		_, err = tc.ResolveVirtualWithMaxParam(2)
+		_, _, err = tc.ResolveVirtualWithMaxParam(2)
 		if err != nil {
 			t.Fatalf("Error resolving virtual in re-org chain: %+v", err)
 		}
 
 		// Resolve one more step
-		isCompletelyResolved, err := tc.ResolveVirtualWithMaxParam(2)
+		_, isCompletelyResolved, err := tc.ResolveVirtualWithMaxParam(2)
 		if err != nil {
 			t.Fatalf("Error resolving virtual in re-org chain: %+v", err)
 		}
@@ -205,10 +225,220 @@ func TestAddGenesisChildAfterTwoResolveVirtualCalls(t *testing.T) {
 
 		// Complete resolving virtual
 		for !isCompletelyResolved {
-			isCompletelyResolved, err = tc.ResolveVirtualWithMaxParam(2)
+			_, isCompletelyResolved, err = tc.ResolveVirtualWithMaxParam(2)
+			if err != nil {
+				t.Fatalf("Error resolving virtual in re-org chain: %+v", err)
+			}
+		}
+
+		verifyUtxoDiffPaths(t, tc, hashes)
+	})
+}
+
+func TestResolveVirtualBackAndForthReorgs(t *testing.T) {
+
+	testutils.ForAllNets(t, true, func(t *testing.T, consensusConfig *consensus.Config) {
+		factory := consensus.NewFactory()
+
+		tc, teardown, err := factory.NewTestConsensus(consensusConfig, "TestAddGenesisChildAfterTwoResolveVirtualCalls")
+		if err != nil {
+			t.Fatalf("Error setting up consensus: %+v", err)
+		}
+		defer teardown(false)
+
+		hashes := []*externalapi.DomainHash{consensusConfig.GenesisHash}
+		blocks := make(map[externalapi.DomainHash]string)
+		blocks[*consensusConfig.GenesisHash] = "g"
+		blocks[*model.VirtualBlockHash] = "v"
+		printfDebug("%s\n\n", consensusConfig.GenesisHash)
+
+		// Create a chain of blocks
+		const initialChainLength = 6
+		previousBlockHash := consensusConfig.GenesisHash
+		for i := 0; i < initialChainLength; i++ {
+			previousBlockHash, _, err = tc.AddBlock([]*externalapi.DomainHash{previousBlockHash}, nil, nil)
+			blocks[*previousBlockHash] = fmt.Sprintf("A_%d", i)
+			hashes = append(hashes, previousBlockHash)
+			printfDebug("A_%d: %s\n", i, previousBlockHash)
+
+			if err != nil {
+				t.Fatalf("Error mining block no. %d in initial chain: %+v", i, err)
+			}
+		}
+
+		printfDebug("\n")
+		verifyUtxoDiffPaths(t, tc, hashes)
+
+		firstChainTip := previousBlockHash
+
+		// Mine a chain with more blocks, to re-organize the DAG
+		const reorgChainLength = 12 // initialChainLength + 1
+		previousBlockHash = consensusConfig.GenesisHash
+		for i := 0; i < reorgChainLength; i++ {
+			previousBlock, _, err := tc.BuildBlockWithParents([]*externalapi.DomainHash{previousBlockHash}, nil, nil)
+			if err != nil {
+				t.Fatalf("Error mining block no. %d in re-org chain: %+v", i, err)
+			}
+			previousBlockHash = consensushashing.BlockHash(previousBlock)
+			blocks[*previousBlockHash] = fmt.Sprintf("B_%d", i)
+			hashes = append(hashes, previousBlockHash)
+			printfDebug("B_%d: %s\n", i, previousBlockHash)
+
+			// Do not UTXO validate in order to resolve virtual later
+			err = tc.ValidateAndInsertBlock(previousBlock, false)
+			if err != nil {
+				t.Fatalf("Error mining block no. %d in re-org chain: %+v", i, err)
+			}
+		}
+
+		printfDebug("\n")
+
+		printUtxoDiffChildren(t, tc, hashes, blocks)
+		verifyUtxoDiffPaths(t, tc, hashes)
+
+		previousVirtualSelectedParent, err := tc.GetVirtualSelectedParent()
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		// Resolve one step
+		virtualChangeSet, _, err := tc.ResolveVirtualWithMaxParam(3)
+		if err != nil {
+			printUtxoDiffChildren(t, tc, hashes, blocks)
+			t.Fatalf("Error resolving virtual in re-org chain: %+v", err)
+		}
+
+		newVirtualSelectedParent, err := tc.GetVirtualSelectedParent()
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		// Make sure the reported change-set is compatible with actual changes.
+		// Checking this for one call should suffice to avoid possible bugs.
+		reportedPreviousVirtualSelectedParent := virtualChangeSet.VirtualSelectedParentChainChanges.Removed[0]
+		reportedNewVirtualSelectedParent := virtualChangeSet.VirtualSelectedParentChainChanges.
+			Added[len(virtualChangeSet.VirtualSelectedParentChainChanges.Added)-1]
+
+		if !previousVirtualSelectedParent.Equal(reportedPreviousVirtualSelectedParent) {
+			t.Fatalf("The reported changeset is incompatible with actual changes")
+		}
+		if !newVirtualSelectedParent.Equal(reportedNewVirtualSelectedParent) {
+			t.Fatalf("The reported changeset is incompatible with actual changes")
+		}
+
+		// Resolve one more step
+		_, isCompletelyResolved, err := tc.ResolveVirtualWithMaxParam(3)
+		if err != nil {
+			t.Fatalf("Error resolving virtual in re-org chain: %+v", err)
+		}
+
+		// Complete resolving virtual
+		for !isCompletelyResolved {
+			_, isCompletelyResolved, err = tc.ResolveVirtualWithMaxParam(3)
 			if err != nil {
 				t.Fatalf("Error resolving virtual in re-org chain: %+v", err)
 			}
 		}
+
+		printUtxoDiffChildren(t, tc, hashes, blocks)
+		verifyUtxoDiffPaths(t, tc, hashes)
+
+		// Now get the first chain back to the wining position
+		previousBlockHash = firstChainTip
+		for i := 0; i < reorgChainLength; i++ {
+			previousBlockHash, _, err = tc.AddBlock([]*externalapi.DomainHash{previousBlockHash}, nil, nil)
+			blocks[*previousBlockHash] = fmt.Sprintf("A_%d", initialChainLength+i)
+			hashes = append(hashes, previousBlockHash)
+			printfDebug("A_%d: %s\n", initialChainLength+i, previousBlockHash)
+
+			if err != nil {
+				t.Fatalf("Error mining block no. %d in initial chain: %+v", initialChainLength+i, err)
+			}
+		}
+
+		printfDebug("\n")
+
+		printUtxoDiffChildren(t, tc, hashes, blocks)
+		verifyUtxoDiffPaths(t, tc, hashes)
 	})
 }
+
+func verifyUtxoDiffPathToRoot(t *testing.T, tc testapi.TestConsensus, stagingArea *model.StagingArea, block, utxoDiffRoot *externalapi.DomainHash) {
+	current := block
+	for !current.Equal(utxoDiffRoot) {
+		hasUTXODiffChild, err := tc.UTXODiffStore().HasUTXODiffChild(tc.DatabaseContext(), stagingArea, current)
+		if err != nil {
+			t.Fatalf("Error while reading utxo diff store: %+v", err)
+		}
+		if !hasUTXODiffChild {
+			t.Fatalf("%s is expected to have a UTXO diff child", current)
+		}
+		current, err = tc.UTXODiffStore().UTXODiffChild(tc.DatabaseContext(), stagingArea, current)
+		if err != nil {
+			t.Fatalf("Error while reading utxo diff store: %+v", err)
+		}
+	}
+}
+
+func verifyUtxoDiffPaths(t *testing.T, tc testapi.TestConsensus, hashes []*externalapi.DomainHash) {
+	stagingArea := model.NewStagingArea()
+
+	virtualGHOSTDAGData, err := tc.GHOSTDAGDataStore().Get(tc.DatabaseContext(), stagingArea, model.VirtualBlockHash, false)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	utxoDiffRoot := virtualGHOSTDAGData.SelectedParent()
+	hasUTXODiffChild, err := tc.UTXODiffStore().HasUTXODiffChild(tc.DatabaseContext(), stagingArea, utxoDiffRoot)
+	if err != nil {
+		t.Fatalf("Error while reading utxo diff store: %+v", err)
+	}
+	if hasUTXODiffChild {
+		t.Fatalf("Virtual selected parent is not expected to have an explicit diff child")
+	}
+	_, err = tc.UTXODiffStore().UTXODiff(tc.DatabaseContext(), stagingArea, utxoDiffRoot)
+	if err != nil {
+		t.Fatalf("Virtual selected parent is expected to have a utxo diff: %+v", err)
+	}
+
+	for _, block := range hashes {
+		hasUTXODiffChild, err = tc.UTXODiffStore().HasUTXODiffChild(tc.DatabaseContext(), stagingArea, block)
+		if err != nil {
+			t.Fatalf("Error while reading utxo diff store: %+v", err)
+		}
+		isOnVirtualSelectedChain, err := tc.DAGTopologyManager().IsInSelectedParentChainOf(stagingArea, block, utxoDiffRoot)
+		if err != nil {
+			t.Fatal(err)
+		}
+		// We expect a valid path to root in both cases: (i) block has a diff child, (ii) block is on the virtual selected chain
+		if hasUTXODiffChild || isOnVirtualSelectedChain {
+			verifyUtxoDiffPathToRoot(t, tc, stagingArea, block, utxoDiffRoot)
+		}
+	}
+}
+
+func printfDebug(format string, a ...any) {
+	// Uncomment below when debugging the test
+	//fmt.Printf(format, a...)
+}
+
+func printUtxoDiffChildren(t *testing.T, tc testapi.TestConsensus, hashes []*externalapi.DomainHash, blocks map[externalapi.DomainHash]string) {
+	printfDebug("\n===============================\nBlock\t\tDiff child\n")
+	stagingArea := model.NewStagingArea()
+	for _, block := range hashes {
+		hasUTXODiffChild, err := tc.UTXODiffStore().HasUTXODiffChild(tc.DatabaseContext(), stagingArea, block)
+		if err != nil {
+			t.Fatalf("Error while reading utxo diff store: %+v", err)
+		}
+		if hasUTXODiffChild {
+			utxoDiffChild, err := tc.UTXODiffStore().UTXODiffChild(tc.DatabaseContext(), stagingArea, block)
+			if err != nil {
+				t.Fatalf("Error while reading utxo diff store: %+v", err)
+			}
+			printfDebug("%s\t\t\t%s\n", blocks[*block], blocks[*utxoDiffChild])
+		} else {
+			printfDebug("%s\n", blocks[*block])
+		}
+	}
+	printfDebug("\n===============================\n")
+}
diff --git a/domain/consensus/test_consensus.go b/domain/consensus/test_consensus.go
index 0eda211c..6f67c47a 100644
--- a/domain/consensus/test_consensus.go
+++ b/domain/consensus/test_consensus.go
@@ -112,11 +112,11 @@ func (tc *testConsensus) AddUTXOInvalidBlock(parentHashes []*externalapi.DomainH
 	return consensushashing.BlockHash(block), virtualChangeSet, nil
 }
 
-func (tc *testConsensus) ResolveVirtualWithMaxParam(maxBlocksToResolve uint64) (bool, error) {
+func (tc *testConsensus) ResolveVirtualWithMaxParam(maxBlocksToResolve uint64) (*externalapi.VirtualChangeSet, bool, error) {
 	tc.lock.Lock()
 	defer tc.lock.Unlock()
 
-	return tc.resolveVirtualNoLock(maxBlocksToResolve)
+	return tc.resolveVirtualChunkNoLock(maxBlocksToResolve)
 }
 
 // jsonBlock is a json representation of a block in mine format