You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Even with advertiser binding fixed (#68), the registrar in p2p/discover/v5_udp.go does not track which tickets have already been redeemed. A valid ticket can be re-presented multiple times within its 5-second ticketValidityWindow, and each presentation walks the admission path independently.
This is a defence-in-depth concern: combined with the missing advertiser binding it produces the Sybil-sharing attack described in #68; on its own it allows an honest advertiser (or one whose identity-binding check passes) to retry the same ticket repeatedly inside the validity window, which is wasted work on the registrar at best and a replay DoS surface at worst.
It does not record that the ticket was just used. The REGTOPIC handler then calls topicTable.Register(...), which is the admission path proper. Nothing along the way prevents the same ticket bytes from being submitted again inside the 5s window and walking the admission path a second time.
Replay DoS by one advertiser: even after Tickets can be reused by multiple registrants (missing advertiser binding) #68 is fixed, the legitimate advertiser can flood the same ticket back at the registrar in a 5-second burst. The topic-table admission code dedupes per source per bucket so it won't produce multiple slots for the same identity, but each presentation still:
Runs MAC verification (SHA-256 over 58 bytes — cheap but non-zero)
Calls topicTable.Register which traverses the IP-similarity tree and waiting-time calculations
Issues either an admission or a fresh ticket back
At Sybil-scale this becomes a useful amplification primitive.
Subtle interaction with renewal: a ticket emitted by the registrar carries LastUsed as the moment it was issued. The advertiser is expected to wait WaitTimeIssued, then redeem within ticketValidityWindow = 5s. If the advertiser submits the same ticket multiple times, the registrar's view of timing doesn't advance per request — each one looks "fresh" because LastUsed is in the ticket bytes, not in the registrar's state. So nothing local moves forward to invalidate the second presentation.
Proposed fix
Track recently-redeemed tickets at the registrar and reject the second presentation. Smallest sufficient change:
Maintain a map[ticketDigest]struct{} of recently-redeemed tickets in topicSystem or alongside TicketSealer, where ticketDigest = sha256(ticketBytes) (cheap, 32 bytes per entry).
On every redemption, check whether the digest is already in the set; if yes, reject the REGTOPIC.
Insert the digest with a TTL >= ticketValidityWindow (e.g. 2 * ticketValidityWindow = 10s). After the window closes, the timing check rejects the ticket anyway, so the set entry can be reaped.
Reap entries lazily (sweep on insert when the set crosses a soft cap; or on a periodic timer).
Memory bound: at sustained-burst throughput of r REGTOPIC/sec the cache holds ~r × 10s entries; even at 10k REG/s that's 100k × 32 bytes = 3.2 MB. Per-registrar cost is bounded by the registrar's own admission rate (legitimate or not), and the cache is bounded in time by the validity window — no unbounded growth.
Alternative considered: invalidate by incrementing LastUsed on every retry. This requires registrar-side state keyed by something stable (the advertiser ID after #68, or the original FirstIssued), and produces near-identical semantics with more bookkeeping. The digest-cache is simpler and harder to get wrong.
Tests to add
topicindex/ticket_test.go: pack a ticket, unpack it once (succeeds), unpack the identical bytes a second time within the validity window → expect a "ticket already used" error.
Integration test: REGTOPIC with a previously-redeemed ticket → registrar responds as if no ticket were presented (i.e., issues a fresh ticket with a fresh wait), not as if the wait had been honoured.
Summary
Even with advertiser binding fixed (#68), the registrar in
p2p/discover/v5_udp.godoes not track which tickets have already been redeemed. A valid ticket can be re-presented multiple times within its 5-secondticketValidityWindow, and each presentation walks the admission path independently.This is a defence-in-depth concern: combined with the missing advertiser binding it produces the Sybil-sharing attack described in #68; on its own it allows an honest advertiser (or one whose identity-binding check passes) to retry the same ticket repeatedly inside the validity window, which is wasted work on the registrar at best and a replay DoS surface at worst.
What the registrar currently does
TicketSealer.Unpack(p2p/discover/topicindex/ticket.go:93) verifies:topicfieldnow ∈ [LastUsed + WaitTimeIssued, LastUsed + WaitTimeIssued + ticketValidityWindow](timing window)It does not record that the ticket was just used. The REGTOPIC handler then calls
topicTable.Register(...), which is the admission path proper. Nothing along the way prevents the same ticket bytes from being submitted again inside the 5s window and walking the admission path a second time.Attack / failure modes
Combined with Tickets can be reused by multiple registrants (missing advertiser binding) #68 (no advertiser binding): Sybil swarm fans the ticket out and all redeem in the validity window. One wait → N admissions.
Replay DoS by one advertiser: even after Tickets can be reused by multiple registrants (missing advertiser binding) #68 is fixed, the legitimate advertiser can flood the same ticket back at the registrar in a 5-second burst. The topic-table admission code dedupes per source per bucket so it won't produce multiple slots for the same identity, but each presentation still:
topicTable.Registerwhich traverses the IP-similarity tree and waiting-time calculationsAt Sybil-scale this becomes a useful amplification primitive.
Subtle interaction with renewal: a ticket emitted by the registrar carries
LastUsedas the moment it was issued. The advertiser is expected to waitWaitTimeIssued, then redeem withinticketValidityWindow = 5s. If the advertiser submits the same ticket multiple times, the registrar's view of timing doesn't advance per request — each one looks "fresh" becauseLastUsedis in the ticket bytes, not in the registrar's state. So nothing local moves forward to invalidate the second presentation.Proposed fix
Track recently-redeemed tickets at the registrar and reject the second presentation. Smallest sufficient change:
map[ticketDigest]struct{}of recently-redeemed tickets intopicSystemor alongsideTicketSealer, whereticketDigest = sha256(ticketBytes)(cheap, 32 bytes per entry).ticketValidityWindow(e.g.2 * ticketValidityWindow = 10s). After the window closes, the timing check rejects the ticket anyway, so the set entry can be reaped.Memory bound: at sustained-burst throughput of
rREGTOPIC/sec the cache holds ~r × 10sentries; even at 10k REG/s that's 100k × 32 bytes = 3.2 MB. Per-registrar cost is bounded by the registrar's own admission rate (legitimate or not), and the cache is bounded in time by the validity window — no unbounded growth.Alternative considered: invalidate by incrementing
LastUsedon every retry. This requires registrar-side state keyed by something stable (the advertiser ID after #68, or the originalFirstIssued), and produces near-identical semantics with more bookkeeping. The digest-cache is simpler and harder to get wrong.Tests to add
topicindex/ticket_test.go: pack a ticket, unpack it once (succeeds), unpack the identical bytes a second time within the validity window → expect a "ticket already used" error.Related