Repository navigation
feat(p2p): per-peer fork download budget - #2133
nullPointerEnjoyer wants to merge 11 commits into
Conversation
Downloading and validating the blocks behind announced headers is an expensive operation. A peer could previously make us perform an unbounded amount of such work over time by repeatedly announcing distinct header chains that don't extend our tip. Introduce a token-bucket budget (capacity + refill interval, both configurable via the p2p protocol config) that every non-empty header list will be charged against; header lists received during the initial block download are exempt so that node bootstrap can't be stalled. No wire-level changes: the budget only gates our own block requests.
Charge every non-empty header list (including tip-anchored ones: a miner can produce an unlimited number of distinct valid children of our tip, so exempting them would leave the cost attack open) against the peer's budget before downloading the announced blocks. An exhausted budget defers the list: the peer is not punished (no ban score, no disconnect; it hasn't violated the protocol), the list is expected to arrive later via a scheduled header request retry that fires once per refill interval and is subject to the budget itself. Also stop requesting more headers from a peer that repeatedly sends already-known header lists while claiming it may have more of them, which would otherwise keep us issuing header requests forever at no cost to it.
Cover the three behaviors of the budget: deferral without punishment followed by recovery after a refill, budget-limited tip-anchored fork variants, and the initial block download exemption.
Addresses an OCR review finding: keep the pre-existing ProtocolConfig test literals stable so that adding a config field in the future doesn't require touching them again.
Addresses an OCR review finding: keep the helper consistent with the existing make_new_blocks helper style by holding the previous block as a borrow instead of cloning it on every iteration.
The budget retry timer runs on the real tokio clock while refills are driven by the mocked one; a long interval leaves the retry pending for a long real time, which can interleave with the sync loops when the test machine is under load.
Addresses OCR review findings: merge the duplicated refill-interval rationale into one accurate comment and describe the actual fork topology of the tip-anchored variants test, including the guarantee it pins down (the budget is charged before the anchoring check, so lists anchored at unknown blocks are deferred rather than punished).
The fork download budget retry had two weaknesses: - The retry slept inside a select! branch with a variable duration, so the sleep was recreated on every loop iteration and could be postponed indefinitely by message traffic. Move the retry deadline check into the loop body and add an unconditional fixed-interval tick arm to guarantee wakeups when the peer is idle. - A repeated deferral re-scheduled the retry further into the future, so announcements that arrive late could keep postponing the fetch. Keep the earliest scheduled retry instead, so a new deferral never postpones a retry the budget may already have recovered for.
|
🔍 OpenCodeReview found 2 issue(s) in this PR.
📄
|
The retry poll tick in the block sync manager event loop ran unconditionally, waking the loop every second even when no fork budget retry was pending. That exposed a pre-existing latent issue under mocked clocks: stalling checks then ran on a clock that jumped forward, e.g. in the ping functional test, disconnecting a peer on a stalling deadline no real activity would have triggered. Arm the tick branch only while a retry is pending, mirroring the existing stalling select arm, so the loop's wake behavior is unchanged otherwise. Also charge at most the configured fork download limit for one header list: a list larger than the budget capacity could never be paid for by even a full bucket and would be deferred forever. A capacity of 0 now disables the budget (the charge is always payable).
A pending fork budget retry whose deadline has passed is only cleared when the node provides the Blocks service to the peer. When it doesn't, the schedule stayed armed forever, making the retry tick wake the event loop every poll interval without any progress. Clear the schedule in that case too; a future deferral re-arms it, and the deferred data is recovered when the peer re-announces it, as before the retry mechanism existed.
Summary
Test plan