This migrates the headers presync parameters (commitment_period and redownload_buffer_size) from being precomputed chainparams to being computed at runtime during startup. This has as advantages:
Less work for maintainers that need to occasionally run the current contrib/devtools/headerssync-params.py script.
Less room for mistakes in that process.
Automatically adapts based on time (as opposed to needing to predict the lifetime of the software).
To achieve this, the parameter search algorithm is ported to C++, and significant optimizations are applied to it step-by-step. The combination brings the runtime from minutes to ~3 ms. As an additional advantage, the result is now actually always optimal (within what can be determined using machine precision); the old code relied on (very reasonable) heuristics to guide the search.
To make the changes reviewable without understanding the details of the algorithmic and mathematical optimizations, the PR starts with a few refactors to bring the existing Python code in a state where it can be tested. A set of test vectors is then introduced (in Python), and those vectors are ported to, and remain valid, in all the further C++ versions too.
Disclaimer: this was written with significant assistance from Claude Opus 4.8 and Fable 5, which came up with the C++ port, the algorithmic and mathematical optimizations, and their implementation and testing. The code comments and commit messages are almost entirely written by me for clarity, but also to convince myself I understood all the changes.
DrahtBot
commented at 3:07 PM on July 2, 2026:
contributor
<!--e57a25ab6845829454e8d69fc972939a-->
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.
If your review is incorrectly listed, please copy-paste <code><!--meta-tag:bot-skip--></code> into the comment that the bot should ignore.
<!--174a7506f384e20aa4161008e828411d-->
Conflicts
Reviewers, this pull request conflicts with the following ones:
#35351 (net: Disallow invalid HeadersSyncState due to lagging clock by hodlinator)
#35301 (Silent Payments: Implement bip352 (take 2) by Eunovo)
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.
<!--5faf32d7da4f0f540f40219e4f7537a3-->
sipa force-pushed on Jul 2, 2026
DrahtBot added the label CI failed on Jul 2, 2026
sipa force-pushed on Jul 2, 2026
in
src/net_processing.cpp:2040
in
cafcdc385aoutdated
2031 | @@ -2029,12 +2032,25 @@ std::unique_ptr<PeerManager> PeerManager::make(CConnman& connman, AddrMan& addrm
2032 | return std::make_unique<PeerManagerImpl>(connman, addrman, banman, chainman, pool, warnings, opts);
2033 | }
2034 |
2035 | +//! Compute the headers sync DoS-protection parameters for the given chain, using its age (genesis2036 | +//! to now) and the height at which its minimum chain work was taken, and log the result.2037 | +static HeadersSyncParams MakeHeadersSyncParams(const CChainParams& chainparams)2038 | +{2039 | + const auto genesis_time{NodeSeconds{std::chrono::seconds{chainparams.GenesisBlock().nTime}}};2040 | + const auto timespan{std::chrono::duration_cast<std::chrono::seconds>(NodeClock::now() - genesis_time)};
Is there any anonymity breaking via NodeClock::now() influencing these parameters? (I think maybe not, but might be worth quantizing or similar to guarantee it?)
The choice of the commitment period should not be observable (besides a very minor time leak perhaps). However, the redownload buffer is observable in that an attacker can probably disconnect mid-redownload, and see how much you accepted afterwards.
I'll run some numbers to see how quickly these parameters change and suggest some quantization or noise.
It looks like the output parameters only change roughly every 22 days. I don't think quantizing further gains us much, especially as the attack only applies during IBD, so peers generally already know you just started.
sipa force-pushed on Jul 2, 2026
sipa force-pushed on Jul 2, 2026
DrahtBot
commented at 2:33 PM on July 9, 2026:
contributor
Could turn into draft while CI is red?
sipa force-pushed on Jul 9, 2026
sipa force-pushed on Jul 9, 2026
sipa force-pushed on Jul 9, 2026
sipa force-pushed on Jul 9, 2026
sipa force-pushed on Jul 9, 2026
DrahtBot removed the label CI failed on Jul 9, 2026
sipa force-pushed on Jul 10, 2026
sipa force-pushed on Jul 10, 2026
DrahtBot added the label CI failed on Jul 10, 2026
DrahtBot removed the label CI failed on Jul 10, 2026
sedited
commented at 10:58 AM on July 24, 2026:
contributor
Concept ACK
DrahtBot added the label Needs rebase on Jul 25, 2026
ajtowns
commented at 2:08 PM on August 6, 2026:
contributor
Approach ACK ; needs rebase if wanted for 32.0 I guess?
darosior
commented at 4:23 PM on August 6, 2026:
member
Concept ACK
sipa force-pushed on Aug 6, 2026
sipa
commented at 5:46 PM on August 6, 2026:
member
Rebased.
DrahtBot removed the label Needs rebase on Aug 6, 2026
DrahtBot added the label CI failed on Aug 6, 2026
DrahtBot removed the label CI failed on Aug 7, 2026
shuv-amp
commented at 4:11 PM on August 12, 2026:
contributor
next_height here is int, while m_current_height is int64_t and ValidateAndStoreRedownloadedHeader uses int64_t for the same step. For the long timewarp chains you mention, the presync height can exceed INT_MAX and overflow. Not a practical concern, but it should probably be int64_t to match the redownload path?
sipa force-pushed on Aug 14, 2026
sipa
commented at 5:36 PM on August 14, 2026:
member
@shuv-amp That's mostly independent of this PR, but added a commit to address that. I have also made a small simplification in the search algorithm.
Ready for more review.
shuv-amp
commented at 7:30 PM on August 14, 2026:
contributor
ComputeHeadersSyncParams() doesn't terminate for a small minchainwork_headers
combined with a short timespan:
30229 is the boundary; below it the region extends out to timespans around 1e4 s.
In attack_rate(), the left side of future_limit * prob < 1.0e-16 * rate can't be
negative, and the right side is exactly 0 once rate is below ~2.5e-308, so that
break stops being reachable. The limit check is the only other exit and Step 1's
Newton call doesn't pass one, so there it has none at all.
The Python this replaces has the same shape
(HEADER_BATCH_COUNT * prob < 1.0e-16 * rate * len(align_choices)) and doesn't
terminate at those arguments either, so it isn't new, it just matters more now that
it runs at startup.
Non-strict is enough, since prob decreases to exactly 0 and takes the left side
with it:
- if (future_limit * prob < 1.0e-16 * rate) break;
+ if (future_limit * prob <= 1.0e-16 * rate) break;
The 50 test vectors still reproduce with that, and 196/196 other inputs I compared
are unchanged.
Nothing in chainparams is close: testnet4 has the lowest minchainwork_height at
123613, and reaches an exponent of 531 against the 1075 where prob underflows. It
is the region a new chain starts in though, and 0 hangs a different way
(attack_headers is 0, so period is NaN and accept_forged_headers > 0.0 never
holds), so a guard on the input might be worth having too.
sipa force-pushed on Aug 19, 2026
sipa
commented at 6:55 PM on August 19, 2026:
member
@shuv-amp Fixed, and added some extra Assume()s in the code.
contrib: reduce headerssync-params.py output to just the parameters
In future commits, the logic in the headerssync-params.py tool will be
made significantly faster, and integrated into the Bitcoin Core node
software itself and run at runtime.
To prepare for that, remove all output from the tool beyond the actual
computation result:
* Drop the printing of the search parameters being explored, because
the search will become fast enough to not need progress output, and
because it interferes with tests.
* Drop the final memory configuration to maximally encapsulate the
computation logic.
* Drop memory_usage's now-unused second and third return values (the
per-scenario breakdowns that only the removed output consumed); it
now returns just the peak memory usage.
3082cc8ffc
contrib: drop the RANDOMIZE_OFFSET option from headerssync-params.py
RANDOMIZE_OFFSET was a configurable option, introduced in the early
stages of the headers-presync work to investigate whether randomizing
the commitment offset was worth it. As that turned out to be the case,
the actual implementation ended up using it all the time.
Thus, remove the configurable and treat it as always True, making the
logic slightly simpler.
This moves the computationally expensive part of the optimization
search into a function that only depends on 3 quantities:
- The maximum number of headers a chain can have.
- The number of known valid headers in the main chain.
- The (fractional) number of headers an attacker is allowed to insert
into a victim's database per attack.
The goal is to make it possible to introduce tests in later commits
that cover the hard part, without hardcoding config parameters that
would invalidate the tests if changed.
cd7f5546a5
contrib: break headerssync-params ties deterministically
It is possible to have multiple adjacent period values for which the
peak memory usage is identical. The current randomized search will find
a random one in this case, which is not great for testing.
Add a postprocessing step that finds the minimum period value for the
optimal found peak memory usage value, so the result is deterministic.
Also make the attack_rate early-exit more robust: rather than comparing
a single batch's contribution against the accumulated rate, compare a
bound on the sum of all remaining batches. This gives a stronger
guarantee that the reported solution is actually the optimal one, and
it also turns out to be needed for the results to stay identical across
the optimizations in later commits.
a48a196073
contrib: drop the ASSUME_CONVEX option from headerssync-params.py
ASSUME_CONVEX assumes the period-to-memory-usage mapping (at the optimal buffer
size for each period) is convex, which lets the search discard whole ranges of
periods at once. It defaulted to True and was only ever run that way; the False
path existed solely as a slower fallback with a stronger guarantee.
Drop the option and always assume convexity, simplifying the search. A later
commit replaces this random search entirely with a solver that finds the true
optimum directly, without any convexity assumption, so the stronger-guarantee
fallback is not needed in the meantime.
cba89c86a8
contrib: add test vectors to headerssync-params.py
This adds a collection of 50 randomly generated test vectors to the
tool. This is in preparation for converting the code to C++ and
applying significant optimization, while remaining confident that the
behavior does not change.
The test vectors are chosen to be inputs that are close to ones where
the optimal output changes, to test the accuracy of the calculation.
Run with pypy3, which evaluates the search far faster than CPython:
pypy3 contrib/devtools/headerssync-params.py --selftest
9f916dd33b
headerssync: port the sync-parameter optimizer to C++
This adds a C++ implementation of the headerssync-params.py tool's
parameter search logic to headerssync.cpp directly, together with its
unit test vectors. The tool's commentary explaining the goals of the
search is copied along with it.
This is rather inefficient for now, but will be optimized in further
commits. Because of that, the unit test only checks one randomly chosen
vector per run for now; a later commit makes the computation fast
enough to check all of them on every run.
fdfb278e48
headerssync: compute attack_rate in closed form
The attack rate averages the not-yet-detected probability over all
{period} randomized commitment offsets. Doing so with an explicit loop
over the alignments costs O(period) per processed batch. This can be
computed much more efficiently however, by counting how many choices
for the alignment have floor(forged_headers / period) commitments, how
many have ceil(forged_headers / period), and weighting both
accordingly. This results in just an O(1) cost per batch.
27fa53c9a0
sipa force-pushed on Aug 20, 2026
sipa
commented at 2:53 PM on August 20, 2026:
member
I made a further simplification to the algorithm, and also added a fuzz test that exercises the code far beyond its intended parameter range, so catch meaningless results, crashes, or infinite loops. The fuzz tests are relatively slow (for extreme values it's just above 1/second, through normal running closer to 50/second), but it's also not a particularly large or complicated space to explore.
fjahr
commented at 9:19 AM on August 21, 2026:
contributor
Concept ACK
headerssync: simplify find_bufsize (preparation)
The find_bufsize algorithm takes two arguments that help its search: a
minimum bufsize to consider, and an upper bound on the amount of
memory. After the changes in the next commit, the memory bound will not
be needed anymore. Get rid of it beforehand here, keeping the
min_bufsize argument and the tracking that feeds it.
0438607e82
headerssync: replace randomized search with better algorithm
Replace the randomized search over all candidate periods with a much
faster deterministic algorithm:
Step 1: Solve the continuous relaxation of the problem, where period and
bufsize can be arbitrary real numbers, using Newton-Raphson
iterations, and round the resulting period to an integer.
Step 2: Try all integer periods within 2 of that rounded value, and
return the one whose corresponding bufsize results in the lowest
peak memory usage.
Unlike the randomized search, this is not guaranteed to find the exactly
optimal configuration, as the true integer optimum could in principle
fall outside the range tried in Step 2. It is however guaranteed to be
very close: rounding the continuous solution to integers increases its
memory usage by only a tiny fraction, and the continuous optimum is a
lower bound on what any integer solution can achieve. In practice the
difference does not appear to materialize at all: the result matches the
exact optimum for all test vectors.
This also makes the computation fast enough that the unit test can check
all test vectors on every run again, instead of one randomly chosen one.
The algorithm's continuous relaxation relies on max_headers being
larger than 2 * minchainwork_headers, which any consistent clock
satisfies by a wide margin. Assume() this in the optimizer, and clamp
in ComputeHeadersSyncParams, so that mocked or badly wrong clocks
cannot produce degenerate inputs (which could otherwise fail to
converge).
The Newton-Raphson step can overshoot arbitrarily -- even to negative
gamma values -- when the continuous optimum lies at a tiny period, from
where subsequent iterations would degenerate into NaNs. Clamp gamma to a
broad range on every iteration to keep it well-defined.
Each candidate period's bufsize search is seeded with the result for
the previous period, minus one: the required bufsize can only grow as
the period increases, and the safety margin of one means that even a
floating-point-level violation of that argument cannot affect the
result.
09fb562fb9
headerssync: make next_height type consistent963a2c5ae5
net_processing: compute headers sync parameters at runtime
So far, the headers sync parameters are committed to the repository,
as part of the chain parameters. This requires maintainers to
occasionally recompute them, based on a prediction of how long the
release is likely to be used.
With the code for the optimization algorithm ported to C++ and
integrated in headerssync.cpp, it becomes possible to instead do this
computation at runtime. This is less work, with fewer ways for
maintainers and reviewers to get it wrong, and automatically adjusts
based on clock time.
Tests that construct many PeerManager instances can avoid recomputing
the parameters each time by passing a precomputed value through
PeerManager::Options. The fuzz targets that construct one per iteration
pass a fixed constant, which both avoids the recomputation and makes
their behavior independent of the (mocked) clock at construction time.
Add two fuzz targets covering the headers sync parameter optimizer over
the entire range of legal inputs, asserting that the resulting
configuration is sane (and, implicitly, that the computation terminates
without crashing or throwing):
- headers_sync_params_inner exercises ComputeHeadersSyncParamsInner
directly. minchainwork_headers is a block height and thus fits an int;
max_headers must exceed twice that (which the caller guarantees by
clamping) and is at most 6 headers/s over the longest timespan any
clock can produce; attack_headers is parametrized logarithmically as a
number of bits of security asked from the commitment structure, whose
legal range follows from the optimizer's continuous relaxation.
- headers_sync_params exercises ComputeHeadersSyncParams with any
timespan a clock -- even a mocked or badly wrong one -- can produce,
and any int-height minchainwork_headers.
Both targets check that the returned period is positive and no larger
than the minimum-chainwork chain, and that the redownload buffer covers
at least one commitment period whenever the acceptable attack rate is
low enough to require any commitment verification at all (which it
always is for the rates ComputeHeadersSyncParams uses).
e330ad91d7
sipa force-pushed on Aug 21, 2026
DrahtBot added the label Needs rebase on Aug 31, 2026
DrahtBot
commented at 4:53 PM on August 31, 2026:
contributor
<!--cf906140f33d8803c4a75a2196329ecb-->
🐙 This pull request conflicts with the target branch and needs rebase.
darosior
commented at 9:15 PM on September 4, 2026:
member
The HeadersSyncParams definition can now be moved from chainparams to the headersync module.
This is a metadata mirror of the GitHub repository
bitcoin/bitcoin.
This site is not affiliated with GitHub.
Content is generated from a GitHub metadata backup.
generated: 2026-09-08 11:51 UTC
This site is hosted by @0xB10C More mirrored repositories can be found on mirror.b10c.me