{
 "number": 36080,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/36080",
 "title": "p2p: Suspend ping timeout while downloading blocks from a peer",
 "author": "mzumsande",
 "author_association": "MEMBER",
 "created_at": "2026-08-25T15:59:29Z",
 "updated_at": "2026-09-16T14:00:09Z",
 "age_days": 23,
 "draft": false,
 "labels": [
  "P2P"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "31280333dd4f9ef2ec818b1a9d25daaeb502659e",
 "head_ref": "202608_ping_ibd",
 "head_repo": "mzumsande/bitcoin",
 "head_history": [
  {
   "t": "2026-08-25T16:04:57Z",
   "sha": "70600bee8d2174c662b011f1900f24bf9e356c1f"
  },
  {
   "t": "2026-08-25T16:09:49Z",
   "sha": "abeb912d45f333e7343f3d335621b9643438edec"
  },
  {
   "t": "2026-09-04T16:34:42Z",
   "sha": "c551ff40a4f924eb42680e3eeef39d47d889e55c"
  },
  {
   "t": "2026-09-16T13:59:38Z",
   "sha": "31280333dd4f9ef2ec818b1a9d25daaeb502659e"
  }
 ],
 "additions": 238,
 "deletions": 18,
 "changed_files": 3,
 "commit_count": 3,
 "size_bucket": "M",
 "mergeable_state": "clean",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "concept_ack": [
     {
      "login": "l0rinc",
      "url": "https://github.com/bitcoin/bitcoin/pull/36080#pullrequestreview-5032343334"
     },
     {
      "login": "danielabrozzoni",
      "url": "https://github.com/bitcoin/bitcoin/pull/36080#pullrequestreview-5068122581"
     }
    ]
   },
   "conflicts": []
  }
 },
 "acks_parsed": {
  "l0rinc": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-08-26T15:37:30Z",
   "stale": false
  },
  "danielabrozzoni": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-09-02T18:46:20Z",
   "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",
   "l0rinc"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "clean",
  "last_author_activity": "2026-09-16T14:00:09Z",
  "last_reviewer_activity": "2026-09-11T16:08:45Z",
  "last_reviewer": "danielabrozzoni",
  "author_silent_days": 1,
  "waiting_on_author_days": 0,
  "days_since_update": 1
 },
 "refs": {
  "mentioned": [
   35761
  ],
  "depends_on": [],
  "fixes": [
   35761
  ],
  "linked_issues": [
   {
    "number": 35761,
    "type": "issue",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "Unnecessary disconnections due to PONG delays during the initial block download"
   }
  ],
  "references": [
   {
    "number": 35761,
    "type": "issue",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "Unnecessary disconnections due to PONG delays during the initial block download"
   }
  ],
  "conflicts": []
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [
  "src/net_processing.cpp",
  "test/functional/p2p_ping_ibd.py"
 ],
 "body": "While serving blocks, there is a system in place that prioritizes a peer's block requests before answering other p2p messages:\nSee\nhttps://github.com/bitcoin/bitcoin/blob/11090c8bb359f894ef7d97b65aff52fe8191aec1/src/net_processing.cpp#L5436\n\nAs a result it can happen that if we do IBD with a low download bandwidth (that is distributed over 10 peers) a peer will not get around to answering our ping before the timeout of 20 minutes, in which case we would disconnect them, although they have done nothing wrong and are not even slow themselves (we are).\nThis situation has been described in #35761.\n\nThis PR fixes the issue by not enforcing the ping timeout from a peer while downloading blocks from them.\nIn order to do that, the ping timeout check is moved out of `MaybeSendPing()` (which was a slightly awkward place anyway, given the name of the function) and suspended until there are no longer blocks in flight with that peer (with a grace period, so that we don't disconnect immediately after the last block was received before the peer got a chance to send us the `pong`).\n\nNote that during block download, there are still other timeouts:\n- A dynamic timeout (`BLOCK_DOWNLOAD_TIMEOUT_BASE` / `BLOCK_DOWNLOAD_TIMEOUT_PER_PEER`) which will result in a timeout of  `600s \u00d7 (1 + 0.5\u00d79) = 55 minutes` per block when downloading from 10 peers in parallel\n- the stalling logic which hits if the peer is much slower in comparison to other peers\n- the socket inactivity check disconnects a peer that hasn't sent us anything at all in the last 20 minutes.\n\nSo the ping timeout didn't add much value anyway in that situation.\n\nFixes #35761",
 "commits": [
  {
   "sha": "56c194e655872e95ef14c64e415377dd8beda28b",
   "date": "2026-09-04T16:32:50Z",
   "message": "test: add functional test for pings during IBD\n\nWhen the node has outstanding block requests, it will first\nserve all of those before answering pings. If this happens\nslowly because the requestor of the blocks has a slow connection,\nthey would disconnect our node due to a ping timeout, even though\nit did nothing wrong.\n\nThe second subtest documents the current behavior from the other side:\nwe disconnect a peer that is busy serving us the blocks we requested\nfrom it, just because it didn't answer our ping in time. A later commit\nchanges this."
  },
  {
   "sha": "95dcf8f10dc83fd1d5048fc82027f53b3f83dba3",
   "date": "2026-09-04T16:33:38Z",
   "message": "p2p: move ping timeout check into SendMessages\n\nThe check is relocated from MaybeSendPing to the timeout\nsection of SendMessages, right after the block download timeout.\n\nThis is in preparation for the following commit, which makes the ping timeout depend\non whether the peer is currently serving us blocks.\nThat requires CNodeState, which is guarded by cs_main and therefore not reachable from MaybeSendPing.\n\nMoving it makes sense anyway, since timeout decisions don't really\nbelong in a function called MaybeSendPing."
  },
  {
   "sha": "31280333dd4f9ef2ec818b1a9d25daaeb502659e",
   "date": "2026-09-16T13:57:13Z",
   "message": "p2p: Don't apply ping timeout while downloading blocks\n\nIf the peer is also running bitcoin core, they will prioritize\nserving the blocks over answering pings. If this is slow due to\nour own download speed, we could timeout the peer even though\nthey did nothing wrong.\n\nTherefore suspend the check while downloading blocks.\nIf the peer is slow, we have other mechanisms (socket timeout,\nblock request timeout logic)\nto disconnect them, so the ping timeout wasn't necessary anyway.\n\nOnce the last new block is received, give the peer a grace period to send\nus the pong.\n\nUpdate the functional test added earlier accordingly: the peer is no\nlonger disconnected while it is serving blocks, and only times out once\nthe grace period after the last block has passed."
  }
 ],
 "timeline": [
  {
   "t": "2026-08-25T16:04:57Z",
   "kind": "force_push",
   "who": "mzumsande",
   "commit": "70600bee8d2174c662b011f1900f24bf9e356c1f"
  },
  {
   "t": "2026-08-25T16:09:49Z",
   "kind": "force_push",
   "who": "mzumsande",
   "commit": "abeb912d45f333e7343f3d335621b9643438edec"
  },
  {
   "t": "2026-08-26T15:36:40Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_ping_ibd.py",
   "commit": "31280333dd4f9ef2ec818b1a9d25daaeb502659e",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nnit: whenever there's a test after the fix, I have a hard time understanding what the behavior was before the fix, it's why I usually add a characterization test before the fix to document the previous behavior, showing that intermediary refactors don't update the test (i.e. aren't changing behavior) while the fix commit only does surgical changes to the assertions, documenting exactly how the fix changes the assumptions, see https://github.com/bitcoin/bitcoin/pull/35260\n\nIt would help with the review if most of the tests would be moved before the fix to help us understand both the before and after states."
  },
  {
   "t": "2026-08-26T15:37:30Z",
   "kind": "review",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "abeb912d45f333e7343f3d335621b9643438edec",
   "text": "Concept ACK"
  },
  {
   "t": "2026-08-31T15:19:04Z",
   "kind": "review_comment",
   "who": "danielabrozzoni",
   "assoc": "MEMBER",
   "path": "src/net_processing.cpp",
   "commit": "31280333dd4f9ef2ec818b1a9d25daaeb502659e",
   "in_reply_to": null,
   "text": "(Comment placed in random location)\nnit: In commit message be32083ddb17d5bc1667a56fe7f3b7a82f7e0e68, there's a typo: `timout -> timeout`"
  },
  {
   "t": "2026-08-31T16:45:44Z",
   "kind": "review_comment",
   "who": "danielabrozzoni",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_ping_ibd.py",
   "commit": "e13760ad536f15a3280ae555c65ff922cf820478",
   "in_reply_to": null,
   "text": "In e13760ad536f15a3280ae555c65ff922cf820478: I don't understand what's the purpose of this check; if the peer was a Bitcoin Core node, we wouldn't be connected anymore (this is the first commit, so there's still ping timeout). The peer is still connected only because `P2PInterface` has no ping timeout."
  },
  {
   "t": "2026-09-02T13:44:58Z",
   "kind": "review_comment",
   "who": "danielabrozzoni",
   "assoc": "MEMBER",
   "path": "src/net_processing.cpp",
   "commit": "31280333dd4f9ef2ec818b1a9d25daaeb502659e",
   "in_reply_to": null,
   "text": "Opus 5 flagged this - I think it's true in theory, but I'm not sure in practice if it can ever happen, there's a lot of details around block relay that I don't really know and I'm trying to understand :)\n\nYou're using `m_last_block_time`, but it gets updated only upon receiving a block we didn't already have, which means that a peer that sends us block that we already had would get a shorter post block pong grace, or none at all.\n\nhttps://github.com/bitcoin/bitcoin/blob/dc0395c5858a1d55239b82a834e5075cf2069219/src/net_processing.cpp#L3676-L3681\n\nSuppose peerA has been sending us blocks, and not responding to our pings in the meantime. We only miss one block, the tip. Then, either of this happens:\n- peerB pushes one or more unsolicited BLOCK message with the tip\n- peerB is one of our high bandwidth compact block peers, and sends us the tip, unsolicited, via CMPCTBLOCK. (I think this particular condition can't happen if we were doing IBD with peerA, but it can happen that we have compact block peers if we were synced and then feel behind by a few blocks)\n\nIn any case, peerB sends us the last block in peerA's queue. Then peerA will be given a shorter post block pong grace, or none at all, if the last new block they sent was more than 1min ago. This is because peerA sending us the last block didn't update `m_last_block_time`, since the block wasn't new to us."
  },
  {
   "t": "2026-09-02T18:31:27Z",
   "kind": "review_comment",
   "who": "danielabrozzoni",
   "assoc": "MEMBER",
   "path": "src/net_processing.cpp",
   "commit": "31280333dd4f9ef2ec818b1a9d25daaeb502659e",
   "in_reply_to": null,
   "text": "Surfaced using Opus 5, I checked manually and I think it's correct:\n\nYou were previously checking whether to disconnect for inactivity first, and only then deciding whether to send a new ping, based on `peer.m_ping_queued` and whether enough time passed from the last ping. Now you're doing the opposite, and what might happen is:\n- peer hasn't been responding to our ping, it's time to disconnect it\n- the user calls the ping RPC\n- in `MaybeSendPing`, `peer.m_ping_queued` is true, so `pingSend` gets set to true, and `peer.m_ping_start = now` and `peer.m_ping_nonce_sent` get reset.\n- When we reach the disconnection check here, `m_ping_start` will be fresh, and we won't disconnect.\n\nThe chances are of course slim, and I'm not sure what's the best way to fix...\n- You can save a snapshot of `m_ping_queued` and `m_ping_start` before `MaybeSendPing` is called, and here evaluate against those - not super clean, but will work\n- In `MaybeSendPing` you could honor the user's ping request only if there's not an outstanding ping, but I don't think that's correct\n- Maybe you can move MaybeSendPing down here, but I'm sure it has other implications...\n- You can document that this might happen and move on, the chances are slim, and if it happens it's not too big of a deal imho :)"
  },
  {
   "t": "2026-09-02T18:46:20Z",
   "kind": "review",
   "who": "danielabrozzoni",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "abeb912d45f333e7343f3d335621b9643438edec",
   "text": "Concept ACK! I am slowly going through it and learning more about block relay :)"
  },
  {
   "t": "2026-09-04T16:34:42Z",
   "kind": "force_push",
   "who": "mzumsande",
   "commit": "c551ff40a4f924eb42680e3eeef39d47d889e55c"
  },
  {
   "t": "2026-09-04T16:35:23Z",
   "kind": "review_comment",
   "who": "mzumsande",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_ping_ibd.py",
   "commit": "31280333dd4f9ef2ec818b1a9d25daaeb502659e",
   "in_reply_to": 3864348923,
   "text": "moved some of the test into the first commit. The grace period test coverage makes no sense there though."
  },
  {
   "t": "2026-09-04T16:35:32Z",
   "kind": "review_comment",
   "who": "mzumsande",
   "assoc": "MEMBER",
   "path": "src/net_processing.cpp",
   "commit": "31280333dd4f9ef2ec818b1a9d25daaeb502659e",
   "in_reply_to": 3895778346,
   "text": "fixed"
  },
  {
   "t": "2026-09-04T16:35:50Z",
   "kind": "review_comment",
   "who": "mzumsande",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_ping_ibd.py",
   "commit": "e13760ad536f15a3280ae555c65ff922cf820478",
   "in_reply_to": 3896482947,
   "text": "makes sense, I removed the check."
  },
  {
   "t": "2026-09-04T16:38:25Z",
   "kind": "review_comment",
   "who": "mzumsande",
   "assoc": "MEMBER",
   "path": "src/net_processing.cpp",
   "commit": "31280333dd4f9ef2ec818b1a9d25daaeb502659e",
   "in_reply_to": 3914749439,
   "text": "I also don't think that this is a real issue in practice, because the original problem exists mostly during IBD, where bandwidth is spread over 10 peers and 16 blocks are queued at the same time. In IBD, we don't request a single block from multiple peers at the same time, so this wouldn't happen. Even if this could happen in theory as a one-off after catching up with the tip as in your scenario, it's a one-off and not a systematic problem as the status quo."
  },
  {
   "t": "2026-09-04T16:42:52Z",
   "kind": "review_comment",
   "who": "mzumsande",
   "assoc": "MEMBER",
   "path": "src/net_processing.cpp",
   "commit": "31280333dd4f9ef2ec818b1a9d25daaeb502659e",
   "in_reply_to": 3917382766,
   "text": "this feels like one of those fringe KI findings that I think don't really need to be addressed:\nAs far as I can see, a user can meddle with pings already today on master - if they send a `ping` rpc every 19 minutes, an unresponsive peer will never be disconnected due to missing pings. For the particular issue here, they would need to time their ping exactly, which seems very unlikely. But I don't even think we need to fix the behavior on master."
  },
  {
   "t": "2026-09-04T16:43:17Z",
   "kind": "comment",
   "who": "mzumsande",
   "assoc": "MEMBER",
   "text": "[abeb912](https://github.com/bitcoin/bitcoin/commit/abeb912d45f333e7343f3d335621b9643438edec) to [c551ff4](https://github.com/bitcoin/bitcoin/commit/c551ff40a4f924eb42680e3eeef39d47d889e55c): addressed feedback"
  },
  {
   "t": "2026-09-11T16:01:37Z",
   "kind": "review_comment",
   "who": "danielabrozzoni",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_ping_ibd.py",
   "commit": "31280333dd4f9ef2ec818b1a9d25daaeb502659e",
   "in_reply_to": null,
   "text": "I think here you should also wait  for `POST_BLOCK_PONG_GRACE + 1` seconds, and check that you don't get disconnected:\n```diff\ndiff --git a/test/functional/p2p_ping_ibd.py b/test/functional/p2p_ping_ibd.py\nindex 0774bf8dc8..7f1ec2d4b5 100755\n--- a/test/functional/p2p_ping_ibd.py\n+++ b/test/functional/p2p_ping_ibd.py\n@@ -186,6 +186,9 @@ class PingIBDTest(BitcoinTestFramework):\n                 self.wait_until(lambda: node.getblockcount() == start_height + peer.blocks_served)\n                 assert peer.is_connected\n         assert_greater_than(node.getpeerinfo()[0][\"pingwait\"], TIMEOUT_INTERVAL)\n+        node.bumpmocktime(POST_BLOCK_PONG_GRACE + 1)\n+        peer.sync_with_ping()\n+        assert peer.is_connected\n\n         self.log.info(\"Serve the remaining blocks, so that nothing is in flight anymore\")\n         while peer.blocks_served < NUM_DOWNLOAD_BLOCKS:\n```\n\nThis way you're checking that we're not disconnecting once the timeout elapsed and the `POST_BLOCK_PONG_GRACE` period passed if there are still blocks in flight.\n\nIf you don't add this check, you can mutate the original code like this, and the test will still pass:\n\n```diff\ndiff --git a/src/net_processing.cpp b/src/net_processing.cpp\nindex c339b471c5..734a41796d 100644\n--- a/src/net_processing.cpp\n+++ b/src/net_processing.cpp\n@@ -6441,7 +6441,7 @@ bool PeerManagerImpl::SendMessages(CNode& node)\n         // If the peer failed to answer our ping in time, disconnect due to timeout.\n         // Skip the check while it is serving us blocks and let the block download timeout above govern\n         // instead. Once the last new block was received, give the peer a grace period to answer the ping.\n-        if (state.vBlocksInFlight.empty() &&\n+        if (/*state.vBlocksInFlight.empty() &&*/\n             now > NodeSeconds{node.m_last_block_time.load()} + POST_BLOCK_PONG_GRACE &&\n             m_connman.ShouldRunInactivityChecks(node, now) &&\n             peer.m_ping_nonce_sent &&\n```"
  },
  {
   "t": "2026-09-11T16:08:45Z",
   "kind": "review",
   "who": "danielabrozzoni",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "c551ff40a4f924eb42680e3eeef39d47d889e55c",
   "text": "Thanks for dealing with my AI-assisted review :) I only left an additional comment, I think the test can be made slightly more robust, by checking that the timeout and post block pong grace are only enforced if there are no blocks in flight, which kills one mutant."
  },
  {
   "t": "2026-09-16T13:59:38Z",
   "kind": "force_push",
   "who": "mzumsande",
   "commit": "31280333dd4f9ef2ec818b1a9d25daaeb502659e"
  },
  {
   "t": "2026-09-16T14:00:09Z",
   "kind": "review_comment",
   "who": "mzumsande",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_ping_ibd.py",
   "commit": "31280333dd4f9ef2ec818b1a9d25daaeb502659e",
   "in_reply_to": 3991013479,
   "text": "good suggestion - done!"
  }
 ],
 "labels_log": [
  {
   "t": "2026-08-25T15:59:34Z",
   "action": "labeled",
   "label": "P2P",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-25T16:06:04Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-25T17:25:20Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  }
 ],
 "state_log": [],
 "text_chars": 10891,
 "text_tokens_estimate": 2722,
 "changed_paths": [
  "src/net_processing.cpp",
  "test/functional/p2p_ping_ibd.py",
  "test/functional/test_runner.py"
 ],
 "files": [
  {
   "path": "src/net_processing.cpp",
   "add": 18,
   "del": 18
  },
  {
   "path": "test/functional/p2p_ping_ibd.py",
   "add": 219,
   "del": 0
  },
  {
   "path": "test/functional/test_runner.py",
   "add": 1,
   "del": 0
  }
 ],
 "test_lines": 220,
 "git": {
  "head": "31280333dd4f9ef2ec818b1a9d25daaeb502659e",
  "head_matches_backup": true,
  "base": "11090c8bb359f894ef7d97b65aff52fe8191aec1",
  "commits": [
   {
    "sha": "56c194e655",
    "subject": "test: add functional test for pings during IBD",
    "files": 2,
    "add": 202,
    "del": 0
   },
   {
    "sha": "95dcf8f10d",
    "subject": "p2p: move ping timeout check into SendMessages",
    "files": 1,
    "add": 12,
    "del": 18
   },
   {
    "sha": "31280333dd",
    "subject": "p2p: Don't apply ping timeout while downloading blocks",
    "files": 2,
    "add": 37,
    "del": 13
   }
  ],
  "patch_truncated": false
 },
 "input_hash": "c6ff63341a3fd47a",
 "extracted_at": "2026-09-17T16:15:31+00:00"
}