1a947c46b2
This exposes the ability to more efficiently create a block locator from a chain view for a given block node by using their ability to do O(1) lookups. It also adds tests to ensure the behavior is correct.
492 lines
16 KiB
Go
492 lines
16 KiB
Go
// Copyright (c) 2017 The btcsuite developers
|
|
// Use of this source code is governed by an ISC
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package blockchain
|
|
|
|
import (
|
|
"fmt"
|
|
"math/rand"
|
|
"reflect"
|
|
"testing"
|
|
|
|
"github.com/btcsuite/btcd/wire"
|
|
)
|
|
|
|
// testNoncePrng provides a deterministic prng for the nonce in generated fake
|
|
// nodes. The ensures that the node have unique hashes.
|
|
var testNoncePrng = rand.New(rand.NewSource(0))
|
|
|
|
// chainedNodes returns the specified number of nodes constructed such that each
|
|
// subsequent node points to the previous one to create a chain. The first node
|
|
// will point to the passed parent which can be nil if desired.
|
|
func chainedNodes(parent *blockNode, numNodes int) []*blockNode {
|
|
nodes := make([]*blockNode, numNodes)
|
|
tip := parent
|
|
for i := 0; i < numNodes; i++ {
|
|
// This is invalid, but all that is needed is enough to get the
|
|
// synthetic tests to work.
|
|
header := wire.BlockHeader{Nonce: testNoncePrng.Uint32()}
|
|
height := int32(0)
|
|
if tip != nil {
|
|
header.PrevBlock = tip.hash
|
|
height = tip.height + 1
|
|
}
|
|
node := newBlockNode(&header, height)
|
|
node.parent = tip
|
|
tip = node
|
|
|
|
nodes[i] = node
|
|
}
|
|
return nodes
|
|
}
|
|
|
|
// String returns the block node as a human-readable name.
|
|
func (node blockNode) String() string {
|
|
return fmt.Sprintf("%s(%d)", node.hash, node.height)
|
|
}
|
|
|
|
// tstTip is a convenience function to grab the tip of a chain of block nodes
|
|
// created via chainedNodes.
|
|
func tstTip(nodes []*blockNode) *blockNode {
|
|
return nodes[len(nodes)-1]
|
|
}
|
|
|
|
// locatorHashes is a convenience function that returns the hashes for all of
|
|
// the passed indexes of the provided nodes. It is used to construct expected
|
|
// block locators in the tests.
|
|
func locatorHashes(nodes []*blockNode, indexes ...int) BlockLocator {
|
|
hashes := make(BlockLocator, 0, len(indexes))
|
|
for _, idx := range indexes {
|
|
hashes = append(hashes, &nodes[idx].hash)
|
|
}
|
|
return hashes
|
|
}
|
|
|
|
// zipLocators is a convenience function that returns a single block locator
|
|
// given a variable number of them and is used in the tests.
|
|
func zipLocators(locators ...BlockLocator) BlockLocator {
|
|
var hashes BlockLocator
|
|
for _, locator := range locators {
|
|
hashes = append(hashes, locator...)
|
|
}
|
|
return hashes
|
|
}
|
|
|
|
// TestChainView ensures all of the exported functionality of chain views works
|
|
// as intended with the expection of some special cases which are handled in
|
|
// other tests.
|
|
func TestChainView(t *testing.T) {
|
|
// Construct a synthetic block index consisting of the following
|
|
// structure.
|
|
// 0 -> 1 -> 2 -> 3 -> 4
|
|
// \-> 2a -> 3a -> 4a -> 5a -> 6a -> 7a -> ... -> 26a
|
|
// \-> 3a'-> 4a' -> 5a'
|
|
branch0Nodes := chainedNodes(nil, 5)
|
|
branch1Nodes := chainedNodes(branch0Nodes[1], 25)
|
|
branch2Nodes := chainedNodes(branch1Nodes[0], 3)
|
|
|
|
tip := tstTip
|
|
tests := []struct {
|
|
name string
|
|
view *chainView // active view
|
|
genesis *blockNode // expected genesis block of active view
|
|
tip *blockNode // expected tip of active view
|
|
side *chainView // side chain view
|
|
sideTip *blockNode // expected tip of side chain view
|
|
fork *blockNode // expected fork node
|
|
contains []*blockNode // expected nodes in active view
|
|
noContains []*blockNode // expected nodes NOT in active view
|
|
equal *chainView // view expected equal to active view
|
|
unequal *chainView // view expected NOT equal to active
|
|
locator BlockLocator // expected locator for active view tip
|
|
}{
|
|
{
|
|
// Create a view for branch 0 as the active chain and
|
|
// another view for branch 1 as the side chain.
|
|
name: "chain0-chain1",
|
|
view: newChainView(tip(branch0Nodes)),
|
|
genesis: branch0Nodes[0],
|
|
tip: tip(branch0Nodes),
|
|
side: newChainView(tip(branch1Nodes)),
|
|
sideTip: tip(branch1Nodes),
|
|
fork: branch0Nodes[1],
|
|
contains: branch0Nodes,
|
|
noContains: branch1Nodes,
|
|
equal: newChainView(tip(branch0Nodes)),
|
|
unequal: newChainView(tip(branch1Nodes)),
|
|
locator: locatorHashes(branch0Nodes, 4, 3, 2, 1, 0),
|
|
},
|
|
{
|
|
// Create a view for branch 1 as the active chain and
|
|
// another view for branch 2 as the side chain.
|
|
name: "chain1-chain2",
|
|
view: newChainView(tip(branch1Nodes)),
|
|
genesis: branch0Nodes[0],
|
|
tip: tip(branch1Nodes),
|
|
side: newChainView(tip(branch2Nodes)),
|
|
sideTip: tip(branch2Nodes),
|
|
fork: branch1Nodes[0],
|
|
contains: branch1Nodes,
|
|
noContains: branch2Nodes,
|
|
equal: newChainView(tip(branch1Nodes)),
|
|
unequal: newChainView(tip(branch2Nodes)),
|
|
locator: zipLocators(
|
|
locatorHashes(branch1Nodes, 24, 23, 22, 21, 20,
|
|
19, 18, 17, 16, 15, 14, 13, 11, 7),
|
|
locatorHashes(branch0Nodes, 1, 0)),
|
|
},
|
|
{
|
|
// Create a view for branch 2 as the active chain and
|
|
// another view for branch 0 as the side chain.
|
|
name: "chain2-chain0",
|
|
view: newChainView(tip(branch2Nodes)),
|
|
genesis: branch0Nodes[0],
|
|
tip: tip(branch2Nodes),
|
|
side: newChainView(tip(branch0Nodes)),
|
|
sideTip: tip(branch0Nodes),
|
|
fork: branch0Nodes[1],
|
|
contains: branch2Nodes,
|
|
noContains: branch0Nodes[2:],
|
|
equal: newChainView(tip(branch2Nodes)),
|
|
unequal: newChainView(tip(branch0Nodes)),
|
|
locator: zipLocators(
|
|
locatorHashes(branch2Nodes, 2, 1, 0),
|
|
locatorHashes(branch1Nodes, 0),
|
|
locatorHashes(branch0Nodes, 1, 0)),
|
|
},
|
|
}
|
|
testLoop:
|
|
for _, test := range tests {
|
|
// Ensure the active and side chain heights are the expected
|
|
// values.
|
|
if test.view.Height() != test.tip.height {
|
|
t.Errorf("%s: unexpected active view height -- got "+
|
|
"%d, want %d", test.name, test.view.Height(),
|
|
test.tip.height)
|
|
continue
|
|
}
|
|
if test.side.Height() != test.sideTip.height {
|
|
t.Errorf("%s: unexpected side view height -- got %d, "+
|
|
"want %d", test.name, test.side.Height(),
|
|
test.sideTip.height)
|
|
continue
|
|
}
|
|
|
|
// Ensure the active and side chain genesis block is the
|
|
// expected value.
|
|
if test.view.Genesis() != test.genesis {
|
|
t.Errorf("%s: unexpected active view genesis -- got "+
|
|
"%v, want %v", test.name, test.view.Genesis(),
|
|
test.genesis)
|
|
continue
|
|
}
|
|
if test.side.Genesis() != test.genesis {
|
|
t.Errorf("%s: unexpected side view genesis -- got %v, "+
|
|
"want %v", test.name, test.view.Genesis(),
|
|
test.genesis)
|
|
continue
|
|
}
|
|
|
|
// Ensure the active and side chain tips are the expected nodes.
|
|
if test.view.Tip() != test.tip {
|
|
t.Errorf("%s: unexpected active view tip -- got %v, "+
|
|
"want %v", test.name, test.view.Tip(), test.tip)
|
|
continue
|
|
}
|
|
if test.side.Tip() != test.sideTip {
|
|
t.Errorf("%s: unexpected active view tip -- got %v, "+
|
|
"want %v", test.name, test.side.Tip(),
|
|
test.sideTip)
|
|
continue
|
|
}
|
|
|
|
// Ensure that regardless of the order the two chains are
|
|
// compared they both return the expected fork point.
|
|
forkNode := test.view.FindFork(test.side.Tip())
|
|
if forkNode != test.fork {
|
|
t.Errorf("%s: unexpected fork node (view, side) -- "+
|
|
"got %v, want %v", test.name, forkNode,
|
|
test.fork)
|
|
continue
|
|
}
|
|
forkNode = test.side.FindFork(test.view.Tip())
|
|
if forkNode != test.fork {
|
|
t.Errorf("%s: unexpected fork node (side, view) -- "+
|
|
"got %v, want %v", test.name, forkNode,
|
|
test.fork)
|
|
continue
|
|
}
|
|
|
|
// Ensure that the fork point for a node that is already part
|
|
// of the chain view is the node itself.
|
|
forkNode = test.view.FindFork(test.view.Tip())
|
|
if forkNode != test.view.Tip() {
|
|
t.Errorf("%s: unexpected fork node (view, tip) -- "+
|
|
"got %v, want %v", test.name, forkNode,
|
|
test.view.Tip())
|
|
continue
|
|
}
|
|
|
|
// Ensure all expected nodes are contained in the active view.
|
|
for _, node := range test.contains {
|
|
if !test.view.Contains(node) {
|
|
t.Errorf("%s: expected %v in active view",
|
|
test.name, node)
|
|
continue testLoop
|
|
}
|
|
}
|
|
|
|
// Ensure all nodes from side chain view are NOT contained in
|
|
// the active view.
|
|
for _, node := range test.noContains {
|
|
if test.view.Contains(node) {
|
|
t.Errorf("%s: unexpected %v in active view",
|
|
test.name, node)
|
|
continue testLoop
|
|
}
|
|
}
|
|
|
|
// Ensure equality of different views into the same chain works
|
|
// as intended.
|
|
if !test.view.Equals(test.equal) {
|
|
t.Errorf("%s: unexpected unequal views", test.name)
|
|
continue
|
|
}
|
|
if test.view.Equals(test.unequal) {
|
|
t.Errorf("%s: unexpected equal views", test.name)
|
|
continue
|
|
}
|
|
|
|
// Ensure all nodes contained in the view return the expected
|
|
// next node.
|
|
for i, node := range test.contains {
|
|
// Final node expects nil for the next node.
|
|
var expected *blockNode
|
|
if i < len(test.contains)-1 {
|
|
expected = test.contains[i+1]
|
|
}
|
|
if next := test.view.Next(node); next != expected {
|
|
t.Errorf("%s: unexpected next node -- got %v, "+
|
|
"want %v", test.name, next, expected)
|
|
continue testLoop
|
|
}
|
|
}
|
|
|
|
// Ensure nodes that are not contained in the view do not
|
|
// produce a successor node.
|
|
for _, node := range test.noContains {
|
|
if next := test.view.Next(node); next != nil {
|
|
t.Errorf("%s: unexpected next node -- got %v, "+
|
|
"want nil", test.name, next)
|
|
continue testLoop
|
|
}
|
|
}
|
|
|
|
// Ensure all nodes contained in the view can be retrieved by
|
|
// height.
|
|
for _, wantNode := range test.contains {
|
|
node := test.view.NodeByHeight(wantNode.height)
|
|
if node != wantNode {
|
|
t.Errorf("%s: unexpected node for height %d -- "+
|
|
"got %v, want %v", test.name,
|
|
wantNode.height, node, wantNode)
|
|
continue testLoop
|
|
}
|
|
}
|
|
|
|
// Ensure the block locator for the tip of the active view
|
|
// consists of the expected hashes.
|
|
locator := test.view.BlockLocator(test.view.tip())
|
|
if !reflect.DeepEqual(locator, test.locator) {
|
|
t.Errorf("%s: unexpected locator -- got %v, want %v",
|
|
test.name, locator, test.locator)
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestChainViewForkCorners ensures that finding the fork between two chains
|
|
// works in some corner cases such as when the two chains have completely
|
|
// unrelated histories.
|
|
func TestChainViewForkCorners(t *testing.T) {
|
|
// Construct two unrelated single branch synthetic block indexes.
|
|
branchNodes := chainedNodes(nil, 5)
|
|
unrelatedBranchNodes := chainedNodes(nil, 7)
|
|
|
|
// Create chain views for the two unrelated histories.
|
|
view1 := newChainView(tstTip(branchNodes))
|
|
view2 := newChainView(tstTip(unrelatedBranchNodes))
|
|
|
|
// Ensure attempting to find a fork point with a node that doesn't exist
|
|
// doesn't produce a node.
|
|
if fork := view1.FindFork(nil); fork != nil {
|
|
t.Fatalf("FindFork: unexpected fork -- got %v, want nil", fork)
|
|
}
|
|
|
|
// Ensure attempting to find a fork point in two chain views with
|
|
// totally unrelated histories doesn't produce a node.
|
|
for _, node := range branchNodes {
|
|
if fork := view2.FindFork(node); fork != nil {
|
|
t.Fatalf("FindFork: unexpected fork -- got %v, want nil",
|
|
fork)
|
|
}
|
|
}
|
|
for _, node := range unrelatedBranchNodes {
|
|
if fork := view1.FindFork(node); fork != nil {
|
|
t.Fatalf("FindFork: unexpected fork -- got %v, want nil",
|
|
fork)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestChainViewSetTip ensures changing the tip works as intended including
|
|
// capacity changes.
|
|
func TestChainViewSetTip(t *testing.T) {
|
|
// Construct a synthetic block index consisting of the following
|
|
// structure.
|
|
// 0 -> 1 -> 2 -> 3 -> 4
|
|
// \-> 2a -> 3a -> 4a -> 5a -> 6a -> 7a -> ... -> 26a
|
|
branch0Nodes := chainedNodes(nil, 5)
|
|
branch1Nodes := chainedNodes(branch0Nodes[1], 25)
|
|
|
|
tip := tstTip
|
|
tests := []struct {
|
|
name string
|
|
view *chainView // active view
|
|
tips []*blockNode // tips to set
|
|
contains [][]*blockNode // expected nodes in view for each tip
|
|
}{
|
|
{
|
|
// Create an empty view and set the tip to increasingly
|
|
// longer chains.
|
|
name: "increasing",
|
|
view: newChainView(nil),
|
|
tips: []*blockNode{tip(branch0Nodes), tip(branch1Nodes)},
|
|
contains: [][]*blockNode{branch0Nodes, branch1Nodes},
|
|
},
|
|
{
|
|
// Create a view with a longer chain and set the tip to
|
|
// increasingly shorter chains.
|
|
name: "decreasing",
|
|
view: newChainView(tip(branch1Nodes)),
|
|
tips: []*blockNode{tip(branch0Nodes), nil},
|
|
contains: [][]*blockNode{branch0Nodes, nil},
|
|
},
|
|
{
|
|
// Create a view with a shorter chain and set the tip to
|
|
// a longer chain followed by setting it back to the
|
|
// shorter chain.
|
|
name: "small-large-small",
|
|
view: newChainView(tip(branch0Nodes)),
|
|
tips: []*blockNode{tip(branch1Nodes), tip(branch0Nodes)},
|
|
contains: [][]*blockNode{branch1Nodes, branch0Nodes},
|
|
},
|
|
{
|
|
// Create a view with a longer chain and set the tip to
|
|
// a smaller chain followed by setting it back to the
|
|
// longer chain.
|
|
name: "large-small-large",
|
|
view: newChainView(tip(branch1Nodes)),
|
|
tips: []*blockNode{tip(branch0Nodes), tip(branch1Nodes)},
|
|
contains: [][]*blockNode{branch0Nodes, branch1Nodes},
|
|
},
|
|
}
|
|
|
|
testLoop:
|
|
for _, test := range tests {
|
|
for i, tip := range test.tips {
|
|
// Ensure the view tip is the expected node.
|
|
test.view.SetTip(tip)
|
|
if test.view.Tip() != tip {
|
|
t.Errorf("%s: unexpected view tip -- got %v, "+
|
|
"want %v", test.name, test.view.Tip(),
|
|
tip)
|
|
continue testLoop
|
|
}
|
|
|
|
// Ensure all expected nodes are contained in the view.
|
|
for _, node := range test.contains[i] {
|
|
if !test.view.Contains(node) {
|
|
t.Errorf("%s: expected %v in active view",
|
|
test.name, node)
|
|
continue testLoop
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestChainViewNil ensures that creating and accessing a nil chain view behaves
|
|
// as expected.
|
|
func TestChainViewNil(t *testing.T) {
|
|
// Ensure two unininitialized views are considered equal.
|
|
view := newChainView(nil)
|
|
if !view.Equals(newChainView(nil)) {
|
|
t.Fatal("uninitialized nil views unequal")
|
|
}
|
|
|
|
// Ensure the genesis of an uninitialized view does not produce a node.
|
|
if genesis := view.Genesis(); genesis != nil {
|
|
t.Fatalf("Genesis: unexpected genesis -- got %v, want nil",
|
|
genesis)
|
|
}
|
|
|
|
// Ensure the tip of an uninitialized view does not produce a node.
|
|
if tip := view.Tip(); tip != nil {
|
|
t.Fatalf("Tip: unexpected tip -- got %v, want nil", tip)
|
|
}
|
|
|
|
// Ensure the height of an uninitialized view is the expected value.
|
|
if height := view.Height(); height != -1 {
|
|
t.Fatalf("Height: unexpected height -- got %d, want -1", height)
|
|
}
|
|
|
|
// Ensure attempting to get a node for a height that does not exist does
|
|
// not produce a node.
|
|
if node := view.NodeByHeight(10); node != nil {
|
|
t.Fatalf("NodeByHeight: unexpected node -- got %v, want nil", node)
|
|
}
|
|
|
|
// Ensure an uninitialized view does not report it contains nodes.
|
|
fakeNode := chainedNodes(nil, 1)[0]
|
|
if view.Contains(fakeNode) {
|
|
t.Fatalf("Contains: view claims it contains node %v", fakeNode)
|
|
}
|
|
|
|
// Ensure the next node for a node that does not exist does not produce
|
|
// a node.
|
|
if next := view.Next(nil); next != nil {
|
|
t.Fatalf("Next: unexpected next node -- got %v, want nil", next)
|
|
}
|
|
|
|
// Ensure the next node for a node that exists does not produce a node.
|
|
if next := view.Next(fakeNode); next != nil {
|
|
t.Fatalf("Next: unexpected next node -- got %v, want nil", next)
|
|
}
|
|
|
|
// Ensure attempting to find a fork point with a node that doesn't exist
|
|
// doesn't produce a node.
|
|
if fork := view.FindFork(nil); fork != nil {
|
|
t.Fatalf("FindFork: unexpected fork -- got %v, want nil", fork)
|
|
}
|
|
|
|
// Ensure attempting to get a block locator for the tip doesn't produce
|
|
// one since the tip is nil.
|
|
if locator := view.BlockLocator(nil); locator != nil {
|
|
t.Fatalf("BlockLocator: unexpected locator -- got %v, want nil",
|
|
locator)
|
|
}
|
|
|
|
// Ensure attempting to get a block locator for a node that exists still
|
|
// works as intended.
|
|
branchNodes := chainedNodes(nil, 50)
|
|
wantLocator := locatorHashes(branchNodes, 49, 48, 47, 46, 45, 44, 43,
|
|
42, 41, 40, 39, 38, 36, 32, 24, 8, 0)
|
|
locator := view.BlockLocator(tstTip(branchNodes))
|
|
if !reflect.DeepEqual(locator, wantLocator) {
|
|
t.Fatalf("BlockLocator: unexpected locator -- got %v, want %v",
|
|
locator, wantLocator)
|
|
}
|
|
}
|