mirror of
https://github.com/kaspanet/kaspad.git
synced 2025-05-22 14:56:44 +00:00

* Mine JSON * [Reindex tests] add test_params and validate_mining flag to test_consensus * Rename file and extend tests * Ignore local test datasets * Use spaces over tabs * Reindex algorithm - full algorithm, initial commit, some tests fail * Reindex algorithm - a few critical fixes * Reindex algorithm - move reindex struct and all related operations to new file * Reindex algorithm - added a validateIntervals method and modified tests to use it (instead of exact comparisons) * Reindex algorithm - modified reindexIntervals to receive the new child as argument and fixed an important related bug * Reindex attack tests - move logic to helper function and add stretch test * Reindex algorithm - variable names and some comments * Reindex algorithm - minor changes * Reindex algorithm - minor changes 2 * Reindex algorithm - extended stretch test * Reindex algorithm - small fix to validate function * Reindex tests - move tests and add DAG files * go format fixes * TestParams doc comment * Reindex tests - exact comparisons are not needed * Update to version 0.8.6 * Remove TestParams and use AddUTXOInvalidHeader instead * Use gzipeed test files * This unintended change somehow slipped in through branch merges * Rename test * Move interval increase/decrease methods to reachability interval file * Addressing a bunch of minor review comments * Addressed a few more minor review comments * Make code of offsetSiblingsBefore and offsetSiblingsAfter symmetric * Optimize reindex logic in cases where reorg occurs + reorg test * Do not change reindex root too fast (on reorg) * Some comments * A few more comments * Addressing review comments * Remove TestNoAttackAlternateReorg and assert chain attack * Minor Co-authored-by: Elichai Turkel <elichai.turkel@gmail.com> Co-authored-by: Mike Zak <feanorr@gmail.com> Co-authored-by: Ori Newman <orinewman1@gmail.com>
522 lines
14 KiB
Go
522 lines
14 KiB
Go
package reachabilitymanager
|
|
|
|
import (
|
|
"math"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/kaspanet/kaspad/domain/consensus/utils/reachabilitydata"
|
|
|
|
"github.com/kaspanet/kaspad/domain/consensus/model"
|
|
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
|
|
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
func newReachabilityTreeData() model.ReachabilityData {
|
|
// Please see the comment above model.ReachabilityTreeNode to understand why
|
|
// we use these initial values.
|
|
interval := newReachabilityInterval(1, math.MaxUint64-1)
|
|
data := reachabilitydata.EmptyReachabilityData()
|
|
data.SetInterval(interval)
|
|
|
|
return data
|
|
}
|
|
|
|
/*
|
|
|
|
Interval helper functions
|
|
|
|
*/
|
|
|
|
func (rt *reachabilityManager) intervalRangeForChildAllocation(node *externalapi.DomainHash) (*model.ReachabilityInterval, error) {
|
|
interval, err := rt.interval(node)
|
|
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) {
|
|
childrenRange, 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 childrenRange, nil
|
|
}
|
|
|
|
firstChildInterval, err := rt.interval(children[0])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return newReachabilityInterval(childrenRange.Start, firstChildInterval.Start-1), nil
|
|
}
|
|
|
|
func (rt *reachabilityManager) remainingIntervalAfter(node *externalapi.DomainHash) (*model.ReachabilityInterval, error) {
|
|
childrenRange, 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 childrenRange, nil
|
|
}
|
|
|
|
lastChildInterval, err := rt.interval(children[len(children)-1])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return newReachabilityInterval(lastChildInterval.End+1, childrenRange.End), nil
|
|
}
|
|
|
|
func (rt *reachabilityManager) remainingSlackBefore(node *externalapi.DomainHash) (uint64, error) {
|
|
interval, err := rt.remainingIntervalBefore(node)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return intervalSize(interval), nil
|
|
}
|
|
|
|
func (rt *reachabilityManager) remainingSlackAfter(node *externalapi.DomainHash) (uint64, error) {
|
|
interval, err := rt.remainingIntervalAfter(node)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return intervalSize(interval), 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
|
|
}
|
|
|
|
/*
|
|
|
|
ReachabilityManager API functions
|
|
|
|
*/
|
|
|
|
// 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
|
|
}
|
|
|
|
// FindNextAncestor finds the reachability tree child
|
|
// of 'ancestor' which is also an ancestor of 'descendant'.
|
|
func (rt *reachabilityManager) FindNextAncestor(descendant, ancestor *externalapi.DomainHash) (*externalapi.DomainHash, error) {
|
|
childrenOfAncestor, err := rt.children(ancestor)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
nextAncestor, ok := rt.findAncestorOfNode(childrenOfAncestor, descendant)
|
|
if !ok {
|
|
return nil, errors.Errorf("ancestor is not an ancestor of descendant")
|
|
}
|
|
|
|
return nextAncestor, nil
|
|
}
|
|
|
|
// 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{nodeInterval.String()}
|
|
for len(queue) > 0 {
|
|
var current *externalapi.DomainHash
|
|
current, queue = queue[0], queue[1:]
|
|
children, err := rt.children(current)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if len(children) == 0 {
|
|
continue
|
|
}
|
|
|
|
line := ""
|
|
for _, child := range children {
|
|
childInterval, err := rt.interval(child)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
line += childInterval.String()
|
|
queue = append(queue, child)
|
|
}
|
|
lines = append([]string{line}, lines...)
|
|
}
|
|
return strings.Join(lines, "\n"), nil
|
|
}
|
|
|
|
/*
|
|
|
|
Tree helper functions
|
|
|
|
*/
|
|
|
|
func (rt *reachabilityManager) isStrictAncestorOf(node, other *externalapi.DomainHash) (bool, error) {
|
|
if node.Equal(other) {
|
|
return false, nil
|
|
}
|
|
return rt.IsReachabilityTreeAncestorOf(node, other)
|
|
}
|
|
|
|
// findCommonAncestor 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) findCommonAncestor(node, root *externalapi.DomainHash) (*externalapi.DomainHash, error) {
|
|
current := node
|
|
for {
|
|
isAncestorOf, err := rt.IsReachabilityTreeAncestorOf(current, root)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if isAncestorOf {
|
|
return current, nil
|
|
}
|
|
|
|
current, err = rt.parent(current)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
// splitChildren splits `node` children into two slices: the nodes that are before
|
|
// `pivot` and the nodes that are after.
|
|
func (rt *reachabilityManager) splitChildren(node, pivot *externalapi.DomainHash) (
|
|
nodesBeforePivot, nodesAfterPivot []*externalapi.DomainHash, err error) {
|
|
|
|
children, err := rt.children(node)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
for i, child := range children {
|
|
if child.Equal(pivot) {
|
|
return children[:i], children[i+1:], nil
|
|
}
|
|
}
|
|
return nil, nil, errors.Errorf("pivot not a pivot of node")
|
|
}
|
|
|
|
/*
|
|
|
|
Internal reachabilityManager API
|
|
|
|
*/
|
|
|
|
// addChild adds child to this tree node. If this node has no
|
|
// remaining interval to allocate, a reindexing is triggered. When a reindexing
|
|
// is triggered, the reindex root point is used within the
|
|
// reindex algorithm's logic
|
|
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.stageAddChild(node, child)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = rt.stageParent(child, node)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// No allocation space left at parent -- reindex
|
|
if intervalSize(remaining) == 0 {
|
|
|
|
// Initially set the child's interval to the empty remaining interval.
|
|
// This is done since in some cases, the underlying algorithm will
|
|
// allocate space around this point and call intervalIncreaseEnd or
|
|
// intervalDecreaseStart making for intervalSize > 0
|
|
err = rt.stageInterval(child, remaining)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
rc := newReindexContext(rt)
|
|
|
|
reindexStartTime := time.Now()
|
|
err := rc.reindexIntervals(child, reindexRoot)
|
|
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)
|
|
}
|
|
|
|
func (rt *reachabilityManager) updateReindexRoot(selectedTip *externalapi.DomainHash) error {
|
|
|
|
currentReindexRoot, err := rt.reindexRoot()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// First, find the new root
|
|
reindexRootAncestor, newReindexRoot, err := rt.findNextReindexRoot(currentReindexRoot, selectedTip)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// No update to root, return
|
|
if currentReindexRoot.Equal(newReindexRoot) {
|
|
return nil
|
|
}
|
|
|
|
rc := newReindexContext(rt)
|
|
|
|
// Iterate from reindexRootAncestor towards newReindexRoot
|
|
for {
|
|
chosenChild, err := rt.FindNextAncestor(selectedTip, reindexRootAncestor)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
isFinalReindexRoot := chosenChild.Equal(newReindexRoot)
|
|
|
|
// Concentrate interval from current ancestor to it's chosen child
|
|
err = rc.concentrateInterval(reindexRootAncestor, chosenChild, isFinalReindexRoot)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if isFinalReindexRoot {
|
|
break
|
|
}
|
|
|
|
reindexRootAncestor = chosenChild
|
|
}
|
|
|
|
// Update reindex root data store
|
|
rt.stageReindexRoot(newReindexRoot)
|
|
return nil
|
|
}
|
|
|
|
// findNextReindexRoot finds the new reindex root based on the current one and the new selected tip.
|
|
// The function also returns the common ancestor between the current and new reindex roots (possibly current root itself).
|
|
// This ancestor should be used as a starting point for concentrating the interval towards the new root.
|
|
func (rt *reachabilityManager) findNextReindexRoot(currentReindexRoot, selectedTip *externalapi.DomainHash) (
|
|
reindexRootAncestor, newReindexRoot *externalapi.DomainHash, err error) {
|
|
|
|
reindexRootAncestor = currentReindexRoot
|
|
newReindexRoot = currentReindexRoot
|
|
|
|
selectedTipGHOSTDAGData, err := rt.ghostdagDataStore.Get(rt.databaseContext, selectedTip)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
isCurrentAncestorOfTip, err := rt.IsReachabilityTreeAncestorOf(currentReindexRoot, selectedTip)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// Test if current root is ancestor of selected tip - if not, this is a reorg case
|
|
if !isCurrentAncestorOfTip {
|
|
currentRootGHOSTDAGData, err := rt.ghostdagDataStore.Get(rt.databaseContext, currentReindexRoot)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// We have reindex root out of selected tip chain, however we switch chains only after a sufficient
|
|
// threshold of reindexSlack score in order to address possible alternating reorg attacks.
|
|
// The reindexSlack constant is used as an heuristic for a large enough constant on the one hand, but
|
|
// one which will not harm performance on the other hand - given the available slack at the chain split point
|
|
if selectedTipGHOSTDAGData.BlueScore()-currentRootGHOSTDAGData.BlueScore() < rt.reindexSlack {
|
|
// Return current - this indicates no change
|
|
return currentReindexRoot, currentReindexRoot, nil
|
|
}
|
|
|
|
// The common ancestor is where we should start concentrating the interval from
|
|
commonAncestor, err := rt.findCommonAncestor(selectedTip, currentReindexRoot)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
reindexRootAncestor = commonAncestor
|
|
newReindexRoot = commonAncestor
|
|
}
|
|
|
|
// Iterate from ancestor towards selected tip until passing the reindexWindow threshold,
|
|
// for finding the new reindex root
|
|
for {
|
|
chosenChild, err := rt.FindNextAncestor(selectedTip, newReindexRoot)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
chosenChildGHOSTDAGData, err := rt.ghostdagDataStore.Get(rt.databaseContext, chosenChild)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
if selectedTipGHOSTDAGData.BlueScore()-chosenChildGHOSTDAGData.BlueScore() < rt.reindexWindow {
|
|
break
|
|
}
|
|
|
|
newReindexRoot = chosenChild
|
|
}
|
|
|
|
return reindexRootAncestor, newReindexRoot, nil
|
|
}
|
|
|
|
/*
|
|
|
|
Test helper functions
|
|
|
|
*/
|
|
|
|
// Helper function (for testing purposes) to validate that all tree intervals
|
|
// under a specified subtree root are allocated correctly and as expected
|
|
func (rt *reachabilityManager) validateIntervals(root *externalapi.DomainHash) error {
|
|
queue := []*externalapi.DomainHash{root}
|
|
for len(queue) > 0 {
|
|
var current *externalapi.DomainHash
|
|
current, queue = queue[0], queue[1:]
|
|
|
|
children, err := rt.children(current)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(children) > 0 {
|
|
queue = append(queue, children...)
|
|
}
|
|
|
|
currentInterval, err := rt.interval(current)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if currentInterval.Start > currentInterval.End {
|
|
err := errors.Errorf("Interval allocation is empty")
|
|
return err
|
|
}
|
|
|
|
for i, child := range children {
|
|
childInterval, err := rt.interval(child)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if i > 0 {
|
|
siblingInterval, err := rt.interval(children[i-1])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if siblingInterval.End+1 != childInterval.Start {
|
|
err := errors.Errorf("Child intervals are expected be right after each other")
|
|
return err
|
|
}
|
|
}
|
|
|
|
if childInterval.Start < currentInterval.Start {
|
|
err := errors.Errorf("Child interval to the left of parent")
|
|
return err
|
|
}
|
|
|
|
if childInterval.End >= currentInterval.End {
|
|
err := errors.Errorf("Child interval to the right of parent")
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Helper function (for testing purposes) to get all nodes under a specified subtree root
|
|
func (rt *reachabilityManager) getAllNodes(root *externalapi.DomainHash) ([]*externalapi.DomainHash, error) {
|
|
queue := []*externalapi.DomainHash{root}
|
|
nodes := []*externalapi.DomainHash{root}
|
|
for len(queue) > 0 {
|
|
var current *externalapi.DomainHash
|
|
current, queue = queue[0], queue[1:]
|
|
|
|
children, err := rt.children(current)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(children) > 0 {
|
|
queue = append(queue, children...)
|
|
nodes = append(nodes, children...)
|
|
}
|
|
}
|
|
|
|
return nodes, nil
|
|
}
|