{
 "number": 33663,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/33663",
 "title": "net: Filter addrman during address selection via AddrPolicy to avoid underfill",
 "author": "waketraindev",
 "author_association": "CONTRIBUTOR",
 "created_at": "2025-10-20T18:03:51Z",
 "updated_at": "2026-09-15T18:01:18Z",
 "age_days": 331,
 "draft": false,
 "labels": [
  "P2P"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "a02f911ef4aad0a084ebdf1d0f5fc70ca5bcce75",
 "head_ref": "2025-10-getaddressesunsafe-underfill",
 "head_repo": "waketraindev/bitcoin",
 "head_history": [
  {
   "t": "2025-10-22T10:55:36Z",
   "sha": "70e2204b614af0d83e138b060de1139b651e5445"
  },
  {
   "t": "2025-10-28T06:51:38Z",
   "sha": "934a7819ff038ff2cf9a62d8e9b5f64df39b006d"
  },
  {
   "t": "2025-10-28T07:01:49Z",
   "sha": "1a89761e128d96ea49fe45fc10ef3dc95d9e0f99"
  },
  {
   "t": "2025-10-28T10:07:33Z",
   "sha": "d7c5cbb7f9ad13b6fa7f8d359d86c68f512749af"
  },
  {
   "t": "2025-10-28T14:34:33Z",
   "sha": "4f39b9255bcb2d116e776e41beaef7817f9a734d"
  },
  {
   "t": "2025-11-17T11:19:14Z",
   "sha": "fb3efdb3d267924266134f63b1ba6ddda0b6ce81"
  },
  {
   "t": "2025-12-28T13:16:07Z",
   "sha": "33e7b6647979112fbe657e5c70790b869fa7af57"
  },
  {
   "t": "2025-12-28T14:45:54Z",
   "sha": "bb637a85c6d0fec27dbcdd24cf04ae3118bc7b60"
  },
  {
   "t": "2026-01-19T15:33:03Z",
   "sha": "33e59cf4ece4f91304500cce3e827ec80250d843"
  },
  {
   "t": "2026-01-20T21:30:37Z",
   "sha": "5ecd70b9a11c066e4242609ef11cd23201d92150"
  },
  {
   "t": "2026-06-26T07:35:29Z",
   "sha": "a02f911ef4aad0a084ebdf1d0f5fc70ca5bcce75"
  }
 ],
 "additions": 86,
 "deletions": 18,
 "changed_files": 6,
 "commit_count": 1,
 "size_bucket": "M",
 "mergeable_state": "clean",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "concept_ack": [
     {
      "login": "frankomosh",
      "url": "https://github.com/bitcoin/bitcoin/pull/33663#pullrequestreview-3464332521"
     },
     {
      "login": "sipa",
      "url": "https://github.com/bitcoin/bitcoin/pull/33663#pullrequestreview-4311315532"
     }
    ],
    "stale_ack": [
     {
      "login": "mzumsande",
      "url": "https://github.com/bitcoin/bitcoin/pull/33663#pullrequestreview-3605973252"
     }
    ]
   },
   "conflicts": [
    {
     "number": 34162,
     "title": "net: Avoid undershooting in GetAddressesUnsafe",
     "author": "fjahr"
    }
   ]
  }
 },
 "acks_parsed": {
  "mzumsande": {
   "kind": "ack",
   "hash": "fb3efdb3d267924266134f63b1ba6ddda0b6ce81",
   "t": "2025-12-22T23:00:46Z",
   "stale": true
  },
  "frankomosh": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2025-11-14T10:54:01Z",
   "stale": false
  },
  "sipa": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-05-18T15:04:13Z",
   "stale": false
  }
 },
 "acks_tally": {
  "ack": 0,
  "stale_ack": 1,
  "concept_ack": 2,
  "approach_ack": 0,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 0,
  "changes_requested": 0,
  "distinct_reviewers": [
   "Bicaru20",
   "ajtowns",
   "brunoerg",
   "fjahr",
   "frankomosh",
   "mzumsande",
   "sedited",
   "sipa"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "clean",
  "last_author_activity": "2026-06-26T07:35:29Z",
  "last_reviewer_activity": "2026-09-15T18:01:18Z",
  "last_reviewer": "frankomosh",
  "author_silent_days": 83,
  "waiting_on_author_days": 1,
  "days_since_update": 1
 },
 "refs": {
  "mentioned": [
   34162
  ],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [
   {
    "number": 34162,
    "type": "pull",
    "state": "closed",
    "merged": false,
    "merged_at": null,
    "title": "net: Avoid undershooting in GetAddressesUnsafe"
   }
  ],
  "conflicts": [
   34162
  ]
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [
  "src/addrman.cpp",
  "src/addrman.h",
  "src/net.cpp",
  "src/test/addrman_tests.cpp",
  "src/test/fuzz/addrman.cpp"
 ],
 "body": "This PR introduces `AddrMan::AddrPolicy`, a predicate that allows callers to\nexclude addresses during selection. The policy returns true to skip a given\nentry and is evaluated while holding `AddrMan::cs`.\n\nThe mechanism is used in `CConnman::GetAddressesUnsafe()` to filter out\nbanned and discouraged peers during address selection instead of removing\nthem afterward. This prevents P2P responses (`GETADDR`) and RPCs (`getnodeaddresses)` from\nreturning fewer results than requested when large portions of the address space are filtered.\n\nAdditional improvements:\n- Logs the number of filtered entries for diagnostics\n- Avoids redundant filtering passes in higher-level callers\n- Keeps locking behavior unchanged (the policy runs under `AddrMan::cs`)\n\n**Testing**\n- Repro: ban or discourage a large set of addresses, then call\n  `getnodeaddresses 1`,  previously underfilled.\n- After this change, the RPC returns up to the requested count as expected.\n\nNo behavior change for unaffected callers.",
 "commits": [
  {
   "sha": "a02f911ef4aad0a084ebdf1d0f5fc70ca5bcce75",
   "date": "2026-06-26T07:21:51Z",
   "message": "addrman, net: filter during selection via AddrPolicy to avoid underfill\n\nAdd AddrMan::AddrPolicy, a predicate returning true to exclude an address\nduring selection. This allows callers to apply custom filtering directly in\nAddrMan rather than post-processing results.\n\nUse the new policy in CConnman::GetAddressesUnsafe to exclude banned and\ndiscouraged peers while filling address requests. This fixes underfilled\nresults observed when banned peers were removed after selection, causing\nP2P responses from `GETADDR` to return fewer entries than requested.\n\nAlso log the number of filtered entries for debugging and diagnostics."
  }
 ],
 "timeline": [
  {
   "t": "2025-10-21T13:38:12Z",
   "kind": "review_comment",
   "who": "mzumsande",
   "assoc": "MEMBER",
   "path": "src/addrman.cpp",
   "commit": "c8cb2ea11454a6edd8d8a8a83bb1be3596e9ff17",
   "in_reply_to": null,
   "text": "here and below: would be good to expand this over multiple lines (clang-format)"
  },
  {
   "t": "2025-10-22T08:18:50Z",
   "kind": "review",
   "who": "mzumsande",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "c8cb2ea11454a6edd8d8a8a83bb1be3596e9ff17",
   "text": "Concept ACK\n\nThe main function of this is p2p  (answering GetAddr requests from peers), RPCs like `getnodeaddresses` are of secondary importance.\nSince the p2p use is also affected by this, the PR description should be changed to reflect this.\n\nThat said, it seems good to always try to return 1000 addresses in a GetAddr answer, so that peers don't know whether we have a long list of banned addresses or not."
  },
  {
   "t": "2025-10-22T10:55:36Z",
   "kind": "force_push",
   "who": "waketraindev",
   "commit": "70e2204b614af0d83e138b060de1139b651e5445"
  },
  {
   "t": "2025-10-22T13:02:01Z",
   "kind": "comment",
   "who": "waketraindev",
   "assoc": "CONTRIBUTOR",
   "text": "* updated PR description and commit message to highlight P2p getaddr responses\n* corrected formatting"
  },
  {
   "t": "2025-10-24T14:49:01Z",
   "kind": "review_comment",
   "who": "brunoerg",
   "assoc": "MEMBER",
   "path": "src/addrman.cpp",
   "commit": "a02f911ef4aad0a084ebdf1d0f5fc70ca5bcce75",
   "in_reply_to": null,
   "text": "You should add a functional/unit test for this. See uncovered new code: https://corecheck.dev/bitcoin/bitcoin/pulls/33663"
  },
  {
   "t": "2025-10-24T14:51:26Z",
   "kind": "review_comment",
   "who": "waketraindev",
   "assoc": "CONTRIBUTOR",
   "path": "src/addrman.cpp",
   "commit": "a02f911ef4aad0a084ebdf1d0f5fc70ca5bcce75",
   "in_reply_to": 2460863763,
   "text": "Will do, looking into coverege *and sonarcloud* right now"
  },
  {
   "t": "2025-10-28T06:51:38Z",
   "kind": "force_push",
   "who": "waketraindev",
   "commit": "934a7819ff038ff2cf9a62d8e9b5f64df39b006d"
  },
  {
   "t": "2025-10-28T06:55:33Z",
   "kind": "comment",
   "who": "waketraindev",
   "assoc": "CONTRIBUTOR",
   "text": "Added AddrPolicy coverage in addrman_tests"
  },
  {
   "t": "2025-10-28T07:01:49Z",
   "kind": "force_push",
   "who": "waketraindev",
   "commit": "1a89761e128d96ea49fe45fc10ef3dc95d9e0f99"
  },
  {
   "t": "2025-10-28T10:07:33Z",
   "kind": "force_push",
   "who": "waketraindev",
   "commit": "d7c5cbb7f9ad13b6fa7f8d359d86c68f512749af"
  },
  {
   "t": "2025-10-28T13:59:08Z",
   "kind": "review_comment",
   "who": "brunoerg",
   "assoc": "MEMBER",
   "path": "src/addrman.cpp",
   "commit": "a02f911ef4aad0a084ebdf1d0f5fc70ca5bcce75",
   "in_reply_to": null,
   "text": "Thanks for adding test coverage. By doing mutation testing for this PR, I noticed that the following mutant has not been killed (it means tests were not able to detect this change):\n\n```diff\ndiff --git a/src/addrman.cpp b/src/addrman.cpp\nindex c06fbb0673..4cc7073c3b 100644\n--- a/src/addrman.cpp\n+++ b/src/addrman.cpp\n@@ -854,7 +854,7 @@ std::vector<CAddress> AddrManImpl::GetAddr_(size_t max_addresses, size_t max_pct\n         // Filter by policy\n         if (policy && policy(ai)) {\n             ++filtered_addresses;\n-            continue;\n+            break;\n         }\n```\n\nThis is because you're checking that the addresses that are returned when filtering by port are - de facto - according to the policy. However, you could also check that all the proper addresses are being returned, I think just adding the following check would be enough:\n\n```diff\ndiff --git a/src/test/addrman_tests.cpp b/src/test/addrman_tests.cpp\nindex 987249104d..3073656ed5 100644\n--- a/src/test/addrman_tests.cpp\n+++ b/src/test/addrman_tests.cpp\n@@ -421,6 +421,7 @@ BOOST_AUTO_TEST_CASE(addrman_getaddr)\n     BOOST_CHECK_EQUAL(vAddrNoPolicy.size(), addedAddresses);\n     BOOST_CHECK_EQUAL(vAddrPolicyNoSkip.size(), vAddrNoPolicy.size());\n     BOOST_CHECK_EQUAL(vAddrPolicySkipAll.size(), 0);\n+    BOOST_CHECK_EQUAL(vAddrPolicyPort.size(), 4U);\n     BOOST_CHECK(std::none_of(vAddrPolicyPort.begin(), vAddrPolicyPort.end(), policyPortPolicy));\n```"
  },
  {
   "t": "2025-10-28T14:29:39Z",
   "kind": "review_comment",
   "who": "waketraindev",
   "assoc": "CONTRIBUTOR",
   "path": "src/addrman.cpp",
   "commit": "a02f911ef4aad0a084ebdf1d0f5fc70ca5bcce75",
   "in_reply_to": 2469690257,
   "text": "Thanks for running mutation testing and for review!\n\nI'm thinking:\n```cpp\n    const std::set<CAddress> vExpectedAddresses = {addr1, addr3, addr4, addr5};\n    BOOST_CHECK_EQUAL(vAddrPolicyPort.size(), vExpectedAddresses.size());\n    BOOST_CHECK(std::all_of(vAddrPolicyPort.begin(), vAddrPolicyPort.end(), [&](const CAddress& a){ return vExpectedAddresses.contains(a); }));\n    BOOST_CHECK(std::none_of(vAddrPolicyPort.begin(), vAddrPolicyPort.end(), policyPortPolicy));\n```"
  },
  {
   "t": "2025-10-28T14:34:33Z",
   "kind": "force_push",
   "who": "waketraindev",
   "commit": "4f39b9255bcb2d116e776e41beaef7817f9a734d"
  },
  {
   "t": "2025-10-28T14:39:34Z",
   "kind": "comment",
   "who": "waketraindev",
   "assoc": "CONTRIBUTOR",
   "text": "Added test verifying only expected addresses are returned and filtered ones excluded."
  },
  {
   "t": "2025-11-14T10:46:55Z",
   "kind": "review_comment",
   "who": "frankomosh",
   "assoc": "CONTRIBUTOR",
   "path": "src/net.cpp",
   "commit": "fb3efdb3d267924266134f63b1ba6ddda0b6ce81",
   "in_reply_to": null,
   "text": "Is there a way/test to verify that the `BanMan` methods here are safe to call while `AddrMan`'s lock is held?\n\nCan see a comment in `addrman.h` says the policy \"must not acquire locks that can conflict with `AddrMan::cs`\". So it would make sense to ensure deadlock scenario is not accidentally created."
  },
  {
   "t": "2025-11-14T10:53:36Z",
   "kind": "review_comment",
   "who": "frankomosh",
   "assoc": "CONTRIBUTOR",
   "path": "src/addrman.cpp",
   "commit": "4f39b9255bcb2d116e776e41beaef7817f9a734d",
   "in_reply_to": null,
   "text": "Would network mismatches also count as `filtered` i this case?"
  },
  {
   "t": "2025-11-14T10:54:01Z",
   "kind": "review",
   "who": "frankomosh",
   "assoc": "CONTRIBUTOR",
   "state": "COMMENTED",
   "commit": "4f39b9255bcb2d116e776e41beaef7817f9a734d",
   "text": "Concept ACK"
  },
  {
   "t": "2025-11-14T13:01:46Z",
   "kind": "review_comment",
   "who": "waketraindev",
   "assoc": "CONTRIBUTOR",
   "path": "src/net.cpp",
   "commit": "fb3efdb3d267924266134f63b1ba6ddda0b6ce81",
   "in_reply_to": 2527033639,
   "text": "The BanMan methods used here take `m_banned_mutex` and never call back into `AddrMan`, so the lock order in this policy is currently `AddrMan::cs`>`BanMan::m_banned_mutex` with no reverse lock order in the codebase.\n\nUsing the predicate here also enforces that `AddrMan::cs` is always taken first, before any `BanMan` locks which helps maintain a consistent lock order.\n\n`GetAddressesUnsafe()` is invoked on startup to load `peers.dat` with the responses being cached for `CConnman::GetAddresses()`, every 24h for `CConnman::ASMapHealthCheck()`, when a GETADDR message is called and the peer has permission `NetPermissionFlags::Addr` and finally by RPC `getnodeaddresses`.  I tested each of these manually and checked the lock ordering and lock wait times.\n\nRegarding ways to test for lock ordering, a debug build will enable `DEBUG_LOCKORDER` and `DEBUG_LOCKCONTENTION` which will log issues with lock ordering or lock waits (`--debug=lock`).\n(see: https://github.com/bitcoin/bitcoin/blob/master/doc/developer-notes.md#debug_lockorder)"
  },
  {
   "t": "2025-11-14T13:06:01Z",
   "kind": "review_comment",
   "who": "waketraindev",
   "assoc": "CONTRIBUTOR",
   "path": "src/addrman.cpp",
   "commit": "4f39b9255bcb2d116e776e41beaef7817f9a734d",
   "in_reply_to": 2527062353,
   "text": "Yes; Currently all skipped addresses in `AddrMan::GetAddr` are counted as `filtered`; If you'd prefer (or find it useful) separate counters for each reason (Network, Quality or Policy), I can add that."
  },
  {
   "t": "2025-11-17T08:07:01Z",
   "kind": "review_comment",
   "who": "frankomosh",
   "assoc": "CONTRIBUTOR",
   "path": "src/addrman.cpp",
   "commit": "4f39b9255bcb2d116e776e41beaef7817f9a734d",
   "in_reply_to": 2527062353,
   "text": "Maybe separate would be clearer for diagnostics. Up to you though,.. if you think the current logging is clear enough then that's fine too."
  },
  {
   "t": "2025-11-17T08:13:40Z",
   "kind": "review_comment",
   "who": "frankomosh",
   "assoc": "CONTRIBUTOR",
   "path": "src/net.cpp",
   "commit": "fb3efdb3d267924266134f63b1ba6ddda0b6ce81",
   "in_reply_to": 2527033639,
   "text": "[quoted text omitted]\nThanks. Might be worth a comment somewhere in the code documenting this ordering requirement so someone does not accidentally introduce the reverse path in future ?"
  },
  {
   "t": "2025-11-17T11:19:14Z",
   "kind": "force_push",
   "who": "waketraindev",
   "commit": "fb3efdb3d267924266134f63b1ba6ddda0b6ce81"
  },
  {
   "t": "2025-11-17T11:25:35Z",
   "kind": "review_comment",
   "who": "waketraindev",
   "assoc": "CONTRIBUTOR",
   "path": "src/addrman.cpp",
   "commit": "4f39b9255bcb2d116e776e41beaef7817f9a734d",
   "in_reply_to": 2527062353,
   "text": "Went with separate counters and also added the selected network to the log output for clarity.\nExample log entry:\n```\n[addrman] GetAddr returned 62823 random addresses from ipv4; 1104 filtered (0 network, 0 quality, 1104 policy)\n[addrman] GetAddr returned 0 random addresses from ipv6; 63927 filtered (63927 network, 0 quality, 0 policy)\n[addrman] GetAddr returned 0 random addresses from onion; 63927 filtered (66927 network, 0 quality, 0 policy)\n```"
  },
  {
   "t": "2025-11-17T11:27:38Z",
   "kind": "review_comment",
   "who": "waketraindev",
   "assoc": "CONTRIBUTOR",
   "path": "src/net.cpp",
   "commit": "fb3efdb3d267924266134f63b1ba6ddda0b6ce81",
   "in_reply_to": 2527033639,
   "text": "Updated comments for more clarity\nin addrman.h:\n```cpp\n    /** Predicate used to exclude addresses during selection.\n     *  Return true to skip the given address.\n     *\n     *  Runs while holding AddrMan::cs, so it must be non-blocking, must not\n     *  attempt to reacquire AddrMan::cs, and must preserve lock ordering to\n     *  avoid deadlocks.\n     */\n```\nin net.cpp -> GetAddressesUnsafe()\n```cpp\n        // This runs under AddrMan::cs, and BanMan checks take m_banned_mutex.\n        // The lock order here is AddrMan::cs -> m_banned_mutex.\n```"
  },
  {
   "t": "2025-11-17T16:01:57Z",
   "kind": "review_comment",
   "who": "frankomosh",
   "assoc": "CONTRIBUTOR",
   "path": "src/net.cpp",
   "commit": "fb3efdb3d267924266134f63b1ba6ddda0b6ce81",
   "in_reply_to": 2527033639,
   "text": "Thanks"
  },
  {
   "t": "2025-11-17T16:08:36Z",
   "kind": "review_comment",
   "who": "frankomosh",
   "assoc": "CONTRIBUTOR",
   "path": "src/addrman.cpp",
   "commit": "4f39b9255bcb2d116e776e41beaef7817f9a734d",
   "in_reply_to": 2527062353,
   "text": "Awesome"
  },
  {
   "t": "2025-12-22T22:33:35Z",
   "kind": "review_comment",
   "who": "mzumsande",
   "assoc": "MEMBER",
   "path": "src/net.cpp",
   "commit": "fb3efdb3d267924266134f63b1ba6ddda0b6ce81",
   "in_reply_to": null,
   "text": "nit: I don't think hints are helpful if the passed variables already have the same name, they just add noise."
  },
  {
   "t": "2025-12-22T22:35:50Z",
   "kind": "review_comment",
   "who": "mzumsande",
   "assoc": "MEMBER",
   "path": "src/net.cpp",
   "commit": "fb3efdb3d267924266134f63b1ba6ddda0b6ce81",
   "in_reply_to": null,
   "text": "indentation is incorrect here."
  },
  {
   "t": "2025-12-22T22:50:51Z",
   "kind": "review_comment",
   "who": "mzumsande",
   "assoc": "MEMBER",
   "path": "src/test/addrman_tests.cpp",
   "commit": "a02f911ef4aad0a084ebdf1d0f5fc70ca5bcce75",
   "in_reply_to": null,
   "text": "Would be nice to also have some coverage in the addrman fuzz test."
  },
  {
   "t": "2025-12-22T23:00:46Z",
   "kind": "review",
   "who": "mzumsande",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "fb3efdb3d267924266134f63b1ba6ddda0b6ce81",
   "text": "Code Review ACK fb3efdb3d267924266134f63b1ba6ddda0b6ce81\n\nAs far as I can see, the reason for creating and passing an `AddrPolicy` is that the alternative (having `addrman` reach into `banman` to request if an addr is banned) would introduce coupling between the modules, while the current approach only introduces an implied lock order, which is somewhat of a potential footgun - but I don't really see a better solution.\nFor example, we probably wouldn't want to remove entries from addrman when they are banned, because bans are often temporary."
  },
  {
   "t": "2025-12-25T22:05:39Z",
   "kind": "review_comment",
   "who": "fjahr",
   "assoc": "MEMBER",
   "path": "src/addrman.h",
   "commit": "fb3efdb3d267924266134f63b1ba6ddda0b6ce81",
   "in_reply_to": null,
   "text": "Hm, the `true = exclude` part doesn't have the same meaning as above because this is not a bool parameter. Maybe update this to be more precise. The `filtered` option doesn't say that it's optional so you might add that while you touch this."
  },
  {
   "t": "2025-12-25T22:09:40Z",
   "kind": "review_comment",
   "who": "fjahr",
   "assoc": "MEMBER",
   "path": "src/addrman.cpp",
   "commit": "a02f911ef4aad0a084ebdf1d0f5fc70ca5bcce75",
   "in_reply_to": null,
   "text": "A general problem with the separate tracking like this is that it's order dependent and so the values may actually be somewhat misleading. I.e. if there are some addrs filtered by policy but they are already caught by the net filter they won't be included here. Doing this in a cleaner way may be overengineering but I wanted to mention that this isn't ideal."
  },
  {
   "t": "2025-12-25T22:15:10Z",
   "kind": "review_comment",
   "who": "fjahr",
   "assoc": "MEMBER",
   "path": "src/addrman.cpp",
   "commit": "a02f911ef4aad0a084ebdf1d0f5fc70ca5bcce75",
   "in_reply_to": 2647288578,
   "text": "Adding to that: the non-optional filter is now in between two optional filters. Feels to me like the optional checks should go after the mandatory but maybe there are arguments for doing it the other way around."
  },
  {
   "t": "2025-12-25T22:21:15Z",
   "kind": "review_comment",
   "who": "fjahr",
   "assoc": "MEMBER",
   "path": "src/addrman.cpp",
   "commit": "a02f911ef4aad0a084ebdf1d0f5fc70ca5bcce75",
   "in_reply_to": null,
   "text": "Would be generally nice if the options that weren't used are also not logged."
  },
  {
   "t": "2025-12-25T22:29:39Z",
   "kind": "review",
   "who": "fjahr",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "fb3efdb3d267924266134f63b1ba6ddda0b6ce81",
   "text": "While leaving some nit-ish comments I have realized that if we want to add such a generalized concept for filtering, we should implement the current filtering with it as well. E.g. `network` and `filtered` params go away and we only have `AddrPolicy` as param to `GetAddr`. Then use `AddrPolicy` to implement what `network` and `filtered` were doing. That's a bigger refactor but having this current mix with this redundancy is kind of ugly and I don't really think it's worth it alone."
  },
  {
   "t": "2025-12-25T22:31:55Z",
   "kind": "review_comment",
   "who": "fjahr",
   "assoc": "MEMBER",
   "path": "src/addrman.cpp",
   "commit": "a02f911ef4aad0a084ebdf1d0f5fc70ca5bcce75",
   "in_reply_to": 2647288578,
   "text": "If my suggestion from the top level comment is taken this is kind of obsolete but if not i think rather than trying to count each filter individually I would rather just log the total filtered count and which filters were active. I think that's simpler and least confusing."
  },
  {
   "t": "2025-12-27T04:55:38Z",
   "kind": "comment",
   "who": "waketraindev",
   "assoc": "CONTRIBUTOR",
   "text": "[quoted text omitted]\n\nThanks for the suggestion! network and filtered serve distinct, standard purposes (network filters by network, filtered disables quality filtering), while AddrPolicy is specifically for custom filters that skip entries without reducing the returned count. Using AddrPolicy also avoids coupling AddrMan to other modules (like BanMan) for filtering."
  },
  {
   "t": "2025-12-27T14:03:42Z",
   "kind": "comment",
   "who": "fjahr",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nWhat distinguishes a \"standard\" from a \"custom\" filter? Is that documented somewhere? And if we need to add another filter that is \"standard\" we would add another param to the function rather than using `AddrPolicy`?\n\n[quoted text omitted]\nHuh? If they don't reduce the count how are they filtering anything? This is not what the code is doing, the code is filtering and reducing the returned count with it as well of course. Also demonstrated by the tests. Am I misunderstanding what you mean with count?\n\n[quoted text omitted]\nI don't see how that is an argument against using it for network and quality filters."
  },
  {
   "t": "2025-12-27T18:25:26Z",
   "kind": "comment",
   "who": "waketraindev",
   "assoc": "CONTRIBUTOR",
   "text": "[quoted text omitted]\n\nBy \"standard\" I mean filters that are intrinsic to `AddrMan` and operate on `AddrInfo` (for example network classification and quality filtering via `ai.IsTerrible(now)`), and whose logic does not depend on external subsystems. By \"custom\" I mean filters that depend on external state or policy decisions outside of `AddrMan` (for example `BanMan`), which is what `AddrPolicy` is intended to support.\nIf a future filter is intrinsic to `AddrMan` and depends on `AddrInfo`, adding it as a separate parameter would be consistent with the existing design. If it depends on external state, `AddrPolicy` is the appropriate mechanism.\n\n[quoted text omitted]\nCandidates are filtered and skipped during selection. The change here is *when* filtering happens. Previously, addresses were selected up to the target count and then banned or discouraged addresses were removed afterward, which caused the final `CConnman::GetAddressesUnsafe` result to be underfilled. With the current approach, filtering happens during selection, and the loop continues until the requested number of unfiltered addresses is reached or the address space is exhausted. So filtering still occurs, but it no longer causes `CConnman::GetAddressesUnsafe` to be underfilled relative to the request.\n\n[quoted text omitted]\nThe point is not that `AddrPolicy` cannot express network or quality filters, but that doing so would conflate concerns and duplicate logic. Network and quality filtering are `AddrMan` responsibilities and require access to `AddrInfo`. Expressing them via `AddrPolicy` would either broaden the policy contract or require reimplementing intrinsic `AddrMan` logic in call sites, without addressing a concrete issue in this PR. This change keeps the PR scoped to fixing underfilling and avoids introducing additional coupling or refactors.\n\n---\n\nTo keep this PR focused: its purpose is to fix the underfilling issue by moving ban and discourage filtering into the selection phase. The current design achieves that with minimal surface area change and without refactoring existing `AddrMan` responsibilities or introducing new abstractions. Broader refactors (for example unifying all filters behind `AddrPolicy` or introducing builders/factories) can be explored separately if there is concrete motivation, but are out of scope for this change. Unless there are functional issues with the current approach, I intend to keep the implementation as is."
  },
  {
   "t": "2025-12-27T21:42:41Z",
   "kind": "review_comment",
   "who": "fjahr",
   "assoc": "MEMBER",
   "path": "src/addrman.cpp",
   "commit": "fb3efdb3d267924266134f63b1ba6ddda0b6ce81",
   "in_reply_to": null,
   "text": "nit: ordering"
  },
  {
   "t": "2025-12-27T21:42:50Z",
   "kind": "review_comment",
   "who": "fjahr",
   "assoc": "MEMBER",
   "path": "src/addrman.h",
   "commit": "fb3efdb3d267924266134f63b1ba6ddda0b6ce81",
   "in_reply_to": null,
   "text": "nit: ordering"
  },
  {
   "t": "2025-12-28T00:22:33Z",
   "kind": "review",
   "who": "fjahr",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "fb3efdb3d267924266134f63b1ba6ddda0b6ce81",
   "text": "[quoted text omitted]\n\nThe docs don't mention any of this but instead the naming `AddrPolicy` and the docs suggest that it's a generalized concept and could be used for any kind of filtering. I still think there is no reason to restrict usage of `AddrPolicy` in this way when it appears to be possible to just do both.\n\n[quoted text omitted]\nIf preventing underfilling is the only goal of this PR I would suggest a simpler approach: For example request more addresses than `max_addresses` and then trim at the end. Could be done smartly with using `m_banned.size()` to make sure we only overshoot as much as necessary. But simplest would be to just request with `max_addresses = 0`. The current approach doesn't seem minimal and if there is no plan to use it more broadly it seems like overengineering. At least the conceptual limitations and \"policy contract\" that you are describing would need to be documented clearly in the code as well."
  },
  {
   "t": "2025-12-28T13:16:07Z",
   "kind": "force_push",
   "who": "waketraindev",
   "commit": "33e7b6647979112fbe657e5c70790b869fa7af57"
  },
  {
   "t": "2025-12-28T13:21:50Z",
   "kind": "comment",
   "who": "waketraindev",
   "assoc": "CONTRIBUTOR",
   "text": "added fuzz coverage, fixed identation, removed hints, ordered includes, cleared nits"
  },
  {
   "t": "2025-12-28T14:23:31Z",
   "kind": "review_comment",
   "who": "fjahr",
   "assoc": "MEMBER",
   "path": "src/test/fuzz/addrman.cpp",
   "commit": "33e7b6647979112fbe657e5c70790b869fa7af57",
   "in_reply_to": null,
   "text": "Why would you call the same line as above again? This line is completely redundant."
  },
  {
   "t": "2025-12-28T14:45:54Z",
   "kind": "force_push",
   "who": "waketraindev",
   "commit": "bb637a85c6d0fec27dbcdd24cf04ae3118bc7b60"
  },
  {
   "t": "2025-12-28T14:52:09Z",
   "kind": "review_comment",
   "who": "waketraindev",
   "assoc": "CONTRIBUTOR",
   "path": "src/test/fuzz/addrman.cpp",
   "commit": "33e7b6647979112fbe657e5c70790b869fa7af57",
   "in_reply_to": 2649731154,
   "text": "oopsie"
  },
  {
   "t": "2025-12-28T17:02:53Z",
   "kind": "comment",
   "who": "fjahr",
   "assoc": "MEMBER",
   "text": "It appears you aren't interested in addressing the rest of my comments @waketraindev so I have implemented my suggested approach in #34162 instead. I prefer this approach due to its simplicity, as stated above. It fixes the overshooting problem in 2 lines and adds a functional test case for the behavior change. I will leave it to the other reviewers to decide which approach they prefer."
  },
  {
   "t": "2025-12-28T23:17:11Z",
   "kind": "comment",
   "who": "Bicaru20",
   "assoc": "CONTRIBUTOR",
   "text": "I think @fjahr has a good point. If the main goal is simply to always return 1000 addresses in `GetAddr`, introducing a predicate feels like adding unnecessary complexity. I believe his solution is cleaner.\n\n[quoted text omitted]\nI agree. If a generalized filtering concept is added, it should replace the current filtering mechanism. This would make it easier to add new filters in the future. However, for the problem trying to be solved, I think it\u2019s complicating things too much."
  },
  {
   "t": "2025-12-29T19:02:31Z",
   "kind": "comment",
   "who": "waketraindev",
   "assoc": "CONTRIBUTOR",
   "text": "[quoted text omitted]\n\nRegarding the \"over-engineering\" concerns, the lambda(the predicate) here is inline, concise, and used only once.\nIt encapsulates a simple banned/discouraged filter applied during selection to ensure max_addresses are correctly returned.\nThis approach is both functionally necessary and efficient, and does not introduce unnecessary complexity, so I do not consider it over-engineering.\n\nOn the other hand, introducing a Builder to craft the policy, include other \"generalized\" filters, and export AddrInfo would be over-engineering in this context.\n\nThe #34162 approach is highly inefficient for a database of 70k+ records, with on-demand calls through RPC with a max_addresses limit, and whitelisted nodes. It is insufficient for my use cases.\n\nThough both performance and simplicity are in the eyes of the beholder ig.\n\nThanks for your feedback!"
  },
  {
   "t": "2026-01-05T15:24:19Z",
   "kind": "comment",
   "who": "fjahr",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nIf you could be more explicit with what your use-case is then maybe you could convince more people that your approach is really necessary, even when you refuse to use it more broadly. It comes down to if core wants to support your use case bad enough to take on the extra code to maintain, I guess. You have a lot of addresses, ok! But I don't understand how whitelists play into this. Can you elaborate on that?  And why would you do frequent calls to `getnodeaddresses` with a max_addresses limit, couldn't you just increase the limit?\n\nI did some rough high-level benchmarking using the functional test from my PR ([code](https://github.com/fjahr/bitcoin/commit/8f2b63286386b8dbd83981a4c33a4634530323d8)). Due to the bucket collisions I couldn't get beyond 64k addresses, but I guess this is close enough and I didn't try to get to an even higher number. I am using an arbitrary 1k banned addresses and 1k max limit.\n\nThis PR (#33663) has the best performance, not surprising:\n\n```\n2025-12-30T14:34:41.400316Z TestFramework (INFO): Test that getnodeaddresses returns max count even with banned addresses\n2025-12-30T14:34:41.770049Z TestFramework (INFO): Adding addresses to addrman...\n2025-12-30T14:36:31.483147Z TestFramework (INFO): Successfully added 62636 addresses\n2025-12-30T14:36:31.483466Z TestFramework (INFO): Banning 1000 addresses...\n2025-12-30T14:36:32.511395Z TestFramework (INFO): Requesting 1000 addresses...\n2025-12-30T14:36:32.523990Z TestFramework (INFO): getnodeaddresses returned 1000 addresses in 0.013s\n```\n\nMy alternative approach (#34162) is much slower, also not surprising, but at 0.5s it doesn't seem like this would break any workflows I can imagine:\n\n```\n2025-12-30T14:18:12.848562Z TestFramework (INFO): Test that getnodeaddresses returns max count even with banned addresses\n2025-12-30T14:18:13.218102Z TestFramework (INFO): Adding addresses to addrman...\n2025-12-30T14:19:58.335032Z TestFramework (INFO): Successfully added 63471 addresses\n2025-12-30T14:19:58.335895Z TestFramework (INFO): Banning 1000 addresses...\n2025-12-30T14:19:59.381759Z TestFramework (INFO): Requesting 1000 addresses...\n2025-12-30T14:19:59.940406Z TestFramework (INFO): getnodeaddresses returned 1000 addresses in 0.559s\n```\n\nAs I had said previously, my suggest PR could be tweaked to get a middle ground between performance and simplicity, like for example fetch not all but only 2x max limit addresses to get to comparable performance and this should really work in 99% percent of the cases I can imagine. But sure, it's not as clean as #34162 but performance is comparable to this PR ( [demo code](https://github.com/fjahr/bitcoin/commit/556dcada56aca9136184098e61a07526ab408009)):\n\n```\n2025-12-30T14:52:23.292663Z TestFramework (INFO): Test that getnodeaddresses returns max count even with banned addresses\n2025-12-30T14:52:23.663001Z TestFramework (INFO): Adding addresses to addrman...\n2025-12-30T14:54:09.192166Z TestFramework (INFO): Successfully added 63395 addresses\n2025-12-30T14:54:09.193381Z TestFramework (INFO): Banning 1000 addresses...\n2025-12-30T14:54:10.218400Z TestFramework (INFO): Requesting 1000 addresses...\n2025-12-30T14:54:10.238231Z TestFramework (INFO): getnodeaddresses returned 1000 addresses in 0.020s\n2025-12-30T14:54:11.333769Z TestFramework (INFO): Stopping nodes\n```\n\nI also learned from playing around with the numbers that the total number of addresses only has minor impact on the performance. The big performance impact is coming from the number of banned addresses. So maybe that's where the discussion on performance (aside from the goals) should focus. I am not sure I have a good grasp on what % of addresses are realistically banned or discouraged. @0xB10C maybe you can weigh in?"
  },
  {
   "t": "2026-01-19T15:33:03Z",
   "kind": "force_push",
   "who": "waketraindev",
   "commit": "33e59cf4ece4f91304500cce3e827ec80250d843"
  },
  {
   "t": "2026-01-20T21:30:37Z",
   "kind": "force_push",
   "who": "waketraindev",
   "commit": "5ecd70b9a11c066e4242609ef11cd23201d92150"
  },
  {
   "t": "2026-03-20T10:11:25Z",
   "kind": "comment",
   "who": "ajtowns",
   "assoc": "MEMBER",
   "text": "Should we be clearing banned addresses out of addrman? We already skip adding them if they're already banned, so I think it would be consistent. Presuming we did that, then undershooting significantly should become pretty rare, I think, and it would avoid banned addresses competing for slots with un-banned addresses."
  },
  {
   "t": "2026-03-20T10:26:54Z",
   "kind": "comment",
   "who": "mzumsande",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\n[Above](https://github.com/bitcoin/bitcoin/pull/33663#pullrequestreview-3360827654) I wrote: \"For example, we probably wouldn't want to remove entries from addrman when they are banned, because bans are often temporary.\"."
  },
  {
   "t": "2026-03-20T13:07:48Z",
   "kind": "comment",
   "who": "ajtowns",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nThe reason bans are temporary is because we hope that nodes that we ban change their behaviour so they no longer have any reason to be banned, no? To me, that seems like more of a reason to reconsider re-adding them to addrman once the ban expires, rather than keeping them in addrman while they are banned.\n\nI could see the argument for only removing `IsBanned()` but not `IsDiscouraged()`, since discouragement is automatic, probabilistic and not persisted to disk.\n\nI don't think there should be significant differences in addrman behaviour if you start a fresh node with a large banlist, vs if you apply a large banlist to a node that's been running for a week. Currently I think there is: starting with a banlist, you'll reject banned nodes from entering your addrdb; applying the banlist later will mean they're already there."
  },
  {
   "t": "2026-05-18T15:04:13Z",
   "kind": "review",
   "who": "sipa",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "5ecd70b9a11c066e4242609ef11cd23201d92150",
   "text": "Concept ACK\n\nBut if we're going this direction, I think we could reasonably move all selection logic into to function argument?\n\nI.e. provide just `std::vector<CAddress> CAddrMan::GetAddr(AddrMan::AddrPolicy& policy) const`, and then move the `max_addresses`, `max_pct`, `filtered` arguments into the lambda. It could receive the current number of selected elements + total number of addresses + IsTerribleness in addrman as arguments."
  },
  {
   "t": "2026-06-26T07:35:29Z",
   "kind": "force_push",
   "who": "waketraindev",
   "commit": "a02f911ef4aad0a084ebdf1d0f5fc70ca5bcce75"
  },
  {
   "t": "2026-09-09T09:53:45Z",
   "kind": "comment",
   "who": "sedited",
   "assoc": "MEMBER",
   "text": "@frankomosh can you take another look here?"
  },
  {
   "t": "2026-09-15T16:10:48Z",
   "kind": "review_comment",
   "who": "frankomosh",
   "assoc": "CONTRIBUTOR",
   "path": "src/test/addrman_tests.cpp",
   "commit": "a02f911ef4aad0a084ebdf1d0f5fc70ca5bcce75",
   "in_reply_to": null,
   "text": "These calls all pass `max_addresses=0, max_pct=0`, so both caps are off, and `nNodes` is the whole table. With no count to fill, they only check that the policy excludes the right addresses, and not that a filtered request still returns what was asked for, which I think is what the PR is for?\n\nHere is a mutant that reverts the fix\n\n```diff\n     for (unsigned int n = 0; n < vRandom.size(); n++) {\n-        if (addresses.size() >= nNodes)\n+        if (addresses.size() + filtered_addr_policy >= nNodes)\n             break;\n```\n\nI tried a test case that kills this mutant:\n\nTest case\n\n```cpp\nBOOST_AUTO_TEST_CASE(addrman_getaddr_policy_no_underfill)\n{\n    auto addrman = std::make_unique<AddrMan>(EMPTY_NETGROUPMAN, DETERMINISTIC, GetCheckRatio(m_node));\n    const CNetAddr source = ResolveIP(\"252.2.2.2\");\n\n    std::set<CService> skipped;\n    unsigned int added{0};\n\n    for (unsigned int i = 1; i <= 100; ++i) {\n        const CService addr{ResolveService(\"1.\" + ToString(i) + \".1.1\", 8333)};\n        if (!addrman->Add({CAddress(addr, NODE_NONE)}, source)) continue;\n        if (++added % 2 == 0) skipped.insert(addr);\n    }\n\n    const AddrMan::AddrPolicy policy = [&skipped](const CAddress& addr) {\n        return skipped.contains(addr);\n    };\n\n    BOOST_REQUIRE(addrman->Size() - skipped.size() >= 20);\n\n    // Roughly half the table is excluded by policy; the request must still be filled.\n    const std::vector<CAddress> result{addrman->GetAddr(\n        /*max_addresses=*/20,\n        /*max_pct=*/0,\n        /*network=*/std::nullopt,\n        /*filtered=*/false,\n        /*policy=*/policy)};\n\n    BOOST_CHECK_EQUAL(result.size(), 20U);\n    BOOST_CHECK(std::none_of(result.begin(), result.end(), policy));\n}\n```\n\nMutant: `check result.size() == 20U has failed [10 != 20]`"
  },
  {
   "t": "2026-09-15T17:43:57Z",
   "kind": "review_comment",
   "who": "frankomosh",
   "assoc": "CONTRIBUTOR",
   "path": "src/test/fuzz/addrman.cpp",
   "commit": "a02f911ef4aad0a084ebdf1d0f5fc70ca5bcce75",
   "in_reply_to": null,
   "text": "We could add an oracle on this ?\n\nFor example; a check that an empty policy and a policy that skips nothing should return the same number of addresses for any input. Since `GetAddr_` samples randomly, the contents differ between calls, but the count shouldn't.\n\na proposed check\n\n```diff\n-    (void)const_addr_man.GetAddr(max_addresses, max_pct, network,\n-                                 filtered);\n+    const auto baseline{const_addr_man.GetAddr(max_addresses, max_pct,\n+                                               network, filtered)};\n\n     CallOneOf(\n         fuzzed_data_provider,\n         [&] {\n             const AddrMan::AddrPolicy policyNoSkip = [](const CAddress&) { return false; };\n\n-            (void)const_addr_man.GetAddr(max_addresses, max_pct, network,\n-                                         filtered, policyNoSkip);\n+            const auto with_noskip{const_addr_man.GetAddr(max_addresses,\n+                                                          max_pct, network,\n+                                                          filtered,\n+                                                          policyNoSkip)};\n+            assert(baseline.size() == with_noskip.size());\n         },\n```"
  },
  {
   "t": "2026-09-15T17:54:44Z",
   "kind": "review_comment",
   "who": "frankomosh",
   "assoc": "CONTRIBUTOR",
   "path": "src/addrman.h",
   "commit": "a02f911ef4aad0a084ebdf1d0f5fc70ca5bcce75",
   "in_reply_to": null,
   "text": "is there something enforceable here,  like in banman's own `EXCLUSIVE_LOCKS_REQUIRED(!m_banned_mutex)` annotations,? ..or is it even necessary to do that for the case of this PR?"
  },
  {
   "t": "2026-09-15T18:01:18Z",
   "kind": "review",
   "who": "frankomosh",
   "assoc": "CONTRIBUTOR",
   "state": "COMMENTED",
   "commit": "a02f911ef4aad0a084ebdf1d0f5fc70ca5bcce75",
   "text": "Still concept ack, I think filtering during selection is good approach."
  }
 ],
 "labels_log": [
  {
   "t": "2025-11-08T11:17:19Z",
   "action": "labeled",
   "label": "P2P",
   "who": "DrahtBot"
  },
  {
   "t": "2026-01-19T12:05:17Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-01-19T17:40:04Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-01-20T15:59:41Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-01-20T22:54:34Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-06-15T14:31:56Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-06-26T09:41:40Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  }
 ],
 "state_log": [
  {
   "t": "2025-10-20T18:17:15Z",
   "kind": "ready_for_review",
   "who": "waketraindev"
  },
  {
   "t": "2025-11-03T18:02:02Z",
   "kind": "renamed",
   "who": "waketraindev",
   "from": "addrman, net: filter during address selection via AddrPolicy to avoid underfill",
   "to": "addrman, net: Filter during address selection via AddrPolicy to avoid underfill"
  },
  {
   "t": "2025-11-08T11:17:16Z",
   "kind": "renamed",
   "who": "waketraindev",
   "from": "addrman, net: Filter during address selection via AddrPolicy to avoid underfill",
   "to": "net: Filter during address selection via AddrPolicy to avoid underfill"
  },
  {
   "t": "2025-11-08T11:17:41Z",
   "kind": "renamed",
   "who": "waketraindev",
   "from": "net: Filter during address selection via AddrPolicy to avoid underfill",
   "to": "net: Filter addrman during address selection via AddrPolicy to avoid underfill"
  }
 ],
 "text_chars": 25145,
 "text_tokens_estimate": 6286,
 "changed_paths": [
  "src/addrman.cpp",
  "src/addrman.h",
  "src/addrman_impl.h",
  "src/net.cpp",
  "src/test/addrman_tests.cpp",
  "src/test/fuzz/addrman.cpp"
 ],
 "files": [
  {
   "path": "src/addrman.cpp",
   "add": 27,
   "del": 8
  },
  {
   "path": "src/addrman.h",
   "add": 12,
   "del": 1
  },
  {
   "path": "src/addrman_impl.h",
   "add": 2,
   "del": 2
  },
  {
   "path": "src/net.cpp",
   "add": 6,
   "del": 7
  },
  {
   "path": "src/test/addrman_tests.cpp",
   "add": 21,
   "del": 0
  },
  {
   "path": "src/test/fuzz/addrman.cpp",
   "add": 18,
   "del": 0
  }
 ],
 "test_lines": 39,
 "git": {
  "head": "a02f911ef4aad0a084ebdf1d0f5fc70ca5bcce75",
  "head_matches_backup": true,
  "base": "93012d7ff918eef41adf453bf10ddb4da21f3df4",
  "commits": [
   {
    "sha": "a02f911ef4",
    "subject": "addrman, net: filter during selection via AddrPolicy to avoid underfill",
    "files": 6,
    "add": 86,
    "del": 18
   }
  ],
  "patch_truncated": false
 },
 "input_hash": "9cd7855a4250b232",
 "extracted_at": "2026-09-17T16:15:31+00:00"
}