Problem
In the topic search table (p2p/discover/topicindex/search.go), each bucket's asked set was a map[enode.ID]struct{} — it stored only node IDs, not the nodes. The bucket's IP-limit tracker (b.ips, searchBucketIPLimit = 1 per /24) gets an entry when a node is first added to new, and setAsked moves the node new → asked without touching b.ips.
Search.removeNode (added in #71) could therefore only free the IP-limit entry for nodes still in new, because that's the only set holding the *enode.Node (and thus the IP). Removing a node that had already moved to asked would drop it from the bucket but leave its /24 reserved for the rest of the search — blocking a same-subnet replacement (limit is 1 per /24).
This was not reachable on any current path: HandleErrorResponse only ever runs on a node still in new (a queried-but-unanswered node is not setAsked until a successful response, and answered nodes are never re-queried). It was a latent footgun — any future change that removed an asked node would silently leak the IP slot.
Fix
asked now stores *enode.Node (mirroring new), and removeNode releases the IP-limit entry whether the node was in new or asked. Regression test TestSearchRemoveAskedNodeFreesIP fails on the old code (reason=iplimit) and passes after.
Fixed in #71.
Problem
In the topic search table (
p2p/discover/topicindex/search.go), each bucket'saskedset was amap[enode.ID]struct{}— it stored only node IDs, not the nodes. The bucket's IP-limit tracker (b.ips,searchBucketIPLimit = 1per/24) gets an entry when a node is first added tonew, andsetAskedmoves the nodenew → askedwithout touchingb.ips.Search.removeNode(added in #71) could therefore only free the IP-limit entry for nodes still innew, because that's the only set holding the*enode.Node(and thus the IP). Removing a node that had already moved toaskedwould drop it from the bucket but leave its/24reserved for the rest of the search — blocking a same-subnet replacement (limit is 1 per/24).This was not reachable on any current path:
HandleErrorResponseonly ever runs on a node still innew(a queried-but-unanswered node is notsetAskeduntil a successful response, and answered nodes are never re-queried). It was a latent footgun — any future change that removed anaskednode would silently leak the IP slot.Fix
askednow stores*enode.Node(mirroringnew), andremoveNodereleases the IP-limit entry whether the node was inneworasked. Regression testTestSearchRemoveAskedNodeFreesIPfails on the old code (reason=iplimit) and passes after.Fixed in #71.