mirror of
https://github.com/btcsuite/btcd.git
synced 2024-11-19 18:00:11 +01:00
46bfb784a6
ChainTips method allows for callers to get all the chain tips the node is aware of. This is useful for supporting the getchaintips rpc call.
1158 lines
37 KiB
Go
1158 lines
37 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"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
"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: unxpected 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: unxpected 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: unxpected 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: unxpected 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())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|