diff --git a/blockchain/chainview.go b/blockchain/chainview.go new file mode 100644 index 00000000..f9ee0189 --- /dev/null +++ b/blockchain/chainview.go @@ -0,0 +1,316 @@ +// 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 ( + "sync" +) + +// approxNodesPerWeek is an approximation of the number of new blocks there are +// in a week on average. +const approxNodesPerWeek = 6 * 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 int32(cap(c.nodes)) < needed { + c.nodes = make([]*blockNode, needed, needed+approxNodesPerWeek) + } else { + c.nodes = c.nodes[0:needed] + } + + 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() int32 { + return int32(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() int32 { + 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 int32) *blockNode { + if height < 0 || height >= int32(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 int32) *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 { + 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 00000000..a967a74f --- /dev/null +++ b/blockchain/chainview_test.go @@ -0,0 +1,412 @@ +// 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" + "testing" + + "github.com/btcsuite/btcd/wire" +) + +// 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{} + 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] +} + +// 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 + }{ + { + // 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)), + }, + { + // 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)), + }, + { + // 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)), + }, + } +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 := 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 + tips []*blockNode + }{ + { + // Create an empty view and set the tip to increasingly + // longer chains. + name: "increasing", + view: newChainView(nil), + tips: []*blockNode{tip(branch0Nodes), tip(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}, + }, + { + // 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)}, + }, + { + // 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)}, + }, + } + + for _, test := range tests { + for _, 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 + } + } + } +} + +// 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) + } +}