netsync: use ProcessBlockHeader in handleBlockHeader
What changed, and why it matters
This commit refactors how btcd handles incoming block headers during peer synchronization. It replaces a custom header-list processing path with a centralized `ProcessBlockHeader` function. The most notable behavioral change is that the code no longer disconnects peers merely for sending headers when the node is not in 'headers-first sync' mode. This could allow unrequested headers to be processed, but the actual validation work is now delegated to the chain's standard header verification logic. There is no explicit security claim in the commit, and the change appears primarily architectural.
Treat as a routine refactor with a minor security-relevant side effect. Reviewers should verify that `ProcessBlockHeader` correctly rejects malformed, non-connecting, and checkpoint-violating headers, and that removing the unrequested-header disconnect does not introduce a denial-of-service or eclipse-vector. No immediate incident response is warranted based solely on this diff.
Security signals we found
Removed explicit disconnect for unrequested headers when not in headers-first mode
Replaced custom header-chain connectivity and checkpoint verification with centralized ProcessBlockHeader
Behavioral change in peer tolerance: previously unrequested headers caused immediate disconnect; now they are validated through standard chain logic
No explicit security bug fix language in commit message or diff
Evidence from the diff
The patch modifies netsync/manager.go, specifically handleHeadersMsg. It removes the old headerList based incremental validation, checkpoint matching, and the guard that disconnected peers sending unrequested headers outside headersFirstMode. Instead, each received header is passed to sm.chain.ProcessBlockHeader(blockchain.BFNone, false). After processing, if the node is in headers-first mode and the sync peer has more headers, it requests the next batch; otherwise it transitions to fetching blocks. The removal of the unrequested-header disconnect and the checkpoint-break logic are the main security-relevant behavioral deltas.
Changed components
netsync/manager.gohandleHeadersMsgheaders-first synchronizationpeer header processingInspect captured patch +31 / −79
diff --git a/netsync/manager.go b/netsync/manager.go
index 69bb3f7..fe8464f 100644
--- a/netsync/manager.go
+++ b/netsync/manager.go
@@ -1058,7 +1058,8 @@ func (sm *SyncManager) fetchHeaderBlocks() {
}
// handleHeadersMsg handles block header messages from all peers. Headers are
-// requested when performing a headers-first sync.
+// requested when performing a headers-first sync and are propagated by peers
+// once the headers-first sync is complete.
func (sm *SyncManager) handleHeadersMsg(hmsg *headersMsg) {
peer := hmsg.peer
_, exists := sm.peerStates[peer]
@@ -1067,102 +1068,53 @@ func (sm *SyncManager) handleHeadersMsg(hmsg *headersMsg) {
return
}
- // The remote peer is misbehaving if we didn't request headers.
+ // Nothing to do for an empty headers message.
msg := hmsg.headers
numHeaders := len(msg.Headers)
- if !sm.headersFirstMode {
- log.Warnf("Got %d unrequested headers from %s -- "+
- "disconnecting", numHeaders, peer.Addr())
- peer.Disconnect()
- return
- }
-
- // Nothing to do for an empty headers message.
if numHeaders == 0 {
return
}
- // Process all of the received headers ensuring each one connects to the
- // previous and that checkpoints match.
- receivedCheckpoint := false
- var finalHash *chainhash.Hash
for _, blockHeader := range msg.Headers {
- blockHash := blockHeader.BlockHash()
- finalHash = &blockHash
-
- // Ensure there is a previous header to compare against.
- prevNodeEl := sm.headerList.Back()
- if prevNodeEl == nil {
- log.Warnf("Header list does not contain a previous" +
- "element as expected -- disconnecting peer")
+ _, err := sm.chain.ProcessBlockHeader(
+ blockHeader, blockchain.BFNone, false,
+ )
+ if err != nil {
+ log.Warnf("Received block header from peer %v "+
+ "failed header verification -- disconnecting",
+ peer.Addr())
peer.Disconnect()
return
}
- // Ensure the header properly connects to the previous one and
- // add it to the list of headers.
- node := headerNode{hash: &blockHash}
- prevNode := prevNodeEl.Value.(*headerNode)
- if prevNode.hash.IsEqual(&blockHeader.PrevBlock) {
- node.height = prevNode.height + 1
- e := sm.headerList.PushBack(&node)
- if sm.startHeader == nil {
- sm.startHeader = e
- }
- } else {
- log.Warnf("Received block header that does not "+
- "properly connect to the chain from peer %s "+
- "-- disconnecting", peer.Addr())
- peer.Disconnect()
+ sm.progressLogger.SetLastLogTime(time.Now())
+ }
+
+ bestHash, bestHeight := sm.chain.BestHeader()
+ if sm.headersFirstMode {
+ if sm.syncPeer == nil {
+ // Return if we've disconnected from the syncPeer.
return
}
- // Verify the header at the next checkpoint height matches.
- if node.height == sm.nextCheckpoint.Height {
- if node.hash.IsEqual(sm.nextCheckpoint.Hash) {
- receivedCheckpoint = true
- log.Infof("Verified downloaded block "+
- "header against checkpoint at height "+
- "%d/hash %s", node.height, node.hash)
- } else {
- log.Warnf("Block header at height %d/hash "+
- "%s from peer %s does NOT match "+
- "expected checkpoint hash of %s -- "+
- "disconnecting", node.height,
- node.hash, peer.Addr(),
- sm.nextCheckpoint.Hash)
- peer.Disconnect()
- return
- }
- break
+ // Update the last progress time to prevent the stall handler
+ // from disconnecting the sync peer during header download.
+ if peer == sm.syncPeer {
+ sm.lastProgressTime = time.Now()
}
- }
- // When this header is a checkpoint, switch to fetching the blocks for
- // all of the headers since the last checkpoint.
- if receivedCheckpoint {
- // Since the first entry of the list is always the final block
- // that is already in the database and is only used to ensure
- // the next header links properly, it must be removed before
- // fetching the blocks.
- sm.headerList.Remove(sm.headerList.Front())
- log.Infof("Received %v block headers: Fetching blocks",
- sm.headerList.Len())
- sm.progressLogger.SetLastLogTime(time.Now())
- sm.fetchHeaderBlocks()
- return
+ if bestHeight < sm.syncPeer.LastBlock() {
+ locator := blockchain.BlockLocator([]*chainhash.Hash{&bestHash})
+ sm.syncPeer.PushGetHeadersMsg(locator, &zeroHash)
+ return
+ }
}
- // This header is not a checkpoint, so request the next batch of
- // headers starting from the latest known header and ending with the
- // next checkpoint.
- locator := blockchain.BlockLocator([]*chainhash.Hash{finalHash})
- err := peer.PushGetHeadersMsg(locator, sm.nextCheckpoint.Hash)
- if err != nil {
- log.Warnf("Failed to send getheaders message to "+
- "peer %s: %v", peer.Addr(), err)
- return
- }
+ bestHeaderHash, bestHeaderHeight := sm.chain.BestHeader()
+ log.Infof("downloaded headers to %v(%v) from peer %v "+
+ "-- now fetching blocks",
+ bestHeaderHash, bestHeaderHeight, hmsg.peer.String())
+ sm.fetchHeaderBlocks()
}
// handleNotFoundMsg handles notfound messages from all peers.
Why this scored 32/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.