diff --git a/domain/consensus/factory.go b/domain/consensus/factory.go index e934aa2d7..ce12f7f4a 100644 --- a/domain/consensus/factory.go +++ b/domain/consensus/factory.go @@ -25,7 +25,7 @@ import ( "github.com/kaspanet/kaspad/domain/consensus/processes/ghostdagmanager" "github.com/kaspanet/kaspad/domain/consensus/processes/pastmediantimemanager" "github.com/kaspanet/kaspad/domain/consensus/processes/pruningmanager" - "github.com/kaspanet/kaspad/domain/consensus/processes/reachabilitytree" + "github.com/kaspanet/kaspad/domain/consensus/processes/reachabilitymanager" "github.com/kaspanet/kaspad/domain/consensus/processes/transactionvalidator" "github.com/kaspanet/kaspad/domain/dagconfig" "github.com/kaspanet/kaspad/infrastructure/db/database" @@ -56,12 +56,14 @@ func (f *factory) NewConsensus(dagParams *dagconfig.Params, db database.Database dbManager := dbmanager.New(db) // Processes - reachabilityTree := reachabilitytree.New( + reachabilityManager := reachabilitymanager.New( + dbManager, + ghostdagDataStore, blockRelationStore, reachabilityDataStore) dagTopologyManager := dagtopologymanager.New( dbManager, - reachabilityTree, + reachabilityManager, blockRelationStore) ghostdagManager := ghostdagmanager.New( dbManager, @@ -137,7 +139,7 @@ func (f *factory) NewConsensus(dagParams *dagconfig.Params, db database.Database pruningManager, blockValidator, dagTopologyManager, - reachabilityTree, + reachabilityManager, difficultyManager, pastMedianTimeManager, ghostdagManager, diff --git a/domain/consensus/model/interface_processes_reachabilitytree.go b/domain/consensus/model/interface_processes_reachabilitytree.go index fd9c38b6c..34684fe83 100644 --- a/domain/consensus/model/interface_processes_reachabilitytree.go +++ b/domain/consensus/model/interface_processes_reachabilitytree.go @@ -8,4 +8,5 @@ type ReachabilityTree interface { AddBlock(blockHash *externalapi.DomainHash) error IsReachabilityTreeAncestorOf(blockHashA *externalapi.DomainHash, blockHashB *externalapi.DomainHash) (bool, error) IsDAGAncestorOf(blockHashA *externalapi.DomainHash, blockHashB *externalapi.DomainHash) (bool, error) + UpdateReindexRoot(selectedTip *externalapi.DomainHash) error } diff --git a/domain/consensus/model/reachabilitydata.go b/domain/consensus/model/reachabilitydata.go index 17925d66f..31833b338 100644 --- a/domain/consensus/model/reachabilitydata.go +++ b/domain/consensus/model/reachabilitydata.go @@ -1,5 +1,7 @@ package model +import "github.com/kaspanet/kaspad/domain/consensus/model/externalapi" + // ReachabilityData holds the set of data required to answer // reachability queries type ReachabilityData struct { @@ -23,8 +25,8 @@ type ReachabilityData struct { // case, and so reindexing should always succeed unless more than // 2^64 blocks are added to the DAG/tree. type ReachabilityTreeNode struct { - Children []*ReachabilityTreeNode - Parent *ReachabilityTreeNode + Children []*externalapi.DomainHash + Parent *externalapi.DomainHash // interval is the index interval containing all intervals of // blocks in this node's subtree @@ -39,12 +41,6 @@ type ReachabilityInterval struct { End uint64 } -// OrderedTreeNodeSet is an ordered set of reachabilityTreeNodes -// Note that this type does not validate order validity. It's the -// responsibility of the caller to construct instances of this -// type properly. -type OrderedTreeNodeSet []*ReachabilityTreeNode - // FutureCoveringTreeNodeSet represents a collection of blocks in the future of // a certain block. Once a block B is added to the DAG, every block A_i in // B's selected parent anticone must register B in its FutureCoveringTreeNodeSet. This allows @@ -58,4 +54,4 @@ type OrderedTreeNodeSet []*ReachabilityTreeNode // // See insertNode, hasAncestorOf, and reachabilityTree.isInPast for further // details. -type FutureCoveringTreeNodeSet OrderedTreeNodeSet +type FutureCoveringTreeNodeSet []*externalapi.DomainHash diff --git a/domain/consensus/processes/reachabilitymanager/fetch.go b/domain/consensus/processes/reachabilitymanager/fetch.go new file mode 100644 index 000000000..d3196c3e9 --- /dev/null +++ b/domain/consensus/processes/reachabilitymanager/fetch.go @@ -0,0 +1,59 @@ +package reachabilitymanager + +import ( + "github.com/kaspanet/kaspad/domain/consensus/model" + "github.com/kaspanet/kaspad/domain/consensus/model/externalapi" +) + +func (rt *reachabilityManager) data(blockHash *externalapi.DomainHash) (*model.ReachabilityData, error) { + return rt.reachabilityDataStore.ReachabilityData(rt.databaseContext, blockHash) +} + +func (rt *reachabilityManager) futureCoveringSet(blockHash *externalapi.DomainHash) ([]*externalapi.DomainHash, error) { + data, err := rt.data(blockHash) + if err != nil { + return nil, err + } + + return data.FutureCoveringSet, nil +} + +func (rt *reachabilityManager) treeNode(blockHash *externalapi.DomainHash) (*model.ReachabilityTreeNode, error) { + data, err := rt.data(blockHash) + if err != nil { + return nil, err + } + + return data.TreeNode, nil +} + +func (rt *reachabilityManager) interval(blockHash *externalapi.DomainHash) (*model.ReachabilityInterval, error) { + treeNode, err := rt.treeNode(blockHash) + if err != nil { + return nil, err + } + + return treeNode.Interval, nil +} + +func (rt *reachabilityManager) children(blockHash *externalapi.DomainHash) ([]*externalapi.DomainHash, error) { + data, err := rt.data(blockHash) + if err != nil { + return nil, err + } + + return data.TreeNode.Children, nil +} + +func (rt *reachabilityManager) parent(blockHash *externalapi.DomainHash) (*externalapi.DomainHash, error) { + data, err := rt.data(blockHash) + if err != nil { + return nil, err + } + + return data.TreeNode.Parent, nil +} + +func (rt *reachabilityManager) reindexRoot() (*externalapi.DomainHash, error) { + return rt.reachabilityDataStore.ReachabilityReindexRoot(rt.databaseContext) +} diff --git a/domain/consensus/processes/reachabilitymanager/future_covering_set.go b/domain/consensus/processes/reachabilitymanager/future_covering_set.go new file mode 100644 index 000000000..79d5ee7a0 --- /dev/null +++ b/domain/consensus/processes/reachabilitymanager/future_covering_set.go @@ -0,0 +1,132 @@ +package reachabilitymanager + +import ( + "github.com/kaspanet/kaspad/domain/consensus/model/externalapi" +) + +// futureCoveringTreeNodeSet represents a collection of blocks in the future of +// a certain block. Once a block B is added to the DAG, every block A_i in +// B's selected parent anticone must register B in its futureCoveringTreeNodeSet. This allows +// to relatively quickly (O(log(|futureCoveringTreeNodeSet|))) query whether B +// is a descendent (is in the "future") of any block that previously +// registered it. +// +// Note that futureCoveringTreeNodeSet is meant to be queried only if B is not +// a reachability tree descendant of the block in question, as reachability +// tree queries are always O(1). +// +// See insertNode, hasAncestorOf, and reachabilityTree.isInPast for further +// details. +type futureCoveringTreeNodeSet orderedTreeNodeSet + +// insertToFutureCoveringSet inserts the given block into this node's FutureCoveringSet +// while keeping it ordered by interval. +// If a block B ∈ node.FutureCoveringSet exists such that its interval +// contains block's interval, block need not be added. If block's +// interval contains B's interval, it replaces it. +// +// Notes: +// * Intervals never intersect unless one contains the other +// (this follows from the tree structure and the indexing rule). +// * Since node.FutureCoveringSet is kept ordered, a binary search can be +// used for insertion/queries. +// * Although reindexing may change a block's interval, the +// is-superset relation will by definition +// be always preserved. +func (rt *reachabilityManager) insertToFutureCoveringSet(node, futureNode *externalapi.DomainHash) error { + futureCoveringSet, err := rt.futureCoveringSet(node) + if err != nil { + return err + } + + ancestorIndex, ok, err := rt.findAncestorIndexOfNode(futureCoveringSet, futureNode) + if err != nil { + return err + } + + if !ok { + newSet := append([]*externalapi.DomainHash{futureNode}, futureCoveringSet...) + err := rt.stageFutureCoveringSet(node, newSet) + if err != nil { + return err + } + + return nil + } + + candidate := futureCoveringSet[ancestorIndex] + candidateIsAncestorOfFutureNode, err := rt.IsReachabilityTreeAncestorOf(candidate, futureNode) + if err != nil { + return err + } + + if candidateIsAncestorOfFutureNode { + // candidate is an ancestor of futureNode, no need to insert + return nil + } + + futureNodeIsAncestorOfCandidate, err := rt.IsReachabilityTreeAncestorOf(futureNode, candidate) + if err != nil { + return err + } + + if futureNodeIsAncestorOfCandidate { + // futureNode is an ancestor of candidate, and can thus replace it + newSet := make([]*externalapi.DomainHash, len(futureCoveringSet)) + copy(newSet, futureCoveringSet) + newSet[ancestorIndex] = futureNode + + return rt.stageFutureCoveringSet(node, newSet) + } + + // Insert futureNode in the correct index to maintain futureCoveringTreeNodeSet as + // a sorted-by-interval list. + // Note that ancestorIndex might be equal to len(futureCoveringTreeNodeSet) + left := futureCoveringSet[:ancestorIndex+1] + right := append([]*externalapi.DomainHash{futureNode}, futureCoveringSet[ancestorIndex+1:]...) + newSet := append(left, right...) + return rt.stageFutureCoveringSet(node, newSet) + +} + +// futureCoveringSetHasAncestorOf resolves whether the given node `other` is in the subtree of +// any node in this.FutureCoveringSet. +// See insertNode method for the complementary insertion behavior. +// +// Like the insert method, this method also relies on the fact that +// this.FutureCoveringSet is kept ordered by interval to efficiently perform a +// binary search over this.FutureCoveringSet and answer the query in +// O(log(|futureCoveringTreeNodeSet|)). +func (rt *reachabilityManager) futureCoveringSetHasAncestorOf(this, other *externalapi.DomainHash) (bool, error) { + futureCoveringSet, err := rt.futureCoveringSet(this) + if err != nil { + return false, err + } + + ancestorIndex, ok, err := rt.findAncestorIndexOfNode(futureCoveringSet, other) + if err != nil { + return false, err + } + + if !ok { + // No candidate to contain other + return false, nil + } + + candidate := futureCoveringSet[ancestorIndex] + return rt.IsReachabilityTreeAncestorOf(candidate, other) +} + +// futureCoveringSetString returns a string representation of the intervals in this futureCoveringSet. +func (rt *reachabilityManager) futureCoveringSetString(futureCoveringSet []*externalapi.DomainHash) (string, error) { + intervalsString := "" + for _, node := range futureCoveringSet { + nodeInterval, err := rt.interval(node) + if err != nil { + return "", err + } + + intervalsString += intervalString(nodeInterval) + } + return intervalsString, nil +} diff --git a/domain/consensus/processes/reachabilitymanager/interval.go b/domain/consensus/processes/reachabilitymanager/interval.go new file mode 100644 index 000000000..8ce46407a --- /dev/null +++ b/domain/consensus/processes/reachabilitymanager/interval.go @@ -0,0 +1,123 @@ +package reachabilitymanager + +import ( + "fmt" + "github.com/kaspanet/kaspad/domain/consensus/model" + "github.com/pkg/errors" + "math" +) + +func newReachabilityInterval(start uint64, end uint64) *model.ReachabilityInterval { + return &model.ReachabilityInterval{Start: start, End: end} +} + +// intervalSize returns the size of this interval. Note that intervals are +// inclusive from both sides. +func intervalSize(ri *model.ReachabilityInterval) uint64 { + return ri.End - ri.Start + 1 +} + +// intervalSplitInHalf splits this interval by a fraction of 0.5. +// See splitFraction for further details. +func intervalSplitInHalf(ri *model.ReachabilityInterval) ( + left *model.ReachabilityInterval, right *model.ReachabilityInterval, err error) { + + return intervalSplitFraction(ri, 0.5) +} + +// intervalSplitFraction splits this interval to two parts such that their +// union is equal to the original interval and the first (left) part +// contains the given fraction of the original interval's size. +// Note: if the split results in fractional parts, this method rounds +// the first part up and the last part down. +func intervalSplitFraction(ri *model.ReachabilityInterval, fraction float64) ( + left *model.ReachabilityInterval, right *model.ReachabilityInterval, err error) { + + if fraction < 0 || fraction > 1 { + return nil, nil, errors.Errorf("fraction must be between 0 and 1") + } + if intervalSize(ri) == 0 { + return nil, nil, errors.Errorf("cannot split an empty interval") + } + + allocationSize := uint64(math.Ceil(float64(intervalSize(ri)) * fraction)) + left = newReachabilityInterval(ri.Start, ri.Start+allocationSize-1) + right = newReachabilityInterval(ri.Start+allocationSize, ri.End) + return left, right, nil +} + +// intervalSplitExact splits this interval to exactly |sizes| parts where +// |part_i| = sizes[i]. This method expects sum(sizes) to be exactly +// equal to the interval's size. +func intervalSplitExact(ri *model.ReachabilityInterval, sizes []uint64) ([]*model.ReachabilityInterval, error) { + sizesSum := uint64(0) + for _, size := range sizes { + sizesSum += size + } + if sizesSum != intervalSize(ri) { + return nil, errors.Errorf("sum of sizes must be equal to the interval's size") + } + + intervals := make([]*model.ReachabilityInterval, len(sizes)) + start := ri.Start + for i, size := range sizes { + intervals[i] = newReachabilityInterval(start, start+size-1) + start += size + } + return intervals, nil +} + +// intervalSplitWithExponentialBias splits this interval to |sizes| parts +// by the allocation rule described below. This method expects sum(sizes) +// to be smaller or equal to the interval's size. Every part_i is +// allocated at least sizes[i] capacity. The remaining budget is +// split by an exponentially biased rule described below. +// +// This rule follows the GHOSTDAG protocol behavior where the child +// with the largest subtree is expected to dominate the competition +// for new blocks and thus grow the most. However, we may need to +// add slack for non-largest subtrees in order to make CPU reindexing +// attacks unworthy. +func intervalSplitWithExponentialBias(ri *model.ReachabilityInterval, sizes []uint64) ([]*model.ReachabilityInterval, error) { + intervalSize := intervalSize(ri) + sizesSum := uint64(0) + for _, size := range sizes { + sizesSum += size + } + if sizesSum > intervalSize { + return nil, errors.Errorf("sum of sizes must be less than or equal to the interval's size") + } + if sizesSum == intervalSize { + return intervalSplitExact(ri, sizes) + } + + // Add a fractional bias to every size in the given sizes + totalBias := intervalSize - sizesSum + remainingBias := totalBias + biasedSizes := make([]uint64, len(sizes)) + fractions := exponentialFractions(sizes) + for i, fraction := range fractions { + var bias uint64 + if i == len(fractions)-1 { + bias = remainingBias + } else { + bias = uint64(math.Round(float64(totalBias) * fraction)) + if bias > remainingBias { + bias = remainingBias + } + } + biasedSizes[i] = sizes[i] + bias + remainingBias -= bias + } + return intervalSplitExact(ri, biasedSizes) +} + +// intervalContains returns true if ri contains other. +func intervalContains(ri *model.ReachabilityInterval, other *model.ReachabilityInterval) bool { + return ri.Start <= other.Start && other.End <= ri.End +} + +// intervalString returns a string representation of the interval. +func intervalString(ri *model.ReachabilityInterval) string { + return fmt.Sprintf("[%d,%d]", ri.Start, ri.End) +} diff --git a/domain/consensus/processes/reachabilitymanager/log.go b/domain/consensus/processes/reachabilitymanager/log.go new file mode 100644 index 000000000..8579a1227 --- /dev/null +++ b/domain/consensus/processes/reachabilitymanager/log.go @@ -0,0 +1,7 @@ +package reachabilitymanager + +import ( + "github.com/kaspanet/kaspad/infrastructure/logger" +) + +var log, _ = logger.Get(logger.SubsystemTags.REAC) diff --git a/domain/consensus/processes/reachabilitymanager/ordered_tree_node_set.go b/domain/consensus/processes/reachabilitymanager/ordered_tree_node_set.go new file mode 100644 index 000000000..0713ceda0 --- /dev/null +++ b/domain/consensus/processes/reachabilitymanager/ordered_tree_node_set.go @@ -0,0 +1,79 @@ +package reachabilitymanager + +import ( + "github.com/kaspanet/kaspad/domain/consensus/model" + "github.com/kaspanet/kaspad/domain/consensus/model/externalapi" +) + +// orderedTreeNodeSet is an ordered set of model.ReachabilityTreeNodes +// Note that this type does not validate order validity. It's the +// responsibility of the caller to construct instances of this +// type properly. +type orderedTreeNodeSet []*externalapi.DomainHash + +// findAncestorOfNode finds the reachability tree ancestor of `node` +// among the nodes in `tns`. +func (rt *reachabilityManager) findAncestorOfNode(tns orderedTreeNodeSet, node *externalapi.DomainHash) (*externalapi.DomainHash, bool) { + ancestorIndex, ok, err := rt.findAncestorIndexOfNode(tns, node) + if err != nil { + return nil, false + } + + if !ok { + return nil, false + } + + return tns[ancestorIndex], true +} + +// findAncestorIndexOfNode finds the index of the reachability tree +// ancestor of `node` among the nodes in `tns`. It does so by finding +// the index of the block with the maximum start that is below the +// given block. +func (rt *reachabilityManager) findAncestorIndexOfNode(tns orderedTreeNodeSet, node *externalapi.DomainHash) (int, bool, error) { + treeNode, err := rt.treeNode(node) + if err != nil { + return 0, false, err + } + + blockInterval := treeNode.Interval + end := blockInterval.End + + low := 0 + high := len(tns) + for low < high { + middle := (low + high) / 2 + middleInterval, err := rt.interval(tns[middle]) + if err != nil { + return 0, false, err + } + + if end < middleInterval.Start { + high = middle + } else { + low = middle + 1 + } + } + + if low == 0 { + return 0, false, nil + } + return low - 1, true, nil +} + +func (rt *reachabilityManager) propagateIntervals(tns orderedTreeNodeSet, intervals []*model.ReachabilityInterval, + subtreeSizeMaps []map[externalapi.DomainHash]uint64) error { + + for i, node := range tns { + err := rt.stageInterval(node, intervals[i]) + if err != nil { + return err + } + subtreeSizeMap := subtreeSizeMaps[i] + err = rt.propagateInterval(node, subtreeSizeMap) + if err != nil { + return err + } + } + return nil +} diff --git a/domain/consensus/processes/reachabilitymanager/reachability.go b/domain/consensus/processes/reachabilitymanager/reachability.go new file mode 100644 index 000000000..da5582495 --- /dev/null +++ b/domain/consensus/processes/reachabilitymanager/reachability.go @@ -0,0 +1,30 @@ +package reachabilitymanager + +import ( + "github.com/kaspanet/kaspad/domain/consensus/model/externalapi" +) + +// IsDAGAncestorOf returns true if blockHashA is an ancestor of +// blockHashB in the DAG. +// +// Note: this method will return true if blockHashA == blockHashB +// The complexity of this method is O(log(|this.futureCoveringTreeNodeSet|)) +func (rt *reachabilityManager) IsDAGAncestorOf(blockHashA, blockHashB *externalapi.DomainHash) (bool, error) { + // Check if this node is a reachability tree ancestor of the + // other node + isReachabilityTreeAncestor, err := rt.IsReachabilityTreeAncestorOf(blockHashA, blockHashB) + if err != nil { + return false, err + } + if isReachabilityTreeAncestor { + return true, nil + } + + // Otherwise, use previously registered future blocks to complete the + // reachability test + return rt.futureCoveringSetHasAncestorOf(blockHashA, blockHashB) +} + +func (rt *reachabilityManager) UpdateReindexRoot(selectedTip *externalapi.DomainHash) error { + return rt.updateReindexRoot(selectedTip) +} diff --git a/domain/consensus/processes/reachabilitymanager/reachabilitymanager.go b/domain/consensus/processes/reachabilitymanager/reachabilitymanager.go new file mode 100644 index 000000000..b785155d8 --- /dev/null +++ b/domain/consensus/processes/reachabilitymanager/reachabilitymanager.go @@ -0,0 +1,77 @@ +package reachabilitymanager + +import ( + "github.com/kaspanet/kaspad/domain/consensus/model" + "github.com/kaspanet/kaspad/domain/consensus/model/externalapi" +) + +// reachabilityManager maintains a structure that allows to answer +// reachability queries in sub-linear time +type reachabilityManager struct { + databaseContext model.DBReader + blockRelationStore model.BlockRelationStore + reachabilityDataStore model.ReachabilityDataStore + ghostdagDataStore model.GHOSTDAGDataStore +} + +// New instantiates a new reachabilityManager +func New( + databaseContext model.DBReader, + ghostdagDataStore model.GHOSTDAGDataStore, + blockRelationStore model.BlockRelationStore, + reachabilityDataStore model.ReachabilityDataStore, +) model.ReachabilityTree { + return &reachabilityManager{ + databaseContext: databaseContext, + ghostdagDataStore: ghostdagDataStore, + blockRelationStore: blockRelationStore, + reachabilityDataStore: reachabilityDataStore, + } +} + +// AddBlock adds the block with the given blockHash into the reachability tree. +func (rt *reachabilityManager) AddBlock(blockHash *externalapi.DomainHash) error { + // Allocate a new reachability tree node + newTreeNode := newReachabilityTreeNode() + err := rt.stageTreeNode(blockHash, newTreeNode) + if err != nil { + return err + } + + ghostdagData, err := rt.ghostdagDataStore.Get(rt.databaseContext, blockHash) + if err != nil { + return err + } + + // If this is the genesis node, simply initialize it and return + if ghostdagData.SelectedParent == nil { + rt.stageReindexRoot(blockHash) + return nil + } + + reindexRoot, err := rt.reindexRoot() + if err != nil { + return err + } + + // Insert the node into the selected parent's reachability tree + err = rt.addChild(ghostdagData.SelectedParent, blockHash, reindexRoot) + if err != nil { + return err + } + + // Add the block to the futureCoveringSets of all the blocks + // in the merget set + mergeSet := make([]*externalapi.DomainHash, len(ghostdagData.MergeSetBlues)+len(ghostdagData.MergeSetReds)) + copy(mergeSet, ghostdagData.MergeSetBlues) + copy(mergeSet[len(ghostdagData.MergeSetBlues):], ghostdagData.MergeSetReds) + + for _, current := range mergeSet { + err = rt.insertToFutureCoveringSet(current, blockHash) + if err != nil { + return err + } + } + + return nil +} diff --git a/domain/consensus/processes/reachabilitymanager/stage.go b/domain/consensus/processes/reachabilitymanager/stage.go new file mode 100644 index 000000000..0c38d61d8 --- /dev/null +++ b/domain/consensus/processes/reachabilitymanager/stage.go @@ -0,0 +1,66 @@ +package reachabilitymanager + +import ( + "github.com/kaspanet/kaspad/domain/consensus/model" + "github.com/kaspanet/kaspad/domain/consensus/model/externalapi" +) + +func (rt *reachabilityManager) stageData(blockHash *externalapi.DomainHash, data *model.ReachabilityData) { + rt.reachabilityDataStore.StageReachabilityData(blockHash, data) +} + +func (rt *reachabilityManager) stageFutureCoveringSet(blockHash *externalapi.DomainHash, set model.FutureCoveringTreeNodeSet) error { + data, err := rt.data(blockHash) + if err != nil { + return err + } + + data.FutureCoveringSet = set + rt.reachabilityDataStore.StageReachabilityData(blockHash, data) + return nil +} + +func (rt *reachabilityManager) stageTreeNode(blockHash *externalapi.DomainHash, node *model.ReachabilityTreeNode) error { + data, err := rt.data(blockHash) + if err != nil { + return err + } + + data.TreeNode = node + rt.reachabilityDataStore.StageReachabilityData(blockHash, data) + return nil +} + +func (rt *reachabilityManager) stageReindexRoot(blockHash *externalapi.DomainHash) { + rt.reachabilityDataStore.StageReachabilityReindexRoot(blockHash) +} + +func (rt *reachabilityManager) addChildAndStage(node, child *externalapi.DomainHash) error { + nodeData, err := rt.data(node) + if err != nil { + return err + } + + nodeData.TreeNode.Children = append(nodeData.TreeNode.Children, child) + return rt.stageTreeNode(node, nodeData.TreeNode) +} + +func (rt *reachabilityManager) stageParent(node, parent *externalapi.DomainHash) error { + treeNode, err := rt.treeNode(node) + if err != nil { + return err + } + + treeNode.Parent = parent + return rt.stageTreeNode(node, treeNode) +} + +func (rt *reachabilityManager) stageInterval(node *externalapi.DomainHash, interval *model.ReachabilityInterval) error { + treeNode, err := rt.treeNode(node) + if err != nil { + return err + } + + treeNode.Interval = interval + return rt.stageTreeNode(node, treeNode) +} diff --git a/domain/consensus/processes/reachabilitymanager/tree.go b/domain/consensus/processes/reachabilitymanager/tree.go new file mode 100644 index 000000000..57286d798 --- /dev/null +++ b/domain/consensus/processes/reachabilitymanager/tree.go @@ -0,0 +1,1039 @@ +package reachabilitymanager + +import ( + "github.com/kaspanet/kaspad/domain/consensus/model" + "github.com/kaspanet/kaspad/domain/consensus/model/externalapi" + "math" + "strings" + "time" + + "github.com/pkg/errors" +) + +var ( + // reachabilityReindexWindow is the target window size for reachability + // reindexes. Note that this is not a constant for testing purposes. + reachabilityReindexWindow uint64 = 200 + + // reachabilityReindexSlack is the slack interval given to reachability + // tree nodes not in the selected parent chain. Note that this is not + // a constant for testing purposes. + reachabilityReindexSlack uint64 = 1 << 12 + + // slackReachabilityIntervalForReclaiming is the slack interval to + // reclaim during reachability reindexes earlier than the reindex root. + // See reclaimIntervalBeforeChosenChild for further details. Note that + // this is not a constant for testing purposes. + slackReachabilityIntervalForReclaiming uint64 = 1 +) + +// exponentialFractions returns a fraction of each size in sizes +// as follows: +// fraction[i] = 2^size[i] / sum_j(2^size[j]) +// In the code below the above equation is divided by 2^max(size) +// to avoid exploding numbers. Note that in 1 / 2^(max(size)-size[i]) +// we divide 1 by potentially a very large number, which will +// result in loss of float precision. This is not a problem - all +// numbers close to 0 bear effectively the same weight. +func exponentialFractions(sizes []uint64) []float64 { + maxSize := uint64(0) + for _, size := range sizes { + if size > maxSize { + maxSize = size + } + } + fractions := make([]float64, len(sizes)) + for i, size := range sizes { + fractions[i] = 1 / math.Pow(2, float64(maxSize-size)) + } + fractionsSum := float64(0) + for _, fraction := range fractions { + fractionsSum += fraction + } + for i, fraction := range fractions { + fractions[i] = fraction / fractionsSum + } + return fractions +} + +func newReachabilityTreeNode() *model.ReachabilityTreeNode { + // Please see the comment above model.ReachabilityTreeNode to understand why + // we use these initial values. + interval := newReachabilityInterval(1, math.MaxUint64-1) + return &model.ReachabilityTreeNode{Interval: interval} +} + +func (rt *reachabilityManager) intervalRangeForChildAllocation(hash *externalapi.DomainHash) (*model.ReachabilityInterval, error) { + interval, err := rt.interval(hash) + if err != nil { + return nil, err + } + + // We subtract 1 from the end of the range to prevent the node from allocating + // the entire interval to its child, so its interval would *strictly* contain the interval of its child. + return newReachabilityInterval(interval.Start, interval.End-1), nil +} + +func (rt *reachabilityManager) remainingIntervalBefore(node *externalapi.DomainHash) (*model.ReachabilityInterval, error) { + childRange, err := rt.intervalRangeForChildAllocation(node) + if err != nil { + return nil, err + } + + children, err := rt.children(node) + if err != nil { + return nil, err + } + + if len(children) == 0 { + return childRange, nil + } + + firstChildInterval, err := rt.interval(children[0]) + if err != nil { + return nil, err + } + + return newReachabilityInterval(childRange.Start, firstChildInterval.Start-1), nil +} + +func (rt *reachabilityManager) remainingIntervalAfter(node *externalapi.DomainHash) (*model.ReachabilityInterval, error) { + childRange, err := rt.intervalRangeForChildAllocation(node) + if err != nil { + return nil, err + } + + children, err := rt.children(node) + if err != nil { + return nil, err + } + + if len(children) == 0 { + return childRange, nil + } + + lastChildInterval, err := rt.interval(children[len(children)-1]) + if err != nil { + return nil, err + } + + return newReachabilityInterval(lastChildInterval.End+1, childRange.End), nil +} + +func (rt *reachabilityManager) hasSlackIntervalBefore(node *externalapi.DomainHash) (bool, error) { + interval, err := rt.remainingIntervalBefore(node) + if err != nil { + return false, err + } + + return intervalSize(interval) > 0, nil +} + +func (rt *reachabilityManager) hasSlackIntervalAfter(node *externalapi.DomainHash) (bool, error) { + interval, err := rt.remainingIntervalAfter(node) + if err != nil { + return false, err + } + + return intervalSize(interval) > 0, nil +} + +// addChild adds child to this tree node. If this node has no +// remaining interval to allocate, a reindexing is triggered. +// This method returns a list of model.ReachabilityTreeNodes modified +// by it. +func (rt *reachabilityManager) addChild(node, child, reindexRoot *externalapi.DomainHash) error { + remaining, err := rt.remainingIntervalAfter(node) + if err != nil { + return err + } + + // Set the parent-child relationship + err = rt.addChildAndStage(node, child) + if err != nil { + return err + } + + err = rt.stageParent(child, node) + if err != nil { + return err + } + + // Temporarily set the child's interval to be empty, at + // the start of node's remaining interval. This is done + // so that child-of-node checks (e.g. + // findAncestorOfThisAmongChildrenOfOther) will not fail for node. + err = rt.stageInterval(child, newReachabilityInterval(remaining.Start, remaining.Start-1)) + if err != nil { + return err + } + + // Handle node not being a descendant of the reindex root. + // Note that we check node here instead of child because + // at this point we don't yet know child's interval. + isReindexRootAncestorOfNode, err := rt.IsReachabilityTreeAncestorOf(reindexRoot, node) + if err != nil { + return err + } + + if !isReindexRootAncestorOfNode { + reindexStartTime := time.Now() + err := rt.reindexIntervalsEarlierThanReindexRoot(node, reindexRoot) + if err != nil { + return err + } + reindexTimeElapsed := time.Since(reindexStartTime) + log.Debugf("Reachability reindex triggered for "+ + "block %s. This block is not a child of the current "+ + "reindex root %s. Took %dms.", + node, reindexRoot, reindexTimeElapsed.Milliseconds()) + return nil + } + + // No allocation space left -- reindex + if intervalSize(remaining) == 0 { + reindexStartTime := time.Now() + err := rt.reindexIntervals(node) + if err != nil { + return err + } + reindexTimeElapsed := time.Since(reindexStartTime) + log.Debugf("Reachability reindex triggered for "+ + "block %s. Took %dms.", + node, reindexTimeElapsed.Milliseconds()) + return nil + } + + // Allocate from the remaining space + allocated, _, err := intervalSplitInHalf(remaining) + if err != nil { + return err + } + + return rt.stageInterval(child, allocated) +} + +// reindexIntervals traverses the reachability subtree that's +// defined by this node and reallocates reachability interval space +// such that another reindexing is unlikely to occur shortly +// thereafter. It does this by traversing down the reachability +// tree until it finds a node with a subreeSize that's greater than +// its interval size. See propagateInterval for further details. +// This method returns a list of model.ReachabilityTreeNodes modified by it. +func (rt *reachabilityManager) reindexIntervals(node *externalapi.DomainHash) error { + current := node + + // Initial interval and subtree sizes + currentInterval, err := rt.interval(node) + if err != nil { + return err + } + + size := intervalSize(currentInterval) + subTreeSizeMap := make(map[externalapi.DomainHash]uint64) + err = rt.countSubtrees(current, subTreeSizeMap) + if err != nil { + return err + } + + currentSubtreeSize := subTreeSizeMap[*current] + + // Find the first ancestor that has sufficient interval space + for size < currentSubtreeSize { + currentParent, err := rt.parent(current) + if err != nil { + return err + } + + if currentParent == nil { + // If we ended up here it means that there are more + // than 2^64 blocks, which shouldn't ever happen. + return errors.Errorf("missing tree " + + "parent during reindexing. Theoretically, this " + + "should only ever happen if there are more " + + "than 2^64 blocks in the DAG.") + } + current = currentParent + currentInterval, err := rt.interval(current) + if err != nil { + return err + } + + size = intervalSize(currentInterval) + err = rt.countSubtrees(current, subTreeSizeMap) + if err != nil { + return err + } + + currentSubtreeSize = subTreeSizeMap[*current] + } + + // Propagate the interval to the subtree + return rt.propagateInterval(current, subTreeSizeMap) +} + +// countSubtrees counts the size of each subtree under this node, +// and populates the provided subTreeSizeMap with the results. +// It is equivalent to the following recursive implementation: +// +// func (rt *reachabilityManager) countSubtrees(node *model.ReachabilityTreeNode) uint64 { +// subtreeSize := uint64(0) +// for _, child := range node.children { +// subtreeSize += child.countSubtrees() +// } +// return subtreeSize + 1 +// } +// +// However, we are expecting (linearly) deep trees, and so a +// recursive stack-based approach is inefficient and will hit +// recursion limits. Instead, the same logic was implemented +// using a (queue-based) BFS method. At a high level, the +// algorithm uses BFS for reaching all leaves and pushes +// intermediate updates from leaves via parent chains until all +// size information is gathered at the root of the operation +// (i.e. at node). +func (rt *reachabilityManager) countSubtrees(node *externalapi.DomainHash, subTreeSizeMap map[externalapi.DomainHash]uint64) error { + queue := []*externalapi.DomainHash{node} + calculatedChildrenCount := make(map[externalapi.DomainHash]uint64) + for len(queue) > 0 { + var current *externalapi.DomainHash + current, queue = queue[0], queue[1:] + currentChildren, err := rt.children(current) + if err != nil { + return err + } + + if len(currentChildren) == 0 { + // We reached a leaf + subTreeSizeMap[*current] = 1 + } else if _, ok := subTreeSizeMap[*current]; !ok { + // We haven't yet calculated the subtree size of + // the current node. Add all its children to the + // queue + queue = append(queue, currentChildren...) + continue + } + + // We reached a leaf or a pre-calculated subtree. + // Push information up + for current != node { + current, err = rt.parent(current) + if err != nil { + return err + } + + calculatedChildrenCount[*current]++ + + currentChildren, err := rt.children(current) + if err != nil { + return err + } + + if calculatedChildrenCount[*current] != uint64(len(currentChildren)) { + // Not all subtrees of the current node are ready + break + } + // All children of `current` have calculated their subtree size. + // Sum them all together and add 1 to get the sub tree size of + // `current`. + childSubtreeSizeSum := uint64(0) + for _, child := range currentChildren { + childSubtreeSizeSum += subTreeSizeMap[*child] + } + subTreeSizeMap[*current] = childSubtreeSizeSum + 1 + } + } + + return nil +} + +// propagateInterval propagates the new interval using a BFS traversal. +// Subtree intervals are recursively allocated according to subtree sizes and +// the allocation rule in splitWithExponentialBias. This method returns +// a list of model.ReachabilityTreeNodes modified by it. +func (rt *reachabilityManager) propagateInterval(node *externalapi.DomainHash, subTreeSizeMap map[externalapi.DomainHash]uint64) error { + + queue := []*externalapi.DomainHash{node} + for len(queue) > 0 { + var current *externalapi.DomainHash + current, queue = queue[0], queue[1:] + + currentChildren, err := rt.children(current) + if err != nil { + return err + } + + if len(currentChildren) > 0 { + sizes := make([]uint64, len(currentChildren)) + for i, child := range currentChildren { + sizes[i] = subTreeSizeMap[*child] + } + + interval, err := rt.intervalRangeForChildAllocation(current) + if err != nil { + return err + } + + intervals, err := intervalSplitWithExponentialBias(interval, sizes) + if err != nil { + return err + } + for i, child := range currentChildren { + childInterval := intervals[i] + err = rt.stageInterval(child, childInterval) + if err != nil { + return err + } + queue = append(queue, child) + } + } + } + return nil +} + +func (rt *reachabilityManager) reindexIntervalsEarlierThanReindexRoot(node, + reindexRoot *externalapi.DomainHash) error { + + // Find the common ancestor for both node and the reindex root + commonAncestor, err := rt.findCommonAncestorWithReindexRoot(node, reindexRoot) + if err != nil { + return err + } + + // The chosen child is: + // a. A reachability tree child of `commonAncestor` + // b. A reachability tree ancestor of `reindexRoot` + commonAncestorChosenChild, err := rt.findAncestorOfThisAmongChildrenOfOther(reindexRoot, commonAncestor) + if err != nil { + return err + } + + nodeInterval, err := rt.interval(node) + if err != nil { + return err + } + + commonAncestorChosenChildInterval, err := rt.interval(commonAncestorChosenChild) + if err != nil { + return err + } + + if nodeInterval.End < commonAncestorChosenChildInterval.Start { + // node is in the subtree before the chosen child + return rt.reclaimIntervalBeforeChosenChild(node, commonAncestor, + commonAncestorChosenChild, reindexRoot) + } + + // node is either: + // * in the subtree after the chosen child + // * the common ancestor + // In both cases we reclaim from the "after" subtree. In the + // latter case this is arbitrary + return rt.reclaimIntervalAfterChosenChild(node, commonAncestor, + commonAncestorChosenChild, reindexRoot) +} + +func (rt *reachabilityManager) reclaimIntervalBeforeChosenChild(rtn, commonAncestor, commonAncestorChosenChild, + reindexRoot *externalapi.DomainHash) error { + + current := commonAncestorChosenChild + + commonAncestorChosenChildHasSlackIntervalBefore, err := rt.hasSlackIntervalBefore(commonAncestorChosenChild) + if err != nil { + return err + } + + if !commonAncestorChosenChildHasSlackIntervalBefore { + // The common ancestor ran out of slack before its chosen child. + // Climb up the reachability tree toward the reindex root until + // we find a node that has enough slack. + for { + currentHasSlackIntervalBefore, err := rt.hasSlackIntervalBefore(current) + if err != nil { + return err + } + + if currentHasSlackIntervalBefore || *current == *reindexRoot { + break + } + + current, err = rt.findAncestorOfThisAmongChildrenOfOther(reindexRoot, current) + if err != nil { + return err + } + } + + if current == reindexRoot { + // "Deallocate" an interval of slackReachabilityIntervalForReclaiming + // from this node. This is the interval that we'll use for the new + // node. + originalInterval, err := rt.interval(current) + if err != nil { + return err + } + + err = rt.stageInterval(current, newReachabilityInterval( + originalInterval.Start+slackReachabilityIntervalForReclaiming, + originalInterval.End, + )) + if err != nil { + return err + } + + err = rt.countSubtreesAndPropagateInterval(current) + if err != nil { + return err + } + + err = rt.stageInterval(current, originalInterval) + if err != nil { + return err + } + } + } + + // Go down the reachability tree towards the common ancestor. + // On every hop we reindex the reachability subtree before the + // current node with an interval that is smaller by + // slackReachabilityIntervalForReclaiming. This is to make room + // for the new node. + for current != commonAncestor { + currentInterval, err := rt.interval(current) + if err != nil { + return err + } + + err = rt.stageInterval(current, newReachabilityInterval( + currentInterval.Start+slackReachabilityIntervalForReclaiming, + currentInterval.End, + )) + if err != nil { + return err + } + + currentParent, err := rt.parent(current) + if err != nil { + return err + } + + err = rt.reindexIntervalsBeforeNode(currentParent, current) + if err != nil { + return err + } + current, err = rt.parent(current) + if err != nil { + return err + } + } + + return nil +} + +// reindexIntervalsBeforeNode applies a tight interval to the reachability +// subtree before `node`. Note that `node` itself is unaffected. +func (rt *reachabilityManager) reindexIntervalsBeforeNode(rtn, node *externalapi.DomainHash) error { + + childrenBeforeNode, _, err := rt.splitChildrenAroundChild(rtn, node) + if err != nil { + return err + } + + childrenBeforeNodeSizes, childrenBeforeNodeSubtreeSizeMaps, childrenBeforeNodeSizesSum := + rt.calcReachabilityTreeNodeSizes(childrenBeforeNode) + + // Apply a tight interval + nodeInterval, err := rt.interval(node) + if err != nil { + return err + } + + newIntervalEnd := nodeInterval.Start - 1 + newInterval := newReachabilityInterval(newIntervalEnd-childrenBeforeNodeSizesSum+1, newIntervalEnd) + intervals, err := intervalSplitExact(newInterval, childrenBeforeNodeSizes) + if err != nil { + return err + } + return rt.propagateIntervals(childrenBeforeNode, intervals, childrenBeforeNodeSubtreeSizeMaps) +} + +func (rt *reachabilityManager) reclaimIntervalAfterChosenChild(node, commonAncestor, commonAncestorChosenChild, + reindexRoot *externalapi.DomainHash) error { + + current := commonAncestorChosenChild + commonAncestorChosenChildHasSlackIntervalAfter, err := rt.hasSlackIntervalAfter(commonAncestorChosenChild) + if err != nil { + return err + } + + if !commonAncestorChosenChildHasSlackIntervalAfter { + // The common ancestor ran out of slack after its chosen child. + // Climb up the reachability tree toward the reindex root until + // we find a node that has enough slack. + for { + currentHasSlackIntervalAfter, err := rt.hasSlackIntervalAfter(commonAncestorChosenChild) + if err != nil { + return err + } + + if currentHasSlackIntervalAfter || *current == *reindexRoot { + break + } + + current, err = rt.findAncestorOfThisAmongChildrenOfOther(reindexRoot, current) + if err != nil { + return err + } + } + + if current == reindexRoot { + // "Deallocate" an interval of slackReachabilityIntervalForReclaiming + // from this node. This is the interval that we'll use for the new + // node. + originalInterval, err := rt.interval(current) + if err != nil { + return err + } + + err = rt.stageInterval(current, newReachabilityInterval( + originalInterval.Start, + originalInterval.End-slackReachabilityIntervalForReclaiming, + )) + if err != nil { + return err + } + + err = rt.countSubtreesAndPropagateInterval(current) + if err != nil { + return err + } + + err = rt.stageInterval(current, originalInterval) + if err != nil { + return err + } + } + } + + // Go down the reachability tree towards the common ancestor. + // On every hop we reindex the reachability subtree after the + // current node with an interval that is smaller by + // slackReachabilityIntervalForReclaiming. This is to make room + // for the new node. + for current != commonAncestor { + currentInterval, err := rt.interval(current) + if err != nil { + return err + } + + err = rt.stageInterval(current, newReachabilityInterval( + currentInterval.Start, + currentInterval.End-slackReachabilityIntervalForReclaiming, + )) + + currentParent, err := rt.parent(current) + if err != nil { + return err + } + + err = rt.reindexIntervalsAfterNode(currentParent, current) + if err != nil { + return err + } + current = currentParent + } + + return nil +} + +// reindexIntervalsAfterNode applies a tight interval to the reachability +// subtree after `node`. Note that `node` itself is unaffected. +func (rt *reachabilityManager) reindexIntervalsAfterNode(rtn, node *externalapi.DomainHash) error { + + _, childrenAfterNode, err := rt.splitChildrenAroundChild(rtn, node) + if err != nil { + return err + } + + childrenAfterNodeSizes, childrenAfterNodeSubtreeSizeMaps, childrenAfterNodeSizesSum := + rt.calcReachabilityTreeNodeSizes(childrenAfterNode) + + // Apply a tight interval + nodeInterval, err := rt.interval(node) + if err != nil { + return err + } + + newIntervalStart := nodeInterval.End + 1 + newInterval := newReachabilityInterval(newIntervalStart, newIntervalStart+childrenAfterNodeSizesSum-1) + intervals, err := intervalSplitExact(newInterval, childrenAfterNodeSizes) + if err != nil { + return err + } + return rt.propagateIntervals(childrenAfterNode, intervals, childrenAfterNodeSubtreeSizeMaps) +} + +// IsReachabilityTreeAncestorOf checks if this node is a reachability tree ancestor +// of the other node. Note that we use the graph theory convention +// here which defines that node is also an ancestor of itself. +func (rt *reachabilityManager) IsReachabilityTreeAncestorOf(node, other *externalapi.DomainHash) (bool, error) { + nodeInterval, err := rt.interval(node) + if err != nil { + return false, err + } + + otherInterval, err := rt.interval(other) + if err != nil { + return false, err + } + + return intervalContains(nodeInterval, otherInterval), nil +} + +// findCommonAncestorWithReindexRoot finds the most recent reachability +// tree ancestor common to both node and the given reindex root. Note +// that we assume that almost always the chain between the reindex root +// and the common ancestor is longer than the chain between node and the +// common ancestor. +func (rt *reachabilityManager) findCommonAncestorWithReindexRoot(node, reindexRoot *externalapi.DomainHash) (*externalapi.DomainHash, error) { + currentThis := node + for { + isAncestorOf, err := rt.IsReachabilityTreeAncestorOf(currentThis, reindexRoot) + if err != nil { + return nil, err + } + + if isAncestorOf { + return currentThis, nil + } + + currentThis, err = rt.parent(currentThis) + if err != nil { + return nil, err + } + } +} + +// String returns a string representation of a reachability tree node +// and its children. +func (rt *reachabilityManager) String(node *externalapi.DomainHash) (string, error) { + queue := []*externalapi.DomainHash{node} + nodeInterval, err := rt.interval(node) + if err != nil { + return "", err + } + + lines := []string{intervalString(nodeInterval)} + for len(queue) > 0 { + var current *externalapi.DomainHash + current, queue = queue[0], queue[1:] + currentChildren, err := rt.children(current) + if err != nil { + return "", err + } + + if len(currentChildren) == 0 { + continue + } + + line := "" + for _, child := range currentChildren { + childInterval, err := rt.interval(child) + if err != nil { + return "", err + } + + line += intervalString(childInterval) + queue = append(queue, child) + } + lines = append([]string{line}, lines...) + } + return strings.Join(lines, "\n"), nil +} + +func (rt *reachabilityManager) updateReindexRoot(newTreeNode *externalapi.DomainHash) error { + + nextReindexRoot, err := rt.reindexRoot() + if err != nil { + return err + } + + for { + candidateReindexRoot, found, err := rt.maybeMoveReindexRoot(nextReindexRoot, newTreeNode) + if err != nil { + return err + } + if !found { + break + } + nextReindexRoot = candidateReindexRoot + } + + rt.stageReindexRoot(nextReindexRoot) + return nil +} + +func (rt *reachabilityManager) maybeMoveReindexRoot(reindexRoot, newTreeNode *externalapi.DomainHash) ( + newReindexRoot *externalapi.DomainHash, found bool, err error) { + + isAncestorOf, err := rt.IsReachabilityTreeAncestorOf(reindexRoot, newTreeNode) + if err != nil { + return nil, false, err + } + if !isAncestorOf { + commonAncestor, err := rt.findCommonAncestorWithReindexRoot(newTreeNode, reindexRoot) + if err != nil { + return nil, false, err + } + + return commonAncestor, true, nil + } + + reindexRootChosenChild, err := rt.findAncestorOfThisAmongChildrenOfOther(newTreeNode, reindexRoot) + if err != nil { + return nil, false, err + } + + newTreeNodeGHOSTDAGData, err := rt.ghostdagDataStore.Get(rt.databaseContext, newTreeNode) + if err != nil { + return nil, false, err + } + + reindexRootChosenChildGHOSTDAGData, err := rt.ghostdagDataStore.Get(rt.databaseContext, reindexRootChosenChild) + if err != nil { + return nil, false, err + } + + if newTreeNodeGHOSTDAGData.BlueScore-reindexRootChosenChildGHOSTDAGData.BlueScore < reachabilityReindexWindow { + return nil, false, nil + } + + err = rt.concentrateIntervalAroundReindexRootChosenChild(reindexRoot, reindexRootChosenChild) + if err != nil { + return nil, false, err + } + + return reindexRootChosenChild, true, nil +} + +// findAncestorOfThisAmongChildrenOfOther finds the reachability tree child +// of node that is the ancestor of node. +func (rt *reachabilityManager) findAncestorOfThisAmongChildrenOfOther(this, other *externalapi.DomainHash) (*externalapi.DomainHash, error) { + otherChildren, err := rt.children(other) + if err != nil { + return nil, err + } + + ancestor, ok := rt.findAncestorOfNode(otherChildren, this) + if !ok { + return nil, errors.Errorf("node is not an ancestor of node") + } + + return ancestor, nil +} + +func (rt *reachabilityManager) concentrateIntervalAroundReindexRootChosenChild(reindexRoot, + reindexRootChosenChild *externalapi.DomainHash) error { + + reindexRootChildNodesBeforeChosen, reindexRootChildNodesAfterChosen, err := + rt.splitChildrenAroundChild(reindexRoot, reindexRootChosenChild) + if err != nil { + return err + } + + reindexRootChildNodesBeforeChosenSizesSum, err := + rt.tightenIntervalsBeforeReindexRootChosenChild(reindexRoot, reindexRootChildNodesBeforeChosen) + if err != nil { + return err + } + + reindexRootChildNodesAfterChosenSizesSum, err := + rt.tightenIntervalsAfterReindexRootChosenChild(reindexRoot, reindexRootChildNodesAfterChosen) + if err != nil { + return err + } + + err = rt.expandIntervalInReindexRootChosenChild(reindexRoot, reindexRootChosenChild, + reindexRootChildNodesBeforeChosenSizesSum, reindexRootChildNodesAfterChosenSizesSum) + if err != nil { + return err + } + + return nil +} + +// splitChildrenAroundChild splits `node` into two slices: the nodes that are before +// `child` and the nodes that are after. +func (rt *reachabilityManager) splitChildrenAroundChild(node, child *externalapi.DomainHash) ( + nodesBeforeChild, nodesAfterChild []*externalapi.DomainHash, err error) { + + nodeChildren, err := rt.children(node) + if err != nil { + return nil, nil, err + } + + for i, candidateChild := range nodeChildren { + if candidateChild == child { + return nodeChildren[:i], nodeChildren[i+1:], nil + } + } + return nil, nil, errors.Errorf("child not a child of node") +} + +func (rt *reachabilityManager) tightenIntervalsBeforeReindexRootChosenChild( + reindexRoot *externalapi.DomainHash, + reindexRootChildNodesBeforeChosen []*externalapi.DomainHash) (reindexRootChildNodesBeforeChosenSizesSum uint64, + err error) { + + reindexRootChildNodesBeforeChosenSizes, reindexRootChildNodesBeforeChosenSubtreeSizeMaps, reindexRootChildNodesBeforeChosenSizesSum := + rt.calcReachabilityTreeNodeSizes(reindexRootChildNodesBeforeChosen) + + reindexRootInterval, err := rt.interval(reindexRoot) + if err != nil { + return 0, err + } + + intervalBeforeReindexRootStart := newReachabilityInterval( + reindexRootInterval.Start+reachabilityReindexSlack, + reindexRootInterval.Start+reachabilityReindexSlack+reindexRootChildNodesBeforeChosenSizesSum-1, + ) + + err = rt.propagateChildIntervals(intervalBeforeReindexRootStart, reindexRootChildNodesBeforeChosen, + reindexRootChildNodesBeforeChosenSizes, reindexRootChildNodesBeforeChosenSubtreeSizeMaps) + if err != nil { + return 0, err + } + return reindexRootChildNodesBeforeChosenSizesSum, nil +} + +func (rt *reachabilityManager) tightenIntervalsAfterReindexRootChosenChild( + reindexRoot *externalapi.DomainHash, + reindexRootChildNodesAfterChosen []*externalapi.DomainHash) (reindexRootChildNodesAfterChosenSizesSum uint64, + err error) { + + reindexRootChildNodesAfterChosenSizes, reindexRootChildNodesAfterChosenSubtreeSizeMaps, + reindexRootChildNodesAfterChosenSizesSum := + rt.calcReachabilityTreeNodeSizes(reindexRootChildNodesAfterChosen) + + reindexRootInterval, err := rt.interval(reindexRoot) + if err != nil { + return 0, err + } + + intervalAfterReindexRootEnd := newReachabilityInterval( + reindexRootInterval.End-reachabilityReindexSlack-reindexRootChildNodesAfterChosenSizesSum, + reindexRootInterval.End-reachabilityReindexSlack-1, + ) + + err = rt.propagateChildIntervals(intervalAfterReindexRootEnd, reindexRootChildNodesAfterChosen, + reindexRootChildNodesAfterChosenSizes, reindexRootChildNodesAfterChosenSubtreeSizeMaps) + if err != nil { + return 0, err + } + return reindexRootChildNodesAfterChosenSizesSum, nil +} + +func (rt *reachabilityManager) expandIntervalInReindexRootChosenChild(reindexRoot, + reindexRootChosenChild *externalapi.DomainHash, reindexRootChildNodesBeforeChosenSizesSum uint64, + reindexRootChildNodesAfterChosenSizesSum uint64) error { + + reindexRootInterval, err := rt.interval(reindexRoot) + if err != nil { + return err + } + + newReindexRootChildInterval := newReachabilityInterval( + reindexRootInterval.Start+reindexRootChildNodesBeforeChosenSizesSum+reachabilityReindexSlack, + reindexRootInterval.End-reindexRootChildNodesAfterChosenSizesSum-reachabilityReindexSlack-1, + ) + + reindexRootChosenChildInterval, err := rt.interval(reindexRootChosenChild) + if err != nil { + return err + } + + if !intervalContains(newReindexRootChildInterval, reindexRootChosenChildInterval) { + // New interval doesn't contain the previous one, propagation is required + + // We assign slack on both sides as an optimization. Were we to + // assign a tight interval, the next time the reindex root moves we + // would need to propagate intervals again. That is to say, When we + // DO allocate slack, next time + // expandIntervalInReindexRootChosenChild is called (next time the + // reindex root moves), newReindexRootChildInterval is likely to + // contain reindexRootChosenChild.Interval. + err := rt.stageInterval(reindexRootChosenChild, newReachabilityInterval( + newReindexRootChildInterval.Start+reachabilityReindexSlack, + newReindexRootChildInterval.End-reachabilityReindexSlack, + )) + if err != nil { + return err + } + + err = rt.countSubtreesAndPropagateInterval(reindexRootChosenChild) + if err != nil { + return err + } + } + + err = rt.stageInterval(reindexRootChosenChild, newReindexRootChildInterval) + if err != nil { + return err + } + return nil +} + +func (rt *reachabilityManager) countSubtreesAndPropagateInterval(node *externalapi.DomainHash) error { + subtreeSizeMap := make(map[externalapi.DomainHash]uint64) + err := rt.countSubtrees(node, subtreeSizeMap) + if err != nil { + return err + } + + return rt.propagateInterval(node, subtreeSizeMap) +} + +func (rt *reachabilityManager) calcReachabilityTreeNodeSizes(treeNodes []*externalapi.DomainHash) ( + sizes []uint64, subtreeSizeMaps []map[externalapi.DomainHash]uint64, sum uint64) { + + sizes = make([]uint64, len(treeNodes)) + subtreeSizeMaps = make([]map[externalapi.DomainHash]uint64, len(treeNodes)) + sum = 0 + for i, node := range treeNodes { + subtreeSizeMap := make(map[externalapi.DomainHash]uint64) + err := rt.countSubtrees(node, subtreeSizeMap) + if err != nil { + return nil, nil, 0 + } + + subtreeSize := subtreeSizeMap[*node] + sizes[i] = subtreeSize + subtreeSizeMaps[i] = subtreeSizeMap + sum += subtreeSize + } + return sizes, subtreeSizeMaps, sum +} + +func (rt *reachabilityManager) propagateChildIntervals(interval *model.ReachabilityInterval, + childNodes []*externalapi.DomainHash, sizes []uint64, subtreeSizeMaps []map[externalapi.DomainHash]uint64) error { + + childIntervalSizes, err := intervalSplitExact(interval, sizes) + if err != nil { + return err + } + + for i, child := range childNodes { + childInterval := childIntervalSizes[i] + err := rt.stageInterval(child, childInterval) + if err != nil { + return err + } + + childSubtreeSizeMap := subtreeSizeMaps[i] + err = rt.propagateInterval(child, childSubtreeSizeMap) + if err != nil { + return err + } + } + + return nil +} diff --git a/domain/consensus/processes/reachabilitytree/reachabilitytree.go b/domain/consensus/processes/reachabilitytree/reachabilitytree.go deleted file mode 100644 index 07673caba..000000000 --- a/domain/consensus/processes/reachabilitytree/reachabilitytree.go +++ /dev/null @@ -1,41 +0,0 @@ -package reachabilitytree - -import ( - "github.com/kaspanet/kaspad/domain/consensus/model" - "github.com/kaspanet/kaspad/domain/consensus/model/externalapi" -) - -// reachabilityTree maintains a structure that allows to answer -// reachability queries in sub-linear time -type reachabilityTree struct { - blockRelationStore model.BlockRelationStore - reachabilityDataStore model.ReachabilityDataStore -} - -// New instantiates a new ReachabilityTree -func New( - blockRelationStore model.BlockRelationStore, - reachabilityDataStore model.ReachabilityDataStore) model.ReachabilityTree { - return &reachabilityTree{ - blockRelationStore: blockRelationStore, - reachabilityDataStore: reachabilityDataStore, - } -} - -// AddBlock adds the block with the given blockHash into the reachability tree. -func (rt *reachabilityTree) AddBlock(blockHash *externalapi.DomainHash) error { - return nil -} - -// IsReachabilityTreeAncestorOf returns true if blockHashA is an -// ancestor of blockHashB in the reachability tree. Note that this -// does not necessarily mean that it isn't its ancestor in the DAG. -func (rt *reachabilityTree) IsReachabilityTreeAncestorOf(blockHashA *externalapi.DomainHash, blockHashB *externalapi.DomainHash) (bool, error) { - return false, nil -} - -// IsDAGAncestorOf returns true if blockHashA is an ancestor of -// blockHashB in the DAG. -func (rt *reachabilityTree) IsDAGAncestorOf(blockHashA *externalapi.DomainHash, blockHashB *externalapi.DomainHash) (bool, error) { - return false, nil -} diff --git a/infrastructure/logger/logger.go b/infrastructure/logger/logger.go index 71887d3d7..58ae577d7 100644 --- a/infrastructure/logger/logger.go +++ b/infrastructure/logger/logger.go @@ -54,6 +54,7 @@ var ( snvrLog = BackendLog.Logger("SNVR") ibdsLog = BackendLog.Logger("IBDS") wsvcLog = BackendLog.Logger("WSVC") + reacLog = BackendLog.Logger("REAC") ) // SubsystemTags is an enum of all sub system tags @@ -85,7 +86,8 @@ var SubsystemTags = struct { DNSS, SNVR, IBDS, - WSVC string + WSVC, + REAC string }{ ADXR: "ADXR", AMGR: "AMGR", @@ -115,6 +117,7 @@ var SubsystemTags = struct { SNVR: "SNVR", IBDS: "IBDS", WSVC: "WSVC", + REAC: "REAC", } // subsystemLoggers maps each subsystem identifier to its associated logger. @@ -147,6 +150,7 @@ var subsystemLoggers = map[string]*Logger{ SubsystemTags.SNVR: snvrLog, SubsystemTags.IBDS: ibdsLog, SubsystemTags.WSVC: wsvcLog, + SubsystemTags.REAC: reacLog, } // InitLog attaches log file and error log file to the backend log.