From b1aef3ad21e4fd7bf0d1aeb3acbeaf5b0c9e8d0e Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Tue, 23 Sep 2025 01:10:27 +0900 Subject: [PATCH] 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. --- netsync/manager.go | 41 ++++ netsync/manager_test.go | 228 ++++++++++++++++++ .../testdata/blockheaders-mainnet-1-11.txt | 11 + 3 files changed, 280 insertions(+) create mode 100644 netsync/manager_test.go create mode 100644 netsync/testdata/blockheaders-mainnet-1-11.txt diff --git a/netsync/manager.go b/netsync/manager.go index d10188d5..480eead5 100644 --- a/netsync/manager.go +++ b/netsync/manager.go @@ -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 diff --git a/netsync/manager_test.go b/netsync/manager_test.go new file mode 100644 index 00000000..ee84cea0 --- /dev/null +++ b/netsync/manager_test.go @@ -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 := ¶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 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) + } +} diff --git a/netsync/testdata/blockheaders-mainnet-1-11.txt b/netsync/testdata/blockheaders-mainnet-1-11.txt new file mode 100644 index 00000000..27b5f18c --- /dev/null +++ b/netsync/testdata/blockheaders-mainnet-1-11.txt @@ -0,0 +1,11 @@ +010000006fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000982051fd1e4ba744bbbe680e1fee14677ba1a3c3540bf7b1cdb606e857233e0e61bc6649ffff001d01e36299 +010000004860eb18bf1b1620e37e9490fc8a427514416fd75159ab86688e9a8300000000d5fdcc541e25de1c7a5addedf24858b8bb665c9f36ef744ee42c316022c90f9bb0bc6649ffff001d08d2bd61 +01000000bddd99ccfda39da1b108ce1a5d70038d0a967bacb68b6b63065f626a0000000044f672226090d85db9a9f2fbfe5f0f9609b387af7be5b7fbb7a1767c831c9e995dbe6649ffff001d05e0ed6d +010000004944469562ae1c2c74d9a535e00b6f3e40ffbad4f2fda3895501b582000000007a06ea98cd40ba2e3288262b28638cec5337c1456aaf5eedc8e9e5a20f062bdf8cc16649ffff001d2bfee0a9 +0100000085144a84488ea88d221c8bd6c059da090e88f8a2c99690ee55dbba4e00000000e11c48fecdd9e72510ca84f023370c9a38bf91ac5cae88019bee94d24528526344c36649ffff001d1d03e477 +01000000fc33f596f822a0a1951ffdbf2a897b095636ad871707bf5d3162729b00000000379dfb96a5ea8c81700ea4ac6b97ae9a9312b2d4301a29580e924ee6761a2520adc46649ffff001d189c4c97 +010000008d778fdc15a2d3fb76b7122a3b5582bea4f21f5a0c693537e7a03130000000003f674005103b42f984169c7d008370967e91920a6a5d64fd51282f75bc73a68af1c66649ffff001d39a59c86 +010000004494c8cf4154bdcc0720cd4a59d9c9b285e4b146d45f061d2b6c967100000000e3855ed886605b6d4a99d5fa2ef2e9b0b164e63df3c4136bebf2d0dac0f1f7a667c86649ffff001d1c4b5666 +01000000c60ddef1b7618ca2348a46e868afc26e3efc68226c78aa47f8488c4000000000c997a5e56e104102fa209c6a852dd90660a20b2d9c352423edce25857fcd37047fca6649ffff001d28404f53 +010000000508085c47cc849eb80ea905cc7800a3be674ffc57263cf210c59d8d00000000112ba175a1e04b14ba9e7ea5f76ab640affeef5ec98173ac9799a852fa39add320cd6649ffff001d1e2de565 +01000000e915d9a478e3adf3186c07c61a22228b10fd87df343c92782ecc052c000000006e06373c80de397406dc3d19c90d71d230058d28293614ea58d6a57f8f5d32f8b8ce6649ffff001d173807f8