netsync: add checkHeadersList

checkHeadersList takes in a blockhash and returns if it's a checkpointed
block and the correct behavior flags for the verification of the block.
This commit is contained in:
Calvin Kim 2025-09-23 01:10:27 +09:00
parent eeb2d43a79
commit b1aef3ad21
3 changed files with 280 additions and 0 deletions

View file

@ -690,6 +690,47 @@ func (sm *SyncManager) current() bool {
return true
}
// checkHeadersList checks if the sync manager is in the initial block download
// mode and returns if the given block hash is a checkpointed block and the
// behavior flags for this block. If the block is still under the checkpoint,
// then it's given the fast-add flag.
func (sm *SyncManager) checkHeadersList(blockHash *chainhash.Hash) (
bool, blockchain.BehaviorFlags) {
// Always return false and BFNone if we're not in ibd mode.
if !sm.headersFirstMode {
return false, blockchain.BFNone
}
isCheckpointBlock := false
behaviorFlags := blockchain.BFNone
// If we don't already know this is a valid header, return false and
// BFNone.
if !sm.chain.IsValidHeader(blockHash) {
return false, blockchain.BFNone
}
height, err := sm.chain.HeaderHeightByHash(*blockHash)
if err != nil {
return false, blockchain.BFNone
}
// Since findNextHeaderCheckpoint returns the next checkpoint after the
// passed height, we do a -1 to include the current block.
checkpoint := sm.findNextHeaderCheckpoint(height - 1)
if checkpoint == nil {
return false, blockchain.BFNone
}
behaviorFlags |= blockchain.BFFastAdd
if blockHash.IsEqual(checkpoint.Hash) {
isCheckpointBlock = true
}
return isCheckpointBlock, behaviorFlags
}
// handleBlockMsg handles block messages from all peers.
func (sm *SyncManager) handleBlockMsg(bmsg *blockMsg) {
peer := bmsg.peer

228
netsync/manager_test.go Normal file
View file

@ -0,0 +1,228 @@
package netsync
import (
"bufio"
"bytes"
"encoding/hex"
"fmt"
"os"
"path/filepath"
"testing"
"github.com/btcsuite/btcd/blockchain"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"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"
"github.com/btcsuite/btcd/wire"
"github.com/stretchr/testify/require"
)
// 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 := &paramsCopy.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: &paramsCopy,
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, &params)
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 headers-first mode is off, we always get
// false and BFNone.
sm.headersFirstMode = 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.headersFirstMode = true
isCheckpoint, gotFlags = sm.checkHeadersList(hash)
require.Equal(t, test.isCheckpointBlock, isCheckpoint)
require.Equal(t, test.behaviorFlags, gotFlags)
}
}

View file

@ -0,0 +1,11 @@
010000006fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000982051fd1e4ba744bbbe680e1fee14677ba1a3c3540bf7b1cdb606e857233e0e61bc6649ffff001d01e36299
010000004860eb18bf1b1620e37e9490fc8a427514416fd75159ab86688e9a8300000000d5fdcc541e25de1c7a5addedf24858b8bb665c9f36ef744ee42c316022c90f9bb0bc6649ffff001d08d2bd61
01000000bddd99ccfda39da1b108ce1a5d70038d0a967bacb68b6b63065f626a0000000044f672226090d85db9a9f2fbfe5f0f9609b387af7be5b7fbb7a1767c831c9e995dbe6649ffff001d05e0ed6d
010000004944469562ae1c2c74d9a535e00b6f3e40ffbad4f2fda3895501b582000000007a06ea98cd40ba2e3288262b28638cec5337c1456aaf5eedc8e9e5a20f062bdf8cc16649ffff001d2bfee0a9
0100000085144a84488ea88d221c8bd6c059da090e88f8a2c99690ee55dbba4e00000000e11c48fecdd9e72510ca84f023370c9a38bf91ac5cae88019bee94d24528526344c36649ffff001d1d03e477
01000000fc33f596f822a0a1951ffdbf2a897b095636ad871707bf5d3162729b00000000379dfb96a5ea8c81700ea4ac6b97ae9a9312b2d4301a29580e924ee6761a2520adc46649ffff001d189c4c97
010000008d778fdc15a2d3fb76b7122a3b5582bea4f21f5a0c693537e7a03130000000003f674005103b42f984169c7d008370967e91920a6a5d64fd51282f75bc73a68af1c66649ffff001d39a59c86
010000004494c8cf4154bdcc0720cd4a59d9c9b285e4b146d45f061d2b6c967100000000e3855ed886605b6d4a99d5fa2ef2e9b0b164e63df3c4136bebf2d0dac0f1f7a667c86649ffff001d1c4b5666
01000000c60ddef1b7618ca2348a46e868afc26e3efc68226c78aa47f8488c4000000000c997a5e56e104102fa209c6a852dd90660a20b2d9c352423edce25857fcd37047fca6649ffff001d28404f53
010000000508085c47cc849eb80ea905cc7800a3be674ffc57263cf210c59d8d00000000112ba175a1e04b14ba9e7ea5f76ab640affeef5ec98173ac9799a852fa39add320cd6649ffff001d1e2de565
01000000e915d9a478e3adf3186c07c61a22228b10fd87df343c92782ecc052c000000006e06373c80de397406dc3d19c90d71d230058d28293614ea58d6a57f8f5d32f8b8ce6649ffff001d173807f8