pub struct DhtService { /* private fields */ }Expand description
The DHT service for one node. Cloneable-by-Arc internally; wrap in Arc to share between the
serving task (inbound RPC) and querying callers.
Implementations§
Source§impl DhtService
impl DhtService
Sourcepub fn new(
local_id: PeerId,
local_addresses: Vec<CandidateAddr>,
config: DhtConfig,
transport: Arc<dyn DhtTransport>,
) -> Self
pub fn new( local_id: PeerId, local_addresses: Vec<CandidateAddr>, config: DhtConfig, transport: Arc<dyn DhtTransport>, ) -> Self
Create a service for the node identified by local_id, advertising local_addresses in the
provider records it announces, driving RPC over transport.
Sourcepub async fn bootstrap(
&self,
peers: &[BootstrapPeer],
) -> Result<usize, DhtError>
pub async fn bootstrap( &self, peers: &[BootstrapPeer], ) -> Result<usize, DhtError>
Seed the routing table from peers and populate it by looking up this node’s own id (the
canonical Kademlia bootstrap: a self-lookup fills the buckets around us). Returns the number
of distinct peers now known.
Safe to call repeatedly (on reconnect / when new bootstrap peers arrive) — it merges, never resets.
Sourcepub async fn add_peer(&self, peer_id: &PeerId, addresses: Vec<CandidateAddr>)
pub async fn add_peer(&self, peer_id: &PeerId, addresses: Vec<CandidateAddr>)
Add a single live peer to the routing table as it connects (e.g. a dig-gossip
PoolEvent::PeerAdded), WITHOUT the network round-trip bootstrap does.
This is the LIVE seam the one-shot pre-connect bootstrap cannot cover: in a freshly-formed
network the pool is empty when bootstrap runs, so routing stays empty and find_providers
finds nobody. Feeding each connected peer here populates routing as the pool fills, which is
what makes cross-node discovery work (#1574). Idempotent — re-adding a known peer merges its
address(es) via the routing table’s insert policy; adding this node’s own id is a no-op.
Sourcepub async fn remove_peer(&self, peer_id_hex: &str) -> bool
pub async fn remove_peer(&self, peer_id_hex: &str) -> bool
Remove a peer from the routing table as it leaves (a dig-gossip PoolEvent::PeerRemoved),
keeping routing accurate so lookups don’t seed from a dead contact. Returns whether it was
present. peer_id_hex is the 64-char hex id (as carried on Contact::provider_peer_id /
PeerId::to_hex).
Sourcepub async fn find_node(
&self,
peer_id: &PeerId,
) -> Result<Vec<Contact>, DhtError>
pub async fn find_node( &self, peer_id: &PeerId, ) -> Result<Vec<Contact>, DhtError>
Find the k peers closest to peer_id (the routing primitive). Runs an iterative
find_node lookup and returns the converged closest contacts.
Sourcepub async fn find_providers(
&self,
content: &ContentId,
) -> Result<Vec<ProviderRecord>, DhtError>
pub async fn find_providers( &self, content: &ContentId, ) -> Result<Vec<ProviderRecord>, DhtError>
Find the providers of content — the peers holding it. Answers from this node’s
discovery cache when a recent lookup for the same key is still live (SPEC §6.8);
otherwise runs an iterative find_providers lookup toward the content key, caches what it
learns, and returns every live provider record collected (deduped by provider). The node
then connects to those providers over dig-nat and fetches via the L7 peer RPC.
Cached answers are what make a later direct dial free (dig_ecosystem#3128 requirement 7):
a .dig fetch issues many requests against the same store, and without the cache each one
paid a fresh Kademlia walk. A live cache entry is treated as evidence that this node
completed a lookup for the key recently, so the walk is skipped entirely — records this node
holds AUTHORITATIVELY are deliberately NOT such evidence, since they may be its own announce
and short-circuiting on them would stop a publisher ever learning the other holders of its
own content.
A cached holder is a claim by an untrusted peer, so a dial to it may fail. That costs one
failed dial, never a wrong answer — the content is accepted because it verifies against the
merkle root, never because a peer supplied it (NC-12). A caller that finds every cached
candidate undialable calls forget_discovered and asks again,
which re-runs the full walk.
Returns an empty vec (not an error) when the content simply has no known providers; returns
DhtError::NoPeers only when there is no one to ask (empty routing table + no bootstrap).
Sourcepub async fn cached_providers(&self, content: &ContentId) -> Vec<ProviderRecord>
pub async fn cached_providers(&self, content: &ContentId) -> Vec<ProviderRecord>
The provider records this node has CACHED for content from its own lookups, live as of
now — the direct-dial shortcut requirement 7 exists to provide, with no network round-trip
and no fallback walk.
§These records MUST NOT be re-served to anyone
They are hearsay: some peer along a lookup said that some other peer holds this content, and
nothing authenticated that claim — unlike an authoritative record, which either names the
mTLS-verified caller that announced it or was signature-checked by the caller of
ingest_verified_provider. Hearsay belongs on the FETCH
path, where a wrong candidate is merely a wasted dial because the merkle bind catches it. On
the ASSERTION path — an inbound find_providers, a redirect answer, anything a stranger
reads — it becomes THIS NODE’S claim about the world, and re-serving it would launder an
attacker’s fabricated holder into an answer other nodes trust. This node therefore never
serves the cache (see handle_request_from, which reads the
authoritative store only) and never publishes it (see
provider_snapshot).
Sourcepub async fn forget_discovered(&self, content: &ContentId) -> usize
pub async fn forget_discovered(&self, content: &ContentId) -> usize
Forget every cached provider for content, so the next
find_providers runs a real lookup again. Returns how many cached
records were dropped.
This is what keeps a cache miss CHEAP and keeps it from being mistaken for absence: a caller that has tried every cached candidate and reached none of them calls this and asks again, rather than concluding the content has no providers. It touches only this node’s own cache — never the authoritative store, so it can neither censor a key this node serves nor be observed by any other peer.
Sourcepub async fn announce_provider(
&self,
content: &ContentId,
) -> Result<usize, DhtError>
pub async fn announce_provider( &self, content: &ContentId, ) -> Result<usize, DhtError>
Announce that THIS node holds content: build a provider record (this node’s peer_id +
addresses, expiring at now + provider_ttl), store it locally, remember to republish it, and
PUT it at the k nodes closest to the content key. Returns how many peers accepted the PUT.
Called when the node’s inventory gains content (a new capsule/root/resource it now serves).
Sourcepub async fn withdraw_provider(&self, content: &ContentId) -> bool
pub async fn withdraw_provider(&self, content: &ContentId) -> bool
Stop announcing content (the node no longer holds it). The record ages out of the DHT via
TTL; we just stop republishing it. Returns whether it was being announced.
This is the passive withdraw: it leaves this node’s own local provider record in place
(it only expires with TTL) and merely stops re-publishing it, so a find_providers on this
node may still return self until the local record’s TTL elapses. For an immediate
own-retract — the local-state half of the #1423 evict+retract step — use
retract_own_provider.
Sourcepub async fn ingest_verified_provider(
&self,
record: ProviderRecord,
) -> PutOutcome
pub async fn ingest_verified_provider( &self, record: ProviderRecord, ) -> PutOutcome
Ingest a provider record for a THIRD-PARTY holder that the caller has ALREADY verified was
signed by record.provider_peer_id — the inbound-add half of the real-time holdings map
(SPEC §6.5). Returns the store admission outcome.
This is the authenticated push path a node’s announce receiver calls after verifying a
signed HoldingsAnnounce (dig-gossip opcode 222): the holder’s signature has replaced mTLS
attribution as the proof of who provides the content, so — unlike the serving-side
add_provider (§6.4) — this method bypasses the mTLS self-announce identity check (the
caller, not the DHT, established authenticity). dig-dht itself stays crypto-free (SPEC §15):
it NEVER verifies a signature; passing an unverified record here is a caller bug that
poisons the local provider set.
Every other admission guard still applies exactly as for add_provider: the address list is
capped (MAX_ADDRESSES_PER_RECORD), expires_at is
clamped to min(record.expires_at, now + provider_ttl) (§6.2), and the per-key / global
admission caps (§6.3) are enforced — an over-capacity ingest returns
PutOutcome::RejectedOverCapacity and stores nothing. On acceptance the holder is folded
into the routing table so this node can reach it.
Sourcepub async fn remove_provider_record(
&self,
content_key: &str,
provider_peer_id: &str,
) -> bool
pub async fn remove_provider_record( &self, content_key: &str, provider_peer_id: &str, ) -> bool
Remove exactly the local provider record for (content_key, provider_peer_id) — the
inbound-retract half of the real-time holdings map (SPEC §6.6). Returns whether a record
was removed.
content_key and provider_peer_id are the 64-hex forms as they appear on a
ProviderRecord (content → content.to_key().to_hex(); the holder’s peer_id hex).
The caller MUST have verified the retract was signed by that same provider_peer_id
(authenticated retract): a retract signed by one holder removes ONLY that holder’s record and
can never evict another provider of the same key (censorship-resistance, §6.6). dig-dht does
not verify the signature (SPEC §15) — that is the caller’s responsibility.
Sourcepub async fn provider_snapshot(&self, max_keys: usize) -> ProviderSnapshot
pub async fn provider_snapshot(&self, max_keys: usize) -> ProviderSnapshot
A bounded, AGGREGATED view of this node’s provider store — content keys and their live provider COUNTS, with no provider identities (dig_ecosystem #1935).
Exposed so a node can answer the relay’s RLY-009 get_dht_records without the caller needing
access to the store itself. Because a Kademlia node holds records for keys near its OWN
peer_id, this describes MANY OTHER peers’ content rather than what this node caches — which
is what makes the union across nodes a usable view of the network’s content layer.
max_keys bounds the result; see ProviderStore::snapshot for the truncation and privacy
contract. Expired records are excluded as of the current time, so the counts agree with what
find_providers would actually return.
Sourcepub async fn retract_own_provider(&self, content: &ContentId) -> bool
pub async fn retract_own_provider(&self, content: &ContentId) -> bool
Actively retract THIS node’s own provider record for content: remove the local record AND
stop republishing it, so find_providers on this node stops returning self as a holder
immediately (SPEC §6.6). Returns whether this node was providing the content (a local record
existed or the key was being announced).
This is the local-state half of the #1423 atomic evict + retract step (on an LRU cache
eviction the node no longer serves the content). Unlike the passive
withdraw_provider (which leaves the local record to expire via
TTL), this deletes it now. The copies previously PUT at the k closest peers are NOT deleted
by this call — they age out via TTL, or are removed sooner when dig-node floods the signed
retract announce and each recipient calls
remove_provider_record.
Sourcepub async fn holders_of(
&self,
content: &ContentId,
) -> Result<Vec<PeerId>, DhtError>
pub async fn holders_of( &self, content: &ContentId, ) -> Result<Vec<PeerId>, DhtError>
The peer_ids of the peers that hold content — a thin, address-free convenience over
find_providers for callers that only need “which peers hold X”
(e.g. an RPC holder-set query) and do not dial the holders themselves.
find_providers remains the PRIMARY API: it returns full ProviderRecords with candidate
addresses, which dig-download needs to actually connect and fetch. This method runs the same
distributed iterative lookup and simply projects each record to its holder peer_id
(records with a malformed peer id are skipped; the set is already deduped by provider).
Sourcepub async fn republish(&self) -> usize
pub async fn republish(&self) -> usize
Republish every content key this node still announces — re-runs the announce PUT so provider
records never expire while the node is online. Call on the DhtConfig::republish_interval.
Returns the number of content keys republished.
Sourcepub async fn refresh_buckets(&self) -> usize
pub async fn refresh_buckets(&self) -> usize
Refresh populated buckets by looking up a random key in each — keeps the routing table fresh
as peers churn. Call on the DhtConfig::refresh_interval. Returns the number of buckets
refreshed.
Sourcepub async fn gc(&self) -> usize
pub async fn gc(&self) -> usize
Drop expired provider records from BOTH the authoritative store and the discovery cache (SPEC §6.8). Call periodically (piggy-backs on republish/refresh). Returns the total number of records removed.
One now for both sweeps, so a maintenance tick cannot leave the two stores disagreeing
about which instant it ran at.
Sourcepub async fn ping(&self, peer: &Contact) -> bool
pub async fn ping(&self, peer: &Contact) -> bool
Ping a peer for liveness; on failure, evict it from the routing table. Used by the ping-and-replace maintenance when a bucket is full. Returns whether the peer is alive.
Sourcepub async fn handle_request(&self, request: DhtRequest) -> DhtResponse
pub async fn handle_request(&self, request: DhtRequest) -> DhtResponse
Answer an inbound DHT request from another node, without a known caller identity. Prefer
handle_request_from on an authenticated transport (it lets the
responder learn the caller and populate its routing table bidirectionally, the way Kademlia
tables fill).
Sourcepub async fn handle_request_from(
&self,
caller: Option<Contact>,
request: DhtRequest,
) -> DhtResponse
pub async fn handle_request_from( &self, caller: Option<Contact>, request: DhtRequest, ) -> DhtResponse
Answer an inbound DHT request, folding the authenticated caller into the routing table.
This is the server half — a dig-node wires it to inbound DHT streams, passing the caller’s
mTLS-verified Contact as caller. Learning the caller from every inbound RPC is how a
Kademlia node discovers peers without an explicit announce: a node that talks to you becomes
a candidate in your table. The caller MUST come from the authenticated transport (the mTLS
peer_id), never from the request body — identity is not self-asserted.
It reads/writes only local state (routing table + provider store) and never makes outbound RPCs, so it cannot recurse or block on the network.
Sourcepub async fn known_closest(&self, target: &Key) -> Vec<Contact>
pub async fn known_closest(&self, target: &Key) -> Vec<Contact>
The contacts currently in this node’s routing table closest to target (diagnostic /
introspection — the peers this node knows without any network round-trip).
Sourcepub async fn routing_len(&self) -> usize
pub async fn routing_len(&self) -> usize
The number of peers currently in this node’s routing table (diagnostic / metrics).
Auto Trait Implementations§
impl !RefUnwindSafe for DhtService
impl !UnwindSafe for DhtService
impl Freeze for DhtService
impl Send for DhtService
impl Sync for DhtService
impl Unpin for DhtService
impl UnsafeUnpin for DhtService
Blanket Implementations§
Source§impl<'a, T, E> AsTaggedExplicit<'a, E> for Twhere
T: 'a,
impl<'a, T, E> AsTaggedExplicit<'a, E> for Twhere
T: 'a,
Source§impl<'a, T, E> AsTaggedImplicit<'a, E> for Twhere
T: 'a,
impl<'a, T, E> AsTaggedImplicit<'a, E> for Twhere
T: 'a,
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
Source§impl<T> FmtForward for T
impl<T> FmtForward for T
Source§fn fmt_binary(self) -> FmtBinary<Self>where
Self: Binary,
fn fmt_binary(self) -> FmtBinary<Self>where
Self: Binary,
self to use its Binary implementation when Debug-formatted.Source§fn fmt_display(self) -> FmtDisplay<Self>where
Self: Display,
fn fmt_display(self) -> FmtDisplay<Self>where
Self: Display,
self to use its Display implementation when
Debug-formatted.Source§fn fmt_lower_exp(self) -> FmtLowerExp<Self>where
Self: LowerExp,
fn fmt_lower_exp(self) -> FmtLowerExp<Self>where
Self: LowerExp,
self to use its LowerExp implementation when
Debug-formatted.Source§fn fmt_lower_hex(self) -> FmtLowerHex<Self>where
Self: LowerHex,
fn fmt_lower_hex(self) -> FmtLowerHex<Self>where
Self: LowerHex,
self to use its LowerHex implementation when
Debug-formatted.Source§fn fmt_octal(self) -> FmtOctal<Self>where
Self: Octal,
fn fmt_octal(self) -> FmtOctal<Self>where
Self: Octal,
self to use its Octal implementation when Debug-formatted.Source§fn fmt_pointer(self) -> FmtPointer<Self>where
Self: Pointer,
fn fmt_pointer(self) -> FmtPointer<Self>where
Self: Pointer,
self to use its Pointer implementation when
Debug-formatted.Source§fn fmt_upper_exp(self) -> FmtUpperExp<Self>where
Self: UpperExp,
fn fmt_upper_exp(self) -> FmtUpperExp<Self>where
Self: UpperExp,
self to use its UpperExp implementation when
Debug-formatted.Source§fn fmt_upper_hex(self) -> FmtUpperHex<Self>where
Self: UpperHex,
fn fmt_upper_hex(self) -> FmtUpperHex<Self>where
Self: UpperHex,
self to use its UpperHex implementation when
Debug-formatted.Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> Pipe for Twhere
T: ?Sized,
impl<T> Pipe for Twhere
T: ?Sized,
Source§fn pipe<R>(self, func: impl FnOnce(Self) -> R) -> Rwhere
Self: Sized,
fn pipe<R>(self, func: impl FnOnce(Self) -> R) -> Rwhere
Self: Sized,
Source§fn pipe_ref<'a, R>(&'a self, func: impl FnOnce(&'a Self) -> R) -> Rwhere
R: 'a,
fn pipe_ref<'a, R>(&'a self, func: impl FnOnce(&'a Self) -> R) -> Rwhere
R: 'a,
self and passes that borrow into the pipe function. Read moreSource§fn pipe_ref_mut<'a, R>(&'a mut self, func: impl FnOnce(&'a mut Self) -> R) -> Rwhere
R: 'a,
fn pipe_ref_mut<'a, R>(&'a mut self, func: impl FnOnce(&'a mut Self) -> R) -> Rwhere
R: 'a,
self and passes that borrow into the pipe function. Read moreSource§fn pipe_borrow<'a, B, R>(&'a self, func: impl FnOnce(&'a B) -> R) -> R
fn pipe_borrow<'a, B, R>(&'a self, func: impl FnOnce(&'a B) -> R) -> R
Source§fn pipe_borrow_mut<'a, B, R>(
&'a mut self,
func: impl FnOnce(&'a mut B) -> R,
) -> R
fn pipe_borrow_mut<'a, B, R>( &'a mut self, func: impl FnOnce(&'a mut B) -> R, ) -> R
Source§fn pipe_as_ref<'a, U, R>(&'a self, func: impl FnOnce(&'a U) -> R) -> R
fn pipe_as_ref<'a, U, R>(&'a self, func: impl FnOnce(&'a U) -> R) -> R
self, then passes self.as_ref() into the pipe function.Source§fn pipe_as_mut<'a, U, R>(&'a mut self, func: impl FnOnce(&'a mut U) -> R) -> R
fn pipe_as_mut<'a, U, R>(&'a mut self, func: impl FnOnce(&'a mut U) -> R) -> R
self, then passes self.as_mut() into the pipe
function.Source§fn pipe_deref<'a, T, R>(&'a self, func: impl FnOnce(&'a T) -> R) -> R
fn pipe_deref<'a, T, R>(&'a self, func: impl FnOnce(&'a T) -> R) -> R
self, then passes self.deref() into the pipe function.impl<T> Read<Exclusive, BecauseExclusive> for Twhere
T: ?Sized,
Source§impl<T> Tap for T
impl<T> Tap for T
Source§fn tap_borrow<B>(self, func: impl FnOnce(&B)) -> Self
fn tap_borrow<B>(self, func: impl FnOnce(&B)) -> Self
Borrow<B> of a value. Read moreSource§fn tap_borrow_mut<B>(self, func: impl FnOnce(&mut B)) -> Self
fn tap_borrow_mut<B>(self, func: impl FnOnce(&mut B)) -> Self
BorrowMut<B> of a value. Read moreSource§fn tap_ref<R>(self, func: impl FnOnce(&R)) -> Self
fn tap_ref<R>(self, func: impl FnOnce(&R)) -> Self
AsRef<R> view of a value. Read moreSource§fn tap_ref_mut<R>(self, func: impl FnOnce(&mut R)) -> Self
fn tap_ref_mut<R>(self, func: impl FnOnce(&mut R)) -> Self
AsMut<R> view of a value. Read moreSource§fn tap_deref<T>(self, func: impl FnOnce(&T)) -> Self
fn tap_deref<T>(self, func: impl FnOnce(&T)) -> Self
Deref::Target of a value. Read moreSource§fn tap_deref_mut<T>(self, func: impl FnOnce(&mut T)) -> Self
fn tap_deref_mut<T>(self, func: impl FnOnce(&mut T)) -> Self
Deref::Target of a value. Read moreSource§fn tap_dbg(self, func: impl FnOnce(&Self)) -> Self
fn tap_dbg(self, func: impl FnOnce(&Self)) -> Self
.tap() only in debug builds, and is erased in release builds.Source§fn tap_mut_dbg(self, func: impl FnOnce(&mut Self)) -> Self
fn tap_mut_dbg(self, func: impl FnOnce(&mut Self)) -> Self
.tap_mut() only in debug builds, and is erased in release
builds.Source§fn tap_borrow_dbg<B>(self, func: impl FnOnce(&B)) -> Self
fn tap_borrow_dbg<B>(self, func: impl FnOnce(&B)) -> Self
.tap_borrow() only in debug builds, and is erased in release
builds.Source§fn tap_borrow_mut_dbg<B>(self, func: impl FnOnce(&mut B)) -> Self
fn tap_borrow_mut_dbg<B>(self, func: impl FnOnce(&mut B)) -> Self
.tap_borrow_mut() only in debug builds, and is erased in release
builds.Source§fn tap_ref_dbg<R>(self, func: impl FnOnce(&R)) -> Self
fn tap_ref_dbg<R>(self, func: impl FnOnce(&R)) -> Self
.tap_ref() only in debug builds, and is erased in release
builds.Source§fn tap_ref_mut_dbg<R>(self, func: impl FnOnce(&mut R)) -> Self
fn tap_ref_mut_dbg<R>(self, func: impl FnOnce(&mut R)) -> Self
.tap_ref_mut() only in debug builds, and is erased in release
builds.Source§fn tap_deref_dbg<T>(self, func: impl FnOnce(&T)) -> Self
fn tap_deref_dbg<T>(self, func: impl FnOnce(&T)) -> Self
.tap_deref() only in debug builds, and is erased in release
builds.