mirror of
https://github.com/btcsuite/btcd.git
synced 2026-08-17 13:06:26 +02:00
1270 lines
36 KiB
Go
1270 lines
36 KiB
Go
package netsync
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"math"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/btcsuite/btcd/blockchain"
|
|
"github.com/btcsuite/btcd/btcutil/v2"
|
|
"github.com/btcsuite/btcd/chaincfg/v2"
|
|
"github.com/btcsuite/btcd/chainhash/v2"
|
|
"github.com/btcsuite/btcd/database"
|
|
_ "github.com/btcsuite/btcd/database/ffldb"
|
|
"github.com/btcsuite/btcd/mempool"
|
|
"github.com/btcsuite/btcd/peer"
|
|
"github.com/btcsuite/btcd/txscript/v2"
|
|
"github.com/btcsuite/btcd/wire/v2"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// The package-level log variable is nil by default. Set it to the
|
|
// disabled logger so that log calls in the sync manager don't panic.
|
|
func init() {
|
|
DisableLog()
|
|
}
|
|
|
|
// noopPeerNotifier is a no-op implementation of PeerNotifier for tests.
|
|
type noopPeerNotifier struct{}
|
|
|
|
func (noopPeerNotifier) AnnounceNewTransactions([]*mempool.TxDesc) {}
|
|
func (noopPeerNotifier) UpdatePeerHeights(*chainhash.Hash, int32, *peer.Peer) {}
|
|
func (noopPeerNotifier) RelayInventory(*wire.InvVect, interface{}) {}
|
|
func (noopPeerNotifier) TransactionConfirmed(*btcutil.Tx) {}
|
|
|
|
// dbSetup is used to create a new db with the genesis block already inserted.
|
|
// It returns a teardown function the caller should invoke when done testing to
|
|
// clean up. The database is stored under t.TempDir() which is automatically
|
|
// removed when the test finishes.
|
|
func dbSetup(t *testing.T, params *chaincfg.Params) (database.DB, func(), error) {
|
|
dbPath := filepath.Join(t.TempDir(), "ffldb")
|
|
db, err := database.Create("ffldb", dbPath, params.Net)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("error creating db: %v", err)
|
|
}
|
|
|
|
teardown := func() {
|
|
db.Close()
|
|
}
|
|
|
|
return db, teardown, nil
|
|
}
|
|
|
|
// chainSetup is used to create a new db and chain instance with the genesis
|
|
// block already inserted. In addition to the new chain instance, it returns
|
|
// a teardown function the caller should invoke when done testing to clean up.
|
|
func chainSetup(t *testing.T, params *chaincfg.Params) (
|
|
*blockchain.BlockChain, func(), error) {
|
|
|
|
db, teardown, err := dbSetup(t, params)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// Copy the chain params to ensure any modifications the tests do to
|
|
// the chain parameters do not affect the global instance.
|
|
paramsCopy := *params
|
|
|
|
// Deep-copy deployment starters/enders so that parallel tests don't
|
|
// race on the shared blockClock field written by SynchronizeClock.
|
|
for i := range paramsCopy.Deployments {
|
|
d := ¶msCopy.Deployments[i]
|
|
if s, ok := d.DeploymentStarter.(*chaincfg.MedianTimeDeploymentStarter); ok {
|
|
d.DeploymentStarter = chaincfg.NewMedianTimeDeploymentStarter(
|
|
s.StartTime())
|
|
}
|
|
if e, ok := d.DeploymentEnder.(*chaincfg.MedianTimeDeploymentEnder); ok {
|
|
d.DeploymentEnder = chaincfg.NewMedianTimeDeploymentEnder(
|
|
e.EndTime())
|
|
}
|
|
}
|
|
|
|
// Create the main chain instance.
|
|
chain, err := blockchain.New(&blockchain.Config{
|
|
DB: db,
|
|
Checkpoints: paramsCopy.Checkpoints,
|
|
ChainParams: ¶msCopy,
|
|
TimeSource: blockchain.NewMedianTime(),
|
|
SigCache: txscript.NewSigCache(1000),
|
|
})
|
|
if err != nil {
|
|
teardown()
|
|
err := fmt.Errorf("failed to create chain instance: %v", err)
|
|
return nil, nil, err
|
|
}
|
|
return chain, teardown, nil
|
|
}
|
|
|
|
// loadHeaders loads headers from mainnet from 1 to 11.
|
|
func loadHeaders(t *testing.T) []*wire.BlockHeader {
|
|
testFile := "blockheaders-mainnet-1-11.txt"
|
|
filename := filepath.Join("testdata/", testFile)
|
|
|
|
file, err := os.Open(filename)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
headers := make([]*wire.BlockHeader, 0, 10)
|
|
scanner := bufio.NewScanner(file)
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
b, err := hex.DecodeString(line)
|
|
if err != nil {
|
|
t.Fatalf("failed to read block headers from file %v", testFile)
|
|
}
|
|
|
|
r := bytes.NewReader(b)
|
|
header := new(wire.BlockHeader)
|
|
header.Deserialize(r)
|
|
|
|
headers = append(headers, header)
|
|
}
|
|
|
|
return headers
|
|
}
|
|
|
|
func makeMockSyncManager(t *testing.T,
|
|
params *chaincfg.Params) (*SyncManager, func()) {
|
|
|
|
t.Helper()
|
|
|
|
chain, tearDown, err := chainSetup(t, params)
|
|
require.NoError(t, err)
|
|
|
|
sm, err := New(&Config{
|
|
PeerNotifier: noopPeerNotifier{},
|
|
Chain: chain,
|
|
ChainParams: params,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
return sm, tearDown
|
|
}
|
|
|
|
func TestCheckHeadersList(t *testing.T) {
|
|
// Set params to mainnet with a checkpoint at block 11.
|
|
params := chaincfg.MainNetParams
|
|
checkpointHeight := int32(11)
|
|
checkpointHash, err := chainhash.NewHashFromStr(
|
|
"0000000097be56d606cdd9c54b04d4747e957d3608abe69198c661f2add73073")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
params.Checkpoints = []chaincfg.Checkpoint{
|
|
{
|
|
Height: checkpointHeight,
|
|
Hash: checkpointHash,
|
|
},
|
|
}
|
|
|
|
// Create mock SyncManager.
|
|
sm, tearDown := makeMockSyncManager(t, ¶ms)
|
|
defer tearDown()
|
|
|
|
// Setup SyncManager with headers processed.
|
|
headers := loadHeaders(t)
|
|
for _, header := range headers {
|
|
isMainChain, err := sm.chain.ProcessBlockHeader(
|
|
header, blockchain.BFNone, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if !isMainChain {
|
|
t.Fatalf("expected block header %v to be in the main chain",
|
|
header.BlockHash())
|
|
}
|
|
}
|
|
|
|
tests := []struct {
|
|
hash string
|
|
isCheckpointBlock bool
|
|
behaviorFlags blockchain.BehaviorFlags
|
|
}{
|
|
{
|
|
hash: chaincfg.MainNetParams.GenesisHash.String(),
|
|
isCheckpointBlock: false,
|
|
behaviorFlags: blockchain.BFFastAdd,
|
|
},
|
|
{
|
|
// Block 10.
|
|
hash: "000000002c05cc2e78923c34df87fd108b22221ac6076c18f3ade378a4d915e9",
|
|
isCheckpointBlock: false,
|
|
behaviorFlags: blockchain.BFFastAdd,
|
|
},
|
|
{
|
|
// Block 11.
|
|
hash: "0000000097be56d606cdd9c54b04d4747e957d3608abe69198c661f2add73073",
|
|
isCheckpointBlock: true,
|
|
behaviorFlags: blockchain.BFFastAdd,
|
|
},
|
|
{
|
|
// Block 12.
|
|
hash: "0000000027c2488e2510d1acf4369787784fa20ee084c258b58d9fbd43802b5e",
|
|
isCheckpointBlock: false,
|
|
behaviorFlags: blockchain.BFNone,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
hash, err := chainhash.NewHashFromStr(test.hash)
|
|
if err != nil {
|
|
t.Errorf("NewHashFromStr: %v", err)
|
|
continue
|
|
}
|
|
|
|
// Make sure that when the ibd mode is off, we always get
|
|
// false and BFNone.
|
|
sm.ibdMode = false
|
|
isCheckpoint, gotFlags := sm.checkHeadersList(hash)
|
|
require.Equal(t, false, isCheckpoint)
|
|
require.Equal(t, blockchain.BFNone, gotFlags)
|
|
|
|
// Now check that the test values are correct.
|
|
sm.ibdMode = true
|
|
isCheckpoint, gotFlags = sm.checkHeadersList(hash)
|
|
require.Equal(t, test.isCheckpointBlock, isCheckpoint)
|
|
require.Equal(t, test.behaviorFlags, gotFlags)
|
|
}
|
|
}
|
|
|
|
func TestFetchHigherPeers(t *testing.T) {
|
|
// Create mock SyncManager.
|
|
sm, tearDown := makeMockSyncManager(t, &chaincfg.MainNetParams)
|
|
defer tearDown()
|
|
|
|
tests := []struct {
|
|
peerHeights []int32
|
|
peerSyncCandidate []bool
|
|
height int32
|
|
expectedCnt int
|
|
}{
|
|
{
|
|
peerHeights: []int32{9, 10, 10, 10},
|
|
peerSyncCandidate: []bool{true, true, true, true},
|
|
height: 5,
|
|
expectedCnt: 4,
|
|
},
|
|
|
|
{
|
|
peerHeights: []int32{9, 10, 10, 10},
|
|
peerSyncCandidate: []bool{false, false, true, true},
|
|
height: 5,
|
|
expectedCnt: 2,
|
|
},
|
|
|
|
{
|
|
peerHeights: []int32{1, 100, 100, 100, 100},
|
|
peerSyncCandidate: []bool{true, false, true, true, false},
|
|
height: 100,
|
|
expectedCnt: 0,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
// Setup peers.
|
|
sm.peerStates = make(map[*peer.Peer]*peerSyncState)
|
|
for i, height := range test.peerHeights {
|
|
peer := peer.NewInboundPeer(&peer.Config{})
|
|
peer.UpdateLastBlockHeight(height)
|
|
sm.peerStates[peer] = &peerSyncState{
|
|
syncCandidate: test.peerSyncCandidate[i],
|
|
requestedTxns: make(map[chainhash.Hash]struct{}),
|
|
requestedBlocks: make(map[chainhash.Hash]struct{}),
|
|
}
|
|
}
|
|
|
|
// Fetch higher peers and assert.
|
|
peers := sm.fetchHigherPeers(test.height)
|
|
require.Equal(t, test.expectedCnt, len(peers))
|
|
|
|
for _, peer := range peers {
|
|
state, found := sm.peerStates[peer]
|
|
require.True(t, found)
|
|
require.True(t, state.syncCandidate)
|
|
}
|
|
}
|
|
}
|
|
|
|
// mockTimeSource is used to trick the BlockChain instance to think that we're
|
|
// in the past. This is so that we can force it to return true for isCurrent().
|
|
type mockTimeSource struct {
|
|
adjustedTime time.Time
|
|
}
|
|
|
|
// AdjustedTime returns the internal adjustedTime.
|
|
//
|
|
// Part of the MedianTimeSource interface implementation.
|
|
func (m *mockTimeSource) AdjustedTime() time.Time {
|
|
return m.adjustedTime
|
|
}
|
|
|
|
// AddTimeSample isn't relevant so we just leave it as emtpy.
|
|
//
|
|
// Part of the MedianTimeSource interface implementation.
|
|
func (m *mockTimeSource) AddTimeSample(id string, timeVal time.Time) {
|
|
// purposely left empty
|
|
}
|
|
|
|
// Offset isn't relevant so we just return 0.
|
|
//
|
|
// Part of the MedianTimeSource interface implementation.
|
|
func (m *mockTimeSource) Offset() time.Duration {
|
|
return 0
|
|
}
|
|
|
|
// TestBuildBlockRequestSkipsInflightBlocks verifies that buildBlockRequest
|
|
// does not re-request blocks that are already in sm.requestedBlocks. When
|
|
// the pipeline refill threshold triggers fetchHeaderBlocks while blocks are
|
|
// still in-flight, re-requesting them causes the peer to send duplicates.
|
|
// The first copy gets processed (removing the hash from requestedBlocks),
|
|
// and the second copy then arrives as "unrequested", disconnecting the peer.
|
|
func TestBuildBlockRequestSkipsInflightBlocks(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
// inflightHeights are the block heights already in
|
|
// requestedBlocks before calling buildBlockRequest.
|
|
inflightHeights []int32
|
|
// wantRequestedHeights are the block heights that should
|
|
// appear in the returned getdata message.
|
|
wantRequestedHeights []int32
|
|
}{
|
|
{
|
|
name: "no blocks in-flight requests all",
|
|
inflightHeights: nil,
|
|
wantRequestedHeights: []int32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11},
|
|
},
|
|
{
|
|
name: "all blocks in-flight requests none",
|
|
inflightHeights: []int32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11},
|
|
wantRequestedHeights: nil,
|
|
},
|
|
{
|
|
name: "first 5 in-flight requests remaining 6",
|
|
inflightHeights: []int32{1, 2, 3, 4, 5},
|
|
wantRequestedHeights: []int32{6, 7, 8, 9, 10, 11},
|
|
},
|
|
{
|
|
name: "last 6 in-flight requests first 5",
|
|
inflightHeights: []int32{6, 7, 8, 9, 10, 11},
|
|
wantRequestedHeights: []int32{1, 2, 3, 4, 5},
|
|
},
|
|
{
|
|
name: "scattered in-flight requests gaps",
|
|
inflightHeights: []int32{2, 4, 6, 8, 10},
|
|
wantRequestedHeights: []int32{1, 3, 5, 7, 9, 11},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
params := chaincfg.MainNetParams
|
|
params.Checkpoints = nil
|
|
sm, tearDown := makeMockSyncManager(t, ¶ms)
|
|
defer tearDown()
|
|
|
|
// Process headers 1-11 so the header chain is
|
|
// ahead of the block chain.
|
|
headers := loadHeaders(t)
|
|
for _, header := range headers {
|
|
_, err := sm.chain.ProcessBlockHeader(
|
|
header, blockchain.BFNone, false)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Set up a disconnected peer as syncPeer.
|
|
syncPeer := peer.NewInboundPeer(&peer.Config{})
|
|
sm.syncPeer = syncPeer
|
|
syncPeerState := &peerSyncState{
|
|
requestedTxns: make(map[chainhash.Hash]struct{}),
|
|
requestedBlocks: make(map[chainhash.Hash]struct{}),
|
|
}
|
|
sm.peerStates[syncPeer] = syncPeerState
|
|
|
|
// Pre-populate in-flight blocks.
|
|
for _, h := range tc.inflightHeights {
|
|
hash, err := sm.chain.HeaderHashByHeight(h)
|
|
require.NoError(t, err)
|
|
sm.requestedBlocks[*hash] = struct{}{}
|
|
syncPeerState.requestedBlocks[*hash] = struct{}{}
|
|
}
|
|
|
|
gdmsg := sm.buildBlockRequest(syncPeer)
|
|
|
|
// Collect the hashes from the getdata message.
|
|
got := make(map[chainhash.Hash]struct{}, len(gdmsg.InvList))
|
|
for _, iv := range gdmsg.InvList {
|
|
got[iv.Hash] = struct{}{}
|
|
}
|
|
|
|
require.Equal(t, len(tc.wantRequestedHeights), len(gdmsg.InvList))
|
|
for _, h := range tc.wantRequestedHeights {
|
|
hash, err := sm.chain.HeaderHashByHeight(h)
|
|
require.NoError(t, err)
|
|
require.Contains(t, got, *hash,
|
|
"block at height %d should be requested", h)
|
|
}
|
|
for _, h := range tc.inflightHeights {
|
|
hash, err := sm.chain.HeaderHashByHeight(h)
|
|
require.NoError(t, err)
|
|
require.NotContains(t, got, *hash,
|
|
"in-flight block at height %d should not be re-requested", h)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsInIBDMode(t *testing.T) {
|
|
tests := []struct {
|
|
peerState map[*peer.Peer]*peerSyncState
|
|
params *chaincfg.Params
|
|
timesource *mockTimeSource
|
|
isIBDMode bool
|
|
}{
|
|
// Is not current, higher peers.
|
|
{
|
|
params: &chaincfg.MainNetParams,
|
|
peerState: func() map[*peer.Peer]*peerSyncState {
|
|
ps := make(map[*peer.Peer]*peerSyncState)
|
|
peer := peer.NewInboundPeer(&peer.Config{})
|
|
peer.UpdateLastBlockHeight(900_000)
|
|
ps[peer] = &peerSyncState{
|
|
syncCandidate: true,
|
|
requestedTxns: make(map[chainhash.Hash]struct{}),
|
|
requestedBlocks: make(map[chainhash.Hash]struct{}),
|
|
}
|
|
return ps
|
|
}(),
|
|
timesource: nil,
|
|
isIBDMode: true,
|
|
},
|
|
// Is not current, no higher peers.
|
|
{
|
|
params: &chaincfg.MainNetParams,
|
|
peerState: func() map[*peer.Peer]*peerSyncState {
|
|
ps := make(map[*peer.Peer]*peerSyncState)
|
|
peer := peer.NewInboundPeer(&peer.Config{})
|
|
peer.UpdateLastBlockHeight(0)
|
|
ps[peer] = &peerSyncState{
|
|
syncCandidate: true,
|
|
requestedTxns: make(map[chainhash.Hash]struct{}),
|
|
requestedBlocks: make(map[chainhash.Hash]struct{}),
|
|
}
|
|
return ps
|
|
}(),
|
|
timesource: nil,
|
|
isIBDMode: true,
|
|
},
|
|
// Is current, higher peers.
|
|
{
|
|
params: func() *chaincfg.Params {
|
|
params := chaincfg.MainNetParams
|
|
params.Checkpoints = nil
|
|
return ¶ms
|
|
}(),
|
|
peerState: func() map[*peer.Peer]*peerSyncState {
|
|
ps := make(map[*peer.Peer]*peerSyncState)
|
|
peer := peer.NewInboundPeer(&peer.Config{})
|
|
peer.UpdateLastBlockHeight(900_000)
|
|
ps[peer] = &peerSyncState{
|
|
syncCandidate: true,
|
|
requestedTxns: make(map[chainhash.Hash]struct{}),
|
|
requestedBlocks: make(map[chainhash.Hash]struct{}),
|
|
}
|
|
return ps
|
|
}(),
|
|
timesource: &mockTimeSource{
|
|
chaincfg.MainNetParams.GenesisBlock.Header.Timestamp,
|
|
},
|
|
isIBDMode: true,
|
|
},
|
|
// Is current, no higher peers.
|
|
{
|
|
params: func() *chaincfg.Params {
|
|
params := chaincfg.MainNetParams
|
|
params.Checkpoints = nil
|
|
return ¶ms
|
|
}(),
|
|
peerState: func() map[*peer.Peer]*peerSyncState {
|
|
ps := make(map[*peer.Peer]*peerSyncState)
|
|
peer := peer.NewInboundPeer(&peer.Config{})
|
|
peer.UpdateLastBlockHeight(0)
|
|
ps[peer] = &peerSyncState{
|
|
syncCandidate: true,
|
|
requestedTxns: make(map[chainhash.Hash]struct{}),
|
|
requestedBlocks: make(map[chainhash.Hash]struct{}),
|
|
}
|
|
return ps
|
|
}(),
|
|
timesource: &mockTimeSource{
|
|
chaincfg.MainNetParams.GenesisBlock.Header.Timestamp,
|
|
},
|
|
isIBDMode: false,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
db, tearDown, err := dbSetup(t, test.params)
|
|
if err != nil {
|
|
tearDown()
|
|
t.Fatal(err)
|
|
}
|
|
|
|
timesource := blockchain.NewMedianTime()
|
|
if test.timesource != nil {
|
|
timesource = test.timesource
|
|
}
|
|
|
|
// Create the main chain instance.
|
|
chain, err := blockchain.New(&blockchain.Config{
|
|
DB: db,
|
|
Checkpoints: test.params.Checkpoints,
|
|
ChainParams: test.params,
|
|
TimeSource: timesource,
|
|
SigCache: txscript.NewSigCache(1000),
|
|
})
|
|
if err != nil {
|
|
tearDown()
|
|
t.Fatal(err)
|
|
}
|
|
sm, err := New(&Config{
|
|
Chain: chain,
|
|
ChainParams: test.params,
|
|
})
|
|
if err != nil {
|
|
tearDown()
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Run test and assert.
|
|
sm.peerStates = test.peerState
|
|
got := sm.isInIBDMode()
|
|
require.Equal(t, test.isIBDMode, got)
|
|
tearDown()
|
|
}
|
|
}
|
|
|
|
// createTestCoinbase creates a minimal coinbase transaction for the given
|
|
// block height. The signature script encodes the height to ensure unique
|
|
// transaction hashes across blocks.
|
|
func createTestCoinbase(height int32, params *chaincfg.Params) *wire.MsgTx {
|
|
tx := wire.NewMsgTx(wire.TxVersion)
|
|
|
|
// Encode the height as a minimally encoded script integer and add a trailing
|
|
// OP_0 so the script also satisfies the generic coinbase-length checks.
|
|
sigScript, err := txscript.NewScriptBuilder().
|
|
AddInt64(int64(height)).
|
|
AddInt64(0).
|
|
Script()
|
|
if err != nil {
|
|
panic(fmt.Sprintf("unable to encode coinbase height %d: %v",
|
|
height, err))
|
|
}
|
|
|
|
tx.AddTxIn(&wire.TxIn{
|
|
PreviousOutPoint: wire.OutPoint{
|
|
Hash: chainhash.Hash{},
|
|
Index: wire.MaxPrevOutIndex,
|
|
},
|
|
SignatureScript: sigScript,
|
|
Sequence: wire.MaxTxInSequenceNum,
|
|
})
|
|
|
|
tx.AddTxOut(&wire.TxOut{
|
|
Value: blockchain.CalcBlockSubsidy(height, params),
|
|
PkScript: []byte{txscript.OP_TRUE},
|
|
})
|
|
|
|
return tx
|
|
}
|
|
|
|
// solveTestBlock finds a nonce that satisfies the proof of work for the given
|
|
// header. With regression test parameters the difficulty is minimal and a
|
|
// solution is found almost immediately.
|
|
func solveTestBlock(header *wire.BlockHeader, params *chaincfg.Params) bool {
|
|
target := blockchain.CompactToBig(params.PowLimitBits)
|
|
for nonce := uint32(0); nonce < math.MaxUint32; nonce++ {
|
|
header.Nonce = nonce
|
|
hash := header.BlockHash()
|
|
if blockchain.HashToBig(&hash).Cmp(target) <= 0 {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// generateTestBlocks creates count valid blocks chaining from the genesis
|
|
// block of the given params. Each block contains only a coinbase transaction.
|
|
func generateTestBlocks(
|
|
t *testing.T, params *chaincfg.Params, count int) []*btcutil.Block {
|
|
|
|
t.Helper()
|
|
|
|
blocks := make([]*btcutil.Block, 0, count)
|
|
prevHash := params.GenesisHash
|
|
prevTime := params.GenesisBlock.Header.Timestamp
|
|
|
|
for h := int32(1); h <= int32(count); h++ {
|
|
cb := createTestCoinbase(h, params)
|
|
merkleRoot := cb.TxHash()
|
|
|
|
header := wire.BlockHeader{
|
|
// Regtest enforces the BIP34/65/66 version floor from height 1.
|
|
Version: 4,
|
|
PrevBlock: *prevHash,
|
|
MerkleRoot: merkleRoot,
|
|
Timestamp: prevTime.Add(time.Minute),
|
|
Bits: params.PowLimitBits,
|
|
}
|
|
require.True(t, solveTestBlock(&header, params),
|
|
"failed to solve block at height %d", h)
|
|
|
|
msgBlock := &wire.MsgBlock{
|
|
Header: header,
|
|
Transactions: []*wire.MsgTx{cb},
|
|
}
|
|
block := btcutil.NewBlock(msgBlock)
|
|
blocks = append(blocks, block)
|
|
|
|
bh := block.Hash()
|
|
prevHash = bh
|
|
prevTime = header.Timestamp
|
|
}
|
|
|
|
return blocks
|
|
}
|
|
|
|
// TestSyncStateMachine exercises the end-to-end IBD sync flow:
|
|
//
|
|
// ┌→ startSync
|
|
// │ ↓
|
|
// │ fetchHeaders
|
|
// │ ↓
|
|
// │ handleHeadersMsg
|
|
// │ ↓
|
|
// │ fetchHeaderBlocks ←┐
|
|
// │ ↓ │ (refill)
|
|
// │ handleBlockMsg ────┘──→ IBD complete
|
|
// │
|
|
// │ (stall detected at any phase above)
|
|
// │ ↓
|
|
// │ handleStallSample
|
|
// │ ↓
|
|
// └── handleDonePeerMsg
|
|
//
|
|
// It verifies that header processing transitions to block download, that the
|
|
// pipeline refill path in handleBlockMsg is exercised, and that IBD mode is
|
|
// properly cleared once the chain catches up to the best header.
|
|
//
|
|
// The "fresh ibd" case tests a complete sync from genesis: headers are fetched
|
|
// and then blocks are downloaded.
|
|
//
|
|
// The "stall before any headers" and "stall mid header download" cases test
|
|
// recovery when the sync peer stalls during header download. A replacement
|
|
// peer delivers the remaining (or all) headers and then all blocks.
|
|
//
|
|
// The "headers complete peer stalls on blocks" case tests recovery when the
|
|
// sync peer delivers all headers but stalls before sending any blocks; a
|
|
// replacement peer downloads all blocks.
|
|
//
|
|
// The "stalled sync peer recovery" case tests recovery mid-block-download: a
|
|
// sync peer stops responding after some blocks, handleStallSample detects the
|
|
// inactivity, the stalled peer is disconnected, and a replacement peer
|
|
// finishes IBD.
|
|
//
|
|
// The "stall mid headers then stall on blocks" case combines both failure
|
|
// modes: one peer stalls during headers (peer 2 takes over and finishes
|
|
// headers), then peer 2 stalls during block download (peer 3 finishes blocks).
|
|
// This exercises recovery across three distinct peers.
|
|
func TestSyncStateMachine(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const testTotalBlocks = 2 * minInFlightBlocks
|
|
|
|
tests := []struct {
|
|
name string
|
|
totalBlocks int
|
|
|
|
// stallHeadersAfter, when >= 0, triggers a stall during
|
|
// header download: deliver this many headers, then stall
|
|
// the sync peer and verify a replacement finishes header
|
|
// download. Set to -1 for no header stall.
|
|
stallHeadersAfter int
|
|
|
|
// stallAfter, when >= 0, triggers a stall during block
|
|
// download: deliver all headers, then process this many
|
|
// blocks before stalling. Set to -1 for no block stall.
|
|
stallAfter int
|
|
}{
|
|
{
|
|
name: "fresh ibd",
|
|
totalBlocks: testTotalBlocks,
|
|
stallHeadersAfter: -1,
|
|
stallAfter: -1,
|
|
},
|
|
{
|
|
name: "stall before any headers",
|
|
totalBlocks: testTotalBlocks,
|
|
stallHeadersAfter: 0,
|
|
stallAfter: -1,
|
|
},
|
|
{
|
|
name: "stall mid header download",
|
|
totalBlocks: testTotalBlocks,
|
|
stallHeadersAfter: testTotalBlocks / 2,
|
|
stallAfter: -1,
|
|
},
|
|
{
|
|
name: "headers complete peer stalls on blocks",
|
|
totalBlocks: testTotalBlocks,
|
|
stallHeadersAfter: -1,
|
|
stallAfter: 0,
|
|
},
|
|
{
|
|
name: "stalled sync peer recovery",
|
|
totalBlocks: testTotalBlocks,
|
|
stallHeadersAfter: -1,
|
|
stallAfter: 5,
|
|
},
|
|
{
|
|
name: "stall mid headers then stall on blocks",
|
|
totalBlocks: testTotalBlocks,
|
|
stallHeadersAfter: testTotalBlocks / 2,
|
|
stallAfter: 5,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
params := chaincfg.RegressionNetParams
|
|
params.Checkpoints = nil
|
|
|
|
sm, tearDown := makeMockSyncManager(t, ¶ms)
|
|
defer tearDown()
|
|
|
|
blocks := generateTestBlocks(t, ¶ms, tc.totalBlocks)
|
|
|
|
// Register a sync candidate and call startSync,
|
|
// which activates IBD mode and sends getheaders.
|
|
peer1 := startIBD(t, sm, tc.totalBlocks)
|
|
|
|
if tc.stallHeadersAfter >= 0 {
|
|
// Stall during header download;
|
|
// replacement sends remaining headers.
|
|
peer2 := newSyncCandidate(t, sm,
|
|
int32(tc.totalBlocks))
|
|
syncStalledHeaderRecovery(
|
|
t, sm, peer1, peer2,
|
|
blocks, tc.stallHeadersAfter,
|
|
tc.totalBlocks,
|
|
)
|
|
|
|
if tc.stallAfter >= 0 {
|
|
peer3 := newSyncCandidate(t, sm,
|
|
int32(tc.totalBlocks))
|
|
syncStalledPeerRecovery(
|
|
t, sm, peer2,
|
|
peer3, blocks,
|
|
tc.stallAfter,
|
|
tc.totalBlocks,
|
|
)
|
|
} else {
|
|
syncProcessBlocks(t, sm,
|
|
peer2, blocks,
|
|
tc.totalBlocks)
|
|
}
|
|
} else {
|
|
syncSendHeaders(t, sm, peer1,
|
|
blocks, tc.totalBlocks)
|
|
|
|
if tc.stallAfter >= 0 {
|
|
peer2 := newSyncCandidate(t, sm,
|
|
int32(tc.totalBlocks))
|
|
syncStalledPeerRecovery(
|
|
t, sm, peer1,
|
|
peer2, blocks,
|
|
tc.stallAfter,
|
|
tc.totalBlocks,
|
|
)
|
|
} else {
|
|
syncProcessBlocks(t, sm,
|
|
peer1, blocks,
|
|
tc.totalBlocks)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// newSyncCandidate creates and registers a sync-candidate peer at the
|
|
// given height without triggering startSync.
|
|
func newSyncCandidate(t *testing.T, sm *SyncManager,
|
|
height int32) *peer.Peer {
|
|
|
|
t.Helper()
|
|
|
|
p := peer.NewInboundPeer(&peer.Config{
|
|
ChainParams: sm.chainParams,
|
|
})
|
|
p.UpdateLastBlockHeight(height)
|
|
sm.peerStates[p] = &peerSyncState{
|
|
syncCandidate: true,
|
|
requestedTxns: make(map[chainhash.Hash]struct{}),
|
|
requestedBlocks: make(map[chainhash.Hash]struct{}),
|
|
}
|
|
return p
|
|
}
|
|
|
|
// assertIBDComplete verifies that IBD finished: chain height matches
|
|
// totalBlocks, ibdMode is off, and no blocks remain in-flight.
|
|
func assertIBDComplete(t *testing.T, sm *SyncManager,
|
|
peerState *peerSyncState, totalBlocks int) {
|
|
|
|
t.Helper()
|
|
|
|
best := sm.chain.BestSnapshot()
|
|
require.Equal(t, int32(totalBlocks), best.Height)
|
|
require.False(t, sm.ibdMode,
|
|
"ibdMode should be off after catching up")
|
|
require.Empty(t, sm.requestedBlocks,
|
|
"all requested blocks should be fulfilled")
|
|
require.Empty(t, peerState.requestedBlocks,
|
|
"peer should have no outstanding block requests")
|
|
}
|
|
|
|
// startIBD registers a sync peer and calls startSync, verifying that IBD
|
|
// mode is activated and the peer is selected.
|
|
func startIBD(t *testing.T, sm *SyncManager,
|
|
peerHeight int) *peer.Peer {
|
|
|
|
t.Helper()
|
|
|
|
syncPeer := newSyncCandidate(t, sm, int32(peerHeight))
|
|
|
|
sm.startSync()
|
|
|
|
require.True(t, sm.syncPeer == syncPeer,
|
|
"syncPeer should be set after startSync")
|
|
require.True(t, sm.ibdMode, "ibdMode should be on")
|
|
require.False(t, sm.lastProgressTime.IsZero(),
|
|
"lastProgressTime should be set")
|
|
|
|
return syncPeer
|
|
}
|
|
|
|
// syncSendHeaders delivers block headers to the sync manager and verifies
|
|
// that block requests are generated.
|
|
func syncSendHeaders(t *testing.T, sm *SyncManager,
|
|
syncPeer *peer.Peer, blocks []*btcutil.Block, totalBlocks int) {
|
|
|
|
t.Helper()
|
|
|
|
// Record the progress time set by startIBD so we can verify
|
|
// that handleHeadersMsg advances it.
|
|
progressBefore := sm.lastProgressTime
|
|
|
|
headers := wire.NewMsgHeaders()
|
|
for _, block := range blocks {
|
|
err := headers.AddBlockHeader(&block.MsgBlock().Header)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
sm.handleHeadersMsg(&headersMsg{
|
|
headers: headers,
|
|
peer: syncPeer,
|
|
})
|
|
|
|
_, bestHeaderHeight := sm.chain.BestHeader()
|
|
require.Equal(t, int32(totalBlocks), bestHeaderHeight)
|
|
|
|
require.True(t, sm.lastProgressTime.After(progressBefore),
|
|
"handleHeadersMsg should update lastProgressTime")
|
|
|
|
wantRequested := make(map[chainhash.Hash]struct{}, len(blocks))
|
|
for _, block := range blocks {
|
|
wantRequested[*block.Hash()] = struct{}{}
|
|
}
|
|
require.Equal(t, wantRequested, sm.requestedBlocks)
|
|
require.Equal(t, wantRequested, sm.peerStates[syncPeer].requestedBlocks)
|
|
}
|
|
|
|
// syncProcessBlocks feeds all blocks to handleBlockMsg and verifies that IBD
|
|
// mode remains active until the final block, at which point IBD completes.
|
|
func syncProcessBlocks(t *testing.T, sm *SyncManager, syncPeer *peer.Peer,
|
|
blocks []*btcutil.Block, totalBlocks int) {
|
|
|
|
t.Helper()
|
|
|
|
peerState := sm.peerStates[syncPeer]
|
|
|
|
for i, block := range blocks {
|
|
sm.handleBlockMsg(&blockMsg{
|
|
block: block,
|
|
peer: syncPeer,
|
|
reply: make(chan struct{}, 1),
|
|
})
|
|
|
|
if i < len(blocks)-1 {
|
|
require.True(t, sm.ibdMode,
|
|
"ibdMode should still be on at height %d", i+1)
|
|
}
|
|
}
|
|
|
|
assertIBDComplete(t, sm, peerState, totalBlocks)
|
|
}
|
|
|
|
// syncStalledPeerRecovery processes stallAfter blocks from stalledPeer,
|
|
// triggers stall detection, verifies that stalledPeer is removed and
|
|
// replacementPeer takes over, then feeds remaining blocks and verifies
|
|
// IBD completes.
|
|
func syncStalledPeerRecovery(t *testing.T, sm *SyncManager,
|
|
stalledPeer, replacementPeer *peer.Peer,
|
|
blocks []*btcutil.Block, stallAfter, totalBlocks int) {
|
|
|
|
t.Helper()
|
|
|
|
// Process the first stallAfter blocks from the stalled peer.
|
|
for _, block := range blocks[:stallAfter] {
|
|
sm.handleBlockMsg(&blockMsg{
|
|
block: block,
|
|
peer: stalledPeer,
|
|
reply: make(chan struct{}, 1),
|
|
})
|
|
}
|
|
|
|
best := sm.chain.BestSnapshot()
|
|
require.Equal(t, int32(stallAfter), best.Height)
|
|
require.True(t, sm.ibdMode)
|
|
|
|
// Trigger stall detection.
|
|
sm.lastProgressTime = time.Now().Add(
|
|
-(maxStallDuration + time.Minute))
|
|
sm.handleStallSample()
|
|
|
|
// Verify that handleStallSample called Disconnect() on the
|
|
// stalled peer (which closes p.quit, making WaitForDisconnect
|
|
// return immediately).
|
|
disconnected := make(chan struct{})
|
|
go func() {
|
|
stalledPeer.WaitForDisconnect()
|
|
close(disconnected)
|
|
}()
|
|
select {
|
|
case <-disconnected:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Disconnect() was not called on stalled peer")
|
|
}
|
|
|
|
// Snapshot the stalled peer's outstanding requested blocks before
|
|
// disconnection so we can verify they are cleaned up.
|
|
stalledState := sm.peerStates[stalledPeer]
|
|
stalledRequested := make([]chainhash.Hash, 0, len(stalledState.requestedBlocks))
|
|
for hash := range stalledState.requestedBlocks {
|
|
stalledRequested = append(stalledRequested, hash)
|
|
}
|
|
require.NotEmpty(t, stalledRequested,
|
|
"stalled peer should have outstanding requested blocks")
|
|
|
|
// In production, Disconnect() triggers handleDonePeerMsg
|
|
// asynchronously via the peer goroutine. Call it directly to
|
|
// complete the removal. Note: handleDonePeerMsg first clears the
|
|
// stalled peer's requested blocks from the global map via
|
|
// clearRequestedState, then updateSyncPeer → startSync immediately
|
|
// re-requests them for the replacement peer.
|
|
sm.handleDonePeerMsg(stalledPeer)
|
|
|
|
_, stalledTracked := sm.peerStates[stalledPeer]
|
|
require.False(t, stalledTracked,
|
|
"stalled peer should be removed")
|
|
require.True(t, sm.syncPeer == replacementPeer,
|
|
"replacement peer should take over as sync peer")
|
|
require.True(t, sm.ibdMode)
|
|
|
|
// Verify that the replacement peer re-requested the exact same
|
|
// blocks that were outstanding from the stalled peer.
|
|
replacementState := sm.peerStates[replacementPeer]
|
|
require.Equal(t, len(stalledRequested),
|
|
len(replacementState.requestedBlocks),
|
|
"replacement peer should request same number of blocks")
|
|
for _, hash := range stalledRequested {
|
|
_, exists := replacementState.requestedBlocks[hash]
|
|
require.True(t, exists,
|
|
"block %v should be requested from replacement peer",
|
|
hash)
|
|
}
|
|
|
|
// Feed remaining blocks from the replacement peer.
|
|
for _, block := range blocks[stallAfter:] {
|
|
sm.handleBlockMsg(&blockMsg{
|
|
block: block,
|
|
peer: replacementPeer,
|
|
reply: make(chan struct{}, 1),
|
|
})
|
|
}
|
|
|
|
assertIBDComplete(t, sm, replacementState, totalBlocks)
|
|
}
|
|
|
|
// syncStalledHeaderRecovery simulates a stall during header download.
|
|
// It optionally delivers headersSent headers from stalledPeer, triggers stall
|
|
// detection, verifies that stalledPeer is removed and replacementPeer takes
|
|
// over, then delivers remaining headers and verifies block requests are
|
|
// generated. The caller is responsible for the block-download phase.
|
|
func syncStalledHeaderRecovery(t *testing.T, sm *SyncManager,
|
|
stalledPeer, replacementPeer *peer.Peer,
|
|
blocks []*btcutil.Block, headersSent, totalBlocks int) {
|
|
|
|
t.Helper()
|
|
|
|
// Deliver partial headers from the stalled peer. When
|
|
// headersSent is 0, this is a no-op (peer stalls immediately).
|
|
if headersSent > 0 {
|
|
headers := wire.NewMsgHeaders()
|
|
for _, block := range blocks[:headersSent] {
|
|
err := headers.AddBlockHeader(
|
|
&block.MsgBlock().Header)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
sm.handleHeadersMsg(&headersMsg{
|
|
headers: headers,
|
|
peer: stalledPeer,
|
|
})
|
|
|
|
_, bestHeaderHeight := sm.chain.BestHeader()
|
|
require.Equal(t, int32(headersSent), bestHeaderHeight)
|
|
}
|
|
|
|
// No blocks should have been requested during header download
|
|
// since the headers haven't caught up to the peer's height yet.
|
|
require.Empty(t, sm.requestedBlocks,
|
|
"no blocks should be requested during header download")
|
|
|
|
// Trigger stall detection.
|
|
sm.lastProgressTime = time.Now().Add(
|
|
-(maxStallDuration + time.Minute))
|
|
sm.handleStallSample()
|
|
|
|
// Verify that handleStallSample called Disconnect() on the
|
|
// stalled peer.
|
|
disconnected := make(chan struct{})
|
|
go func() {
|
|
stalledPeer.WaitForDisconnect()
|
|
close(disconnected)
|
|
}()
|
|
select {
|
|
case <-disconnected:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Disconnect() was not called on stalled peer")
|
|
}
|
|
|
|
// Complete peer removal. handleDonePeerMsg clears state and
|
|
// triggers startSync which selects the replacement peer.
|
|
sm.handleDonePeerMsg(stalledPeer)
|
|
|
|
_, stalledTracked := sm.peerStates[stalledPeer]
|
|
require.False(t, stalledTracked,
|
|
"stalled peer should be removed")
|
|
require.True(t, sm.syncPeer == replacementPeer,
|
|
"replacement peer should take over as sync peer")
|
|
require.True(t, sm.ibdMode)
|
|
|
|
// Deliver remaining headers from the replacement peer. When
|
|
// headersSent is 0, this is all headers.
|
|
remainingHeaders := wire.NewMsgHeaders()
|
|
for _, block := range blocks[headersSent:] {
|
|
err := remainingHeaders.AddBlockHeader(
|
|
&block.MsgBlock().Header)
|
|
require.NoError(t, err)
|
|
}
|
|
sm.handleHeadersMsg(&headersMsg{
|
|
headers: remainingHeaders,
|
|
peer: replacementPeer,
|
|
})
|
|
|
|
_, bestHeaderHeight := sm.chain.BestHeader()
|
|
require.Equal(t, int32(totalBlocks), bestHeaderHeight)
|
|
|
|
// Verify all blocks were requested from the replacement.
|
|
wantRequested := make(map[chainhash.Hash]struct{}, len(blocks))
|
|
for _, block := range blocks {
|
|
wantRequested[*block.Hash()] = struct{}{}
|
|
}
|
|
require.Equal(t, wantRequested, sm.requestedBlocks)
|
|
replacementState := sm.peerStates[replacementPeer]
|
|
require.Equal(t, wantRequested, replacementState.requestedBlocks)
|
|
}
|
|
|
|
// TestStartSyncBlockFallback verifies the startSync fallback path where
|
|
// headers are already caught up but the block chain lags behind. In this
|
|
// case startSync should skip header download and directly request blocks.
|
|
func TestStartSyncBlockFallback(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
params := chaincfg.RegressionNetParams
|
|
params.Checkpoints = nil
|
|
|
|
sm, tearDown := makeMockSyncManager(t, ¶ms)
|
|
defer tearDown()
|
|
|
|
// Process headers so the header chain is at numBlocks while the
|
|
// block chain stays at genesis.
|
|
const numBlocks = 11
|
|
blocks := generateTestBlocks(t, ¶ms, numBlocks)
|
|
for _, block := range blocks {
|
|
_, err := sm.chain.ProcessBlockHeader(
|
|
&block.MsgBlock().Header, blockchain.BFNone, false)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Add a peer whose height equals the header height.
|
|
// fetchHigherPeers(bestHeaderHeight) returns nothing because
|
|
// the peer is not strictly higher than our headers.
|
|
// fetchHigherPeers(bestBlockHeight=0) returns the peer.
|
|
syncPeer := peer.NewInboundPeer(&peer.Config{})
|
|
syncPeer.UpdateLastBlockHeight(int32(numBlocks))
|
|
sm.peerStates[syncPeer] = &peerSyncState{
|
|
syncCandidate: true,
|
|
requestedTxns: make(map[chainhash.Hash]struct{}),
|
|
requestedBlocks: make(map[chainhash.Hash]struct{}),
|
|
}
|
|
|
|
sm.startSync()
|
|
|
|
require.NotNil(t, sm.syncPeer,
|
|
"sync peer should be set for block download")
|
|
require.NotEmpty(t, sm.requestedBlocks,
|
|
"blocks should be requested via fetchHeaderBlocks")
|
|
}
|
|
|
|
// TestStallNoDisconnectAtSameHeight verifies that handleStallSample does
|
|
// not disconnect a sync peer whose advertised height equals our own.
|
|
func TestStallNoDisconnectAtSameHeight(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
params := chaincfg.RegressionNetParams
|
|
params.Checkpoints = nil
|
|
|
|
sm, tearDown := makeMockSyncManager(t, ¶ms)
|
|
defer tearDown()
|
|
|
|
p := peer.NewInboundPeer(&peer.Config{})
|
|
p.UpdateLastBlockHeight(0) // Same height as our genesis chain.
|
|
sm.peerStates[p] = &peerSyncState{}
|
|
sm.syncPeer = p
|
|
sm.ibdMode = true
|
|
sm.lastProgressTime = time.Now().Add(
|
|
-(maxStallDuration + time.Minute))
|
|
|
|
sm.handleStallSample()
|
|
|
|
_, tracked := sm.peerStates[p]
|
|
require.True(t, tracked,
|
|
"peer at same height should not be disconnected")
|
|
require.Nil(t, sm.syncPeer,
|
|
"we should have nil syncPeer after handleStallSample")
|
|
}
|
|
|
|
// TestStartSyncChainCurrent verifies that startSync does not set syncPeer
|
|
// or ibdMode when the chain is current and no peer is strictly higher.
|
|
// isInIBDMode sees IsCurrent()==true with no higher peers, returns false,
|
|
// and startSync exits immediately.
|
|
func TestStartSyncChainCurrent(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
params := chaincfg.RegressionNetParams
|
|
params.Checkpoints = nil
|
|
|
|
sm, tearDown := makeMockSyncManager(t, ¶ms)
|
|
defer tearDown()
|
|
|
|
// Mine a single block with a recent timestamp so
|
|
// IsCurrent() returns true.
|
|
cb := createTestCoinbase(1, ¶ms)
|
|
header := wire.BlockHeader{
|
|
// Regtest enforces the BIP34/65/66 version floor from height 1.
|
|
Version: 4,
|
|
PrevBlock: *params.GenesisHash,
|
|
MerkleRoot: cb.TxHash(),
|
|
Timestamp: time.Now().Truncate(time.Second),
|
|
Bits: params.PowLimitBits,
|
|
}
|
|
require.True(t, solveTestBlock(&header, ¶ms))
|
|
|
|
block := btcutil.NewBlock(&wire.MsgBlock{
|
|
Header: header,
|
|
Transactions: []*wire.MsgTx{cb},
|
|
})
|
|
_, _, err := sm.chain.ProcessBlock(block, blockchain.BFNone)
|
|
require.NoError(t, err)
|
|
require.True(t, sm.chain.IsCurrent())
|
|
|
|
// Peer at our height — not higher.
|
|
newSyncCandidate(t, sm, 1)
|
|
|
|
sm.startSync()
|
|
|
|
require.Nil(t, sm.syncPeer,
|
|
"syncPeer should not be set when chain is already current")
|
|
require.False(t, sm.ibdMode,
|
|
"ibdMode should not be activated when chain is already current")
|
|
}
|
|
|
|
// TestIsSyncCandidateRegtest verifies that isSyncCandidate accepts any peer
|
|
// on regtest regardless of address, including non-localhost Docker bridge
|
|
// addresses.
|
|
func TestIsSyncCandidateRegtest(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
params := chaincfg.RegressionNetParams
|
|
sm, tearDown := makeMockSyncManager(t, ¶ms)
|
|
defer tearDown()
|
|
|
|
tests := []struct {
|
|
name string
|
|
addr string
|
|
want bool
|
|
}{
|
|
{
|
|
name: "localhost",
|
|
addr: "127.0.0.1:18444",
|
|
want: true,
|
|
},
|
|
{
|
|
name: "docker bridge ip",
|
|
addr: "172.18.0.2:18444",
|
|
want: true,
|
|
},
|
|
{
|
|
name: "remote ip",
|
|
addr: "93.184.216.34:18444",
|
|
want: true,
|
|
},
|
|
{
|
|
name: "ipv6 loopback",
|
|
addr: "[::1]:18444",
|
|
want: true,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
p := peer.NewInboundPeer(&peer.Config{
|
|
ChainParams: sm.chainParams,
|
|
})
|
|
|
|
got := sm.isSyncCandidate(p)
|
|
require.Equal(t, tc.want, got)
|
|
})
|
|
}
|
|
}
|