github.com/btcsuite/btcd@v0.24.0/blockchain/chainview_test.go (about)

     1  // Copyright (c) 2017 The btcsuite developers
     2  // Use of this source code is governed by an ISC
     3  // license that can be found in the LICENSE file.
     4  
     5  package blockchain
     6  
     7  import (
     8  	"fmt"
     9  	"math/rand"
    10  	"reflect"
    11  	"testing"
    12  
    13  	"github.com/btcsuite/btcd/wire"
    14  )
    15  
    16  // testNoncePrng provides a deterministic prng for the nonce in generated fake
    17  // nodes.  The ensures that the node have unique hashes.
    18  var testNoncePrng = rand.New(rand.NewSource(0))
    19  
    20  // chainedNodes returns the specified number of nodes constructed such that each
    21  // subsequent node points to the previous one to create a chain.  The first node
    22  // will point to the passed parent which can be nil if desired.
    23  func chainedNodes(parent *blockNode, numNodes int) []*blockNode {
    24  	nodes := make([]*blockNode, numNodes)
    25  	tip := parent
    26  	for i := 0; i < numNodes; i++ {
    27  		// This is invalid, but all that is needed is enough to get the
    28  		// synthetic tests to work.
    29  		header := wire.BlockHeader{Nonce: testNoncePrng.Uint32()}
    30  		if tip != nil {
    31  			header.PrevBlock = tip.hash
    32  		}
    33  		nodes[i] = newBlockNode(&header, tip)
    34  		tip = nodes[i]
    35  	}
    36  	return nodes
    37  }
    38  
    39  // String returns the block node as a human-readable name.
    40  func (node blockNode) String() string {
    41  	return fmt.Sprintf("%s(%d)", node.hash, node.height)
    42  }
    43  
    44  // tstTip is a convenience function to grab the tip of a chain of block nodes
    45  // created via chainedNodes.
    46  func tstTip(nodes []*blockNode) *blockNode {
    47  	return nodes[len(nodes)-1]
    48  }
    49  
    50  // locatorHashes is a convenience function that returns the hashes for all of
    51  // the passed indexes of the provided nodes.  It is used to construct expected
    52  // block locators in the tests.
    53  func locatorHashes(nodes []*blockNode, indexes ...int) BlockLocator {
    54  	hashes := make(BlockLocator, 0, len(indexes))
    55  	for _, idx := range indexes {
    56  		hashes = append(hashes, &nodes[idx].hash)
    57  	}
    58  	return hashes
    59  }
    60  
    61  // zipLocators is a convenience function that returns a single block locator
    62  // given a variable number of them and is used in the tests.
    63  func zipLocators(locators ...BlockLocator) BlockLocator {
    64  	var hashes BlockLocator
    65  	for _, locator := range locators {
    66  		hashes = append(hashes, locator...)
    67  	}
    68  	return hashes
    69  }
    70  
    71  // TestChainView ensures all of the exported functionality of chain views works
    72  // as intended with the exception of some special cases which are handled in
    73  // other tests.
    74  func TestChainView(t *testing.T) {
    75  	// Construct a synthetic block index consisting of the following
    76  	// structure.
    77  	// 0 -> 1 -> 2  -> 3  -> 4
    78  	//       \-> 2a -> 3a -> 4a  -> 5a -> 6a -> 7a -> ... -> 26a
    79  	//             \-> 3a'-> 4a' -> 5a'
    80  	branch0Nodes := chainedNodes(nil, 5)
    81  	branch1Nodes := chainedNodes(branch0Nodes[1], 25)
    82  	branch2Nodes := chainedNodes(branch1Nodes[0], 3)
    83  
    84  	tip := tstTip
    85  	tests := []struct {
    86  		name       string
    87  		view       *chainView   // active view
    88  		genesis    *blockNode   // expected genesis block of active view
    89  		tip        *blockNode   // expected tip of active view
    90  		side       *chainView   // side chain view
    91  		sideTip    *blockNode   // expected tip of side chain view
    92  		fork       *blockNode   // expected fork node
    93  		contains   []*blockNode // expected nodes in active view
    94  		noContains []*blockNode // expected nodes NOT in active view
    95  		equal      *chainView   // view expected equal to active view
    96  		unequal    *chainView   // view expected NOT equal to active
    97  		locator    BlockLocator // expected locator for active view tip
    98  	}{
    99  		{
   100  			// Create a view for branch 0 as the active chain and
   101  			// another view for branch 1 as the side chain.
   102  			name:       "chain0-chain1",
   103  			view:       newChainView(tip(branch0Nodes)),
   104  			genesis:    branch0Nodes[0],
   105  			tip:        tip(branch0Nodes),
   106  			side:       newChainView(tip(branch1Nodes)),
   107  			sideTip:    tip(branch1Nodes),
   108  			fork:       branch0Nodes[1],
   109  			contains:   branch0Nodes,
   110  			noContains: branch1Nodes,
   111  			equal:      newChainView(tip(branch0Nodes)),
   112  			unequal:    newChainView(tip(branch1Nodes)),
   113  			locator:    locatorHashes(branch0Nodes, 4, 3, 2, 1, 0),
   114  		},
   115  		{
   116  			// Create a view for branch 1 as the active chain and
   117  			// another view for branch 2 as the side chain.
   118  			name:       "chain1-chain2",
   119  			view:       newChainView(tip(branch1Nodes)),
   120  			genesis:    branch0Nodes[0],
   121  			tip:        tip(branch1Nodes),
   122  			side:       newChainView(tip(branch2Nodes)),
   123  			sideTip:    tip(branch2Nodes),
   124  			fork:       branch1Nodes[0],
   125  			contains:   branch1Nodes,
   126  			noContains: branch2Nodes,
   127  			equal:      newChainView(tip(branch1Nodes)),
   128  			unequal:    newChainView(tip(branch2Nodes)),
   129  			locator: zipLocators(
   130  				locatorHashes(branch1Nodes, 24, 23, 22, 21, 20,
   131  					19, 18, 17, 16, 15, 14, 13, 11, 7),
   132  				locatorHashes(branch0Nodes, 1, 0)),
   133  		},
   134  		{
   135  			// Create a view for branch 2 as the active chain and
   136  			// another view for branch 0 as the side chain.
   137  			name:       "chain2-chain0",
   138  			view:       newChainView(tip(branch2Nodes)),
   139  			genesis:    branch0Nodes[0],
   140  			tip:        tip(branch2Nodes),
   141  			side:       newChainView(tip(branch0Nodes)),
   142  			sideTip:    tip(branch0Nodes),
   143  			fork:       branch0Nodes[1],
   144  			contains:   branch2Nodes,
   145  			noContains: branch0Nodes[2:],
   146  			equal:      newChainView(tip(branch2Nodes)),
   147  			unequal:    newChainView(tip(branch0Nodes)),
   148  			locator: zipLocators(
   149  				locatorHashes(branch2Nodes, 2, 1, 0),
   150  				locatorHashes(branch1Nodes, 0),
   151  				locatorHashes(branch0Nodes, 1, 0)),
   152  		},
   153  	}
   154  testLoop:
   155  	for _, test := range tests {
   156  		// Ensure the active and side chain heights are the expected
   157  		// values.
   158  		if test.view.Height() != test.tip.height {
   159  			t.Errorf("%s: unexpected active view height -- got "+
   160  				"%d, want %d", test.name, test.view.Height(),
   161  				test.tip.height)
   162  			continue
   163  		}
   164  		if test.side.Height() != test.sideTip.height {
   165  			t.Errorf("%s: unexpected side view height -- got %d, "+
   166  				"want %d", test.name, test.side.Height(),
   167  				test.sideTip.height)
   168  			continue
   169  		}
   170  
   171  		// Ensure the active and side chain genesis block is the
   172  		// expected value.
   173  		if test.view.Genesis() != test.genesis {
   174  			t.Errorf("%s: unexpected active view genesis -- got "+
   175  				"%v, want %v", test.name, test.view.Genesis(),
   176  				test.genesis)
   177  			continue
   178  		}
   179  		if test.side.Genesis() != test.genesis {
   180  			t.Errorf("%s: unexpected side view genesis -- got %v, "+
   181  				"want %v", test.name, test.view.Genesis(),
   182  				test.genesis)
   183  			continue
   184  		}
   185  
   186  		// Ensure the active and side chain tips are the expected nodes.
   187  		if test.view.Tip() != test.tip {
   188  			t.Errorf("%s: unexpected active view tip -- got %v, "+
   189  				"want %v", test.name, test.view.Tip(), test.tip)
   190  			continue
   191  		}
   192  		if test.side.Tip() != test.sideTip {
   193  			t.Errorf("%s: unexpected active view tip -- got %v, "+
   194  				"want %v", test.name, test.side.Tip(),
   195  				test.sideTip)
   196  			continue
   197  		}
   198  
   199  		// Ensure that regardless of the order the two chains are
   200  		// compared they both return the expected fork point.
   201  		forkNode := test.view.FindFork(test.side.Tip())
   202  		if forkNode != test.fork {
   203  			t.Errorf("%s: unexpected fork node (view, side) -- "+
   204  				"got %v, want %v", test.name, forkNode,
   205  				test.fork)
   206  			continue
   207  		}
   208  		forkNode = test.side.FindFork(test.view.Tip())
   209  		if forkNode != test.fork {
   210  			t.Errorf("%s: unexpected fork node (side, view) -- "+
   211  				"got %v, want %v", test.name, forkNode,
   212  				test.fork)
   213  			continue
   214  		}
   215  
   216  		// Ensure that the fork point for a node that is already part
   217  		// of the chain view is the node itself.
   218  		forkNode = test.view.FindFork(test.view.Tip())
   219  		if forkNode != test.view.Tip() {
   220  			t.Errorf("%s: unexpected fork node (view, tip) -- "+
   221  				"got %v, want %v", test.name, forkNode,
   222  				test.view.Tip())
   223  			continue
   224  		}
   225  
   226  		// Ensure all expected nodes are contained in the active view.
   227  		for _, node := range test.contains {
   228  			if !test.view.Contains(node) {
   229  				t.Errorf("%s: expected %v in active view",
   230  					test.name, node)
   231  				continue testLoop
   232  			}
   233  		}
   234  
   235  		// Ensure all nodes from side chain view are NOT contained in
   236  		// the active view.
   237  		for _, node := range test.noContains {
   238  			if test.view.Contains(node) {
   239  				t.Errorf("%s: unexpected %v in active view",
   240  					test.name, node)
   241  				continue testLoop
   242  			}
   243  		}
   244  
   245  		// Ensure equality of different views into the same chain works
   246  		// as intended.
   247  		if !test.view.Equals(test.equal) {
   248  			t.Errorf("%s: unexpected unequal views", test.name)
   249  			continue
   250  		}
   251  		if test.view.Equals(test.unequal) {
   252  			t.Errorf("%s: unexpected equal views", test.name)
   253  			continue
   254  		}
   255  
   256  		// Ensure all nodes contained in the view return the expected
   257  		// next node.
   258  		for i, node := range test.contains {
   259  			// Final node expects nil for the next node.
   260  			var expected *blockNode
   261  			if i < len(test.contains)-1 {
   262  				expected = test.contains[i+1]
   263  			}
   264  			if next := test.view.Next(node); next != expected {
   265  				t.Errorf("%s: unexpected next node -- got %v, "+
   266  					"want %v", test.name, next, expected)
   267  				continue testLoop
   268  			}
   269  		}
   270  
   271  		// Ensure nodes that are not contained in the view do not
   272  		// produce a successor node.
   273  		for _, node := range test.noContains {
   274  			if next := test.view.Next(node); next != nil {
   275  				t.Errorf("%s: unexpected next node -- got %v, "+
   276  					"want nil", test.name, next)
   277  				continue testLoop
   278  			}
   279  		}
   280  
   281  		// Ensure all nodes contained in the view can be retrieved by
   282  		// height.
   283  		for _, wantNode := range test.contains {
   284  			node := test.view.NodeByHeight(wantNode.height)
   285  			if node != wantNode {
   286  				t.Errorf("%s: unexpected node for height %d -- "+
   287  					"got %v, want %v", test.name,
   288  					wantNode.height, node, wantNode)
   289  				continue testLoop
   290  			}
   291  		}
   292  
   293  		// Ensure the block locator for the tip of the active view
   294  		// consists of the expected hashes.
   295  		locator := test.view.BlockLocator(test.view.tip())
   296  		if !reflect.DeepEqual(locator, test.locator) {
   297  			t.Errorf("%s: unexpected locator -- got %v, want %v",
   298  				test.name, locator, test.locator)
   299  			continue
   300  		}
   301  	}
   302  }
   303  
   304  // TestChainViewForkCorners ensures that finding the fork between two chains
   305  // works in some corner cases such as when the two chains have completely
   306  // unrelated histories.
   307  func TestChainViewForkCorners(t *testing.T) {
   308  	// Construct two unrelated single branch synthetic block indexes.
   309  	branchNodes := chainedNodes(nil, 5)
   310  	unrelatedBranchNodes := chainedNodes(nil, 7)
   311  
   312  	// Create chain views for the two unrelated histories.
   313  	view1 := newChainView(tstTip(branchNodes))
   314  	view2 := newChainView(tstTip(unrelatedBranchNodes))
   315  
   316  	// Ensure attempting to find a fork point with a node that doesn't exist
   317  	// doesn't produce a node.
   318  	if fork := view1.FindFork(nil); fork != nil {
   319  		t.Fatalf("FindFork: unexpected fork -- got %v, want nil", fork)
   320  	}
   321  
   322  	// Ensure attempting to find a fork point in two chain views with
   323  	// totally unrelated histories doesn't produce a node.
   324  	for _, node := range branchNodes {
   325  		if fork := view2.FindFork(node); fork != nil {
   326  			t.Fatalf("FindFork: unexpected fork -- got %v, want nil",
   327  				fork)
   328  		}
   329  	}
   330  	for _, node := range unrelatedBranchNodes {
   331  		if fork := view1.FindFork(node); fork != nil {
   332  			t.Fatalf("FindFork: unexpected fork -- got %v, want nil",
   333  				fork)
   334  		}
   335  	}
   336  }
   337  
   338  // TestChainViewSetTip ensures changing the tip works as intended including
   339  // capacity changes.
   340  func TestChainViewSetTip(t *testing.T) {
   341  	// Construct a synthetic block index consisting of the following
   342  	// structure.
   343  	// 0 -> 1 -> 2  -> 3  -> 4
   344  	//       \-> 2a -> 3a -> 4a  -> 5a -> 6a -> 7a -> ... -> 26a
   345  	branch0Nodes := chainedNodes(nil, 5)
   346  	branch1Nodes := chainedNodes(branch0Nodes[1], 25)
   347  
   348  	tip := tstTip
   349  	tests := []struct {
   350  		name     string
   351  		view     *chainView     // active view
   352  		tips     []*blockNode   // tips to set
   353  		contains [][]*blockNode // expected nodes in view for each tip
   354  	}{
   355  		{
   356  			// Create an empty view and set the tip to increasingly
   357  			// longer chains.
   358  			name:     "increasing",
   359  			view:     newChainView(nil),
   360  			tips:     []*blockNode{tip(branch0Nodes), tip(branch1Nodes)},
   361  			contains: [][]*blockNode{branch0Nodes, branch1Nodes},
   362  		},
   363  		{
   364  			// Create a view with a longer chain and set the tip to
   365  			// increasingly shorter chains.
   366  			name:     "decreasing",
   367  			view:     newChainView(tip(branch1Nodes)),
   368  			tips:     []*blockNode{tip(branch0Nodes), nil},
   369  			contains: [][]*blockNode{branch0Nodes, nil},
   370  		},
   371  		{
   372  			// Create a view with a shorter chain and set the tip to
   373  			// a longer chain followed by setting it back to the
   374  			// shorter chain.
   375  			name:     "small-large-small",
   376  			view:     newChainView(tip(branch0Nodes)),
   377  			tips:     []*blockNode{tip(branch1Nodes), tip(branch0Nodes)},
   378  			contains: [][]*blockNode{branch1Nodes, branch0Nodes},
   379  		},
   380  		{
   381  			// Create a view with a longer chain and set the tip to
   382  			// a smaller chain followed by setting it back to the
   383  			// longer chain.
   384  			name:     "large-small-large",
   385  			view:     newChainView(tip(branch1Nodes)),
   386  			tips:     []*blockNode{tip(branch0Nodes), tip(branch1Nodes)},
   387  			contains: [][]*blockNode{branch0Nodes, branch1Nodes},
   388  		},
   389  	}
   390  
   391  testLoop:
   392  	for _, test := range tests {
   393  		for i, tip := range test.tips {
   394  			// Ensure the view tip is the expected node.
   395  			test.view.SetTip(tip)
   396  			if test.view.Tip() != tip {
   397  				t.Errorf("%s: unexpected view tip -- got %v, "+
   398  					"want %v", test.name, test.view.Tip(),
   399  					tip)
   400  				continue testLoop
   401  			}
   402  
   403  			// Ensure all expected nodes are contained in the view.
   404  			for _, node := range test.contains[i] {
   405  				if !test.view.Contains(node) {
   406  					t.Errorf("%s: expected %v in active view",
   407  						test.name, node)
   408  					continue testLoop
   409  				}
   410  			}
   411  
   412  		}
   413  	}
   414  }
   415  
   416  // TestChainViewNil ensures that creating and accessing a nil chain view behaves
   417  // as expected.
   418  func TestChainViewNil(t *testing.T) {
   419  	// Ensure two unininitialized views are considered equal.
   420  	view := newChainView(nil)
   421  	if !view.Equals(newChainView(nil)) {
   422  		t.Fatal("uninitialized nil views unequal")
   423  	}
   424  
   425  	// Ensure the genesis of an uninitialized view does not produce a node.
   426  	if genesis := view.Genesis(); genesis != nil {
   427  		t.Fatalf("Genesis: unexpected genesis -- got %v, want nil",
   428  			genesis)
   429  	}
   430  
   431  	// Ensure the tip of an uninitialized view does not produce a node.
   432  	if tip := view.Tip(); tip != nil {
   433  		t.Fatalf("Tip: unexpected tip -- got %v, want nil", tip)
   434  	}
   435  
   436  	// Ensure the height of an uninitialized view is the expected value.
   437  	if height := view.Height(); height != -1 {
   438  		t.Fatalf("Height: unexpected height -- got %d, want -1", height)
   439  	}
   440  
   441  	// Ensure attempting to get a node for a height that does not exist does
   442  	// not produce a node.
   443  	if node := view.NodeByHeight(10); node != nil {
   444  		t.Fatalf("NodeByHeight: unexpected node -- got %v, want nil", node)
   445  	}
   446  
   447  	// Ensure an uninitialized view does not report it contains nodes.
   448  	fakeNode := chainedNodes(nil, 1)[0]
   449  	if view.Contains(fakeNode) {
   450  		t.Fatalf("Contains: view claims it contains node %v", fakeNode)
   451  	}
   452  
   453  	// Ensure the next node for a node that does not exist does not produce
   454  	// a node.
   455  	if next := view.Next(nil); next != nil {
   456  		t.Fatalf("Next: unexpected next node -- got %v, want nil", next)
   457  	}
   458  
   459  	// Ensure the next node for a node that exists does not produce a node.
   460  	if next := view.Next(fakeNode); next != nil {
   461  		t.Fatalf("Next: unexpected next node -- got %v, want nil", next)
   462  	}
   463  
   464  	// Ensure attempting to find a fork point with a node that doesn't exist
   465  	// doesn't produce a node.
   466  	if fork := view.FindFork(nil); fork != nil {
   467  		t.Fatalf("FindFork: unexpected fork -- got %v, want nil", fork)
   468  	}
   469  
   470  	// Ensure attempting to get a block locator for the tip doesn't produce
   471  	// one since the tip is nil.
   472  	if locator := view.BlockLocator(nil); locator != nil {
   473  		t.Fatalf("BlockLocator: unexpected locator -- got %v, want nil",
   474  			locator)
   475  	}
   476  
   477  	// Ensure attempting to get a block locator for a node that exists still
   478  	// works as intended.
   479  	branchNodes := chainedNodes(nil, 50)
   480  	wantLocator := locatorHashes(branchNodes, 49, 48, 47, 46, 45, 44, 43,
   481  		42, 41, 40, 39, 38, 36, 32, 24, 8, 0)
   482  	locator := view.BlockLocator(tstTip(branchNodes))
   483  	if !reflect.DeepEqual(locator, wantLocator) {
   484  		t.Fatalf("BlockLocator: unexpected locator -- got %v, want %v",
   485  			locator, wantLocator)
   486  	}
   487  }