Skip to main content

dig_dht/
config.rs

1//! [`DhtConfig`] — the Kademlia tuning parameters (replication `k`, lookup parallelism `α`, provider
2//! TTL, the maintenance intervals, the provider-store admission-control caps, and the
3//! discovery-cache bounds).
4
5use std::time::Duration;
6
7use crate::provider_store::ProviderStoreLimits;
8
9/// Kademlia parameters for a [`DhtService`](crate::DhtService).
10///
11/// The defaults follow the canonical Kademlia paper (`k = 20`, `α = 3`) and typical provider-record
12/// lifetimes; every field is documented so a node operator can tune replication vs. traffic.
13#[derive(Debug, Clone)]
14pub struct DhtConfig {
15    /// **Replication parameter `k`** — the bucket size and the number of closest peers a lookup
16    /// converges on. A provider record is announced to (and a `find_node` returns) up to `k` peers.
17    /// Larger `k` = more redundancy against churn, more traffic. Canonical default: 20.
18    pub k: usize,
19
20    /// **Lookup parallelism `α`** — how many peers an iterative lookup queries concurrently per
21    /// round. Larger `α` = faster convergence, more in-flight traffic. Canonical default: 3.
22    pub alpha: usize,
23
24    /// **Provider-record TTL** — how long a PUT provider record is considered valid. A holder
25    /// republishes before this elapses; a finder discards records older than this. Default: 2 hours.
26    ///
27    /// This is also the **clamp ceiling** for inbound `add_provider` records (SPEC §6.2, §14): a
28    /// responder never stores a third-party `expires_at` further in the future than
29    /// `now + provider_ttl`, so a malicious record can never outlive local GC indefinitely.
30    pub provider_ttl: Duration,
31
32    /// **Republish interval** — how often the holder re-announces the content it still holds, so its
33    /// provider records never expire while it is online. MUST be shorter than [`Self::provider_ttl`].
34    /// Default: 1 hour.
35    pub republish_interval: Duration,
36
37    /// **Bucket-refresh interval** — how often a bucket with no recent activity is refreshed by
38    /// looking up a random key that falls in it, keeping the routing table populated. Default: 1 hour.
39    pub refresh_interval: Duration,
40
41    /// **Per-RPC timeout** — how long a single request to one peer may take before that peer is
42    /// treated as unresponsive and the lookup moves on. Default: 5 seconds.
43    pub rpc_timeout: Duration,
44
45    /// **Provider-store admission-control caps** — the per-content-key and global record limits
46    /// enforced on every inbound `add_provider` (SPEC §6.3, §14). Bounds worst-case memory growth
47    /// from a single peer (or a small set of colluding peers) flooding announces. Default:
48    /// [`ProviderStoreLimits::default`].
49    pub provider_store_limits: ProviderStoreLimits,
50
51    /// **Discovery-cache TTL** — how long a provider record LEARNED FROM THIS NODE'S OWN LOOKUP is
52    /// kept so a later fetch of the same content can dial directly instead of walking the DHT again
53    /// (SPEC §6.8). Default: 15 minutes.
54    ///
55    /// Deliberately far shorter than [`Self::provider_ttl`], for a reason specific to this cache:
56    /// **nothing republishes into it.** An authoritative record survives 2 hours because its holder
57    /// refreshes it on [`Self::republish_interval`]; a cached one has no such keeper, so its age is
58    /// pure guesswork about a holder this node has not spoken to since. 15 minutes spans a whole
59    /// multi-range download of one store and the re-reads that immediately follow it — where the
60    /// saving actually accrues — while keeping a holder that dropped the content (an LRU eviction
61    /// upstream takes minutes, not hours) from being dialed for the rest of the afternoon.
62    ///
63    /// It is a CLAMP, never an extension: a cached record expires at
64    /// `min(record.expires_at, now + discovery_cache_ttl)`, so a peer cannot lengthen its own
65    /// residence in this node's cache by claiming a distant expiry.
66    pub discovery_cache_ttl: Duration,
67
68    /// **Discovery-cache caps** — the per-content-key and global record limits enforced on the
69    /// cache (SPEC §6.8), by the same [`ProviderStore`](crate::provider_store::ProviderStore)
70    /// admission control the authoritative store uses.
71    ///
72    /// Tighter than [`Self::provider_store_limits`] on both axes, because the cache answers a
73    /// narrower question. Per key it holds **8** — a caller dials a handful of candidates and gives
74    /// up, so a ninth is memory spent on a dial nobody will make, and it matches the
75    /// `MAX_ADDRESSES_PER_RECORD` disclosure budget the node's redirect path already settled on.
76    /// Globally it holds **10 000** keys' worth: the cache is keyed by what THIS node went looking
77    /// for, which is bounded by its own fetch behaviour rather than by strangers' announces, so the
78    /// authoritative store's 100 000-record ceiling would buy nothing but a larger footprint for a
79    /// pathological workload to fill.
80    pub discovery_cache_limits: ProviderStoreLimits,
81}
82
83impl Default for DhtConfig {
84    fn default() -> Self {
85        DhtConfig {
86            k: 20,
87            alpha: 3,
88            provider_ttl: Duration::from_secs(2 * 60 * 60),
89            republish_interval: Duration::from_secs(60 * 60),
90            refresh_interval: Duration::from_secs(60 * 60),
91            rpc_timeout: Duration::from_secs(5),
92            provider_store_limits: ProviderStoreLimits::default(),
93            discovery_cache_ttl: Duration::from_secs(15 * 60),
94            discovery_cache_limits: ProviderStoreLimits {
95                max_providers_per_key: 8,
96                max_total_records: 10_000,
97            },
98        }
99    }
100}
101
102impl DhtConfig {
103    /// The provider TTL in whole seconds (records store an absolute Unix-seconds expiry).
104    pub fn provider_ttl_secs(&self) -> u64 {
105        self.provider_ttl.as_secs()
106    }
107
108    /// The discovery-cache TTL in whole seconds (the cache stores absolute Unix-seconds expiries).
109    pub fn discovery_cache_ttl_secs(&self) -> u64 {
110        self.discovery_cache_ttl.as_secs()
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[test]
119    fn defaults_follow_kademlia() {
120        let c = DhtConfig::default();
121        assert_eq!(c.k, 20);
122        assert_eq!(c.alpha, 3);
123        assert_eq!(c.provider_ttl_secs(), 7200);
124    }
125
126    #[test]
127    fn republish_is_shorter_than_ttl() {
128        // Invariant: a record must be republished before it expires, or providers vanish while online.
129        let c = DhtConfig::default();
130        assert!(c.republish_interval < c.provider_ttl);
131    }
132
133    #[test]
134    fn discovery_cache_ttl_is_shorter_than_the_provider_ttl() {
135        // Invariant (SPEC §6.8): nothing republishes into the discovery cache, so a cached record
136        // must age out sooner than an authoritative one that a live holder keeps refreshing.
137        let c = DhtConfig::default();
138        assert!(c.discovery_cache_ttl < c.provider_ttl);
139        assert_eq!(c.discovery_cache_ttl_secs(), 900);
140    }
141
142    #[test]
143    fn default_discovery_cache_limits_are_bounded() {
144        // The cache is written from records supplied by untrusted peers, so its growth must be
145        // bounded out of the box exactly as the authoritative store's is.
146        let c = DhtConfig::default();
147        assert!(c.discovery_cache_limits.max_providers_per_key > 0);
148        assert!(c.discovery_cache_limits.max_total_records > 0);
149        assert!(
150            c.discovery_cache_limits.max_total_records < c.provider_store_limits.max_total_records
151        );
152    }
153
154    #[test]
155    fn default_provider_store_limits_are_bounded() {
156        // The audit's "unbounded provider store" finding: the default config MUST carry a non-zero,
157        // finite cap so a freshly constructed DhtService is never unbounded out of the box.
158        let c = DhtConfig::default();
159        assert!(c.provider_store_limits.max_providers_per_key > 0);
160        assert!(c.provider_store_limits.max_total_records > 0);
161    }
162}