{
 "number": 34486,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/34486",
 "title": "net: Reduce local network activity when networkactive=0",
 "author": "willcl-ark",
 "author_association": "MEMBER",
 "created_at": "2026-02-02T17:46:47Z",
 "updated_at": "2026-09-17T06:06:49Z",
 "age_days": 226,
 "draft": false,
 "labels": [
  "P2P",
  "Needs rebase"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "48f1321a603f565b8264bb2adb9d0bb4c54a68c6",
 "head_ref": "respect-networkactive",
 "head_repo": "willcl-ark/bitcoin",
 "head_history": [
  {
   "t": "2026-02-03T09:26:25Z",
   "sha": "745aed37224102888332823aadc6d766e701aa0b"
  },
  {
   "t": "2026-03-16T08:46:47Z",
   "sha": "804cb1a969ae2adb44bdb094578b684f8527bca2"
  },
  {
   "t": "2026-03-24T14:11:23Z",
   "sha": "98ddbb69994031aede3d6670a4c852ce3c463caf"
  },
  {
   "t": "2026-06-02T11:05:32Z",
   "sha": "e5ed5f279b471c533ea94140aadcb447c480f473"
  },
  {
   "t": "2026-07-20T11:15:49Z",
   "sha": "d31aaef595d85f7017f87361c500b5ae66d2ea02"
  },
  {
   "t": "2026-08-17T08:43:07Z",
   "sha": "48f1321a603f565b8264bb2adb9d0bb4c54a68c6"
  }
 ],
 "additions": 222,
 "deletions": 8,
 "changed_files": 12,
 "commit_count": 5,
 "size_bucket": "M",
 "mergeable_state": "dirty",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "concept_ack": [
     {
      "login": "sedited",
      "url": "https://github.com/bitcoin/bitcoin/pull/34486#issuecomment-3836747059"
     },
     {
      "login": "fanquake",
      "url": "https://github.com/bitcoin/bitcoin/pull/34486#issuecomment-3840365105"
     },
     {
      "login": "Jhackman2019",
      "url": "https://github.com/bitcoin/bitcoin/pull/34486#issuecomment-3863485711"
     }
    ],
    "approach_ack": [
     {
      "login": "winterrdog",
      "url": "https://github.com/bitcoin/bitcoin/pull/34486#pullrequestreview-4960228824"
     }
    ],
    "stale_ack": [
     {
      "login": "brunoerg",
      "url": "https://github.com/bitcoin/bitcoin/pull/34486#pullrequestreview-4694751628"
     }
    ]
   },
   "conflicts": [
    {
     "number": 36260,
     "title": "torcontrol: Use reconnect backoff after dropped connections",
     "author": "fjahr"
    },
    {
     "number": 36170,
     "title": "net: require a dedicated bind for automatic Tor",
     "author": "l0rinc"
    },
    {
     "number": 36142,
     "title": "net: validate Tor onion service replies and cached keys",
     "author": "l0rinc"
    },
    {
     "number": 35292,
     "title": "test: Add coverage for Tor control `HASHEDPASSWORD` authentication",
     "author": "winterrdog"
    },
    {
     "number": 34534,
     "title": "rpc: Manual prune lock management (Take 2)",
     "author": "fjahr"
    },
    {
     "number": 34213,
     "title": "net: preserve anchors when network is disabled",
     "author": "brunoerg"
    }
   ]
  }
 },
 "acks_parsed": {
  "sedited": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-02-02T17:50:21Z",
   "stale": false
  },
  "fanquake": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-02-03T10:10:44Z",
   "stale": false
  },
  "brunoerg": {
   "kind": "ack",
   "hash": "e5ed5f279b471c533ea94140aadcb447c480f473",
   "t": "2026-07-14T13:31:16Z",
   "stale": true
  },
  "winterrdog": {
   "kind": "approach_ack",
   "hash": "48f1321a603f565b8264bb2adb9d0bb4c54a68c6",
   "t": "2026-08-18T11:17:14Z",
   "stale": false
  }
 },
 "acks_tally": {
  "ack": 0,
  "stale_ack": 1,
  "concept_ack": 2,
  "approach_ack": 1,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 1,
  "changes_requested": 0,
  "distinct_reviewers": [
   "Jhackman2019",
   "bradleystachurski",
   "brunoerg",
   "fanquake",
   "sedited",
   "winterrdog"
  ]
 },
 "signals": {
  "needs_rebase": true,
  "ci_failed": false,
  "mergeable_state": "dirty",
  "last_author_activity": "2026-08-17T08:43:07Z",
  "last_reviewer_activity": "2026-08-18T11:17:14Z",
  "last_reviewer": "winterrdog",
  "author_silent_days": 31,
  "waiting_on_author_days": 30,
  "days_since_update": 0
 },
 "refs": {
  "mentioned": [
   34158,
   34190,
   34467
  ],
  "depends_on": [],
  "fixes": [
   34190
  ],
  "linked_issues": [
   {
    "number": 34190,
    "type": "issue",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "bitcoind shouldn't perform network activity if the option networkactive=0, e.g. [addrman] Selected IP:PORT from tried"
   }
  ],
  "references": [
   {
    "number": 34190,
    "type": "issue",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "bitcoind shouldn't perform network activity if the option networkactive=0, e.g. [addrman] Selected IP:PORT from tried"
   },
   {
    "number": 34158,
    "type": "pull",
    "state": "closed",
    "merged": true,
    "merged_at": "2026-04-08",
    "title": "torcontrol: Remove libevent usage"
   },
   {
    "number": 34467,
    "type": "pull",
    "state": "closed",
    "merged": false,
    "merged_at": null,
    "title": "net: don't perform network activity when networkactive=0"
   }
  ],
  "conflicts": [
   36260,
   36170,
   36142,
   35292,
   34534,
   34213
  ]
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [
  "src/torcontrol.cpp",
  "test/functional/feature_mapport.py"
 ],
 "body": "Fixes #34190\n\nWhen networkactive=0 is set, NAT-PMP port mapping and Tor control connections still run in the background, mapping ports and logging retry attempts despite the node being \"inactive.\"\n\nThis wires both subsystems to CConnman::SetNetworkActive so they start and stop with the network state:\n\n- mapport: `EnableMapPort` is injected as a callback via CConnman::Options. SetNetworkActive and SetMapPortEnabled both gate on network state.\n\n- torcontrol: `TorController` is injected similarly through a CConnman::Options callback. The controller thread stays alive while networkactive=0, but idles without connecting or reconnecting to the Tor control port. Re-enabling the network wakes the controller promptly and resets reconnect backoff.",
 "commits": [
  {
   "sha": "1bf49e54758853cb9a50fe3f25e375b31dd774af",
   "date": "2026-06-02T10:57:40Z",
   "message": "net: wire mapport lifecycle to CConnman\n\nWhen -networkactive=0 is set, NAT-PMP port mapping was still running and\nattempting gateway queries. Move mapport lifecycle management into\nCConnman, which already owns SetNetworkActive, so it can control mapport\nbased on both the -natpmp setting and network state.\n\nEnableMapPort is injected as a callback via CConnman::Options to avoid a\ncircular dependency between net and mapport."
  },
  {
   "sha": "bfce4f989bd343135754c8b7ce39b3fbc6b2289b",
   "date": "2026-07-20T09:55:23Z",
   "message": "test: add test for mapport networkactive\n\nAdd functional test to verify that PCP/NAT-PMP port mapping respects the\nnetworkactive state:\n\n- Does not run when started with -networkactive=0\n- Starts when network is activated via setnetworkactive RPC\n- Stops when network is deactivated\n- Resumes when network is reactivated"
  },
  {
   "sha": "c0b6b23f192c6d46ead00e1d60fc59c919e8a96a",
   "date": "2026-08-17T08:13:13Z",
   "message": "net: run tor control based on networkactive\n\nWire tor control lifecycle to CConnman on top of the libevent-free tor\ncontroller being introduced in upstream/pr/34158.\n\nHave CConnman propagate networkactive changes to the node-owned\nTorController and make the controller thread idle while networking is\ndisabled instead of attempting Tor control connections or reconnects."
  },
  {
   "sha": "ca870adb70bf00719d9b0fa1c3e32cb6a146499d",
   "date": "2026-08-17T08:13:14Z",
   "message": "test: add torcontrol networkactive coverage\n\nAdd functional coverage for torcontrol when networkactive is disabled at startup and toggled back on later.\n\nAlso clear the mock server connection handle on close so the disconnect assertion is reliable."
  },
  {
   "sha": "48f1321a603f565b8264bb2adb9d0bb4c54a68c6",
   "date": "2026-08-17T08:13:14Z",
   "message": "test: check tor control reconnection in p2p_private_broadcast\n\nVerify that the tor controller attempts reconnection to the control port\nby asserting the retry log message when started with an unreachable\n`-torcontrol` address.\n\nCo-authored-by: brunoerg <brunoerg@users.noreply.github.com>"
  }
 ],
 "timeline": [
  {
   "t": "2026-02-02T17:47:42Z",
   "kind": "comment",
   "who": "willcl-ark",
   "assoc": "MEMBER",
   "text": "This is an alternative to #34467. The key difference is that #34467 gates mapport and tor control at init time with if (networkactive) guards, which means those subsystems are permanently disabled for the lifetime of the process. `setnetworkactive true` via RPC won't start them. Boot-time and runtime behavior would now differ.\n\nThis PR instead wires both subsystems into CConnman::SetNetworkActive so they follow the network state through the full lifecycle: starting with -networkactive=0 and later calling setnetworkactive true works as expected.\n\nI'm unsure how this will conflict with the work currently being done to remove libevent."
  },
  {
   "t": "2026-02-02T17:50:21Z",
   "kind": "comment",
   "who": "sedited",
   "assoc": "MEMBER",
   "text": "Concept ACK"
  },
  {
   "t": "2026-02-02T21:42:28Z",
   "kind": "review",
   "who": "bradleystachurski",
   "assoc": "NONE",
   "state": "COMMENTED",
   "commit": "35da4e03b18b7ef5eb23e1b038f5755fe37825a1",
   "text": "Reviewed 35da4e03b18b7ef5eb23e1b038f5755fe37825a1\n\nTested on Linux: started with `-networkactive=0 -natpmp=1 -debug=net -debug=tor`, confirmed no portmap/tor activity at startup, toggled via `setnetworkactive`, confirmed mapport thread start/stop and tor connection attempts follow network state.\n\nVerified `feature_mapport.py` fails when the `m_mapport` callback is removed from `SetNetworkActive`.\n\nnit: `TorController::SetNetworkActive` doesn't reset `reconnect_timeout`, so re-enabling network after backoff has grown continues from the stale value. Verified this fixes it:\n\n```diff\ndiff --git a/src/torcontrol.cpp b/src/torcontrol.cpp\nindex d5aeb55a72..934499d72b 100644\n--- a/src/torcontrol.cpp\n+++ b/src/torcontrol.cpp\n@@ -677,6 +677,8 @@ void TorController::SetNetworkActive(bool set_active)\n         // Disconnect if currently connected\n         conn.Disconnect();\n     }\n+    // Reset backoff when re-enabling network\n+    reconnect_timeout = RECONNECT_TIMEOUT_START;\n     // Connect handles both cases: connects if active, reschedules if inactive\n     Connect();\n }\n```"
  },
  {
   "t": "2026-02-03T09:26:25Z",
   "kind": "force_push",
   "who": "willcl-ark",
   "commit": "745aed37224102888332823aadc6d766e701aa0b"
  },
  {
   "t": "2026-02-03T09:28:28Z",
   "kind": "comment",
   "who": "willcl-ark",
   "assoc": "MEMBER",
   "text": "Thanks @bradleystachurski that's a nice suggestion which I've taken in 745aed37224102888332823aadc6d766e701aa0b\n\nI also reworked the `if` logic slightly to make the flow of this function clearer (and not always call `Connect()`)."
  },
  {
   "t": "2026-02-03T10:10:44Z",
   "kind": "comment",
   "who": "fanquake",
   "assoc": "MEMBER",
   "text": "Concept ACK"
  },
  {
   "t": "2026-02-03T10:25:08Z",
   "kind": "comment",
   "who": "willcl-ark",
   "assoc": "MEMBER",
   "text": "I will address the LLM linter suggestion if I push again."
  },
  {
   "t": "2026-02-07T03:37:52Z",
   "kind": "comment",
   "who": "Jhackman2019",
   "assoc": "NONE",
   "text": "Tested this on my Pi 5 (ARM64, Debian Bookworm). Builds clean, all the networking-related tests pass:\n\n```\nfeature_mapport.py                  PASSED\nfeature_proxy.py                    PASSED\np2p_addr_relay.py                   PASSED\np2p_disconnect_ban.py --v1transport PASSED\np2p_disconnect_ban.py --v2transport PASSED\np2p_dns_seeds.py                    PASSED\n```\n\nHad to clear my `test/cache` first since I had a stale cache from an autotools build \u2014 after that everything was smooth.\n\nConcept ACK"
  },
  {
   "t": "2026-02-17T13:47:18Z",
   "kind": "comment",
   "who": "brunoerg",
   "assoc": "MEMBER",
   "text": "Concept ACK"
  },
  {
   "t": "2026-02-17T17:10:50Z",
   "kind": "review",
   "who": "brunoerg",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "745aed37224102888332823aadc6d766e701aa0b",
   "text": "tested up to 396b56af0ac261863b076ab8d2d02a99f6e03f3a: I checked that portmap activity follows network activity."
  },
  {
   "t": "2026-02-17T17:18:48Z",
   "kind": "review_comment",
   "who": "brunoerg",
   "assoc": "MEMBER",
   "path": "src/torcontrol.cpp",
   "commit": "48f1321a603f565b8264bb2adb9d0bb4c54a68c6",
   "in_reply_to": null,
   "text": "Is there any way to test this condition? I've tried several scenarios manually, but haven't been able to achieve it.\n\nHowever, checking that it is not connected to any Tor control port and then will retry was easier, could also check it on a functional test, e.g:\n\n```diff\ndiff --git a/test/functional/p2p_private_broadcast.py b/test/functional/p2p_private_broadcast.py\nindex 4943841790..b163020e85 100755\n--- a/test/functional/p2p_private_broadcast.py\n+++ b/test/functional/p2p_private_broadcast.py\n@@ -433,9 +433,11 @@ class P2PPrivateBroadcast(BitcoinTestFramework):\n             # the RPC should throw.\n             \"-torcontrol=127.0.0.1:1\",\n             \"-listenonion\",\n+            \"-debug=tor\",\n         ])\n-        assert_raises_rpc_error(-1, \"none of the Tor or I2P networks is reachable\",\n-                                tx_originator.sendrawtransaction, hexstring=txs[0][\"hex\"], maxfeerate=0.1)\n+        with tx_originator.assert_debug_log(['Not connected to Tor control port'], timeout=5):\n+            assert_raises_rpc_error(-1, \"none of the Tor or I2P networks is reachable\",\n+                                    tx_originator.sendrawtransaction, hexstring=txs[0][\"hex\"], maxfeerate=0.1)\n\n```"
  },
  {
   "t": "2026-02-19T16:20:27Z",
   "kind": "review_comment",
   "who": "willcl-ark",
   "assoc": "MEMBER",
   "path": "src/torcontrol.cpp",
   "commit": "48f1321a603f565b8264bb2adb9d0bb4c54a68c6",
   "in_reply_to": 2818143201,
   "text": "Thanksf or the review!\n\nYes I think this is hard to reach in a functional test, as `conn.Disconnect()` frees the `bufferevent` without firing the callback, so this guard only triggers if a libevent disconnect event was already queued when `SetNetworkActive(false)` runs.\n\nIt's mainly intended to be defensive against an event-loop ordering edge case, and even if it wasn't here, `Connect()` has its own `m_network_active check`, but this guard also prevents a misleading \"retrying\" log message.\n\nYour p2p_private_broadcast.py change seems like good coverage for the reworked connection initiation path. Happy to include it."
  },
  {
   "t": "2026-03-15T21:03:06Z",
   "kind": "comment",
   "who": "willcl-ark",
   "assoc": "MEMBER",
   "text": "Drafting this to rework/rebase on https://github.com/bitcoin/bitcoin/pull/34158, and not undo libevent removal."
  },
  {
   "t": "2026-03-16T08:46:47Z",
   "kind": "force_push",
   "who": "willcl-ark",
   "commit": "804cb1a969ae2adb44bdb094578b684f8527bca2"
  },
  {
   "t": "2026-03-24T14:11:23Z",
   "kind": "force_push",
   "who": "willcl-ark",
   "commit": "98ddbb69994031aede3d6670a4c852ce3c463caf"
  },
  {
   "t": "2026-06-02T11:05:32Z",
   "kind": "force_push",
   "who": "willcl-ark",
   "commit": "e5ed5f279b471c533ea94140aadcb447c480f473"
  },
  {
   "t": "2026-06-02T12:25:03Z",
   "kind": "comment",
   "who": "willcl-ark",
   "assoc": "MEMBER",
   "text": "Sorry I forgot about this while #34158 was in progress. Rebased and undrafted."
  },
  {
   "t": "2026-07-13T18:39:02Z",
   "kind": "review",
   "who": "brunoerg",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "e5ed5f279b471c533ea94140aadcb447c480f473",
   "text": "I manually tested it (e5ed5f279b471c533ea94140aadcb447c480f473), worked fine. Unfortunately, our functional tests are somewhat limited for scenarios like these. So we end up relying on logs.\n\nI've created (vibe-coded) a docker environment with Tor, a minimal real NAT-PMP/PCP gateway and a script to assert basically the same that the functional test does but based on the actual captured packets, worked fine as well (happy to share).\n\n```sh\n \u2714 Image networkactive_docker-natpmp_gw       Built                                                                                                                                                                                         7.5s\n \u2714 Image networkactive_docker-tor             Built                                                                                                                                                                                        12.7s\n \u2714 Image networkactive_docker-node            Built                                                                                                                                                                                       711.5s\n \u2714 Network networkactive_docker_netactive_net Created                                                                                                                                                                                       0.0s\n \u2714 Container networkactive_docker-tor-1       Created                                                                                                                                                                                       0.7s\n \u2714 Container networkactive_docker-natpmp_gw-1 Created                                                                                                                                                                                       0.7s\n \u2714 Container networkactive_docker-node-1      Created                                                                                                                                                                                       0.0s\n== Waiting for bitcoind RPC ==\n\n== Phase 0: startup state ==\n  PASS: node starts with networkactive=false (-networkactive=0)\n\n== Phase 1: idle (networkactive=0) -- expect ZERO packets on the wire ==\n  PASS: zero packets captured on eth0 while networkactive=0 (any protocol)\n  PASS: NAT-PMP/PCP responder received no requests while networkactive=0\n  PASS: tor daemon has no established connection from the node while networkactive=0\n\n== Phase 2: setnetworkactive true -- expect REAL NAT-PMP/PCP + tor traffic ==\n  PASS: getnetworkinfo reports networkactive=true\n  PASS: real NAT-PMP/PCP UDP packets seen on the wire (port 5351): 6\n  PASS: real TCP packets seen on the wire to tor control port (9051): 15\n  PASS: NAT-PMP/PCP responder actually parsed a MAP request and granted a mapping\n  PASS: tor daemon shows a real ESTABLISHED connection from the node\n\n== Phase 3: setnetworkactive false -- expect traffic to STOP and tor connection to CLOSE ==\n  PASS: getnetworkinfo reports networkactive=false\n  PASS: zero new packets on eth0 after deactivation\n  PASS: tor daemon's connection from the node was actually torn down\n\n== Phase 4: setnetworkactive true again -- expect traffic to RESUME ==\n  PASS: NAT-PMP/PCP traffic resumed after reactivation: 6 packets\n  PASS: tor control connection re-established after reactivation\n\n== Summary: 14 passed, 0 failed ==\n```\n\nI intend to review the code by tomorrow."
  },
  {
   "t": "2026-07-14T13:27:13Z",
   "kind": "review_comment",
   "who": "brunoerg",
   "assoc": "MEMBER",
   "path": "test/functional/feature_mapport.py",
   "commit": "48f1321a603f565b8264bb2adb9d0bb4c54a68c6",
   "in_reply_to": null,
   "text": "aef9e741697d041137b594682f77c06f7d8cd821: nit: Since \"portmap:\" logs can also appear during the startup, we could move the `self.restart_node` to `assert_debug_log` as well."
  },
  {
   "t": "2026-07-14T13:31:16Z",
   "kind": "review",
   "who": "brunoerg",
   "assoc": "MEMBER",
   "state": "APPROVED",
   "commit": "e5ed5f279b471c533ea94140aadcb447c480f473",
   "text": "ACK e5ed5f279b471c533ea94140aadcb447c480f473\n\nedit: I didn't get any, but timeouts+sleeps on functional tests are a good territory to be flaky."
  },
  {
   "t": "2026-07-20T11:15:49Z",
   "kind": "force_push",
   "who": "willcl-ark",
   "commit": "d31aaef595d85f7017f87361c500b5ae66d2ea02"
  },
  {
   "t": "2026-07-20T11:17:29Z",
   "kind": "comment",
   "who": "willcl-ark",
   "assoc": "MEMBER",
   "text": "Thanks for your review bruno. I've pushed a small update to address the points you raised:\n\n- `restart_node() moved inside assert_debug_log` moved inside `assert_debug_log`\n- drop the `time.sleep()` from feature_torcontrol.py and require waiting on \"Retrying in 1.0 seconds\""
  },
  {
   "t": "2026-08-15T19:47:48Z",
   "kind": "review_comment",
   "who": "winterrdog",
   "assoc": "CONTRIBUTOR",
   "path": "src/torcontrol.cpp",
   "commit": "48f1321a603f565b8264bb2adb9d0bb4c54a68c6",
   "in_reply_to": null,
   "text": "i noticed `m_network_active_change_count` is checked both at the top of the loop and right after `m_interrupt.sleep_for()`\n\nsince `sleep_for()` handles thread signaling and the next loop iteration re-evaluates the atomic load anyway, is there a subtle memory ordering edge case i am missing where omitting the second check would be problematic ?"
  },
  {
   "t": "2026-08-17T08:13:01Z",
   "kind": "review_comment",
   "who": "willcl-ark",
   "assoc": "MEMBER",
   "path": "src/torcontrol.cpp",
   "commit": "48f1321a603f565b8264bb2adb9d0bb4c54a68c6",
   "in_reply_to": 3790126467,
   "text": "Yep, I agree with you that this is redundant. If the counter changes during sleep it's checked on the next iteration before sleeping again.\n\nWill push an update when the CI is not being problematic."
  },
  {
   "t": "2026-08-17T08:43:07Z",
   "kind": "force_push",
   "who": "willcl-ark",
   "commit": "48f1321a603f565b8264bb2adb9d0bb4c54a68c6"
  },
  {
   "t": "2026-08-18T10:58:38Z",
   "kind": "review_comment",
   "who": "winterrdog",
   "assoc": "CONTRIBUTOR",
   "path": "src/torcontrol.cpp",
   "commit": "c0b6b23f192c6d46ead00e1d60fc59c919e8a96a",
   "in_reply_to": null,
   "text": "premise: _backoff keeps growing after a quick off/on during sleep_\n\nthere is a subtle gap in how `ThreadControl` handles backoff timeouts during rapid state toggles i.e. something that could happen when automated scripts or tests issue `setnetworkactive false` followed immediately by `setnetworkactive true`\n\nif the node is currently in a backoff sleep (say, waiting 30 seconds after several failed Tor connection attempts) and a script toggles the network off and back on within that single sleep window, `SleepWithNetworkPolling` wakes up early due to the change count. however, because the script already flipped network activity back to `true`, the live `if (!m_network_active)` check evaluates to `false`. execution then falls through directly into growing the backoff delay (`m_reconnect_timeout *= RECONNECT_TIMEOUT_EXP`), punishing the node with a longer reconnect delay instead of resetting it back to `1.0s` despite a fresh reactivation occurring\n\nthe test below reproduces this by doing the off/on calls back-to-back while the node is inside a reconnect sleep. the next retry incorrectly gets the grown delay (`1.5s`) instead of starting again at `1.0s`:\n\n    diff\n\n```diff\ndiff --git a/test/functional/feature_torcontrol.py b/test/functional/feature_torcontrol.py\nindex 7a6fc4e6c0..053fc65b3f 100755\n--- a/test/functional/feature_torcontrol.py\n+++ b/test/functional/feature_torcontrol.py\n@@ -313,6 +313,40 @@ class TorControlTest(BitcoinTestFramework):\n         ], timeout=2):\n             node.setnetworkactive(state=True)\n\n+    def test_networkactive_race_during_backoff(self):\n+        self.log.info(\"Test that a rapid off->on cycle during a reconnect backoff sleep still resets the backoff\")\n+\n+        node = self.nodes[0]\n+\n+        # nothing listens on 127.0.0.1:1, so every connection attempt fails\n+        # immediately thus the retry/backoff cycle is predictable: the first\n+        # retry is always logged at RECONNECT_TIMEOUT_START (1.0s)\n+        with node.assert_debug_log(expected_msgs=[\"Retrying in 1.0 seconds\"], timeout=10):\n+            self.restart_node(0, extra_args=[\n+                \"-torcontrol=127.0.0.1:1\",\n+                \"-listenonion=1\",\n+                \"-debug=tor\",\n+            ])\n+\n+        # we are now inside that first ~1.0s backoff sleep. toggle the network\n+        # off then immediately back on, as quick as possible, so the whole\n+        # round trip lands inside this single sleep window rather than\n+        # spanning two separate ThreadControl loop iterations\n+        node.setnetworkactive(state=False)\n+        node.setnetworkactive(state=True)\n+\n+        # a reactivation should always reset the backoff to\n+        # RECONNECT_TIMEOUT_START, regardless of whether the deactivate/\n+        # reactivate pair happened between loop iterations or inside one\n+        # backoff sleep. so the *next* retry should again log 1.0s, not a\n+        # grown value (i.e. 1.5s) carried over from before the toggle\n+        with node.assert_debug_log(\n+            expected_msgs=[\"Retrying in 1.0 seconds\"],\n+            unexpected_msgs=[\"Retrying in 1.5 seconds\"],\n+            timeout=3,\n+        ):\n+            pass\n+\n     def run_test(self):\n         self.test_basic()\n         self.test_partial_data()\n@@ -321,6 +355,7 @@ class TorControlTest(BitcoinTestFramework):\n         self.test_overmany_lines()\n         self.test_networkactive()\n         self.test_networkactive_reactivation_resets_backoff()\n+        self.test_networkactive_race_during_backoff()\n\n if __name__ == '__main__':\n```\n\n### potential fix\nthe fix is to check whether the network-active change count changed during the sleep itself. if it did and the network is active again, treat that as a fresh reactivation & reset the backoff:\n\n    diff\n\n```diff\ndiff --git a/src/torcontrol.cpp b/src/torcontrol.cpp\nindex dfded42746..5cfaaad515 100644\n--- a/src/torcontrol.cpp\n+++ b/src/torcontrol.cpp\n@@ -447,11 +447,14 @@ void TorController::ThreadControl()\n                 LogDebug(BCLog::TOR, \"Retrying in %.1f seconds\", m_reconnect_timeout.count());\n                 const auto change_count{m_network_active_change_count.load()};\n                 if (!SleepWithNetworkPolling(std::chrono::duration_cast<std::chrono::milliseconds>(m_reconnect_timeout), change_count) && m_interrupt) {\n                     break;\n                 }\n-                if (!m_network_active) {\n+                if (change_count != m_network_active_change_count.load()) {\n+                    if (m_network_active) {\n+                        m_reconnect_timeout = RECONNECT_TIMEOUT_START;\n+                    }\n                     continue;\n                 }\n                 m_reconnect_timeout = std::min(m_reconnect_timeout * RECONNECT_TIMEOUT_EXP, RECONNECT_TIMEOUT_MAX);\n                 continue;\n             }\n```\n\n[quoted text omitted]\nthoughts ?"
  },
  {
   "t": "2026-08-18T11:17:14Z",
   "kind": "review",
   "who": "winterrdog",
   "assoc": "CONTRIBUTOR",
   "state": "COMMENTED",
   "commit": "48f1321a603f565b8264bb2adb9d0bb4c54a68c6",
   "text": "approach ACK 48f1321a603f565b8264bb2adb9d0bb4c54a68c6"
  }
 ],
 "labels_log": [
  {
   "t": "2026-02-02T17:46:51Z",
   "action": "labeled",
   "label": "P2P",
   "who": "DrahtBot"
  },
  {
   "t": "2026-03-23T08:21:28Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-03-24T14:56:41Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-03-24T15:10:50Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-03-26T19:45:44Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-06-02T11:18:26Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-06-02T12:49:15Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-17T09:09:36Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-17T12:31:39Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-16T11:28:13Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  }
 ],
 "state_log": [
  {
   "t": "2026-03-15T21:02:38Z",
   "kind": "convert_to_draft",
   "who": "willcl-ark"
  },
  {
   "t": "2026-06-02T12:25:04Z",
   "kind": "ready_for_review",
   "who": "willcl-ark"
  },
  {
   "t": "2026-08-17T09:09:18Z",
   "kind": "closed",
   "who": "fanquake"
  },
  {
   "t": "2026-08-17T09:09:24Z",
   "kind": "reopened",
   "who": "fanquake"
  }
 ],
 "text_chars": 16735,
 "text_tokens_estimate": 4183,
 "changed_paths": [],
 "files": [],
 "test_lines": null,
 "git": null,
 "input_hash": "2527ee0b5bf0cd0d",
 "extracted_at": "2026-09-17T16:15:31+00:00"
}