{
 "number": 35839,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/35839",
 "title": "p2p: prevent IBD stall after empty headers",
 "author": "l0rinc",
 "author_association": "MEMBER",
 "created_at": "2026-07-29T20:52:22Z",
 "updated_at": "2026-09-16T20:02:33Z",
 "age_days": 49,
 "draft": false,
 "labels": [
  "P2P"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "be1f152ffe18143651ca7456113d6d684b53da17",
 "head_ref": "l0rinc/p2p-empty-headers-sync",
 "head_repo": "l0rinc/bitcoin",
 "head_history": [
  {
   "t": "2026-07-29T21:15:35Z",
   "sha": "ea55ab8c1c0e0b007202a76e2660218810dc43ca"
  },
  {
   "t": "2026-07-30T21:26:28Z",
   "sha": "6d10e8e1932246de7c486febe2dec2b2e68da266"
  },
  {
   "t": "2026-08-29T21:56:32Z",
   "sha": "f374ee9995c78415092416d8683e7d7f67477f41"
  },
  {
   "t": "2026-09-01T17:10:57Z",
   "sha": "ec9fd7e66d390684d049fd5d36840279cf321b3d"
  },
  {
   "t": "2026-09-01T17:27:20Z",
   "sha": "be1f152ffe18143651ca7456113d6d684b53da17"
  }
 ],
 "additions": 62,
 "deletions": 11,
 "changed_files": 2,
 "commit_count": 5,
 "size_bucket": "S",
 "mergeable_state": "clean",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "concept_ack": [
     {
      "login": "jeanpablojp",
      "url": "https://github.com/bitcoin/bitcoin/pull/35839#pullrequestreview-5064424044"
     },
     {
      "login": "hodlinator",
      "url": "https://github.com/bitcoin/bitcoin/pull/35839#pullrequestreview-5207469944"
     }
    ]
   },
   "conflicts": []
  }
 },
 "acks_parsed": {
  "jeanpablojp": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-08-31T08:19:20Z",
   "stale": false
  },
  "hodlinator": {
   "kind": "concept_ack",
   "hash": "be1f152ffe18143651ca7456113d6d684b53da17",
   "t": "2026-09-15T09:31:58Z",
   "stale": false
  }
 },
 "acks_tally": {
  "ack": 0,
  "stale_ack": 0,
  "concept_ack": 2,
  "approach_ack": 0,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 0,
  "changes_requested": 0,
  "distinct_reviewers": [
   "danielabrozzoni",
   "davidgumberg",
   "hodlinator",
   "jeanpablojp",
   "maflcko",
   "mzumsande"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "clean",
  "last_author_activity": "2026-09-01T17:29:35Z",
  "last_reviewer_activity": "2026-09-16T16:45:36Z",
  "last_reviewer": "danielabrozzoni",
  "author_silent_days": 15,
  "waiting_on_author_days": 0,
  "days_since_update": 0
 },
 "refs": {
  "mentioned": [
   34096
  ],
  "depends_on": [],
  "fixes": [
   34096
  ],
  "linked_issues": [
   {
    "number": 34096,
    "type": "issue",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "IBD stalls if addnode peer is in IBD"
   }
  ],
  "references": [
   {
    "number": 34096,
    "type": "issue",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "IBD stalls if addnode peer is in IBD"
   }
  ],
  "conflicts": []
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [
  "src/net_processing.cpp",
  "test/functional/p2p_initial_headers_sync.py"
 ],
 "body": "**Problem:** While the best header is at least a day old, a node normally selects one peer for initial headers sync.\nAn inbound or manual peer can answer with valid empty `headers` and remain selected, so another eligible peer is not asked.\nEmpty `headers` may mean that the peer is caught up with the node or stuck at the same height, so the response is not misbehavior.\nWhen no other preferred download peer is available, the headers timeout will not release the peer, and automatic outbound eviction does not apply.\n\n**Fix:** Release an inbound or manual peer from initial headers sync after an empty response while the best header is stale and cancel its download deadline, allowing another eligible peer to take over.\nRestart the normal response window when releasing the slot so the same peer is not immediately selected again.\nA new block announcement can still trigger the existing one-shot request.\nAutomatic full-relay and block-relay peers keep their existing headers timeout and old-chain eviction behavior.\n\nManual reproducer\n\nBoth runs use the checked-out `build/bin/bitcoind`, with the downloaded Python script supplying only two inbound P2P peers.\n\n```bash\n{ cmake -B build && cmake --build build -j10 --target bitcoind; } >/dev/null 2>&1\nbuild/bin/bitcoind -regtest -daemonwait -datadir=\"$(mktemp -d)\" -connect=0 -listen=1 -port=18455\ntime python3 <(curl -fsSL https://gist.github.com/l0rinc/39e471dd6835fe332205d3812ebb9e92/raw)\nkillall bitcoind\n```\n\nThe fixed node returns as soon as the replacement peer receives `getheaders`.\nOn the old node, that wait times out after 10 seconds because the initial peer is still treated as active.\n\nFixes #34096",
 "commits": [
  {
   "sha": "ee9662eb2825a0ff9e01c320e2d602b3c8b0cad3",
   "date": "2026-09-01T17:25:55Z",
   "message": "test: isolate block announcement setup\n\nThe block-announcement checks send empty headers only to let the same peer receive another request.\nThe later fix changes how empty responses affect request timing.\n\nAdvance mock time past the normal two-minute response window so the test remains focused on selecting one additional peer per announcement."
  },
  {
   "sha": "30897517633f96d521e1cfcfe11c9c132b104787",
   "date": "2026-09-01T17:25:55Z",
   "message": "test: characterize inbound empty headers stall\n\nDuring IBD, a delayed empty `headers` response leaves an inbound peer selected for initial headers sync, so an eligible replacement receives no `getheaders` request.\n\nExtend the existing test with this stalled replacement before changing inbound peer handling."
  },
  {
   "sha": "f248d495b06a0c2ad209cd5d8803293b42397ea7",
   "date": "2026-09-01T17:25:55Z",
   "message": "p2p: release inbound peer after empty headers\n\nDuring IBD, an inbound peer can answer the initial `getheaders` request with empty `headers` and retain the only sync slot.\nWith no preferred download peer available, the headers timeout will not release it.\n\nRelease the slot after an empty response while the best header is at least a day old.\nRestart the normal response window so an eligible replacement is preferred before the same peer can be selected again.\nA new block announcement can still trigger the existing one-shot request.\n\nAutomatic outbound peers keep their existing headers timeout and old-chain eviction behavior."
  },
  {
   "sha": "d6e96ec6f15b6f041559f718501c864f59eb6726",
   "date": "2026-09-01T17:25:55Z",
   "message": "test: characterize manual empty headers stall\n\nA manual peer can also retain the initial headers-sync slot after a delayed empty `headers` response, so an eligible replacement receives no `getheaders` request.\n\nAdd the corresponding stalled-replacement case before extending empty-response handling to manual connections.\n\nCo-authored-by: JP <jeanpablo.jp@hotmail.com>"
  },
  {
   "sha": "be1f152ffe18143651ca7456113d6d684b53da17",
   "date": "2026-09-01T17:25:55Z",
   "message": "p2p: release manual peer after empty headers\n\nAfter an empty `headers` response, a manual peer can also retain the initial headers-sync slot.\nWith no other preferred download peer, the timeout does not release it, and automatic outbound eviction does not apply.\n\nApply the same slot release and retry delay used for inbound peers, allowing another eligible peer to take over."
  }
 ],
 "timeline": [
  {
   "t": "2026-07-29T21:15:35Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "ea55ab8c1c0e0b007202a76e2660218810dc43ca"
  },
  {
   "t": "2026-07-30T07:19:54Z",
   "kind": "comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "text": "Looks like CI fails on f169af459b5a1a6715fe392f270076cfaa31df0f:\n\n```\ntest  2026-07-30T05:28:45.141085Z TestFramework (ERROR): Unexpected exception:\n                                   Traceback (most recent call last):\n                                     File \"/home/runner/work/bitcoin/bitcoin/test/functional/test_framework/test_framework.py\", line 145, in main\n                                       self.run_test()\n                                     File \"/home/runner/work/bitcoin/bitcoin/ci_build/test/functional/p2p_segwit.py\", line 261, in run_test\n                                       self.test_non_witness_transaction()\n                                     File \"/home/runner/work/bitcoin/bitcoin/ci_build/test/functional/p2p_segwit.py\", line 112, in func_wrapper\n                                       func(self, *args, **kwargs)\n                                     File \"/home/runner/work/bitcoin/bitcoin/ci_build/test/functional/p2p_segwit.py\", line 308, in test_non_witness_transaction\n                                       self.generate(self.wallet, 99)  # let the block mature\n                                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n                                     File \"/home/runner/work/bitcoin/bitcoin/test/functional/test_framework/test_framework.py\", line 662, in generate\n                                       sync_fun() if sync_fun else self.sync_all()\n                                                                   ^^^^^^^^^^^^^^^\n                                     File \"/home/runner/work/bitcoin/bitcoin/test/functional/test_framework/test_framework.py\", line 743, in sync_all\n                                       self.sync_blocks(nodes)\n                                     File \"/home/runner/work/bitcoin/bitcoin/test/functional/test_framework/test_framework.py\", line 714, in sync_blocks\n                                       raise AssertionError(\"Block sync timed out after {}s:{}\".format(\n                                   AssertionError: Block sync timed out after 60s:\n                                     '44c1a1dd2c9cd9b6e27c3236cb82a66ac501e990bc4d84642fa9706b89169839'\n                                     '0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206'"
  },
  {
   "t": "2026-07-30T17:31:58Z",
   "kind": "review",
   "who": "mzumsande",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "ea55ab8c1c0e0b007202a76e2660218810dc43ca",
   "text": "Could you explain the motivation a bit more - is this a countermeasure against deliberate stalling by malicious peers, or something that is sometimes observed with legit peers or maybe broken peers on mainnet? Or just something that could happen in theory?"
  },
  {
   "t": "2026-07-30T21:26:28Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "6d10e8e1932246de7c486febe2dec2b2e68da266"
  },
  {
   "t": "2026-07-30T21:43:55Z",
   "kind": "comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nIt seems I split the change into too many parts, and there apparently isn't a simple enough seam here: releasing the peer while leaving the block-announcement retry for later creates a broken intermediate state. I squashed the two commits.\n\n[quoted text omitted]\nI found this while investigating crashes in code paths I\u2019m more familiar with, rather than from a mainnet observation.\nMy understanding is that an empty headers response is protocol-valid: the peer may be up to date with us or stuck at the same height, so it is not necessarily misbehaving. The [second commit](https://github.com/bitcoin/bitcoin/pull/35839/changes/9c76ffe82ffbccef06c0c3d9577ed54b54acd7c5) adjusted the test explicitly relying on this.\nI reproduced the stall locally with the reproducer included in this PR. I assume that translates to mainnet behavior. Updated PR description slightly, let me know if it makes sense."
  },
  {
   "t": "2026-08-04T22:32:32Z",
   "kind": "comment",
   "who": "davidgumberg",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nThis fixes https://github.com/bitcoin/bitcoin/issues/34096, isn't that the motivation? (https://github.com/l0rinc/bitcoin/pull/69)\n\n#34096 happens on mainnet and requires the use of addnode, but I don't see why it couldn't work to stall a random new node, but I haven't reproduced this."
  },
  {
   "t": "2026-08-04T22:52:01Z",
   "kind": "comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nAbsolutely, it was in the queue for so long that I forgot about it, thanks for noticing, added it to the PR description.\nCan you check if it fixes the problem for you"
  },
  {
   "t": "2026-08-24T20:32:13Z",
   "kind": "review_comment",
   "who": "danielabrozzoni",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_initial_headers_sync.py",
   "commit": "fc25b1be02488744c5f6d7e3adf08d4adc4f183c",
   "in_reply_to": null,
   "text": "This wasn\u2019t obvious to me on first read: `wait_for_getheaders()` removes the matching `getheaders` entry from `last_message`. Without knowing this, the assertion below at L96 looked useless, because I assumed it would have been true even if we didn't send any `getheaders` after receiving the unconnecting header.\n\nMaybe it's worth it to add as a comment:\n\n```\n# wait_for_getheaders() consumes the matching entry from last_message,\n# so a later \"getheaders\" entry represents a newly received request.\npeer1.wait_for_getheaders(block_hash=best_block_hash)\n```"
  },
  {
   "t": "2026-08-24T20:35:45Z",
   "kind": "review_comment",
   "who": "danielabrozzoni",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_initial_headers_sync.py",
   "commit": "fc25b1be02488744c5f6d7e3adf08d4adc4f183c",
   "in_reply_to": null,
   "text": "nit: it can be made slightly clearer with a comment:\n```\nAnswer the previous getheaders with an empty response,\nthen send a different unconnecting header to trigger a new\nrequest\n```"
  },
  {
   "t": "2026-08-25T12:50:56Z",
   "kind": "review_comment",
   "who": "danielabrozzoni",
   "assoc": "MEMBER",
   "path": "src/net_processing.cpp",
   "commit": "79aca0b97fd61fcf07b5e62f44aefe58946ae37c",
   "in_reply_to": null,
   "text": "nit: This should be in the other if branch"
  },
  {
   "t": "2026-08-25T13:31:50Z",
   "kind": "review_comment",
   "who": "danielabrozzoni",
   "assoc": "MEMBER",
   "path": "src/net_processing.cpp",
   "commit": "79aca0b97fd61fcf07b5e62f44aefe58946ae37c",
   "in_reply_to": null,
   "text": "(Found with gpt 5.6-sol): this means that a peer can send an already-known connecting header and bypass the backoff. Consider this:\n1. Peer `P` receives the initial getheaders\n2. `P` sends empty headers and is released with backoff\n3. `P` sends one already-known header (such as our current best header)\n4. The header connects, so we clear the backoff and the released flag\n5. if no repleacement peer has taken the slot yet, `SendMessages()` can immediately select P again\n\nIf P wants to stall us for as long as possible, and there are other eligible peers, it could:\n1. Receive getheaders\n2. Hold the slot without replying until shortly before `m_headers_sync_timeout` (~15 minutes)\n3. Send  an empty headers, releasing the slot\n4. Send an already known connecting header\n5. If no other peer acquired the slot, acquire the slot, refreshing `m_headers_sync_timeout`\n6. Repeat\n\n(If there are no other eligble peers, P can stall us indefinitely because we won't disconnect, this is true for master too)\n\nNow, I'm not sure if this is worth fixing or not, because I don't know if there are many ways to stall a peer during header sync, but if we decide not to fix, it should be at least pointed out in a comment"
  },
  {
   "t": "2026-08-25T14:47:48Z",
   "kind": "review_comment",
   "who": "danielabrozzoni",
   "assoc": "MEMBER",
   "path": "src/net_processing.cpp",
   "commit": "79aca0b97fd61fcf07b5e62f44aefe58946ae37c",
   "in_reply_to": null,
   "text": "nit: I would change the order of the sentences, so we explain the case where headers connect first, and then when they don't:\n\n```\n// If headers connect, assume that this is in response to any outstanding getheaders\n// request we may have sent, and clear out the time of our last request. This will also\n// ends any released-slot backoff. Non-connecting\n// headers cannot be a response to a getheaders request.\n```"
  },
  {
   "t": "2026-08-25T14:57:01Z",
   "kind": "review",
   "who": "danielabrozzoni",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "6d10e8e1932246de7c486febe2dec2b2e68da266",
   "text": "Partial review, up to 79aca0b97fd61fcf07b5e62f44aefe58946ae37c\n\nI am still wrapping my head around the code, I'm not very familiar with it :)\n\nI left a few nits that, imho, make the comments easier to follow, plus one issue that GPT-5.6-sol found. I reviewed it manually and think it\u2019s correct, but I\u2019m not sure whether it\u2019s worth fixing or whether a comment pointing it out would be enough."
  },
  {
   "t": "2026-08-29T21:56:32Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "f374ee9995c78415092416d8683e7d7f67477f41"
  },
  {
   "t": "2026-08-29T22:40:18Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_initial_headers_sync.py",
   "commit": "fc25b1be02488744c5f6d7e3adf08d4adc4f183c",
   "in_reply_to": 3847231919,
   "text": "Thanks, that sequence is gone now. The rewritten characterization checks `peer4.last_message` directly, so it no longer depends on `wait_for_getheaders()` consuming an earlier message."
  },
  {
   "t": "2026-08-29T22:40:54Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_initial_headers_sync.py",
   "commit": "fc25b1be02488744c5f6d7e3adf08d4adc4f183c",
   "in_reply_to": 3847258874,
   "text": "That repeated-empty and unconnecting-header sequence is gone in the rewritten test, so this no longer applies."
  },
  {
   "t": "2026-08-29T22:41:13Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/net_processing.cpp",
   "commit": "79aca0b97fd61fcf07b5e62f44aefe58946ae37c",
   "in_reply_to": 3853116023,
   "text": "That branch and `m_initial_headers_sync_released` are gone now. The remaining comment is next to the conditional timestamp clear."
  },
  {
   "t": "2026-08-29T22:42:52Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/net_processing.cpp",
   "commit": "79aca0b97fd61fcf07b5e62f44aefe58946ae37c",
   "in_reply_to": 3853482754,
   "text": "[quoted text omitted]\n\nGood catch, this still applies because connecting headers clear `m_last_getheaders_timestamp`.\nI started implementing this first but it got more complicated than I was comfortable with - we should fix it after this is merged.\nInstead I narrowed this PR to #34096's valid empty-response stall because I do not see a small fix for deliberate reacquisition that would not complicate low-work and continuation handling."
  },
  {
   "t": "2026-08-29T22:43:06Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/net_processing.cpp",
   "commit": "79aca0b97fd61fcf07b5e62f44aefe58946ae37c",
   "in_reply_to": 3854191131,
   "text": "That comment and the released-slot flag are gone in the rewrite, so this no longer applies."
  },
  {
   "t": "2026-08-29T22:51:27Z",
   "kind": "review",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "f374ee9995c78415092416d8683e7d7f67477f41",
   "text": "Thanks for the review, rebased and simplified this a lot in the latest push: we don't have to fix every headers sync problem here, it was getting uncomfortably bloated...\n\nThe previous version released every initial headers-sync peer after an empty response. That also stopped automatic outbound old-chain eviction, so it needed a new release flag, helper refactors, broader tests, and 2 follow-up commits to keep eviction active. I reverted that machinery and the outbound-specific tests because they expanded the change beyond #34096. Automatic full-relay and block-relay peers now keep their existing bounded timeout and eviction behavior.\n\nThe current change focuses on the indefinite inbound/manual stall. During stale-header IBD, an inbound or manual peer can return valid empty headers while retaining the only sync slot.\nThe node releases peer1\u2019s initial-sync slot and cancels the download deadline attached to it, allowing another eligible peer to take over. It retains peer1\u2019s recent getheaders timestamp only to prevent the scheduler from immediately assigning the free slot back to the same peer. Another eligible peer can take over, while a later block announcement can still trigger the existing one-shot request.\n\nThe first commit removes the block-announcement test\u2019s dependency on empty-header behavior, the second characterizes the stalled replacement peer, and the third changes production behavior and flips that single expectation. Deliberate reacquisition with an already-known header remains separate follow-up work because I did not find a small fix that stayed independent of low-work headers sync."
  },
  {
   "t": "2026-08-31T08:19:20Z",
   "kind": "review",
   "who": "jeanpablojp",
   "assoc": "CONTRIBUTOR",
   "state": "COMMENTED",
   "commit": "f374ee9995c78415092416d8683e7d7f67477f41",
   "text": "Concept ACK\n\nIf the empty response arrives more than two minutes after the request, the peer is released and takes the slot back in the same `SendMessages`, with a fresh `m_headers_sync_timeout`. Answering that way every time keeps every other peer out of the sync and the deadline never fires. The retained timestamp is the request's, not the release's, so the backoff has already expired when the response arrives.\n\nWith mocktime advanced 121s before each empty response and another peer available, the merge base disconnects the peer at the deadline and this branch keeps it, eighteen `initial getheaders` in the log and none to the other peer. Same for manual connections. Is this intentional?"
  },
  {
   "t": "2026-08-31T08:19:20Z",
   "kind": "review_comment",
   "who": "jeanpablojp",
   "assoc": "CONTRIBUTOR",
   "path": "src/net_processing.cpp",
   "commit": "ec9fd7e66d390684d049fd5d36840279cf321b3d",
   "in_reply_to": null,
   "text": "This is outside the `fSyncStarted` check, so a peer that isn't holding the slot gets the backoff too, and the reason in the commit message doesn't cover that case. Moving it inside keeps the tests green here. Worth it?"
  },
  {
   "t": "2026-08-31T08:19:20Z",
   "kind": "review_comment",
   "who": "jeanpablojp",
   "assoc": "CONTRIBUTOR",
   "path": "test/functional/p2p_initial_headers_sync.py",
   "commit": "be1f152ffe18143651ca7456113d6d684b53da17",
   "in_reply_to": null,
   "text": "`IsManualConn()` is the only arm of the guard with no coverage, and removing it breaks no test. `addconnection` has no manual type, but you can build one with `addnode onetry` on top of `peer_accept_connection`."
  },
  {
   "t": "2026-08-31T15:16:42Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/net_processing.cpp",
   "commit": "ec9fd7e66d390684d049fd5d36840279cf321b3d",
   "in_reply_to": 3892792058,
   "text": "Good catch, even better, we don't need two separate conditions and we can modify `m_last_getheaders_timestamp` directly (we just have to bump `peer.m_last_getheaders_timestamp = {}` to the top)"
  },
  {
   "t": "2026-08-31T17:46:28Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_initial_headers_sync.py",
   "commit": "be1f152ffe18143651ca7456113d6d684b53da17",
   "in_reply_to": 3892792059,
   "text": "Another good catch, thanks. I added a manual-peer test using `addnode onetry` on top of `peer_accept_connection()`, and split the inbound and manual cases into separate characterization/fix pairs. Removing either guard arm now breaks its corresponding test. Added you as coauthor, thanks."
  },
  {
   "t": "2026-08-31T17:49:35Z",
   "kind": "review",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "f374ee9995c78415092416d8683e7d7f67477f41",
   "text": "Rebased after [the related lagging-clock headers-sync fix](https://github.com/bitcoin/bitcoin/pull/35351).\n\nThanks @jeanpablojp, took all your suggestions.\nThis update starts the retry delay when a peer releases the slot, splits inbound and manual handling into separate characterization/fix pairs, flattens the nested release conditions, and adds delayed manual-peer coverage."
  },
  {
   "t": "2026-09-01T07:50:46Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_initial_headers_sync.py",
   "commit": "be1f152ffe18143651ca7456113d6d684b53da17",
   "in_reply_to": null,
   "text": "Could you please add a comment here in the code describing why we bump the mocktime before announcing the block?\n\nSame for the later bump."
  },
  {
   "t": "2026-09-01T08:04:30Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "path": "src/net_processing.cpp",
   "commit": "ec9fd7e66d390684d049fd5d36840279cf321b3d",
   "in_reply_to": null,
   "text": "nanonit: 80 columns?\n```suggestion\n        // Empty headers cannot be an announcement, so clear the request\n        // timestamp unless it is retained as a retry delay.\n```"
  },
  {
   "t": "2026-09-01T08:12:06Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "path": "src/net_processing.cpp",
   "commit": "ec9fd7e66d390684d049fd5d36840279cf321b3d",
   "in_reply_to": null,
   "text": "Could you please add a comment explaining why we check this condition?\n\n\"Only keep the header request timestamp for this peer if our local best header is more than 1 day old since [reason not immediately obvious]\""
  },
  {
   "t": "2026-09-01T11:56:59Z",
   "kind": "review",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "f374ee9995c78415092416d8683e7d7f67477f41",
   "text": "Will take some time to fully wrap my brain around this, thanks for at least decreasing the scope already."
  },
  {
   "t": "2026-09-01T16:33:08Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/net_processing.cpp",
   "commit": "ec9fd7e66d390684d049fd5d36840279cf321b3d",
   "in_reply_to": 3902035417,
   "text": "I prefer the code and especially comments to occupy the space proportional to their usefulness and how often we need them, so I usually try to compress all info on a single line for new comments - if they're really needed."
  },
  {
   "t": "2026-09-01T17:10:57Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "ec9fd7e66d390684d049fd5d36840279cf321b3d"
  },
  {
   "t": "2026-09-01T17:27:20Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "be1f152ffe18143651ca7456113d6d684b53da17"
  },
  {
   "t": "2026-09-01T17:28:32Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/net_processing.cpp",
   "commit": "ec9fd7e66d390684d049fd5d36840279cf321b3d",
   "in_reply_to": 3892792058,
   "text": "Looks like I forgot to push yesterday, see the latest push"
  },
  {
   "t": "2026-09-01T17:29:24Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_initial_headers_sync.py",
   "commit": "be1f152ffe18143651ca7456113d6d684b53da17",
   "in_reply_to": 3901934628,
   "text": "I was travelling yesterday, looks like my force push timed out and you were reviewing the previous version - my mistake, added the code comments and the previous restructure"
  },
  {
   "t": "2026-09-01T17:29:35Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/net_processing.cpp",
   "commit": "ec9fd7e66d390684d049fd5d36840279cf321b3d",
   "in_reply_to": 3902099920,
   "text": "Added, let me know if it helps"
  },
  {
   "t": "2026-09-15T08:25:30Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_initial_headers_sync.py",
   "commit": "be1f152ffe18143651ca7456113d6d684b53da17",
   "in_reply_to": null,
   "text": "nit: Could explain why:\n```suggestion\n        # Take control over time so we can trigger timeouts precisely.\n        self.nodes[0].setmocktime(int(time.time()))\n```"
  },
  {
   "t": "2026-09-15T08:28:15Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_initial_headers_sync.py",
   "commit": "be1f152ffe18143651ca7456113d6d684b53da17",
   "in_reply_to": null,
   "text": "nit: Could replace this with:\n```diff\n-         # An empty reply will clear the outstanding getheaders request,\n-         # allowing additional getheaders requests to be sent to this peer in\n-         # the future.\n-         peer1.send_without_ping(msg_headers())\n-\n+        # Don't answer the getheaders request since we want to time out later.\n+\n```"
  },
  {
   "t": "2026-09-15T08:42:54Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "path": "src/net_processing.cpp",
   "commit": "be1f152ffe18143651ca7456113d6d684b53da17",
   "in_reply_to": null,
   "text": "remark: In my mind manually added nodes are even more trusted than outbound nodes, which are more trusted than inbound nodes. I guess there could still be some kind of hiccup causing manual nodes to not have all the headers though."
  },
  {
   "t": "2026-09-15T08:51:21Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "path": "src/net_processing.cpp",
   "commit": "ec9fd7e66d390684d049fd5d36840279cf321b3d",
   "in_reply_to": 3902099920,
   "text": "thread https://github.com/bitcoin/bitcoin/pull/35839#discussion_r3902099920:\n\n```c++\n            // Match the scheduler's one-day threshold and retain the timestamp so this peer cannot immediately retake the sync slot\n```\n\nStill not sure which scheduler this refers to. My best guess is that this is referring to the two 24h thresholds in `PeerManagerImpl::SendMessages()`?\n\nLong lines in tests as in https://github.com/bitcoin/bitcoin/pull/35839#discussion_r3906237668 seem more okay. But here we go far beyond the method's established convention of ~80 chars (past 120 too)."
  },
  {
   "t": "2026-09-15T09:01:20Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "path": "src/net_processing.cpp",
   "commit": "be1f152ffe18143651ca7456113d6d684b53da17",
   "in_reply_to": null,
   "text": "Could you please include a reference to `PeerManagerImpl::ConsiderEviction()`, where outbound or block relay peers are evicted, either in the comment or the commit message?"
  },
  {
   "t": "2026-09-15T09:30:33Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "path": "src/net_processing.cpp",
   "commit": "be1f152ffe18143651ca7456113d6d684b53da17",
   "in_reply_to": null,
   "text": "PR desc:\n\n[quoted text omitted]\nThis sounds like it re-enables the attack somewhat? Could add a note admitting so and that it should be fixed in a later PR.\n\nCould also add a sentence about deferring fixing https://github.com/bitcoin/bitcoin/pull/35839#discussion_r3853482754 to the PR desc.\n\nNot sure the reproducer script linked in the PR desc adds that much beyond the functional tests. I guess it's nice to have a minimal example. But it feels insecure to encourage download and execution of script source code obscured behind a URL."
  },
  {
   "t": "2026-09-15T09:31:58Z",
   "kind": "review",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "be1f152ffe18143651ca7456113d6d684b53da17",
   "text": "Concept ACK be1f152ffe18143651ca7456113d6d684b53da17"
  },
  {
   "t": "2026-09-16T16:45:36Z",
   "kind": "review_comment",
   "who": "danielabrozzoni",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_initial_headers_sync.py",
   "commit": "d6e96ec6f15b6f041559f718501c864f59eb6726",
   "in_reply_to": null,
   "text": "I'm confused, is the bumpmocktime really necessary? I think the point is that regardless of timeout, if an inbound/manual sends empty headers, we release the slot. The test seems to pass even if I comment this out"
  }
 ],
 "labels_log": [
  {
   "t": "2026-07-29T20:52:26Z",
   "action": "labeled",
   "label": "P2P",
   "who": "DrahtBot"
  },
  {
   "t": "2026-07-29T21:16:07Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-07-30T22:29:22Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-29T22:56:47Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-30T00:29:44Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-01T17:28:31Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-01T18:30:28Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  }
 ],
 "state_log": [
  {
   "t": "2026-07-29T20:58:51Z",
   "kind": "convert_to_draft",
   "who": "l0rinc"
  },
  {
   "t": "2026-07-29T21:15:23Z",
   "kind": "ready_for_review",
   "who": "l0rinc"
  },
  {
   "t": "2026-07-30T05:06:02Z",
   "kind": "closed",
   "who": "l0rinc"
  },
  {
   "t": "2026-07-30T05:06:05Z",
   "kind": "reopened",
   "who": "l0rinc"
  },
  {
   "t": "2026-08-29T21:58:08Z",
   "kind": "renamed",
   "who": "l0rinc",
   "from": "p2p: select another peer after empty headers",
   "to": "p2p: prevent IBD stall after empty headers"
  },
  {
   "t": "2026-08-29T22:56:17Z",
   "kind": "closed",
   "who": "l0rinc"
  },
  {
   "t": "2026-08-29T22:56:22Z",
   "kind": "reopened",
   "who": "l0rinc"
  }
 ],
 "text_chars": 18463,
 "text_tokens_estimate": 4615,
 "changed_paths": [
  "src/net_processing.cpp",
  "test/functional/p2p_initial_headers_sync.py"
 ],
 "files": [
  {
   "path": "src/net_processing.cpp",
   "add": 23,
   "del": 4
  },
  {
   "path": "test/functional/p2p_initial_headers_sync.py",
   "add": 39,
   "del": 7
  }
 ],
 "test_lines": 46,
 "git": {
  "head": "be1f152ffe18143651ca7456113d6d684b53da17",
  "head_matches_backup": true,
  "base": "dc0395c5858a1d55239b82a834e5075cf2069219",
  "commits": [
   {
    "sha": "ee9662eb28",
    "subject": "test: isolate block announcement setup",
    "files": 1,
    "add": 6,
    "del": 7
   },
   {
    "sha": "3089751763",
    "subject": "test: characterize inbound empty headers stall",
    "files": 1,
    "add": 13,
    "del": 0
   },
   {
    "sha": "f248d495b0",
    "subject": "p2p: release inbound peer after empty headers",
    "files": 2,
    "add": 24,
    "del": 5
   },
   {
    "sha": "d6e96ec6f1",
    "subject": "test: characterize manual empty headers stall",
    "files": 1,
    "add": 20,
    "del": 0
   },
   {
    "sha": "be1f152ffe",
    "subject": "p2p: release manual peer after empty headers",
    "files": 2,
    "add": 2,
    "del": 2
   }
  ],
  "patch_truncated": false
 },
 "input_hash": "ae8c64580d067eda",
 "extracted_at": "2026-09-17T16:15:31+00:00"
}