net: Disallow invalid HeadersSyncState due to lagging clock - #35351
net: Disallow invalid HeadersSyncState due to lagging clock#35351hodlinator wants to merge 2 commits into
Conversation
|
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers. Code Coverage & BenchmarksFor details see: https://corecheck.dev/bitcoin/bitcoin/pulls/35351. ReviewsSee the guideline and AI policy for information on the review process.
If your review is incorrectly listed, please copy-paste ConflictsReviewers, this pull request conflicts with the following ones:
If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first. LLM Linter (✨ experimental)Possible places where comparison-specific test macros should replace generic comparisons:
2026-08-19 11:04:18 |
|
Concept ACK |
|
Been thinking more about this during the weekend. One could posit that Bitcoin nodes should be more shelf stable in a post-apocalyptic future where NTP servers are unreachable. Re-establishing a shared accurate definition of current UTC time just from observing the sun's position could be challenging. In that kind of scenario, preventing the node from continuing, as this PR does, could be problematic. So should we do like #35208 and just set Although, if clocks in the network disagree too much about UTC time, block propagation would also be suffering due to the rule to not accept blocks from too far into the future ( |
l0rinc
left a comment
There was a problem hiding this comment.
I'm not sure this is going in the right direction, seems weird to discover time drift in the constructor and catch on use site - instead of sanitizing the value before call.
Please see https://github.com/l0rinc/bitcoin/pull/186/commits for how I imagined simplifying this a lot more.
hodlinator
left a comment
There was a problem hiding this comment.
Thanks for your review & suggestions @l0rinc!
Pushed the straightforward ones for now.
I'm not sure this is going in the right direction, seems weird to discover time drift in the constructor and catch on use site - instead of sanitizing the value before call.
Curious what you think about sending unsigned max_commitments into HeadersSyncState() (see inline comment).
|
(Rebased against master in latest push to resolve conflicts). |
11ae9d4 to
6ee2df1
Compare
l0rinc
left a comment
There was a problem hiding this comment.
lgtm, please see my remaining suggestion to reduce side-effects and simplify testing
hodlinator
left a comment
There was a problem hiding this comment.
(Latest few pushes after 1c3ad92d0bdcc66f252f647deb58f8becfce2772 attempt to reconcile differing viewpoints which me & @l0rinc partially discussed out-of-band last week).
|
ACK 8b680b2 @optout21, @vasild, @dergoegge, this is an alternative to the original acked PR, your re-review here would be welcome. |
|
Concept ACK I thought the original PR was also fine. Code here also looks fine, but I won't prioritize giving this a full review. |
|
@hodlinator, can you please rebase the change? |
8b680b2 to
70ec860
Compare
l0rinc
left a comment
There was a problem hiding this comment.
ACK 70ec860
The approach looks correct: reject the invalid elapsed-time calculation before constructing HeadersSyncState, then trigger fatal shutdown.
I left a few non-blocking suggestions and some prose is also stale:
- The PR description still says the constructor throws an exception.
- The first commit message says
commitment_period=1, although the test usesCOMMITMENT_PERIOD(600), and says the fix flips the test although it replaces it with direct boundary checks. - The final commit subject says "local tip", but the comparison uses the chain-start header MTP, which is not necessarily the tip. Its body could also briefly explain why this condition warrants shutdown.
70ec860 to
0799f3b
Compare
hodlinator
left a comment
There was a problem hiding this comment.
Thanks for your latest review @l0rinc, incorporated all of it in the latest push, with one exception:
You disagree that the chain start header is the same as the local tip - when would that not be the case?
Beyond your feedback I also added/corrected some comments in net_processing.cpp in the last commit.
There was a problem hiding this comment.
ACK 0799f3b
This correctly rejects a negative elapsed interval before it can become a large unsigned presync commitment cap, prevents constructing an invalid HeadersSyncState, and routes the unexpected clock condition through fatal shutdown. The unit test covers the exact boundary, while the functional test covers the P2P-to-shutdown path. The focused unit and functional tests pass.
I left some suggestions to make the review easier by separating risky changes from pure refactors (which also help with making the characterization test changes minimal) - I applied these suggestions in https://github.com/l0rinc/bitcoin/pull/276/commits during local review.
I'm also okay with merging this as is and happy to rereview if any of the suggestion are taken.
You disagree that the chain start header is the same as the local tip - when would that not be the case?
My understanding is that the relevant presync path receives a peer-supplied full headers message, then looks up chain_start_header from its first header.
As far as I can tell it may be any known block (we only require valid PoW, continuity, and a connection to our block index), for example an older active-chain block or a stale-fork block. I’d call it the chain-start header rather than the local tip.
0799f3b to
1695d30
Compare
|
Thanks for the solid feedback again @l0rinc, agree it should make it easier to review. Took the changes from your branch with some variation, hope it's acceptable to you. The largest deviation from your l0rinc#276 end state is that I decided to |
| /*chain_start=*/start_index, | ||
| /*minimum_required_work=*/min_work); | ||
| /*minimum_required_work=*/min_work, | ||
| /*max_commitments=*/*max_commitments); |
There was a problem hiding this comment.
8593ec4 refactor(p2p): Pass max commitments into HeadersSyncState():
nit, if you need to touch again:
| /*max_commitments=*/*max_commitments); | |
| *max_commitments); |
|
Looked at the patch set a few days ago and it looks correct and fine to me, but I'm not completely sure yet of the approach. Could it be better to just crash immediately with an abort if the system clock is that far out of sync, akin to maflcko's patch here: https://github.com/bitcoin/bitcoin/pull/35676/changes#diff-c24fd5f39e56e513a81d84b494862608c1620c194e5a7efbded93953b6ef43e2R106 ? Externalizing the max commitments calculation does feel like bit of a code smell to me. |
There was a problem hiding this comment.
Looked at the patch set a few days ago and it looks correct and fine to me, but I'm not completely sure yet of the approach. Could it be better to just crash immediately with an abort if the system clock is that far out of sync, akin to maflcko's patch here: https://github.com/bitcoin/bitcoin/pull/35676/changes#diff-c24fd5f39e56e513a81d84b494862608c1620c194e5a7efbded93953b6ef43e2R106 ?
with "immediately", do you mean with that to check the system time vs best header in init and crash there without ever starting up network operations?
Externalizing the max commitments calculation does feel like bit of a code smell to me.
Another option could be to only do the time check with chain_start_header.GetMedianTimePast() in TryLowWorkHeadersSync(), and leave the max commitments calculation inside HeadersSync- so that we can change the internal check to an Assume(max_seconds_since_start >= 0);. Having to repeat the time check seems irrelevant performance-wise.
The node currently continues low-work headers presync and requests more headers when its clock is more than `MAX_FUTURE_BLOCK_TIME` behind the chain-start MTP. Record this behavior before the follow-up rejects the invalid elapsed-time calculation. The unit test covers HeadersSyncState() behavior while the functional test covers net_processing.cpp behavior. Co-authored-by: Lőrinc <pap.lorinc@gmail.com>
1695d30 to
4cf06b1
Compare
|
Latest push (4cf06b1) switches back to It reverts back to throwing an exception from Footnotes |
We should not proceed syncing headers from peers when the local system clock is incorrectly set. A node with a system clock set too far back will typically fail early during startup when the chainstate detects the tip to be too far in the future. This means that in practice we don't expect the failure to ever happen in net_processing.cpp. An exception is thrown from HeadersSyncState() in order to only compute the error condition once. An alternative would be to compute it a second time in TryLowWorkHeadersSync() to guard against calling HeadersSyncState(), and have an assert inside HeadersSyncState(). We shut down the process so possible resource leaks due to the exception should not be an issue, although none have been spotted. Throwing an exception also keeps the unit test straightforward. Co-authored-by: Lőrinc <pap.lorinc@gmail.com>
4cf06b1 to
ff3e2e4
Compare
|
diff and code review ACK ff3e2e4 nit: the PR description still describes the old graceful-shutdown |
Problem
Headers presync computes
m_max_commitmentsfrom the elapsed time since the chain-start MTP plusMAX_FUTURE_BLOCK_TIME. When the local system clock is more thanMAX_FUTURE_BLOCK_TIMEbehind the chain-start MTP, that elapsed value is negative, but it is used in arithmetic assigned to the unsigned commitment cap. This can turn the intended zero bound into a large cap, letting low-work headers presync continue instead of aborting when a reasonable commitment cap would have been exceeded.Fix
Instead of allowing an invalid
HeadersSyncStateobject to be created, emit an error and abort the node process.Typically, the node will detect that the system clock is set too far in the past when comparing it to the chain tip during chain state loading and shut down before we start syncing headers. So in practice this is very unlikely to make a difference (might be possible if the system clock jumps backwards after we loaded the chain state).
Commits
Replaces #35208 which was clamping
m_max_commitmentsto zero and then letting theHeadersSyncStateconsume headers until the block height either reached the the nextcommitment_periodpoint and aborted, or reached the minimum work threshold and succeeded (possible when having been offline for >144 blocks).