diff --git a/blockchain/bench_test.go b/blockchain/bench_test.go index a937ca58..2992d361 100644 --- a/blockchain/bench_test.go +++ b/blockchain/bench_test.go @@ -29,3 +29,16 @@ func BenchmarkIsCoinBaseTx(b *testing.B) { IsCoinBaseTx(tx) } } + +func BenchmarkAncestor(b *testing.B) { + height := 1 << 19 + blockNodes := chainedNodes(nil, height) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + blockNodes[len(blockNodes)-1].Ancestor(0) + for j := 0; j <= 19; j++ { + blockNodes[len(blockNodes)-1].Ancestor(1 << j) + } + } +} diff --git a/blockchain/blockindex.go b/blockchain/blockindex.go index a0ed7603..b9e8704d 100644 --- a/blockchain/blockindex.go +++ b/blockchain/blockindex.go @@ -91,6 +91,9 @@ type blockNode struct { // parent is the parent block for this node. parent *blockNode + // ancestor is a block that is more than one block back from this node. + ancestor *blockNode + // hash is the double sha 256 of the block. hash chainhash.Hash @@ -137,6 +140,7 @@ func initBlockNode(node *blockNode, blockHeader *wire.BlockHeader, parent *block node.parent = parent node.height = parent.height + 1 node.workSum = node.workSum.Add(parent.workSum, node.workSum) + node.buildAncestor() } } @@ -168,6 +172,26 @@ func (node *blockNode) Header() wire.BlockHeader { } } +// invertLowestOne turns the lowest 1 bit in the binary representation of a number into a 0. +func invertLowestOne(n int32) int32 { + return n & (n - 1) +} + +// getAncestorHeight returns a suitable ancestor for the node at the given height. +func getAncestorHeight(height int32) int32 { + // We pop off two 1 bits of the height. + // This results in a maximum of 330 steps to go back to an ancestor + // from height 1<<29. + return invertLowestOne(invertLowestOne(height)) +} + +// buildAncestor sets an ancestor for the given blocknode. +func (node *blockNode) buildAncestor() { + if node.parent != nil { + node.ancestor = node.parent.Ancestor(getAncestorHeight(node.height)) + } +} + // Ancestor returns the ancestor block node at the provided height by following // the chain backwards from this node. The returned block will be nil when a // height is requested that is after the height of the passed node or is less @@ -179,9 +203,22 @@ func (node *blockNode) Ancestor(height int32) *blockNode { return nil } + // Traverse back until we find the desired node. n := node - for ; n != nil && n.height != height; n = n.parent { - // Intentionally left blank + for n != nil && n.height != height { + // If there's an ancestor available, use it. Otherwise, just + // follow the parent. + if n.ancestor != nil { + // Calculate the height for this ancestor and + // check if we can take the ancestor skip. + if getAncestorHeight(n.height) >= height { + n = n.ancestor + continue + } + } + + // We couldn't take the ancestor skip so traverse back to the parent. + n = n.parent } return n diff --git a/blockchain/blockindex_test.go b/blockchain/blockindex_test.go new file mode 100644 index 00000000..cd08969f --- /dev/null +++ b/blockchain/blockindex_test.go @@ -0,0 +1,42 @@ +// Copyright (c) 2023 The utreexo developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package blockchain + +import ( + "math/rand" + "testing" +) + +func TestAncestor(t *testing.T) { + height := 500_000 + blockNodes := chainedNodes(nil, height) + + for i, blockNode := range blockNodes { + // Grab a random node that's a child of this node + // and try to fetch the current blockNode with Ancestor. + randNode := blockNodes[rand.Intn(height-i)+i] + got := randNode.Ancestor(blockNode.height) + + // See if we got the right one. + if got.hash != blockNode.hash { + t.Fatalf("expected ancestor at height %d "+ + "but got a node at height %d", + blockNode.height, got.height) + } + + // Gensis doesn't have ancestors so skip the check below. + if blockNode.height == 0 { + continue + } + + // The ancestors are deterministic so check that this node's + // ancestor is the correct one. + if blockNode.ancestor.height != getAncestorHeight(blockNode.height) { + t.Fatalf("expected anestor at height %d, but it was at %d", + getAncestorHeight(blockNode.height), + blockNode.ancestor.height) + } + } +}