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}