{
 "number": 35642,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/35642",
 "title": "headersync: do parameter search at runtime",
 "author": "sipa",
 "author_association": "MEMBER",
 "created_at": "2026-07-02T15:06:59Z",
 "updated_at": "2026-10-29T14:17:20Z",
 "description_hash": "79a1aa2d1d55af65",
 "age_days": 122,
 "draft": false,
 "labels": [],
 "milestone": null,
 "base": "master",
 "head_sha": "0316d0dc9ec6a1abbe84489c44a47a2dd94c79ee",
 "head_ref": "202607_headerssync_cpp",
 "head_repo": "sipa/bitcoin",
 "head_history": [
  {
   "t": "2026-07-02T15:07:50Z",
   "sha": "befe560698c6227a9364d93f4fd797943c22e1cc"
  },
  {
   "t": "2026-07-02T15:14:55Z",
   "sha": "cafcdc385aaa266ee8e98d048206059c6bb428b7"
  },
  {
   "t": "2026-07-02T17:41:23Z",
   "sha": "6ea06616798b0f39dc9f868b4dd702754d45851b"
  },
  {
   "t": "2026-07-02T20:50:00Z",
   "sha": "3dbae98576823d24117b2ae90659addb3ffc2325"
  },
  {
   "t": "2026-07-09T17:55:05Z",
   "sha": "8cd423b86f90092b714d16a853622baff92066b9"
  },
  {
   "t": "2026-07-09T18:01:48Z",
   "sha": "bae2fd5ab05580c0287123f13daa3f2043e0524a"
  },
  {
   "t": "2026-07-09T20:24:06Z",
   "sha": "e8df9d86a5a5402ea65c19beb8d449d58fe743b7"
  },
  {
   "t": "2026-07-09T20:31:42Z",
   "sha": "3e9535e2d808709b8aab113ffd4ea4cf3f65f9c3"
  },
  {
   "t": "2026-07-09T21:58:35Z",
   "sha": "b985a4130b45e92803eab2a1bd31d3be0efedcae"
  },
  {
   "t": "2026-07-10T02:29:27Z",
   "sha": "6aa5f8df5df4c4a0a6c3fd86833317a42b8ed230"
  },
  {
   "t": "2026-07-10T03:15:31Z",
   "sha": "7d23abba9308d13aa1d2552ff1da91310122d5ba"
  },
  {
   "t": "2026-08-06T17:45:45Z",
   "sha": "bf7d72a521cdec4fc26735b56d8e6c4506dc2945"
  },
  {
   "t": "2026-08-14T17:36:01Z",
   "sha": "635edca83b417a40373301c040f380b24776dad0"
  },
  {
   "t": "2026-08-19T18:50:10Z",
   "sha": "0714981fc4d0cf59b3f69e056943f4cd598c31bd"
  },
  {
   "t": "2026-08-20T14:51:34Z",
   "sha": "5abf7a2b20a88b30f655e2a7fdd9793352caac06"
  },
  {
   "t": "2026-08-21T15:19:18Z",
   "sha": "e330ad91d756279f8100f4db1ec367a4bd609f24"
  },
  {
   "t": "2026-09-15T18:13:59Z",
   "sha": "50cfd5a99215bda36347a4f3abab7b70ecf6f404"
  },
  {
   "t": "2026-09-16T19:20:18Z",
   "sha": "526db3911d400b793e5715c0f989f1d3b66ac12e"
  },
  {
   "t": "2026-09-18T20:42:50Z",
   "sha": "0e473d175224746c13678db5106f9d4ab1086b9d"
  },
  {
   "t": "2026-09-29T21:00:06Z",
   "sha": "9f7bcfe874729d8b20c46dd61d9dab8a0e5ea3fa"
  },
  {
   "t": "2026-10-08T18:52:17Z",
   "sha": "9451c93ff38435f2ce100af786b9221b0c565c5c"
  },
  {
   "t": "2026-10-09T17:05:49Z",
   "sha": "02d1084eba30de70c45eeaee77312fd417f79bd8"
  },
  {
   "t": "2026-10-20T14:12:20Z",
   "sha": "672e628d96bf1d1f2890ff666b4d16f37309b61e"
  },
  {
   "t": "2026-10-21T18:17:26Z",
   "sha": "0457cec22a273eba31e92950bd485bf72c864ab0"
  },
  {
   "t": "2026-10-21T19:30:50Z",
   "sha": "0316d0dc9ec6a1abbe84489c44a47a2dd94c79ee"
  }
 ],
 "additions": 656,
 "deletions": 424,
 "changed_files": 13,
 "commit_count": 5,
 "size_bucket": "XL",
 "mergeable_state": "clean",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "ack": [
     {
      "login": "l0rinc",
      "url": "https://github.com/bitcoin/bitcoin/pull/35642#issuecomment-6324747295"
     },
     {
      "login": "fametrano",
      "url": "https://github.com/bitcoin/bitcoin/pull/35642#issuecomment-6394267241"
     }
    ],
    "concept_ack": [
     {
      "login": "sedited",
      "url": "https://github.com/bitcoin/bitcoin/pull/35642#issuecomment-5069107232"
     },
     {
      "login": "fjahr",
      "url": "https://github.com/bitcoin/bitcoin/pull/35642#issuecomment-5367998461"
     },
     {
      "login": "hodlinator",
      "url": "https://github.com/bitcoin/bitcoin/pull/35642#pullrequestreview-5379510928"
     }
    ],
    "approach_ack": [
     {
      "login": "ajtowns",
      "url": "https://github.com/bitcoin/bitcoin/pull/35642#issuecomment-5205781986"
     }
    ],
    "stale_ack": [
     {
      "login": "darosior",
      "url": "https://github.com/bitcoin/bitcoin/pull/35642#pullrequestreview-5368176826"
     }
    ]
   },
   "conflicts": []
  }
 },
 "acks_parsed": {
  "sedited": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-07-24T10:58:44Z",
   "stale": false
  },
  "ajtowns": {
   "kind": "approach_ack",
   "hash": null,
   "t": "2026-08-06T14:08:43Z",
   "stale": false
  },
  "darosior": {
   "kind": "ack",
   "hash": "9f7bcfe8747",
   "t": "2026-09-30T15:09:12Z",
   "stale": true
  },
  "fjahr": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-08-21T09:19:53Z",
   "stale": false
  },
  "l0rinc": {
   "kind": "ack",
   "hash": "0316d0dc9ec6a1abbe84489c44a47a2dd94c79ee",
   "t": "2026-10-21T19:32:12Z",
   "stale": false
  }
 },
 "acks_tally": {
  "ack": 1,
  "stale_ack": 1,
  "concept_ack": 2,
  "approach_ack": 1,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 7,
  "changes_requested": 1,
  "distinct_reviewers": [
   "ajtowns",
   "darosior",
   "fametrano",
   "fjahr",
   "hodlinator",
   "l0rinc",
   "sedited",
   "shuv-amp"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "clean",
  "last_author_activity": "2026-10-24T12:48:25Z",
  "last_reviewer_activity": "2026-10-25T06:12:42Z",
  "last_reviewer": "fametrano",
  "author_silent_days": 8,
  "waiting_on_author_days": 7,
  "days_since_update": 3
 },
 "refs": {
  "mentioned": [
   35208
  ],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [
   {
    "number": 35208,
    "type": "pull",
    "state": "closed",
    "merged": false,
    "merged_at": null,
    "title": "net: cap future-MTP headers commitments"
   }
  ],
  "conflicts": []
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [
  "doc/release-process.md",
  "src/headerssync.cpp",
  "src/headerssync.h",
  "src/net_processing.cpp",
  "src/net_processing.h",
  "src/test/fuzz/headerssync.cpp",
  "src/test/headerssync_tests.cpp"
 ],
 "body": "This migrates the headers presync parameters (`commitment_period` and `redownload_buffer_size`) from being precomputed chainparams to being computed at runtime whenever a headers sync starts. This has as advantages:\n* Less work for maintainers that need to occasionally run the current `contrib/devtools/headerssync-params.py` script.\n* Less room for mistakes in that process.\n* Automatically adapts based on time (as opposed to needing to predict the lifetime of the software).\n\nTo achieve this, the parameter search algorithm is implemented in C++. It is simpler, because it does not attempt to model the fact that the attacker is forced to submit their headers in batches of 2000 if they want to maintain the attack. The old Python code did that, but actually got it (slightly) wrong by missing the fact that attackers are capable of strategically deciding to stop the attack in the middle of a batch (found by darosior).\n\nUnit tests and fuzz tests are added, and at the end the old Python code is deleted.",
 "commits": [
  {
   "sha": "3ddc510850bd2a8f1deede6f957fc4d697a7df5d",
   "date": "2026-10-21T17:09:32Z",
   "message": "headerssync: add computation of optimal sync parameters\n\nAdd ComputeHeadersSyncParams(), which finds (near-)optimal commitment\nperiod and redownload buffer size values for the headers sync\nDoS-protection mechanism, given the timespan since genesis and the\nnumber of headers in the minimum-chainwork chain, along with the\ninternal ComputeHeadersSyncParamsInner() it is based on.\n\nThe attack model treats headers as arriving one at a time, which\nslightly overestimates what an attacker can achieve (in reality headers\narrive in batches that are only accepted in their entirety), but leads\nto a closed-form expression for the expected number of accepted forged\nheaders. That expression can be inverted, which gives the buffer size\nneeded for a given period directly, so only the period is searched for:\nby bisecting the legal range for the two adjacent periods which straddle\nthe optimum of the problem's continuous relaxation, and comparing those\ntwo. If that optimum lies outside the legal range, the two periods\nnearest the corresponding end are compared instead.\n\nAlso widen next_height in the presync header validation to int64_t,\nmatching the m_current_height it is computed from. A presync chain is\nonly bounded by 6 headers per second since genesis, so it can exceed\nINT_MAX headers, which the implicit truncation would turn into a\nnegative height.\n\nThe unit test checks 50 inputs against their exact optima, found by an\nexhaustive search over all periods. The script which samples such inputs\nand computes their optima is included with the test records."
  },
  {
   "sha": "7281e51459e7b956046a9b1f03be90de17a1316d",
   "date": "2026-10-21T17:09:32Z",
   "message": "fuzz: add headers sync parameter computation targets\n\nAdd two fuzz targets covering the headers sync parameter optimizer over\na broad range of legal inputs, asserting that the resulting\nconfiguration is sane (and, implicitly, that the computation terminates\nwithout crashing or throwing):\n\n- headers_sync_params_inner exercises ComputeHeadersSyncParamsInner\n  directly. minchainwork_headers is a block height and thus fits an int;\n  max_headers must exceed twice that (which the caller guarantees by\n  clamping) and is at most 6 headers/s over the longest timespan that\n  std::chrono::seconds is guaranteed to represent; attack_headers is a\n  positive rate, parametrized logarithmically over a wide range of\n  magnitudes.\n\n- headers_sync_params exercises ComputeHeadersSyncParams over the\n  portable range of std::chrono::seconds, and any int-height\n  minchainwork_headers.\n\nBoth targets check that the returned period is positive and no larger\nthan the minimum-chainwork chain, and that the redownload buffer covers\nat least one commitment period whenever the acceptable attack rate is\nlow enough to require any commitment verification at all (which it\nalways is for the rates ComputeHeadersSyncParams uses)."
  },
  {
   "sha": "990b3fa6d007ea725a6da6374e5c2c1bec2fa08e",
   "date": "2026-10-21T19:28:18Z",
   "message": "net_processing: compute headers sync parameters at runtime\n\nSo far, the headers sync parameters are committed to the repository,\nas part of the chain parameters. This requires maintainers to\noccasionally recompute them, based on a prediction of how long the\nrelease is likely to be used.\n\nWith the optimization algorithm now available in headerssync.cpp, it\nbecomes possible to instead do this computation at runtime. This is\nless work, with fewer ways for maintainers and reviewers to get it\nwrong, and automatically adjusts based on clock time.\n\nThe computation takes well under a microsecond, so it is simply redone\nwhenever a headers sync is started. Doing it there, rather than once at\nstartup, keeps the commitment period consistent with the bound that\nHeadersSyncState derives from the clock on how many commitments a peer\nmay make us store: a node which starts up with a badly wrong clock and\nhas it corrected before syncing would otherwise use a period sized for\na much shorter chain than a peer can then feed it, inflating that bound\n(by a factor 50 for a clock which starts out before genesis). The fuzz\ntargets which reach this code fix the clock around message processing,\nso the parameters they end up with do not vary with the fuzz input.\n\nAs the chain parameters no longer hold them, HeadersSyncParams moves to\nheaderssync.h, next to the code which computes and consumes it.\n\nCo-Authored-By: L\u0151rinc <pap.lorinc@gmail.com>"
  },
  {
   "sha": "4c04debcd7c906335c07de1a4d6373e8bf1be135",
   "date": "2026-10-21T19:28:18Z",
   "message": "contrib: remove headerssync-params.py\n\nThe headers sync parameters are now computed at runtime, so the tool\nthat used to generate them for chainparams.cpp has no purpose anymore."
  },
  {
   "sha": "0316d0dc9ec6a1abbe84489c44a47a2dd94c79ee",
   "date": "2026-10-21T19:28:18Z",
   "message": "test: verify the sync parameters by brute force\n\nThe test vectors compare against values obtained by a separate\nexhaustive search, so they detect changes in behavior, but not a mistake\nin that search. Establish instead, for each of them, that the\nconfiguration really is the one with the lowest memory usage, by\nevaluating the attack rate for every integer period, and for every\ninteger bufsize up to the point where it becomes acceptable. Periods\nwhose redownload buffer alone outweighs the best configuration found so\nfar are ruled out by a bound rather than tried, which keeps the search\nover the recorded chain lengths affordable.\n\nThe recorded bufsize is in addition held against the attack model\nitself, by averaging the accepted headers over the offset at which the\nattacker's first commitment check falls, instead of against the closed\nform that model is condensed into and inverted.\n\nCo-Authored-By: L\u0151rinc <pap.lorinc@gmail.com>"
  }
 ],
 "timeline": [
  {
   "t": "2026-07-02T15:07:50Z",
   "kind": "force_push",
   "who": "sipa",
   "commit": "befe560698c6227a9364d93f4fd797943c22e1cc",
   "id": "push:befe560698c6227a9364d93f4fd797943c22e1cc"
  },
  {
   "t": "2026-07-02T15:14:55Z",
   "kind": "force_push",
   "who": "sipa",
   "commit": "cafcdc385aaa266ee8e98d048206059c6bb428b7",
   "id": "push:cafcdc385aaa266ee8e98d048206059c6bb428b7"
  },
  {
   "t": "2026-07-02T15:51:50Z",
   "kind": "review_comment",
   "who": "ajtowns",
   "assoc": "MEMBER",
   "path": "src/net_processing.cpp",
   "commit": "3e9535e2d808709b8aab113ffd4ea4cf3f65f9c3",
   "in_reply_to": null,
   "text": "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?)",
   "id": "rc:3514402726",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r3514402726",
   "hash": "f1fc9b9e53622e66",
   "edited": null
  },
  {
   "t": "2026-07-02T16:18:20Z",
   "kind": "review_comment",
   "who": "sipa",
   "assoc": "MEMBER",
   "path": "src/net_processing.cpp",
   "commit": "3e9535e2d808709b8aab113ffd4ea4cf3f65f9c3",
   "in_reply_to": 3514402726,
   "text": "Good point, I had not considered that.\n\nThe 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.\n\nI'll run some numbers to see how quickly these parameters change and suggest some quantization or noise.",
   "id": "rc:3514568670",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r3514568670",
   "hash": "06e5f532a5ccb073",
   "edited": null
  },
  {
   "t": "2026-07-02T17:00:51Z",
   "kind": "review_comment",
   "who": "sipa",
   "assoc": "MEMBER",
   "path": "src/net_processing.cpp",
   "commit": "3e9535e2d808709b8aab113ffd4ea4cf3f65f9c3",
   "in_reply_to": 3514402726,
   "text": "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.",
   "id": "rc:3514828065",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r3514828065",
   "hash": "0c083711788ee3d1",
   "edited": "2026-07-03T13:44:41Z"
  },
  {
   "t": "2026-07-02T17:41:23Z",
   "kind": "force_push",
   "who": "sipa",
   "commit": "6ea06616798b0f39dc9f868b4dd702754d45851b",
   "id": "push:6ea06616798b0f39dc9f868b4dd702754d45851b"
  },
  {
   "t": "2026-07-02T20:50:00Z",
   "kind": "force_push",
   "who": "sipa",
   "commit": "3dbae98576823d24117b2ae90659addb3ffc2325",
   "id": "push:3dbae98576823d24117b2ae90659addb3ffc2325"
  },
  {
   "t": "2026-07-09T17:55:05Z",
   "kind": "force_push",
   "who": "sipa",
   "commit": "8cd423b86f90092b714d16a853622baff92066b9",
   "id": "push:8cd423b86f90092b714d16a853622baff92066b9"
  },
  {
   "t": "2026-07-09T18:01:48Z",
   "kind": "force_push",
   "who": "sipa",
   "commit": "bae2fd5ab05580c0287123f13daa3f2043e0524a",
   "id": "push:bae2fd5ab05580c0287123f13daa3f2043e0524a"
  },
  {
   "t": "2026-07-09T20:24:06Z",
   "kind": "force_push",
   "who": "sipa",
   "commit": "e8df9d86a5a5402ea65c19beb8d449d58fe743b7",
   "id": "push:e8df9d86a5a5402ea65c19beb8d449d58fe743b7"
  },
  {
   "t": "2026-07-09T20:31:42Z",
   "kind": "force_push",
   "who": "sipa",
   "commit": "3e9535e2d808709b8aab113ffd4ea4cf3f65f9c3",
   "id": "push:3e9535e2d808709b8aab113ffd4ea4cf3f65f9c3"
  },
  {
   "t": "2026-07-09T21:58:35Z",
   "kind": "force_push",
   "who": "sipa",
   "commit": "b985a4130b45e92803eab2a1bd31d3be0efedcae",
   "id": "push:b985a4130b45e92803eab2a1bd31d3be0efedcae"
  },
  {
   "t": "2026-07-10T02:29:27Z",
   "kind": "force_push",
   "who": "sipa",
   "commit": "6aa5f8df5df4c4a0a6c3fd86833317a42b8ed230",
   "id": "push:6aa5f8df5df4c4a0a6c3fd86833317a42b8ed230"
  },
  {
   "t": "2026-07-10T03:15:31Z",
   "kind": "force_push",
   "who": "sipa",
   "commit": "7d23abba9308d13aa1d2552ff1da91310122d5ba",
   "id": "push:7d23abba9308d13aa1d2552ff1da91310122d5ba"
  },
  {
   "t": "2026-07-24T10:58:44Z",
   "kind": "comment",
   "who": "sedited",
   "assoc": "MEMBER",
   "text": "Concept ACK",
   "id": "c:5069107232",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#issuecomment-5069107232",
   "hash": "e8334cc43879b73c",
   "edited": null
  },
  {
   "t": "2026-08-06T14:08:43Z",
   "kind": "comment",
   "who": "ajtowns",
   "assoc": "MEMBER",
   "text": "Approach ACK ; needs rebase if wanted for 32.0 I guess?",
   "id": "c:5205781986",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#issuecomment-5205781986",
   "hash": "1ac27a2609de9c74",
   "edited": null
  },
  {
   "t": "2026-08-06T16:23:41Z",
   "kind": "comment",
   "who": "darosior",
   "assoc": "MEMBER",
   "text": "Concept ACK",
   "id": "c:5207404954",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#issuecomment-5207404954",
   "hash": "e8334cc43879b73c",
   "edited": null
  },
  {
   "t": "2026-08-06T17:45:45Z",
   "kind": "force_push",
   "who": "sipa",
   "commit": "bf7d72a521cdec4fc26735b56d8e6c4506dc2945",
   "id": "push:bf7d72a521cdec4fc26735b56d8e6c4506dc2945"
  },
  {
   "t": "2026-08-06T17:46:04Z",
   "kind": "comment",
   "who": "sipa",
   "assoc": "MEMBER",
   "text": "Rebased.",
   "id": "c:5207935901",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#issuecomment-5207935901",
   "hash": "3d15a2c7e45f5a1c",
   "edited": null
  },
  {
   "t": "2026-08-12T16:11:20Z",
   "kind": "comment",
   "who": "shuv-amp",
   "assoc": "CONTRIBUTOR",
   "text": "`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?",
   "id": "c:5269371252",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#issuecomment-5269371252",
   "hash": "44e8f80b6e601246",
   "edited": null
  },
  {
   "t": "2026-08-14T17:36:01Z",
   "kind": "force_push",
   "who": "sipa",
   "commit": "635edca83b417a40373301c040f380b24776dad0",
   "id": "push:635edca83b417a40373301c040f380b24776dad0"
  },
  {
   "t": "2026-08-14T17:36:46Z",
   "kind": "comment",
   "who": "sipa",
   "assoc": "MEMBER",
   "text": "@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.\n\nReady for more review.",
   "id": "c:5296342342",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#issuecomment-5296342342",
   "hash": "9036d84cc9437ae1",
   "edited": null
  },
  {
   "t": "2026-08-14T19:30:43Z",
   "kind": "comment",
   "who": "shuv-amp",
   "assoc": "CONTRIBUTOR",
   "text": "`ComputeHeadersSyncParams()` doesn't terminate for a small `minchainwork_headers`\ncombined with a short timespan:\n\n```c++\nComputeHeadersSyncParams(std::chrono::seconds{0}, 30228);  // hangs\nComputeHeadersSyncParams(std::chrono::seconds{0}, 30229);  // period=2 bufsize=40\n```\n\n30229 is the boundary; below it the region extends out to timespans around 1e4 s.\n\nIn `attack_rate()`, the left side of `future_limit * prob < 1.0e-16 * rate` can't be\nnegative, and the right side is exactly 0 once `rate` is below ~2.5e-308, so that\nbreak stops being reachable. The `limit` check is the only other exit and Step 1's\nNewton call doesn't pass one, so there it has none at all.\n\nThe Python this replaces has the same shape\n(`HEADER_BATCH_COUNT * prob < 1.0e-16 * rate * len(align_choices)`) and doesn't\nterminate at those arguments either, so it isn't new, it just matters more now that\nit runs at startup.\n\nNon-strict is enough, since `prob` decreases to exactly 0 and takes the left side\nwith it:\n\n```c++\n-    if (future_limit * prob < 1.0e-16 * rate) break;\n+    if (future_limit * prob <= 1.0e-16 * rate) break;\n```\n\nThe 50 test vectors still reproduce with that, and 196/196 other inputs I compared\nare unchanged.\n\nNothing in chainparams is close: testnet4 has the lowest `minchainwork_height` at\n123613, and reaches an exponent of 531 against the 1075 where `prob` underflows. It\nis the region a new chain starts in though, and 0 hangs a different way\n(`attack_headers` is 0, so `period` is NaN and `accept_forged_headers > 0.0` never\nholds), so a guard on the input might be worth having too.",
   "id": "c:5297347941",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#issuecomment-5297347941",
   "hash": "59253671e242c003",
   "edited": null
  },
  {
   "t": "2026-08-19T18:50:10Z",
   "kind": "force_push",
   "who": "sipa",
   "commit": "0714981fc4d0cf59b3f69e056943f4cd598c31bd",
   "id": "push:0714981fc4d0cf59b3f69e056943f4cd598c31bd"
  },
  {
   "t": "2026-08-19T18:55:03Z",
   "kind": "comment",
   "who": "sipa",
   "assoc": "MEMBER",
   "text": "@shuv-amp Fixed, and added some extra `Assume()`s in the code.",
   "id": "c:5346650795",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#issuecomment-5346650795",
   "hash": "e150e7460f13b121",
   "edited": null
  },
  {
   "t": "2026-08-20T14:51:34Z",
   "kind": "force_push",
   "who": "sipa",
   "commit": "5abf7a2b20a88b30f655e2a7fdd9793352caac06",
   "id": "push:5abf7a2b20a88b30f655e2a7fdd9793352caac06"
  },
  {
   "t": "2026-08-20T14:53:17Z",
   "kind": "comment",
   "who": "sipa",
   "assoc": "MEMBER",
   "text": "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.",
   "id": "c:5357592309",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#issuecomment-5357592309",
   "hash": "8d713f5754ca91ca",
   "edited": null
  },
  {
   "t": "2026-08-21T09:19:53Z",
   "kind": "comment",
   "who": "fjahr",
   "assoc": "MEMBER",
   "text": "Concept ACK",
   "id": "c:5367998461",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#issuecomment-5367998461",
   "hash": "e8334cc43879b73c",
   "edited": null
  },
  {
   "t": "2026-08-21T15:19:18Z",
   "kind": "force_push",
   "who": "sipa",
   "commit": "e330ad91d756279f8100f4db1ec367a4bd609f24",
   "id": "push:e330ad91d756279f8100f4db1ec367a4bd609f24"
  },
  {
   "t": "2026-09-04T21:15:24Z",
   "kind": "comment",
   "who": "darosior",
   "assoc": "MEMBER",
   "text": "The `HeadersSyncParams` definition can now be moved from chainparams to the headersync module.",
   "id": "c:5546533833",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#issuecomment-5546533833",
   "hash": "d2a17660657ad308",
   "edited": null
  },
  {
   "t": "2026-09-09T15:16:13Z",
   "kind": "review_comment",
   "who": "darosior",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "e330ad91d756279f8100f4db1ec367a4bd609f24",
   "in_reply_to": null,
   "text": "nit: `forged_headers` counts the number of headers deviating from the honest chain up to that point (i.e. that batch and previous ones), whereas `accept_forged_headers` counts the additional number of headers accepted as the result of that batch. Took me a minute, and i think this could be clarified in the comment:\n\n```suggestion\n                /** The forged headers received so far, up to and including this batch. */\n                const int64_t forged_headers = (batch + 1) * HEADER_BATCH_COUNT - honest;\n                /** The number of forged headers that would be accepted as a result of this batch,\n                 *  if the commitments in this batch and all earlier batches verify correctly. */\n                const double accept_forged_headers = std::clamp<double>(forged_headers - bufsize, 0.0, HEADER_BATCH_COUNT);\n```",
   "id": "rc:3970063056",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r3970063056",
   "hash": "09a92d8f9d03cd9e",
   "edited": "2026-09-14T21:18:09Z"
  },
  {
   "t": "2026-09-09T19:10:44Z",
   "kind": "review_comment",
   "who": "darosior",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "e330ad91d756279f8100f4db1ec367a4bd609f24",
   "in_reply_to": null,
   "text": "nit: feel free to ignore, just mentioning what gave me pause when i was reading from scratch.\n\nThat last sentence in the comment threw me off a little bit. `future_limit` is a number of headers, weighted by the exponentially-decreasing contribution each batch makes. The last sentence could be read as it representing a single batch, while really it represents *all* batches, so the check later can be essentially \"if the contribution of the entire tail of headers at the current probability is no more than a rounding error, we are done\".",
   "id": "rc:3972085092",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r3972085092",
   "hash": "f176766998aa583c",
   "edited": "2026-09-14T21:18:09Z"
  },
  {
   "t": "2026-09-09T19:39:00Z",
   "kind": "review_comment",
   "who": "darosior",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "e330ad91d756279f8100f4db1ec367a4bd609f24",
   "in_reply_to": null,
   "text": "Why is it not always true?",
   "id": "rc:3972310818",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r3972310818",
   "hash": "e8c531b8992c5bba",
   "edited": "2026-09-14T21:18:09Z"
  },
  {
   "t": "2026-09-14T19:57:29Z",
   "kind": "review_comment",
   "who": "darosior",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "e330ad91d756279f8100f4db1ec367a4bd609f24",
   "in_reply_to": null,
   "text": "Why is this square root \"indirection\" necessary? Using `gamma = bufsize / period` (intuitively \"bits of security\") and `memory_factor = (max_headers - mcw_headers) / COMPACT_HEADER_SIZE` directly seem clearer to me, and equivalent?",
   "id": "rc:4009020433",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4009020433",
   "hash": "5260e7f280dc20ba",
   "edited": "2026-09-14T21:18:09Z"
  },
  {
   "t": "2026-09-14T21:16:48Z",
   "kind": "review_comment",
   "who": "darosior",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "e330ad91d756279f8100f4db1ec367a4bd609f24",
   "in_reply_to": null,
   "text": "I think this routine underestimates the rate, because an incomplete batch sent by an attacker may be detected, but will still lead to processing as many headers from the buffer.",
   "id": "rc:4009670918",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4009670918",
   "hash": "955324f94e9b7d7d",
   "edited": "2026-09-14T21:18:09Z"
  },
  {
   "t": "2026-09-14T21:18:09Z",
   "kind": "review",
   "who": "darosior",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "e330ad91d756279f8100f4db1ec367a4bd609f24",
   "text": "Submitting my comments so far, because i think `attack_rate` will need to be revised.",
   "id": "r:5156278183",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#pullrequestreview-5156278183",
   "hash": "25864af0e21f42e0",
   "edited": null
  },
  {
   "t": "2026-09-14T21:33:48Z",
   "kind": "comment",
   "who": "sipa",
   "assoc": "MEMBER",
   "text": "Converted to draft. @darosior found that the current version (and the original Python it's based on) did not take one attack scenario into account (using a final headers batch of size less than 2000). Fixing that is possible, but makes the code even more complicated, and results in parameters that increase per-peer memory by ~2.2 KiB.\n\nI'm considering instead going for a much simpler approach, that drops the assumption that all headers arrive in batches of 2000 altogether. That'd be much simpler and faster, and perhaps more obviously correct, but increase per-peer memory by ~14.7 KiB. I think that's acceptable compared to existing memory usage.",
   "id": "c:5671114593",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#issuecomment-5671114593",
   "hash": "0f464ffb3fce49ee",
   "edited": null
  },
  {
   "t": "2026-09-15T18:13:59Z",
   "kind": "force_push",
   "who": "sipa",
   "commit": "50cfd5a99215bda36347a4f3abab7b70ecf6f404",
   "id": "push:50cfd5a99215bda36347a4f3abab7b70ecf6f404"
  },
  {
   "t": "2026-09-15T18:19:19Z",
   "kind": "comment",
   "who": "sipa",
   "assoc": "MEMBER",
   "text": "Rewritten to take that approach. The commit history is simpler now too, as it doesn't attempt to show equivalence between the old Python script and the new code anymore.",
   "id": "c:5685669580",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#issuecomment-5685669580",
   "hash": "bc3fd66e03fac6fb",
   "edited": null
  },
  {
   "t": "2026-09-16T19:20:18Z",
   "kind": "force_push",
   "who": "sipa",
   "commit": "526db3911d400b793e5715c0f989f1d3b66ac12e",
   "id": "push:526db3911d400b793e5715c0f989f1d3b66ac12e"
  },
  {
   "t": "2026-09-18T16:18:23Z",
   "kind": "review_comment",
   "who": "darosior",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "526db3911d400b793e5715c0f989f1d3b66ac12e",
   "in_reply_to": null,
   "text": "Is Step 2 really useful anymore with the new approach? It made sense when Step 1 was a Newton-Raphson search that stopped at some threshold, to use as a basis for a more accurate search. But now Step 1 is an exact (according to the model) derivation.\n\nI asked Claude to check if Step 2 had any material impact in the new version. It claims that over 20,168 fuzz domain cases Step 2 picks offset 0 in 99.32% of cases. Furthermore, when it does pick a different offset it only saves about 0.0039% memory on realistic inputs.",
   "id": "rc:4048544161",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4048544161",
   "hash": "054770ed3484193c",
   "edited": "2026-09-18T18:36:32Z"
  },
  {
   "t": "2026-09-18T18:36:32Z",
   "kind": "review",
   "who": "darosior",
   "assoc": "MEMBER",
   "state": "APPROVED",
   "commit": "526db3911d400b793e5715c0f989f1d3b66ac12e",
   "text": "ACK 526db3911d400b793e5715c0f989f1d3b66ac12e\n\nI was not familiar with the existing script, so i reviewed the new calculation from scratch. The latest approach is a lot easier to grasp. I think this slightly looser upper bound is the right tradeoff.",
   "id": "r:5250047249",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#pullrequestreview-5250047249",
   "hash": "d2d7b912d8eaac91",
   "edited": null
  },
  {
   "t": "2026-09-18T20:42:50Z",
   "kind": "force_push",
   "who": "sipa",
   "commit": "0e473d175224746c13678db5106f9d4ab1086b9d",
   "id": "push:0e473d175224746c13678db5106f9d4ab1086b9d"
  },
  {
   "t": "2026-09-18T20:44:41Z",
   "kind": "review_comment",
   "who": "sipa",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "526db3911d400b793e5715c0f989f1d3b66ac12e",
   "in_reply_to": 4048544161,
   "text": "I've changed it to only checking the `floor()` and `ceil()` of the continuous optimal period. Experimentally, the only examples where neither of those wins occur with extremely low security levels (they all involve `bufsize` < `period`).",
   "id": "rc:4050560007",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4050560007",
   "hash": "4a3ab40e5ab81caf",
   "edited": null
  },
  {
   "t": "2026-09-18T21:21:51Z",
   "kind": "review_comment",
   "who": "darosior",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "0316d0dc9ec6a1abbe84489c44a47a2dd94c79ee",
   "in_reply_to": null,
   "text": "Neat.",
   "id": "rc:4050783818",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4050783818",
   "hash": "070a9a135c9aac3e",
   "edited": "2026-09-18T21:22:08Z"
  },
  {
   "t": "2026-09-18T21:22:08Z",
   "kind": "review",
   "who": "darosior",
   "assoc": "MEMBER",
   "state": "APPROVED",
   "commit": "0e473d175224746c13678db5106f9d4ab1086b9d",
   "text": "re-ACK 0e473d175224746c13678db5106f9d4ab1086b9d",
   "id": "r:5252721431",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#pullrequestreview-5252721431",
   "hash": "1205136abe9b7ac1",
   "edited": null
  },
  {
   "t": "2026-09-29T21:00:06Z",
   "kind": "force_push",
   "who": "sipa",
   "commit": "9f7bcfe874729d8b20c46dd61d9dab8a0e5ea3fa",
   "id": "push:9f7bcfe874729d8b20c46dd61d9dab8a0e5ea3fa"
  },
  {
   "t": "2026-09-30T13:47:43Z",
   "kind": "comment",
   "who": "sipa",
   "assoc": "MEMBER",
   "text": "Rebased, should be ready for review.",
   "id": "c:5912605013",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#issuecomment-5912605013",
   "hash": "e7067bdc12d535be",
   "edited": null
  },
  {
   "t": "2026-09-30T15:09:12Z",
   "kind": "review",
   "who": "darosior",
   "assoc": "MEMBER",
   "state": "APPROVED",
   "commit": "9f7bcfe874729d8b20c46dd61d9dab8a0e5ea3fa",
   "text": "re-ACK 9f7bcfe8747",
   "id": "r:5368176826",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#pullrequestreview-5368176826",
   "hash": "57913084c423fe69",
   "edited": null
  },
  {
   "t": "2026-10-01T12:56:59Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "02d1084eba30de70c45eeaee77312fd417f79bd8",
   "in_reply_to": null,
   "text": "nanonit: If you touch again, could const and brace-initialize to further tighten the screws (mutation+anti-narrowing verification).\n```suggestion\n    const int64_t next_height{m_current_height + 1};\n```",
   "id": "rc:4155588139",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4155588139",
   "hash": "6c96d0496a6c7272",
   "edited": "2026-10-13T12:18:14Z"
  },
  {
   "t": "2026-10-01T13:09:28Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "path": "src/headerssync.h",
   "commit": "02d1084eba30de70c45eeaee77312fd417f79bd8",
   "in_reply_to": null,
   "text": "nanonit: It is used internally in production, as stated in the line above.\n```suggestion\n * Only exposed for tests.\n```",
   "id": "rc:4155702490",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4155702490",
   "hash": "743c7686131bd2ba",
   "edited": "2026-10-13T12:18:14Z"
  },
  {
   "t": "2026-10-01T13:22:30Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "02d1084eba30de70c45eeaee77312fd417f79bd8",
   "in_reply_to": null,
   "text": "Rather than magic numbers, could we use `sizeof(CBlockHeader)` or compute it somehow, or improve the doc-string?\n80 is the value of `sizeof(CBlockHeader)` but the 81 constant appears in headerssync-params.py on master. I would think it typically serializes down to something smaller due to compact integers, but then there is overhead for the network magic, message type etc?",
   "id": "rc:4155828163",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4155828163",
   "hash": "5201e6410bd2a242",
   "edited": "2026-10-13T12:18:14Z"
  },
  {
   "t": "2026-10-01T17:28:34Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "02d1084eba30de70c45eeaee77312fd417f79bd8",
   "in_reply_to": null,
   "text": "nit: Might as well use `static_assert` message, and specify \"case records\".\n```suggestion\n    static_assert(COMPACT_HEADER_SIZE == 384,\n        \"Unit test case records will need to be regenerated if this constant were to change.\");\n```",
   "id": "rc:4158424480",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4158424480",
   "hash": "afe659666a405e26",
   "edited": "2026-10-13T12:18:14Z"
  },
  {
   "t": "2026-10-01T17:42:55Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "9f7bcfe874729d8b20c46dd61d9dab8a0e5ea3fa",
   "in_reply_to": null,
   "text": "remark: \"commitment phase\" is clearer than \"presync phase\". Wish it was used everywhere in older code.",
   "id": "rc:4158555646",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4158555646",
   "hash": "464e8eb858281a06",
   "edited": "2026-10-13T12:18:14Z"
  },
  {
   "t": "2026-10-01T18:11:04Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "9f7bcfe874729d8b20c46dd61d9dab8a0e5ea3fa",
   "in_reply_to": null,
   "text": "nit: This lambda could be declared inside `make_config` to decrease cognitive complexity.",
   "id": "rc:4158813431",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4158813431",
   "hash": "73c372444e931868",
   "edited": "2026-10-13T12:18:14Z"
  },
  {
   "t": "2026-10-07T20:14:13Z",
   "kind": "review_comment",
   "who": "sipa",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "e330ad91d756279f8100f4db1ec367a4bd609f24",
   "in_reply_to": 3972310818,
   "text": "In practice, it is always true. Outside of that (fuzz tests) it could be untrue, so it's good to document under what conditions the function's behavior is well-defined.",
   "id": "rc:4211502840",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4211502840",
   "hash": "bbbb66bfa1f61b60",
   "edited": null
  },
  {
   "t": "2026-10-08T03:55:12Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "9f7bcfe874729d8b20c46dd61d9dab8a0e5ea3fa",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nFollowing up on https://github.com/bitcoin/bitcoin/pull/35642#issuecomment-5297347941: is there a reason to use `Assume` (rather than `Assert`) for these preconditions? Shouldn't these abort in production? It seems to me that zero inputs could theoretically still lead to division by zero or an infinity-to-integer conversion.",
   "id": "rc:4214406451",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4214406451",
   "hash": "783700fe07ff41ba",
   "edited": "2026-10-08T08:14:43Z"
  },
  {
   "t": "2026-10-08T06:04:20Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "9f7bcfe874729d8b20c46dd61d9dab8a0e5ea3fa",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nCould we cap this loop? I was surprised we need a binary search here (considered a few alternatives but don't have a fully working version) and wondered about `NaN` risk, but that does actually make this condition break.\nDigging further with AI pointed to x87 excess precision: a midpoint can compare between the endpoints, then round back to one when stored, leaving the loop with no progress. [GCC describes the relevant behavior](https://gcc.gnu.org/gcc-13/porting_to.html#excess-precision).\n\nBound the existing bisection loop\n\nThe saved GCC 12.2/i686 runs timed out on the 50 existing vectors. The capped runs matched the native outputs. I inspected those results but have not rerun the compiler matrix.\n\n```diff\ndiff --git a/src/headerssync.cpp b/src/headerssync.cpp\nindex e218f99d3d..5c09d516fe 100644\n--- a/src/headerssync.cpp\n+++ b/src/headerssync.cpp\n@@ -542,7 +542,7 @@ std::pair<size_t, size_t> ComputeHeadersSyncParamsInner(int64_t max_headers, int\n         const double target = double(max_headers - minchainwork_headers) / COMPACT_HEADER_SIZE;\n         double lo = 1.0;\n         double hi = minchainwork_headers;\n-        while (true) {\n+        for (int iter{0}; iter < 128; ++iter) {\n             const double mid = 0.5 * (lo + hi);\n             if (!(lo < mid && mid < hi)) break;\n             if (mid * cont_bufsize(mid) < target) {\n```",
   "id": "rc:4215432352",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4215432352",
   "hash": "eb8a5bef67f5984d",
   "edited": "2026-10-08T08:14:43Z"
  },
  {
   "t": "2026-10-08T06:18:59Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/net_processing.cpp",
   "commit": "9f7bcfe874729d8b20c46dd61d9dab8a0e5ea3fa",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nSince we already have a `HeadersSyncParams` type could we move it and use it here as well (as @darosior also hinted I think)?\n\nReturn the existing named parameter type\n\n```diff\ndiff --git a/src/headerssync.cpp b/src/headerssync.cpp\nindex 5c09d516fe..b89468b174 100644\n--- a/src/headerssync.cpp\n+++ b/src/headerssync.cpp\n@@ -390,7 +390,7 @@ CBlockLocator HeadersSyncState::NextHeadersRequestLocator() const\n //    alternate chain. This trade-off is ignored here, as it results in at most a small constant\n //    factor in attack rate.\n\n-std::pair<size_t, size_t> ComputeHeadersSyncParamsInner(int64_t max_headers, int64_t minchainwork_headers, double attack_headers)\n+HeadersSyncParams ComputeHeadersSyncParamsInner(int64_t max_headers, int64_t minchainwork_headers, double attack_headers)\n {\n     /** Headers in the redownload buffer are stored without prevhash, as a CompressedHeader. [bits] */\n     constexpr int64_t COMPACT_HEADER_SIZE{sizeof(CompressedHeader) * 8};\n@@ -568,12 +568,12 @@ std::pair<size_t, size_t> ComputeHeadersSyncParamsInner(int64_t max_headers, int\n         make_config(std::floor(cont_period)),\n         make_config(std::ceil(cont_period)));\n\n-    return {size_t(best_period), size_t(best_bufsize)};\n+    return {.commitment_period = size_t(best_period), .redownload_buffer_size = size_t(best_bufsize)};\n }\n\n // Derive max_headers (the longest a valid chain could be by now) and the attack_headers budget from\n // the chain's age and minimum-chain-work header count, then optimize.\n-std::pair<size_t, size_t> ComputeHeadersSyncParams(std::chrono::seconds timespan, int64_t minchainwork_headers)\n+HeadersSyncParams ComputeHeadersSyncParams(std::chrono::seconds timespan, int64_t minchainwork_headers)\n {\n     /** Expected block interval. [seconds] */\n     constexpr double BLOCK_INTERVAL{600.0};\ndiff --git a/src/headerssync.h b/src/headerssync.h\nindex 5827db43f6..0dd1d0e3fe 100644\n--- a/src/headerssync.h\n+++ b/src/headerssync.h\n@@ -15,9 +15,9 @@\n #include <util/hasher.h>\n\n #include <chrono>\n+#include <cstddef>\n #include <deque>\n #include <stdexcept>\n-#include <utility>\n #include <vector>\n\n // A compressed CBlockHeader, which leaves out the prevhash\n@@ -56,6 +56,14 @@ struct CompressedHeader {\n     };\n };\n\n+//! Configuration for headers sync memory usage.\n+struct HeadersSyncParams {\n+    //! Distance in blocks between header commitments.\n+    size_t commitment_period{0};\n+    //! Minimum number of validated headers to accumulate in the redownload\n+    //! buffer before feeding them into the permanent block index.\n+    size_t redownload_buffer_size{0};\n+};\n\n /** Find a near-optimal (period, bufsize) headers synchronization configuration for:\n  *\n@@ -63,11 +71,8 @@ struct CompressedHeader {\n  * - A specified number of headers in the minchainwork chain.\n  *\n  * See the constants in the implementation that control what attack success rate is acceptable.\n- *\n- * Returns {commitment period, redownload buffer size}.\n  */\n-std::pair<size_t, size_t> ComputeHeadersSyncParams(std::chrono::seconds timespan, int64_t minchainwork_headers);\n-\n+HeadersSyncParams ComputeHeadersSyncParams(std::chrono::seconds timespan, int64_t minchainwork_headers);\n\n /** Find a near-optimal (period, bufsize) headers synchronization configuration for:\n  *\n@@ -79,8 +84,7 @@ std::pair<size_t, size_t> ComputeHeadersSyncParams(std::chrono::seconds timespan\n  * This is the internal part of ComputeHeadersSyncParams that performs the actual optimization.\n  * Test-only.\n  */\n-std::pair<size_t, size_t> ComputeHeadersSyncParamsInner(int64_t max_headers, int64_t minchainwork_headers, double attack_headers);\n-\n+HeadersSyncParams ComputeHeadersSyncParamsInner(int64_t max_headers, int64_t minchainwork_headers, double attack_headers);\n\n /** HeadersSyncState:\n  *\ndiff --git a/src/kernel/chainparams.h b/src/kernel/chainparams.h\nindex e9761f70ae..61c734b0a4 100644\n--- a/src/kernel/chainparams.h\n+++ b/src/kernel/chainparams.h\n@@ -14,7 +14,6 @@\n #include <util/hash_type.h>\n #include <util/vector.h>\n\n-#include <cstddef>\n #include <cstdint>\n #include <memory>\n #include <optional>\n@@ -60,15 +59,6 @@ struct ChainTxData {\n     double dTxRate;   //!< estimated number of transactions per second after that timestamp\n };\n\n-//! Configuration for headers sync memory usage.\n-struct HeadersSyncParams {\n-    //! Distance in blocks between header commitments.\n-    size_t commitment_period{0};\n-    //! Minimum number of validated headers to accumulate in the redownload\n-    //! buffer before feeding them into the permanent block index.\n-    size_t redownload_buffer_size{0};\n-};\n-\n /**\n  * CChainParams defines various tweakable parameters of a given instance of the\n  * Bitcoin system.\ndiff --git a/src/net_processing.cpp b/src/net_processing.cpp\nindex 5524e16b4f..ea800ba9a3 100644\n--- a/src/net_processing.cpp\n+++ b/src/net_processing.cpp\n@@ -2141,13 +2141,13 @@ std::unique_ptr<PeerManager> PeerManager::make(CConnman& connman, AddrMan& addrm\n static HeadersSyncParams MakeHeadersSyncParams(const CChainParams& chainparams,\n                                                const std::optional<std::pair<size_t, size_t>>& precomputed)\n {\n-    const auto [period, bufsize] = precomputed ? *precomputed : [&] {\n+    const auto params{precomputed ? HeadersSyncParams{.commitment_period = precomputed->first, .redownload_buffer_size = precomputed->second} : [&] {\n         const auto genesis_time{NodeSeconds{std::chrono::seconds{chainparams.GenesisBlock().nTime}}};\n         const auto timespan{std::chrono::duration_cast<std::chrono::seconds>(NodeClock::now() - genesis_time)};\n         return ComputeHeadersSyncParams(timespan, chainparams.GetConsensus().minchainwork_height);\n-    }();\n-    LogInfo(\"Headers sync parameters: commitment period=%u, redownload buffer size=%u\\n\", period, bufsize);\n-    return HeadersSyncParams{.commitment_period = period, .redownload_buffer_size = bufsize};\n+    }()};\n+    LogInfo(\"Headers sync parameters: commitment period=%u, redownload buffer size=%u\", params.commitment_period, params.redownload_buffer_size);\n+    return params;\n }\n\n PeerManagerImpl::PeerManagerImpl(CConnman& connman, AddrMan& addrman,\n```",
   "id": "rc:4215548260",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4215548260",
   "hash": "57628ace484a1b25",
   "edited": "2026-10-08T08:14:43Z"
  },
  {
   "t": "2026-10-08T06:20:51Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/net_processing.h",
   "commit": "9f7bcfe874729d8b20c46dd61d9dab8a0e5ea3fa",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nWith the rewrite down to about 3 \u00b5s per call and fixed clocks already in these fuzz targets, is the override still worth keeping?\n\nUse the existing fixed fuzz clocks\n\n(Based on previous local suggestions, so this patch may not apply cleanly)\n\n```diff\ndiff --git a/src/net_processing.cpp b/src/net_processing.cpp\nindex ea800ba9a3..d107c5fbc8 100644\n--- a/src/net_processing.cpp\n+++ b/src/net_processing.cpp\n@@ -2135,17 +2135,11 @@ std::unique_ptr<PeerManager> PeerManager::make(CConnman& connman, AddrMan& addrm\n     return std::make_unique<PeerManagerImpl>(connman, addrman, banman, chainman, pool, warnings, opts);\n }\n\n-//! Compute the headers sync DoS-protection parameters for the given chain (unless precomputed\n-//! ones are provided), using its age (genesis to now) and the height at which its minimum chain\n-//! work was taken, and log the result.\n-static HeadersSyncParams MakeHeadersSyncParams(const CChainParams& chainparams,\n-                                               const std::optional<std::pair<size_t, size_t>>& precomputed)\n+//! Compute the headers sync DoS-protection parameters for the given chain, using its age (genesis\n+//! to now) and the height at which its minimum chain work was taken, and log the result.\n+static HeadersSyncParams MakeHeadersSyncParams(const CChainParams& chainparams)\n {\n-    const auto params{precomputed ? HeadersSyncParams{.commitment_period = precomputed->first, .redownload_buffer_size = precomputed->second} : [&] {\n-        const auto genesis_time{NodeSeconds{std::chrono::seconds{chainparams.GenesisBlock().nTime}}};\n-        const auto timespan{std::chrono::duration_cast<std::chrono::seconds>(NodeClock::now() - genesis_time)};\n-        return ComputeHeadersSyncParams(timespan, chainparams.GetConsensus().minchainwork_height);\n-    }()};\n+    const auto params{ComputeHeadersSyncParams(Now<NodeSeconds>() - chainparams.GenesisBlock().Time(), chainparams.GetConsensus().minchainwork_height)};\n     LogInfo(\"Headers sync parameters: commitment period=%u, redownload buffer size=%u\", params.commitment_period, params.redownload_buffer_size);\n     return params;\n }\n@@ -2156,7 +2150,7 @@ PeerManagerImpl::PeerManagerImpl(CConnman& connman, AddrMan& addrman,\n     : m_rng{opts.deterministic_rng},\n       m_fee_filter_rounder{CFeeRate{DEFAULT_MIN_RELAY_TX_FEE}, m_rng},\n       m_chainparams(chainman.GetParams()),\n-      m_headers_sync_params{MakeHeadersSyncParams(m_chainparams, opts.headers_sync_params)},\n+      m_headers_sync_params{MakeHeadersSyncParams(m_chainparams)},\n       m_connman(connman),\n       m_addrman(addrman),\n       m_banman(banman),\ndiff --git a/src/net_processing.h b/src/net_processing.h\nindex 3a9ecbab63..edaf48418f 100644\n--- a/src/net_processing.h\n+++ b/src/net_processing.h\n@@ -18,12 +18,10 @@\n\n #include <atomic>\n #include <chrono>\n-#include <cstddef>\n #include <cstdint>\n #include <memory>\n #include <optional>\n #include <string>\n-#include <utility>\n #include <vector>\n\n class AddrMan;\n@@ -112,9 +110,6 @@ public:\n         bool private_broadcast{DEFAULT_PRIVATE_BROADCAST};\n         //! Maximum per-second rate for sending transaction inventory to peers.\n         unsigned int tx_send_rate{DEFAULT_TX_SEND_RATE};\n-        //! Precomputed headers-sync parameters, as {commitment period, redownload buffer size};\n-        //! when unset, they are computed at construction time (this is a test-only option).\n-        std::optional<std::pair<size_t, size_t>> headers_sync_params{std::nullopt};\n     };\n\n     static std::unique_ptr<PeerManager> make(CConnman& connman, AddrMan& addrman,\ndiff --git a/src/test/fuzz/cmpctblock.cpp b/src/test/fuzz/cmpctblock.cpp\nindex 4f1e0df805..fea6229790 100644\n--- a/src/test/fuzz/cmpctblock.cpp\n+++ b/src/test/fuzz/cmpctblock.cpp\n@@ -24,7 +24,6 @@\n #include <test/fuzz/FuzzedDataProvider.h>\n #include <test/fuzz/fuzz.h>\n #include <test/fuzz/util.h>\n-#include <test/fuzz/util/headerssync.h>\n #include <test/fuzz/util/net.h>\n #include <test/util/net.h>\n #include <test/util/random.h>\n@@ -141,7 +140,6 @@ FUZZ_TARGET(cmpctblock, .init = initialize_cmpctblock)\n                                      mempool, *setup->m_node.warnings,\n                                      PeerManager::Options{\n                                          .deterministic_rng = true,\n-                                         .headers_sync_params = FUZZ_HEADERS_SYNC_PARAMS,\n                                      });\n     connman.SetMsgProc(peerman.get());\n\ndiff --git a/src/test/fuzz/p2p_handshake.cpp b/src/test/fuzz/p2p_handshake.cpp\nindex 8d1c1b2373..0ab3e0c248 100644\n--- a/src/test/fuzz/p2p_handshake.cpp\n+++ b/src/test/fuzz/p2p_handshake.cpp\n@@ -10,7 +10,6 @@\n #include <test/fuzz/FuzzedDataProvider.h>\n #include <test/fuzz/fuzz.h>\n #include <test/fuzz/util.h>\n-#include <test/fuzz/util/headerssync.h>\n #include <test/fuzz/util/net.h>\n #include <test/util/net.h>\n #include <test/util/setup_common.h>\n@@ -58,7 +57,6 @@ FUZZ_TARGET(p2p_handshake, .init = ::initialize)\n                                      PeerManager::Options{\n                                          .reconcile_txs = true,\n                                          .deterministic_rng = true,\n-                                         .headers_sync_params = FUZZ_HEADERS_SYNC_PARAMS,\n                                      });\n     connman.SetMsgProc(node.peerman.get());\n     connman.SetAddrman(*node.addrman);\ndiff --git a/src/test/fuzz/process_message.cpp b/src/test/fuzz/process_message.cpp\nindex e5320155a8..a63f8a0a6a 100644\n--- a/src/test/fuzz/process_message.cpp\n+++ b/src/test/fuzz/process_message.cpp\n@@ -14,7 +14,6 @@\n #include <test/fuzz/FuzzedDataProvider.h>\n #include <test/fuzz/fuzz.h>\n #include <test/fuzz/util.h>\n-#include <test/fuzz/util/headerssync.h>\n #include <test/fuzz/util/net.h>\n #include <test/util/net.h>\n #include <test/util/random.h>\n@@ -91,7 +90,6 @@ FUZZ_TARGET(process_message, .init = initialize_process_message)\n                                      PeerManager::Options{\n                                          .reconcile_txs = true,\n                                          .deterministic_rng = true,\n-                                         .headers_sync_params = FUZZ_HEADERS_SYNC_PARAMS,\n                                      });\n\n     connman.SetMsgProc(node.peerman.get());\ndiff --git a/src/test/fuzz/process_messages.cpp b/src/test/fuzz/process_messages.cpp\nindex 4ede3ce674..f4a93df8dc 100644\n--- a/src/test/fuzz/process_messages.cpp\n+++ b/src/test/fuzz/process_messages.cpp\n@@ -14,7 +14,6 @@\n #include <test/fuzz/FuzzedDataProvider.h>\n #include <test/fuzz/fuzz.h>\n #include <test/fuzz/util.h>\n-#include <test/fuzz/util/headerssync.h>\n #include <test/fuzz/util/net.h>\n #include <test/util/net.h>\n #include <test/util/random.h>\n@@ -83,7 +82,6 @@ FUZZ_TARGET(process_messages, .init = initialize_process_messages)\n                                      PeerManager::Options{\n                                          .reconcile_txs = true,\n                                          .deterministic_rng = true,\n-                                         .headers_sync_params = FUZZ_HEADERS_SYNC_PARAMS,\n                                      });\n     connman.SetMsgProc(node.peerman.get());\n     connman.SetAddrman(*node.addrman);\ndiff --git a/src/test/fuzz/util/headerssync.h b/src/test/fuzz/util/headerssync.h\ndeleted file mode 100644\nindex abfea0ab68..0000000000\n--- a/src/test/fuzz/util/headerssync.h\n+++ /dev/null\n@@ -1,18 +0,0 @@\n-// Copyright (c) The Bitcoin Core developers\n-// Distributed under the MIT software license, see the accompanying\n-// file COPYING or http://www.opensource.org/licenses/mit-license.php.\n-\n-#ifndef BITCOIN_TEST_FUZZ_UTIL_HEADERSSYNC_H\n-#define BITCOIN_TEST_FUZZ_UTIL_HEADERSSYNC_H\n-\n-#include <cstddef>\n-#include <utility>\n-\n-/** Fixed headers-sync parameters for fuzz targets that construct a PeerManager per iteration,\n- *  avoiding both recomputing them every time and any dependence on the (mocked) clock at\n- *  construction time, as {commitment period, redownload buffer size}. The values are the regtest\n- *  constants that were committed in chainparams before the parameters became runtime-computed,\n- *  but any sane values would do. */\n-inline constexpr std::pair<size_t, size_t> FUZZ_HEADERS_SYNC_PARAMS{275, 7017};\n-\n-#endif // BITCOIN_TEST_FUZZ_UTIL_HEADERSSYNC_H\n```",
   "id": "rc:4215563224",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4215563224",
   "hash": "c603d7cb98b26643",
   "edited": "2026-10-08T08:14:43Z"
  },
  {
   "t": "2026-10-08T06:22:38Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "9f7bcfe874729d8b20c46dd61d9dab8a0e5ea3fa",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nSince `fail_buf = 2 * succ_buf + 1` throughout the growth loop, we can grow just `fail_buf` internally and derive `succ_buf` at the end.\n\nTrack one bound while growing the search range\n\n```diff\ndiff --git a/src/headerssync.cpp b/src/headerssync.cpp\nindex b89468b174..06964703a4 100644\n--- a/src/headerssync.cpp\n+++ b/src/headerssync.cpp\n@@ -466,14 +466,12 @@ HeadersSyncParams ComputeHeadersSyncParamsInner(int64_t max_headers, int64_t min\n\n     /** Smallest bufsize whose attack rate against (period, bufsize) is below attack_headers. */\n     auto find_bufsize = [&](int64_t period) noexcept -> int64_t {\n-        int64_t succ_buf{0};\n         int64_t fail_buf{1};\n         // First double iteratively until an upper bound for failure is found.\n         while (attack_rate(period, fail_buf) >= attack_headers) {\n-            const int64_t next_fail = 3 * fail_buf - 2 * succ_buf;\n-            succ_buf = fail_buf;\n-            fail_buf = next_fail;\n+            fail_buf = 2 * fail_buf + 1;\n         }\n+        int64_t succ_buf{fail_buf / 2};\n         // Then perform a bisection search to narrow it down.\n         while (fail_buf > succ_buf + 1) {\n             const int64_t try_buf = (succ_buf + fail_buf) / 2;\n```",
   "id": "rc:4215577524",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4215577524",
   "hash": "6439e7b946b273bd",
   "edited": "2026-10-08T08:14:43Z"
  },
  {
   "t": "2026-10-08T07:27:37Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "9f7bcfe874729d8b20c46dd61d9dab8a0e5ea3fa",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nUnless I'm missing something, this could be simplified to modulo, see https://godbolt.org/z/53PfMqxnj.\nIf we cannot use that, could you add a same-line comment explaining why?\n\nUse the remainder operator\n\n```diff\ndiff --git a/src/headerssync.cpp b/src/headerssync.cpp\nindex 06964703a4..1b5ec9bc43 100644\n--- a/src/headerssync.cpp\n+++ b/src/headerssync.cpp\n@@ -448,7 +448,7 @@ HeadersSyncParams ComputeHeadersSyncParamsInner(int64_t max_headers, int64_t min\n      */\n     auto attack_rate = [](int64_t period, int64_t bufsize) noexcept -> double {\n         auto k = bufsize / period;\n-        auto m = bufsize - k * period;\n+        auto m = bufsize % period;\n         return std::ldexp((period * (6.0 * period - 4.0 * m - 2.0) + m * (m + 1.0)) / period, -int(k + 2));\n     };\n\n```",
   "id": "rc:4216174276",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4216174276",
   "hash": "426355fad53bebbc",
   "edited": "2026-10-08T08:14:43Z"
  },
  {
   "t": "2026-10-08T07:29:24Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/test/headerssync_tests.cpp",
   "commit": "9f7bcfe874729d8b20c46dd61d9dab8a0e5ea3fa",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nNit: this only tests `ComputeHeadersSyncParamsInner` (which doesn\u2019t need the node setup), could we drop the fixture?\n\nDrop the unnecessary test fixture\n\n```diff\ndiff --git a/src/test/headerssync_tests.cpp b/src/test/headerssync_tests.cpp\nindex 4a3000cdb2..a093bf6287 100644\n--- a/src/test/headerssync_tests.cpp\n+++ b/src/test/headerssync_tests.cpp\n@@ -4,13 +4,11 @@\n\n #include <headerssync.h>\n\n-#include <test/util/setup_common.h>\n-\n #include <boost/test/unit_test.hpp>\n\n #include <cstdint>\n\n-BOOST_FIXTURE_TEST_SUITE(headerssync_tests, BasicTestingSetup)\n+BOOST_AUTO_TEST_SUITE(headerssync_tests)\n\n // Test vectors checking that ComputeHeadersSyncParamsInner() finds the optimal (period, bufsize)\n // configuration, each a tuple containing:\n```",
   "id": "rc:4216190418",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4216190418",
   "hash": "9a3eba2d0806c75b",
   "edited": "2026-10-08T08:14:43Z"
  },
  {
   "t": "2026-10-08T07:31:43Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "02d1084eba30de70c45eeaee77312fd417f79bd8",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nSince this assertion protects the test vectors (and since `ComputeHeadersSyncParamsInner` is already very complicated), I\u2019d keep it in the test.\n\nMove the test-vector size assertion\n\n```diff\ndiff --git a/src/headerssync.cpp b/src/headerssync.cpp\nindex 1b5ec9bc43..be9b3d8b1a 100644\n--- a/src/headerssync.cpp\n+++ b/src/headerssync.cpp\n@@ -394,9 +394,6 @@ HeadersSyncParams ComputeHeadersSyncParamsInner(int64_t max_headers, int64_t min\n {\n     /** Headers in the redownload buffer are stored without prevhash, as a CompressedHeader. [bits] */\n     constexpr int64_t COMPACT_HEADER_SIZE{sizeof(CompressedHeader) * 8};\n-    // The unit tests would need to be regenerated if this constant were to change, so\n-    // static_assert to give a more targeted error message.\n-    static_assert(COMPACT_HEADER_SIZE == 384);\n\n     // The chain has at least one header.\n     Assume(minchainwork_headers > 0);\ndiff --git a/src/test/headerssync_tests.cpp b/src/test/headerssync_tests.cpp\nindex a093bf6287..b5b13da883 100644\n--- a/src/test/headerssync_tests.cpp\n+++ b/src/test/headerssync_tests.cpp\n@@ -31,6 +31,8 @@ BOOST_AUTO_TEST_SUITE(headerssync_tests)\n\n BOOST_AUTO_TEST_CASE(compute_headers_sync_params)\n {\n+    constexpr int64_t COMPACT_HEADER_SIZE{384};\n+    static_assert(sizeof(CompressedHeader) * 8 == COMPACT_HEADER_SIZE, \"Regenerate the optimizer test vectors if CompressedHeader changes size\");\n     const struct {\n         int64_t max_headers;\n         int64_t minchainwork_headers;\n```",
   "id": "rc:4216211760",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4216211760",
   "hash": "50eda5029324e309",
   "edited": "2026-10-08T08:14:43Z"
  },
  {
   "t": "2026-10-08T07:56:56Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "9f7bcfe874729d8b20c46dd61d9dab8a0e5ea3fa",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nNit: I find the preceding `2^-K * ...` form easier to follow. Could we separate the expected headers from the probability of passing the first `K` checks here?\n\nSeparate the probability from the expected headers\n\n```diff\ndiff --git a/src/headerssync.cpp b/src/headerssync.cpp\nindex bb1701cebd..7a083848c0 100644\n--- a/src/headerssync.cpp\n+++ b/src/headerssync.cpp\n@@ -448,7 +448,9 @@ HeadersSyncParams ComputeHeadersSyncParamsInner(int64_t max_headers, int64_t min\n     auto attack_rate = [](int64_t period, int64_t bufsize) noexcept -> double {\n         auto k = bufsize / period;\n         auto m = bufsize % period;\n-        return std::ldexp((period * (6.0 * period - 4.0 * m - 2.0) + m * (m + 1.0)) / period, -int(k + 2));\n+        // Expected accepted headers, given the first k checks pass\n+        const double expected_headers = (period * (6.0 * period - 4.0 * m - 2.0) + m * (m + 1.0)) / (4.0 * period);\n+        return std::ldexp(expected_headers, -int(k));\n     };\n\n     /** The peak per-peer memory a (period,bufsize) configuration needs. */\n```",
   "id": "rc:4216439505",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4216439505",
   "hash": "780975092c2551a7",
   "edited": "2026-10-08T08:14:43Z"
  },
  {
   "t": "2026-10-08T07:59:40Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "9f7bcfe874729d8b20c46dd61d9dab8a0e5ea3fa",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nI wonder whether we need to find the optimum this precisely. If we restricted the buffer to whole commitment periods, we could calculate its size directly, and a logarithmic estimate might replace the period search too. The earlier experiments suggest a small memory increase could let us remove both searches. Do you think that tradeoff is worth exploring?\n\nWhole-period calculation and earlier measurements\n\nAI summary:\n[quoted text omitted]",
   "id": "rc:4216461724",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4216461724",
   "hash": "c27ef66b4ed05555",
   "edited": "2026-10-08T08:14:43Z"
  },
  {
   "t": "2026-10-08T08:01:39Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "9f7bcfe874729d8b20c46dd61d9dab8a0e5ea3fa",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nNit: The attack budget stays fixed during this search, so we could calculate its logarithm once in the capture.\n\nCalculate the fixed logarithm once\n\n```diff\ndiff --git a/src/headerssync.cpp b/src/headerssync.cpp\nindex 7a083848c0..1f8485e0a7 100644\n--- a/src/headerssync.cpp\n+++ b/src/headerssync.cpp\n@@ -525,8 +525,8 @@ HeadersSyncParams ComputeHeadersSyncParamsInner(int64_t max_headers, int64_t min\n         //\n         //   M^2 - (4*P - 1)*M + 6*P^2 - 2*P - attack_headers * P * 2^(K+2) = 0.\n         //\n-        auto cont_bufsize = [&](double period) noexcept -> double {\n-            const double k = std::floor(std::log2(1.5 * period - 0.5) - std::log2(attack_headers));\n+        auto cont_bufsize = [attack_headers, log_attack_headers = std::log2(attack_headers)](double period) noexcept -> double {\n+            const double k = std::floor(std::log2(1.5 * period - 0.5) - log_attack_headers);\n             const double m = 0.5 * (4.0 * period - 1.0 - std::sqrt(std::ldexp(attack_headers * period, int(k + 4)) - 8.0 * period * period + 1.0));\n             return k * period + m;\n         };\n```",
   "id": "rc:4216477737",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4216477737",
   "hash": "74c1aec2e16bb3ed",
   "edited": "2026-10-08T08:14:43Z"
  },
  {
   "t": "2026-10-08T08:14:43Z",
   "kind": "review",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "state": "CHANGES_REQUESTED",
   "commit": "9f7bcfe874729d8b20c46dd61d9dab8a0e5ea3fa",
   "text": "Concept ACK on calculating the parameters at startup from elapsed time since genesis, instead of predicting the release's lifetime.\n\nI found the calculation difficult to review, and the lengthy derivations interspersed with the code made it harder to follow. I left a few simplification suggestions and a question about whether a small memory tradeoff could eliminate the searches, and whether the unbounded floating-point loop is safe (and whether we can eliminate it completely).",
   "id": "r:5412352111",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#pullrequestreview-5412352111",
   "hash": "02e7a616989d3e1d",
   "edited": null
  },
  {
   "t": "2026-10-08T18:52:17Z",
   "kind": "force_push",
   "who": "sipa",
   "commit": "9451c93ff38435f2ce100af786b9221b0c565c5c",
   "id": "push:9451c93ff38435f2ce100af786b9221b0c565c5c"
  },
  {
   "t": "2026-10-08T18:54:31Z",
   "kind": "review_comment",
   "who": "sipa",
   "assoc": "MEMBER",
   "path": "src/test/headerssync_tests.cpp",
   "commit": "9f7bcfe874729d8b20c46dd61d9dab8a0e5ea3fa",
   "in_reply_to": 4216190418,
   "text": "Done.",
   "id": "rc:4222944637",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4222944637",
   "hash": "ed251864987c367e",
   "edited": "2026-10-08T19:07:05Z"
  },
  {
   "t": "2026-10-08T18:54:39Z",
   "kind": "review_comment",
   "who": "sipa",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "9f7bcfe874729d8b20c46dd61d9dab8a0e5ea3fa",
   "in_reply_to": 4214406451,
   "text": "Done.",
   "id": "rc:4222945901",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4222945901",
   "hash": "ed251864987c367e",
   "edited": "2026-10-08T19:07:05Z"
  },
  {
   "t": "2026-10-08T18:57:18Z",
   "kind": "review_comment",
   "who": "sipa",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "02d1084eba30de70c45eeaee77312fd417f79bd8",
   "in_reply_to": 4216211760,
   "text": "I didn't take this, as it risks a disconnect between the two (e.g. a new `ExtraCompressedHeader` is introduced but only one of the two locations is updated to use it). Could be resolved by exporting the actually used `COMPACT_HEADER_SIZE`, and asserting on that in the test, but I feel that's overkill.",
   "id": "rc:4222970647",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4222970647",
   "hash": "a59672cd9b4d2dcf",
   "edited": "2026-10-08T19:07:05Z"
  },
  {
   "t": "2026-10-08T19:00:12Z",
   "kind": "review_comment",
   "who": "sipa",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "9f7bcfe874729d8b20c46dd61d9dab8a0e5ea3fa",
   "in_reply_to": 4216461724,
   "text": "It's true that the need for exactness is less relevant than in earlier iterations of this PR. But I still like the fact that it actually finds *the* optimal solution (within rounding precision) to a well-defined (but weakened) variant of the real problem, so a more naive search will find the exact same solutions.\n\nTo demonstrate that, I added a commit that does a more naive search on random inputs.",
   "id": "rc:4222997179",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4222997179",
   "hash": "8cd59759a54b1e66",
   "edited": "2026-10-08T19:18:04Z"
  },
  {
   "t": "2026-10-08T19:03:53Z",
   "kind": "review_comment",
   "who": "sipa",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "9f7bcfe874729d8b20c46dd61d9dab8a0e5ea3fa",
   "in_reply_to": 4216174276,
   "text": "Done.\n\nThe reason for not doing this was my (folklore, perhaps) aversion due to division and modulus operators which are very slow instructions (as far as CPU instructions go; they used to be ~50s slower than multiplication IIRC).\n\nModern compilers don't need this, as will turn the computation of both a division and a modulus into a single instruction anyway, so I took your change.",
   "id": "rc:4223031132",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4223031132",
   "hash": "d4bec89b7aea6e60",
   "edited": "2026-10-08T19:07:05Z"
  },
  {
   "t": "2026-10-08T19:04:02Z",
   "kind": "review_comment",
   "who": "sipa",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "9f7bcfe874729d8b20c46dd61d9dab8a0e5ea3fa",
   "in_reply_to": 4216439505,
   "text": "Done.",
   "id": "rc:4223032721",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4223032721",
   "hash": "ed251864987c367e",
   "edited": "2026-10-08T19:07:05Z"
  },
  {
   "t": "2026-10-08T19:04:12Z",
   "kind": "review_comment",
   "who": "sipa",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "9f7bcfe874729d8b20c46dd61d9dab8a0e5ea3fa",
   "in_reply_to": 4215577524,
   "text": "Done.",
   "id": "rc:4223034130",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4223034130",
   "hash": "ed251864987c367e",
   "edited": "2026-10-08T19:07:05Z"
  },
  {
   "t": "2026-10-08T19:04:17Z",
   "kind": "review_comment",
   "who": "sipa",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "9f7bcfe874729d8b20c46dd61d9dab8a0e5ea3fa",
   "in_reply_to": 4216477737,
   "text": "Done.",
   "id": "rc:4223034868",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4223034868",
   "hash": "ed251864987c367e",
   "edited": "2026-10-08T19:07:05Z"
  },
  {
   "t": "2026-10-08T19:05:11Z",
   "kind": "review_comment",
   "who": "sipa",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "9f7bcfe874729d8b20c46dd61d9dab8a0e5ea3fa",
   "in_reply_to": 4215432352,
   "text": "Nice catch. Fixed, but with a much better alternative: do the bisection over integer `bufsize` values, as those are the only ones we care about anyway. Avoids floating-point shenanigans like these, and is faster too.",
   "id": "rc:4223043409",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4223043409",
   "hash": "f606334e9b87f8d9",
   "edited": "2026-10-08T19:07:05Z"
  },
  {
   "t": "2026-10-08T19:06:25Z",
   "kind": "review_comment",
   "who": "sipa",
   "assoc": "MEMBER",
   "path": "src/net_processing.cpp",
   "commit": "9f7bcfe874729d8b20c46dd61d9dab8a0e5ea3fa",
   "in_reply_to": 4215548260,
   "text": "Done, in a later commit.\n\nThe reason for this approach (still kept in the first commit) is that doing it while such parameters are still defined in chainparams would need a cyclic dependency. With the later commit that removes them from chainparams, it's possible to move the `HeadersSyncParams` to headerssync.h.",
   "id": "rc:4223054953",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4223054953",
   "hash": "3c23cb41c4f6e0b0",
   "edited": "2026-10-08T19:07:05Z"
  },
  {
   "t": "2026-10-08T19:06:42Z",
   "kind": "review_comment",
   "who": "sipa",
   "assoc": "MEMBER",
   "path": "src/net_processing.h",
   "commit": "9f7bcfe874729d8b20c46dd61d9dab8a0e5ea3fa",
   "in_reply_to": 4215563224,
   "text": "Done, and attributed.",
   "id": "rc:4223057506",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4223057506",
   "hash": "9ea18b6524226c34",
   "edited": "2026-10-08T19:07:05Z"
  },
  {
   "t": "2026-10-09T06:16:03Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/headerssync.h",
   "commit": "9451c93ff38435f2ce100af786b9221b0c565c5c",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nNit: The caller uses the current time, even when the local tip is still genesis. I\u2019d describe this as genesis-to-now.\n\nMatch the age description to its caller\n\n```diff\ndiff --git a/src/headerssync.h b/src/headerssync.h\nindex 74516120db..d3448d722b 100644\n--- a/src/headerssync.h\n+++ b/src/headerssync.h\n@@ -68,7 +68,7 @@ struct HeadersSyncParams {\n\n /** Find a near-optimal (period, bufsize) headers synchronization configuration for:\n  *\n- * - A specified timespan between genesis and the tip of the chain.\n+ * - A specified timespan between genesis and now.\n  * - A specified number of headers in the minchainwork chain.\n  *\n  * See the constants in the implementation that control what attack success rate is acceptable.\n```",
   "id": "rc:4227208074",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4227208074",
   "hash": "6361eb16e090ef46",
   "edited": "2026-10-09T08:31:27Z"
  },
  {
   "t": "2026-10-09T06:18:34Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "9451c93ff38435f2ce100af786b9221b0c565c5c",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nNit: Now that both searches use integer bounds, we could use `std::midpoint` in both places.\n\nUse the standard midpoint helper\n\n```diff\ndiff --git a/src/headerssync.cpp b/src/headerssync.cpp\nindex 19b9b517f4..4bdc6f8ff0 100644\n--- a/src/headerssync.cpp\n+++ b/src/headerssync.cpp\n@@ -13,6 +13,7 @@\n #include <algorithm>\n #include <cmath>\n #include <cstdint>\n+#include <numeric>\n #include <tuple>\n #include <utility>\n\n@@ -481,7 +482,7 @@ HeadersSyncParams ComputeHeadersSyncParamsInner(int64_t max_headers, int64_t min\n         int64_t succ_buf = fail_buf / 2;\n         // Then perform a bisection search to narrow it down.\n         while (fail_buf > succ_buf + 1) {\n-            const int64_t try_buf = (succ_buf + fail_buf) / 2;\n+            const int64_t try_buf = std::midpoint(succ_buf, fail_buf);\n             if (HeadersSyncAttackRate(period, try_buf) >= attack_headers) {\n                 succ_buf = try_buf;\n             } else {\n@@ -554,7 +555,7 @@ HeadersSyncParams ComputeHeadersSyncParamsInner(int64_t max_headers, int64_t min\n         int64_t lo = 0;\n         int64_t hi = minchainwork_headers + 1;\n         while (hi - lo > 1) {\n-            const int64_t mid = lo + (hi - lo) / 2;\n+            const int64_t mid = std::midpoint(lo, hi);\n             if (mid * cont_bufsize(mid) < target) {\n                 lo = mid;\n             } else {\n```",
   "id": "rc:4227225807",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4227225807",
   "hash": "75436060c9328650",
   "edited": "2026-10-09T08:31:27Z"
  },
  {
   "t": "2026-10-09T06:20:28Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "9f7bcfe874729d8b20c46dd61d9dab8a0e5ea3fa",
   "in_reply_to": 4215432352,
   "text": "Thanks, please resolve",
   "id": "rc:4227237701",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4227237701",
   "hash": "f7ae2253e9324778",
   "edited": "2026-10-09T08:31:27Z"
  },
  {
   "t": "2026-10-09T06:27:59Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "9f7bcfe874729d8b20c46dd61d9dab8a0e5ea3fa",
   "in_reply_to": 4216461724,
   "text": "Since this is the optimum for our simplified model of the attack surface, how much confidence do we have that the extra precision translates into a better tradeoff in practice?\nHonest question, I'm still working on fully understanding it.\nCould a slightly less memory-efficient result within that model be worth a simpler calculation, while preserving the same security bound?\n\nI won't block either way, but the added complexity makes it harder for me to ACK confidently.",
   "id": "rc:4227285565",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4227285565",
   "hash": "b5419929fdf68744",
   "edited": "2026-10-09T08:31:27Z"
  },
  {
   "t": "2026-10-09T08:23:35Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "9451c93ff38435f2ce100af786b9221b0c565c5c",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nNit: With the integer search, a zero height would give Step 2 invalid clamp bounds, so I would update this explanation. The earlier \"real-valued\" wording also helped me see that this formula allows fractional inputs.\n\nRefresh the comments after the search rewrite\n\n```diff\ndiff --git a/src/headerssync.cpp b/src/headerssync.cpp\nindex 4bdc6f8ff0..40b4a05057 100644\n--- a/src/headerssync.cpp\n+++ b/src/headerssync.cpp\n@@ -448,8 +448,7 @@ HeadersSyncParams ComputeHeadersSyncParamsInner(int64_t max_headers, int64_t min\n     // static_assert to give a more targeted error message.\n     static_assert(COMPACT_HEADER_SIZE == 384);\n\n-    // The chain has at least one header. Otherwise Step 1 below returns a zero period, which\n-    // Step 2 then divides by.\n+    // Step 2 requires a nonempty range of legal periods [1, minchainwork_headers]\n     Assert(minchainwork_headers > 0);\n     // Some small attacker success rate is inevitable. A non-positive one cannot be reached, which\n     // would make Step 1 below compute with infinities, and the search for bufsize not terminate.\n@@ -516,7 +515,7 @@ HeadersSyncParams ComputeHeadersSyncParamsInner(int64_t max_headers, int64_t min\n         //\n         // Both of these turn into exact equalities in the continuous relaxation:\n         // (1) becomes HeadersSyncAttackRate(period, bufsize) = attack_headers, because there is no need\n-        //     overshoot.\n+        //     to overshoot.\n         // (2) becomes mem_timewarp(period, bufsize) = mem_mainchain(period, bufsize), because\n         //     timewarp memory goes down with increasing period, and mainchain memory goes up with\n         //     increasing period (with bufsize set to keep the attack rate constant). The maximum\n@@ -525,7 +524,7 @@ HeadersSyncParams ComputeHeadersSyncParamsInner(int64_t max_headers, int64_t min\n         //     practically always true.\n         //\n         // For the continuous relaxation, the expression from HeadersSyncAttackRate is used with\n-        // period and bufsize (and thus M in [0, P)). As a function of bufsize it is continuous and\n+        // real-valued period and bufsize (and thus M in [0, P)). As a function of bufsize it is continuous and\n         // strictly decreasing, and for the bufsize values sharing a given K it takes values in\n         // (2^-K * (3*P - 1) / 4, 2^-K * (3*P - 1) / 2]. Thus (1) can be solved for bufsize\n         // directly: K is the unique integer for which attack_headers falls in that range, after\n```",
   "id": "rc:4228134177",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4228134177",
   "hash": "7be38903e496516e",
   "edited": "2026-10-09T08:31:27Z"
  },
  {
   "t": "2026-10-09T08:28:18Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/test/headerssync_tests.cpp",
   "commit": "9451c93ff38435f2ce100af786b9221b0c565c5c",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nNit: I would find the failing period more useful than a failure count here. Initializing the best memory to the maximum also avoids treating the first iteration differently.\n\nReport the period at a boundary failure\n\n```diff\ndiff --git a/src/net_processing.h b/src/net_processing.h\nindex 6f946bd98a..ec17e8f60a 100644\n--- a/src/net_processing.h\n+++ b/src/net_processing.h\n@@ -23,7 +23,6 @@\n #include <memory>\n #include <optional>\n #include <string>\n-#include <utility>\n #include <vector>\n\n class AddrMan;\ndiff --git a/src/test/headerssync_tests.cpp b/src/test/headerssync_tests.cpp\nindex 78aaae7cfb..42237afa17 100644\n--- a/src/test/headerssync_tests.cpp\n+++ b/src/test/headerssync_tests.cpp\n@@ -12,6 +12,7 @@\n #include <chrono>\n #include <cmath>\n #include <cstdint>\n+#include <limits>\n\n BOOST_AUTO_TEST_SUITE(headerssync_tests)\n\n@@ -130,20 +131,18 @@ static void CheckOptimalByBruteForce(int64_t max_headers, int64_t minchainwork_h\n     constexpr int64_t COMPACT_HEADER_SIZE{sizeof(CompressedHeader) * 8};\n     int64_t best_period{0};\n     int64_t best_bufsize{0};\n-    int64_t best_mem{0};\n+    int64_t best_mem{std::numeric_limits<int64_t>::max()};\n     int64_t bufsize{1};\n-    int64_t boundary_failures{0};\n     for (int64_t period{1}; period <= minchainwork_headers; ++period) {\n         while (HeadersSyncAttackRate(period, bufsize) >= attack_headers) ++bufsize;\n-        if (bufsize > 1 && HeadersSyncAttackRate(period, bufsize - 1) < attack_headers) ++boundary_failures;\n+        BOOST_CHECK_MESSAGE(HeadersSyncAttackRate(period, bufsize - 1) >= attack_headers, \"period=\" << period);\n         const int64_t mem{std::max(max_headers / period, minchainwork_headers / period + bufsize * COMPACT_HEADER_SIZE)};\n-        if (best_period == 0 || mem < best_mem) {\n+        if (mem < best_mem) {\n             best_period = period;\n             best_bufsize = bufsize;\n             best_mem = mem;\n         }\n     }\n-    BOOST_CHECK_EQUAL(boundary_failures, 0);\n     BOOST_CHECK_EQUAL(expected_period, best_period);\n     BOOST_CHECK_EQUAL(expected_bufsize, best_bufsize);\n }\n```",
   "id": "rc:4228169398",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4228169398",
   "hash": "915741881e8de8f9",
   "edited": "2026-10-09T08:58:42Z"
  },
  {
   "t": "2026-10-09T08:28:40Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "9451c93ff38435f2ce100af786b9221b0c565c5c",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nWould it make sense to encapsulate the bounds on legal periods in the search, so Step 2 can use its two candidates directly?\n\nTry the two endpoints of the legal bracket\n\n```diff\ndiff --git a/src/headerssync.cpp b/src/headerssync.cpp\nindex 40b4a05057..01786fa0fc 100644\n--- a/src/headerssync.cpp\n+++ b/src/headerssync.cpp\n@@ -494,9 +494,9 @@ HeadersSyncParams ComputeHeadersSyncParamsInner(int64_t max_headers, int64_t min\n     // The overall strategy to solve our problem (minimizing peak memory usage while staying under\n     // the acceptable attack rate), is as follows:\n     //\n-    // Step 1: assume that period and bufsize can be arbitrary real numbers, and find the largest\n-    //         integer period which is still below the optimum of that continuous relaxation.\n-    // Step 2: try that period and the next one, and return the one whose memory usage is lowest\n+    // Step 1: assume that period and bufsize can be arbitrary real numbers, and find the two\n+    //         adjacent legal integer periods around the optimum of that continuous relaxation.\n+    // Step 2: try both periods, and return the one whose memory usage is lowest\n     //         (breaking ties toward the smallest period).\n     //\n     // As a function of the period, the memory usage is the maximum of a decreasing (timewarp) and\n@@ -506,9 +506,9 @@ HeadersSyncParams ComputeHeadersSyncParamsInner(int64_t max_headers, int64_t min\n     // for absurdly high attack rates, and even then, it only results in memory usage slightly\n     // above the optimal.\n\n-    // Step 1: find the largest integer period below the optimum of the continuous relaxation of\n-    //         the problem (0 if even the smallest legal period exceeds that optimum).\n-    const auto period_below = [&]() {\n+    // Step 1: find the adjacent legal periods around the optimum of the continuous relaxation of\n+    //         the problem (or the two at the end of the legal range it lies beyond).\n+    const auto [period_below, period_above] = [&] {\n         // Abstractly, our goal is to find (period, bufsize):\n         // (1) such that HeadersSyncAttackRate(period, bufsize) <= attack_headers\n         // (2) such that memory_usage(period, bufsize) is minimal\n@@ -545,14 +545,14 @@ HeadersSyncParams ComputeHeadersSyncParamsInner(int64_t max_headers, int64_t min\n         //\n         // whose left-hand side increases with period. The periods below the continuous optimum are\n         // therefore exactly those whose left-hand side is still below the right-hand side, so the\n-        // largest one can be found by bisection. As only integer periods are ever tried, that\n-        // bisection can be performed over the integers, bracketed just outside the legal range so\n-        // that an optimum below 1 or above minchainwork_headers yields that endpoint. Note that\n+        // nearest legal periods can be found by bisection. As only integer periods are ever tried, that\n+        // bisection can be performed over the integers within the legal range. If the optimum lies\n+        // outside that range, return the two periods nearest that endpoint instead. Note that\n         // the interval shrinks by construction on every iteration, independent of how accurately\n         // the condition is evaluated.\n         const auto target = double(max_headers - minchainwork_headers) / COMPACT_HEADER_SIZE;\n-        int64_t lo = 0;\n-        int64_t hi = minchainwork_headers + 1;\n+        int64_t lo = 1;\n+        int64_t hi = minchainwork_headers;\n         while (hi - lo > 1) {\n             const int64_t mid = std::midpoint(lo, hi);\n             if (mid * cont_bufsize(mid) < target) {\n@@ -561,15 +561,12 @@ HeadersSyncParams ComputeHeadersSyncParamsInner(int64_t max_headers, int64_t min\n                 hi = mid;\n             }\n         }\n-        return lo;\n+        return std::pair{lo, hi};\n     }();\n\n-    // Step 2: try period_below and the period above it, and pick the one which, together with its\n-    //         corresponding bufsize, results in the lowest memory usage. Both candidates are\n-    //         clamped into the legal range, which matters only when the continuous optimum lies\n-    //         outside of it.\n-    const auto make_config = [&](int64_t candidate) noexcept {\n-        const auto period = std::clamp<int64_t>(candidate, 1, minchainwork_headers);\n+    // Step 2: try period_below and period_above, and pick the one which, together with its\n+    //         corresponding bufsize, results in the lowest memory usage.\n+    const auto make_config = [&](int64_t period) noexcept {\n         const auto bufsize = find_bufsize(period);\n         return std::tuple{memory_usage(period, bufsize), period, bufsize};\n     };\n@@ -577,7 +574,7 @@ HeadersSyncParams ComputeHeadersSyncParamsInner(int64_t max_headers, int64_t min\n     // low ones the smallest period.\n     const auto [_memory, best_period, best_bufsize] = std::min(\n         make_config(period_below),\n-        make_config(period_below + 1));\n+        make_config(period_above));\n\n     return {.commitment_period = size_t(best_period), .redownload_buffer_size = size_t(best_bufsize)};\n }\n```",
   "id": "rc:4228172014",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4228172014",
   "hash": "53b44bb6a34f5067",
   "edited": "2026-10-09T08:31:27Z"
  },
  {
   "t": "2026-10-09T08:31:27Z",
   "kind": "review",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "state": "APPROVED",
   "commit": "9451c93ff38435f2ce100af786b9221b0c565c5c",
   "text": "Only had time for a quick pass and left a few questions. I'll review more carefully later.",
   "id": "r:5466423724",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#pullrequestreview-5466423724",
   "hash": "701635380a8aa249",
   "edited": null
  },
  {
   "t": "2026-10-09T17:05:49Z",
   "kind": "force_push",
   "who": "sipa",
   "commit": "02d1084eba30de70c45eeaee77312fd417f79bd8",
   "id": "push:02d1084eba30de70c45eeaee77312fd417f79bd8"
  },
  {
   "t": "2026-10-09T17:08:48Z",
   "kind": "review_comment",
   "who": "sipa",
   "assoc": "MEMBER",
   "path": "src/test/headerssync_tests.cpp",
   "commit": "9451c93ff38435f2ce100af786b9221b0c565c5c",
   "in_reply_to": 4228169398,
   "text": "I have dropped this commit, because `attack_rate` is now gone entirely (see further). I could bring it back, with a test-only copy of the old `attack_rate` code if desirable, but it seems overkill now.",
   "id": "rc:4232635355",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4232635355",
   "hash": "f01b0bfaaeab9364",
   "edited": "2026-10-09T17:16:40Z"
  },
  {
   "t": "2026-10-09T17:14:49Z",
   "kind": "review_comment",
   "who": "sipa",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "9f7bcfe874729d8b20c46dd61d9dab8a0e5ea3fa",
   "in_reply_to": 4216461724,
   "text": "I forgot where, but you commented somewhere about simplifying to remove the need to search for bufsize.\n\nIt turns out that no simplification is needed, since we already have a closed-form function that finds the optimal bufsize when relaxed to real numbers (formerly `cont_bufsize`, now renamed to `real_bufsize`). The integer version doesn't need bisection, it can just round `real_bufsize` up to an integer.\n\nThis still leaves a bisection search for the periods. That one is harder to get rid of. It's possible to simplify the `attack_rate` formula a bit more so the result can be found using a closed form expression with the [Lambert W](https://en.wikipedia.org/wiki/Lambert_W_function) function, and evaluating that needs a loop still too. Simplifying it even further so that the solution can be written explicitly using just elementary functions would lose a great deal of accuracy.",
   "id": "rc:4232684141",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4232684141",
   "hash": "f47ec6455930bbf1",
   "edited": "2026-10-09T17:16:40Z"
  },
  {
   "t": "2026-10-09T17:16:04Z",
   "kind": "review_comment",
   "who": "sipa",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "9451c93ff38435f2ce100af786b9221b0c565c5c",
   "in_reply_to": 4228134177,
   "text": "Done.",
   "id": "rc:4232694024",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4232694024",
   "hash": "ed251864987c367e",
   "edited": "2026-10-09T17:16:40Z"
  },
  {
   "t": "2026-10-09T17:16:17Z",
   "kind": "review_comment",
   "who": "sipa",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "9451c93ff38435f2ce100af786b9221b0c565c5c",
   "in_reply_to": 4227225807,
   "text": "Done. Cool, I didn't know about `std::midpoint`.",
   "id": "rc:4232695689",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4232695689",
   "hash": "2563ca2722b29962",
   "edited": "2026-10-09T17:16:40Z"
  },
  {
   "t": "2026-10-09T17:16:27Z",
   "kind": "review_comment",
   "who": "sipa",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "9451c93ff38435f2ce100af786b9221b0c565c5c",
   "in_reply_to": 4228172014,
   "text": "Done.",
   "id": "rc:4232697215",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4232697215",
   "hash": "ed251864987c367e",
   "edited": "2026-10-09T17:16:40Z"
  },
  {
   "t": "2026-10-09T17:16:33Z",
   "kind": "review_comment",
   "who": "sipa",
   "assoc": "MEMBER",
   "path": "src/headerssync.h",
   "commit": "9451c93ff38435f2ce100af786b9221b0c565c5c",
   "in_reply_to": 4227208074,
   "text": "Done.",
   "id": "rc:4232698022",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4232698022",
   "hash": "ed251864987c367e",
   "edited": "2026-10-09T17:16:40Z"
  },
  {
   "t": "2026-10-10T01:49:05Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "02d1084eba30de70c45eeaee77312fd417f79bd8",
   "in_reply_to": 4216211760,
   "text": "Thanks, please resolve",
   "id": "rc:4235898637",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4235898637",
   "hash": "f7ae2253e9324778",
   "edited": "2026-10-10T14:18:46Z"
  },
  {
   "t": "2026-10-10T13:42:27Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/test/fuzz/headerssync.cpp",
   "commit": "02d1084eba30de70c45eeaee77312fd417f79bd8",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nNit: A few commit message and PR details my clankers found, just resolve if irrelevant:\n\n- The first commit message still describes finding the largest period below the optimum. It could describe the two candidates within the legal range instead, including when the optimum lies outside it.\n- The fuzz commit says \"any representable timespan\" and \"the entire range of legal inputs\", but the targets use bounded ranges. \"A broad range of inputs\" and \"the portable timespan range\" seem more accurate.\n- In the PR description, \"batches of up to 2000\" would leave room for the shorter final batch mentioned in the next sentence.\n- The fuzz comment says \"roughly twice\", but these bounds are closer to three times the current values. \"Above\" would avoid pinning that explanation to a particular ratio.",
   "id": "rc:4237807559",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4237807559",
   "hash": "3f3fdedb3107c928",
   "edited": "2026-10-10T14:18:46Z"
  },
  {
   "t": "2026-10-10T13:43:01Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/net_processing.cpp",
   "commit": "0316d0dc9ec6a1abbe84489c44a47a2dd94c79ee",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nSimilarly to #35208, could an incorrect startup clock leave us with an inflated memory allowance after the clock is corrected? Fresh nodes seem to skip the clock check against the stored chain tip for some reason: https://github.com/bitcoin/bitcoin/blob/02d1084eba30de70c45eeaee77312fd417f79bd8/src/node/chainstate.cpp#L249-L255\n\nAccording to my clanker, starting with a clock near the Unix epoch and correcting it before headers sync could leave the per-peer commitment allowance about 50x higher than intended - could we defer the estimate after the clock checks?",
   "id": "rc:4237809017",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4237809017",
   "hash": "8bbfe82c62e93be9",
   "edited": "2026-10-10T14:18:46Z"
  },
  {
   "t": "2026-10-10T14:18:46Z",
   "kind": "review",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "state": "APPROVED",
   "commit": "02d1084eba30de70c45eeaee77312fd417f79bd8",
   "text": "Light ACK 02d1084eba30de70c45eeaee77312fd417f79bd8\n\nI'd appreciate another pair of eyes on the math.\nThe clock correction case still seems worth addressing here or in a follow-up, but isn't blocking for me, it's not systemic.",
   "id": "r:5476952469",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#pullrequestreview-5476952469",
   "hash": "36e000423a89d478",
   "edited": null
  },
  {
   "t": "2026-10-13T11:23:52Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "02d1084eba30de70c45eeaee77312fd417f79bd8",
   "in_reply_to": null,
   "text": "This first section is somewhat duplicating the header:\nhttps://github.com/bitcoin/bitcoin/blob/4bacf21a13c2ed25ef9362ca38f26bf0a67d22c9/src/headerssync.h#L59-L100\n\nCould we refer to that instead, boil this down further, or merge them?",
   "id": "rc:4257997365",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4257997365",
   "hash": "3e28dd3a230736e1",
   "edited": "2026-10-13T12:18:14Z"
  },
  {
   "t": "2026-10-13T11:41:15Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "path": "src/test/fuzz/headerssync.cpp",
   "commit": "02d1084eba30de70c45eeaee77312fd417f79bd8",
   "in_reply_to": 4237807559,
   "text": "Would change it to:\n```suggestion\n    // Well over double what the optimizer computes for mainnet at the time of writing, hardcoded so that the covered\n```",
   "id": "rc:4258154382",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4258154382",
   "hash": "0d2ed547346821b1",
   "edited": "2026-10-13T12:18:14Z"
  },
  {
   "t": "2026-10-13T11:57:31Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "path": "src/test/headerssync_tests.cpp",
   "commit": "02d1084eba30de70c45eeaee77312fd417f79bd8",
   "in_reply_to": null,
   "text": "While the comment is quite descriptive, I feel that it is sufficiently advanced that including the code for generating and filtering would be helpful for the next human needing update the record values. Maybe you consider the comment sufficient for LLMs and therefore not worth including dead code which harms code coverage stats?",
   "id": "rc:4258306782",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4258306782",
   "hash": "add99e7f973e663e",
   "edited": "2026-10-13T12:18:14Z"
  },
  {
   "t": "2026-10-13T12:04:51Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "02d1084eba30de70c45eeaee77312fd417f79bd8",
   "in_reply_to": null,
   "text": "Feels clearer explicitly capture here and for `make_config`:\n```suggestion\n    const auto int_bufsize = [real_bufsize](int64_t period) noexcept {\n```",
   "id": "rc:4258377027",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4258377027",
   "hash": "16c302f77cb6e343",
   "edited": "2026-10-13T12:18:14Z"
  },
  {
   "t": "2026-10-13T12:18:14Z",
   "kind": "review",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "02d1084eba30de70c45eeaee77312fd417f79bd8",
   "text": "Human linter comments for 02d1084eba30de70c45eeaee77312fd417f79bd8\n\nWill see if I find time to dig into the math.\n\nConcept ACK!",
   "id": "r:5379510928",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#pullrequestreview-5379510928",
   "hash": "bf6e2ed5af52305a",
   "edited": null
  },
  {
   "t": "2026-10-20T09:39:08Z",
   "kind": "review_comment",
   "who": "sipa",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "02d1084eba30de70c45eeaee77312fd417f79bd8",
   "in_reply_to": 4155828163,
   "text": "Historically, `CBlockHeader` was not a separate type, and headers were relayed as `CBlock`-with-0-transactions. That's where the value 81 comes from, and it is exact: there is just a trailing 0x00 byte at the end for the number of transactions.\n\nThe overhead from the P2P transport (length/magic/command/checksum in V1, length/header/auth in V2) doesn't apply here because those are paid only once (and a single `headers` message can carry 2000 of them).",
   "id": "rc:4323567371",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4323567371",
   "hash": "36df0ce67dd3bbd0",
   "edited": "2026-10-20T09:39:09Z"
  },
  {
   "t": "2026-10-20T12:19:41Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "02d1084eba30de70c45eeaee77312fd417f79bd8",
   "in_reply_to": 4155828163,
   "text": "Ah, I see the tx count is ignored here at the end as you say:\n\nhttps://github.com/bitcoin/bitcoin/blob/4bacf21a13c2ed25ef9362ca38f26bf0a67d22c9/src/net_processing.cpp#L5072-L5083\n\nAnd none of the integers in `CBlockHeader` are serialized in compact form. So it is indeed exact.\n\nStill think it would be more helpful to have:\n```suggestion\n    // + 1 for the always-zero tx count\n    constexpr int64_t NET_HEADER_SIZE{(sizeof(CBlockHeader) + 1) * 8};\n```",
   "id": "rc:4324836013",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4324836013",
   "hash": "2ad4e52697ef5bec",
   "edited": "2026-10-20T12:19:42Z"
  },
  {
   "t": "2026-10-20T14:12:20Z",
   "kind": "force_push",
   "who": "sipa",
   "commit": "672e628d96bf1d1f2890ff666b4d16f37309b61e",
   "id": "push:672e628d96bf1d1f2890ff666b4d16f37309b61e"
  },
  {
   "t": "2026-10-20T14:14:18Z",
   "kind": "review_comment",
   "who": "sipa",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "02d1084eba30de70c45eeaee77312fd417f79bd8",
   "in_reply_to": 4155588139,
   "text": "Done.",
   "id": "rc:4325989887",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4325989887",
   "hash": "ed251864987c367e",
   "edited": null
  },
  {
   "t": "2026-10-20T14:14:28Z",
   "kind": "review_comment",
   "who": "sipa",
   "assoc": "MEMBER",
   "path": "src/headerssync.h",
   "commit": "02d1084eba30de70c45eeaee77312fd417f79bd8",
   "in_reply_to": 4155702490,
   "text": "Done.",
   "id": "rc:4325992656",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4325992656",
   "hash": "ed251864987c367e",
   "edited": "2026-10-20T14:14:30Z"
  },
  {
   "t": "2026-10-20T14:18:11Z",
   "kind": "review_comment",
   "who": "sipa",
   "assoc": "MEMBER",
   "path": "src/test/fuzz/headerssync.cpp",
   "commit": "02d1084eba30de70c45eeaee77312fd417f79bd8",
   "in_reply_to": 4237807559,
   "text": "Reworded.",
   "id": "rc:4326035134",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4326035134",
   "hash": "0ad62600fc25e084",
   "edited": null
  },
  {
   "t": "2026-10-20T14:19:09Z",
   "kind": "review_comment",
   "who": "sipa",
   "assoc": "MEMBER",
   "path": "src/net_processing.cpp",
   "commit": "0316d0dc9ec6a1abbe84489c44a47a2dd94c79ee",
   "in_reply_to": 4237809017,
   "text": "Nice catch. Changed to instead just compute the parameters at runtime per connection, now that it's super fast. The need to precompute it dated from when it was too slow to repeatedly recompute.",
   "id": "rc:4326045291",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4326045291",
   "hash": "a18aabcd12068d70",
   "edited": null
  },
  {
   "t": "2026-10-20T14:19:47Z",
   "kind": "review_comment",
   "who": "sipa",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "02d1084eba30de70c45eeaee77312fd417f79bd8",
   "in_reply_to": 4155828163,
   "text": "Explained in the comment now.",
   "id": "rc:4326052306",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4326052306",
   "hash": "93770660961d2de1",
   "edited": null
  },
  {
   "t": "2026-10-20T14:20:56Z",
   "kind": "review_comment",
   "who": "sipa",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "9f7bcfe874729d8b20c46dd61d9dab8a0e5ea3fa",
   "in_reply_to": 4158813431,
   "text": "I don't think that's very helpful.",
   "id": "rc:4326064863",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4326064863",
   "hash": "d7038843798c8387",
   "edited": null
  },
  {
   "t": "2026-10-20T14:22:21Z",
   "kind": "review_comment",
   "who": "sipa",
   "assoc": "MEMBER",
   "path": "src/test/headerssync_tests.cpp",
   "commit": "02d1084eba30de70c45eeaee77312fd417f79bd8",
   "in_reply_to": 4258306782,
   "text": "I've included the code in a comment, so it doesn't affect code coverage stats, but still allows re-running (or examining by an LLM for inspiration for future improvements).",
   "id": "rc:4326080748",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4326080748",
   "hash": "b8c65d2e761a1a64",
   "edited": null
  },
  {
   "t": "2026-10-20T14:22:35Z",
   "kind": "review_comment",
   "who": "sipa",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "02d1084eba30de70c45eeaee77312fd417f79bd8",
   "in_reply_to": 4258377027,
   "text": "I have made all captures explicit.",
   "id": "rc:4326083415",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4326083415",
   "hash": "98ef32dcfdd66610",
   "edited": "2026-10-20T14:22:36Z"
  },
  {
   "t": "2026-10-20T14:24:20Z",
   "kind": "review_comment",
   "who": "sipa",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "02d1084eba30de70c45eeaee77312fd417f79bd8",
   "in_reply_to": 4257997365,
   "text": "I've compacted the new text a bit.",
   "id": "rc:4326100538",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4326100538",
   "hash": "a334a2f5ffa8a607",
   "edited": "2026-10-20T14:24:21Z"
  },
  {
   "t": "2026-10-20T14:24:47Z",
   "kind": "review_comment",
   "who": "sipa",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "02d1084eba30de70c45eeaee77312fd417f79bd8",
   "in_reply_to": 4158424480,
   "text": "Done.",
   "id": "rc:4326105378",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4326105378",
   "hash": "ed251864987c367e",
   "edited": "2026-10-20T14:24:48Z"
  },
  {
   "t": "2026-10-20T21:05:49Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/test/headerssync_tests.cpp",
   "commit": "0316d0dc9ec6a1abbe84489c44a47a2dd94c79ee",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nCould we also validate these expected values against the attack-rate formula?\nThat would check the test records themselves, alongside checking that the optimizer reproduces them.\n\nCheck the buffer using the existing vectors\n\n```diff\ndiff --git a/src/test/headerssync_tests.cpp b/src/test/headerssync_tests.cpp\nindex d291c11371..363df619f9 100644\n--- a/src/test/headerssync_tests.cpp\n+++ b/src/test/headerssync_tests.cpp\n@@ -6,6 +6,7 @@\n\n #include <boost/test/unit_test.hpp>\n\n+#include <cmath>\n #include <cstdint>\n\n BOOST_AUTO_TEST_SUITE(headerssync_tests)\n@@ -96,7 +97,7 @@ BOOST_AUTO_TEST_SUITE(headerssync_tests)\n\n BOOST_AUTO_TEST_CASE(compute_headers_sync_params)\n {\n-    const struct {\n+    constexpr struct {\n         int64_t max_headers;\n         int64_t minchainwork_headers;\n         double attack_headers;\n@@ -154,10 +155,20 @@ BOOST_AUTO_TEST_CASE(compute_headers_sync_params)\n         {9516499040, 2241730, 5.612939355277791e-05, 1002, 24723},\n         {2762151481, 103252, 1.7805562892829343e-06, 501, 14355},\n     };\n-    for (const auto& cas : cases) {\n-        const auto [period, bufsize] = ComputeHeadersSyncParamsInner(cas.max_headers, cas.minchainwork_headers, cas.attack_headers);\n-        BOOST_CHECK_EQUAL(period, cas.expected_period);\n-        BOOST_CHECK_EQUAL(bufsize, cas.expected_bufsize);\n+    const auto expected_headers{[](int64_t period, int64_t bufsize) {\n+        double accepted{0};\n+        for (int64_t offset{0}; offset < period; ++offset) {\n+            const int64_t checks{(bufsize - offset + period - 1) / period}; // ceil((B-F)/P)\n+            accepted += std::ldexp(double(offset + period * checks - bufsize + period), -int(checks));\n+        }\n+        return accepted / period;\n+    }};\n+    for (const auto& [max_headers, minchainwork_headers, attack_headers, expected_period, expected_bufsize] : cases) {\n+        const auto [period, bufsize]{ComputeHeadersSyncParamsInner(max_headers, minchainwork_headers, attack_headers)};\n+        BOOST_CHECK_EQUAL(period, expected_period);\n+        BOOST_CHECK_EQUAL(bufsize, expected_bufsize);\n+        BOOST_CHECK_LT(expected_headers(expected_period, expected_bufsize), attack_headers);\n+        BOOST_CHECK_GE(expected_headers(expected_period, expected_bufsize - 1), attack_headers);\n     }\n }\n\n```",
   "id": "rc:4330312444",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4330312444",
   "hash": "c4720456e3a80e62",
   "edited": "2026-10-20T21:14:31Z"
  },
  {
   "t": "2026-10-20T21:06:12Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "doc/release-process.md",
   "commit": "0457cec22a273eba31e92950bd485bf72c864ab0",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nThe release-process note and PR description could say that the parameters are computed at runtime whenever a headers sync begins - the current startup wording describes the previous implementation.\n\nAlign the wording with the current computation point\n\n```diff\ndiff --git a/doc/release-process.md b/doc/release-process.md\nindex 29ef13845b..3cf70bddd2 100644\n--- a/doc/release-process.md\n+++ b/doc/release-process.md\n@@ -46,7 +46,7 @@ Release Process\n     - On mainnet, the selected value must not be orphaned, so it may be useful to set the height two blocks back from the tip.\n     - Testnet should be set with a height some tens of thousands back from the tip, due to reorgs there.\n   - `nMinimumChainWork` with the \"chainwork\" value of RPC `getblockheader` using the same height as that selected for the previous step.\n-  - `minchainwork_height` with that same height. It is the number of headers corresponding to `nMinimumChainWork`, and is used to size the headers synchronization DoS-protection parameters (which are computed at node startup).\n+  - `minchainwork_height` with that same height. It is the number of headers corresponding to `nMinimumChainWork`, and is used to size the headers synchronization DoS-protection parameters, which are computed at runtime when a headers sync begins.\n   - `m_assumeutxo_data` array should be appended to with the values returned by calling `bitcoin-cli -rpcclienttimeout=0 -named dumptxoutset utxo.dat rollback=<height or hash>`\n     The same height considerations for `defaultAssumeValid` apply.\n - Clear the release notes and move them to the wiki (see \"Write the release notes\" below).\n```",
   "id": "rc:4330315941",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4330315941",
   "hash": "5da77d8e1f093943",
   "edited": "2026-10-20T21:14:31Z"
  },
  {
   "t": "2026-10-20T21:06:31Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/test/headerssync_tests.cpp",
   "commit": "0316d0dc9ec6a1abbe84489c44a47a2dd94c79ee",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nI'd find this easier to rerun as a script in `contrib/testgen/`, with a reference here.\nThat would also let Python tooling check it and keep the test easier to read.\n\nMove the generator to contrib/testgen\n\n```diff\ndiff --git a/contrib/testgen/README.md b/contrib/testgen/README.md\nindex 2f0288df16..4190c6d89d 100644\n--- a/contrib/testgen/README.md\n+++ b/contrib/testgen/README.md\n@@ -6,3 +6,7 @@ To use inside a scripted-diff (or just execute directly):\n\n     ./gen_key_io_test_vectors.py valid 70 > ../../src/test/data/key_io_valid.json\n     ./gen_key_io_test_vectors.py invalid 70 > ../../src/test/data/key_io_invalid.json\n+\n+To regenerate the headers sync parameter records in `src/test/headerssync_tests.cpp`, run from the repository root:\n+\n+    pypy3 contrib/testgen/gen_headerssync_test_vectors.py\ndiff --git a/contrib/testgen/gen_headerssync_test_vectors.py b/contrib/testgen/gen_headerssync_test_vectors.py\nnew file mode 100644\nindex 0000000000..e963085307\n--- /dev/null\n+++ b/contrib/testgen/gen_headerssync_test_vectors.py\n@@ -0,0 +1,81 @@\n+#!/usr/bin/env python3\n+# Copyright (c) The Bitcoin Core developers\n+# Distributed under the MIT software license, see the accompanying\n+# file COPYING or http://www.opensource.org/licenses/mit-license.php.\n+\n+\"\"\"Generate headers sync parameter test vectors by exhaustively searching all periods.\"\"\"\n+\n+import math\n+import random\n+\n+# Both must match src/headerssync.cpp.\n+COMPACT_HEADER_SIZE = 48 * 8\n+LIMIT_FRACTION = (0.2 / 600.0) / (6000000000.0 / (81 * 8))\n+# Reject solutions which are only better by this much relatively speaking (floating point precision).\n+MARGIN = 1e-9\n+\n+def memory_usage(max_headers, mch, period, bufsize):\n+    return max(max_headers // period, mch // period + bufsize * COMPACT_HEADER_SIZE)\n+\n+def round_error(value):\n+    return abs(value - round(value)) / value\n+\n+def rate(period, bufsize):\n+    \"\"\"Expected number of forged headers accepted per attack: the closed form of headerssync.cpp.\"\"\"\n+    k, m = divmod(bufsize, period)\n+    return 2.0**-k * (6 * period**2 - 4 * period * m + m * m - 2 * period + m) / (4 * period)\n+\n+def optimum(max_headers, mch, attack_headers):\n+    \"\"\"Exhaustively search for the optimal (period, bufsize), and the runner-up it beats.\"\"\"\n+    best, runner_up, bufsize = None, None, 1\n+    for period in range(1, mch + 1):\n+        while rate(period, bufsize) >= attack_headers:  # bufsize never decreases with period\n+            bufsize += 1\n+        candidate = (memory_usage(max_headers, mch, period, bufsize), period, bufsize)\n+        if best is None or candidate < best:\n+            best, runner_up = candidate, best\n+        elif runner_up is None or candidate < runner_up:\n+            runner_up = candidate\n+    return best, runner_up\n+\n+def sample():\n+    \"\"\"One record, or None if its inputs are illegal or its optimum is too ambiguous.\"\"\"\n+    # Start with random initial values:\n+    # - A timespan, log-normally distributed with mode 12 years and mean 18 years.\n+    sigma = math.sqrt(math.log(18.0 / 12.0) / 1.5)\n+    timespan = random.lognormvariate(math.log(12.0 * 365.25 * 86400) + sigma**2, sigma)\n+    # - minchainwork_headers, exponentially distributed with mean 1000000.\n+    mch = random.expovariate(1.0 / 1000000.0)\n+    # - attack_headers, log-normally distributed with median LIMIT_FRACTION * mch, sigma 1.5.\n+    attack_headers = random.lognormvariate(math.log(LIMIT_FRACTION * mch), 1.5)\n+    # Convert to a starting point in (max_headers, minchainwork_headers, attack_headers) space.\n+    start = [6.0 * timespan, mch, attack_headers]\n+    # Pick a random point on the 3D sphere with radius 1/200 as (logarithm of) walk step factor to explore.\n+    direction = [random.gauss(0.0, 1.0) for _ in range(3)]\n+    step = [d * 0.005 / math.hypot(*direction) for d in direction]\n+    # Pick 100 points, starting from start, each a further factor exp(step) away from the previous one.\n+    walk = ([c * math.exp(d * i) for c, d in zip(start, step)] for i in range(100))\n+    # Among those 100 points, pick the one whose max_headers and mch are closest to integers.\n+    max_headers, mch, attack_headers = min(walk, key=lambda pt: max(round_error(pt[0]), round_error(pt[1])))\n+    max_headers, mch = round(max_headers), round(mch)\n+    # Reject if outside the inputs the optimizer is defined for.\n+    if mch < 1 or max_headers <= 2 * mch:\n+        return None\n+    best, runner_up = optimum(max_headers, mch, attack_headers)\n+    _, period, bufsize = best\n+    # Reject if too close to the runner up, to avoid depending on floating point rounding behavior.\n+    if not runner_up or runner_up[0] <= best[0] * (1.0 + MARGIN):\n+        return None  # another period is just as good\n+    if not (rate(period, bufsize) < attack_headers * (1.0 - MARGIN)\n+            and rate(period, bufsize - 1) > attack_headers * (1.0 + MARGIN)):\n+        return None  # another bufsize is just as good\n+    return max_headers, mch, attack_headers, period, bufsize\n+\n+random.seed(0)\n+count = 0\n+while count < 50:\n+    record = sample()\n+    if not record:\n+        continue\n+    print(\"        {%d, %d, %r, %d, %d},\" % record)\n+    count += 1\ndiff --git a/src/test/headerssync_tests.cpp b/src/test/headerssync_tests.cpp\nindex 363df619f9..4c4e7b2b64 100644\n--- a/src/test/headerssync_tests.cpp\n+++ b/src/test/headerssync_tests.cpp\n@@ -16,84 +16,9 @@ BOOST_AUTO_TEST_SUITE(headerssync_tests)\n // - inputs: max_headers, minchainwork_headers, attack_headers\n // - expected outputs: period, bufsize\n //\n-// The records below were generated by the following script, which finds the exact optimum for each\n-// input through an exhaustive search over all periods, independent of the search strategy the\n-// implementation uses. Run under PyPy for faster results.\n-//\n-// import math\n-// import random\n-//\n-// # Both must match src/headerssync.cpp.\n-// COMPACT_HEADER_SIZE = 48 * 8\n-// LIMIT_FRACTION = (0.2 / 600.0) / (6000000000.0 / (81 * 8))\n-// # Reject solutions which are only better by this much relatively speaking (floating point precision).\n-// MARGIN = 1e-9\n-//\n-// def memory_usage(max_headers, mch, period, bufsize):\n-//     return max(max_headers // period, mch // period + bufsize * COMPACT_HEADER_SIZE)\n-//\n-// def round_error(value):\n-//     return abs(value - round(value)) / value\n-//\n-// def rate(period, bufsize):\n-//     \"\"\"Expected number of forged headers accepted per attack: the closed form of headerssync.cpp.\"\"\"\n-//     k, m = divmod(bufsize, period)\n-//     return 2.0**-k * (6 * period**2 - 4 * period * m + m * m - 2 * period + m) / (4 * period)\n-//\n-// def optimum(max_headers, mch, attack_headers):\n-//     \"\"\"Exhaustively search for the optimal (period, bufsize), and the runner-up it beats.\"\"\"\n-//     best, runner_up, bufsize = None, None, 1\n-//     for period in range(1, mch + 1):\n-//         while rate(period, bufsize) >= attack_headers:  # bufsize never decreases with period\n-//             bufsize += 1\n-//         candidate = (memory_usage(max_headers, mch, period, bufsize), period, bufsize)\n-//         if best is None or candidate < best:\n-//             best, runner_up = candidate, best\n-//         elif runner_up is None or candidate < runner_up:\n-//             runner_up = candidate\n-//     return best, runner_up\n-//\n-// def sample():\n-//     \"\"\"One record, or None if its inputs are illegal or its optimum is too ambiguous.\"\"\"\n-//     # Start with random initial values:\n-//     # - A timespan, log-normally distributed with mode 12 years and mean 18 years.\n-//     sigma = math.sqrt(math.log(18.0 / 12.0) / 1.5)\n-//     timespan = random.lognormvariate(math.log(12.0 * 365.25 * 86400) + sigma**2, sigma)\n-//     # - minchainwork_headers, exponentially distributed with mean 1000000.\n-//     mch = random.expovariate(1.0 / 1000000.0)\n-//     # - attack_headers, log-normally distributed with median LIMIT_FRACTION * mch, sigma 1.5.\n-//     attack_headers = random.lognormvariate(math.log(LIMIT_FRACTION * mch), 1.5)\n-//     # Convert to a starting point in (max_headers, minchainwork_headers, attack_headers) space.\n-//     start = [6.0 * timespan, mch, attack_headers]\n-//     # Pick a random point on the 3D sphere with radius 1/200 as (logarithm of) walk step factor to explore.\n-//     direction = [random.gauss(0.0, 1.0) for _ in range(3)]\n-//     step = [d * 0.005 / math.hypot(*direction) for d in direction]\n-//     # Pick 100 points, starting from start, each a further factor exp(step) away from the previous one.\n-//     walk = ([c * math.exp(d * i) for c, d in zip(start, step)] for i in range(100))\n-//     # Among those 100 points, pick the one whose max_headers and mch are closest to integers.\n-//     max_headers, mch, attack_headers = min(walk, key=lambda pt: max(round_error(pt[0]), round_error(pt[1])))\n-//     max_headers, mch = round(max_headers), round(mch)\n-//     # Reject if outside the inputs the optimizer is defined for.\n-//     if mch < 1 or max_headers <= 2 * mch:\n-//         return None\n-//     best, runner_up = optimum(max_headers, mch, attack_headers)\n-//     _, period, bufsize = best\n-//     # Reject if too close to the runner up, to avoid depending on floating point rounding behavior.\n-//     if not runner_up or runner_up[0] <= best[0] * (1.0 + MARGIN):\n-//         return None  # another period is just as good\n-//     if not (rate(period, bufsize) < attack_headers * (1.0 - MARGIN)\n-//             and rate(period, bufsize - 1) > attack_headers * (1.0 + MARGIN)):\n-//         return None  # another bufsize is just as good\n-//     return max_headers, mch, attack_headers, period, bufsize\n-//\n-// random.seed(0)\n-// count = 0\n-// while count < 50:\n-//     record = sample()\n-//     if not record:\n-//         continue\n-//     print(\"        {%d, %d, %r, %d, %d},\" % record)\n-//     count += 1\n+// The records below were generated by contrib/testgen/gen_headerssync_test_vectors.py\n+// The generator finds the exact optimum through an exhaustive search over all periods, independently of the implementation's search strategy\n+// Run from the repository root: pypy3 contrib/testgen/gen_headerssync_test_vectors.py\n\n BOOST_AUTO_TEST_CASE(compute_headers_sync_params)\n {\n```",
   "id": "rc:4330318796",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4330318796",
   "hash": "90294e4c95696911",
   "edited": "2026-10-20T21:14:31Z"
  },
  {
   "t": "2026-10-20T21:14:31Z",
   "kind": "review",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "state": "APPROVED",
   "commit": "672e628d96bf1d1f2890ff666b4d16f37309b61e",
   "text": "lightly tested ACK 672e628d96bf1d1f2890ff666b4d16f37309b61e\n\nSince my last review, the parameters are computed whenever a headers sync starts (addressing my clock-correction concern), and the test-vector generation script is now included.\nI like the more compact code comments, and I'd still appreciate another pair of eyes to double-check the math.\n\nThe fixed test vectors could also be checked against the actual attack-rate formula, and the Python generator could move to `contrib/testgen/`, but neither suggestion is blocking for me.",
   "id": "r:5591041144",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#pullrequestreview-5591041144",
   "hash": "c5000172d9b27b99",
   "edited": null
  },
  {
   "t": "2026-10-21T18:17:26Z",
   "kind": "force_push",
   "who": "sipa",
   "commit": "0457cec22a273eba31e92950bd485bf72c864ab0",
   "id": "push:0457cec22a273eba31e92950bd485bf72c864ab0"
  },
  {
   "t": "2026-10-21T18:17:53Z",
   "kind": "review_comment",
   "who": "sipa",
   "assoc": "MEMBER",
   "path": "src/test/headerssync_tests.cpp",
   "commit": "0316d0dc9ec6a1abbe84489c44a47a2dd94c79ee",
   "in_reply_to": 4330312444,
   "text": "Done, and went further by implementing more checks to verify optimality of the test vectors.",
   "id": "rc:4340360561",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4340360561",
   "hash": "be76caebb7987053",
   "edited": "2026-10-21T18:17:54Z"
  },
  {
   "t": "2026-10-21T18:20:15Z",
   "kind": "review_comment",
   "who": "sipa",
   "assoc": "MEMBER",
   "path": "src/test/headerssync_tests.cpp",
   "commit": "0316d0dc9ec6a1abbe84489c44a47a2dd94c79ee",
   "in_reply_to": 4330318796,
   "text": "I'm not taking this.\n\nThe test generator script isn't particularly important, it's just selecting some cases to try perhaps slightly better than purely randomly. There is no requirement that the test vectors match its output, or even that it's reproducible. Burdening the project with maintaining this code is overkill.\n\nIf you think it's undesirable to have this code in the comment, I'd rather delete it entirely and replace it with a single sentence explaining what considerations were made when generating the vectors.",
   "id": "rc:4340382471",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4340382471",
   "hash": "b2e15ec486cbb524",
   "edited": null
  },
  {
   "t": "2026-10-21T19:05:14Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/test/headerssync_tests.cpp",
   "commit": "0316d0dc9ec6a1abbe84489c44a47a2dd94c79ee",
   "in_reply_to": 4330318796,
   "text": "Thanks for considering, please resolve the suggestion.",
   "id": "rc:4340820050",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4340820050",
   "hash": "a0d8c3ac9db44bd6",
   "edited": "2026-10-21T19:25:01Z"
  },
  {
   "t": "2026-10-21T19:25:01Z",
   "kind": "review",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "state": "APPROVED",
   "commit": "0457cec22a273eba31e92950bd485bf72c864ab0",
   "text": "Lightly tested diff re-ACK 0457cec22a273eba31e92950bd485bf72c864ab0",
   "id": "r:5604810367",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#pullrequestreview-5604810367",
   "hash": "8cef413a49cc6b53",
   "edited": null
  },
  {
   "t": "2026-10-21T19:30:50Z",
   "kind": "force_push",
   "who": "sipa",
   "commit": "0316d0dc9ec6a1abbe84489c44a47a2dd94c79ee",
   "id": "push:0316d0dc9ec6a1abbe84489c44a47a2dd94c79ee"
  },
  {
   "t": "2026-10-21T19:31:23Z",
   "kind": "review_comment",
   "who": "sipa",
   "assoc": "MEMBER",
   "path": "doc/release-process.md",
   "commit": "0457cec22a273eba31e92950bd485bf72c864ab0",
   "in_reply_to": 4330315941,
   "text": "Done.",
   "id": "rc:4341081031",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4341081031",
   "hash": "ed251864987c367e",
   "edited": "2026-10-21T19:31:24Z"
  },
  {
   "t": "2026-10-21T19:32:12Z",
   "kind": "comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "text": "reACK 0316d0dc9ec6a1abbe84489c44a47a2dd94c79ee",
   "id": "c:6324747295",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#issuecomment-6324747295",
   "hash": "d59a2dcc8c0b8c18",
   "edited": null
  },
  {
   "t": "2026-10-21T19:41:44Z",
   "kind": "review_comment",
   "who": "sipa",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "9f7bcfe874729d8b20c46dd61d9dab8a0e5ea3fa",
   "in_reply_to": 4158555646,
   "text": "Fair point, but not going to touch this everywhere now.",
   "id": "rc:4341179298",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4341179298",
   "hash": "d32fcf8a3bf7c42f",
   "edited": "2026-10-21T19:41:45Z"
  },
  {
   "t": "2026-10-23T11:56:16Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "0316d0dc9ec6a1abbe84489c44a47a2dd94c79ee",
   "in_reply_to": null,
   "text": "nit: Decreasing the difficulty allows even faster hashing?\n```suggestion\n//      commitment bits. Such a chain may be very long, by exploiting the timewarp bug to minimize\n//      the difficulty. There is however an absolute limit on how long such a chain can be: 6\n```",
   "id": "rc:4359242249",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4359242249",
   "hash": "b688e6722c0e03d2",
   "edited": "2026-10-24T09:38:57Z"
  },
  {
   "t": "2026-10-23T12:09:20Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "0316d0dc9ec6a1abbe84489c44a47a2dd94c79ee",
   "in_reply_to": null,
   "text": "nanonit: Could keep to the name in `Consensus::Params`:\n\n```suggestion\n// the chain's age and minimum-chain-work height, then optimize.\nHeadersSyncParams ComputeHeadersSyncParams(std::chrono::seconds timespan, int64_t minchainwork_height)\n```",
   "id": "rc:4359348103",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4359348103",
   "hash": "d33b55d8462c5ed9",
   "edited": "2026-10-24T09:38:57Z"
  },
  {
   "t": "2026-10-23T12:26:32Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "0316d0dc9ec6a1abbe84489c44a47a2dd94c79ee",
   "in_reply_to": null,
   "text": "nit: How about swapping the function definition order for `ComputeHeadersSyncParams()` & `ComputeHeadersSyncParamsInner()` to match the order in the header?\nIn order to attempt understanding the `attack_headers` argument coming in it's helpful to at least have glimpsed at the body of `ComputeHeadersSyncParams()`.",
   "id": "rc:4359486611",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4359486611",
   "hash": "ec13fcd30be10a6b",
   "edited": "2026-10-24T09:38:57Z"
  },
  {
   "t": "2026-10-23T12:27:33Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "0316d0dc9ec6a1abbe84489c44a47a2dd94c79ee",
   "in_reply_to": null,
   "text": "nanonit: Helps human eyes:\n```suggestion\n    constexpr double ATTACK_BANDWIDTH{6'000'000'000.0};\n```",
   "id": "rc:4359495466",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4359495466",
   "hash": "7705d3daf1fdc463",
   "edited": "2026-10-24T09:38:57Z"
  },
  {
   "t": "2026-10-23T12:39:18Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "0316d0dc9ec6a1abbe84489c44a47a2dd94c79ee",
   "in_reply_to": null,
   "text": "Why jointly, isn't it more correct that multiple headers sync peers could be sending you these low-difficulty headers in parallel?\n```suggestion\n    /** How much additional permanent memory usage is an attacking peer allowed to cause in the\n     *  victim, expressed as fraction of the normal memory usage due to mainchain growth, for the\n     *  duration the attack is sustained. [unitless]. 0.2 means that attackers, while they keep up the\n     *  attack, can cause permanent memory usage due to headers storage to grow at 1.2 header per\n     *  BLOCK_INTERVAL. */\n    constexpr double ATTACK_FRACTION{0.2};\n    /** How many bits a header uses in the P2P protocol: a block header, plus the trailing zero\n     *  transaction count headers are relayed with. [bits] */\n    constexpr int64_t NET_HEADER_SIZE{(sizeof(CBlockHeader) + 1) * 8};\n    /** What rate of headers worth of RAM attackers are allowed to cause in the victim. [headers/s] */\n    constexpr double LIMIT_HEADERRATE{ATTACK_FRACTION / BLOCK_INTERVAL};\n    /** How many headers can an attacking peer send a victim per second. [headers/s] */\n```",
   "id": "rc:4359592481",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4359592481",
   "hash": "79c54aefcdce383b",
   "edited": "2026-10-24T09:38:57Z"
  },
  {
   "t": "2026-10-23T12:57:01Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "0316d0dc9ec6a1abbe84489c44a47a2dd94c79ee",
   "in_reply_to": null,
   "text": "Would expect `attack_headers => 1`, but `LIMIT_FRACTION` becomes `3.6000000000000005E-11`. For a `mainchainwork_headers` value of `966143` that makes the `attack_headers` parameter equal `0.00003478...`.\nGoing by the above comment, the number of headers sent by attackers that can at most be accepted by a victim is... `0.00003478...`?! Does that mean zero attack headers are accepted? Or do we round it up to only 1 attack header?\nWould it be possible to better explain this in the comment?",
   "id": "rc:4359745705",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4359745705",
   "hash": "df015154ab1b0e44",
   "edited": "2026-10-24T09:38:57Z"
  },
  {
   "t": "2026-10-24T09:42:15Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "0316d0dc9ec6a1abbe84489c44a47a2dd94c79ee",
   "in_reply_to": 4359592481,
   "text": "(To affect RAM usage the attacker(s) would need unique header chains for each peer but the difficulty could be timewarped to a minimum before forking).",
   "id": "rc:4366741311",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4366741311",
   "hash": "db553e2cf3f2adf5",
   "edited": "2026-10-24T09:42:16Z"
  },
  {
   "t": "2026-10-24T12:04:52Z",
   "kind": "review_comment",
   "who": "sipa",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "0316d0dc9ec6a1abbe84489c44a47a2dd94c79ee",
   "in_reply_to": 4359242249,
   "text": "If they fork off after genesis, indeed, but in this attack scenario (just wasting the victim's per-peer memory using the commitment bit data structure) I believe it is always better to start at genesis. Will address if I retouch.",
   "id": "rc:4367060848",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4367060848",
   "hash": "4c9263530929ae05",
   "edited": "2026-10-24T12:04:53Z"
  },
  {
   "t": "2026-10-24T12:05:21Z",
   "kind": "review_comment",
   "who": "sipa",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "0316d0dc9ec6a1abbe84489c44a47a2dd94c79ee",
   "in_reply_to": 4359348103,
   "text": "Will address if I retouch.",
   "id": "rc:4367061849",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4367061849",
   "hash": "fbbc1f41d4a0c215",
   "edited": "2026-10-24T12:05:22Z"
  },
  {
   "t": "2026-10-24T12:05:32Z",
   "kind": "review_comment",
   "who": "sipa",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "0316d0dc9ec6a1abbe84489c44a47a2dd94c79ee",
   "in_reply_to": 4359486611,
   "text": "Will address if I retouch.",
   "id": "rc:4367062179",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4367062179",
   "hash": "fbbc1f41d4a0c215",
   "edited": "2026-10-24T12:05:33Z"
  },
  {
   "t": "2026-10-24T12:05:43Z",
   "kind": "review_comment",
   "who": "sipa",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "0316d0dc9ec6a1abbe84489c44a47a2dd94c79ee",
   "in_reply_to": 4359495466,
   "text": "Will address if I retouch.",
   "id": "rc:4367062492",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4367062492",
   "hash": "fbbc1f41d4a0c215",
   "edited": "2026-10-24T12:05:44Z"
  },
  {
   "t": "2026-10-24T12:17:34Z",
   "kind": "review_comment",
   "who": "sipa",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "0316d0dc9ec6a1abbe84489c44a47a2dd94c79ee",
   "in_reply_to": 4359592481,
   "text": "Yes, there may be multiple attackers in parallel. But their joint bandwidth to the victim is what matters, not their individual bandwidth.\n\nThe attack here consists of the attacker(s) sending the honest main chain during the commitment phase, and then giving a forged low-difficulty headers chain during the redownload phase. On average, for every attempt at this some tiny amount of these headers will be accepted into the blockchain index. If an someone could perform this attack an infinite number of times, our memory growth would be infinite too.\n\nWhat saves us is that the attack costs download volume; each attack requires sending the entirety of the main chain's headers during a commitment phase. And we are okay with accepting a tiny amount of forged headers into our index, as long as that growth is sufficiently slow. After all, we're already okay with our blockchain index growing at a rate of ~1 block per 10 minutes. If all an attacker can do is increase that slightly (here, to 1.2 blocks per 10 minutes), then that's fine.\n\nSo the reasoning is: we accept that attacks are possible, and will be successful occasionally, but on average they cannot get more than a certain number of headers accepted per mainchain they send us. Since the attacker has a finite amount of bandwidth to us, that translates to a small finite amount of headers accepted per unit of time. As long as that is no more than 0.2 headers per 10 minutes, we are okay.\n\nBut what happens when there are multiple attackers, all performing the same attack on us? If each individual attacker was limited to 6 Gbit/s and our parameters are chosen so that a single 6 Gbit/s attacker could get 0.2 headers per 10 minutes accepted, then 1000 attackers doing the same thing in parallel could get 200 headers per 10 minutes accepted which would be totally unacceptable. However, this is not a concern: it's not each individual attacker that is limited to 6 Gbit/s, but all attackers combined. So, the 6 Gbit/s number must represent the joint bandwidth all attackers together have to us, which is indeed limited (our own network connection is a bound, as is the rate at which we can compute hashes).",
   "id": "rc:4367090817",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4367090817",
   "hash": "117f399c359d894a",
   "edited": "2026-10-24T17:13:40Z"
  },
  {
   "t": "2026-10-24T12:48:25Z",
   "kind": "review_comment",
   "who": "sipa",
   "assoc": "MEMBER",
   "path": "src/headerssync.cpp",
   "commit": "0316d0dc9ec6a1abbe84489c44a47a2dd94c79ee",
   "in_reply_to": 4359745705,
   "text": "(disregard my now-deleted earlier comment, I missed you're already multiplying with `minchainwork_headers`)\n\nThe number is correct, in expectation. We permit 0.00003478 headers to be accepted per attack. That is, an attacker that performs the attack 1000000000 times, will on average get no more than 3478 headers accepted.",
   "id": "rc:4367161562",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#discussion_r4367161562",
   "hash": "dd581411063a6996",
   "edited": "2026-10-24T12:48:40Z"
  },
  {
   "t": "2026-10-25T06:12:42Z",
   "kind": "comment",
   "who": "fametrano",
   "assoc": "NONE",
   "text": "tACK 0316d0dc9ec6a1abbe84489c44a47a2dd94c79ee\n\nCompiled from this branch, the two new functions return the exact optimum of their model for all five chains, every six hours from 2026-10-25 to 2041. The same holds on a grid of heights from 1 to 5 million and clocks, some before genesis. The chosen bufsize always meets the attack budget. Summing the attack model term by term, in exact arithmetic, gives the closed form for every period up to 16 and bufsize up to 60. I checked this with a Python port and a brute-force search; I can share the scripts as a gist. The new unit tests and p2p_headers_sync_with_minchainwork.py pass, and the parameters its nodes log match the port at each node's clock.\n\nAt 2026-10-25 00:00 UTC the parameters, as period/bufsize, are mainnet 597/14697, testnet3 590/13096, testnet4 217/5604, signet 345/8770 and regtest 535/14517. Mainnet needs 689 KiB per peer against 722 KiB for the hard-coded 649/15400. On 2029-04-10, the date that pair was sized for, mainnet needs 14.7 KiB more than the pair, as you estimated. Testnet4 and signet fall much further, from 616/16199 and 629/15885, because each chain now uses its own genesis time, like HeadersSyncState's commitment bound when syncing from genesis. Rerunning the old script with 2009-01-03, mainnet's genesis date, reproduces all four hard-coded pairs.",
   "id": "c:6394267241",
   "url": "https://github.com/bitcoin/bitcoin/pull/35642#issuecomment-6394267241",
   "hash": "3b8839fc26164613",
   "edited": null
  }
 ],
 "labels_log": [
  {
   "t": "2026-07-02T15:08:28Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-07-09T23:01:23Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-07-10T03:15:58Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-07-10T04:20:21Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-07-25T12:10:06Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-06T18:26:00Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-06T20:31:28Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-07T07:05:08Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-31T16:53:38Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-15T19:18:06Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-15T20:05:07Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-16T20:25:54Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-24T01:14:45Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-29T21:32:07Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-29T22:29:50Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-30T09:45:03Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  }
 ],
 "state_log": [
  {
   "t": "2026-09-14T21:29:26Z",
   "kind": "convert_to_draft",
   "who": "sipa"
  },
  {
   "t": "2026-09-15T18:14:16Z",
   "kind": "ready_for_review",
   "who": "sipa"
  }
 ],
 "text_chars": 80609,
 "text_tokens_estimate": 20152,
 "changed_paths": [
  "contrib/devtools/README.md",
  "contrib/devtools/headerssync-params.py",
  "doc/release-process.md",
  "src/consensus/params.h",
  "src/headerssync.cpp",
  "src/headerssync.h",
  "src/kernel/chainparams.cpp",
  "src/kernel/chainparams.h",
  "src/net_processing.cpp",
  "src/net_processing.h",
  "src/test/CMakeLists.txt",
  "src/test/fuzz/headerssync.cpp",
  "src/test/headerssync_tests.cpp"
 ],
 "files": [
  {
   "path": "contrib/devtools/README.md",
   "add": 0,
   "del": 10
  },
  {
   "path": "contrib/devtools/headerssync-params.py",
   "add": 0,
   "del": 357
  },
  {
   "path": "doc/release-process.md",
   "add": 1,
   "del": 8
  },
  {
   "path": "src/consensus/params.h",
   "add": 4,
   "del": 0
  },
  {
   "path": "src/headerssync.cpp",
   "add": 278,
   "del": 4
  },
  {
   "path": "src/headerssync.h",
   "add": 32,
   "del": 0
  },
  {
   "path": "src/kernel/chainparams.cpp",
   "add": 11,
   "del": 30
  },
  {
   "path": "src/kernel/chainparams.h",
   "add": 0,
   "del": 12
  },
  {
   "path": "src/net_processing.cpp",
   "add": 19,
   "del": 1
  },
  {
   "path": "src/net_processing.h",
   "add": 1,
   "del": 0
  },
  {
   "path": "src/test/CMakeLists.txt",
   "add": 1,
   "del": 0
  },
  {
   "path": "src/test/fuzz/headerssync.cpp",
   "add": 63,
   "del": 2
  },
  {
   "path": "src/test/headerssync_tests.cpp",
   "add": 246,
   "del": 0
  }
 ],
 "test_lines": 679,
 "git": {
  "head": "0316d0dc9ec6a1abbe84489c44a47a2dd94c79ee",
  "head_matches_backup": true,
  "base": "f86c614e69256b9814c27b0dd096ee46daa71aa3",
  "patch_id": "759750a38d7f8671",
  "commits": [
   {
    "sha": "3ddc510850",
    "subject": "headerssync: add computation of optimal sync parameters",
    "files": 4,
    "add": 468,
    "del": 4
   },
   {
    "sha": "7281e51459",
    "subject": "fuzz: add headers sync parameter computation targets",
    "files": 1,
    "add": 59,
    "del": 0
   },
   {
    "sha": "990b3fa6d0",
    "subject": "net_processing: compute headers sync parameters at runtime",
    "files": 9,
    "add": 55,
    "del": 61
   },
   {
    "sha": "4c04debcd7",
    "subject": "contrib: remove headerssync-params.py",
    "files": 2,
    "add": 0,
    "del": 367
   },
   {
    "sha": "0316d0dc9e",
    "subject": "test: verify the sync parameters by brute force",
    "files": 1,
    "add": 84,
    "del": 2
   }
  ],
  "patch_truncated": true
 },
 "patch_id": "759750a38d7f8671",
 "input_hash": "8441b5ae9a67ed9b",
 "extracted_at": "2026-11-02T01:09:49+00:00"
}