-
Notifications
You must be signed in to change notification settings - Fork 38.7k
net: Filter addrman during address selection via AddrPolicy to avoid underfill #33663
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
net: Filter addrman during address selection via AddrPolicy to avoid underfill #33663
Conversation
|
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers. Code Coverage & BenchmarksFor details see: https://corecheck.dev/bitcoin/bitcoin/pulls/33663. ReviewsSee the guideline for information on the review process.
If your review is incorrectly listed, please copy-paste ConflictsReviewers, this pull request conflicts with the following ones:
If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first. LLM Linter (✨ experimental)Possible places where named args for integral literals may be used (e.g.
2025-12-28 |
mzumsande
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Concept ACK
The main function of this is p2p (answering GetAddr requests from peers), RPCs like getnodeaddresses are of secondary importance.
Since the p2p use is also affected by this, the PR description should be changed to reflect this.
That 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.
c8cb2ea to
70e2204
Compare
|
70e2204 to
934a781
Compare
|
Added AddrPolicy coverage in addrman_tests |
934a781 to
1a89761
Compare
1a89761 to
d7c5cbb
Compare
d7c5cbb to
4f39b92
Compare
|
Added test verifying only expected addresses are returned and filtered ones excluded. |
frankomosh
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Concept ACK
4f39b92 to
fb3efdb
Compare
mzumsande
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review ACK fb3efdb
As 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.
For example, we probably wouldn't want to remove entries from addrman when they are banned, because bans are often temporary.
fjahr
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
src/addrman.h
Outdated
| * @param[in] max_pct Maximum percentage of addresses to return (0 = all). Value must be from 0 to 100. | ||
| * @param[in] network Select only addresses of this network (nullopt = all). | ||
| * @param[in] filtered Select only addresses that are considered good quality (false = all). | ||
| * @param[in] policy Optional predicate to exclude candidates during selection (true = exclude). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| const auto now{Now<NodeSeconds>()}; | ||
| std::vector<CAddress> addresses; | ||
| addresses.reserve(nNodes); | ||
| size_t filtered_addr_net = 0, filtered_addr_quality = 0, filtered_addr_policy = 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| } | ||
| LogDebug(BCLog::ADDRMAN, "GetAddr returned %d random addresses\n", addresses.size()); | ||
| LogDebug(BCLog::ADDRMAN, "GetAddr returned %zu random addresses from %s; %zu filtered " | ||
| "(%zu network, %zu quality, %zu policy)\n", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be generally nice if the options that weren't used are also not logged.
Thanks 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. |
What 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
Huh? 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?
I don't see how that is an argument against using it for network and quality filters. |
By "standard" I mean filters that are intrinsic to
Candidates 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
The point is not that To 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 |
fjahr
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By "standard" I mean filters that are intrinsic to
AddrManand operate onAddrInfo(for example network classification and quality filtering viaai.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 ofAddrMan(for exampleBanMan), which is whatAddrPolicyis intended to support.
If a future filter is intrinsic toAddrManand depends onAddrInfo, adding it as a separate parameter would be consistent with the existing design. If it depends on external state,AddrPolicyis the appropriate mechanism.
The 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.
To 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
AddrManresponsibilities or introducing new abstractions.
If 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.
fb3efdb to
33e7b66
Compare
|
added fuzz coverage, fixed identation, removed hints, ordered includes, cleared nits |
src/test/fuzz/addrman.cpp
Outdated
| CallOneOf( | ||
| fuzzed_data_provider, | ||
| [&] { | ||
| (void)const_addr_man.GetAddr(max_addresses, max_pct, network, filtered); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why would you call the same line as above again? This line is completely redundant.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oopsie
Add AddrMan::AddrPolicy, a predicate returning true to exclude an address during selection. This allows callers to apply custom filtering directly in AddrMan rather than post-processing results. Use the new policy in CConnman::GetAddressesUnsafe to exclude banned and discouraged peers while filling address requests. This fixes underfilled results observed when banned peers were removed after selection, causing P2P responses from `GETADDR` to return fewer entries than requested. Also log the number of filtered entries for debugging and diagnostics.
33e7b66 to
bb637a8
Compare
|
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. |
|
I think @fjahr has a good point. If the main goal is simply to always return 1000 addresses in
I 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’s complicating things too much. |
Regarding the "over-engineering" concerns, the lambda(the predicate) here is inline, concise, and used only once. On the other hand, introducing a Builder to craft the policy, include other "generalized" filters, and export AddrInfo would be over-engineering in this context. The #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. Though both performance and simplicity are in the eyes of the beholder ig. Thanks for your feedback! |
If 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 I did some rough high-level benchmarking using the functional test from my PR (code). 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. This PR (#33663) has the best performance, not surprising: My 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: As 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): I 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? |
This PR introduces
AddrMan::AddrPolicy, a predicate that allows callers toexclude addresses during selection. The policy returns true to skip a given
entry and is evaluated while holding
AddrMan::cs.The mechanism is used in
CConnman::GetAddressesUnsafe()to filter outbanned and discouraged peers during address selection instead of removing
them afterward. This prevents P2P responses (
GETADDR) and RPCs (getnodeaddresses)fromreturning fewer results than requested when large portions of the address space are filtered.
Additional improvements:
AddrMan::cs)Testing
getnodeaddresses 1, previously underfilled.No behavior change for unaffected callers.