{
 "number": 36372,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/36372",
 "title": "p2p: replace full outbound peers that don't support tx relay (take 2)",
 "author": "fjahr",
 "author_association": "MEMBER",
 "created_at": "2026-09-28T17:19:39Z",
 "updated_at": "2026-10-31T21:43:30Z",
 "description_hash": "c7b9340ee2af1291",
 "age_days": 34,
 "draft": false,
 "labels": [
  "P2P"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "07e58de16805e66fc2bccc842c1c2edc4ee1f897",
 "head_ref": "2026-09-pr28538-pickup",
 "head_repo": "fjahr/bitcoin",
 "head_history": [
  {
   "t": "2026-09-28T19:06:25Z",
   "sha": "07e58de16805e66fc2bccc842c1c2edc4ee1f897"
  }
 ],
 "additions": 196,
 "deletions": 17,
 "changed_files": 8,
 "commit_count": 4,
 "size_bucket": "M",
 "mergeable_state": "clean",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "ack": [
     {
      "login": "Bicaru20",
      "url": "https://github.com/bitcoin/bitcoin/pull/36372#pullrequestreview-5502202695"
     }
    ],
    "concept_ack": [
     {
      "login": "mzumsande",
      "url": "https://github.com/bitcoin/bitcoin/pull/36372#pullrequestreview-5344753685"
     }
    ]
   },
   "conflicts": []
  }
 },
 "acks_parsed": {
  "mzumsande": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-09-28T21:16:20Z",
   "stale": false
  }
 },
 "acks_tally": {
  "ack": 0,
  "stale_ack": 0,
  "concept_ack": 1,
  "approach_ack": 0,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 0,
  "changes_requested": 0,
  "distinct_reviewers": [
   "Bicaru20",
   "fametrano",
   "instagibbs",
   "mzumsande"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "clean",
  "last_author_activity": "2026-09-28T19:06:25Z",
  "last_reviewer_activity": "2026-10-13T12:14:09Z",
  "last_reviewer": "Bicaru20",
  "author_silent_days": 34,
  "waiting_on_author_days": 19,
  "days_since_update": 1
 },
 "refs": {
  "mentioned": [
   16418,
   28371,
   28538
  ],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [
   {
    "number": 28538,
    "type": "pull",
    "state": "closed",
    "merged": false,
    "merged_at": null,
    "title": "p2p: attempt to fill full outbound connection slots with peers that support tx relay"
   },
   {
    "number": 16418,
    "type": "issue",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "Ensure we have sufficient transaction-relay peers"
   },
   {
    "number": 28371,
    "type": "issue",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "Seek more/different peers when ours all have too high feefilter"
   }
  ],
  "conflicts": []
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [
  "src/net.cpp",
  "src/test/denialofservice_tests.cpp"
 ],
 "body": "Picks up #28538\n\nOutbound-full-relay slots should be used by peers that actually relay transactions, besides #28538 see #16418 and #28371 for the motivation. So try to replace peers that are not relaying txs with peers that do.\n\nThe approach differs from the original PR in a few details in response to some of the review comments there:\n\n- A replacement is connected before the peer is evicted. The original PR disconnected a non-tx-relaying peer once all full outbound slots were filled. Reviewers preferred having a replacement available first (https://github.com/bitcoin/bitcoin/pull/28538#issuecomment-1883481846). Now an extra outbound-full-relay connection is requested, like the extra network-specific peers, and the existing eviction of extra outbound peers prefers peers that don't relay txs.\n- Peers not relaying txs don't get `m_protect`. Otherwise they could stay forever (https://github.com/bitcoin/bitcoin/pull/28538#discussion_r1443336121), and ignoring the protection was rejected in the same thread (https://github.com/bitcoin/bitcoin/pull/28538#discussion_r1443312604).\n- An only peer on a network is not replaced. Otherwise the network-specific extra connections and this logic would keep replacing each other (https://github.com/bitcoin/bitcoin/pull/28538#issuecomment-1883481846). This also covers the `-maxconnections=1` case (https://github.com/bitcoin/bitcoin/pull/28538#discussion_r1417178196).\n\nNothing is changed in `-blocksonly` mode or during IBD. The rename commit and the functional test are taken from the original PR with some changes to match the new behavior.",
 "commits": [
  {
   "sha": "7ed4cbe6c65cb9c9825cd61bd1f1c32e59f9dccf",
   "date": "2026-09-28T13:04:36Z",
   "message": "refactor: rename ForEachNode and remove duplicate checks\n\nRename ForEachNode to ForEachFullyConnectedNode because it\nhas an inbuilt check that only executes the passed function\nfor fully connected nodes (i.e. if fSuccessfullyConnected is set\nand fDisconnect isn't).\n\nThis means callers don't need to need to worry about that, so remove\nthose duplicate checks and related comments."
  },
  {
   "sha": "9798b32508b9aeb716015c469d788a600bbfe9d4",
   "date": "2026-09-28T15:59:04Z",
   "message": "p2p: Don't protect outbound peers not participating in tx relay\n\nCo-authored-by: Martin Zumsande <mzumsande@gmail.com>"
  },
  {
   "sha": "8ac7e7897e96823243555ddd46a445ad50b7895c",
   "date": "2026-09-28T15:59:04Z",
   "message": "p2p: Replace outbound peers not participating in tx relay\n\nCo-authored-by: Martin Zumsande <mzumsande@gmail.com>"
  },
  {
   "sha": "07e58de16805e66fc2bccc842c1c2edc4ee1f897",
   "date": "2026-09-28T19:05:28Z",
   "message": "test: add test for interaction with -blocksonly peers\n\nCo-authored-by: Martin Zumsande <mzumsande@gmail.com>"
  }
 ],
 "timeline": [
  {
   "t": "2026-09-28T19:06:25Z",
   "kind": "force_push",
   "who": "fjahr",
   "commit": "07e58de16805e66fc2bccc842c1c2edc4ee1f897",
   "id": "push:07e58de16805e66fc2bccc842c1c2edc4ee1f897"
  },
  {
   "t": "2026-09-28T21:16:20Z",
   "kind": "review",
   "who": "mzumsande",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "07e58de16805e66fc2bccc842c1c2edc4ee1f897",
   "text": "Concept ACK, thanks for picking this up!",
   "id": "r:5344753685",
   "url": "https://github.com/bitcoin/bitcoin/pull/36372#pullrequestreview-5344753685",
   "hash": "4944587f48853cb2",
   "edited": null
  },
  {
   "t": "2026-09-30T19:46:55Z",
   "kind": "comment",
   "who": "instagibbs",
   "assoc": "MEMBER",
   "text": "Just a conceptual question IIUC: we could be evicting a peer that was giving us useful work for a peer that *claims* to do useful work. Would we want to do more validation of those claims before evicting someone?\n\nedit: do we have evidence this is a real problem? Obviously it would be problematic if nodes quickly started converting to blocksonly, but I don't see that happening fast. Can we crawl the network to see evidence of this?",
   "id": "c:5918463449",
   "url": "https://github.com/bitcoin/bitcoin/pull/36372#issuecomment-5918463449",
   "hash": "6e6ca0fa4ed9d942",
   "edited": "2026-10-01T20:13:16Z"
  },
  {
   "t": "2026-10-02T11:49:40Z",
   "kind": "comment",
   "who": "fametrano",
   "assoc": "NONE",
   "text": "The \"not evicted\" checks in the first and third cases of the new `p2p_blocksonly.py` test can't fail. Each case connects one peer, and a node never evicts or replaces its only full-outbound peer on a network. So replacing peers in `-blocksonly` mode (dropping `!m_opts.ignore_incoming_txs` from `replace_no_tx_relay`) passes every test in the PR. Evicting below the outbound limit (`GetExtraFullOutboundCount() >= 0`) also passes this test, though the existing unit tests catch it. The second case works: disconnecting the peer instead of asking for a replacement fails it.\n\nTwo peers in each case fix this. In the first case, mocktime also has to pass MINIMUM_CONNECT_TIME (30 s), because a newer peer is never evicted. In the blocksonly case, use `-maxconnections=2` and check that \"setting replace no-tx-relay peer=true\" is not logged. With that, both changes fail the test, and it passed 6 of 6 runs on 07e58de. Diff below.\n\nI only looked at the new tests; I have not reviewed the C++ change.\n\nDiff\n\n```diff\n--- a/test/functional/p2p_blocksonly.py\n+++ b/test/functional/p2p_blocksonly.py\n@@ -83,30 +83,41 @@\n         self.log.info(\"Tests with outbound peers that set txrelay to false, as peers in blocksonly mode do\")\n         self.restart_node(0, [\"-noblocksonly\"])\n         self.log.info(\"Check that we sustain full-relay outbound connections to blocksonly peers when not at the full outbound limit\")\n-        blocksonly_peer = self.nodes[0].add_outbound_p2p_connection(P2PInterface(), p2p_idx=1, txrelay=False)\n-        # Trigger CheckForStaleTipAndEvictPeers, which is scheduled every 45s (EXTRA_PEER_CHECK_INTERVAL)\n-        self.nodes[0].mockscheduler(46)\n-        blocksonly_peer.sync_with_ping()\n-        blocksonly_peer.peer_disconnect()\n+        peers = self.add_blocksonly_peers()\n+        self.trigger_eviction_check()\n+        self.check_connected(peers)\n\n         self.log.info(\"Check that we request a replacement instead of evicting full-relay outbound connections to blocksonly peers at the full outbound limit\")\n         # Simulate the situation of being at the max limit of full-outbound connections by setting -maxconnections to a low value\n         self.restart_node(0, [\"-noblocksonly\", \"-maxconnections=2\"])\n-        blocksonly_peer1 = self.nodes[0].add_outbound_p2p_connection(P2PInterface(), p2p_idx=1, txrelay=False)\n-        blocksonly_peer2 = self.nodes[0].add_outbound_p2p_connection(P2PInterface(), p2p_idx=2, txrelay=False)\n+        peers = self.add_blocksonly_peers()\n         with self.nodes[0].assert_debug_log([\"setting replace no-tx-relay peer=true\"]):\n-            self.nodes[0].mockscheduler(46)\n-        blocksonly_peer1.sync_with_ping()\n-        blocksonly_peer2.sync_with_ping()\n-        self.nodes[0].disconnect_p2ps()\n+            self.trigger_eviction_check()\n+        self.check_connected(peers)\n\n         self.log.info(\"Check that we don't evict full-relay outbound connections to blocksonly peers if we are blocksonly ourselves\")\n-        self.restart_node(0, [\"-blocksonly\", \"-maxconnections=1\"])\n-        blocksonly_peer = self.nodes[0].add_outbound_p2p_connection(P2PInterface(), p2p_idx=1, txrelay=False)\n+        self.restart_node(0, [\"-blocksonly\", \"-maxconnections=2\"])\n+        peers = self.add_blocksonly_peers()\n+        with self.nodes[0].assert_debug_log([], unexpected_msgs=[\"setting replace no-tx-relay peer=true\"]):\n+            self.trigger_eviction_check()\n+        self.check_connected(peers)\n+\n+    def add_blocksonly_peers(self):\n+        # Two peers, so that neither is kept only for being the sole peer on its network\n+        return [self.nodes[0].add_outbound_p2p_connection(P2PInterface(), p2p_idx=i, txrelay=False) for i in (1, 2)]\n+\n+    def trigger_eviction_check(self):\n+        # Pass MINIMUM_CONNECT_TIME (30s), so that a chosen peer is not kept for being new\n+        self.nodes[0].setmocktime(int(time.time()) + 31)\n+        # Trigger CheckForStaleTipAndEvictPeers, which is scheduled every 45s (EXTRA_PEER_CHECK_INTERVAL)\n         self.nodes[0].mockscheduler(46)\n-        blocksonly_peer.sync_with_ping()\n-        blocksonly_peer.peer_disconnect()\n\n+    def check_connected(self, peers):\n+        for peer in peers:\n+            peer.sync_with_ping()\n+        self.nodes[0].disconnect_p2ps()\n+        self.nodes[0].setmocktime(0)\n+\n     def blocks_relay_conn_tests(self):\n         self.log.info('Tests with node in normal mode with block-relay-only connections')\n         self.restart_node(0, [\"-noblocksonly\"])  # disables blocks only mode\n```",
   "id": "c:5951741726",
   "url": "https://github.com/bitcoin/bitcoin/pull/36372#issuecomment-5951741726",
   "hash": "4a922bb377d02103",
   "edited": null
  },
  {
   "t": "2026-10-13T10:32:16Z",
   "kind": "review_comment",
   "who": "Bicaru20",
   "assoc": "CONTRIBUTOR",
   "path": "src/net.cpp",
   "commit": "07e58de16805e66fc2bccc842c1c2edc4ee1f897",
   "in_reply_to": null,
   "text": "nit:\n```suggestion\n        LogDebug(BCLog::NET, \"setting replace no-tx-relay peer=%s\\n\", flag ? \"true\" : \"false\");\n```",
   "id": "rc:4257543093",
   "url": "https://github.com/bitcoin/bitcoin/pull/36372#discussion_r4257543093",
   "hash": "275868ee5be477a3",
   "edited": "2026-10-13T12:14:09Z"
  },
  {
   "t": "2026-10-13T11:10:35Z",
   "kind": "review_comment",
   "who": "Bicaru20",
   "assoc": "CONTRIBUTOR",
   "path": "src/test/denialofservice_tests.cpp",
   "commit": "07e58de16805e66fc2bccc842c1c2edc4ee1f897",
   "in_reply_to": null,
   "text": "I think it would be good to add a new test case here to check that a replecemnt is requested even when the candidate with `m_relays_txs=false` cannot be evicted yet because it is younger than MINIMUM_CONNECT_TIME\n\n```cpp\n    vNodes[1]->fDisconnect = false;\n    AddRandomOutboundPeer(id, vNodes, *peerLogic, *connman, ConnectionType::OUTBOUND_FULL_RELAY);\n    vNodes.back()->m_relays_txs = false;\n    AddRandomOutboundPeer(id, vNodes, *peerLogic, *connman, ConnectionType::OUTBOUND_FULL_RELAY);\n    vNodes.back()->m_relays_txs = true;\n    clock += 1s;\n    peerLogic->CheckForStaleTipAndEvictPeers();\n    BOOST_CHECK(connman->GetReplaceNoTxRelayPeer());\n    for (const CNode* node : vNodes) {\n        BOOST_CHECK(!node->fDisconnect);\n    }\n```",
   "id": "rc:4257881028",
   "url": "https://github.com/bitcoin/bitcoin/pull/36372#discussion_r4257881028",
   "hash": "31f5e4fc721082ed",
   "edited": "2026-10-13T12:14:09Z"
  },
  {
   "t": "2026-10-13T12:14:09Z",
   "kind": "review",
   "who": "Bicaru20",
   "assoc": "CONTRIBUTOR",
   "state": "COMMENTED",
   "commit": "07e58de16805e66fc2bccc842c1c2edc4ee1f897",
   "text": "Conecpt ACK 07e58de16805e66fc2bccc842c1c2edc4ee1f897\nI left a suggestion to try to make the tests more robusts\n\nWith the new changes, if we are not in -blocksonly mode or in IBD, our node will be able to evict the nodes that do not relay transaction first. Also, these nodes will not be protected against eviction with the `m_protect` flag. Before evicting one of the nodes, it ensures a new peer has been found.\n\nOne question I have is what happens if we have a node that doesn't relay transactions and another one that does but has been silent for some time. In this case, if I have understood it correclty, it would evict first the peer that doesn't relay the tansactions. Shouldn't we chose the peer that is active over the one that has been silent for some time even if the active peer is not relaying transactions?",
   "id": "r:5502202695",
   "url": "https://github.com/bitcoin/bitcoin/pull/36372#pullrequestreview-5502202695",
   "hash": "2cb724ddf6298969",
   "edited": null
  }
 ],
 "labels_log": [
  {
   "t": "2026-09-28T17:19:44Z",
   "action": "labeled",
   "label": "P2P",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-28T18:23:30Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-28T20:05:19Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  }
 ],
 "state_log": [],
 "text_chars": 9076,
 "text_tokens_estimate": 2269,
 "changed_paths": [
  "src/net.cpp",
  "src/net.h",
  "src/net_processing.cpp",
  "src/test/denialofservice_tests.cpp",
  "src/test/fuzz/connman.cpp",
  "test/functional/p2p_blocksonly.py",
  "test/functional/p2p_outbound_eviction.py",
  "test/functional/test_framework/p2p.py"
 ],
 "files": [
  {
   "path": "src/net.cpp",
   "add": 21,
   "del": 0
  },
  {
   "path": "src/net.h",
   "add": 10,
   "del": 2
  },
  {
   "path": "src/net_processing.cpp",
   "add": 31,
   "del": 11
  },
  {
   "path": "src/test/denialofservice_tests.cpp",
   "add": 52,
   "del": 0
  },
  {
   "path": "src/test/fuzz/connman.cpp",
   "add": 1,
   "del": 1
  },
  {
   "path": "test/functional/p2p_blocksonly.py",
   "add": 29,
   "del": 0
  },
  {
   "path": "test/functional/p2p_outbound_eviction.py",
   "add": 48,
   "del": 0
  },
  {
   "path": "test/functional/test_framework/p2p.py",
   "add": 4,
   "del": 3
  }
 ],
 "test_lines": 138,
 "git": {
  "head": "07e58de16805e66fc2bccc842c1c2edc4ee1f897",
  "head_matches_backup": true,
  "base": "05bc2f53ce0cb239c17dbdd6b261bd2db7d2a940",
  "patch_id": "9351eb56969474cd",
  "commits": [
   {
    "sha": "7ed4cbe6c6",
    "subject": "refactor: rename ForEachNode and remove duplicate checks",
    "files": 3,
    "add": 10,
    "del": 11
   },
   {
    "sha": "9798b32508",
    "subject": "p2p: Don't protect outbound peers not participating in tx relay",
    "files": 3,
    "add": 56,
    "del": 4
   },
   {
    "sha": "8ac7e7897e",
    "subject": "p2p: Replace outbound peers not participating in tx relay",
    "files": 4,
    "add": 101,
    "del": 2
   },
   {
    "sha": "07e58de168",
    "subject": "test: add test for interaction with -blocksonly peers",
    "files": 1,
    "add": 29,
    "del": 0
   }
  ],
  "patch_truncated": false
 },
 "patch_id": "9351eb56969474cd",
 "input_hash": "3fb7e9fe4fb4daeb",
 "extracted_at": "2026-11-02T01:09:49+00:00"
}