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.
This treats the peer as caught up when the work it has shown so far during presync (GetPresyncWork()) is at least our tip's work, the same way a stored header with enough work already resets the timeout. m_headers_sync_mutex is never held while taking cs_main, so taking it inside ConsiderEviction doesn't create a lock order problem.
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.