// Copyright (c) 2013-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" "math/rand" "reflect" "testing" "time" "github.com/btcsuite/btcd/blockchain/internal/testhelper" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" ) // TestHaveBlock tests the HaveBlock API to ensure proper functionality. func TestHaveBlock(t *testing.T) { // Load up blocks such that there is a side chain. // (genesis block) -> 1 -> 2 -> 3 -> 4 // \-> 3a testFiles := []string{ "blk_0_to_4.dat.bz2", "blk_3A.dat.bz2", } var blocks []*btcutil.Block for _, file := range testFiles { blockTmp, err := loadBlocks(file) if err != nil { t.Errorf("Error loading file: %v\n", err) return } blocks = append(blocks, blockTmp...) } // Create a new database and chain instance to run tests against. chain, teardownFunc, err := chainSetup("haveblock", &chaincfg.MainNetParams) if err != nil { t.Errorf("Failed to setup chain instance: %v", err) return } defer teardownFunc() // Since we're not dealing with the real block chain, set the coinbase // maturity to 1. chain.TstSetCoinbaseMaturity(1) for i := 1; i < len(blocks); i++ { _, isOrphan, err := chain.ProcessBlock(blocks[i], BFNone) if err != nil { t.Errorf("ProcessBlock fail on block %v: %v\n", i, err) return } if isOrphan { t.Errorf("ProcessBlock incorrectly returned block %v "+ "is an orphan\n", i) return } } // Insert an orphan block. _, isOrphan, err := chain.ProcessBlock(btcutil.NewBlock(&Block100000), BFNone) if err != nil { t.Errorf("Unable to process block: %v", err) return } if !isOrphan { t.Errorf("ProcessBlock indicated block is an not orphan when " + "it should be\n") return } tests := []struct { hash string want bool }{ // Genesis block should be present (in the main chain). {hash: chaincfg.MainNetParams.GenesisHash.String(), want: true}, // Block 3a should be present (on a side chain). {hash: "00000000474284d20067a4d33f6a02284e6ef70764a3a26d6a5b9df52ef663dd", want: true}, // Block 100000 should be present (as an orphan). {hash: "000000000003ba27aa200b1cecaad478d2b00432346c3f1f3986da1afd33e506", want: true}, // Random hashes should not be available. {hash: "123", want: false}, } for i, test := range tests { hash, err := chainhash.NewHashFromStr(test.hash) if err != nil { t.Errorf("NewHashFromStr: %v", err) continue } result, err := chain.HaveBlock(hash) if err != nil { t.Errorf("HaveBlock #%d unexpected error: %v", i, err) return } if result != test.want { t.Errorf("HaveBlock #%d got %v want %v", i, result, test.want) continue } } } // TestCalcSequenceLock tests the LockTimeToSequence function, and the // CalcSequenceLock method of a Chain instance. The tests exercise several // combinations of inputs to the CalcSequenceLock function in order to ensure // the returned SequenceLocks are correct for each test instance. func TestCalcSequenceLock(t *testing.T) { netParams := &chaincfg.SimNetParams // We need to activate CSV in order to test the processing logic, so // manually craft the block version that's used to signal the soft-fork // activation. csvBit := netParams.Deployments[chaincfg.DeploymentCSV].BitNumber blockVersion := int32(0x20000000 | (uint32(1) << csvBit)) // Generate enough synthetic blocks to activate CSV. chain := newFakeChain(netParams) node := chain.bestChain.Tip() blockTime := node.Header().Timestamp numBlocksToActivate := (netParams.MinerConfirmationWindow * 3) for i := uint32(0); i < numBlocksToActivate; i++ { blockTime = blockTime.Add(time.Second) node = newFakeNode(node, blockVersion, 0, blockTime) chain.index.AddNode(node) chain.bestChain.SetTip(node) } // Create a utxo view with a fake utxo for the inputs used in the // transactions created below. This utxo is added such that it has an // age of 4 blocks. targetTx := btcutil.NewTx(&wire.MsgTx{ TxOut: []*wire.TxOut{{ PkScript: nil, Value: 10, }}, }) utxoView := NewUtxoViewpoint() utxoView.AddTxOuts(targetTx, int32(numBlocksToActivate)-4) utxoView.SetBestHash(&node.hash) // Create a utxo that spends the fake utxo created above for use in the // transactions created in the tests. It has an age of 4 blocks. Note // that the sequence lock heights are always calculated from the same // point of view that they were originally calculated from for a given // utxo. That is to say, the height prior to it. utxo := wire.OutPoint{ Hash: *targetTx.Hash(), Index: 0, } prevUtxoHeight := int32(numBlocksToActivate) - 4 // Obtain the median time past from the PoV of the input created above. // The MTP for the input is the MTP from the PoV of the block *prior* // to the one that included it. medianTime := CalcPastMedianTime(node.RelativeAncestor(5)).Unix() // The median time calculated from the PoV of the best block in the // test chain. For unconfirmed inputs, this value will be used since // the MTP will be calculated from the PoV of the yet-to-be-mined // block. nextMedianTime := CalcPastMedianTime(node).Unix() nextBlockHeight := int32(numBlocksToActivate) + 1 // Add an additional transaction which will serve as our unconfirmed // output. unConfTx := &wire.MsgTx{ TxOut: []*wire.TxOut{{ PkScript: nil, Value: 5, }}, } unConfUtxo := wire.OutPoint{ Hash: unConfTx.TxHash(), Index: 0, } // Adding a utxo with a height of 0x7fffffff indicates that the output // is currently unmined. utxoView.AddTxOuts(btcutil.NewTx(unConfTx), 0x7fffffff) tests := []struct { tx *wire.MsgTx view *UtxoViewpoint mempool bool want *SequenceLock }{ // A transaction of version one should disable sequence locks // as the new sequence number semantics only apply to // transactions version 2 or higher. { tx: &wire.MsgTx{ Version: 1, TxIn: []*wire.TxIn{{ PreviousOutPoint: utxo, Sequence: LockTimeToSequence(false, 3), }}, }, view: utxoView, want: &SequenceLock{ Seconds: -1, BlockHeight: -1, }, }, // A transaction with a single input with max sequence number. // This sequence number has the high bit set, so sequence locks // should be disabled. { tx: &wire.MsgTx{ Version: 2, TxIn: []*wire.TxIn{{ PreviousOutPoint: utxo, Sequence: wire.MaxTxInSequenceNum, }}, }, view: utxoView, want: &SequenceLock{ Seconds: -1, BlockHeight: -1, }, }, // A transaction with a single input whose lock time is // expressed in seconds. However, the specified lock time is // below the required floor for time based lock times since // they have time granularity of 512 seconds. As a result, the // seconds lock-time should be just before the median time of // the targeted block. { tx: &wire.MsgTx{ Version: 2, TxIn: []*wire.TxIn{{ PreviousOutPoint: utxo, Sequence: LockTimeToSequence(true, 2), }}, }, view: utxoView, want: &SequenceLock{ Seconds: medianTime - 1, BlockHeight: -1, }, }, // A transaction with a single input whose lock time is // expressed in seconds. The number of seconds should be 1023 // seconds after the median past time of the last block in the // chain. { tx: &wire.MsgTx{ Version: 2, TxIn: []*wire.TxIn{{ PreviousOutPoint: utxo, Sequence: LockTimeToSequence(true, 1024), }}, }, view: utxoView, want: &SequenceLock{ Seconds: medianTime + 1023, BlockHeight: -1, }, }, // A transaction with multiple inputs. The first input has a // lock time expressed in seconds. The second input has a // sequence lock in blocks with a value of 4. The last input // has a sequence number with a value of 5, but has the disable // bit set. So the first lock should be selected as it's the // latest lock that isn't disabled. { tx: &wire.MsgTx{ Version: 2, TxIn: []*wire.TxIn{{ PreviousOutPoint: utxo, Sequence: LockTimeToSequence(true, 2560), }, { PreviousOutPoint: utxo, Sequence: LockTimeToSequence(false, 4), }, { PreviousOutPoint: utxo, Sequence: LockTimeToSequence(false, 5) | wire.SequenceLockTimeDisabled, }}, }, view: utxoView, want: &SequenceLock{ Seconds: medianTime + (5 << wire.SequenceLockTimeGranularity) - 1, BlockHeight: prevUtxoHeight + 3, }, }, // Transaction with a single input. The input's sequence number // encodes a relative lock-time in blocks (3 blocks). The // sequence lock should have a value of -1 for seconds, but a // height of 2 meaning it can be included at height 3. { tx: &wire.MsgTx{ Version: 2, TxIn: []*wire.TxIn{{ PreviousOutPoint: utxo, Sequence: LockTimeToSequence(false, 3), }}, }, view: utxoView, want: &SequenceLock{ Seconds: -1, BlockHeight: prevUtxoHeight + 2, }, }, // A transaction with two inputs with lock times expressed in // seconds. The selected sequence lock value for seconds should // be the time further in the future. { tx: &wire.MsgTx{ Version: 2, TxIn: []*wire.TxIn{{ PreviousOutPoint: utxo, Sequence: LockTimeToSequence(true, 5120), }, { PreviousOutPoint: utxo, Sequence: LockTimeToSequence(true, 2560), }}, }, view: utxoView, want: &SequenceLock{ Seconds: medianTime + (10 << wire.SequenceLockTimeGranularity) - 1, BlockHeight: -1, }, }, // A transaction with two inputs with lock times expressed in // blocks. The selected sequence lock value for blocks should // be the height further in the future, so a height of 10 // indicating it can be included at height 11. { tx: &wire.MsgTx{ Version: 2, TxIn: []*wire.TxIn{{ PreviousOutPoint: utxo, Sequence: LockTimeToSequence(false, 1), }, { PreviousOutPoint: utxo, Sequence: LockTimeToSequence(false, 11), }}, }, view: utxoView, want: &SequenceLock{ Seconds: -1, BlockHeight: prevUtxoHeight + 10, }, }, // A transaction with multiple inputs. Two inputs are time // based, and the other two are block based. The lock lying // further into the future for both inputs should be chosen. { tx: &wire.MsgTx{ Version: 2, TxIn: []*wire.TxIn{{ PreviousOutPoint: utxo, Sequence: LockTimeToSequence(true, 2560), }, { PreviousOutPoint: utxo, Sequence: LockTimeToSequence(true, 6656), }, { PreviousOutPoint: utxo, Sequence: LockTimeToSequence(false, 3), }, { PreviousOutPoint: utxo, Sequence: LockTimeToSequence(false, 9), }}, }, view: utxoView, want: &SequenceLock{ Seconds: medianTime + (13 << wire.SequenceLockTimeGranularity) - 1, BlockHeight: prevUtxoHeight + 8, }, }, // A transaction with a single unconfirmed input. As the input // is confirmed, the height of the input should be interpreted // as the height of the *next* block. So, a 2 block relative // lock means the sequence lock should be for 1 block after the // *next* block height, indicating it can be included 2 blocks // after that. { tx: &wire.MsgTx{ Version: 2, TxIn: []*wire.TxIn{{ PreviousOutPoint: unConfUtxo, Sequence: LockTimeToSequence(false, 2), }}, }, view: utxoView, mempool: true, want: &SequenceLock{ Seconds: -1, BlockHeight: nextBlockHeight + 1, }, }, // A transaction with a single unconfirmed input. The input has // a time based lock, so the lock time should be based off the // MTP of the *next* block. { tx: &wire.MsgTx{ Version: 2, TxIn: []*wire.TxIn{{ PreviousOutPoint: unConfUtxo, Sequence: LockTimeToSequence(true, 1024), }}, }, view: utxoView, mempool: true, want: &SequenceLock{ Seconds: nextMedianTime + 1023, BlockHeight: -1, }, }, } t.Logf("Running %v SequenceLock tests", len(tests)) for i, test := range tests { utilTx := btcutil.NewTx(test.tx) seqLock, err := chain.CalcSequenceLock(utilTx, test.view, test.mempool) if err != nil { t.Fatalf("test #%d, unable to calc sequence lock: %v", i, err) } if seqLock.Seconds != test.want.Seconds { t.Fatalf("test #%d got %v seconds want %v seconds", i, seqLock.Seconds, test.want.Seconds) } if seqLock.BlockHeight != test.want.BlockHeight { t.Fatalf("test #%d got height of %v want height of %v ", i, seqLock.BlockHeight, test.want.BlockHeight) } } } // nodeHashes is a convenience function that returns the hashes for all of the // passed indexes of the provided nodes. It is used to construct expected hash // slices in the tests. func nodeHashes(nodes []*blockNode, indexes ...int) []chainhash.Hash { hashes := make([]chainhash.Hash, 0, len(indexes)) for _, idx := range indexes { hashes = append(hashes, nodes[idx].hash) } return hashes } // nodeHeaders is a convenience function that returns the headers for all of // the passed indexes of the provided nodes. It is used to construct expected // located headers in the tests. func nodeHeaders(nodes []*blockNode, indexes ...int) []wire.BlockHeader { headers := make([]wire.BlockHeader, 0, len(indexes)) for _, idx := range indexes { headers = append(headers, nodes[idx].Header()) } return headers } // TestLocateInventory ensures that locating inventory via the LocateHeaders and // LocateBlocks functions behaves as expected. func TestLocateInventory(t *testing.T) { // Construct a synthetic block chain with a block index consisting of // the following structure. // genesis -> 1 -> 2 -> ... -> 15 -> 16 -> 17 -> 18 // \-> 16a -> 17a tip := tstTip chain := newFakeChain(&chaincfg.MainNetParams) branch0Nodes := chainedNodes(chain.bestChain.Genesis(), 18) branch1Nodes := chainedNodes(branch0Nodes[14], 2) for _, node := range branch0Nodes { chain.index.AddNode(node) } for _, node := range branch1Nodes { chain.index.AddNode(node) } chain.bestChain.SetTip(tip(branch0Nodes)) // Create chain views for different branches of the overall chain to // simulate a local and remote node on different parts of the chain. localView := newChainView(tip(branch0Nodes)) remoteView := newChainView(tip(branch1Nodes)) // Create a chain view for a completely unrelated block chain to // simulate a remote node on a totally different chain. unrelatedBranchNodes := chainedNodes(nil, 5) unrelatedView := newChainView(tip(unrelatedBranchNodes)) tests := []struct { name string locator BlockLocator // locator for requested inventory hashStop chainhash.Hash // stop hash for locator maxAllowed uint32 // max to locate, 0 = wire const headers []wire.BlockHeader // expected located headers hashes []chainhash.Hash // expected located hashes }{ { // Empty block locators and unknown stop hash. No // inventory should be located. name: "no locators, no stop", locator: nil, hashStop: chainhash.Hash{}, headers: nil, hashes: nil, }, { // Empty block locators and stop hash in side chain. // The expected result is the requested block. name: "no locators, stop in side", locator: nil, hashStop: tip(branch1Nodes).hash, headers: nodeHeaders(branch1Nodes, 1), hashes: nodeHashes(branch1Nodes, 1), }, { // Empty block locators and stop hash in main chain. // The expected result is the requested block. name: "no locators, stop in main", locator: nil, hashStop: branch0Nodes[12].hash, headers: nodeHeaders(branch0Nodes, 12), hashes: nodeHashes(branch0Nodes, 12), }, { // Locators based on remote being on side chain and a // stop hash local node doesn't know about. The // expected result is the blocks after the fork point in // the main chain and the stop hash has no effect. name: "remote side chain, unknown stop", locator: remoteView.BlockLocator(nil), hashStop: chainhash.Hash{0x01}, headers: nodeHeaders(branch0Nodes, 15, 16, 17), hashes: nodeHashes(branch0Nodes, 15, 16, 17), }, { // Locators based on remote being on side chain and a // stop hash in side chain. The expected result is the // blocks after the fork point in the main chain and the // stop hash has no effect. name: "remote side chain, stop in side", locator: remoteView.BlockLocator(nil), hashStop: tip(branch1Nodes).hash, headers: nodeHeaders(branch0Nodes, 15, 16, 17), hashes: nodeHashes(branch0Nodes, 15, 16, 17), }, { // Locators based on remote being on side chain and a // stop hash in main chain, but before fork point. The // expected result is the blocks after the fork point in // the main chain and the stop hash has no effect. name: "remote side chain, stop in main before", locator: remoteView.BlockLocator(nil), hashStop: branch0Nodes[13].hash, headers: nodeHeaders(branch0Nodes, 15, 16, 17), hashes: nodeHashes(branch0Nodes, 15, 16, 17), }, { // Locators based on remote being on side chain and a // stop hash in main chain, but exactly at the fork // point. The expected result is the blocks after the // fork point in the main chain and the stop hash has no // effect. name: "remote side chain, stop in main exact", locator: remoteView.BlockLocator(nil), hashStop: branch0Nodes[14].hash, headers: nodeHeaders(branch0Nodes, 15, 16, 17), hashes: nodeHashes(branch0Nodes, 15, 16, 17), }, { // Locators based on remote being on side chain and a // stop hash in main chain just after the fork point. // The expected result is the blocks after the fork // point in the main chain up to and including the stop // hash. name: "remote side chain, stop in main after", locator: remoteView.BlockLocator(nil), hashStop: branch0Nodes[15].hash, headers: nodeHeaders(branch0Nodes, 15), hashes: nodeHashes(branch0Nodes, 15), }, { // Locators based on remote being on side chain and a // stop hash in main chain some time after the fork // point. The expected result is the blocks after the // fork point in the main chain up to and including the // stop hash. name: "remote side chain, stop in main after more", locator: remoteView.BlockLocator(nil), hashStop: branch0Nodes[16].hash, headers: nodeHeaders(branch0Nodes, 15, 16), hashes: nodeHashes(branch0Nodes, 15, 16), }, { // Locators based on remote being on main chain in the // past and a stop hash local node doesn't know about. // The expected result is the blocks after the known // point in the main chain and the stop hash has no // effect. name: "remote main chain past, unknown stop", locator: localView.BlockLocator(branch0Nodes[12]), hashStop: chainhash.Hash{0x01}, headers: nodeHeaders(branch0Nodes, 13, 14, 15, 16, 17), hashes: nodeHashes(branch0Nodes, 13, 14, 15, 16, 17), }, { // Locators based on remote being on main chain in the // past and a stop hash in a side chain. The expected // result is the blocks after the known point in the // main chain and the stop hash has no effect. name: "remote main chain past, stop in side", locator: localView.BlockLocator(branch0Nodes[12]), hashStop: tip(branch1Nodes).hash, headers: nodeHeaders(branch0Nodes, 13, 14, 15, 16, 17), hashes: nodeHashes(branch0Nodes, 13, 14, 15, 16, 17), }, { // Locators based on remote being on main chain in the // past and a stop hash in the main chain before that // point. The expected result is the blocks after the // known point in the main chain and the stop hash has // no effect. name: "remote main chain past, stop in main before", locator: localView.BlockLocator(branch0Nodes[12]), hashStop: branch0Nodes[11].hash, headers: nodeHeaders(branch0Nodes, 13, 14, 15, 16, 17), hashes: nodeHashes(branch0Nodes, 13, 14, 15, 16, 17), }, { // Locators based on remote being on main chain in the // past and a stop hash in the main chain exactly at that // point. The expected result is the blocks after the // known point in the main chain and the stop hash has // no effect. name: "remote main chain past, stop in main exact", locator: localView.BlockLocator(branch0Nodes[12]), hashStop: branch0Nodes[12].hash, headers: nodeHeaders(branch0Nodes, 13, 14, 15, 16, 17), hashes: nodeHashes(branch0Nodes, 13, 14, 15, 16, 17), }, { // Locators based on remote being on main chain in the // past and a stop hash in the main chain just after // that point. The expected result is the blocks after // the known point in the main chain and the stop hash // has no effect. name: "remote main chain past, stop in main after", locator: localView.BlockLocator(branch0Nodes[12]), hashStop: branch0Nodes[13].hash, headers: nodeHeaders(branch0Nodes, 13), hashes: nodeHashes(branch0Nodes, 13), }, { // Locators based on remote being on main chain in the // past and a stop hash in the main chain some time // after that point. The expected result is the blocks // after the known point in the main chain and the stop // hash has no effect. name: "remote main chain past, stop in main after more", locator: localView.BlockLocator(branch0Nodes[12]), hashStop: branch0Nodes[15].hash, headers: nodeHeaders(branch0Nodes, 13, 14, 15), hashes: nodeHashes(branch0Nodes, 13, 14, 15), }, { // Locators based on remote being at exactly the same // point in the main chain and a stop hash local node // doesn't know about. The expected result is no // located inventory. name: "remote main chain same, unknown stop", locator: localView.BlockLocator(nil), hashStop: chainhash.Hash{0x01}, headers: nil, hashes: nil, }, { // Locators based on remote being at exactly the same // point in the main chain and a stop hash at exactly // the same point. The expected result is no located // inventory. name: "remote main chain same, stop same point", locator: localView.BlockLocator(nil), hashStop: tip(branch0Nodes).hash, headers: nil, hashes: nil, }, { // Locators from remote that don't include any blocks // the local node knows. This would happen if the // remote node is on a completely separate chain that // isn't rooted with the same genesis block. The // expected result is the blocks after the genesis // block. name: "remote unrelated chain", locator: unrelatedView.BlockLocator(nil), hashStop: chainhash.Hash{}, headers: nodeHeaders(branch0Nodes, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17), hashes: nodeHashes(branch0Nodes, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17), }, { // Locators from remote for second block in main chain // and no stop hash, but with an overridden max limit. // The expected result is the blocks after the second // block limited by the max. name: "remote genesis", locator: locatorHashes(branch0Nodes, 0), hashStop: chainhash.Hash{}, maxAllowed: 3, headers: nodeHeaders(branch0Nodes, 1, 2, 3), hashes: nodeHashes(branch0Nodes, 1, 2, 3), }, { // Poorly formed locator. // // Locator from remote that only includes a single // block on a side chain the local node knows. The // expected result is the blocks after the genesis // block since even though the block is known, it is on // a side chain and there are no more locators to find // the fork point. name: "weak locator, single known side block", locator: locatorHashes(branch1Nodes, 1), hashStop: chainhash.Hash{}, headers: nodeHeaders(branch0Nodes, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17), hashes: nodeHashes(branch0Nodes, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17), }, { // Poorly formed locator. // // Locator from remote that only includes multiple // blocks on a side chain the local node knows however // none in the main chain. The expected result is the // blocks after the genesis block since even though the // blocks are known, they are all on a side chain and // there are no more locators to find the fork point. name: "weak locator, multiple known side blocks", locator: locatorHashes(branch1Nodes, 1), hashStop: chainhash.Hash{}, headers: nodeHeaders(branch0Nodes, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17), hashes: nodeHashes(branch0Nodes, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17), }, { // Poorly formed locator. // // Locator from remote that only includes multiple // blocks on a side chain the local node knows however // none in the main chain but includes a stop hash in // the main chain. The expected result is the blocks // after the genesis block up to the stop hash since // even though the blocks are known, they are all on a // side chain and there are no more locators to find the // fork point. name: "weak locator, multiple known side blocks, stop in main", locator: locatorHashes(branch1Nodes, 1), hashStop: branch0Nodes[5].hash, headers: nodeHeaders(branch0Nodes, 0, 1, 2, 3, 4, 5), hashes: nodeHashes(branch0Nodes, 0, 1, 2, 3, 4, 5), }, } for _, test := range tests { // Ensure the expected headers are located. var headers []wire.BlockHeader if test.maxAllowed != 0 { // Need to use the unexported function to override the // max allowed for headers. chain.chainLock.RLock() headers = chain.locateHeaders(test.locator, &test.hashStop, test.maxAllowed) chain.chainLock.RUnlock() } else { headers = chain.LocateHeaders(test.locator, &test.hashStop) } if !reflect.DeepEqual(headers, test.headers) { t.Errorf("%s: unexpected headers -- got %v, want %v", test.name, headers, test.headers) continue } // Ensure the expected block hashes are located. maxAllowed := uint32(wire.MaxBlocksPerMsg) if test.maxAllowed != 0 { maxAllowed = test.maxAllowed } hashes := chain.LocateBlocks(test.locator, &test.hashStop, maxAllowed) if !reflect.DeepEqual(hashes, test.hashes) { t.Errorf("%s: unexpected hashes -- got %v, want %v", test.name, hashes, test.hashes) continue } } } // TestHeightToHashRange ensures that fetching a range of block hashes by start // height and end hash works as expected. func TestHeightToHashRange(t *testing.T) { // Construct a synthetic block chain with a block index consisting of // the following structure. // genesis -> 1 -> 2 -> ... -> 15 -> 16 -> 17 -> 18 // \-> 16a -> 17a -> 18a (unvalidated) tip := tstTip chain := newFakeChain(&chaincfg.MainNetParams) branch0Nodes := chainedNodes(chain.bestChain.Genesis(), 18) branch1Nodes := chainedNodes(branch0Nodes[14], 3) for _, node := range branch0Nodes { chain.index.SetStatusFlags(node, statusValid) chain.index.AddNode(node) } for _, node := range branch1Nodes { if node.height < 18 { chain.index.SetStatusFlags(node, statusValid) } chain.index.AddNode(node) } chain.bestChain.SetTip(tip(branch0Nodes)) tests := []struct { name string startHeight int32 // locator for requested inventory endHash chainhash.Hash // stop hash for locator maxResults int // max to locate, 0 = wire const hashes []chainhash.Hash // expected located hashes expectError bool }{ { name: "blocks below tip", startHeight: 11, endHash: branch0Nodes[14].hash, maxResults: 10, hashes: nodeHashes(branch0Nodes, 10, 11, 12, 13, 14), }, { name: "blocks on main chain", startHeight: 15, endHash: branch0Nodes[17].hash, maxResults: 10, hashes: nodeHashes(branch0Nodes, 14, 15, 16, 17), }, { name: "blocks on stale chain", startHeight: 15, endHash: branch1Nodes[1].hash, maxResults: 10, hashes: append(nodeHashes(branch0Nodes, 14), nodeHashes(branch1Nodes, 0, 1)...), }, { name: "invalid start height", startHeight: 19, endHash: branch0Nodes[17].hash, maxResults: 10, expectError: true, }, { name: "too many results", startHeight: 1, endHash: branch0Nodes[17].hash, maxResults: 10, expectError: true, }, { name: "unvalidated block", startHeight: 15, endHash: branch1Nodes[2].hash, maxResults: 10, expectError: true, }, } for _, test := range tests { hashes, err := chain.HeightToHashRange(test.startHeight, &test.endHash, test.maxResults) if err != nil { if !test.expectError { t.Errorf("%s: unexpected error: %v", test.name, err) } continue } if !reflect.DeepEqual(hashes, test.hashes) { t.Errorf("%s: unexpected hashes -- got %v, want %v", test.name, hashes, test.hashes) } } } // TestIntervalBlockHashes ensures that fetching block hashes at specified // intervals by end hash works as expected. func TestIntervalBlockHashes(t *testing.T) { // Construct a synthetic block chain with a block index consisting of // the following structure. // genesis -> 1 -> 2 -> ... -> 15 -> 16 -> 17 -> 18 // \-> 16a -> 17a -> 18a (unvalidated) tip := tstTip chain := newFakeChain(&chaincfg.MainNetParams) branch0Nodes := chainedNodes(chain.bestChain.Genesis(), 18) branch1Nodes := chainedNodes(branch0Nodes[14], 3) for _, node := range branch0Nodes { chain.index.SetStatusFlags(node, statusValid) chain.index.AddNode(node) } for _, node := range branch1Nodes { if node.height < 18 { chain.index.SetStatusFlags(node, statusValid) } chain.index.AddNode(node) } chain.bestChain.SetTip(tip(branch0Nodes)) tests := []struct { name string endHash chainhash.Hash interval int hashes []chainhash.Hash expectError bool }{ { name: "blocks on main chain", endHash: branch0Nodes[17].hash, interval: 8, hashes: nodeHashes(branch0Nodes, 7, 15), }, { name: "blocks on stale chain", endHash: branch1Nodes[1].hash, interval: 8, hashes: append(nodeHashes(branch0Nodes, 7), nodeHashes(branch1Nodes, 0)...), }, { name: "no results", endHash: branch0Nodes[17].hash, interval: 20, hashes: []chainhash.Hash{}, }, { name: "unvalidated block", endHash: branch1Nodes[2].hash, interval: 8, expectError: true, }, } for _, test := range tests { hashes, err := chain.IntervalBlockHashes(&test.endHash, test.interval) if err != nil { if !test.expectError { t.Errorf("%s: unexpected error: %v", test.name, err) } continue } if !reflect.DeepEqual(hashes, test.hashes) { t.Errorf("%s: unexpected hashes -- got %v, want %v", test.name, hashes, test.hashes) } } } func TestChainTips(t *testing.T) { tests := []struct { name string chainTipGen func() (*BlockChain, map[chainhash.Hash]ChainTip) }{ { name: "one active chain tip", chainTipGen: func() (*BlockChain, map[chainhash.Hash]ChainTip) { // Construct a synthetic block chain with a block index consisting of // the following structure. // genesis -> 1 -> 2 -> 3 tip := tstTip chain := newFakeChain(&chaincfg.MainNetParams) branch0Nodes := chainedNodes(chain.bestChain.Genesis(), 3) for _, node := range branch0Nodes { chain.index.SetStatusFlags(node, statusDataStored) chain.index.SetStatusFlags(node, statusValid) chain.index.AddNode(node) } chain.bestChain.SetTip(tip(branch0Nodes)) activeTip := ChainTip{ Height: 3, BlockHash: (tip(branch0Nodes)).hash, BranchLen: 0, Status: StatusActive, } chainTips := make(map[chainhash.Hash]ChainTip) chainTips[activeTip.BlockHash] = activeTip return chain, chainTips }, }, { name: "one active chain tip, one unknown chain tip", chainTipGen: func() (*BlockChain, map[chainhash.Hash]ChainTip) { // Construct a synthetic block chain with a block index consisting of // the following structure. // genesis -> 1 -> 2 -> 3 ... -> 10 -> 11 -> 12 -> 13 (active) // \-> 11a -> 12a (unknown) tip := tstTip chain := newFakeChain(&chaincfg.MainNetParams) branch0Nodes := chainedNodes(chain.bestChain.Genesis(), 13) for _, node := range branch0Nodes { chain.index.SetStatusFlags(node, statusDataStored) chain.index.SetStatusFlags(node, statusValid) chain.index.AddNode(node) } chain.bestChain.SetTip(tip(branch0Nodes)) branch1Nodes := chainedNodes(branch0Nodes[9], 2) for _, node := range branch1Nodes { chain.index.AddNode(node) } activeTip := ChainTip{ Height: 13, BlockHash: (tip(branch0Nodes)).hash, BranchLen: 0, Status: StatusActive, } unknownTip := ChainTip{ Height: 12, BlockHash: (tip(branch1Nodes)).hash, BranchLen: 2, Status: StatusUnknown, } chainTips := make(map[chainhash.Hash]ChainTip) chainTips[activeTip.BlockHash] = activeTip chainTips[unknownTip.BlockHash] = unknownTip return chain, chainTips }, }, { name: "1 inactive tip, 1 invalid tip, 1 active tip", chainTipGen: func() (*BlockChain, map[chainhash.Hash]ChainTip) { // Construct a synthetic block chain with a block index consisting of // the following structure. // genesis -> 1 -> 2 -> 3 (active) // \ -> 1a (valid-fork) // \ -> 1b (invalid) tip := tstTip chain := newFakeChain(&chaincfg.MainNetParams) branch0Nodes := chainedNodes(chain.bestChain.Genesis(), 3) for _, node := range branch0Nodes { chain.index.SetStatusFlags(node, statusDataStored) chain.index.SetStatusFlags(node, statusValid) chain.index.AddNode(node) } chain.bestChain.SetTip(tip(branch0Nodes)) branch1Nodes := chainedNodes(chain.bestChain.Genesis(), 1) for _, node := range branch1Nodes { chain.index.SetStatusFlags(node, statusDataStored) chain.index.SetStatusFlags(node, statusValid) chain.index.AddNode(node) } branch2Nodes := chainedNodes(chain.bestChain.Genesis(), 1) for _, node := range branch2Nodes { chain.index.SetStatusFlags(node, statusDataStored) chain.index.SetStatusFlags(node, statusValidateFailed) chain.index.AddNode(node) } activeTip := ChainTip{ Height: tip(branch0Nodes).height, BlockHash: (tip(branch0Nodes)).hash, BranchLen: 0, Status: StatusActive, } inactiveTip := ChainTip{ Height: tip(branch1Nodes).height, BlockHash: (tip(branch1Nodes)).hash, BranchLen: 1, Status: StatusValidFork, } invalidTip := ChainTip{ Height: tip(branch2Nodes).height, BlockHash: (tip(branch2Nodes)).hash, BranchLen: 1, Status: StatusInvalid, } chainTips := make(map[chainhash.Hash]ChainTip) chainTips[activeTip.BlockHash] = activeTip chainTips[inactiveTip.BlockHash] = inactiveTip chainTips[invalidTip.BlockHash] = invalidTip return chain, chainTips }, }, } for _, test := range tests { chain, expectedChainTips := test.chainTipGen() gotChainTips := chain.ChainTips() if len(gotChainTips) != len(expectedChainTips) { t.Errorf("TestChainTips Failed test %s. Expected %d "+ "chain tips, got %d", test.name, len(expectedChainTips), len(gotChainTips)) } for _, gotChainTip := range gotChainTips { testChainTip, found := expectedChainTips[gotChainTip.BlockHash] if !found { t.Errorf("TestChainTips Failed test %s. Couldn't find an expected "+ "chain tip with height %d, hash %s, branchlen %d, status \"%s\"", test.name, testChainTip.Height, testChainTip.BlockHash.String(), testChainTip.BranchLen, testChainTip.Status.String()) } if !reflect.DeepEqual(testChainTip, gotChainTip) { t.Errorf("TestChainTips Failed test %s. Expected chain tip with "+ "height %d, hash %s, branchlen %d, status \"%s\" but got "+ "height %d, hash %s, branchlen %d, status \"%s\"", test.name, testChainTip.Height, testChainTip.BlockHash.String(), testChainTip.BranchLen, testChainTip.Status.String(), gotChainTip.Height, gotChainTip.BlockHash.String(), gotChainTip.BranchLen, gotChainTip.Status.String()) } switch testChainTip.Status { case StatusActive: if testChainTip.Status.String() != "active" { t.Errorf("TestChainTips Fail: Expected string of \"active\", got \"%s\"", testChainTip.Status.String()) } case StatusInvalid: if testChainTip.Status.String() != "invalid" { t.Errorf("TestChainTips Fail: Expected string of \"invalid\", got \"%s\"", testChainTip.Status.String()) } case StatusValidFork: if testChainTip.Status.String() != "valid-fork" { t.Errorf("TestChainTips Fail: Expected string of \"valid-fork\", got \"%s\"", testChainTip.Status.String()) } case StatusUnknown: if testChainTip.Status.String() != fmt.Sprintf("unknown: %b", testChainTip.Status) { t.Errorf("TestChainTips Fail: Expected string of \"unknown\", got \"%s\"", testChainTip.Status.String()) } } } } } func TestIsAncestor(t *testing.T) { // Construct a synthetic block chain with a block index consisting of // the following structure. // genesis -> 1 -> 2 -> 3 (active) // \ -> 1a (valid-fork) // \ -> 1b (invalid) tip := tstTip chain := newFakeChain(&chaincfg.MainNetParams) branch0Nodes := chainedNodes(chain.bestChain.Genesis(), 3) for _, node := range branch0Nodes { chain.index.SetStatusFlags(node, statusDataStored) chain.index.SetStatusFlags(node, statusValid) chain.index.AddNode(node) } chain.bestChain.SetTip(tip(branch0Nodes)) branch1Nodes := chainedNodes(chain.bestChain.Genesis(), 1) for _, node := range branch1Nodes { chain.index.SetStatusFlags(node, statusDataStored) chain.index.SetStatusFlags(node, statusValid) chain.index.AddNode(node) } branch2Nodes := chainedNodes(chain.bestChain.Genesis(), 1) for _, node := range branch2Nodes { chain.index.SetStatusFlags(node, statusDataStored) chain.index.SetStatusFlags(node, statusValidateFailed) chain.index.AddNode(node) } // Is 1 an ancestor of 3? // // genesis -> 1 -> 2 -> 3 (active) // \ -> 1a (valid-fork) // \ -> 1b (invalid) shouldBeTrue := branch0Nodes[2].IsAncestor(branch0Nodes[0]) if !shouldBeTrue { t.Errorf("TestIsAncestor fail. Node %s is an ancestor of node %s but got false", branch0Nodes[0].hash.String(), branch0Nodes[2].hash.String()) } // Is 1 an ancestor of 2? // // genesis -> 1 -> 2 -> 3 (active) // \ -> 1a (valid-fork) // \ -> 1b (invalid) shouldBeTrue = branch0Nodes[1].IsAncestor(branch0Nodes[0]) if !shouldBeTrue { t.Errorf("TestIsAncestor fail. Node %s is an ancestor of node %s but got false", branch0Nodes[0].hash.String(), branch0Nodes[1].hash.String()) } // Is the genesis an ancestor of 1? // // genesis -> 1 -> 2 -> 3 (active) // \ -> 1a (valid-fork) // \ -> 1b (invalid) shouldBeTrue = branch0Nodes[0].IsAncestor(chain.bestChain.Genesis()) if !shouldBeTrue { t.Errorf("TestIsAncestor fail. The genesis block is an ancestor of all blocks "+ "but got false for node %s", branch0Nodes[0].hash.String()) } // Is the genesis an ancestor of 1a? // // genesis -> 1 -> 2 -> 3 (active) // \ -> 1a (valid-fork) // \ -> 1b (invalid) shouldBeTrue = branch1Nodes[0].IsAncestor(chain.bestChain.Genesis()) if !shouldBeTrue { t.Errorf("TestIsAncestor fail. The genesis block is an ancestor of all blocks "+ "but got false for node %s", branch1Nodes[0].hash.String()) } // Is the genesis an ancestor of 1b? // // genesis -> 1 -> 2 -> 3 (active) // \ -> 1a (valid-fork) // \ -> 1b (invalid) shouldBeTrue = branch2Nodes[0].IsAncestor(chain.bestChain.Genesis()) if !shouldBeTrue { t.Errorf("TestIsAncestor fail. The genesis block is an ancestor of all blocks "+ "but got false for node %s", branch2Nodes[0].hash.String()) } // Is 1 an ancestor of 1a? // // genesis -> 1 -> 2 -> 3 (active) // \ -> 1a (valid-fork) // \ -> 1b (invalid) shouldBeFalse := branch1Nodes[0].IsAncestor(branch0Nodes[0]) if shouldBeFalse { t.Errorf("TestIsAncestor fail. Node %s is in a different branch than "+ "node %s but got true", branch1Nodes[0].hash.String(), branch0Nodes[0].hash.String()) } // Is 1 an ancestor of 1b? // // genesis -> 1 -> 2 -> 3 (active) // \ -> 1a (valid-fork) // \ -> 1b (invalid) shouldBeFalse = branch2Nodes[0].IsAncestor(branch0Nodes[0]) if shouldBeFalse { t.Errorf("TestIsAncestor fail. Node %s is in a different branch than "+ "node %s but got true", branch2Nodes[0].hash.String(), branch0Nodes[0].hash.String()) } // Is 1a an ancestor of 1b? // // genesis -> 1 -> 2 -> 3 (active) // \ -> 1a (valid-fork) // \ -> 1b (invalid) shouldBeFalse = branch2Nodes[0].IsAncestor(branch1Nodes[0]) if shouldBeFalse { t.Errorf("TestIsAncestor fail. Node %s is in a different branch than "+ "node %s but got true", branch2Nodes[0].hash.String(), branch1Nodes[0].hash.String()) } // Is 1 an ancestor of 1? // // genesis -> 1 -> 2 -> 3 (active) // \ -> 1a (valid-fork) // \ -> 1b (invalid) shouldBeFalse = branch0Nodes[0].IsAncestor(branch0Nodes[0]) if shouldBeFalse { t.Errorf("TestIsAncestor fail. Node is not an ancestor of itself but got true for node %s", branch0Nodes[0].hash.String()) } // Is the geneis an ancestor of genesis? // // genesis -> 1 -> 2 -> 3 (active) // \ -> 1a (valid-fork) // \ -> 1b (invalid) shouldBeFalse = chain.bestChain.Genesis().IsAncestor(chain.bestChain.Genesis()) if shouldBeFalse { t.Errorf("TestIsAncestor fail. Node is not an ancestor of itself but got true for node %s", chain.bestChain.Genesis().hash.String()) } // Is a block from another chain an ancestor of 1b? fakeChain := newFakeChain(&chaincfg.TestNet3Params) shouldBeFalse = branch2Nodes[0].IsAncestor(fakeChain.bestChain.Genesis()) if shouldBeFalse { t.Errorf("TestIsAncestor fail. Node %s is in a different chain than "+ "node %s but got true", fakeChain.bestChain.Genesis().hash.String(), branch2Nodes[0].hash.String()) } } // randomSelect selects random amount of random elements from a slice and returns a // new slice. The selected elements are removed. func randomSelect(input []*testhelper.SpendableOut) ( []*testhelper.SpendableOut, []*testhelper.SpendableOut) { selected := []*testhelper.SpendableOut{} // Select random elements from the input slice amount := rand.Intn(len(input)) for i := 0; i < amount; i++ { // Generate a random index randIdx := rand.Intn(len(input)) // Append the selected element to the new slice selected = append(selected, input[randIdx]) // Remove the selected element from the input slice. // This ensures that each selected element is unique. input = append(input[:randIdx], input[randIdx+1:]...) } return input, selected } // addBlocks generates new blocks and adds them to the chain. The newly generated // blocks will spend from the spendable outputs passed in. The returned hases are // the hashes of the newly generated blocks. func addBlocks(count int, chain *BlockChain, prevBlock *btcutil.Block, allSpendableOutputs []*testhelper.SpendableOut) ( []*chainhash.Hash, [][]*testhelper.SpendableOut, error) { blockHashes := make([]*chainhash.Hash, 0, count) spendablesOuts := make([][]*testhelper.SpendableOut, 0, count) // Always spend everything on the first block. This ensures we get unique blocks // every time. The random select may choose not to spend any and that results // in getting the same block. nextSpends := allSpendableOutputs allSpendableOutputs = allSpendableOutputs[:0] for b := 0; b < count; b++ { newBlock, newSpendableOuts, err := addBlock(chain, prevBlock, nextSpends) if err != nil { return nil, nil, err } prevBlock = newBlock blockHashes = append(blockHashes, newBlock.Hash()) spendablesOuts = append(spendablesOuts, newSpendableOuts) allSpendableOutputs = append(allSpendableOutputs, newSpendableOuts...) // Grab utxos to be spent in the next block. allSpendableOutputs, nextSpends = randomSelect(allSpendableOutputs) } return blockHashes, spendablesOuts, nil } func TestInvalidateBlock(t *testing.T) { tests := []struct { name string chainGen func() (*BlockChain, []*chainhash.Hash, func()) }{ { name: "one branch, invalidate once", chainGen: func() (*BlockChain, []*chainhash.Hash, func()) { chain, params, tearDown := utxoCacheTestChain( "TestInvalidateBlock-one-branch-" + "invalidate-once") // Grab the tip of the chain. tip := btcutil.NewBlock(params.GenesisBlock) // Create a chain with 11 blocks. _, _, err := addBlocks(11, chain, tip, []*testhelper.SpendableOut{}) if err != nil { t.Fatal(err) } // Invalidate block 5. block, err := chain.BlockByHeight(5) if err != nil { t.Fatal(err) } invalidateHash := block.Hash() return chain, []*chainhash.Hash{invalidateHash}, tearDown }, }, { name: "invalidate twice", chainGen: func() (*BlockChain, []*chainhash.Hash, func()) { chain, params, tearDown := utxoCacheTestChain("TestInvalidateBlock-invalidate-twice") // Grab the tip of the chain. tip := btcutil.NewBlock(params.GenesisBlock) // Create a chain with 11 blocks. _, spendableOuts, err := addBlocks(11, chain, tip, []*testhelper.SpendableOut{}) //_, _, err := addBlocks(11, chain, tip, []*testhelper.SpendableOut{}) if err != nil { t.Fatal(err) } // Set invalidateHash as block 5. block, err := chain.BlockByHeight(5) if err != nil { t.Fatal(err) } invalidateHash := block.Hash() // Create a side chain with 7 blocks that builds on block 1. b1, err := chain.BlockByHeight(1) if err != nil { t.Fatal(err) } altBlockHashes, _, err := addBlocks(6, chain, b1, spendableOuts[0]) if err != nil { t.Fatal(err) } // Grab block at height 5: // // b2, b3, b4, b5 // 0, 1, 2, 3 invalidateHash2 := altBlockHashes[3] // Sanity checking that we grabbed the correct hash. node := chain.index.LookupNode(invalidateHash) if node == nil || node.height != 5 { t.Fatalf("wanted to grab block at height 5 but got height %v", node.height) } return chain, []*chainhash.Hash{invalidateHash, invalidateHash2}, tearDown }, }, { name: "invalidate a side branch", chainGen: func() (*BlockChain, []*chainhash.Hash, func()) { chain, params, tearDown := utxoCacheTestChain("TestInvalidateBlock-invalidate-side-branch") tip := btcutil.NewBlock(params.GenesisBlock) // Grab the tip of the chain. tip, err := chain.BlockByHash(&chain.bestChain.Tip().hash) if err != nil { t.Fatal(err) } // Create a chain with 11 blocks. _, spendableOuts, err := addBlocks(11, chain, tip, []*testhelper.SpendableOut{}) if err != nil { t.Fatal(err) } // Create a side chain with 7 blocks that builds on block 1. b1, err := chain.BlockByHeight(1) if err != nil { t.Fatal(err) } altBlockHashes, _, err := addBlocks(6, chain, b1, spendableOuts[0]) if err != nil { t.Fatal(err) } // Grab block at height 4: // // b2, b3, b4 // 0, 1, 2 invalidateHash := altBlockHashes[2] // Sanity checking that we grabbed the correct hash. node := chain.index.LookupNode(invalidateHash) if node == nil || node.height != 4 { t.Fatalf("wanted to grab block at height 4 but got height %v", node.height) } return chain, []*chainhash.Hash{invalidateHash}, tearDown }, }, } for _, test := range tests { chain, invalidateHashes, tearDown := test.chainGen() func() { defer tearDown() for _, invalidateHash := range invalidateHashes { chainTipsBefore := chain.ChainTips() // Mark if we're invalidating a block that's a part of the best chain. var bestChainBlock bool node := chain.index.LookupNode(invalidateHash) if chain.bestChain.Contains(node) { bestChainBlock = true } // Actual invalidation. err := chain.InvalidateBlock(invalidateHash) if err != nil { t.Fatal(err) } chainTipsAfter := chain.ChainTips() // Create a map for easy lookup. chainTipMap := make(map[chainhash.Hash]ChainTip, len(chainTipsAfter)) activeTipCount := 0 for _, chainTip := range chainTipsAfter { chainTipMap[chainTip.BlockHash] = chainTip if chainTip.Status == StatusActive { activeTipCount++ } } if activeTipCount != 1 { t.Fatalf("TestInvalidateBlock fail. Expected "+ "1 active chain tip but got %d", activeTipCount) } bestTip := chain.bestChain.Tip() validForkCount := 0 for _, tip := range chainTipsBefore { // If the chaintip was an active tip and we invalidated a block // in the active tip, assert that it's invalid now. if bestChainBlock && tip.Status == StatusActive { gotTip, found := chainTipMap[tip.BlockHash] if !found { t.Fatalf("TestInvalidateBlock fail. Expected "+ "block %s not found in chaintips after "+ "invalidateblock", tip.BlockHash.String()) } if gotTip.Status != StatusInvalid { t.Fatalf("TestInvalidateBlock fail. "+ "Expected block %s to be invalid, got status: %s", gotTip.BlockHash.String(), gotTip.Status) } } if !bestChainBlock && tip.Status != StatusActive { gotTip, found := chainTipMap[tip.BlockHash] if !found { t.Fatalf("TestInvalidateBlock fail. Expected "+ "block %s not found in chaintips after "+ "invalidateblock", tip.BlockHash.String()) } if gotTip.BlockHash == *invalidateHash && gotTip.Status != StatusInvalid { t.Fatalf("TestInvalidateBlock fail. "+ "Expected block %s to be invalid, got status: %s", gotTip.BlockHash.String(), gotTip.Status) } } // If we're not invalidating the branch with an active tip, // we expect the active tip to remain the same. if !bestChainBlock && tip.Status == StatusActive && tip.BlockHash != bestTip.hash { t.Fatalf("TestInvalidateBlock fail. Expected block %s as the tip but got %s", tip.BlockHash.String(), bestTip.hash.String()) } // If this tip is not invalid and not active, it should be // lighter than the current best tip. if tip.Status != StatusActive && tip.Status != StatusInvalid && tip.Height > bestTip.height { tipNode := chain.index.LookupNode(&tip.BlockHash) if bestTip.workSum.Cmp(tipNode.workSum) == -1 { t.Fatalf("TestInvalidateBlock fail. Expected "+ "block %s to be the active tip but block %s "+ "was", tipNode.hash.String(), bestTip.hash.String()) } } if tip.Status == StatusValidFork { validForkCount++ } } // If there are no other valid chain tips besides the active chaintip, // we expect to have one more chain tip after the invalidate. if validForkCount == 0 && len(chainTipsAfter) != len(chainTipsBefore)+1 { t.Fatalf("TestInvalidateBlock fail. Expected %d chaintips but got %d", len(chainTipsBefore)+1, len(chainTipsAfter)) } } // Try to invaliate the already invalidated hash. err := chain.InvalidateBlock(invalidateHashes[0]) if err != nil { t.Fatal(err) } // Try to invaliate a genesis block err = chain.InvalidateBlock(chain.chainParams.GenesisHash) if err == nil { t.Fatalf("TestInvalidateBlock fail. Expected to err when trying to" + "invalidate a genesis block.") } // Try to invaliate a block that doesn't exist. err = chain.InvalidateBlock(chaincfg.MainNetParams.GenesisHash) if err == nil { t.Fatalf("TestInvalidateBlock fail. Expected to err when trying to" + "invalidate a block that doesn't exist.") } }() } } func TestReconsiderBlock(t *testing.T) { tests := []struct { name string chainGen func() (*BlockChain, []*chainhash.Hash, func()) }{ { name: "one branch, invalidate once and revalidate", chainGen: func() (*BlockChain, []*chainhash.Hash, func()) { chain, params, tearDown := utxoCacheTestChain("TestInvalidateBlock-one-branch-invalidate-once") // Create a chain with 101 blocks. tip := btcutil.NewBlock(params.GenesisBlock) _, _, err := addBlocks(101, chain, tip, []*testhelper.SpendableOut{}) if err != nil { t.Fatal(err) } // Invalidate block 5. block, err := chain.BlockByHeight(5) if err != nil { t.Fatal(err) } invalidateHash := block.Hash() return chain, []*chainhash.Hash{invalidateHash}, tearDown }, }, { name: "invalidate the active branch with a side branch present and revalidate", chainGen: func() (*BlockChain, []*chainhash.Hash, func()) { chain, params, tearDown := utxoCacheTestChain("TestReconsiderBlock-invalidate-with-side-branch") // Create a chain with 101 blocks. tip := btcutil.NewBlock(params.GenesisBlock) _, spendableOuts, err := addBlocks(101, chain, tip, []*testhelper.SpendableOut{}) if err != nil { t.Fatal(err) } // Invalidate block 5. block, err := chain.BlockByHeight(5) if err != nil { t.Fatal(err) } invalidateHash := block.Hash() // Create a side chain with 7 blocks that builds on block 1. b1, err := chain.BlockByHeight(1) if err != nil { t.Fatal(err) } _, _, err = addBlocks(6, chain, b1, spendableOuts[0]) if err != nil { t.Fatal(err) } return chain, []*chainhash.Hash{invalidateHash}, tearDown }, }, { name: "invalidate a side branch and revalidate it", chainGen: func() (*BlockChain, []*chainhash.Hash, func()) { chain, params, tearDown := utxoCacheTestChain("TestReconsiderBlock-invalidate-a-side-branch") // Create a chain with 101 blocks. tip := btcutil.NewBlock(params.GenesisBlock) _, spendableOuts, err := addBlocks(101, chain, tip, []*testhelper.SpendableOut{}) if err != nil { t.Fatal(err) } // Create a side chain with 7 blocks that builds on block 1. b1, err := chain.BlockByHeight(1) if err != nil { t.Fatal(err) } altBlockHashes, _, err := addBlocks(6, chain, b1, spendableOuts[0]) if err != nil { t.Fatal(err) } // Grab block at height 4: // // b2, b3, b4, b5 // 0, 1, 2, 3 invalidateHash := altBlockHashes[2] return chain, []*chainhash.Hash{invalidateHash}, tearDown }, }, { name: "reconsider an invalid side branch with a higher work", chainGen: func() (*BlockChain, []*chainhash.Hash, func()) { chain, params, tearDown := utxoCacheTestChain("TestReconsiderBlock-reconsider-an-invalid-side-branch-higher") tip := btcutil.NewBlock(params.GenesisBlock) _, spendableOuts, err := addBlocks(6, chain, tip, []*testhelper.SpendableOut{}) if err != nil { t.Fatal(err) } // Select utxos to be spent from the best block and // modify the amount so that the block will be invalid. nextSpends, _ := randomSelect(spendableOuts[len(spendableOuts)-1]) nextSpends[0].Amount += testhelper.LowFee // Make an invalid block that best on top of the current tip. bestBlock, err := chain.BlockByHash(&chain.BestSnapshot().Hash) if err != nil { t.Fatal(err) } invalidBlock, _, _ := newBlock(chain, bestBlock, nextSpends) invalidateHash := invalidBlock.Hash() // The block validation will fail here and we'll mark the // block as invalid in the block index. chain.ProcessBlock(invalidBlock, BFNone) // Modify the amount again so it's valid. nextSpends[0].Amount -= testhelper.LowFee return chain, []*chainhash.Hash{invalidateHash}, tearDown }, }, { name: "reconsider an invalid side branch with a lower work", chainGen: func() (*BlockChain, []*chainhash.Hash, func()) { chain, params, tearDown := utxoCacheTestChain("TestReconsiderBlock-reconsider-an-invalid-side-branch-lower") tip := btcutil.NewBlock(params.GenesisBlock) _, spendableOuts, err := addBlocks(6, chain, tip, []*testhelper.SpendableOut{}) if err != nil { t.Fatal(err) } // Select utxos to be spent from the best block and // modify the amount so that the block will be invalid. nextSpends, _ := randomSelect(spendableOuts[len(spendableOuts)-1]) nextSpends[0].Amount += testhelper.LowFee // Make an invalid block that best on top of the current tip. bestBlock, err := chain.BlockByHash(&chain.BestSnapshot().Hash) if err != nil { t.Fatal(err) } invalidBlock, _, _ := newBlock(chain, bestBlock, nextSpends) invalidateHash := invalidBlock.Hash() // The block validation will fail here and we'll mark the // block as invalid in the block index. chain.ProcessBlock(invalidBlock, BFNone) // Modify the amount again so it's valid. nextSpends[0].Amount -= testhelper.LowFee // Add more blocks to make the invalid block a // side chain and not the most pow. _, _, err = addBlocks(3, chain, bestBlock, []*testhelper.SpendableOut{}) if err != nil { t.Fatal(err) } return chain, []*chainhash.Hash{invalidateHash}, tearDown }, }, } for _, test := range tests { chain, invalidateHashes, tearDown := test.chainGen() func() { defer tearDown() for _, invalidateHash := range invalidateHashes { // Cache the chain tips before the invalidate. Since we'll reconsider // the invalidated block, we should come back to these tips in the end. tips := chain.ChainTips() expectedChainTips := make(map[chainhash.Hash]ChainTip, len(tips)) for _, tip := range tips { expectedChainTips[tip.BlockHash] = tip } // Invalidation. err := chain.InvalidateBlock(invalidateHash) if err != nil { t.Fatal(err) } // Reconsideration. err = chain.ReconsiderBlock(invalidateHash) if err != nil { t.Fatal(err) } // Compare the tips aginst the tips we've cached. gotChainTips := chain.ChainTips() for _, gotChainTip := range gotChainTips { testChainTip, found := expectedChainTips[gotChainTip.BlockHash] if !found { t.Errorf("TestReconsiderBlock Failed test \"%s\". Couldn't find an expected "+ "chain tip with height %d, hash %s, branchlen %d, status \"%s\"", test.name, testChainTip.Height, testChainTip.BlockHash.String(), testChainTip.BranchLen, testChainTip.Status.String()) } // If the invalid side branch is a lower work, we'll never // actually process the block again until the branch becomes // a greater work chain so it'll show up as valid-fork. if test.name == "reconsider an invalid side branch with a lower work" && testChainTip.BlockHash == *invalidateHash { testChainTip.Status = StatusValidFork } if !reflect.DeepEqual(testChainTip, gotChainTip) { t.Errorf("TestReconsiderBlock Failed test \"%s\". Expected chain tip with "+ "height %d, hash %s, branchlen %d, status \"%s\" but got "+ "height %d, hash %s, branchlen %d, status \"%s\"", test.name, testChainTip.Height, testChainTip.BlockHash.String(), testChainTip.BranchLen, testChainTip.Status.String(), gotChainTip.Height, gotChainTip.BlockHash.String(), gotChainTip.BranchLen, gotChainTip.Status.String()) } } } }() } }