{
 "number": 34213,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/34213",
 "title": "net: preserve anchors when network is disabled",
 "author": "brunoerg",
 "author_association": "MEMBER",
 "created_at": "2026-01-06T21:26:09Z",
 "updated_at": "2026-09-15T10:09:41Z",
 "age_days": 253,
 "draft": false,
 "labels": [
  "P2P"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "ddf033054ff66129e9307c7c4661ad20e08727fe",
 "head_ref": "2026-01-net-anchors-networkactive",
 "head_repo": "brunoerg/bitcoin",
 "head_history": [
  {
   "t": "2026-01-06T21:31:12Z",
   "sha": "f82f7c194d0d67357919058aadc51d5a2713f612"
  },
  {
   "t": "2026-01-06T21:56:34Z",
   "sha": "903a37c1e142b4ee6b83fee99735ab69716084ee"
  },
  {
   "t": "2026-03-27T14:37:49Z",
   "sha": "dce37280c3a2cdebf4a97c99b67fba110b2b84f2"
  },
  {
   "t": "2026-04-13T16:42:14Z",
   "sha": "5a674a336e53905a487be3e2ad923bf62684b449"
  },
  {
   "t": "2026-04-13T16:52:05Z",
   "sha": "cca21e44db55ac3e93e4cca044e0eeeb950b800e"
  },
  {
   "t": "2026-04-13T17:03:19Z",
   "sha": "133cfae47d3f9a62e7bf4f21cb9b69061826e1cf"
  },
  {
   "t": "2026-05-05T12:18:40Z",
   "sha": "e619bc53c5033edbc8f4fcb7a25bd9cb0d858190"
  },
  {
   "t": "2026-05-05T12:44:24Z",
   "sha": "4046d0ec7975be5d15c76eb0dc7c78cbbab2584d"
  },
  {
   "t": "2026-09-11T01:10:17Z",
   "sha": "81135076bcaf6ca864ba234c5d9fbc844b6dbaee"
  },
  {
   "t": "2026-09-11T01:26:00Z",
   "sha": "a74484ce2866bc89461bdb92edcad59df9614836"
  },
  {
   "t": "2026-09-11T13:31:55Z",
   "sha": "ddf033054ff66129e9307c7c4661ad20e08727fe"
  }
 ],
 "additions": 103,
 "deletions": 20,
 "changed_files": 3,
 "commit_count": 6,
 "size_bucket": "M",
 "mergeable_state": "unknown",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "ack": [
     {
      "login": "willcl-ark",
      "url": "https://github.com/bitcoin/bitcoin/pull/34213#pullrequestreview-5208506563"
     }
    ],
    "concept_ack": [
     {
      "login": "waketraindev",
      "url": "https://github.com/bitcoin/bitcoin/pull/34213#issuecomment-3716823758"
     },
     {
      "login": "danielabrozzoni",
      "url": "https://github.com/bitcoin/bitcoin/pull/34213#pullrequestreview-3656784193"
     }
    ],
    "stale_ack": [
     {
      "login": "bensig",
      "url": "https://github.com/bitcoin/bitcoin/pull/34213#issuecomment-3720228097"
     },
     {
      "login": "Bortlesboat",
      "url": "https://github.com/bitcoin/bitcoin/pull/34213#issuecomment-4083239706"
     }
    ]
   },
   "conflicts": [
    {
     "number": 34486,
     "title": "net: Reduce local network activity when networkactive=0",
     "author": "willcl-ark"
    }
   ]
  }
 },
 "acks_parsed": {
  "waketraindev": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-01-07T00:22:37Z",
   "stale": false
  },
  "bensig": {
   "kind": "ack",
   "hash": "903a37c1e142b4ee6b83fee99735ab69716084ee",
   "t": "2026-01-07T18:40:02Z",
   "stale": true
  },
  "danielabrozzoni": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-01-13T16:39:07Z",
   "stale": false
  },
  "Bortlesboat": {
   "kind": "ack",
   "hash": "a52689837bfa210e4426092e34a5d2c6ea24f351",
   "t": "2026-03-18T15:01:24Z",
   "stale": true
  },
  "willcl-ark": {
   "kind": "ack",
   "hash": "ddf033054ff66129e9307c7c4661ad20e08727fe",
   "t": "2026-09-15T10:09:36Z",
   "stale": false
  }
 },
 "acks_tally": {
  "ack": 1,
  "stale_ack": 2,
  "concept_ack": 2,
  "approach_ack": 0,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 1,
  "changes_requested": 0,
  "distinct_reviewers": [
   "Bortlesboat",
   "ajtowns",
   "bensig",
   "danielabrozzoni",
   "fanquake",
   "l0rinc",
   "luke-jr",
   "sedited",
   "w0xlt",
   "waketraindev",
   "willcl-ark"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "unknown",
  "last_author_activity": "2026-09-14T14:19:44Z",
  "last_reviewer_activity": "2026-09-15T10:09:36Z",
  "last_reviewer": "willcl-ark",
  "author_silent_days": 3,
  "waiting_on_author_days": 2,
  "days_since_update": 2
 },
 "refs": {
  "mentioned": [
   34486
  ],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [
   {
    "number": 34486,
    "type": "pull",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "net: Reduce local network activity when networkactive=0"
   }
  ],
  "conflicts": [
   34486
  ]
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [
  "src/net.cpp",
  "src/rpc/net.cpp",
  "test/functional/feature_anchors.py"
 ],
 "body": "### Problem\n\nWhen a node is shut down while network activity is disabled (e.g. via -networkactive=0 or `setnetworkactive` false), `anchors.dat` is overwritten with an empty list. This happens because `StopNodes()` collects anchors via `GetCurrentBlockRelayOnlyConns()`, which returns nothing when there are no active connections. The node then loses its anchors and cannot reconnect to them on the next startup.\n\n### Fix\n\n-  Guard `m_anchors` with a mutex, since it is now accessed outside the connection-opening thread.\n-  In `SetNetworkActive(false)`, save the current block-relay-only connections into `m_anchors` before the connections are closed, so the anchor list is preserved after the network is deactivated. Stored anchors are kept if there are no such\n  connections.\n- In `ThreadOpenConnections()`, only consume anchors while the network is active; otherwise they would be popped and dropped by the connection attempt.\n- In `StopNodes()`, if the network is inactive and `m_anchors` is non-empty, use those stored anchors as the source for `anchors.dat` instead of the (empty) live connection list.\n\nAlso, log that \"X anchors will be tried for connections\" only if the network is active.",
 "commits": [
  {
   "sha": "e5f3ea3bf6f666ae9302f4823b2c96251537faa3",
   "date": "2026-09-11T11:31:34Z",
   "message": "net: guard m_anchors with mutex\n\nm_anchors can be read and written from different threads.\nIntroduce m_anchors_mutex, guard the vector with GUARDED_BY,\nand update EXCLUSIVE_LOCKS_REQUIRED annotations on all callers."
  },
  {
   "sha": "41771236b4d9903eb9bfb92c7d5073f0e031f177",
   "date": "2026-09-11T11:53:41Z",
   "message": "net: store anchors in SetNetworkActive\n\nWhen disabling the network activity, store the current block-relay-only\npeers on m_anchors so they can be retried once the network is re-enabled,\nor dumped at shutdown. Keep any not-yet-tried anchors if there are no\nsuch peers.\n\nAlso stop consuming m_anchors in ThreadOpenConnections() while the\nnetwork is inactive. Otherwise the addresses would be popped and then\ndropped by OpenNetworkConnection(), losing them before the next\nactivation or the dump at shutdown."
  },
  {
   "sha": "04d3d474c48da3fa24e13ee3fcc623da649a62e2",
   "date": "2026-09-11T11:53:41Z",
   "message": "net: preserve anchors on shutdown when network is inactive\n\nDuring StopNodes(), anchors are collected via GetCurrentBlockRelayOnlyConns().\nWhen the network has been deactivated, that call returns an empty list since\nthere are no active connections, causing anchors.dat to be overwritten with no\nentries and losing the anchors that were saved at deactivation time (by\nSetNetworkActive).\n\nFix this by falling back to m_anchors as the source when the network is\ninactive and m_anchors is non-empty."
  },
  {
   "sha": "4b6b85428586fd2ae36386096b4a9beb467cfd8f",
   "date": "2026-09-11T13:12:14Z",
   "message": "test: disabling network activity should not affect anchors\n\nAlso check that the anchors are tried once the network is re-enabled."
  },
  {
   "sha": "b0bff47d443279da7b77d782b739f3a7697ee796",
   "date": "2026-09-11T13:12:14Z",
   "message": "net: log that anchors will be tried for connections only if network is active"
  },
  {
   "sha": "ddf033054ff66129e9307c7c4661ad20e08727fe",
   "date": "2026-09-11T13:29:48Z",
   "message": "test: cover anchor handling across network toggles with mocktime\n\nStrengthen feature_anchors.py so it deterministically exercises\nThreadOpenConnections while the network is inactive. An unreachable\nseednode is passed twice and mocktime is advanced past the seednode\nretry timer, giving a reliable signal that a connection pass has run\nwithout consuming anchors.\n\nThis lets the test assert that:\n- no anchors are tried while the network is disabled (the startup log\n  is gated on network activity),\n- stored anchors survive a network toggle and are tried on re-enable,\n- unconsumed anchors are discarded at shutdown when the network is\n  active."
  }
 ],
 "timeline": [
  {
   "t": "2026-01-06T21:31:12Z",
   "kind": "force_push",
   "who": "brunoerg",
   "commit": "f82f7c194d0d67357919058aadc51d5a2713f612"
  },
  {
   "t": "2026-01-06T21:56:34Z",
   "kind": "force_push",
   "who": "brunoerg",
   "commit": "903a37c1e142b4ee6b83fee99735ab69716084ee"
  },
  {
   "t": "2026-01-07T00:21:54Z",
   "kind": "review_comment",
   "who": "waketraindev",
   "assoc": "CONTRIBUTOR",
   "path": "src/net.cpp",
   "commit": "a52689837bfa210e4426092e34a5d2c6ea24f351",
   "in_reply_to": null,
   "text": "I start some nodes with network inactive.\n\nmicro nit (in both cases):\n```suggestion\n    if (fNetworkActive && m_use_addrman_outgoing) {\n```"
  },
  {
   "t": "2026-01-07T00:22:37Z",
   "kind": "comment",
   "who": "waketraindev",
   "assoc": "CONTRIBUTOR",
   "text": "Concept ACK"
  },
  {
   "t": "2026-01-07T01:50:48Z",
   "kind": "comment",
   "who": "waketraindev",
   "assoc": "CONTRIBUTOR",
   "text": "Have you considered also adding a DumpAnchors() if anchors_to_dump.size() > 1 when setting network active from true to false?\n\nThat would persist the anchors for nodes that do a setnetworkactive false and after shut down and skip dumping them again at shutdown."
  },
  {
   "t": "2026-01-07T18:25:14Z",
   "kind": "review_comment",
   "who": "brunoerg",
   "assoc": "MEMBER",
   "path": "src/net.cpp",
   "commit": "a52689837bfa210e4426092e34a5d2c6ea24f351",
   "in_reply_to": 2666671765,
   "text": "I'll leave as-is for now."
  },
  {
   "t": "2026-01-07T18:26:58Z",
   "kind": "comment",
   "who": "brunoerg",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nYes, but would leave it for a follow-up, it will require more changes than the simple ones to cover the `-networkactive` behavior."
  },
  {
   "t": "2026-01-07T18:40:02Z",
   "kind": "comment",
   "who": "bensig",
   "assoc": "CONTRIBUTOR",
   "text": "ACK 903a37c1e142b4ee6b83fee99735ab69716084ee\n\nTested, `feature_anchors.py` passes"
  },
  {
   "t": "2026-01-07T20:59:40Z",
   "kind": "review",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "state": "COMMENTED",
   "commit": "903a37c1e142b4ee6b83fee99735ab69716084ee",
   "text": "This seems like a reasonable change, but it may be better to document that setting `fNetworkActive` to false via the `setnetworkactive` RPC can prevent anchors from being preserved."
  },
  {
   "t": "2026-01-08T19:48:06Z",
   "kind": "comment",
   "who": "brunoerg",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nSounds good, I will add this information in the `setnetworkactive` RPC."
  },
  {
   "t": "2026-01-09T13:07:59Z",
   "kind": "comment",
   "who": "brunoerg",
   "assoc": "MEMBER",
   "text": "Force-pushed addressing https://github.com/bitcoin/bitcoin/pull/34213#pullrequestreview-3636779749"
  },
  {
   "t": "2026-01-13T16:39:07Z",
   "kind": "review",
   "who": "danielabrozzoni",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "a52689837bfa210e4426092e34a5d2c6ea24f351",
   "text": "Concept ACK. I think this is a good improvement, makes sense to have, and I can\u2019t find any additional surface of attack introduced here.\nTo recap, this is how we use anchors at the moment:\n- Anchors are used to protect from eclipse attacks. When our node cleanly shutdown, it saves our outbound block relay only connections to the anchors.dat file. The attack this is preventing is: if an attacker can manipulate our addrman to make sure that, on restart, we will only connect to the attacker nodes, and we happen to restart our node, the attacker can eclipse us. Anchors protect us because on restart we would connect to some of our previous block relay only peer, and one honest peer is sufficient for us not to become eclipsed.\n- We do not persist anchors if we uncleanly shutdown (power outage, software crash, etc.), because if one of our anchor is malicious and figures out a way to crash our node, and on restart we will reconnect to it, the attacker can keep crashing our node. See: https://github.com/bitcoin/bitcoin/pull/17428#issuecomment-579580035\n\nI started thinking along the lines of the last bullet point, to see whether the code introduced in this PR could be dangerous in any way, but I couldn\u2019t think of anything. While I couldn\u2019t find a way to exploit the bug this PR is fixing (if an attacker can figure out how to manipulate our bitcoind setting to set `networkactive=0`, we likely have bigger problems!), I still think it makes sense to fix this, as it\u2019s low risk, and would avoid unnecessarily losing anchors.\n\n---\nHowever, while reviewing it I started thinking about the anchor logic, and I wonder if we should harden it a bit to make sure that we don\u2019t delete anchors if `networkactive` is not set, but the user is not connected to the internet. I would appreciate feedback from someone who has thought about this area more deeply than me :)\n\nAt the moment, the PR works as intended if we set `-networkactive=0`, but if we simply disconnect the wifi between restarts, Bitcoin Core is still deleting anchors, similarly to master.\n\nFor example, I tried on my own node:\n- Start the node, keep it running until there\u2019s block only connections, shut it down and check that the anchors.dat file is present\n- Disconnect internet\n- Restart the node\n\nFrom the logs, we can see that the node reads the anchors, can\u2019t connect to them, and at the time of flushing to disk, it writes 0 addresses.\n```\n2026-01-13T11:48:49Z Loaded 2 addresses from \"anchors.dat\"\n2026-01-13T11:48:49Z 2 block-relay-only anchors will be tried for connections.\n2026-01-13T11:53:26Z DumpAnchors: Flush 0 outbound block-relay-only peer addresses to anchors.dat started\n2026-01-13T11:53:26Z DumpAnchors: Flush 0 outbound block-relay-only peer addresses to anchors.dat completed (0.00s)\n```\nThe same happens if we start the node, disconnect the wifi, wait for long enough that all the peers are disconnected.\n\nThis happens because `fNetworkActive` only refers to [whether `-networkactive` is set or not](https://github.com/bitcoin/bitcoin/blob/3c8d389a84d29c7e5532548320228f3e8107969d/src/init.cpp#L1607).\n\nI think it would be beneficial to fix this edge case, because if an attacker can figure out how to disconnect us from the internet, then we would lose the anchors and be vulnerable to an eclipse attack. Of course, finding a way to disconnect a node from the internet is not an easy task! I suppose an attacker could try with a BGP attack, but I\u2019m not entirely sure.\nI haven\u2019t looked into how we would implement this, and I\u2019m not sure it\u2019s entirely doable."
  },
  {
   "t": "2026-01-14T12:27:17Z",
   "kind": "comment",
   "who": "brunoerg",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nTo clarify, I don't think this is exploitable, it is mostly about the own user restarting the node (which happened to me) disabling the network and ending up losing the anchors. Also, since the network is disabled it makes sense to not perform any network activity like this."
  },
  {
   "t": "2026-01-14T18:32:34Z",
   "kind": "review",
   "who": "waketraindev",
   "assoc": "CONTRIBUTOR",
   "state": "COMMENTED",
   "commit": "a52689837bfa210e4426092e34a5d2c6ea24f351",
   "text": "still wish anchors would have been persisted if available when network activity is turned off instead just on exit.\n\nedit: shouldn't anchors be loaded at startup even if networkactive is false and maybe just skip saving them if network is inactive?\n\nWith this case both loading and saving will be skipped if node is started with network active false\n\n```cpp\n    if (m_use_addrman_outgoing && fNetworkActive) {\n        // Load addresses from anchors.dat\n        m_anchors = ReadAnchors(gArgs.GetDataDirNet() / ANCHORS_DATABASE_FILENAME);\n        if (m_anchors.size() > MAX_BLOCK_RELAY_ONLY_ANCHORS) {\n            m_anchors.resize(MAX_BLOCK_RELAY_ONLY_ANCHORS);\n        }\n        LogInfo(\"%i block-relay-only anchors will be tried for connections.\\n\", m_anchors.size());\n    }\n```\n\nHow I see it:\n* Anchors should be loaded on startup regardless if network activity is disabled (it might be turned on after)\n* Anchors should not be popped back, attempted connection, if network activity is disabled\n* Anchors should be saved on exit from `GetCurrentBlockRelayOnlyConns()` if populated or `m_anchors` if empty"
  },
  {
   "t": "2026-01-22T20:26:20Z",
   "kind": "review_comment",
   "who": "luke-jr",
   "assoc": "CONTRIBUTOR",
   "path": "src/rpc/net.cpp",
   "commit": "a52689837bfa210e4426092e34a5d2c6ea24f351",
   "in_reply_to": null,
   "text": "This seems like a bug... Maybe we should re-save the original anchors if there's no new ones at a clean shutdown? Perhaps update the \"original anchors\" list just before disabling network activity?"
  },
  {
   "t": "2026-01-22T20:27:41Z",
   "kind": "review_comment",
   "who": "luke-jr",
   "assoc": "CONTRIBUTOR",
   "path": "src/net.cpp",
   "commit": "a52689837bfa210e4426092e34a5d2c6ea24f351",
   "in_reply_to": null,
   "text": "If the user subsequently uses `setnetworkactive` to enable network, should we restore the anchors then?"
  },
  {
   "t": "2026-01-27T12:36:07Z",
   "kind": "review_comment",
   "who": "brunoerg",
   "assoc": "MEMBER",
   "path": "src/rpc/net.cpp",
   "commit": "a52689837bfa210e4426092e34a5d2c6ea24f351",
   "in_reply_to": 2718514479,
   "text": "[quoted text omitted]\n\nI don't think so. What if these anchors are not available anymore? I mean, we tried to connect to them but they are not available anymore."
  },
  {
   "t": "2026-01-27T12:36:21Z",
   "kind": "review_comment",
   "who": "brunoerg",
   "assoc": "MEMBER",
   "path": "src/net.cpp",
   "commit": "a52689837bfa210e4426092e34a5d2c6ea24f351",
   "in_reply_to": 2718518512,
   "text": "Yes, I think so. I will implement it in a follow-up."
  },
  {
   "t": "2026-03-18T15:01:24Z",
   "kind": "comment",
   "who": "Bortlesboat",
   "assoc": "CONTRIBUTOR",
   "text": "ACK a52689837bfa210e4426092e34a5d2c6ea24f351\n\nRan `feature_anchors.py` locally \u2014 passes on both transports."
  },
  {
   "t": "2026-03-18T15:04:10Z",
   "kind": "comment",
   "who": "Bortlesboat",
   "assoc": "CONTRIBUTOR",
   "text": "Re the open question about whether anchors should be loaded at startup even with `-networkactive=0`: I think skipping the load is correct. Anchors protect against eclipse attacks on restart \u2014 if the node starts with the network off, there is no connection activity to eclipse. If the user later calls `setnetworkactive true`, the node has its addrman for peer diversity. The anchors.dat file stays on disk, so the next clean restart with network active loads them normally."
  },
  {
   "t": "2026-03-18T15:07:53Z",
   "kind": "comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "text": "@Bortlesboat, please point your bot to a different repo, this isn't useful..."
  },
  {
   "t": "2026-03-18T15:08:03Z",
   "kind": "comment",
   "who": "fanquake",
   "assoc": "MEMBER",
   "text": "cc @willcl-ark given #34486."
  },
  {
   "t": "2026-03-20T09:52:46Z",
   "kind": "review_comment",
   "who": "ajtowns",
   "assoc": "MEMBER",
   "path": "src/rpc/net.cpp",
   "commit": "a52689837bfa210e4426092e34a5d2c6ea24f351",
   "in_reply_to": 2718514479,
   "text": "I think maintaining `m_anchors` as something like:\n\n```c++\n    auto cutoff = now() - 1m;\n    vector<CAddress> anchors;\n    set<CAddress> old{m_anchors};\n    for (const CNode* pnode : m_nodes) {\n        if (!pnode->IsBlockOnlyConn()) continue;\n        if (!pnode->fSuccessfullyConnected) continue;\n        if (pnode->m_connected > cutoff) continue;\n        anchors.push_back(pnode->addr);\n        old.erase(pnode->addr);\n    }\n    for (auto& o : old) {\n        if (m_nodes.size() >= 2) break;\n        anchors.push_back(o);\n    }\n    m_anchors.swap(anchors);\n```\n\ncould work? ie update it with the address of current blocks-only connections that have been working, but if there aren't any (or enough) such connections, keep the old addresses."
  },
  {
   "t": "2026-03-20T18:14:38Z",
   "kind": "review_comment",
   "who": "brunoerg",
   "assoc": "MEMBER",
   "path": "src/rpc/net.cpp",
   "commit": "a52689837bfa210e4426092e34a5d2c6ea24f351",
   "in_reply_to": 2718514479,
   "text": "Would it prevent us to not save addresses that we tried to connect to and didn't work?\n\nPerhaps when disabling the network, we could save the anchors (we're connected to) to `m_anchors` and use it in `StopNodes` if `GetCurrentBlockRelayOnlyConns` doesn't return any peer."
  },
  {
   "t": "2026-03-27T14:37:49Z",
   "kind": "force_push",
   "who": "brunoerg",
   "commit": "dce37280c3a2cdebf4a97c99b67fba110b2b84f2"
  },
  {
   "t": "2026-03-27T14:52:08Z",
   "kind": "comment",
   "who": "brunoerg",
   "assoc": "MEMBER",
   "text": "I just changed the approach of this PR and changed PR title and description. It addresses concerns like https://github.com/bitcoin/bitcoin/pull/34213#discussion_r2718514479. Now this PR addresses cases involving disabling network via RPC and starting the node with the network disabled via flag.\n\ncc @danielabrozzoni @luke-jr @ajtowns"
  },
  {
   "t": "2026-04-06T16:09:07Z",
   "kind": "review_comment",
   "who": "danielabrozzoni",
   "assoc": "MEMBER",
   "path": "test/functional/feature_anchors.py",
   "commit": "4046d0ec7975be5d15c76eb0dc7c78cbbab2584d",
   "in_reply_to": null,
   "text": "In 71d3aa1957d42d79e075c3380b73f3c7bae37f2e: I think this test should be moved in the next commit (a6d411664e967b4fa06c4a6249e492b9185766b9, \"test: disabling network activity should not affect anchors\").\n\nBoth test additions are testing `networkactive=0`, this one having `-networkactive=0` passed in as a bitcoind argument, and the other one calling `self.nodes[0].setnetworkactive(False)`. Can we move these two closer to each other? This way it's clear that we're testing both ways of setting the network to inactive."
  },
  {
   "t": "2026-04-06T16:12:17Z",
   "kind": "review_comment",
   "who": "danielabrozzoni",
   "assoc": "MEMBER",
   "path": "src/net.cpp",
   "commit": "4046d0ec7975be5d15c76eb0dc7c78cbbab2584d",
   "in_reply_to": null,
   "text": "In dce37280c3a2cdebf4a97c99b67fba110b2b84f2: I think this commit should be the first one, so in the git history we never access m_anchors from multiple threads without having a mutex."
  },
  {
   "t": "2026-04-10T11:15:39Z",
   "kind": "review_comment",
   "who": "danielabrozzoni",
   "assoc": "MEMBER",
   "path": "src/net.cpp",
   "commit": "9a7c4970cdd2989aca2497db4d23a0b6f7c990ce",
   "in_reply_to": null,
   "text": "Right now m_anchors is used for:\n\n1. keeping track of the anchors that were in the file upon startup and we haven't connected to yet\nor\n2. keeping track of the anchors we were connected to when we disable the network\n\nThe docs of m_anchors should be updated to reflect this.\n\nAlso, it would make sense here to append to m_anchors, instead of substituting (and then resizing `m_anchors` so it's not bigger than `MAX_BLOCK_RELAY_ONLY_CONNECTIONS`). I think it's a bit cleaner, because we wouldn't lose any information about anchors that were in the file, and we hadn't connected to yet.\n\nIn this way, point 2 would become:\n\n\"keeping track of the anchors we were connected to **and were about to connect to** when we disable the network\"\n\nRight now, this could happen:\n- we start our node, it reads 2 anchors from the file, `anchor1` and `anchor2`, puts them in m_anchors\n- the node starts our first block only connection, pops `anchor1` from `m_anchors`\n- we set network active to 0: this saves `anchor1` to `m_anchors`, `anchor2` is forgotten."
  },
  {
   "t": "2026-04-10T13:51:47Z",
   "kind": "review_comment",
   "who": "danielabrozzoni",
   "assoc": "MEMBER",
   "path": "src/net.cpp",
   "commit": "3aa8484020430f1fe7fcc554c5d65b8d805ffc4b",
   "in_reply_to": null,
   "text": "nit: can be `if(!anchors_to_dump.empty())`: even if the network is active, but `anchors_to_dump` is empty, you can skip the dump"
  },
  {
   "t": "2026-04-10T14:06:41Z",
   "kind": "review_comment",
   "who": "danielabrozzoni",
   "assoc": "MEMBER",
   "path": "src/net.cpp",
   "commit": "9a7c4970cdd2989aca2497db4d23a0b6f7c990ce",
   "in_reply_to": null,
   "text": "claude code caught this, but I verified manually and I think it's correct: currently there is a data race, you should first save the anchors, and only then set `fNetworkActive = active`, like this:\n\n```cpp\nif (!active) {\n    auto anchors = GetCurrentBlockRelayOnlyConns();\n    LOCK(m_anchors_mutex);\n    m_anchors = std::move(anchors);\n}\nfNetworkActive = active;  // flip AFTER capturing\n```\n\nThe reason is: right now you set `fNetworkActive` to false, then call `GetCurrentBlockRelayOnlyConns()`. Between these two steps there is a small window where `ThreadSocketHandler` might call `DisconnectNodes`, which will lock `m_nodes_mutex` and disconnect all nodes if the network is not active, and clear `m_nodes`:\n\nhttps://github.com/bitcoin/bitcoin/blob/58dccd27e11de68f810145fc30631a2c446f3bf5/src/net.cpp#L1923-L1944\n\n`GetCurrentBlockRelayOnlyConns` needs a lock on `m_nodes_mutex`, so if `DisconnectNodes` runs first, it will have already marked all nodes as `fDisconnect = true` and removed them from `m_nodes`. By the time `GetCurrentBlockRelayOnlyConns` acquires the lock, `m_nodes` is empty and it returns nothing."
  },
  {
   "t": "2026-04-10T15:27:37Z",
   "kind": "review_comment",
   "who": "danielabrozzoni",
   "assoc": "MEMBER",
   "path": "src/net.cpp",
   "commit": "ddf033054ff66129e9307c7c4661ad20e08727fe",
   "in_reply_to": null,
   "text": "You should add a `AssertLockNotHeld` at the start of the function, similarly to `AssertLockNotHeld(m_reconnections_mutex);`"
  },
  {
   "t": "2026-04-10T15:29:21Z",
   "kind": "review_comment",
   "who": "danielabrozzoni",
   "assoc": "MEMBER",
   "path": "src/net.cpp",
   "commit": "4046d0ec7975be5d15c76eb0dc7c78cbbab2584d",
   "in_reply_to": null,
   "text": "Same as other comments, you should add a AssertLockNotHeld at the start of ThreadOpenConnections"
  },
  {
   "t": "2026-04-10T15:30:27Z",
   "kind": "review",
   "who": "danielabrozzoni",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "3aa8484020430f1fe7fcc554c5d65b8d805ffc4b",
   "text": "Review up to 3aa8484020\n\nThanks for updating! This is shaping up nicely, I left a couple of comments"
  },
  {
   "t": "2026-04-13T16:42:14Z",
   "kind": "force_push",
   "who": "brunoerg",
   "commit": "5a674a336e53905a487be3e2ad923bf62684b449"
  },
  {
   "t": "2026-04-13T16:42:36Z",
   "kind": "review_comment",
   "who": "brunoerg",
   "assoc": "MEMBER",
   "path": "test/functional/feature_anchors.py",
   "commit": "4046d0ec7975be5d15c76eb0dc7c78cbbab2584d",
   "in_reply_to": 3040380748,
   "text": "Done. I think it makes sense to have both tests together."
  },
  {
   "t": "2026-04-13T16:45:27Z",
   "kind": "review_comment",
   "who": "brunoerg",
   "assoc": "MEMBER",
   "path": "src/net.cpp",
   "commit": "9a7c4970cdd2989aca2497db4d23a0b6f7c990ce",
   "in_reply_to": 3063859820,
   "text": "I've changed the documentation of `m_anchors` according to the new behavior - it is now contains addresses saved from previous clean shutdown or when the network was deactivated.\n\n[quoted text omitted]\nLeft this as is for now, tried to not change too much. But happy to change it whether I have to touch it again."
  },
  {
   "t": "2026-04-13T16:45:52Z",
   "kind": "review_comment",
   "who": "brunoerg",
   "assoc": "MEMBER",
   "path": "src/net.cpp",
   "commit": "9a7c4970cdd2989aca2497db4d23a0b6f7c990ce",
   "in_reply_to": 3064766043,
   "text": "Nice find, going to address it."
  },
  {
   "t": "2026-04-13T16:52:05Z",
   "kind": "force_push",
   "who": "brunoerg",
   "commit": "cca21e44db55ac3e93e4cca044e0eeeb950b800e"
  },
  {
   "t": "2026-04-13T16:54:46Z",
   "kind": "comment",
   "who": "brunoerg",
   "assoc": "MEMBER",
   "text": "Thanks, @danielabrozzoni for reviewing. Force-pushed - major changes:\n\n- Updated m_anchors documentation to reflect the new behavior;\n- Added missing `AssertLockNotHeld` for `m_anchors`;\n- Fixed data race when disabling the network and saving the anchors."
  },
  {
   "t": "2026-04-13T17:03:19Z",
   "kind": "force_push",
   "who": "brunoerg",
   "commit": "133cfae47d3f9a62e7bf4f21cb9b69061826e1cf"
  },
  {
   "t": "2026-05-05T12:18:40Z",
   "kind": "force_push",
   "who": "brunoerg",
   "commit": "e619bc53c5033edbc8f4fcb7a25bd9cb0d858190"
  },
  {
   "t": "2026-05-05T12:19:08Z",
   "kind": "comment",
   "who": "brunoerg",
   "assoc": "MEMBER",
   "text": "Rebased"
  },
  {
   "t": "2026-05-05T12:44:24Z",
   "kind": "force_push",
   "who": "brunoerg",
   "commit": "4046d0ec7975be5d15c76eb0dc7c78cbbab2584d"
  },
  {
   "t": "2026-09-09T09:52:38Z",
   "kind": "comment",
   "who": "sedited",
   "assoc": "MEMBER",
   "text": "@davidgumberg @stratospher @theuni @vasild you previously expressed interest in looking at this pull request. Are you still interested in the changes?"
  },
  {
   "t": "2026-09-10T10:16:31Z",
   "kind": "review_comment",
   "who": "willcl-ark",
   "assoc": "MEMBER",
   "path": "test/functional/feature_anchors.py",
   "commit": "ddf033054ff66129e9307c7c4661ad20e08727fe",
   "in_reply_to": null,
   "text": "In f5ddca1e0292845c76614b5ce1a3a482bef07c7d\n\nHmmm, `ThreadOpenConnections()` still consumes `m_anchors` while networking is disabled. It pops an address before calling `OpenNetworkConnection()`, which then returns because `fNetworkActive` is false. By shutdown, both the live connections and saved anchors can be empty. The new tests stop the node immediately after disabling networking so it's not visible here.\n\nAdding:\n\n```python\nimport time\ntime.sleep(2)\n```\n\nmakes this test fail here on L61 and on L144"
  },
  {
   "t": "2026-09-11T01:10:17Z",
   "kind": "force_push",
   "who": "brunoerg",
   "commit": "81135076bcaf6ca864ba234c5d9fbc844b6dbaee"
  },
  {
   "t": "2026-09-11T01:26:00Z",
   "kind": "force_push",
   "who": "brunoerg",
   "commit": "a74484ce2866bc89461bdb92edcad59df9614836"
  },
  {
   "t": "2026-09-11T01:26:27Z",
   "kind": "review_comment",
   "who": "brunoerg",
   "assoc": "MEMBER",
   "path": "test/functional/feature_anchors.py",
   "commit": "ddf033054ff66129e9307c7c4661ad20e08727fe",
   "in_reply_to": 3978006683,
   "text": "Good catch! You're right, I just pushed a fix for this. `ThreadOpenConnections()` now skips the anchor branch entirely while the network is inactive, so anchors are no longer popped and discarded before `OpenNetworkConnection()` gets a chance to reject the attempt."
  },
  {
   "t": "2026-09-11T01:29:04Z",
   "kind": "review_comment",
   "who": "brunoerg",
   "assoc": "MEMBER",
   "path": "test/functional/feature_anchors.py",
   "commit": "ddf033054ff66129e9307c7c4661ad20e08727fe",
   "in_reply_to": 3978006683,
   "text": "With this, I dropped the !anchors_to_dump.empty() guard in StopNodes() since it isn't needed."
  },
  {
   "t": "2026-09-11T01:29:41Z",
   "kind": "comment",
   "who": "brunoerg",
   "assoc": "MEMBER",
   "text": "Force-pushed addressing https://github.com/bitcoin/bitcoin/pull/34213#discussion_r3978006683"
  },
  {
   "t": "2026-09-11T02:45:57Z",
   "kind": "review_comment",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "path": "src/net.cpp",
   "commit": "ddf033054ff66129e9307c7c4661ad20e08727fe",
   "in_reply_to": null,
   "text": "`GetCurrentBlockRelayOnlyConns()` returns peers in connection order, while `ThreadOpenConnections()` consumes anchors from the back.\n\n```c++\nanchor_addr = m_anchors.back();\nm_anchors.pop_back();\n```\nIncluding a temporary third peer therefore lets it take the oldest anchor\u2019s place after a network toggle. The patch below keeps only the two oldest peers, matching the existing startup and shutdown limit.\n\n```diff\ndiff --git a/src/net.cpp b/src/net.cpp\nindex f1cc544f4b..039a6df131 100644\n--- a/src/net.cpp\n+++ b/src/net.cpp\n@@ -3418,6 +3418,10 @@ void CConnman::SetNetworkActive(bool active)\n         // once the network is re-enabled, or dumped at shutdown. Keep any\n         // not-yet-tried anchors if there are no such peers.\n         auto anchors = GetCurrentBlockRelayOnlyConns();\n+        if (anchors.size() > MAX_BLOCK_RELAY_ONLY_ANCHORS) {\n+            // Keep the oldest peers, as when saving anchors at shutdown.\n+            anchors.resize(MAX_BLOCK_RELAY_ONLY_ANCHORS);\n+        }\n         if (!anchors.empty()) {\n             LOCK(m_anchors_mutex);\n             m_anchors = std::move(anchors);\n```"
  },
  {
   "t": "2026-09-11T13:31:55Z",
   "kind": "force_push",
   "who": "brunoerg",
   "commit": "ddf033054ff66129e9307c7c4661ad20e08727fe"
  },
  {
   "t": "2026-09-11T13:33:19Z",
   "kind": "review_comment",
   "who": "brunoerg",
   "assoc": "MEMBER",
   "path": "src/net.cpp",
   "commit": "ddf033054ff66129e9307c7c4661ad20e08727fe",
   "in_reply_to": 3985494910,
   "text": "Thanks, addressed it in  a74484c to ddf0330."
  },
  {
   "t": "2026-09-11T13:35:36Z",
   "kind": "comment",
   "who": "brunoerg",
   "assoc": "MEMBER",
   "text": "Force-pushed addressing https://github.com/bitcoin/bitcoin/pull/34213#discussion_r3985494910. Also, mutation testing + @willcl-ark's comment made me think in a way of strengthen the anchors' functional test by using mocktime to deterministically exercise `ThreadOpenConnections` - I just added a new commit with it."
  },
  {
   "t": "2026-09-14T12:20:06Z",
   "kind": "review_comment",
   "who": "willcl-ark",
   "assoc": "MEMBER",
   "path": "src/net.cpp",
   "commit": "41771236b4d9903eb9bfb92c7d5073f0e031f177",
   "in_reply_to": null,
   "text": "In 41771236b4d9903eb9bfb92c7d5073f0e031f177\n\nI guess another execution path here is that we might save only a single anchor and skip any \"pending\" one. But that seems fine to me as there doesn't seem much value is saving one we didnt' yet make a connection to..."
  },
  {
   "t": "2026-09-14T12:22:21Z",
   "kind": "review",
   "who": "willcl-ark",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "ddf033054ff66129e9307c7c4661ad20e08727fe",
   "text": "Looks pretty nice to me now, thanks for addressing my comments.\n\nI think the PR description could be updated as it still has:\n\n[quoted text omitted]"
  },
  {
   "t": "2026-09-14T14:19:44Z",
   "kind": "comment",
   "who": "brunoerg",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nDone, just updated it."
  },
  {
   "t": "2026-09-15T10:09:36Z",
   "kind": "review",
   "who": "willcl-ark",
   "assoc": "MEMBER",
   "state": "APPROVED",
   "commit": "ddf033054ff66129e9307c7c4661ad20e08727fe",
   "text": "ACK ddf033054ff66129e9307c7c4661ad20e08727fe\n\nDisabling networking should not cause the node to lose its anchors. Preserving them across a clean shutdown lets the node reconnect when networking resumes, increasing eclipse attack resilience on restarts."
  }
 ],
 "labels_log": [
  {
   "t": "2026-01-06T21:26:12Z",
   "action": "labeled",
   "label": "P2P",
   "who": "DrahtBot"
  },
  {
   "t": "2026-01-06T21:31:44Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-01-06T23:08:59Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-03-27T14:49:12Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-03-27T15:34:44Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-04-13T16:53:18Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-04-13T18:01:54Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-05-04T09:44:37Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-05-05T12:45:49Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-05-05T13:48:01Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-05-05T15:28:25Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-11T01:26:28Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-11T02:33:18Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  }
 ],
 "state_log": [
  {
   "t": "2026-03-27T14:38:27Z",
   "kind": "renamed",
   "who": "brunoerg",
   "from": "net: do not read/dump anchors if network is not active",
   "to": "net: preserve anchors when network is disabled"
  }
 ],
 "text_chars": 19169,
 "text_tokens_estimate": 4792,
 "changed_paths": [],
 "files": [],
 "test_lines": null,
 "git": null,
 "input_hash": "bae0168c2b9435ae",
 "extracted_at": "2026-09-17T16:15:31+00:00"
}