mirror of
https://github.com/btcsuite/btcd.git
synced 2024-11-19 09:50:08 +01:00
52a8a2a06e
ReconsiderBlock reconsiders the validity of the block for the passed in blockhash. The behavior of the function mimics that of Bitcoin Core. The invalid status of the block nodes are reset and if the chaintip that is being reconsidered has more cumulative work, then we'll validate the blocks and reorganize to it. If the cumulative work is lesser than the current active chain tip, then nothing else will be done.
1851 lines
59 KiB
Go
1851 lines
59 KiB
Go
// 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())
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
}
|