// 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) } }