dcrd/blockchain/blockindex_test.go
Dave Collins b816f16545
blockchain: Add convenience ancestor of func.
This adds a convenience func to block nodes named IsAncestorOf for
determining whether or not a node is an ancestor of a given target node
along with tests to ensure proper functionality.
2021-10-29 12:37:06 -05:00

761 lines
24 KiB
Go

// Copyright (c) 2018-2021 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"
"math/big"
"math/rand"
"reflect"
"testing"
"time"
"github.com/decred/dcrd/chaincfg/v3"
"github.com/decred/dcrd/wire"
)
// TestBlockNodeHeader ensures that block nodes reconstruct the correct header
// and fetching the header from the chain reconstructs it from memory.
func TestBlockNodeHeader(t *testing.T) {
// Create a fake chain and block header with all fields set to nondefault
// values.
params := chaincfg.RegNetParams()
bc := newFakeChain(params)
tip := bc.bestChain.Tip()
testHeader := wire.BlockHeader{
Version: 1,
PrevBlock: tip.hash,
MerkleRoot: *mustParseHash("09876543210987654321"),
StakeRoot: *mustParseHash("43210987654321098765"),
VoteBits: 0x03,
FinalState: [6]byte{0xaa},
Voters: 4,
FreshStake: 5,
Revocations: 6,
PoolSize: 20000,
Bits: 0x1234,
SBits: 123456789,
Height: 1,
Size: 393216,
Timestamp: time.Unix(1454954400, 0),
Nonce: 7,
ExtraData: [32]byte{0xbb},
StakeVersion: 5,
}
node := newBlockNode(&testHeader, tip)
bc.index.AddNode(node)
// Ensure reconstructing the header for the node produces the same header
// used to create the node.
gotHeader := node.Header()
if !reflect.DeepEqual(gotHeader, testHeader) {
t.Fatalf("node.Header: mismatched headers: got %+v, want %+v",
gotHeader, testHeader)
}
// Ensure fetching the header from the chain produces the same header used
// to create the node.
testHeaderHash := testHeader.BlockHash()
gotHeader, err := bc.HeaderByHash(&testHeaderHash)
if err != nil {
t.Fatalf("HeaderByHash: unexpected error: %v", err)
}
if !reflect.DeepEqual(gotHeader, testHeader) {
t.Fatalf("HeaderByHash: mismatched headers: got %+v, want %+v",
gotHeader, testHeader)
}
}
// TestCalcPastMedianTime ensures the CalcPastMedianTie function works as
// intended including when there are less than the typical number of blocks
// which happens near the beginning of the chain.
func TestCalcPastMedianTime(t *testing.T) {
tests := []struct {
name string
timestamps []int64
expected int64
}{
{
name: "one block",
timestamps: []int64{1517188771},
expected: 1517188771,
},
{
name: "two blocks, in order",
timestamps: []int64{1517188771, 1517188831},
expected: 1517188771,
},
{
name: "three blocks, in order",
timestamps: []int64{1517188771, 1517188831, 1517188891},
expected: 1517188831,
},
{
name: "three blocks, out of order",
timestamps: []int64{1517188771, 1517188891, 1517188831},
expected: 1517188831,
},
{
name: "four blocks, in order",
timestamps: []int64{1517188771, 1517188831, 1517188891, 1517188951},
expected: 1517188831,
},
{
name: "four blocks, out of order",
timestamps: []int64{1517188831, 1517188771, 1517188951, 1517188891},
expected: 1517188831,
},
{
name: "eleven blocks, in order",
timestamps: []int64{1517188771, 1517188831, 1517188891, 1517188951,
1517189011, 1517189071, 1517189131, 1517189191, 1517189251,
1517189311, 1517189371},
expected: 1517189071,
},
{
name: "eleven blocks, out of order",
timestamps: []int64{1517188831, 1517188771, 1517188891, 1517189011,
1517188951, 1517189071, 1517189131, 1517189191, 1517189251,
1517189371, 1517189311},
expected: 1517189071,
},
{
name: "fifteen blocks, in order",
timestamps: []int64{1517188771, 1517188831, 1517188891, 1517188951,
1517189011, 1517189071, 1517189131, 1517189191, 1517189251,
1517189311, 1517189371, 1517189431, 1517189491, 1517189551,
1517189611},
expected: 1517189311,
},
{
name: "fifteen blocks, out of order",
timestamps: []int64{1517188771, 1517188891, 1517188831, 1517189011,
1517188951, 1517189131, 1517189071, 1517189251, 1517189191,
1517189371, 1517189311, 1517189491, 1517189431, 1517189611,
1517189551},
expected: 1517189311,
},
}
// Ensure the genesis block timestamp of the test params is before the test
// data. Also, clone the provided parameters first to avoid mutating them.
//
// The timestamp corresponds to 2018-01-01 00:00:00 +0000 UTC.
params := chaincfg.RegNetParams()
params.GenesisBlock.Header.Timestamp = time.Unix(1514764800, 0)
params.GenesisHash = params.GenesisBlock.BlockHash()
for _, test := range tests {
// Create a synthetic chain with the correct number of nodes and the
// timestamps as specified by the test.
bc := newFakeChain(params)
node := bc.bestChain.Tip()
for _, timestamp := range test.timestamps {
node = newFakeNode(node, 0, 0, 0, time.Unix(timestamp, 0))
bc.index.AddNode(node)
bc.bestChain.SetTip(node)
}
// Ensure the median time is the expected value.
gotTime := node.CalcPastMedianTime()
wantTime := time.Unix(test.expected, 0)
if !gotTime.Equal(wantTime) {
t.Errorf("%s: mismatched timestamps -- got: %v, want: %v",
test.name, gotTime, wantTime)
continue
}
}
}
// TestChainTips ensures the chain tip tracking in the block index works
// as expected.
func TestChainTips(t *testing.T) {
params := chaincfg.RegNetParams()
bc := newFakeChain(params)
genesis := bc.bestChain.NodeByHeight(0)
// Construct a synthetic simnet chain consisting of the following structure.
// 0 -> 1 -> 2 -> 3 -> 4
// | \-> 2a -> 3a -> 4a -> 5a -> 6a -> 7a -> ... -> 26a
// | | \-> 3b -> 4b -> 5b
// | \-> 2c -> 3c -> 4c -> 5c -> 6c -> 7c -> ... -> 26c
// \-> 1d
// | \
// \-> 1e |
// \-> 2f (added after 1e)
branches := make([][]*blockNode, 7)
branches[0] = chainedFakeNodes(genesis, 4)
branches[1] = chainedFakeNodes(branches[0][0], 25)
branches[2] = chainedFakeNodes(branches[1][0], 3)
branches[3] = chainedFakeNodes(branches[0][0], 25)
branches[4] = chainedFakeNodes(genesis, 1)
branches[5] = chainedFakeNodes(genesis, 1)
branches[6] = chainedFakeNodes(branches[4][0], 1)
// Add all of the nodes to the index.
for _, branch := range branches {
for _, node := range branch {
bc.index.AddNode(node)
}
}
// Create a map of all of the chain tips the block index believes exist.
chainTips := make(map[*blockNode]struct{})
bc.index.RLock()
for _, entry := range bc.index.chainTips {
chainTips[entry.tip] = struct{}{}
for _, node := range entry.otherTips {
chainTips[node] = struct{}{}
}
}
bc.index.RUnlock()
// Exclude tips that are part of an earlier set of branch nodes that was
// built on via a new set of branch nodes.
excludeExpected := make(map[*blockNode]struct{})
excludeExpected[branchTip(branches[4])] = struct{}{}
// The expected chain tips are the tips of all of the branches minus any
// that were excluded.
expectedTips := make(map[*blockNode]struct{})
for _, branch := range branches {
if _, ok := excludeExpected[branchTip(branch)]; ok {
continue
}
expectedTips[branchTip(branch)] = struct{}{}
}
// Ensure the chain tips are the expected values.
if len(chainTips) != len(expectedTips) {
t.Fatalf("block index reports %d chain tips, but %d were expected",
len(chainTips), len(expectedTips))
}
for node := range expectedTips {
if _, ok := chainTips[node]; !ok {
t.Fatalf("block index does not contain expected tip %s (height %d)",
node.hash, node.height)
}
}
}
// TestAncestorSkipList ensures the skip list functionality and ancestor
// traversal that makes use of it works as expected.
func TestAncestorSkipList(t *testing.T) {
// Create fake nodes to use for skip list traversal.
nodes := chainedFakeSkipListNodes(nil, 250000)
// Ensure the skip list is constructed correctly by checking that each node
// points to an ancestor with a lower height and that said ancestor is
// actually the node at that height.
for i, node := range nodes[1:] {
ancestorHeight := node.skipToAncestor.height
if ancestorHeight >= int64(i+1) {
t.Fatalf("height for skip list pointer %d is not lower than "+
"current node height %d", ancestorHeight, int64(i+1))
}
if node.skipToAncestor != nodes[ancestorHeight] {
t.Fatalf("unexpected node for skip list pointer for height %d",
ancestorHeight)
}
}
// Use a unique random seed each test instance and log it if the tests fail.
seed := time.Now().Unix()
rng := rand.New(rand.NewSource(seed))
defer func(t *testing.T, seed int64) {
if t.Failed() {
t.Logf("random seed: %d", seed)
}
}(t, seed)
for i := 0; i < 2500; i++ {
// Ensure obtaining the ancestor at a random starting height from the
// tip is the expected node.
startHeight := rng.Int63n(int64(len(nodes) - 1))
startNode := nodes[startHeight]
if branchTip(nodes).Ancestor(startHeight) != startNode {
t.Fatalf("unexpected ancestor for height %d from tip",
startHeight)
}
// Ensure obtaining the ancestor at height 0 starting from the node at
// the random starting height is the expected node.
if startNode.Ancestor(0) != nodes[0] {
t.Fatalf("unexpected ancestor for height 0 from start height %d",
startHeight)
}
// Ensure obtaining the ancestor from a random ending height starting
// from the node at the random starting height is the expected node.
endHeight := rng.Int63n(startHeight + 1)
if startNode.Ancestor(endHeight) != nodes[endHeight] {
t.Fatalf("unexpected ancestor for height %d from start height %d",
endHeight, startHeight)
}
}
}
// TestIsAncestorOf ensures determining if a node is an ancestor of another one
// works as expected.
func TestIsAncestorOf(t *testing.T) {
params := chaincfg.RegNetParams()
bc := newFakeChain(params)
genesis := bc.bestChain.NodeByHeight(0)
// Construct a synthetic simnet chain consisting of the following structure.
// 0 -> 1 -> 2 -> 3 -> 4
// | \-> 2a -> 3a -> 4a -> 5a -> 6a -> 7a
// | | \-> 3b -> 4b -> 5b
// | \-> 2c -> 3c -> 4c -> 5c -> 6c -> 7c
// \-> 1d
// \-> 1e
branches := make([][]*blockNode, 6)
branches[0] = chainedFakeNodes(genesis, 4)
branches[1] = chainedFakeNodes(branches[0][0], 8)
branches[2] = chainedFakeNodes(branches[1][0], 3)
branches[3] = chainedFakeNodes(branches[0][0], 8)
branches[4] = chainedFakeNodes(genesis, 1)
branches[5] = chainedFakeNodes(genesis, 1)
// Add all of the nodes to the index.
for _, branch := range branches {
for _, node := range branch {
bc.index.AddNode(node)
}
}
tests := []struct {
name string
n *blockNode
n2 *blockNode
want bool
}{{
name: "node is ancestor of itself",
n: branchTip(branches[0]),
n2: branchTip(branches[0]),
want: true,
}, {
name: "different branch tips at same height",
n: branchTip(branches[1]),
n2: branchTip(branches[3]),
want: false,
}, {
name: "different branch tips at different heights",
n: branchTip(branches[1]),
n2: branchTip(branches[2]),
want: false,
}, {
name: "genesis is ancestor of all blocks (via branch 4)",
n: genesis,
n2: branchTip(branches[4]),
want: true,
}, {
name: "genesis is ancestor of all blocks (via branch 5)",
n: genesis,
n2: branchTip(branches[5]),
want: true,
}, {
name: "descendants are not ancestors (via branch 1)",
n: branchTip(branches[1]),
n2: branches[1][2],
want: false,
}, {
name: "branch 1 ancestor",
n: branches[1][2],
n2: branchTip(branches[1]),
want: true,
}, {
name: "branch 3 node not ancestor of a branch 1 node (smaller height)",
n: branches[3][0],
n2: branchTip(branches[1]),
want: false,
}, {
name: "branch 2 node not ancestor of a branch 1 node (greater height)",
n: branchTip(branches[2]),
n2: branches[1][0],
want: false,
}}
for _, test := range tests {
got := test.n.IsAncestorOf(test.n2)
if got != test.want {
t.Errorf("%q: unexpected result -- got %v, want %v", test.name, got,
test.want)
continue
}
}
}
// TestBetterCandidate ensures the best candidate and hash comparison functions
// work as intended including multiple keys.
func TestBetterCandidate(t *testing.T) {
lowerHash := mustParseHash("000000000000c41019872ff7db8fd2e9bfa05f42d3f8fee8e895e8c1e5b8dcba")
higherHash := mustParseHash("000000000000d41019872ff7db8fd2e9bfa05f42d3f8fee8e895e8c1e5b8dcba")
tests := []struct {
name string // test description
nodeA *blockNode // first node to compare
nodeB *blockNode // second node to compare
wantCmp int // expected result of the hash comparison
wantBetter bool // expected result of the better comparison
}{{
name: "exactly equal, both data",
nodeA: &blockNode{
hash: *mustParseHash("0000000000000000000000000000000000000000000000000000000000000000"),
workSum: big.NewInt(2),
status: statusDataStored,
receivedOrderID: 0,
},
nodeB: &blockNode{
hash: *mustParseHash("0000000000000000000000000000000000000000000000000000000000000000"),
workSum: big.NewInt(2),
status: statusDataStored,
receivedOrderID: 0,
},
wantCmp: 0,
wantBetter: false,
}, {
name: "exactly equal, no data",
nodeA: &blockNode{
hash: *mustParseHash("0000000000000000000000000000000000000000000000000000000000000000"),
workSum: big.NewInt(2),
receivedOrderID: 0,
},
nodeB: &blockNode{
hash: *mustParseHash("0000000000000000000000000000000000000000000000000000000000000000"),
workSum: big.NewInt(2),
receivedOrderID: 0,
},
wantCmp: 0,
wantBetter: false,
}, {
name: "a has more cumulative work, same order, higher hash, b has data",
nodeA: &blockNode{
hash: *higherHash,
workSum: big.NewInt(4),
receivedOrderID: 0,
},
nodeB: &blockNode{
hash: *lowerHash,
workSum: big.NewInt(2),
status: statusDataStored,
receivedOrderID: 0,
},
wantCmp: 1,
wantBetter: true,
}, {
name: "a has less cumulative work, same order, lower hash, a has data",
nodeA: &blockNode{
hash: *lowerHash,
workSum: big.NewInt(2),
status: statusDataStored,
receivedOrderID: 0,
},
nodeB: &blockNode{
hash: *higherHash,
workSum: big.NewInt(4),
receivedOrderID: 0,
},
wantCmp: -1,
wantBetter: false,
}, {
name: "a has same cumulative work, same order, lower hash, b has data",
nodeA: &blockNode{
hash: *lowerHash,
workSum: big.NewInt(2),
receivedOrderID: 0,
},
nodeB: &blockNode{
hash: *higherHash,
workSum: big.NewInt(2),
status: statusDataStored,
receivedOrderID: 0,
},
wantCmp: -1,
wantBetter: false,
}, {
name: "a has same cumulative work, same order, higher hash, a has data",
nodeA: &blockNode{
hash: *higherHash,
workSum: big.NewInt(2),
status: statusDataStored,
receivedOrderID: 0,
},
nodeB: &blockNode{
hash: *lowerHash,
workSum: big.NewInt(2),
receivedOrderID: 0,
},
wantCmp: 1,
wantBetter: true,
}, {
name: "a has same cumulative work, higher order, lower hash, both data",
nodeA: &blockNode{
hash: *lowerHash,
workSum: big.NewInt(2),
status: statusDataStored,
receivedOrderID: 1,
},
nodeB: &blockNode{
hash: *higherHash,
workSum: big.NewInt(2),
status: statusDataStored,
receivedOrderID: 0,
},
wantCmp: -1,
wantBetter: false,
}, {
name: "a has same cumulative work, lower order, lower hash, both data",
nodeA: &blockNode{
hash: *lowerHash,
workSum: big.NewInt(2),
status: statusDataStored,
receivedOrderID: 1,
},
nodeB: &blockNode{
hash: *higherHash,
workSum: big.NewInt(2),
status: statusDataStored,
receivedOrderID: 2,
},
wantCmp: -1,
wantBetter: true,
}, {
name: "a has same cumulative work, same order, lower hash, no data",
nodeA: &blockNode{
hash: *lowerHash,
workSum: big.NewInt(2),
receivedOrderID: 0,
},
nodeB: &blockNode{
hash: *higherHash,
workSum: big.NewInt(2),
receivedOrderID: 0,
},
wantCmp: -1,
wantBetter: true,
}, {
name: "a has same cumulative work, same order, lower hash, both data",
nodeA: &blockNode{
hash: *lowerHash,
workSum: big.NewInt(2),
status: statusDataStored,
receivedOrderID: 0,
},
nodeB: &blockNode{
hash: *higherHash,
workSum: big.NewInt(2),
status: statusDataStored,
receivedOrderID: 0,
},
wantCmp: -1,
wantBetter: true,
}, {
name: "a has same cumulative work, same order, higher hash, both data",
nodeA: &blockNode{
hash: *higherHash,
workSum: big.NewInt(2),
status: statusDataStored,
receivedOrderID: 0,
},
nodeB: &blockNode{
hash: *lowerHash,
workSum: big.NewInt(2),
status: statusDataStored,
receivedOrderID: 0,
},
wantCmp: 1,
wantBetter: false,
}}
for _, test := range tests {
gotCmp := compareHashesAsUint256LE(&test.nodeA.hash, &test.nodeB.hash)
if gotCmp != test.wantCmp {
t.Fatalf("%q: unexpected result -- got %v, want %v", test.name,
gotCmp, test.wantCmp)
}
gotBetter := betterCandidate(test.nodeA, test.nodeB)
if gotBetter != test.wantBetter {
t.Fatalf("%q: unexpected result -- got %v, want %v", test.name,
gotBetter, test.wantBetter)
}
}
}
// TestShortBlockKeyCollisions ensures the block index handles addition and
// lookup of short key collisions as expected.
func TestShortBlockKeyCollisions(t *testing.T) {
params := chaincfg.RegNetParams()
bc := newFakeChain(params)
genesis := bc.bestChain.Genesis()
// Assert the regression test genesis block hasn't changed because this test
// relies on specific hashes that are specially crafted to force collisions.
// Should this assertion fail in the future, it can be resolved by
// overriding the parameters above to match the expected values.
requiredHash := "2ced94b4ae95bba344cfa043268732d230649c640f92dce2d9518823d3057cb0"
if genesis.hash != *mustParseHash(requiredHash) {
t.Fatalf("this test relies on specific hash values which stem from a "+
"regression test genesis block with hash %s, but it has been "+
"changed to %s", requiredHash, genesis.hash)
}
// assertShortKeyCollision causes a fatal test error if the short key for
// the two provided nodes is not the same.
assertShortKeyCollision := func(node1, node2 *blockNode) {
t.Helper()
node1Key := shortBlockKey(&node1.hash)
node2Key := shortBlockKey(&node2.hash)
if node1Key != node2Key {
t.Fatalf("test data did not produce a key collision: %d != %d",
node1Key, node2Key)
}
}
// assertFullKeyCollision causes a fatal test error if the full key for
// the two provided nodes is not the same.
assertFullKeyCollision := func(node1, node2 *blockNode) {
t.Helper()
if node1.hash != node2.hash {
t.Fatalf("test data did not produce a key collision: %s != %s",
node1.hash, node2.hash)
}
}
// assertLookupResult causes a fatal test error if looking up the provided
// node from the block index does not produce given desired node.
assertLookupResult := func(lookup, want *blockNode) {
t.Helper()
got := bc.index.lookupNode(&lookup.hash)
if got != want {
gotStr, wantStr := "nil", "nil"
if got != nil {
gotStr = fmt.Sprintf("%s (short %x, pointer %p)", got.hash,
shortBlockKey(&got.hash), got)
}
if want != nil {
wantStr = fmt.Sprintf("%s (short %x, pointer %p)", want.hash,
shortBlockKey(&want.hash), want)
}
t.Fatalf("failed to lookup %s (short %x) -- got %s, want %s",
lookup.hash, shortBlockKey(&lookup.hash), gotStr, wantStr)
}
}
// mutatedHeader returns a copy of the header from the passed node modified
// to increase its height and timestamp by one second and to have the
// provided nonce and extra data byte.
mutatedHeader := func(node *blockNode, nonce uint32, extraData byte) *wire.BlockHeader {
headerCopy := node.Header()
headerCopy.Height++
headerCopy.Timestamp = headerCopy.Timestamp.Add(time.Second)
headerCopy.Nonce = nonce
headerCopy.ExtraData[0] = extraData
return &headerCopy
}
// Create a few nodes that do not have a collision with the short key.
nodes := make([]*blockNode, 10)
nodes[0] = newBlockNode(mutatedHeader(genesis, 0x1234, 0x00), genesis)
nodes[1] = newBlockNode(mutatedHeader(nodes[0], 0x1235, 0x00), nodes[0])
nodes[2] = newBlockNode(mutatedHeader(nodes[1], 0x1236, 0x00), nodes[1])
nodes[3] = newBlockNode(mutatedHeader(nodes[2], 0x1237, 0x00), nodes[2])
collisionFreeNodes := nodes[0:4]
// Create some nodes that are specifically crafted to have different full
// hashes that collide with the short key.
nodes[4] = newBlockNode(mutatedHeader(nodes[3], 0x1238, 0x00), nodes[3])
nodes[5] = newBlockNode(mutatedHeader(nodes[4], 0xd322d912, 0x00), nodes[4])
assertShortKeyCollision(nodes[4], nodes[5])
nodes[6] = newBlockNode(mutatedHeader(nodes[4], 0xe10a4cee, 0x01), nodes[4])
assertShortKeyCollision(nodes[4], nodes[6])
collisions := nodes[4:7]
// Create a node that has a full hash collision with one of the previous
// nodes that also has a short key collision with another distinct node.
//
// Since the security properties of the hash function dictate that it is
// computationally infeasible to find such a collision with a different
// preimage, just create a different node with the same data to fake it.
nodes[7] = newBlockNode(mutatedHeader(nodes[4], 0xe10a4cee, 0x01), nodes[4])
assertShortKeyCollision(nodes[6], nodes[7])
assertFullKeyCollision(nodes[6], nodes[7])
doubleCollisions := nodes[6:8]
// Similar to the previous, create nodes that have a full hash collision,
// however, this time for nodes do not have short key collisions with any
// other nodes aside from themselves.
nodes[8] = newBlockNode(mutatedHeader(genesis, 0xabcd, 0x00), genesis)
nodes[9] = newBlockNode(mutatedHeader(genesis, 0xabcd, 0x00), genesis)
assertShortKeyCollision(nodes[8], nodes[9])
assertFullKeyCollision(nodes[8], nodes[9])
fullCollisions := nodes[8:10]
// Ensure none of the nodes are returned when looked up before being added.
for _, node := range nodes {
assertLookupResult(node, nil)
}
// Add the nodes that do not have any collisions in the short or full key to
// the block index and ensure they are looked up as expected. Each one is
// looked up as it's added and then all of them are looked up again once all
// nodes are added for extra assurance.
for _, node := range collisionFreeNodes {
bc.index.addNode(node)
assertLookupResult(node, node)
}
for _, node := range collisionFreeNodes {
assertLookupResult(node, node)
}
// Ensure nodes that have a short key collision with a node that is in the
// index but are not themselves in the index do not return the colliding
// node.
bc.index.addNode(collisions[0])
assertLookupResult(collisions[0], collisions[0])
assertLookupResult(collisions[1], nil)
assertLookupResult(collisions[2], nil)
// Add nodes with short key collisions and ensure looking up the nodes
// returns the expected ones instead of the colliding ones.
bc.index.addNode(collisions[1])
assertLookupResult(collisions[0], collisions[0])
assertLookupResult(collisions[1], collisions[1])
assertLookupResult(collisions[2], nil)
bc.index.addNode(collisions[2])
assertLookupResult(collisions[0], collisions[0])
assertLookupResult(collisions[1], collisions[1])
assertLookupResult(collisions[2], collisions[2])
// Ensure a node that has a full hash that collides with a node that also
// has a short key collision with another node with a distinct hash (in
// other words, the existing nodes have already been moved to the collisions
// map) is replaced with the new one.
assertLookupResult(doubleCollisions[0], doubleCollisions[0])
assertLookupResult(doubleCollisions[1], doubleCollisions[0])
bc.index.addNode(doubleCollisions[1])
assertLookupResult(doubleCollisions[0], doubleCollisions[1])
assertLookupResult(doubleCollisions[1], doubleCollisions[1])
// Ensure a node that has a full hash that collides with a node that does
// not have any other short key collisions (in other words, the existing
// node has NOT already been moved to the collisions map) is replaced with
// the new one.
assertLookupResult(fullCollisions[0], nil)
assertLookupResult(fullCollisions[1], nil)
bc.index.addNode(fullCollisions[0])
assertLookupResult(fullCollisions[0], fullCollisions[0])
assertLookupResult(fullCollisions[1], fullCollisions[0])
bc.index.addNode(fullCollisions[1])
assertLookupResult(fullCollisions[0], fullCollisions[1])
assertLookupResult(fullCollisions[1], fullCollisions[1])
}