feat(kad): provider record spillover - #537
Conversation
|
Hi @gmelodie, thanks for working on this! Provider record spillover is a good idea and I believe the network would benefit from it — hotspots on popular keys are a long-standing issue (libp2p/go-libp2p-kad-dht#316). Some high-level feedback before a detailed review:
|
| Support for this extension is optional. A node that supports it MUST include a | ||
| `providerStatus` field (field 11, see [Protobuf](#protobuf)) in its | ||
| `ADD_PROVIDER` response: | ||
|
|
There was a problem hiding this comment.
Could we define the complete ADD_PROVIDER response?
It is not clear if the server should echo the request with providerStatus added, or send a new message containing only type and providerStatus.
It would also help to define which fields must be present and what response is sent for an invalid request.
| reject all `ADD_PROVIDER` requests to suppress its advertisement. Spillover | ||
| mitigates unanimous rejection: any shortfall below `k` causes the advertiser | ||
| to route around those peers and store the record on nodes beyond the adversary's | ||
| controlled set. An adversary can limit—but not prevent—replication by mixing accepts |
There was a problem hiding this comment.
limit-but not prevent-replication
this aint guaranteed, because a malicious node can return accepted and not store the record. In this case Silence counts as accepted. If k peers do this, then the advertiser reaches the target and does not spill over.
It probably makes sense to indicate that the protocol cannot prove that storing the provider happened, and the responses are acknowledgments and not proof of record being stored.
| An absent `providerStatus` field — whether because the responding node does not | ||
| support this extension or due to a timeout — MUST be interpreted as `accepted` | ||
| by the advertiser. |
There was a problem hiding this comment.
I think we gotta diferentiate between missig a field in the response or not receiving any response at all:
A missing field probably means that the server side is using the old response format. However it is possible for the server to not send a response too while keeping the stream open (see here and here), meaning that if a client is waiting for a timeout, it would timeout.
Also there's the possibility of the stream closing, resetting, dial failings or writes failing.
The spec needs to define which of these outcomes count as accepted. i.e. transport failures for sure should not count as accepted
| ``` | ||
|
|
||
| Field 11 is optional; older implementations that do not know about it ignore | ||
| it, maintaining full wire-level backward compatibility. |
There was a problem hiding this comment.
I agree with this being wire-level backward compatible, but seems to me that we need to expand the next section a bit.
i.e.
Currently other implementations like go-libp2p-kad-dht do not return nor read ADD_PROVIDER response
This basically means that:
- go-libp2p wont detect rejection from a server that contains Field 11
- a client that supports this must wait for a timeout when contacting go-libp2p.
This should probably be mentioned.
I.e. these are the possible combinations that happen:
- Old client / Old server = Provider is published as fire and forget, record is stored
- Old client / New server = Client sends the provider and assumes succes, but server may have discarded the record
- New client / Old server = Client waits for
providerStatusbut old server never replies, so every request translates into a timeout - New client / Old server = Rejection and the spillover work as intended
There was a problem hiding this comment.
Or as mentioned in one of the comments, perhaps it's worth of a new kademlia version, in case the consensus determines that this differs too much from base kademlia implementation
| first `maxProvidersPerKey` providers to register for a key can hold their slots | ||
| indefinitely simply by refreshing their records. Nodes that fill up later deny | ||
| new providers entry, so the stored provider set becomes permanently frozen around | ||
| whoever arrived first. Implementations MAY therefore pair `maxProvidersPerKey` |
There was a problem hiding this comment.
There seems to be a contradiction between this and line 73
the node MUST reject the request
Does this optional eviction policy overrides line 73 that the node MUST reject the new provider?
If a node evicts an existing provider when a new one arrives, should it store the new record and return accepted or should it evict the old recod and still return rejected? (this would remove a record without adding a replacement).
Please define the precedence between a record being rejected and being evicted.
| with an eviction policy — for example, evicting the record with the oldest | ||
| `timeReceived` when the limit is reached and a new (non-incumbent) provider | ||
| advertises — to ensure the stored set can rotate over time and is not captured | ||
| by early registrants. |
There was a problem hiding this comment.
Worht mentioning that this could let an attacker evict honest providers if they rotate their peer ID?
| ### Relationship to base advertisement | ||
|
|
||
| This algorithm is a generalisation of the base `ADD_PROVIDER` procedure. When | ||
| the closest chunk alone satisfies the replication target, behaviour is identical |
There was a problem hiding this comment.
This only happens when α >= k, right?
Summary
Adds
kad-dht/provider-record-spillover.md, a working-draft specification for an opt-in extension to the Kademlia DHTADD_PROVIDERflow that addresses provider record hotspots on popular keys.Problem
Under the base
kad-dhtprotocol, thekclosest peers to a key unconditionally accumulate everyADD_PROVIDERrecord for it.For popular keys, this creates an unbounded load concentration with no mechanism for a peer to decline new provider records or for advertisers to spill over to other peers. This has been a known scalability issue, including:
libp2p/go-libp2p-kad-dht#316ipfs/kubo#5613This proposal implements the “rejection + sloppy hashing / spillover” approach originally proposed in
libp2p/specs#163, drawing from the Coral DHT paper:What This Spec Defines
Server-side — provider record limits
A node MAY enforce
maxProvidersPerKey.Once at capacity, it MUST reject
ADD_PROVIDERrequests from new providers for that key, while always accepting re-advertisements from existing providers so records can continue to be refreshed.Wire protocol
A new optional
providerStatusfield (field 11) is added to theADD_PROVIDERresponse message with two possible values:accepted (0)rejected (1)For backward compatibility:
providerStatusfield MUST be treated asaccepted.providerStatusis response-only.Client-side — spillover algorithm
The advertiser performs the normal iterative
FIND_NODElookup, then processes the sorted candidate list in chunks of sizeα, from closest to farthest.After each chunk, it counts peers that:
toward the replication target
k.If
khas not yet been reached, the advertiser continues to the next chunk (a spillover round).The process stops when either:
ksuccessful placements are reached, orNon-responding peers count as accepted to preserve compatibility with nodes that predate this extension.
Compatibility
This extension is fully backward compatible.
Nodes that do not implement the extension:
providerStatusproviderStatusif receivedNo changes are made to:
GET_PROVIDERSFIND_NODERelated