decdn_common/config/types.rs
1//! TOML-deserializable configuration file types.
2//!
3//! All fields are `Option` so that missing keys in the TOML file are
4//! accepted. The merge logic in [`super::resolve_config`] fills in defaults.
5
6use std::path::PathBuf;
7
8use serde::{Deserialize, Serialize};
9
10use super::secret::SecretString;
11use crate::cli::common::{LogFormat, LogLevel};
12
13/// Top-level TOML configuration file structure.
14#[derive(Debug, Default, Serialize, Deserialize)]
15#[serde(deny_unknown_fields)]
16pub struct FileConfig {
17 /// Identity settings.
18 pub identity: Option<IdentityConfig>,
19 /// Network settings.
20 pub network: Option<NetworkConfig>,
21 /// Blockchain settings.
22 pub blockchain: Option<BlockchainConfig>,
23 /// Cache settings.
24 pub cache: Option<CacheConfig>,
25 /// Payment settings.
26 pub payment: Option<PaymentConfig>,
27 /// Observability settings.
28 pub observability: Option<ObservabilityConfig>,
29 /// Connection rate-limiting settings.
30 pub security: Option<SecurityConfig>,
31 /// Node-local load-shedding thresholds (overload protection). Absent
32 /// => the resolved defaults.
33 pub load_shed: Option<LoadShedConfig>,
34 /// `cdn/dht/v1` Kademlia DHT settings (ADR 022). Absent => defaults
35 /// from the ADR 022 §DHT Rate Limiting table.
36 pub dht: Option<DhtConfig>,
37 /// `cdn/probe/v1` settings (ADR 005). Absent => defaults from the
38 /// ADR 005 §Probe rate limiting table.
39 pub probe: Option<ProbeConfig>,
40 /// Download-receipt audit-log retention settings (#802). Absent =>
41 /// defaults (128 MiB per file, 4 retained backups).
42 pub receipts: Option<ReceiptsConfig>,
43 /// Local content denylist (ADR 011 §Local Denylist). Absent => both lists
44 /// empty; nothing is denied locally.
45 pub content: Option<ContentConfig>,
46 /// Client-scoped discovery settings. Absent => no region filter;
47 /// discovery/probing considers every registered node.
48 pub client: Option<ClientConfig>,
49}
50
51/// Identity section of the config file.
52#[derive(Debug, Default, Serialize, Deserialize)]
53#[serde(deny_unknown_fields)]
54pub struct IdentityConfig {
55 /// Node data directory.
56 pub data_dir: Option<PathBuf>,
57 /// ISO 3166-1 alpha-2 region code.
58 pub region: Option<String>,
59}
60
61/// Client-scoped section of the config file. Independent of the
62/// node-scoped `[network.discovery]` (address discovery) — this narrows which
63/// peers a client discovers/probes, never ranks them.
64#[derive(Debug, Default, Serialize, Deserialize)]
65#[serde(deny_unknown_fields)]
66pub struct ClientConfig {
67 /// Regions to restrict discovery/probing to; empty/absent = no filter.
68 ///
69 /// This is a DISCOVERY-TIME filter only: it narrows the pool the client
70 /// probes/rediscovers. It does NOT apply to the probe-less peer-store fast
71 /// path, where measured latency ranking already governs which peers are
72 /// near, so a warm store selects by latency without consulting this list.
73 pub region_allowlist: Option<Vec<String>>,
74}
75
76/// Network section of the config file.
77#[derive(Debug, Default, Serialize, Deserialize)]
78#[serde(deny_unknown_fields)]
79pub struct NetworkConfig {
80 /// QUIC bind port. The node binds it on IPv4 and, when the host has IPv6,
81 /// on IPv6 too.
82 pub bind_port: Option<u16>,
83 /// iroh relay URLs for NAT traversal. Multiple entries give relay
84 /// redundancy/failover. Relay connectivity is logged after bring-up but
85 /// never fatal: when no relay connects the node warns with each relay's
86 /// last error and proceeds (iroh retries in the background).
87 pub relay_urls: Option<Vec<String>>,
88 /// Operator-configurable address discovery (#818 scope 1). Absent => the
89 /// node uses the n0-hosted pkarr/DNS discovery (`presets::N0`, unchanged).
90 /// Present => the node drops the n0 discovery leg and composes only the
91 /// providers configured here (the wiring layer builds on `presets::Minimal`).
92 /// Relay selection ([`Self::relay_urls`]) is an independent, orthogonal knob.
93 pub discovery: Option<DiscoveryConfig>,
94}
95
96/// Operator-configurable address-discovery providers (#818 scope 1).
97///
98/// Two mechanisms, selectable by which keys are present and combinable:
99/// - **Custom pkarr + DNS** ([`Self::pkarr_url`] + [`Self::dns_origin`]) —
100/// publish this node's signed address record to an operator-run pkarr relay
101/// and resolve peers via an operator-run DNS server. Closest to n0's model;
102/// supports dynamic addresses.
103/// - **Static peer map** ([`Self::peers`]) — a `MemoryLookup` address book
104/// supplied directly in config. Fully offline/isolated; addresses must be
105/// known ahead of time.
106#[derive(Debug, Default, Serialize, Deserialize)]
107#[serde(deny_unknown_fields)]
108pub struct DiscoveryConfig {
109 /// pkarr relay URL this node PUBLISHES its signed address record to. Must
110 /// be paired with [`Self::dns_origin`] (a resolver for the same namespace):
111 /// publishing to a relay nothing resolves from is rejected at config
112 /// validation. Parse-checked as a URL at resolution.
113 pub pkarr_url: Option<String>,
114 /// DNS origin domain this node RESOLVES peer addresses from (TXT queries
115 /// `_iroh.<z32-id>.<dns_origin>`). Valid on its own (a resolve-only node
116 /// that does not publish). Checked non-empty at resolution.
117 pub dns_origin: Option<String>,
118 /// Static peer address book seeded into a `MemoryLookup`, keyed by peer
119 /// `NodeId` (the canonical 64-char lowercase-hex form iroh emits; validated
120 /// with the same parser the node uses at bring-up). Composes alongside the
121 /// pkarr/DNS leg when both are set.
122 pub peers: Option<std::collections::HashMap<String, DiscoveryPeer>>,
123}
124
125/// A single static peer entry for [`DiscoveryConfig::peers`]. To be useful at
126/// least one of [`Self::relay_url`] / [`Self::addrs`] should be set — an
127/// id-only entry yields a peer with no transport addresses to dial.
128#[derive(Debug, Default, Clone, Serialize, Deserialize)]
129#[serde(deny_unknown_fields)]
130pub struct DiscoveryPeer {
131 /// This peer's home relay URL (becomes a relay transport address).
132 pub relay_url: Option<String>,
133 /// Direct socket addresses for this peer (`host:port`, each becomes a
134 /// direct transport address). Each entry is parsed as a `SocketAddr` at
135 /// resolution.
136 #[serde(default)]
137 pub addrs: Vec<String>,
138}
139
140/// Blockchain section of the config file.
141#[derive(Debug, Default, Serialize, Deserialize)]
142#[serde(deny_unknown_fields)]
143pub struct BlockchainConfig {
144 /// JSON-RPC endpoint URL.
145 pub rpc_url: Option<String>,
146 /// Ethereum keystore file path.
147 pub eth_keystore: Option<PathBuf>,
148 /// `PaymentPool` contract address — the shared payment pool this node
149 /// registers against for buyer and seller flows alike.
150 pub payment_pool_address: Option<String>,
151 /// `CapacityBond` contract address.
152 pub capacity_bond_address: Option<String>,
153 /// `OriginAssignment` contract address. Optional: when set, the node runs the
154 /// chain-backed origin directory for node-to-node routing, resolving a
155 /// request's namespace via `getOrigins(namespaceId)` (ADR 022) for the
156 /// `FIND_VALUE` fallback. Unset => the origin directory is empty, so that
157 /// fallback resolves nothing.
158 pub origin_assignment_address: Option<String>,
159 /// Positive-hit TTL for the lazy origin directory cache, in seconds. Only
160 /// consulted when `origin_assignment_address` is set. Absent =>
161 /// `DEFAULT_ORIGIN_DIRECTORY_POSITIVE_TTL_SEC`.
162 pub origin_directory_positive_ttl_sec: Option<u64>,
163 /// Negative-hit TTL for the lazy origin directory cache, in seconds. Only
164 /// consulted when `origin_assignment_address` is set. Absent =>
165 /// `DEFAULT_ORIGIN_DIRECTORY_NEGATIVE_TTL_SEC`.
166 pub origin_directory_negative_ttl_sec: Option<u64>,
167 /// Max distinct namespaces held in the lazy origin directory cache (LRU
168 /// eviction). Only consulted when `origin_assignment_address` is set.
169 /// Absent => `DEFAULT_ORIGIN_DIRECTORY_CACHE_CAPACITY`.
170 pub origin_directory_cache_capacity: Option<usize>,
171 /// `PublisherRegistry` contract address. Independent of the origin directory:
172 /// it is the publish CLI's `namespace create` target and is not consumed by
173 /// the node runtime.
174 pub publisher_registry_address: Option<String>,
175 /// `SlashJudge` contract address — the EIP-712 `verifyingContract` for
176 /// `ProbeResponse` / `StreamResponse` `slash_sig` signatures (ADR 014
177 /// §1–2). Required: a wrong/zero address silently produces signatures no
178 /// verifier accepts, so resolution fails fast when it is missing rather
179 /// than defaulting.
180 pub slash_judge_address: Option<String>,
181 /// `SlashAppeal` contract address — the target for `decdn appeal slash`
182 /// (ADR 028). Consumed **only** by that CLI (via `chain_ctx::resolve_appeal`,
183 /// which validates it); the daemon accepts the key here — `[blockchain]`
184 /// denies unknown fields and a node's `node.toml` is shared with the CLI —
185 /// but does not resolve or use it.
186 pub slash_appeal_address: Option<String>,
187 /// `ContentBlacklist` contract address. Required at node config resolution;
188 /// startup completes the initial global + operator-scope replay before any
189 /// ALPN accepts connections (ADR 011/019/031).
190 pub content_blacklist_address: Option<String>,
191 /// Seconds between the blacklist watcher's periodic replay + re-scope pass
192 /// (ADR 011 §Polling cadence). This backstop is what catches scope changes
193 /// with no `ContentBlacklist` event — an operator region/ripening
194 /// transition. Absent =>
195 /// [`super::DEFAULT_CONTENT_BLACKLIST_POLL_INTERVAL_SEC`] (600s). Only
196 /// consulted when `content_blacklist_address` is set.
197 pub content_blacklist_poll_interval_sec: Option<u64>,
198 /// Seconds the node may go without a successful chain read before it stops
199 /// serving (ADR 011 § Serving while chain-stale). While the blacklist
200 /// watcher cannot reach the RPC, every chain-derived serve guard — the
201 /// deny-set, pool solvency, per-signer caps — answers from stale state, so a
202 /// takedown that lands during the outage would go unenforced and serving it
203 /// past the compliance window is slashable. Past this window the serve and
204 /// probe paths refuse rather than sign evidence the node can no longer
205 /// vouch for. Always evaluated (there is no on/off flag); set it large to
206 /// opt out. Absent => [`super::DEFAULT_CHAIN_STALENESS_GRACE_SEC`] (1800s /
207 /// 30 min). Must not be `0` (that would refuse every serve). Only consulted
208 /// when `content_blacklist_address` is set.
209 pub chain_staleness_grace_sec: Option<u64>,
210 /// EIP-712 `chainId` bound into every `slash_sig` domain separator.
211 /// Absent => [`super::DEFAULT_CHAIN_ID`] (Arbitrum Sepolia, the initial
212 /// network target — matches the chain id bound on the runtime signer).
213 pub chain_id: Option<u64>,
214 /// Seconds between RPC connectivity watchdog probes. `0` disables the
215 /// watchdog entirely; absent => default (30s). Non-zero values below
216 /// `MIN_RPC_WATCHDOG_INTERVAL_SEC` are rejected at config resolution.
217 pub rpc_watchdog_interval_sec: Option<u64>,
218 /// Milliseconds between chain-event poll ticks (#1011, #1106). Drives two
219 /// unrelated consumers: the `eth_getLogs` tick cadence of every chain
220 /// watcher, and alloy's pending-transaction receipt heartbeat (see
221 /// `ResolvedBlockchain::event_poll_interval_ms`). The heartbeat is why this
222 /// overrides alloy's own default, which is 250 ms for a localhost RPC (it
223 /// auto-detects `127.0.0.1`/`localhost`) and 7000 ms otherwise — the 250 ms
224 /// local default hammers a dev anvil. Absent => default
225 /// (`DEFAULT_EVENT_POLL_INTERVAL_MS`, 7000 ms, matching alloy's non-local
226 /// cadence so live-RPC load is unchanged). Values below
227 /// `MIN_EVENT_POLL_INTERVAL_MS` are rejected at config resolution.
228 pub event_poll_interval_ms: Option<u64>,
229 /// Maximum block span of one chain-watcher `eth_getLogs` request. Set it to
230 /// the RPC provider's actual `eth_getLogs` range limit: the lowest `span`
231 /// the poller reaches while rejections keep coming (the shrink `warn!` and
232 /// the `decdn_chain_get_logs_span` gauge show it). A provider's documented
233 /// or quoted limit can be wrong. The poller also shrinks its window on its own when the provider
234 /// rejects a range, and grows it back toward this ceiling, so this is a
235 /// ceiling, not a hard requirement. Absent =>
236 /// [`super::DEFAULT_GET_LOGS_MAX_BLOCK_SPAN`] (10 000). Must not be `0`;
237 /// rejected at config resolution.
238 pub get_logs_max_block_span: Option<u64>,
239 /// Seconds between authoritative `FeeRouter.getShares()` re-reads by the
240 /// fee-shares watcher (ADR 041 / ADR 016 § Tunable Economics). This is the
241 /// safety-net cadence *in addition to* the `SharesUpdated` event
242 /// subscription (which follows [`Self::event_poll_interval_ms`]); it
243 /// reconciles any log the event tail missed. Absent =>
244 /// [`super::DEFAULT_FEE_SHARES_POLL_INTERVAL_SEC`] (3600s / 1h). Must not
245 /// be `0` (that would poll every tick); rejected at config resolution.
246 pub fee_shares_poll_interval_sec: Option<u64>,
247 /// Per-chunk redemption floor (base units, `µUSDC`, ADR 003 § Operator
248 /// early withdrawal). The node submits an on-chain redemption
249 /// transaction for a chunk of lanes only once the aggregate un-redeemed
250 /// value across that chunk's lanes reaches this floor; every lane in a
251 /// submitted chunk settles, so a small (dust) lane rides alongside the
252 /// larger lanes that cleared the floor. Larger values amortize gas
253 /// across more delivery; smaller values bound unsettled exposure.
254 /// Absent => default (1 USDC = `1_000_000` `µUSDC`).
255 pub redeem_threshold_micro_usdc: Option<u64>,
256 /// Maximum vouchers packed into one `redeemMany` transaction. The redeemer
257 /// splits a sweep across this many vouchers per transaction so a high-fan-out
258 /// node stays under the block gas limit; a chunk that still fails to send
259 /// oversized is halved and retried. Absent => default (`300`). Must not be
260 /// `0`; rejected at config resolution.
261 pub redeem_max_vouchers_per_tx: Option<u64>,
262 /// Seconds between the redeemer's self-tick sweeps (#327, #751): the
263 /// low-frequency backstop that sweeps every lane and redeems the chunks
264 /// that clear the redemption floor, independent of the advisory per-voucher
265 /// hints, so a dropped hint can never strand an accrued balance. Smaller values withdraw earnings sooner
266 /// at the cost of more pool-state reads; larger values lean harder on the
267 /// hints. Absent => default (300s / 5 min). Must not be `0`; rejected at
268 /// config resolution.
269 pub redeem_interval_secs: Option<u64>,
270 /// Deposit (base units, `µUSDC`) the buyer path escrows when it **opens** a
271 /// payment pool, and the target every top-up refills the pool balance
272 /// toward once it is reused or runs short mid-transfer. The shared pool is
273 /// fully withdrawable, so the buyer opens at this amount directly rather
274 /// than escrowing a smaller first-contact lock. Both refill legs target
275 /// it: the low-water refill, which both binaries run on pool reuse, and
276 /// the funding recovery step, which every fetch path (`decdn get` and the
277 /// daemon's node-to-node cache-miss pull) runs once no node serves at the
278 /// current deposit (#1530). Larger values amortize gas across more delivery
279 /// at the cost of more capital locked. Absent => default (10 USDC =
280 /// `10_000_000`). Must be nonzero (the pool's `openPool` reverts on a zero
281 /// deposit).
282 pub buyer_working_deposit_micro_usdc: Option<u64>,
283 /// Whether to issue an unlimited (max) USDC approval for the
284 /// `PaymentPool` contract so the buyer path can join a pool (#744).
285 /// The absent-default is **profile-dependent**: the node daemon defaults to
286 /// `true` (a long-lived operator amortizes one unlimited approval across many
287 /// node-to-node miss pulls), while `decdn get` defaults to `false`, i.e. an
288 /// **exact deposit-sized** approval scoped to what each pool escrows. Set
289 /// `true` on the client to opt a power user back into the unlimited
290 /// allowance.
291 pub buyer_max_approve: Option<bool>,
292 /// The node's refundable floor `M` (base units, `µUSDC`): the minimum
293 /// remaining on-chain pool balance the node insists on keeping in reserve
294 /// (ADR 003 § Sizing, `M = k·ρ·B·Δ` — a function of the redeem cadence `k`,
295 /// the node's advertised rate `ρ`, the credit window `B`, and the round-trip
296 /// slack `Δ`). The node stops serving a pool once its remaining on-chain
297 /// balance minus `M` can no longer cover the next credit window, so a buyer
298 /// that drains its pool balance cannot leave the node's already-delivered,
299 /// not-yet-redeemed bytes stranded. Absent => default (1 USDC =
300 /// `1_000_000` `µUSDC`), a conservative static value at the redeem-threshold
301 /// scale; sizing this precisely per ADR 003 is governance/ops policy, not a
302 /// build-time constant.
303 pub pool_min_remaining_deposit_micro_usdc: Option<u64>,
304 /// Per-signer LIVE concurrency cap `k`, counted in ramp-start credit windows
305 /// (ADR 003 § Pool solvency, per-signer floor isolation): the most live
306 /// un-vouchered floor reservation any ONE capability signer may hold against a
307 /// pool, `k · one credit window`, underneath the pool-wide `remaining − M`
308 /// ceiling. The credit window is its floor (one chunk plus one chunk group)
309 /// normally, and at `credit_ramp_divisor == 0` it is `credit_max` raised to
310 /// that floor.
311 ///
312 /// This bounds concurrency, not paid throughput: a signer that pays its vouchers
313 /// releases that stream's reservation, so its cap recycles and a stream already
314 /// under way is never slowed. It carries no permanent memory and never penalizes a
315 /// signer for quitting.
316 ///
317 /// The window is the unit actually being rationed: honest need is `concurrent
318 /// un-vouchered streams × one window` regardless of the pool's size, so an absolute
319 /// window count fits it directly. Node-local risk policy — it needs no pool-owner
320 /// cooperation. Lower-clamped to one window (so a lone signer's first stream is
321 /// always admissible). Absent => default (`8`). See
322 /// `DEFAULT_POOL_FLOOR_SIGNER_LIVE_WINDOWS`.
323 pub pool_floor_signer_live_windows: Option<u64>,
324 /// USDC settlement-token contract address — the ERC-20 clients spend on
325 /// deposits into a payment pool. Declared here so a client config that names
326 /// it passes `decdn config validate` under this section's
327 /// `deny_unknown_fields`; the running client resolves the token at runtime
328 /// from `PaymentPool.usdc()`.
329 pub usdc_address: Option<String>,
330}
331
332/// Cache section of the config file.
333///
334/// `deny_unknown_fields` is set so that a misplaced origin field (a flat
335/// `origin_url` / `origin_path` / `decompress` directly under `[cache]`)
336/// gets a clear "unknown field" error at config load instead of a silent
337/// "no origin configured" surprise at the first cache miss. Origin
338/// settings live under the tagged `[cache.origin]` table — see
339/// [`OriginConfig`].
340#[derive(Debug, Default, Serialize, Deserialize)]
341#[serde(deny_unknown_fields)]
342pub struct CacheConfig {
343 /// Blob cache directory.
344 pub cache_dir: Option<PathBuf>,
345 /// Maximum cache size in megabytes. An *upper* bound on the cache ceiling,
346 /// not the ceiling itself: the eviction driver clamps it down each tick so
347 /// the cache never grows into the last [`Self::disk_headroom_mb`] of free
348 /// space on the `cache_dir` volume (#1930). Set it deliberately large to let
349 /// the disk-headroom clamp size the cache to whatever the volume offers.
350 pub cache_size_mb: Option<u64>,
351 /// Free disk in megabytes the eviction driver keeps unused on the
352 /// `cache_dir` volume, defended against *any* process, not just this cache
353 /// (#1930). Absent => [`crate::config::DEFAULT_DISK_HEADROOM_MB`] (8192, 8
354 /// GiB). Each tick the effective ceiling is
355 /// `footprint + max(0, free_disk - disk_headroom_mb)`, capped by
356 /// [`Self::cache_size_mb`]; below the headroom the driver evicts to claw disk
357 /// back. `0` opts out of the disk clamp (only `cache_size_mb` binds). A slow
358 /// soft-evict/GC cycle means the margin is defended reactively, so keep it
359 /// comfortably above one GC interval's worth of writes.
360 pub disk_headroom_mb: Option<u64>,
361 /// Maximum single blob size in megabytes.
362 pub max_blob_size_mb: Option<u64>,
363 /// Buyer-side ABSOLUTE per-MB rate ceiling for paid pulls (#1375), in the same
364 /// per-MB units as the wire `StreamResponse.rate_per_mb`. Absent / `0` =
365 /// unlimited. Bounds what this node, as a BUYER on a cache-miss pull, will
366 /// accept a provider to quote — on top of the always-applied probe-relative
367 /// bound. This is a buyer-side ceiling; the node never bounds its own
368 /// sell rate (a rate below the on-chain delivery floor still sells and
369 /// settles, with only its vote-weight byte credit clamped at redemption).
370 pub max_rate_per_mb: Option<u64>,
371 /// Single origin backend for cache pull-through (#437). Absent =>
372 /// no pull-through (unless [`Self::origins`] is set); cache misses
373 /// fail with `NoOrigin`. The variant (`http`, `fs`, or `s3`) is
374 /// selected by the `kind` field on the inner `[cache.origin]`
375 /// table. Mutually exclusive with [`Self::origins`] — operators
376 /// who need an ordered fallback list use the `[[cache.origins]]`
377 /// array-of-tables form instead. Setting both at once is rejected
378 /// at config resolution.
379 pub origin: Option<OriginConfig>,
380 /// Ordered list of origin backends with automatic fallback (#284).
381 /// On a cache miss the engine tries each entry in order; the next
382 /// entry is consulted when the current one returns `NotFound`,
383 /// returns a permanent error, or exhausts its per-origin transient
384 /// retry budget. Deterministic per-origin failures (hash mismatch,
385 /// blob-too-large) do **not** trigger fallback — they indicate a
386 /// misbehaving or misconfigured backend that must surface, not be
387 /// masked. Mutually exclusive with [`Self::origin`]; an empty
388 /// array is rejected (omit the key for "no pull-through" rather
389 /// than configuring zero origins). Duplicate entries are
390 /// permitted but logged as a warning at startup — operators may
391 /// legitimately want two entries with the same target for
392 /// connection-pool sharding. Set once at startup; changes require
393 /// a process restart, consistent with [`Self::origin_retry`].
394 ///
395 /// Worst-case *backoff* latency for an all-failing chain is
396 /// `len(origins) × sum_of_backoffs(origin_retry)` plus per-attempt
397 /// origin RTT/timeout. With default [`decdn_config_types::RetryPolicy`]
398 /// (3 retries, sleeps `100 + 200 + 400 ms` ± jitter — see
399 /// `RetryPolicy::default`) that's ~700 ms backoff per origin
400 /// (~2.1 s for three origins) — *not* `max_retries × backoff_cap`,
401 /// because the default schedule never saturates the 10 s cap. The
402 /// `max_backoff_ms` cap only dominates if `initial_backoff_ms` is
403 /// raised enough to saturate it; for the worst-case-cap regime use
404 /// `max_retries × max_backoff_ms` and multiply by chain length.
405 /// Plan operator timeouts accordingly — RTT/timeout per attempt
406 /// usually dominates the backoff term.
407 pub origins: Option<Vec<OriginConfig>>,
408 /// Hex-encoded BLAKE3 hashes that must stay cached regardless of LRU
409 /// pressure (#276). Each entry is 64 lowercase hex chars (BLAKE3
410 /// digest size). Invalid hex or wrong-length entries cause config
411 /// resolution to fail — fail-fast at load time beats a silent
412 /// "entry was ignored" surprise hours later when the operator
413 /// discovers the blob got evicted anyway.
414 pub pinned_hashes: Option<Vec<String>>,
415 /// Origin pull-through retry policy (#285, extended in #519).
416 /// Controls exponential backoff for transient HTTP / S3 /
417 /// filesystem errors *and* the body-phase retry strategy for
418 /// mid-stream `io::Error`s. Absent => defaults from
419 /// [`decdn_config_types::RetryPolicy::default`] (3 retries, 100ms
420 /// initial backoff doubling to 10s cap, 10% jitter,
421 /// `buffered_max_bytes = 4 MiB`). `max_retries = 0` opts out and
422 /// makes one attempt with no retry; `buffered_max_bytes = 0`
423 /// independently disables the body-phase buffer path so all
424 /// body-phase failures route through the streaming abort+restart
425 /// path. Set once at startup; changes require a restart.
426 ///
427 /// **Body-phase retry trade-offs (#519):**
428 ///
429 /// - **Buffer-then-commit** applies when the origin's advertised
430 /// `size_hint` is at or below `buffered_max_bytes`. The body
431 /// drains into memory before the iroh-blobs commit, so failed
432 /// attempts cost RAM (bounded by `buffered_max_bytes` per
433 /// in-flight fetch) but leave no on-disk garbage.
434 /// - **Abort + restart** applies above the threshold or when
435 /// `size_hint` is unknown. Each failed attempt strands up to
436 /// `max_blob_size_mb` of partial-import bytes until iroh-blobs
437 /// GC reclaims them at `cache.gc_interval_sec` cadence.
438 /// Worst-case disk amplification per fetch is
439 /// `(1 + max_retries) * max_blob_size_mb`.
440 ///
441 /// `decdn_config_types::RetryPolicy` carries `#[serde(default)]` so
442 /// partial sections (e.g. just `max_retries = 5`) get the rest of
443 /// the fields filled from defaults.
444 pub origin_retry: Option<decdn_config_types::RetryPolicy>,
445 /// Per-origin circuit-breaker policy (#963). Fronts each origin's
446 /// pull-through retry loop: after `failure_threshold` consecutive
447 /// origin-unavailable failures (transient errors that exhausted the
448 /// retry budget — NOT 404s or other permanent per-object errors) the
449 /// breaker trips OPEN and fast-fails every miss for `cooldown_ms`
450 /// without incurring any retry/backoff, then admits
451 /// `half_open_max_calls` trial pulls to probe recovery. Absent =>
452 /// defaults from [`decdn_config_types::CircuitBreakerPolicy::default`]
453 /// (enabled, trip after 5 failures, 30s cooldown, 1 half-open trial).
454 /// Set `enabled = false` (or `failure_threshold = 0`) to opt out and
455 /// run every miss through the full retry loop regardless of origin
456 /// health. Set once at startup; changes require a restart.
457 ///
458 /// `CircuitBreakerPolicy` carries `#[serde(default)]` so partial
459 /// sections (e.g. just `cooldown_ms = 60000`) get the rest of the
460 /// fields filled from defaults.
461 pub circuit_breaker: Option<decdn_config_types::CircuitBreakerPolicy>,
462 /// Optional `User-Agent` override sent on every HTTP origin
463 /// pull-through request (#435). Absent => the workspace default
464 /// (`decdn-node/<version>`); set to attribute CDN traffic in origin
465 /// access logs or to apply origin-side rate limits and routing
466 /// policy that distinguish CDN pulls from end-user clients. Empty
467 /// string is rejected at resolution.
468 pub user_agent: Option<String>,
469 /// Interval between iroh-blobs GC sweeps in seconds (#518). Absent =>
470 /// [`crate::config::DEFAULT_GC_INTERVAL_SEC`]. Set to `0` to disable
471 /// the periodic sweep — operators with external disk-reclaim
472 /// scheduling can opt out, accepting that bytes orphaned by
473 /// hash-mismatch / mid-stream-error pull-through paths leak until
474 /// they manually reset state. Tighter intervals shrink the
475 /// hostile-origin amplification window at the cost of more
476 /// list+sweep CPU per minute.
477 pub gc_interval_sec: Option<u64>,
478 /// Interval between origin-held-index rescans in seconds (#1130). Absent =>
479 /// [`crate::config::DEFAULT_FS_RESCAN_INTERVAL_SEC`]. The node re-walks its
480 /// `fs` origin directory (and re-checks present pins) at this cadence so a
481 /// file dropped into the origin becomes discoverable — probe `has_blob` +
482 /// DHT announce — within one interval, without a restart or reload. `0`
483 /// disables the periodic rescan (startup + `decdn node reload` still run
484 /// it). Shorter intervals pick up new files faster at the cost of more
485 /// directory-walk + `size()` I/O per minute.
486 pub fs_rescan_interval_sec: Option<u64>,
487 /// TTL, in seconds, for a memoised positive live-origin probe answer (#1130
488 /// pt3). Absent => [`crate::config::DEFAULT_ORIGIN_PROBE_TTL_SEC`] (15). A
489 /// probe for a hash absent from the `fs`-enumeration ∪ pins index falls back
490 /// to a live `HEAD`/`HeadObject` against the http/s3 origin; a present-with-
491 /// size answer is cached for this long so a non-pinned bucket object is
492 /// discoverable without a per-probe origin round-trip on every probe.
493 /// Shorter TTLs track origin deletions faster at the cost of more `HEAD`
494 /// traffic. See `origin_probe_negative_ttl_sec` for the absent-answer TTL.
495 pub origin_probe_ttl_sec: Option<u64>,
496 /// TTL, in seconds, for a memoised negative (absent) live-origin probe
497 /// answer (#1130 pt3). Absent =>
498 /// [`crate::config::DEFAULT_ORIGIN_PROBE_NEGATIVE_TTL_SEC`] (2). Caching the
499 /// negative answer is what blunts a random-hash probe flood; keeping this
500 /// TTL short bounds how long a stale `Absent` can hide newly-available own
501 /// content from a probe.
502 pub origin_probe_negative_ttl_sec: Option<u64>,
503 /// TTL, in seconds, for a memoised fault live-origin probe answer (#1130
504 /// pt3). Absent => [`crate::config::DEFAULT_ORIGIN_PROBE_FAULT_TTL_SEC`]
505 /// (5). Longer than `origin_probe_negative_ttl_sec` because the memo is
506 /// keyed per hash and a client retrying one hash against a failing origin
507 /// is the common shape; shorter than `origin_probe_ttl_sec` because a
508 /// memoised fault refuses paying clients for as long as it stands.
509 /// Rejected unless `origin_probe_negative_ttl_sec <=
510 /// origin_probe_fault_ttl_sec <= origin_probe_ttl_sec`.
511 pub origin_probe_fault_ttl_sec: Option<u64>,
512 /// Per-probe ceiling, in milliseconds, on the live-origin `HEAD`/`HeadObject`
513 /// (#1130 pt3). Absent => [`crate::config::DEFAULT_ORIGIN_PROBE_TIMEOUT_MS`]
514 /// (2000). A slow origin must never stall the probe hot path; on timeout the
515 /// probe answers `has_blob: false` (safe — never slashable) and
516 /// the miss is memoised as absent for one TTL.
517 pub origin_probe_timeout_ms: Option<u64>,
518 /// Maximum distinct hashes held in the live-origin probe memo (#1130 pt3).
519 /// Absent => [`crate::config::DEFAULT_ORIGIN_PROBE_MEMO_CAPACITY`] (4096).
520 /// Bounds memo memory under a random-hash probe flood; the cache is
521 /// best-effort, so at capacity one arbitrary entry is dropped to admit a new
522 /// answer (expired entries reclaimed first).
523 pub origin_probe_memo_capacity: Option<u64>,
524 /// Cache eviction driver: percent of [`Self::cache_size_mb`] above which
525 /// the driver actively evicts (#1173, ADR 040). Absent =>
526 /// [`crate::config::DEFAULT_EVICTION_HIGH_WATER_PCT`] (90). Hard bounds
527 /// `[60, 95]`; set above the 25% probe-hold recommendation so a full hold
528 /// budget plus in-flight writes don't trip it, below 95% for write
529 /// headroom between sweeps.
530 pub eviction_high_water_pct: Option<u64>,
531 /// LRU eviction driver: percent of [`Self::cache_size_mb`] the driver
532 /// evicts down to before returning to idle (#1173). Absent =>
533 /// [`crate::config::DEFAULT_EVICTION_TARGET_PCT`] (80). Hard bounds
534 /// `[40, 90]`, and MUST be `<= eviction_high_water_pct - 5` — the 5-point
535 /// hysteresis gap is structural (prevents thrash on writes hovering near
536 /// the trigger), not a tunable nicety.
537 pub eviction_target_pct: Option<u64>,
538 /// LRU eviction driver: maximum candidates removed per tick before the
539 /// driver yields (#1173). Absent =>
540 /// [`crate::config::DEFAULT_EVICTION_PER_SWEEP_BUDGET`] (16). Hard bounds
541 /// `[1, 256]`. Bounds worst-case driver-induced latency on the cache hot
542 /// path (~1 ms filesystem-unlink per entry); the driver continues across
543 /// consecutive ticks until below target or candidates are exhausted.
544 pub eviction_per_sweep_budget: Option<u64>,
545 /// LRU eviction driver: seconds between driver wakeups (#1173). Absent =>
546 /// [`crate::config::DEFAULT_EVICTION_TICK_SECS`] (1). Hard bounds
547 /// `[1, 60]`.
548 ///
549 /// **Cost note:** every tick measures the on-disk footprint, which walks the
550 /// blob list and issues one `status()` per blob, plus an O(n) clone of the
551 /// pinned set — latched or not. An idle tick is not free: the driver keeps
552 /// no cached footprint, so it re-measures on every tick (ADR 040). On a
553 /// cache holding many blobs,
554 /// raise this to trade eviction latency for steady-state store load.
555 pub eviction_tick_secs: Option<u64>,
556 /// Maximum number of concurrently held (eviction-exempt) blobs for the
557 /// probe-triggered hold (ADR 005 §Hold budget, #318). Holds are
558 /// per-blob: multiple peers probing the same hash share one slot. When the
559 /// budget is exhausted, a probe for a blob that is present but gets no slot
560 /// still advertises (`has_blob: true`) and places no eviction hold — the
561 /// blob may be LRU-evicted before the pull, costing one wasted round trip
562 /// (never a slash). Absent => [`crate::config::DEFAULT_MAX_PROBE_HOLDS`]
563 /// (256). `0` is the operator opt-out: holds are disabled and probes answer
564 /// `has_blob: false` for store-backed content — content servable from a
565 /// configured origin takes no hold and is still advertised. Operators with
566 /// small caches SHOULD set this to ≤25% of cache capacity.
567 pub max_probe_holds: Option<u64>,
568 /// Number of probe-hold slots reserved for the **stake lane** —
569 /// registered operators issuing node-to-node cache-miss probes (#757,
570 /// ADR 003 §Admission and Priority). An end-client probe places no eviction
571 /// hold once usage reaches `max_probe_holds - stake_lane_reserved_holds` (it
572 /// still advertises `has_blob: true` if the blob is present), keeping the
573 /// last `stake_lane_reserved_holds` slots available for node-to-node probes
574 /// so end-client load cannot starve their holds. Absent / `0` (the default)
575 /// disables the reservation entirely — a single-lane node is unaffected.
576 /// Values `>= max_probe_holds` reserve the whole budget for the stake lane:
577 /// no end-client probe takes a hold slot, regardless of current usage.
578 pub stake_lane_reserved_holds: Option<u64>,
579 /// Enable node-to-node paid cache-miss pull-through (#831, ADR 001/022).
580 /// Absent / `false` (the default) → a cache miss serves `NotFound`.
581 /// When `true` *and* the buyer pool service bootstrapped, a
582 /// miss triggers DHT provider discovery → probe → ranked paid pull from an
583 /// upstream node, which populates the cache and is then served. OFF by
584 /// default for the initial network: enabling it makes the node front USDC
585 /// egress to fill misses (bounded by `blockchain.buyer_working_deposit_micro_usdc`
586 /// and the upstream's per-MB rate), and the serving path only triggers it
587 /// behind a valid, pool-bound client so an unpaid request cannot drive
588 /// egress.
589 pub node_to_node_pull_through_enabled: Option<bool>,
590 /// When `false`, the node serves and seeds only content its own backend
591 /// holds; a foreign hash is declined like a miss. Absent resolves to a
592 /// role-derived default: `false` (origin-only) when an origin backend is
593 /// configured, `true` (relay) when none is. Set this explicitly to
594 /// override either default. Node-local; `cache.*` is restart-required.
595 pub relay_foreign_namespaces: Option<bool>,
596 /// Number of discovered providers to probe before ranking on a
597 /// node-to-node pull (#831). Absent =>
598 /// [`crate::config::DEFAULT_NODE_PULL_PROBE_FANOUT`] (5). Higher widens
599 /// provider choice at the cost of more probe round trips per miss; `0`
600 /// probes none, so no pull can succeed (a way to disable the pull while
601 /// keeping the feature flag on).
602 pub node_pull_probe_fanout: Option<usize>,
603 /// Wall-clock timeout in seconds for the STREAM-OPEN stage of a single upstream pull
604 /// on a node-to-node miss (#831) — connect, handshake, and the
605 /// signed `StreamResponse`. It does NOT bound the buyer pool open, which precedes
606 /// it on its own 5 s budget. Absent =>
607 /// [`crate::config::DEFAULT_NODE_PULL_TIMEOUT_SEC`] (20). Bounds how long a
608 /// miss blocks the serving path on one upstream before falling through to
609 /// the next ranked candidate (or `NotFound`). This is the *per-upstream*
610 /// budget; the overall pull-through deadline is derived as roughly
611 /// `MAX_PROVIDER_ATTEMPTS × (pool open + it + stall)` plus a fixed discovery
612 /// allowance, so the fallback loop reaches every ranked candidate (#859).
613 ///
614 /// It does NOT bound the streaming stage (#1134) — that is
615 /// [`Self::node_pull_stall_window_sec`]. A wall clock over the bytes would
616 /// cap the blob size a node can pull through at roughly
617 /// `this × link speed`.
618 pub node_pull_timeout_sec: Option<u64>,
619 /// Throughput-floor window in seconds for the STREAMING stage of an upstream pull
620 /// (#1797). Absent => [`crate::config::DEFAULT_NODE_PULL_STALL_WINDOW_SEC`] (20). Bytes
621 /// read off the QUIC stream are counted sub-frame, and the pull aborts when the bytes
622 /// across the trailing window fall below [`Self::node_pull_min_throughput_bps`] — so the
623 /// signal is frame-size-independent and indifferent to blob size. It is what makes a pull
624 /// of any size safe to leave uncapped.
625 ///
626 /// Tune it against upstream responsiveness, not content size: too short and a brief
627 /// network hiccup abandons a healthy transfer; too long and a dead upstream is held onto
628 /// for longer than necessary before the fallback loop moves on. The abort is
629 /// requester-local policy and does NOT score the upstream's reputation (#1797).
630 ///
631 /// "Longer" is multiplied, not added. A silent candidate costs one full window of this,
632 /// and the derived outer deadline budgets that for EVERY candidate, so a second here is
633 /// ~3 seconds of worst-case client wait on a total miss (167.5 s at defaults).
634 ///
635 /// It also sets the head start of the time budget for each own-origin range-pull read
636 /// (ADR 037): one `{H}.obao4` fetch or one data window of at most 4 MiB. That budget is
637 /// this window plus the read size at [`Self::node_pull_min_throughput_bps`], so it
638 /// scales with the read and never caps the blob size.
639 pub node_pull_stall_window_sec: Option<u64>,
640 /// Minimum sustained upstream throughput in bytes per second over
641 /// [`Self::node_pull_stall_window_sec`] (#1797). Absent =>
642 /// [`crate::config::DEFAULT_NODE_PULL_MIN_THROUGHPUT_BPS`] (4096). A stream whose
643 /// throughput stays below this floor for a full window is abandoned — catching both a
644 /// wedged upstream (throughput to zero) and a slow drip (a trickle that never trips a
645 /// bare idle timeout). `0` disables the throughput test and leaves pure idle detection:
646 /// at least one byte per window.
647 ///
648 /// It also sets the average throughput each own-origin range-pull read must sustain
649 /// after the [`Self::node_pull_stall_window_sec`] head start (ADR 037). A read past that
650 /// budget fails as an origin fault and bumps `decdn_cache_origin_range_timeouts_total`.
651 /// Such a read returns the whole body at once, so it has no idle form: `0` leaves
652 /// those reads unbounded.
653 pub node_pull_min_throughput_bps: Option<u64>,
654 /// Cache eviction policy selector (ADR 040). Absent =>
655 /// [`crate::config::DEFAULT_EVICTION_POLICY`] (`"lru"`). Must be `"lru"` or
656 /// `"tinylfu"`; an unrecognized name is rejected at config load rather than
657 /// silently falling back.
658 pub eviction_policy: Option<String>,
659 /// Cache admission policy selector (ADR 040). Absent =>
660 /// [`crate::config::DEFAULT_ADMISSION_POLICY`] (`"always"`). Must be
661 /// `"always"` or `"tinylfu"`; an unrecognized name is rejected at config
662 /// load rather than silently falling back.
663 pub admission_policy: Option<String>,
664 /// W-TinyLFU tuning knobs (ADR 040). Consulted whenever
665 /// [`Self::eviction_policy`] or [`Self::admission_policy`] is `"tinylfu"`;
666 /// otherwise unused. Absent => every field defaults.
667 pub tinylfu: Option<TinyLfuConfig>,
668 /// Refuse-to-serve economics tuning (ADR 041): the margin policy, its
669 /// discount, and per-source speculative-warming allowance. Absent =>
670 /// every field defaults.
671 pub serve_economics: Option<ServeEconomicsConfig>,
672}
673
674/// W-TinyLFU tuning knobs (ADR 040). See [`CacheConfig::tinylfu`].
675#[derive(Debug, Default, Serialize, Deserialize)]
676#[serde(deny_unknown_fields)]
677pub struct TinyLfuConfig {
678 /// Count-min sketch size in bytes, shared by admission and eviction.
679 /// Absent => [`crate::config::DEFAULT_TINYLFU_SKETCH_BYTES`] (262144).
680 /// Larger sketches reduce frequency-estimate collisions at the cost of
681 /// more resident memory.
682 pub sketch_bytes: Option<usize>,
683 /// Prior sightings a probation member needs before promotion to the main
684 /// segment. Absent => [`crate::config::DEFAULT_TINYLFU_PROMOTION_THRESHOLD`]
685 /// (2).
686 pub promotion_threshold: Option<u32>,
687 /// Percent of `cache.cache_size_mb` the probation segment is capped to.
688 /// Absent => [`crate::config::DEFAULT_TINYLFU_PROBATION_TARGET_PCT`] (10).
689 /// Hard bounds `[1, 50]`.
690 pub probation_target_pct: Option<u64>,
691 /// Reserved: half-life in seconds for aging the frequency sketch. Absent
692 /// => [`crate::config::DEFAULT_TINYLFU_AGING_HALFLIFE_SEC`] (600).
693 /// Currently resolved and stored but not consulted — the shipped sketch
694 /// ages on a fixed observation-count window rather than a wall-clock
695 /// half-life.
696 pub aging_halflife_sec: Option<u64>,
697}
698
699/// Refuse-to-serve economics tuning knobs (ADR 041). See
700/// [`CacheConfig::serve_economics`].
701#[derive(Debug, Default, Serialize, Deserialize)]
702#[serde(deny_unknown_fields)]
703pub struct ServeEconomicsConfig {
704 /// Refuse-to-serve policy selector. `"off"` or `"margin"`. Absent =>
705 /// [`crate::config::DEFAULT_SERVE_ECONOMICS_POLICY`] (`"margin"`). An
706 /// unrecognized name is rejected at config load rather than silently
707 /// falling back.
708 pub policy: Option<String>,
709 /// Discount applied to the sell price when deciding whether a
710 /// speculative pull clears its margin gate. Within `(0.0, 1.0]`. Absent
711 /// => [`crate::config::DEFAULT_SERVE_ECONOMICS_DISCOUNT_BPS`] (5000,
712 /// i.e. `0.5`).
713 pub discount: Option<f64>,
714 /// Maximum number of concurrent speculative-warming sources. `>= 1`.
715 /// Absent => [`crate::config::DEFAULT_SERVE_ECONOMICS_N_MAX`] (64).
716 pub n_max: Option<u32>,
717 /// Per-source speculative-warming allowance, in payment base units.
718 /// `> 0`. Absent =>
719 /// [`crate::config::DEFAULT_SERVE_ECONOMICS_WARMING_BUDGET`]
720 /// (5,000,000).
721 pub warming_budget: Option<u64>,
722 /// Per-source allowance refill rate, in payment base units per second.
723 /// `>= 0`; `0` disables time-based refill (allowance only resets on
724 /// restart or via the serve-vindicated upgrade). Absent =>
725 /// [`crate::config::DEFAULT_SERVE_ECONOMICS_WARMING_REFILL`] (58).
726 pub warming_refill: Option<u64>,
727}
728
729/// Origin backend selection (#437). Tagged on the inner `kind` field.
730///
731/// `deny_unknown_fields` is set on the enum and on each variant's
732/// payload so that an operator typo (`decompres = "auto"` on the Http
733/// variant, for instance) fails at config load instead of silently
734/// no-opping.
735///
736/// ```toml
737/// [cache.origin]
738/// kind = "http"
739/// url = "https://origin.example/"
740/// decompress = "auto" # optional; defaults to "auto"
741///
742/// # — or —
743/// [cache.origin]
744/// kind = "fs"
745/// path = "/var/lib/decdn/origin"
746///
747/// # — or —
748/// [cache.origin]
749/// kind = "s3"
750/// bucket = "decdn-blobs"
751/// region = "us-east-1"
752/// # endpoint_url = "https://<accountid>.r2.cloudflarestorage.com" # for R2/B2/MinIO
753/// # path_style = true # for MinIO
754/// # prefix = "blobs/"
755/// # [cache.origin.credentials] source = "static" / "default-chain"
756/// ```
757///
758/// For multi-origin fallback (#284) use the plural `[[cache.origins]]`
759/// array-of-tables form (mutually exclusive with `[cache.origin]`):
760///
761/// ```toml
762/// [[cache.origins]]
763/// kind = "http"
764/// url = "https://primary.example/"
765///
766/// [[cache.origins]]
767/// kind = "s3"
768/// bucket = "mirror-blobs"
769/// region = "us-east-1"
770///
771/// [[cache.origins]]
772/// kind = "fs"
773/// path = "/var/lib/decdn/archive"
774/// ```
775#[derive(Debug, Serialize, Deserialize)]
776#[serde(tag = "kind", rename_all = "lowercase", deny_unknown_fields)]
777pub enum OriginConfig {
778 /// HTTP(S) origin served at `{url}/{blake3_hex}`.
779 Http {
780 /// Base URL; must be `http`/`https` and is path-normalized to
781 /// end with `/` so `{base}.join(&hex)` produces `{base}/{hex}`.
782 url: String,
783 /// How to handle `Content-Encoding` on the HTTP response
784 /// (#312). `"auto"` (default) decompresses gzip/zstd
785 /// transparently; `"strict"` refuses any non-identity
786 /// encoding. The BLAKE3 content-address is computed over the
787 /// canonical (decompressed) form, so `"strict"` is only safe
788 /// for origins guaranteed to serve already-canonical bytes.
789 decompress: Option<decdn_config_types::DecompressMode>,
790 },
791 /// Local filesystem origin rooted at `path`; blobs live at
792 /// `{path}/{hex[0..2]}/{hex}`.
793 Fs {
794 /// Filesystem root.
795 path: PathBuf,
796 },
797 /// S3-compatible object storage (#437): AWS S3, Cloudflare R2,
798 /// Backblaze B2, `MinIO`, etc. Object key layout is
799 /// `{prefix?}{hex[0..2]}/{hex}` — sharded the same way as the
800 /// filesystem backend so operators can copy blobs between
801 /// backends without rewriting tooling.
802 S3(S3OriginConfig),
803}
804
805/// S3-compatible origin configuration (#437) — wire form.
806///
807/// Validation runs at config-resolution time via the (private)
808/// `resolve_s3_origin` helper, which produces the runtime form
809/// [`super::resolved::ResolvedS3Config`]. Unvalidated instances of
810/// this type cannot reach the cache wiring layer because
811/// [`super::resolved::ResolvedOrigin::S3`] holds the resolved form,
812/// not this one.
813#[derive(Debug, Clone, Serialize, Deserialize)]
814#[serde(deny_unknown_fields)]
815pub struct S3OriginConfig {
816 /// Bucket name. Validated DNS-safe at config resolution
817 /// (lowercase `[a-z0-9.-]`, 3–63 chars; see
818 /// `validate_s3_bucket_name`).
819 pub bucket: String,
820 /// AWS region (e.g. `"us-east-1"`). Required because the S3 SDK
821 /// uses it for `SigV4` signing even when a custom `endpoint_url`
822 /// is set. Validated non-empty at config resolution.
823 pub region: String,
824 /// Custom endpoint URL for non-AWS S3-compatible providers (R2,
825 /// B2, `MinIO`). Omit for AWS S3. Validated as `http`/`https` at
826 /// config resolution.
827 pub endpoint_url: Option<String>,
828 /// Use path-style addressing (`https://endpoint/bucket/key`)
829 /// instead of virtual-hosted-style (`https://bucket.endpoint/key`).
830 /// Required `true` for `MinIO` and many self-hosted providers;
831 /// AWS S3 and Cloudflare R2 use virtual-hosted-style by default.
832 /// Absent => fall back to the SDK default (virtual-hosted-style).
833 pub path_style: Option<bool>,
834 /// Optional key prefix prepended to every fetched object. Final
835 /// key is `{prefix}{hex[0..2]}/{hex}`. Validation rejects
836 /// `prefix` starting with `/`, containing `..`, containing
837 /// backslash, or containing ASCII control / whitespace
838 /// characters; auto-appends a trailing `/` if the prefix is
839 /// non-empty and missing one (mirrors `parse_origin_url`'s
840 /// trailing-slash normalization).
841 pub prefix: Option<String>,
842 /// Credential source. Absent => use the AWS default credential
843 /// chain (env vars, `~/.aws/credentials`, IAM role / instance
844 /// profile).
845 pub credentials: Option<S3Credentials>,
846 /// How to handle `Content-Encoding` on the S3 response (#804).
847 /// `"auto"` (default) decompresses gzip/zstd transparently;
848 /// `"strict"` refuses any non-identity encoding. The BLAKE3
849 /// content-address is computed over the canonical (decompressed)
850 /// form, so `"strict"` is only safe for origins guaranteed to serve
851 /// already-canonical bytes. Mirrors the HTTP origin's `decompress`
852 /// knob.
853 pub decompress: Option<decdn_config_types::DecompressMode>,
854}
855
856/// S3 credential source (#437). Tagged on the inner `source` field.
857///
858/// Static credential fields use [`SecretString`] so an incidental
859/// `tracing::debug!(?cfg)` or panic backtrace cannot leak the
860/// material — the wrapper redacts itself in `Debug` output, and
861/// its `Serialize` impl emits a non-reversible hashed placeholder
862/// rather than the cleartext, so any incidental serialization of the
863/// config is redacted too (see [`SecretString`] for the full
864/// contract). The codebase already redacts HTTP-origin URL credentials
865/// (see `decdn_config_types::redact_for_log`); this is the same pattern
866/// for TOML-borne secrets.
867///
868/// ```toml
869/// [cache.origin.credentials]
870/// source = "static"
871/// access_key_id = "AKIA..."
872/// secret_access_key = "..."
873/// # session_token = "..." # optional
874///
875/// # — or —
876/// [cache.origin.credentials]
877/// source = "default-chain"
878/// # profile = "my-profile" # optional, override default profile
879/// ```
880#[derive(Debug, Clone, Serialize, Deserialize)]
881#[serde(tag = "source", rename_all = "kebab-case", deny_unknown_fields)]
882pub enum S3Credentials {
883 /// Static IAM credentials embedded in the config file. Common
884 /// for R2/B2/MinIO operator deployments where simple key-pair
885 /// audit is preferred; for AWS S3, prefer the default credential
886 /// chain so credentials live outside the TOML.
887 Static {
888 /// AWS access key ID. Stored in [`SecretString`] so it
889 /// redacts in `Debug` output alongside the secret key.
890 access_key_id: SecretString,
891 /// AWS secret access key. Redacted in `Debug` output.
892 secret_access_key: SecretString,
893 /// Optional STS session token (for assume-role / SSO flows
894 /// where a static key isn't enough on its own). Redacted.
895 session_token: Option<SecretString>,
896 },
897 /// Use the standard AWS credential chain: `AWS_ACCESS_KEY_ID` /
898 /// `AWS_SECRET_ACCESS_KEY` env vars, then `~/.aws/credentials`
899 /// profile, then IAM role / instance profile.
900 DefaultChain {
901 /// Override the default profile name when reading
902 /// `~/.aws/credentials`. Threaded through to the SDK as the
903 /// equivalent of the `AWS_PROFILE` env var.
904 profile: Option<String>,
905 },
906}
907
908/// Payment section of the config file.
909#[derive(Debug, Default, Serialize, Deserialize)]
910#[serde(deny_unknown_fields)]
911pub struct PaymentConfig {
912 /// Rate per MB in USDC base units.
913 pub rate_per_mb: Option<u64>,
914 /// Downstream credit-window ceiling in bytes (ADR 003 §Credit window). The
915 /// per-stream window ramps toward this cap as the stream pays, bounding the
916 /// node's credit exposure (unbilled egress already on the wire) to
917 /// `paid / credit_ramp_divisor`; the client's exposure stays zero (vouchers
918 /// are cumulative over delivered bytes). Absent =>
919 /// [`crate::config::DEFAULT_CREDIT_MAX`] (64 MiB). Floored at one chunk
920 /// ([`decdn_protocol::client::CHUNK_BYTES`], 1 MiB); a value at or below one
921 /// chunk reproduces the strict stop-and-wait cadence — deliver a chunk, then
922 /// recoup it. It is node-local config, not a governance-owned parameter — no
923 /// contract holds a delivery-cadence value.
924 pub credit_max: Option<decdn_config_types::Bytes>,
925 /// Ramp divisor for the credit window (ADR 003 §Credit window): the window is
926 /// `paid / credit_ramp_divisor`, floored at one chunk plus one chunk group and capped at
927 /// [`Self::credit_max`]. Absent => [`crate::config::DEFAULT_CREDIT_RAMP_DIVISOR`]
928 /// (2). `0` opens the full ceiling immediately, reproducing the flat-window
929 /// behavior. It is node-local config, not a governance-owned parameter.
930 pub credit_ramp_divisor: Option<u64>,
931 /// Target wire-frame size for the serve path, in bytes (ADR 005
932 /// §`cdn/client/v1`). Absent => [`crate::config::DEFAULT_FRAME_TARGET_BYTES`]
933 /// (1 MiB). Must be > 0.
934 ///
935 /// Node-local policy that is never negotiated: no message carries it, the payer
936 /// accepts any non-empty frame, and both bao verification and the payment meter
937 /// are defined over byte counts rather than frame boundaries. Raising it lowers
938 /// per-frame CPU per byte served, up to one `CHUNK_BYTES` payment interval
939 /// (1 MiB) — a frame never crosses a payment-chunk boundary, so a larger value
940 /// is rejected rather than silently ignored. The serve loop also clamps each
941 /// frame to the credit window's remaining room, so a large value does not widen
942 /// credit exposure.
943 pub frame_target_bytes: Option<decdn_config_types::Bytes>,
944 /// Background flush period for the lane store, in ms. See
945 /// [`crate::config::DEFAULT_VOUCHER_COMMIT_INTERVAL_MS`] (5 s). Must be > 0.
946 pub voucher_commit_interval_ms: Option<u64>,
947}
948
949/// Security / rate-limiting section of the config file.
950///
951/// All fields are optional; defaults produce a safe configuration out of
952/// the box.
953///
954/// **All fields are hot-reloadable** on SIGHUP and via `admin_v1_reload`.
955/// The live `ConnectionLimiter` rebuilds its keyed `governor` rate
956/// limiter from the new quota and swaps it under an `RwLock`. The
957/// `Arc<Semaphore>` identity is preserved across cap resizes via
958/// `add_permits` / `acquire_many_owned(...).forget()` so every
959/// in-flight `OwnedSemaphorePermit` keeps draining into the same
960/// semaphore on `Drop`.
961///
962/// **Caveats:** during a shrink the live concurrency cap is `>= new`
963/// until enough handlers drain; under racing reloads (N→N+1→N) the
964/// post-task permit count may briefly land anywhere in `[N, N+1]`
965/// until the next reload reconciles. Accepted as a known trade-off:
966/// strict resize semantics would require a full handler-drain barrier.
967///
968/// Token-bucket state is *not* preserved across a reload — the keyed
969/// limiter is rebuilt from scratch. Operators tuning the per-source
970/// quota live should expect the next acquire after a reload to start
971/// with a fresh burst budget.
972///
973/// **`0` means "disable this layer":**
974/// - `max_concurrent_handlers = 0`: no global concurrency cap.
975/// - `per_source_rate_per_sec = 0`: per-source rate-limit disabled. The
976/// paired `per_source_burst` field is ignored — the limiter
977/// short-circuits before the keyed map is consulted.
978/// - `max_tracked_sources = 0`: rate-limit bookkeeping map is unbounded.
979/// **Warning:** an attacker churning source addresses can grow the
980/// map without bound in this mode — operator opt-in only.
981///
982/// `burst > 0` is required only when paired with a positive rate. Setting
983/// `rate > 0` together with `burst = 0` would deny every request after
984/// the first burst-many — the resolver rejects that combination as a
985/// likely-typo. Setting `rate = 0` together with any `burst` value is
986/// fine; burst is unused once the layer is disabled.
987#[derive(Debug, Default, Serialize, Deserialize)]
988#[serde(deny_unknown_fields)]
989pub struct SecurityConfig {
990 /// Maximum number of concurrently in-flight QUIC handler tasks across
991 /// all deCDN-authored ALPNs. New connections beyond this limit are
992 /// closed immediately with `APP_ERR_RATE_LIMITED`. Default: 256.
993 /// `0` disables the global cap (no concurrency limit).
994 pub max_concurrent_handlers: Option<u32>,
995 /// Refill rate for per-source rate limiting (cells per second).
996 /// More generous than older per-NodeID-only schemes because a single
997 /// source IP may host a legitimate fleet. Default: 100. `0.0`
998 /// disables the per-source layer (the paired `per_source_burst` is
999 /// then ignored).
1000 ///
1001 /// The current implementation keys on the remote `IpAddr`. IPv6
1002 /// addresses are bucketed by their `/64` prefix, not the full
1003 /// 128-bit address. A customer-grade IPv6 allocation is typically
1004 /// `/64` or larger, so without this prefix grouping an attacker can
1005 /// rotate through `2^64` distinct source addresses inside one
1006 /// allocation and trivially defeat the layer. IPv4 addresses are
1007 /// used in full.
1008 ///
1009 /// "Per-source" rather than "per-IP" because the keying axis may
1010 /// extend in the future (e.g. `NodeID`) without renaming the field.
1011 pub per_source_rate_per_sec: Option<f64>,
1012 /// Burst capacity for per-source limiting. Default: 200 (2× the
1013 /// rate, providing headroom for jitter so well-behaved peers don't
1014 /// trip the limit on naturally-clumped requests). Required `> 0`
1015 /// when `per_source_rate_per_sec > 0`; ignored when the layer is
1016 /// disabled (`per_source_rate_per_sec = 0`).
1017 pub per_source_burst: Option<u32>,
1018 /// Hard cap on the number of distinct sources tracked in the
1019 /// rate-limit state. When the live keyed map exceeds this size the
1020 /// limiter prunes entries whose state has refilled to a fresh
1021 /// baseline. Default: 4096. `0` makes the map unbounded — see the
1022 /// type-level docs for the operator-opt-in warning.
1023 pub max_tracked_sources: Option<usize>,
1024}
1025
1026/// `[load_shed]` section — node-local load-shedding thresholds (overload
1027/// protection). All fields optional; unset falls back to the resolved
1028/// defaults.
1029#[derive(Debug, Default, Serialize, Deserialize)]
1030#[serde(deny_unknown_fields)]
1031pub struct LoadShedConfig {
1032 /// `resource-pressure` (default) or `always-admit`.
1033 pub policy: Option<String>,
1034 /// Serving egress budget in Mbps; `0` disables the egress ceiling.
1035 pub egress_budget_mbps: Option<u64>,
1036 /// Concurrency high-water mark (start shedding misses at/above).
1037 pub max_concurrent_serves_high: Option<u32>,
1038 /// Concurrency low-water mark (resume at/below).
1039 pub max_concurrent_serves_low: Option<u32>,
1040 /// Per-client concurrent-serve cap, enforced only under pressure; `0` off.
1041 pub per_client_serve_cap: Option<u32>,
1042}
1043
1044/// `[dht]` section — `cdn/dht/v1` settings (ADR 022).
1045///
1046/// The Kademlia routing-table parameters (k, α, bucket count, refresh
1047/// interval) are pinned by the protocol and not exposed here. The
1048/// rate-limit knobs nest under `[dht.rate_limit]` to match ADR 022 §DHT
1049/// Rate Limiting and to leave room for other future `dht.*` top-level knobs (e.g.
1050/// bootstrap peers, republish overrides) without breaking the operator
1051/// key path.
1052#[derive(Debug, Default, Serialize, Deserialize)]
1053#[serde(deny_unknown_fields)]
1054pub struct DhtConfig {
1055 /// Rate-limiter settings (ADR 022 §DHT Rate Limiting). Absent =>
1056 /// the defaults from the ADR (20/100/1000 req/s with 40/200/2000
1057 /// bursts).
1058 pub rate_limit: Option<DhtRateLimitConfig>,
1059}
1060
1061/// `[dht.rate_limit]` — three-layer token-bucket settings (ADR 022 §DHT
1062/// Rate Limiting).
1063///
1064/// Defaults match the ADR's conservative ceilings on adversarial load;
1065/// they are NOT steady-state operating targets — the ADR's bandwidth
1066/// analysis (§DHT Bandwidth Analysis "Headroom against rate limits")
1067/// shows realistic node traffic is two orders of magnitude below the
1068/// per-peer cap.
1069///
1070/// Setting any `*_rate_per_sec` to `0.0` disables that layer (operator
1071/// opt-out); the matching `*_burst` must also be `0` to avoid a deny-all
1072/// configuration. The resolver enforces this pairing the same way
1073/// `security.per_source_rate_per_sec` / `per_source_burst` are paired.
1074#[derive(Debug, Default, Serialize, Deserialize)]
1075#[serde(deny_unknown_fields)]
1076pub struct DhtRateLimitConfig {
1077 /// Per-peer (source `NodeId`) sustained rate. Absent => 20 req/s
1078 /// (ADR 022 §DHT Rate Limiting). `0.0` disables the layer.
1079 pub per_peer_rate_per_sec: Option<f64>,
1080 /// Per-peer burst capacity. Absent => 40. Required `> 0` when
1081 /// `per_peer_rate_per_sec > 0`.
1082 pub per_peer_burst: Option<u32>,
1083 /// Per-IP sustained rate. Absent => 100 req/s. `0.0` disables.
1084 pub per_ip_rate_per_sec: Option<f64>,
1085 /// Per-IP burst capacity. Absent => 200.
1086 pub per_ip_burst: Option<u32>,
1087 /// Global inbound DHT sustained rate. Absent => 1000 req/s.
1088 pub global_rate_per_sec: Option<f64>,
1089 /// Global inbound DHT burst capacity. Absent => 2000.
1090 pub global_burst: Option<u32>,
1091 /// Hard cap on the per-IP keyed-limiter map. Absent => 4096. `0`
1092 /// makes the map unbounded — operator opt-in (#645). Mirrors
1093 /// `security.max_tracked_sources` for the dispatch layer.
1094 pub max_tracked_per_ip: Option<usize>,
1095 /// Hard cap on the per-peer (`NodeId`) keyed-limiter map. Absent =>
1096 /// 4096. `0` makes the map unbounded (#645).
1097 pub max_tracked_per_peer: Option<usize>,
1098}
1099
1100/// `[probe]` section — `cdn/probe/v1` settings (ADR 005).
1101///
1102/// The rate-limit knobs nest under `[probe.rate_limit]` to match ADR 005
1103/// §Probe rate limiting and to leave room for other future `probe.*` top-level knobs without
1104/// breaking the operator key path.
1105#[derive(Debug, Default, Serialize, Deserialize)]
1106#[serde(deny_unknown_fields)]
1107pub struct ProbeConfig {
1108 /// Rate-limiter settings (ADR 005 §Probe rate limiting). Absent =>
1109 /// the defaults from the ADR (5/50/1000 req/s with 5/200/2000 bursts).
1110 pub rate_limit: Option<ProbeRateLimitConfig>,
1111}
1112
1113/// `[probe.rate_limit]` — three-layer token-bucket settings (ADR 005 §Probe
1114/// rate limiting).
1115///
1116/// Mirrors `[dht.rate_limit]` in shape; only the defaults differ (ADR 005
1117/// specifies a tighter per-peer cap than ADR 022's DHT layer because a probe
1118/// is unauthenticated and cheaper to flood). Setting any `*_rate_per_sec` to
1119/// `0.0` disables that layer (operator opt-out); the matching `*_burst` must
1120/// also be `0` to avoid a deny-all configuration — the resolver enforces this
1121/// pairing, the same way `security.per_source_*` and `dht.rate_limit.*` are
1122/// paired.
1123#[derive(Debug, Default, Serialize, Deserialize)]
1124#[serde(deny_unknown_fields)]
1125pub struct ProbeRateLimitConfig {
1126 /// Per-peer (source `NodeId`) sustained rate. Absent => 5 req/s
1127 /// (ADR 005 §Probe rate limiting). `0.0` disables the layer.
1128 pub per_peer_rate_per_sec: Option<f64>,
1129 /// Per-peer burst capacity. Absent => 5. Required `> 0` when
1130 /// `per_peer_rate_per_sec > 0`.
1131 pub per_peer_burst: Option<u32>,
1132 /// Per-IP sustained rate. Absent => 50 req/s. `0.0` disables.
1133 pub per_ip_rate_per_sec: Option<f64>,
1134 /// Per-IP burst capacity. Absent => 200.
1135 pub per_ip_burst: Option<u32>,
1136 /// Global inbound probe sustained rate. Absent => 1000 req/s.
1137 pub global_rate_per_sec: Option<f64>,
1138 /// Global inbound probe burst capacity. Absent => 2000.
1139 pub global_burst: Option<u32>,
1140 /// Hard cap on the per-IP keyed-limiter map. Absent => 4096. `0`
1141 /// makes the map unbounded — operator opt-in (#645).
1142 pub max_tracked_per_ip: Option<usize>,
1143 /// Hard cap on the per-peer (`NodeId`) keyed-limiter map. Absent =>
1144 /// 4096. `0` makes the map unbounded (#645).
1145 pub max_tracked_per_peer: Option<usize>,
1146}
1147
1148/// Observability section of the config file.
1149#[derive(Debug, Default, Serialize, Deserialize)]
1150#[serde(deny_unknown_fields)]
1151pub struct ObservabilityConfig {
1152 /// Log verbosity level.
1153 pub log_level: Option<LogLevel>,
1154 /// Log output format.
1155 pub log_format: Option<LogFormat>,
1156 /// Prometheus metrics HTTP port.
1157 pub metrics_port: Option<u16>,
1158 /// IP address to bind the metrics HTTP server on. Defaults to
1159 /// `127.0.0.1`. Set to `0.0.0.0` for containerised deployments.
1160 pub metrics_bind: Option<std::net::IpAddr>,
1161 /// Loopback admin HTTP port (`appendix-local-admin-http.md`). Explicit `0` disables the
1162 /// admin server; if the key is absent, resolution defaults to
1163 /// `9191`. Any positive value binds on `127.0.0.1:<port>`.
1164 pub admin_port: Option<u16>,
1165 /// OTLP collector endpoint URL.
1166 pub otlp_endpoint: Option<String>,
1167}
1168
1169/// Download-receipt audit-log retention section of the config file (#802).
1170///
1171/// Bounds the otherwise-unbounded `download_receipts.jsonl` (one line per
1172/// accepted payment proof, ~1 per MiB delivered) with size-based rotation:
1173/// once the live file reaches `max_file_bytes` it is rotated to a numbered
1174/// backup (`download_receipts.jsonl.1`, `.2`, …) and a fresh file is opened;
1175/// the oldest backup beyond `retained_files` is deleted. Both fields are
1176/// optional; the defaults bound disk to roughly `(retained_files + 1) *
1177/// max_file_bytes` while keeping a useful audit window.
1178#[derive(Debug, Default, Serialize, Deserialize)]
1179#[serde(deny_unknown_fields)]
1180pub struct ReceiptsConfig {
1181 /// Rotate the live receipt log once it reaches this many bytes. Absent =>
1182 /// default 128 MiB. Validated against a floor (1 MiB) and ceiling (1 GiB)
1183 /// so a typo cannot rotate every line or defeat rotation entirely.
1184 pub max_file_bytes: Option<u64>,
1185 /// Number of rotated backup files to retain (`.1`..=`.N`). Absent =>
1186 /// default 4. `0` keeps no backups (the live file is truncated in place on
1187 /// rotation). Capped at 100.
1188 pub retained_files: Option<u32>,
1189}
1190
1191/// Local content-denylist section of the config file (ADR 011 §Local Denylist).
1192///
1193/// The operator's own removal lever, independent of governance: entries take
1194/// effect on the next reload and bind only this node. ADR 011 §One-hour
1195/// removal orders makes this the only mechanism sized to a sub-day statutory
1196/// deadline (the EU TCO one-hour clock), because it is the only one entirely
1197/// within the order recipient's control.
1198#[derive(Debug, Default, Serialize, Deserialize)]
1199#[serde(deny_unknown_fields)]
1200pub struct ContentConfig {
1201 /// Blob hashes this node refuses to serve. Bare 64-character lowercase hex,
1202 /// the same spelling as `cache.pinned_hashes`. An invalid entry fails
1203 /// resolution rather than being skipped, so a typo in a takedown cannot
1204 /// silently leave content served. Duplicates are de-duplicated, not
1205 /// rejected — a repeated deny is still a deny.
1206 pub denied_hashes: Option<Vec<String>>,
1207 /// Operator addresses whose payment pools this node refuses to serve.
1208 /// `0x`-prefixed hex, checksum-agnostic (any case accepted); the zero
1209 /// address is rejected.
1210 pub denied_origins: Option<Vec<String>>,
1211}
1212
1213#[cfg(test)]
1214#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] // tests
1215mod tests;