1bbf4a1 headerssync: add computation of optimal sync parameters:
I'd find this easier to rerun as a script in contrib/testgen/, with a reference here.
That would also let Python tooling check it and keep the test easier to read.
<details>
<summary>Move the generator to contrib/testgen</summary>
diff --git a/contrib/testgen/README.md b/contrib/testgen/README.md
index 2f0288df16..4190c6d89d 100644
--- a/contrib/testgen/README.md
+++ b/contrib/testgen/README.md
@@ -6,3 +6,7 @@ To use inside a scripted-diff (or just execute directly):
./gen_key_io_test_vectors.py valid 70 > ../../src/test/data/key_io_valid.json
./gen_key_io_test_vectors.py invalid 70 > ../../src/test/data/key_io_invalid.json
+
+To regenerate the headers sync parameter records in `src/test/headerssync_tests.cpp`, run from the repository root:
+
+ pypy3 contrib/testgen/gen_headerssync_test_vectors.py
diff --git a/contrib/testgen/gen_headerssync_test_vectors.py b/contrib/testgen/gen_headerssync_test_vectors.py
new file mode 100644
index 0000000000..e963085307
--- /dev/null
+++ b/contrib/testgen/gen_headerssync_test_vectors.py
@@ -0,0 +1,81 @@
+#!/usr/bin/env python3
+# Copyright (c) The Bitcoin Core developers
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+"""Generate headers sync parameter test vectors by exhaustively searching all periods."""
+
+import math
+import random
+
+# Both must match src/headerssync.cpp.
+COMPACT_HEADER_SIZE = 48 * 8
+LIMIT_FRACTION = (0.2 / 600.0) / (6000000000.0 / (81 * 8))
+# Reject solutions which are only better by this much relatively speaking (floating point precision).
+MARGIN = 1e-9
+
+def memory_usage(max_headers, mch, period, bufsize):
+ return max(max_headers // period, mch // period + bufsize * COMPACT_HEADER_SIZE)
+
+def round_error(value):
+ return abs(value - round(value)) / value
+
+def rate(period, bufsize):
+ """Expected number of forged headers accepted per attack: the closed form of headerssync.cpp."""
+ k, m = divmod(bufsize, period)
+ return 2.0**-k * (6 * period**2 - 4 * period * m + m * m - 2 * period + m) / (4 * period)
+
+def optimum(max_headers, mch, attack_headers):
+ """Exhaustively search for the optimal (period, bufsize), and the runner-up it beats."""
+ best, runner_up, bufsize = None, None, 1
+ for period in range(1, mch + 1):
+ while rate(period, bufsize) >= attack_headers: # bufsize never decreases with period
+ bufsize += 1
+ candidate = (memory_usage(max_headers, mch, period, bufsize), period, bufsize)
+ if best is None or candidate < best:
+ best, runner_up = candidate, best
+ elif runner_up is None or candidate < runner_up:
+ runner_up = candidate
+ return best, runner_up
+
+def sample():
+ """One record, or None if its inputs are illegal or its optimum is too ambiguous."""
+ # Start with random initial values:
+ # - A timespan, log-normally distributed with mode 12 years and mean 18 years.
+ sigma = math.sqrt(math.log(18.0 / 12.0) / 1.5)
+ timespan = random.lognormvariate(math.log(12.0 * 365.25 * 86400) + sigma**2, sigma)
+ # - minchainwork_headers, exponentially distributed with mean 1000000.
+ mch = random.expovariate(1.0 / 1000000.0)
+ # - attack_headers, log-normally distributed with median LIMIT_FRACTION * mch, sigma 1.5.
+ attack_headers = random.lognormvariate(math.log(LIMIT_FRACTION * mch), 1.5)
+ # Convert to a starting point in (max_headers, minchainwork_headers, attack_headers) space.
+ start = [6.0 * timespan, mch, attack_headers]
+ # Pick a random point on the 3D sphere with radius 1/200 as (logarithm of) walk step factor to explore.
+ direction = [random.gauss(0.0, 1.0) for _ in range(3)]
+ step = [d * 0.005 / math.hypot(*direction) for d in direction]
+ # Pick 100 points, starting from start, each a further factor exp(step) away from the previous one.
+ walk = ([c * math.exp(d * i) for c, d in zip(start, step)] for i in range(100))
+ # Among those 100 points, pick the one whose max_headers and mch are closest to integers.
+ max_headers, mch, attack_headers = min(walk, key=lambda pt: max(round_error(pt[0]), round_error(pt[1])))
+ max_headers, mch = round(max_headers), round(mch)
+ # Reject if outside the inputs the optimizer is defined for.
+ if mch < 1 or max_headers <= 2 * mch:
+ return None
+ best, runner_up = optimum(max_headers, mch, attack_headers)
+ _, period, bufsize = best
+ # Reject if too close to the runner up, to avoid depending on floating point rounding behavior.
+ if not runner_up or runner_up[0] <= best[0] * (1.0 + MARGIN):
+ return None # another period is just as good
+ if not (rate(period, bufsize) < attack_headers * (1.0 - MARGIN)
+ and rate(period, bufsize - 1) > attack_headers * (1.0 + MARGIN)):
+ return None # another bufsize is just as good
+ return max_headers, mch, attack_headers, period, bufsize
+
+random.seed(0)
+count = 0
+while count < 50:
+ record = sample()
+ if not record:
+ continue
+ print(" {%d, %d, %r, %d, %d}," % record)
+ count += 1
diff --git a/src/test/headerssync_tests.cpp b/src/test/headerssync_tests.cpp
index 363df619f9..4c4e7b2b64 100644
--- a/src/test/headerssync_tests.cpp
+++ b/src/test/headerssync_tests.cpp
@@ -16,84 +16,9 @@ BOOST_AUTO_TEST_SUITE(headerssync_tests)
// - inputs: max_headers, minchainwork_headers, attack_headers
// - expected outputs: period, bufsize
//
-// The records below were generated by the following script, which finds the exact optimum for each
-// input through an exhaustive search over all periods, independent of the search strategy the
-// implementation uses. Run under PyPy for faster results.
-//
-// import math
-// import random
-//
-// # Both must match src/headerssync.cpp.
-// COMPACT_HEADER_SIZE = 48 * 8
-// LIMIT_FRACTION = (0.2 / 600.0) / (6000000000.0 / (81 * 8))
-// # Reject solutions which are only better by this much relatively speaking (floating point precision).
-// MARGIN = 1e-9
-//
-// def memory_usage(max_headers, mch, period, bufsize):
-// return max(max_headers // period, mch // period + bufsize * COMPACT_HEADER_SIZE)
-//
-// def round_error(value):
-// return abs(value - round(value)) / value
-//
-// def rate(period, bufsize):
-// """Expected number of forged headers accepted per attack: the closed form of headerssync.cpp."""
-// k, m = divmod(bufsize, period)
-// return 2.0**-k * (6 * period**2 - 4 * period * m + m * m - 2 * period + m) / (4 * period)
-//
-// def optimum(max_headers, mch, attack_headers):
-// """Exhaustively search for the optimal (period, bufsize), and the runner-up it beats."""
-// best, runner_up, bufsize = None, None, 1
-// for period in range(1, mch + 1):
-// while rate(period, bufsize) >= attack_headers: # bufsize never decreases with period
-// bufsize += 1
-// candidate = (memory_usage(max_headers, mch, period, bufsize), period, bufsize)
-// if best is None or candidate < best:
-// best, runner_up = candidate, best
-// elif runner_up is None or candidate < runner_up:
-// runner_up = candidate
-// return best, runner_up
-//
-// def sample():
-// """One record, or None if its inputs are illegal or its optimum is too ambiguous."""
-// # Start with random initial values:
-// # - A timespan, log-normally distributed with mode 12 years and mean 18 years.
-// sigma = math.sqrt(math.log(18.0 / 12.0) / 1.5)
-// timespan = random.lognormvariate(math.log(12.0 * 365.25 * 86400) + sigma**2, sigma)
-// # - minchainwork_headers, exponentially distributed with mean 1000000.
-// mch = random.expovariate(1.0 / 1000000.0)
-// # - attack_headers, log-normally distributed with median LIMIT_FRACTION * mch, sigma 1.5.
-// attack_headers = random.lognormvariate(math.log(LIMIT_FRACTION * mch), 1.5)
-// # Convert to a starting point in (max_headers, minchainwork_headers, attack_headers) space.
-// start = [6.0 * timespan, mch, attack_headers]
-// # Pick a random point on the 3D sphere with radius 1/200 as (logarithm of) walk step factor to explore.
-// direction = [random.gauss(0.0, 1.0) for _ in range(3)]
-// step = [d * 0.005 / math.hypot(*direction) for d in direction]
-// # Pick 100 points, starting from start, each a further factor exp(step) away from the previous one.
-// walk = ([c * math.exp(d * i) for c, d in zip(start, step)] for i in range(100))
-// # Among those 100 points, pick the one whose max_headers and mch are closest to integers.
-// max_headers, mch, attack_headers = min(walk, key=lambda pt: max(round_error(pt[0]), round_error(pt[1])))
-// max_headers, mch = round(max_headers), round(mch)
-// # Reject if outside the inputs the optimizer is defined for.
-// if mch < 1 or max_headers <= 2 * mch:
-// return None
-// best, runner_up = optimum(max_headers, mch, attack_headers)
-// _, period, bufsize = best
-// # Reject if too close to the runner up, to avoid depending on floating point rounding behavior.
-// if not runner_up or runner_up[0] <= best[0] * (1.0 + MARGIN):
-// return None # another period is just as good
-// if not (rate(period, bufsize) < attack_headers * (1.0 - MARGIN)
-// and rate(period, bufsize - 1) > attack_headers * (1.0 + MARGIN)):
-// return None # another bufsize is just as good
-// return max_headers, mch, attack_headers, period, bufsize
-//
-// random.seed(0)
-// count = 0
-// while count < 50:
-// record = sample()
-// if not record:
-// continue
-// print(" {%d, %d, %r, %d, %d}," % record)
-// count += 1
+// The records below were generated by contrib/testgen/gen_headerssync_test_vectors.py
+// The generator finds the exact optimum through an exhaustive search over all periods, independently of the implementation's search strategy
+// Run from the repository root: pypy3 contrib/testgen/gen_headerssync_test_vectors.py
BOOST_AUTO_TEST_CASE(compute_headers_sync_params)
{
</details>