mirror of
https://github.com/kaspanet/kaspad.git
synced 2025-06-05 13:46:42 +00:00

* Illustrate the bug through prints * Change consensus API to a single ResolveVirtual call * nil changeset is not expected when err=nil * Fixes a deep bug in the resolve virtual process * Be more defensive at resolving virtual when adding a block * When finally resolved, set virtual parents properly * Return nil changeset when nothing happened * Make sure the block at the split point is reversed to new chain as well * bump to version 0.12.4 * Avoid locking consensus twice in the common case of adding block with updateVirtual=true * check err * Parents must be picked first before set as virtual parents * Keep the flag for tracking virtual state, since tip sorting perf is high with many tips * Improve and clarify resolve virtual tests * Addressing minor review comments * Fixed a bug in the reported virtual changeset, and modified the test to verify it * Addressing review comments
225 lines
7.6 KiB
Go
225 lines
7.6 KiB
Go
package consensusstatemanager
|
|
|
|
import (
|
|
"github.com/kaspanet/kaspad/domain/consensus/model"
|
|
"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"
|
|
)
|
|
|
|
// 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, err
|
|
}
|
|
|
|
var sortErr error
|
|
sort.Slice(tips, func(i, j int) bool {
|
|
selectedParent, err := csm.ghostdagManager.ChooseSelectedParent(stagingArea, tips[i], tips[j])
|
|
if err != nil {
|
|
sortErr = err
|
|
return false
|
|
}
|
|
|
|
return selectedParent.Equal(tips[i])
|
|
})
|
|
if sortErr != nil {
|
|
return nil, sortErr
|
|
}
|
|
return tips, nil
|
|
}
|
|
|
|
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(stagingArea, tip)
|
|
if err != nil {
|
|
return nil, externalapi.StatusInvalid, err
|
|
}
|
|
|
|
if isViolatingFinality {
|
|
if shouldNotify {
|
|
//TODO: Send finality conflict notification
|
|
log.Warnf("Skipping %s tip resolution because it violates finality", tip)
|
|
}
|
|
continue
|
|
}
|
|
|
|
status, err := csm.blockStatusStore.Get(csm.databaseContext, stagingArea, tip)
|
|
if err != nil {
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
}
|
|
log.Debugf("Has more than %d blocks to resolve. Setting the resolve processing point to %s", maxBlocksToResolve, processingPoint)
|
|
}
|
|
|
|
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()
|
|
|
|
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
|
|
}
|
|
|
|
err = staging.CommitAllChanges(csm.databaseContext, updateVirtualStagingArea)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
selectedParentChainChanges, err := csm.dagTraversalManager.
|
|
CalculateChainPath(updateVirtualStagingArea, previousVirtualSelectedParent, processingPoint)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
virtualParentsOutcome, err := csm.dagTopologyManager.Parents(updateVirtualStagingArea, model.VirtualBlockHash)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
return &externalapi.VirtualChangeSet{
|
|
VirtualSelectedParentChainChanges: selectedParentChainChanges,
|
|
VirtualUTXODiff: virtualUTXODiff,
|
|
VirtualParents: virtualParentsOutcome,
|
|
}, isCompletelyResolved, nil
|
|
}
|