From 87653ae5b16c9722de29b0e974157aa518198c2e Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Fri, 6 Jul 2018 13:02:54 -0500 Subject: [PATCH] blockchain: Implement new chain view. This implements a new type in the blockchain package that takes advantage of the fact that all block nodes are now in memory to provide a flat view of a specific chain of blocks (a specific branch of the overall block tree) from a given tip all the way back to the genesis block along with several convenience functions such as efficiently comparing two views, quickly finding the fork point (if any) between two views, and O(1) lookup of the node at a specific height. The view is not currently used, but the intent is that the code will be refactored to make use of these views to simplify and optimize several areas such as best chain selection and reorg logic and finding successor nodes. They will also greatly simplify the process of disconnecting the download logic from the connection logic. Since the ultimate intent is for there to be a long-lived chain view instance for the current best chain, this also implements efficient handling of setting the tip to new values including domain-specific capacity increase handling which chooses the desired increase amount more intelligently than the default algorithm, which would way overshoot, as well as adds some additional space when the view is initialized. A comprehensive suite of tests is provided to ensure the chain views behave correctly. --- blockchain/blockindex_test.go | 5 +- blockchain/chainview.go | 327 ++++++++++++++++++++++++++++ blockchain/chainview_test.go | 398 ++++++++++++++++++++++++++++++++++ blockchain/common_test.go | 10 +- 4 files changed, 734 insertions(+), 6 deletions(-) create mode 100644 blockchain/chainview.go create mode 100644 blockchain/chainview_test.go diff --git a/blockchain/blockindex_test.go b/blockchain/blockindex_test.go index bbbbc4c196..57b080eafc 100644 --- a/blockchain/blockindex_test.go +++ b/blockchain/blockindex_test.go @@ -212,12 +212,9 @@ func TestChainTips(t *testing.T) { bc.index.RUnlock() // The expected chain tips are the tips of all of the branches. - tip := func(nodes []*blockNode) *blockNode { - return nodes[len(nodes)-1] - } expectedTips := make(map[*blockNode]struct{}) for _, branch := range branches { - expectedTips[tip(branch)] = struct{}{} + expectedTips[branchTip(branch)] = struct{}{} } // Ensure the chain tips are the expected values. diff --git a/blockchain/chainview.go b/blockchain/chainview.go new file mode 100644 index 0000000000..eb84044e79 --- /dev/null +++ b/blockchain/chainview.go @@ -0,0 +1,327 @@ +// Copyright (c) 2017 The btcsuite developers +// Copyright (c) 2018 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package blockchain + +import ( + "sync" +) + +// approxNodesPerWeek is an approximation of the number of new blocks there are +// in a week on average. +const approxNodesPerWeek = 12 * 24 * 7 + +// chainView provides a flat view of a specific branch of the block chain from +// its tip back to the genesis block and provides various convenience functions +// for comparing chains. +// +// For example, assume a block chain with a side chain as depicted below: +// genesis -> 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 +// \-> 4a -> 5a -> 6a +// +// The chain view for the branch ending in 6a consists of: +// genesis -> 1 -> 2 -> 3 -> 4a -> 5a -> 6a +type chainView struct { + mtx sync.Mutex + nodes []*blockNode +} + +// newChainView returns a new chain view for the given tip block node. Passing +// nil as the tip will result in a chain view that is not initialized. The tip +// can be updated at any time via the setTip function. +func newChainView(tip *blockNode) *chainView { + // The mutex is intentionally not held since this is a constructor. + var c chainView + c.setTip(tip) + return &c +} + +// genesis returns the genesis block for the chain view. This only differs from +// the exported version in that it is up to the caller to ensure the lock is +// held. +// +// This function MUST be called with the view mutex locked (for reads). +func (c *chainView) genesis() *blockNode { + if len(c.nodes) == 0 { + return nil + } + + return c.nodes[0] +} + +// Genesis returns the genesis block for the chain view. +// +// This function is safe for concurrent access. +func (c *chainView) Genesis() *blockNode { + c.mtx.Lock() + genesis := c.genesis() + c.mtx.Unlock() + return genesis +} + +// tip returns the current tip block node for the chain view. It will return +// nil if there is no tip. This only differs from the exported version in that +// it is up to the caller to ensure the lock is held. +// +// This function MUST be called with the view mutex locked (for reads). +func (c *chainView) tip() *blockNode { + if len(c.nodes) == 0 { + return nil + } + + return c.nodes[len(c.nodes)-1] +} + +// Tip returns the current tip block node for the chain view. It will return +// nil if there is no tip. +// +// This function is safe for concurrent access. +func (c *chainView) Tip() *blockNode { + c.mtx.Lock() + tip := c.tip() + c.mtx.Unlock() + return tip +} + +// setTip sets the chain view to use the provided block node as the current tip +// and ensures the view is consistent by populating it with the nodes obtained +// by walking backwards all the way to genesis block as necessary. Further +// calls will only perform the minimum work needed, so switching between chain +// tips is efficient. This only differs from the exported version in that it is +// up to the caller to ensure the lock is held. +// +// This function MUST be called with the view mutex locked (for writes). +func (c *chainView) setTip(node *blockNode) { + if node == nil { + // Keep the backing array around for potential future use. + c.nodes = c.nodes[:0] + return + } + + // Create or resize the slice that will hold the block nodes to the + // provided tip height. When creating the slice, it is created with + // some additional capacity for the underlying array as append would do + // in order to reduce overhead when extending the chain later. As long + // as the underlying array already has enough capacity, simply expand or + // contract the slice accordingly. The additional capacity is chosen + // such that the array should only have to be extended about once a + // week. + needed := node.height + 1 + if int64(cap(c.nodes)) < needed { + nodes := make([]*blockNode, needed, needed+approxNodesPerWeek) + copy(nodes, c.nodes) + c.nodes = nodes + } else { + prevLen := int64(len(c.nodes)) + c.nodes = c.nodes[0:needed] + for i := prevLen; i < needed; i++ { + c.nodes[i] = nil + } + } + + for node != nil && c.nodes[node.height] != node { + c.nodes[node.height] = node + node = node.parent + } +} + +// SetTip sets the chain view to use the provided block node as the current tip +// and ensures the view is consistent by populating it with the nodes obtained +// by walking backwards all the way to genesis block as necessary. Further +// calls will only perform the minimum work needed, so switching between chain +// tips is efficient. +// +// This function is safe for concurrent access. +func (c *chainView) SetTip(node *blockNode) { + c.mtx.Lock() + c.setTip(node) + c.mtx.Unlock() +} + +// height returns the height of the tip of the chain view. It will return -1 if +// there is no tip (which only happens if the chain view has not been +// initialized). This only differs from the exported version in that it is up +// to the caller to ensure the lock is held. +// +// This function MUST be called with the view mutex locked (for reads). +func (c *chainView) height() int64 { + return int64(len(c.nodes) - 1) +} + +// Height returns the height of the tip of the chain view. It will return -1 if +// there is no tip (which only happens if the chain view has not been +// initialized). +// +// This function is safe for concurrent access. +func (c *chainView) Height() int64 { + c.mtx.Lock() + height := c.height() + c.mtx.Unlock() + return height +} + +// nodeByHeight returns the block node at the specified height. Nil will be +// returned if the height does not exist. This only differs from the exported +// version in that it is up to the caller to ensure the lock is held. +// +// This function MUST be called with the view mutex locked (for reads). +func (c *chainView) nodeByHeight(height int64) *blockNode { + if height < 0 || height >= int64(len(c.nodes)) { + return nil + } + + return c.nodes[height] +} + +// NodeByHeight returns the block node at the specified height. Nil will be +// returned if the height does not exist. +// +// This function is safe for concurrent access. +func (c *chainView) NodeByHeight(height int64) *blockNode { + c.mtx.Lock() + node := c.nodeByHeight(height) + c.mtx.Unlock() + return node +} + +// Equals returns whether or not two chain views are the same. Uninitialized +// views (tip set to nil) are considered equal. +// +// This function is safe for concurrent access. +func (c *chainView) Equals(other *chainView) bool { + if c == other { + return true + } + + c.mtx.Lock() + other.mtx.Lock() + equals := len(c.nodes) == len(other.nodes) && c.tip() == other.tip() + other.mtx.Unlock() + c.mtx.Unlock() + return equals +} + +// contains returns whether or not the chain view contains the passed block +// node. This only differs from the exported version in that it is up to the +// caller to ensure the lock is held. +// +// This function MUST be called with the view mutex locked (for reads). +func (c *chainView) contains(node *blockNode) bool { + return c.nodeByHeight(node.height) == node +} + +// Contains returns whether or not the chain view contains the passed block +// node. +// +// This function is safe for concurrent access. +func (c *chainView) Contains(node *blockNode) bool { + c.mtx.Lock() + contains := c.contains(node) + c.mtx.Unlock() + return contains +} + +// next returns the successor to the provided node for the chain view. It will +// return nil if there is no successor or the provided node is not part of the +// view. This only differs from the exported version in that it is up to the +// caller to ensure the lock is held. +// +// See the comment on the exported function for more details. +// +// This function MUST be called with the view mutex locked (for reads). +func (c *chainView) next(node *blockNode) *blockNode { + if node == nil || !c.contains(node) { + return nil + } + + return c.nodeByHeight(node.height + 1) +} + +// Next returns the successor to the provided node for the chain view. It will +// return nil if there is no successfor or the provided node is not part of the +// view. +// +// For example, assume a block chain with a side chain as depicted below: +// genesis -> 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 +// \-> 4a -> 5a -> 6a +// +// Further, assume the view is for the longer chain depicted above. That is to +// say it consists of: +// genesis -> 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 +// +// Invoking this function with block node 5 would return block node 6 while +// invoking it with block node 5a would return nil since that node is not part +// of the view. +// +// This function is safe for concurrent access. +func (c *chainView) Next(node *blockNode) *blockNode { + c.mtx.Lock() + next := c.next(node) + c.mtx.Unlock() + return next +} + +// findFork returns the final common block between the provided node and the +// the chain view. It will return nil if there is no common block. This only +// differs from the exported version in that it is up to the caller to ensure +// the lock is held. +// +// See the exported FindFork comments for more details. +// +// This function MUST be called with the view mutex locked (for reads). +func (c *chainView) findFork(node *blockNode) *blockNode { + // No fork point for node that doesn't exist. + if node == nil { + return nil + } + + // When the height of the passed node is higher than the height of the + // tip of the current chain view, walk backwards through the nodes of + // the other chain until the heights match (or there or no more nodes in + // which case there is no common node between the two). + // + // NOTE: This isn't strictly necessary as the following section will + // find the node as well, however, it is more efficient to avoid the + // contains check since it is already known that the common node can't + // possibly be past the end of the current chain view. It also allows + // this code to take advantage of any potential future optimizations to + // the Ancestor function such as using an O(log n) skip list. + chainHeight := c.height() + if node.height > chainHeight { + node = node.Ancestor(chainHeight) + } + + // Walk the other chain backwards as long as the current one does not + // contain the node or there are no more nodes in which case there is no + // common node between the two. + for node != nil && !c.contains(node) { + node = node.parent + } + + return node +} + +// FindFork returns the final common block between the provided node and the +// the chain view. It will return nil if there is no common block. +// +// For example, assume a block chain with a side chain as depicted below: +// genesis -> 1 -> 2 -> ... -> 5 -> 6 -> 7 -> 8 +// \-> 6a -> 7a +// +// Further, assume the view is for the longer chain depicted above. That is to +// say it consists of: +// genesis -> 1 -> 2 -> ... -> 5 -> 6 -> 7 -> 8. +// +// Invoking this function with block node 7a would return block node 5 while +// invoking it with block node 7 would return itself since it is already part of +// the branch formed by the view. +// +// This function is safe for concurrent access. +func (c *chainView) FindFork(node *blockNode) *blockNode { + c.mtx.Lock() + fork := c.findFork(node) + c.mtx.Unlock() + return fork +} diff --git a/blockchain/chainview_test.go b/blockchain/chainview_test.go new file mode 100644 index 0000000000..985a481b54 --- /dev/null +++ b/blockchain/chainview_test.go @@ -0,0 +1,398 @@ +// Copyright (c) 2017 The btcsuite developers +// Copyright (c) 2018 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package blockchain + +import ( + "fmt" + "testing" +) + +// String returns the block node as a human-readable name. +func (node blockNode) String() string { + return fmt.Sprintf("%s(%d)", node.hash, node.height) +} + +// 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 := chainedFakeNodes(nil, 5) + branch1Nodes := chainedFakeNodes(branch0Nodes[1], 25) + branch2Nodes := chainedFakeNodes(branch1Nodes[0], 3) + + 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 + }{ + { + // 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(branchTip(branch0Nodes)), + genesis: branch0Nodes[0], + tip: branchTip(branch0Nodes), + side: newChainView(branchTip(branch1Nodes)), + sideTip: branchTip(branch1Nodes), + fork: branch0Nodes[1], + contains: branch0Nodes, + noContains: branch1Nodes, + equal: newChainView(branchTip(branch0Nodes)), + unequal: newChainView(branchTip(branch1Nodes)), + }, + { + // 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(branchTip(branch1Nodes)), + genesis: branch0Nodes[0], + tip: branchTip(branch1Nodes), + side: newChainView(branchTip(branch2Nodes)), + sideTip: branchTip(branch2Nodes), + fork: branch1Nodes[0], + contains: branch1Nodes, + noContains: branch2Nodes, + equal: newChainView(branchTip(branch1Nodes)), + unequal: newChainView(branchTip(branch2Nodes)), + }, + { + // 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(branchTip(branch2Nodes)), + genesis: branch0Nodes[0], + tip: branchTip(branch2Nodes), + side: newChainView(branchTip(branch0Nodes)), + sideTip: branchTip(branch0Nodes), + fork: branch0Nodes[1], + contains: branch2Nodes, + noContains: branch0Nodes[2:], + equal: newChainView(branchTip(branch2Nodes)), + unequal: newChainView(branchTip(branch0Nodes)), + }, + } +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 + } + } + } +} + +// 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 := chainedFakeNodes(nil, 5) + unrelatedBranchNodes := chainedFakeNodes(nil, 7) + + // Create chain views for the two unrelated histories. + view1 := newChainView(branchTip(branchNodes)) + view2 := newChainView(branchTip(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 := chainedFakeNodes(nil, 5) + branch1Nodes := chainedFakeNodes(branch0Nodes[1], 25) + + 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{branchTip(branch0Nodes), + branchTip(branch1Nodes)}, + contains: [][]*blockNode{branch0Nodes, branch1Nodes}, + }, + { + // Create a view with a longer chain and set the tip to + // increasingly shorter chains. + name: "decreasing", + view: newChainView(branchTip(branch1Nodes)), + tips: []*blockNode{branchTip(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(branchTip(branch0Nodes)), + tips: []*blockNode{branchTip(branch1Nodes), + branchTip(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(branchTip(branch1Nodes)), + tips: []*blockNode{branchTip(branch0Nodes), + branchTip(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 := chainedFakeNodes(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) + } +} diff --git a/blockchain/common_test.go b/blockchain/common_test.go index b26dc697ee..636ac81266 100644 --- a/blockchain/common_test.go +++ b/blockchain/common_test.go @@ -164,12 +164,18 @@ var testNoncePrng = mrand.New(mrand.NewSource(0)) // provided fields populated and fake values for the other fields. func newFakeNode(parent *blockNode, blockVersion int32, stakeVersion uint32, bits uint32, timestamp time.Time) *blockNode { // Make up a header and create a block node from it. + var prevHash chainhash.Hash + var height uint32 + if parent != nil { + prevHash = parent.hash + height = uint32(parent.height + 1) + } header := &wire.BlockHeader{ Version: blockVersion, - PrevBlock: parent.hash, + PrevBlock: prevHash, VoteBits: 0x01, Bits: bits, - Height: uint32(parent.height) + 1, + Height: height, Timestamp: timestamp, Nonce: testNoncePrng.Uint32(), StakeVersion: stakeVersion,