While our best header is below -minimumchainwork, a peer's headers first go through the PRESYNC phase of HeadersSyncState, and none of them are stored. So pindexBestKnownBlock stays null for the peer we're syncing from, and ConsiderEviction (net_processing.cpp:5504) treats it like an outbound peer that never announced a block:
- it starts
CHAIN_SYNC_TIMEOUT(20 min) - it sends a getheaders to "verify chain work"
- after
HEADERS_RESPONSE_TIMEit disconnects withOutbound peer has old chain, best known block = <none>
This happens even though the peer is still sending headers. The presync progress is lost and the next peer starts from scratch.
On mainnet presync is roughly 480 headers round trips. If that takes more than ~22 minutes (about 2.5 s per round trip, e.g. over slow Tor or I2P), the sync peer gets evicted before presync can finish, and the same thing can happen with the next peer. The chain sync logic is from #11490, presync came later in #25717, and the two were never connected.
The timeout is not reset on presync work alone. When it is set, the work the peer has shown during presync is recorded, and when it expires the timeout is renewed for another CHAIN_SYNC_TIMEOUT only if that work has increased since. A peer that stops sending headers is therefore still asked once with getheaders and then disconnected, within about two periods (~42 minutes), as before. m_headers_sync_mutex is never held while taking cs_main, and p2p_headers_sync_with_minchainwork.py and p2p_sendheaders.py also pass with DEBUG_LOCKORDER.
The new test_presync_peer_not_evicted in p2p_headers_sync_with_minchainwork.py restarts a node with -minimumchainwork=0x20000 (65536 headers). An outbound peer then sends it one full headers message per mocked minute for 25 minutes, so presync is still running at the end. On master the peer is disconnected after 24 minutes. With the fix it stays connected and presynced_headers keeps going up. p2p_outbound_eviction.py, p2p_eviction.py and feature_minchainwork.py still pass.
test_presync_peer_stalled_is_evicted covers the other side: a single outbound peer sends one headers message and then only answers pings, and it is disconnected after about 42 minutes of mocktime. That test fails without the progress check.