{
 "number": 36014,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/36014",
 "title": "init: ignore repeated `-addnode` startup values",
 "author": "w0xlt",
 "author_association": "CONTRIBUTOR",
 "created_at": "2026-08-18T20:28:51Z",
 "updated_at": "2026-09-11T01:39:28Z",
 "age_days": 29,
 "draft": false,
 "labels": [],
 "milestone": null,
 "base": "master",
 "head_sha": "4ebff1af2acc6e846eb7932ff8ece03ffa3a9066",
 "head_ref": "net-deduplicate-startup-addnodes",
 "head_repo": "w0xlt/bitcoin",
 "head_history": [
  {
   "t": "2026-09-01T23:21:28Z",
   "sha": "22499b40b286d147e46a86a2d0d4582631d3c469"
  },
  {
   "t": "2026-09-01T23:33:59Z",
   "sha": "4ebff1af2acc6e846eb7932ff8ece03ffa3a9066"
  }
 ],
 "additions": 59,
 "deletions": 9,
 "changed_files": 4,
 "commit_count": 2,
 "size_bucket": "S",
 "mergeable_state": "clean",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "ack": [
     {
      "login": "pablomartin4btc",
      "url": "https://github.com/bitcoin/bitcoin/pull/36014#pullrequestreview-5174095555"
     }
    ],
    "concept_ack": [
     {
      "login": "danielabrozzoni",
      "url": "https://github.com/bitcoin/bitcoin/pull/36014#pullrequestreview-5041831301"
     }
    ]
   },
   "conflicts": [
    {
     "number": 35940,
     "title": "net: allow selecting BIP152 high-bandwidth peers with `-addnode`",
     "author": "w0xlt"
    },
    {
     "number": 34844,
     "title": "util: Add util::NotNull<SmartPtrType>",
     "author": "maflcko"
    }
   ]
  }
 },
 "acks_parsed": {
  "pablomartin4btc": {
   "kind": "ack",
   "hash": "4ebff1af2acc6e846eb7932ff8ece03ffa3a9066",
   "t": "2026-09-11T01:39:24Z",
   "stale": false
  },
  "danielabrozzoni": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-08-27T14:15:29Z",
   "stale": false
  }
 },
 "acks_tally": {
  "ack": 1,
  "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": [
   "danielabrozzoni",
   "pablomartin4btc"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "clean",
  "last_author_activity": "2026-09-01T23:34:28Z",
  "last_reviewer_activity": "2026-09-11T01:39:24Z",
  "last_reviewer": "pablomartin4btc",
  "author_silent_days": 15,
  "waiting_on_author_days": 6,
  "days_since_update": 6
 },
 "refs": {
  "mentioned": [
   35600
  ],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [
   {
    "number": 35600,
    "type": "pull",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "net: prevent duplication manual connections (take 2)"
   }
  ],
  "conflicts": [
   35940,
   34844
  ]
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [
  "src/net.cpp",
  "src/net.h"
 ],
 "body": "Startup `-addnode` values are copied directly into the connection manager's added-node list. Unlike runtime `addnode add` calls, this path does not reject repeated values.\n\nFor example:\n\n```\n-addnode=example.com\n-addnode=example.com\n```\n\nBoth entries are currently stored. While the destination is disconnected, `ThreadOpenAddedConnections()` processes\neach entry during every retry cycle.\n\nThis can cause redundant DNS lookups, connection attempts, and log messages. `getaddednodeinfo` also reports the repeated entry.\n\n---\n\nThis change is complementary to, and independent of, #35600.\n\n#35600 prevents overlapping manual connection attempts by tracking destinations while a connection attempt is in progress.\nThis PR instead removes repeated `-addnode` values before the connection threads start, preventing duplicate stored entries and sequential redundant retries.\n\nIt does not replace or broaden #35600's in-flight connection handling.",
 "commits": [
  {
   "sha": "beadf6a96d16b21d46804193877fd57ad9bd1fc8",
   "date": "2026-09-01T23:33:24Z",
   "message": "net: reject equivalent addnode host-port pairs\n\nCConnman::AddNode rejects equivalent numeric addresses, but compares\nunresolved hostnames as raw strings. As a result, the same hostname\nwith and without an explicit default port can be stored twice.\n\nCompare successfully parsed literal hosts using their effective ports.\nDo not perform DNS resolution, and keep non-default ports distinct.\n\nCo-authored-by: Pablo Martin <pablomartin4btc@gmail.com>"
  },
  {
   "sha": "4ebff1af2acc6e846eb7932ff8ece03ffa3a9066",
   "date": "2026-09-01T23:33:24Z",
   "message": "init: add startup nodes through CConnman::AddNode\n\nStartup -addnode values are copied directly into CConnman state,\nbypassing the duplicate checks used by the addnode RPC. Equivalent\nvalues are consequently stored and retried independently.\n\nInsert startup values through CConnman::AddNode so exact, numeric, and\neffective-port duplicates share the runtime behavior. Keep the first\nentry and log each rejected duplicate.\n\nCo-authored-by: Daniela Brozzoni <danielabrozzoni@protonmail.com>"
  }
 ],
 "timeline": [
  {
   "t": "2026-08-19T18:30:37Z",
   "kind": "review",
   "who": "pablomartin4btc",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "073a33247e235c6e2258d3b5899727be225bcf30",
   "text": "Concept ACK\n\nComparing this patch to the validation performed in `CConnman::AddNode()` (the RPC path), which resolves the target \u2014 full resolution is out of scope as clearly explained in the description and commit body. What about also checking the port (perhaps using `SplitHostPort`), so e.g. `node.example` and `node.example:8333` (`mainnet` default port) would be treated as the same target, and the second one excluded as a duplicate?"
  },
  {
   "t": "2026-08-27T14:15:29Z",
   "kind": "review",
   "who": "danielabrozzoni",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "073a33247e235c6e2258d3b5899727be225bcf30",
   "text": "Concept ACK on ignoring repeated values.\n\nWhen looking at how addnode handles repeated values, I noticed that it uses `CConnMan::AddNode`, which invokes `LookupNumeric` to parse IP addresses and remove equivalent representations (it doesn't do any DNS resolution). For consistency, it may make sense for `-addnode` values to use the same check.\n\nOne way would be to call `AddNode` inside of `CConnman::Init`, but I'm not sure if it's okay to call `LogWarning` from there:\n```diff\ndiff --git a/src/net.h b/src/net.h\nindex ea0c651d11..d69b9a4df3 100644\n--- a/src/net.h\n+++ b/src/net.h\n@@ -1140,12 +1140,13 @@ public:\n         vWhitelistedRangeIncoming = connOptions.vWhitelistedRangeIncoming;\n         vWhitelistedRangeOutgoing = connOptions.vWhitelistedRangeOutgoing;\n         {\n-            LOCK(m_added_nodes_mutex);\n             // Attempt v2 connection if we support v2 - we'll reconnect with v1 if our\n             // peer doesn't support it or immediately disconnects us for another reason.\n             const bool use_v2transport(GetLocalServices() & NODE_P2P_V2);\n             for (const std::string& added_node : connOptions.m_added_nodes) {\n-                m_added_node_params.push_back({added_node, use_v2transport});\n+                if (!AddNode({added_node, use_v2transport})) {\n+                    LogWarning(\"Ignoring duplicate -addnode value: %s\", added_node);\n+                }\n             }\n         }\n```\n\nOtherwise, you can duplicate the logic in `init.cpp`.\n\nTo be clear, I'm okay even with the code as-is, if it gets too complicated to add the `LookupNumeric` checks."
  },
  {
   "t": "2026-09-01T23:21:28Z",
   "kind": "force_push",
   "who": "w0xlt",
   "commit": "22499b40b286d147e46a86a2d0d4582631d3c469"
  },
  {
   "t": "2026-09-01T23:33:59Z",
   "kind": "force_push",
   "who": "w0xlt",
   "commit": "4ebff1af2acc6e846eb7932ff8ece03ffa3a9066"
  },
  {
   "t": "2026-09-01T23:34:28Z",
   "kind": "comment",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "text": "@pablomartin4btc @danielabrozzoni\nThanks for the suggestions. Great catches.\nI split the change into two commits and credited each of you as a co-author on the respective commit."
  },
  {
   "t": "2026-09-11T01:30:48Z",
   "kind": "review_comment",
   "who": "pablomartin4btc",
   "assoc": "MEMBER",
   "path": "src/net.h",
   "commit": "4ebff1af2acc6e846eb7932ff8ece03ffa3a9066",
   "in_reply_to": null,
   "text": "minor nit: this logs only the rejected value, not what it matched against. For a user staring at their config wondering why an entry got dropped, \"Ignoring duplicate -addnode value: node.example:8333 (matches node.example)\" would be more actionable \u2014 especially since the whole point of this PR is catching non-obvious duplicates (different port representations), where \"this is a duplicate\" alone doesn't explain why. Not blocking, just a nice-to-have."
  },
  {
   "t": "2026-09-11T01:37:58Z",
   "kind": "review_comment",
   "who": "pablomartin4btc",
   "assoc": "MEMBER",
   "path": "src/net.cpp",
   "commit": "4ebff1af2acc6e846eb7932ff8ece03ffa3a9066",
   "in_reply_to": null,
   "text": "One more equivalence worth considering, in the same spirit as the host/port work here: hostname comparison is case-sensitive (`host == existing_host`), so `-addnode=Example.com` and `-addnode=example.com` wouldn't be caught as duplicates, since DNS hostnames are case-insensitive. This isn't new \u2014 the original exact-string check had the same gap \u2014 and it's arguably out of scope for this PR. But since the whole point here is catching duplicate representations of the same target rather than just literal string matches, case-folding the hostname comparison feels like a natural extension of the same fix rather than a separate concern, so wanted to flag it while this code is already being touched. No test currently covers it either way, consistent with it not being handled. Happy to leave it out if you'd rather keep this PR narrowly scoped to the port-equivalence case."
  },
  {
   "t": "2026-09-11T01:39:24Z",
   "kind": "review",
   "who": "pablomartin4btc",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "4ebff1af2acc6e846eb7932ff8ece03ffa3a9066",
   "text": "ACK 4ebff1af2acc6e846eb7932ff8ece03ffa3a9066\n\nThanks for taking my suggestion!\n\nLeft a couple of inline nits I noticed on a re-read \u2014 nothing blocking."
  }
 ],
 "labels_log": [
  {
   "t": "2026-09-01T23:34:29Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-02T00:35:18Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  }
 ],
 "state_log": [],
 "text_chars": 5618,
 "text_tokens_estimate": 1404,
 "changed_paths": [
  "src/net.cpp",
  "src/net.h",
  "src/test/net_peer_connection_tests.cpp",
  "test/functional/feature_config_args.py"
 ],
 "files": [
  {
   "path": "src/net.cpp",
   "add": 14,
   "del": 2
  },
  {
   "path": "src/net.h",
   "add": 7,
   "del": 7
  },
  {
   "path": "src/test/net_peer_connection_tests.cpp",
   "add": 15,
   "del": 0
  },
  {
   "path": "test/functional/feature_config_args.py",
   "add": 23,
   "del": 0
  }
 ],
 "test_lines": 38,
 "git": {
  "head": "4ebff1af2acc6e846eb7932ff8ece03ffa3a9066",
  "head_matches_backup": true,
  "base": "b88bffe550a3a017440aff6a189cdb5f79f9b060",
  "commits": [
   {
    "sha": "beadf6a96d",
    "subject": "net: reject equivalent addnode host-port pairs",
    "files": 2,
    "add": 29,
    "del": 2
   },
   {
    "sha": "4ebff1af2a",
    "subject": "init: add startup nodes through CConnman::AddNode",
    "files": 2,
    "add": 30,
    "del": 7
   }
  ],
  "patch_truncated": false
 },
 "input_hash": "8586c8260e63108e",
 "extracted_at": "2026-09-17T16:15:31+00:00"
}