p2p/discover/topicindex: enforce waiting-time lower bound#81
Open
srene wants to merge 4 commits into
Open
Conversation
Implement the DISC-NG / TopDisc paper §6 (spec §2.1.5) anti-gaming lower bound on registration wait times. Once a registrant has been quoted a wait w(t1), any later quote w(t2) must satisfy w(t2) >= w(t1) - (t2 - t1): a re-quoted wait can only decay at real elapsed time, never be reset. Previously WaitTime was memoryless, so an incumbent could reset its accumulated wait to ~0 by re-registering through a brief occupancy dip (e.g. on ad expiry) and re-acquire the close-to-topic registrar slot uncontested. Outsiders that hit a full registrar got a wait above budget and dropped it. The close-registrar membership therefore never rotated, capping continuous-searcher recall in the 10k sims. The bound is tracked per (topic, advertiser id) and per (topic, IP) as a (value, timestamp) tuple in waitTimeState, consulted and refreshed in WaitTime via the table clock. Tuples are dropped once fully decayed (during TopicTable.Expire), so the maps stay bounded by the set of actively-waiting registrants. Adds TestTopicTableWaitTimeLowerBound, which loads the table, quotes an outsider, drains the table to collapse the instantaneous wait, and asserts the prior quote is not reset below w1 - elapsed (it fails without this change, where the quote drops straight back to ~0). Refs #80.
…refix The per-IP anti-gaming bound keyed on the full address, so an attacker could evade it by rotating the last octet (v4) or any of the low 64 bits (v6). Key it on the family's prefix instead — /24 for IPv4 (matching regBucketSubnet) and /64 for IPv6, the smallest real subnet boundary per docs/disc-ng-ipv6-policy.md §3.2 (#53). Transition-address re-routing (6to4/Teredo/NAT64) stays with the full policy in #54. Adds TestTopicTableWaitTimeBoundPrefix (v4 /24 and v6 /64); updates the lower-bound test's fresh node to a different /24 since same-/24 nodes now share a bound.
The wait modifier used max(topicMod+ipMod, 1e-6), so a fresh registrant (both modifiers ~0) owed a microsecond wait — far below RTT and the 1s admission slack, i.e. effectively instant registration. Replace the hack with the paper's additive G term (§6), waitTimeG=0.05, which floors the empty-table wait near 4.5s at the default 15-min AdLifetime. Now that the lower bound persists accumulated wait, G has a defined role. Adds TestTopicTableWaitTimeFloor.
Derive G = waitTimeFloor / (waitBaseModifier * AdLifetime) instead of a fixed dimensionless 0.05. AdLifetime then cancels in the empty-table wait, so the safety floor is a fixed duration (waitTimeFloor = 2s) regardless of AdLifetime, rather than the old 0.05 which meant 4.5s at 15m but 0.15s at 30s. 2s sits just above the 1s admission slack (~1s effective wait). Tests assert the floor is ~waitTimeFloor across AdLifetimes 30s/15m/1h.
harnen
self-requested a review
July 20, 2026 12:51
harnen
reviewed
Jul 20, 2026
| return time.Duration(math.Ceil(neededTime * float64(time.Second))) | ||
| // g is the §6 safety floor, derived so the empty-table floor equals | ||
| // waitTimeFloor independent of AdLifetime (AdLifetime cancels baseTime). | ||
| g := waitTimeFloor.Seconds() / (waitBaseModifier * tab.config.AdLifetime.Seconds()) |
| // Lower-bound anti-gaming state (§6 / spec §2.1.5): per active registrant, the | ||
| // last wait quoted and when. idBounds keys by (topic, id); ipBounds by | ||
| // (topic, IP prefix) so ID rotation from one address can't evade it. | ||
| idBounds map[idBoundKey]waitBound |
There was a problem hiding this comment.
If I understand correctly, this measn that we're storing value per registrar. We should only keep the value per IP and topic
| // last wait quoted and when. idBounds keys by (topic, id); ipBounds by | ||
| // (topic, IP prefix) so ID rotation from one address can't evade it. | ||
| idBounds map[idBoundKey]waitBound | ||
| ipBounds map[ipBoundKey]waitBound |
There was a problem hiding this comment.
I think we have to store IP bounds as a tree as well. Direct comparison won't work. E.g., the attacker's IP my not be in the topic table.
|
|
||
| // ipBoundKeys returns the per-IP lower-bound keys for n, one per address family, | ||
| // each aggregated to its prefix. | ||
| func (wt *waitTimeState) ipBoundKeys(t TopicID, n *enode.Node) []ipBoundKey { |
There was a problem hiding this comment.
Is it possible to have both IPv6 and IPv4 in n? If yes, we should discuss how to handle that.
| } | ||
|
|
||
| // ipBoundKeyFor builds the bound key for ip, aggregated to its family's prefix | ||
| // (/24 for v4, /64 for v6). It reports ok=false for LAN or malformed addresses. |
|
|
||
| // expireBounds drops lower-bound tuples that have fully decayed at 'now'. | ||
| func (wt *waitTimeState) expireBounds(now mclock.AbsTime) { | ||
| for k, b := range wt.idBounds { |
There was a problem hiding this comment.
A for lop migh be costly here. Maybe we should investigate some sorted data structures?
|
|
||
| // waitBaseModifier scales the occupancy-driven base wait (paper's E factor, | ||
| // tuned down 10x for the testbed). | ||
| waitBaseModifier = 0.1 |
There was a problem hiding this comment.
I'm not sure why this is needed. And what does the tuning down for testbed means?
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the TopDisc waiting-time lower bound (paper §6 / compliance §2.1.5). Closes #80.
Invariant
Once a registrant is quoted a wait
w(t1), a later quote must satisfyw(t2) >= w(t1) - (t2 - t1)— a re-quote can only decay at real-time rate, never reset lower.TopicTable.WaitTimewas memoryless, so an incumbent could let its ad expire and re-register through the brief occupancy dip at a near-zero re-quote, permanently holding the registrars closest to a topic. With the bound, accumulated wait persists, the slot becomes contestable, and the close set rotates.Changes (
topictable.go)waitTimeStatekeeps per-(topic, id)and per-(topic, IP-prefix)lower-bound tuples(value, timestamp);WaitTimeraises the computed wait to the highest still-active decayed bound and records the new bound.Expireprunes fully-decayed tuples./24v4 (matchingregBucketSubnet),/64v6 perdocs/disc-ng-ipv6-policy.md§3.2 — so it can't be evaded by rotating within one allocation. Transition-address re-routing stays with Task 2.6: IPv6 prefix policy implementation for DISC-NG IP-diversity scoring #54.G = waitTimeFloor / (waitBaseModifier * AdLifetime)withwaitTimeFloor = 2s— so the empty-table floor is 2s regardless of AdLifetime