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 fetch`,
277 /// `decdn bundle pull`, and the daemon's node-to-node cache-miss pull)
278 /// runs once no node serves at the current deposit (#1530). Larger values amortize gas across more delivery at
279 /// 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 the `decdn` client fetch commands (`fetch`,
288 /// `bundle pull`) default to `false`, i.e. an **exact deposit-sized** approval
289 /// scoped to what each pool escrows. Set `true` on the client to opt a
290 /// power user back into the unlimited 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 one chunk normally, and at
309 /// `credit_ramp_divisor == 0` it is `credit_max` raised to one chunk.
310 ///
311 /// This bounds concurrency, not paid throughput: a signer that pays its vouchers
312 /// releases that stream's reservation, so its cap recycles and a stream already
313 /// under way is never slowed. It carries no permanent memory and never penalizes a
314 /// signer for quitting.
315 ///
316 /// The window is the unit actually being rationed: honest need is `concurrent
317 /// un-vouchered streams × one window` regardless of the pool's size, so an absolute
318 /// window count fits it directly. Node-local risk policy — it needs no pool-owner
319 /// cooperation. Lower-clamped to one window (so a lone signer's first stream is
320 /// always admissible). Absent => default (`8`). See
321 /// `DEFAULT_POOL_FLOOR_SIGNER_LIVE_WINDOWS`.
322 pub pool_floor_signer_live_windows: Option<u64>,
323 /// USDC settlement-token contract address — the ERC-20 clients spend on
324 /// deposits into a payment pool. Declared here so a client config that names
325 /// it passes `decdn config validate` under this section's
326 /// `deny_unknown_fields`; the running client resolves the token at runtime
327 /// from `PaymentPool.usdc()`.
328 pub usdc_address: Option<String>,
329}
330
331/// Cache section of the config file.
332///
333/// `deny_unknown_fields` is set so that a misplaced origin field (a flat
334/// `origin_url` / `origin_path` / `decompress` directly under `[cache]`)
335/// gets a clear "unknown field" error at config load instead of a silent
336/// "no origin configured" surprise at the first cache miss. Origin
337/// settings live under the tagged `[cache.origin]` table — see
338/// [`OriginConfig`].
339#[derive(Debug, Default, Serialize, Deserialize)]
340#[serde(deny_unknown_fields)]
341pub struct CacheConfig {
342 /// Blob cache directory.
343 pub cache_dir: Option<PathBuf>,
344 /// Maximum cache size in megabytes. An *upper* bound on the cache ceiling,
345 /// not the ceiling itself: the eviction driver clamps it down each tick so
346 /// the cache never grows into the last [`Self::disk_headroom_mb`] of free
347 /// space on the `cache_dir` volume (#1930). Set it deliberately large to let
348 /// the disk-headroom clamp size the cache to whatever the volume offers.
349 pub cache_size_mb: Option<u64>,
350 /// Free disk in megabytes the eviction driver keeps unused on the
351 /// `cache_dir` volume, defended against *any* process, not just this cache
352 /// (#1930). Absent => [`crate::config::DEFAULT_DISK_HEADROOM_MB`] (8192, 8
353 /// GiB). Each tick the effective ceiling is
354 /// `footprint + max(0, free_disk - disk_headroom_mb)`, capped by
355 /// [`Self::cache_size_mb`]; below the headroom the driver evicts to claw disk
356 /// back. `0` opts out of the disk clamp (only `cache_size_mb` binds). A slow
357 /// soft-evict/GC cycle means the margin is defended reactively, so keep it
358 /// comfortably above one GC interval's worth of writes.
359 pub disk_headroom_mb: Option<u64>,
360 /// Maximum single blob size in megabytes.
361 pub max_blob_size_mb: Option<u64>,
362 /// Buyer-side ABSOLUTE per-MB rate ceiling for paid pulls (#1375), in the same
363 /// per-MB units as the wire `StreamResponse.rate_per_mb`. Absent / `0` =
364 /// unlimited. Bounds what this node, as a BUYER on a cache-miss pull, will
365 /// accept a provider to quote — on top of the always-applied probe-relative
366 /// bound. This is a buyer-side ceiling; the node never bounds its own
367 /// sell rate (a rate below the on-chain delivery floor still sells and
368 /// settles, with only its vote-weight byte credit clamped at redemption).
369 pub max_rate_per_mb: Option<u64>,
370 /// Single origin backend for cache pull-through (#437). Absent =>
371 /// no pull-through (unless [`Self::origins`] is set); cache misses
372 /// fail with `NoOrigin`. The variant (`http`, `fs`, or `s3`) is
373 /// selected by the `kind` field on the inner `[cache.origin]`
374 /// table. Mutually exclusive with [`Self::origins`] — operators
375 /// who need an ordered fallback list use the `[[cache.origins]]`
376 /// array-of-tables form instead. Setting both at once is rejected
377 /// at config resolution.
378 pub origin: Option<OriginConfig>,
379 /// Ordered list of origin backends with automatic fallback (#284).
380 /// On a cache miss the engine tries each entry in order; the next
381 /// entry is consulted when the current one returns `NotFound`,
382 /// returns a permanent error, or exhausts its per-origin transient
383 /// retry budget. Deterministic per-origin failures (hash mismatch,
384 /// blob-too-large) do **not** trigger fallback — they indicate a
385 /// misbehaving or misconfigured backend that must surface, not be
386 /// masked. Mutually exclusive with [`Self::origin`]; an empty
387 /// array is rejected (omit the key for "no pull-through" rather
388 /// than configuring zero origins). Duplicate entries are
389 /// permitted but logged as a warning at startup — operators may
390 /// legitimately want two entries with the same target for
391 /// connection-pool sharding. Set once at startup; changes require
392 /// a process restart, consistent with [`Self::origin_retry`].
393 ///
394 /// Worst-case *backoff* latency for an all-failing chain is
395 /// `len(origins) × sum_of_backoffs(origin_retry)` plus per-attempt
396 /// origin RTT/timeout. With default [`decdn_config_types::RetryPolicy`]
397 /// (3 retries, sleeps `100 + 200 + 400 ms` ± jitter — see
398 /// `RetryPolicy::default`) that's ~700 ms backoff per origin
399 /// (~2.1 s for three origins) — *not* `max_retries × backoff_cap`,
400 /// because the default schedule never saturates the 10 s cap. The
401 /// `max_backoff_ms` cap only dominates if `initial_backoff_ms` is
402 /// raised enough to saturate it; for the worst-case-cap regime use
403 /// `max_retries × max_backoff_ms` and multiply by chain length.
404 /// Plan operator timeouts accordingly — RTT/timeout per attempt
405 /// usually dominates the backoff term.
406 pub origins: Option<Vec<OriginConfig>>,
407 /// Hex-encoded BLAKE3 hashes that must stay cached regardless of LRU
408 /// pressure (#276). Each entry is 64 lowercase hex chars (BLAKE3
409 /// digest size). Invalid hex or wrong-length entries cause config
410 /// resolution to fail — fail-fast at load time beats a silent
411 /// "entry was ignored" surprise hours later when the operator
412 /// discovers the blob got evicted anyway.
413 pub pinned_hashes: Option<Vec<String>>,
414 /// Origin pull-through retry policy (#285, extended in #519).
415 /// Controls exponential backoff for transient HTTP / S3 /
416 /// filesystem errors *and* the body-phase retry strategy for
417 /// mid-stream `io::Error`s. Absent => defaults from
418 /// [`decdn_config_types::RetryPolicy::default`] (3 retries, 100ms
419 /// initial backoff doubling to 10s cap, 10% jitter,
420 /// `buffered_max_bytes = 4 MiB`). `max_retries = 0` opts out and
421 /// makes one attempt with no retry; `buffered_max_bytes = 0`
422 /// independently disables the body-phase buffer path so all
423 /// body-phase failures route through the streaming abort+restart
424 /// path. Set once at startup; changes require a restart.
425 ///
426 /// **Body-phase retry trade-offs (#519):**
427 ///
428 /// - **Buffer-then-commit** applies when the origin's advertised
429 /// `size_hint` is at or below `buffered_max_bytes`. The body
430 /// drains into memory before the iroh-blobs commit, so failed
431 /// attempts cost RAM (bounded by `buffered_max_bytes` per
432 /// in-flight fetch) but leave no on-disk garbage.
433 /// - **Abort + restart** applies above the threshold or when
434 /// `size_hint` is unknown. Each failed attempt strands up to
435 /// `max_blob_size_mb` of partial-import bytes until iroh-blobs
436 /// GC reclaims them at `cache.gc_interval_sec` cadence.
437 /// Worst-case disk amplification per fetch is
438 /// `(1 + max_retries) * max_blob_size_mb`.
439 ///
440 /// `decdn_config_types::RetryPolicy` carries `#[serde(default)]` so
441 /// partial sections (e.g. just `max_retries = 5`) get the rest of
442 /// the fields filled from defaults.
443 pub origin_retry: Option<decdn_config_types::RetryPolicy>,
444 /// Per-origin circuit-breaker policy (#963). Fronts each origin's
445 /// pull-through retry loop: after `failure_threshold` consecutive
446 /// origin-unavailable failures (transient errors that exhausted the
447 /// retry budget — NOT 404s or other permanent per-object errors) the
448 /// breaker trips OPEN and fast-fails every miss for `cooldown_ms`
449 /// without incurring any retry/backoff, then admits
450 /// `half_open_max_calls` trial pulls to probe recovery. Absent =>
451 /// defaults from [`decdn_config_types::CircuitBreakerPolicy::default`]
452 /// (enabled, trip after 5 failures, 30s cooldown, 1 half-open trial).
453 /// Set `enabled = false` (or `failure_threshold = 0`) to opt out and
454 /// run every miss through the full retry loop regardless of origin
455 /// health. Set once at startup; changes require a restart.
456 ///
457 /// `CircuitBreakerPolicy` carries `#[serde(default)]` so partial
458 /// sections (e.g. just `cooldown_ms = 60000`) get the rest of the
459 /// fields filled from defaults.
460 pub circuit_breaker: Option<decdn_config_types::CircuitBreakerPolicy>,
461 /// Optional `User-Agent` override sent on every HTTP origin
462 /// pull-through request (#435). Absent => the workspace default
463 /// (`decdn-node/<version>`); set to attribute CDN traffic in origin
464 /// access logs or to apply origin-side rate limits and routing
465 /// policy that distinguish CDN pulls from end-user clients. Empty
466 /// string is rejected at resolution.
467 pub user_agent: Option<String>,
468 /// Interval between iroh-blobs GC sweeps in seconds (#518). Absent =>
469 /// [`crate::config::DEFAULT_GC_INTERVAL_SEC`]. Set to `0` to disable
470 /// the periodic sweep — operators with external disk-reclaim
471 /// scheduling can opt out, accepting that bytes orphaned by
472 /// hash-mismatch / mid-stream-error pull-through paths leak until
473 /// they manually reset state. Tighter intervals shrink the
474 /// hostile-origin amplification window at the cost of more
475 /// list+sweep CPU per minute.
476 pub gc_interval_sec: Option<u64>,
477 /// Interval between origin-held-index rescans in seconds (#1130). Absent =>
478 /// [`crate::config::DEFAULT_FS_RESCAN_INTERVAL_SEC`]. The node re-walks its
479 /// `fs` origin directory (and re-checks present pins) at this cadence so a
480 /// file dropped into the origin becomes discoverable — probe `has_blob` +
481 /// DHT announce — within one interval, without a restart or reload. `0`
482 /// disables the periodic rescan (startup + `decdn node reload` still run
483 /// it). Shorter intervals pick up new files faster at the cost of more
484 /// directory-walk + `size()` I/O per minute.
485 pub fs_rescan_interval_sec: Option<u64>,
486 /// TTL, in seconds, for a memoised positive live-origin probe answer (#1130
487 /// pt3). Absent => [`crate::config::DEFAULT_ORIGIN_PROBE_TTL_SEC`] (15). A
488 /// probe for a hash absent from the `fs`-enumeration ∪ pins index falls back
489 /// to a live `HEAD`/`HeadObject` against the http/s3 origin; a present-with-
490 /// size answer is cached for this long so a non-pinned bucket object is
491 /// discoverable without a per-probe origin round-trip on every probe.
492 /// Shorter TTLs track origin deletions faster at the cost of more `HEAD`
493 /// traffic. See `origin_probe_negative_ttl_sec` for the absent-answer TTL.
494 pub origin_probe_ttl_sec: Option<u64>,
495 /// TTL, in seconds, for a memoised negative (absent) live-origin probe
496 /// answer (#1130 pt3). Absent =>
497 /// [`crate::config::DEFAULT_ORIGIN_PROBE_NEGATIVE_TTL_SEC`] (2). Caching the
498 /// negative answer is what blunts a random-hash probe flood; keeping this
499 /// TTL short bounds how long a stale `Absent` can hide newly-available own
500 /// content from a probe.
501 pub origin_probe_negative_ttl_sec: Option<u64>,
502 /// TTL, in seconds, for a memoised fault live-origin probe answer (#1130
503 /// pt3). Absent => [`crate::config::DEFAULT_ORIGIN_PROBE_FAULT_TTL_SEC`]
504 /// (5). Longer than `origin_probe_negative_ttl_sec` because the memo is
505 /// keyed per hash and a client retrying one hash against a failing origin
506 /// is the common shape; shorter than `origin_probe_ttl_sec` because a
507 /// memoised fault refuses paying clients for as long as it stands.
508 /// Rejected unless `origin_probe_negative_ttl_sec <=
509 /// origin_probe_fault_ttl_sec <= origin_probe_ttl_sec`.
510 pub origin_probe_fault_ttl_sec: Option<u64>,
511 /// Per-probe ceiling, in milliseconds, on the live-origin `HEAD`/`HeadObject`
512 /// (#1130 pt3). Absent => [`crate::config::DEFAULT_ORIGIN_PROBE_TIMEOUT_MS`]
513 /// (2000). A slow origin must never stall the probe hot path; on timeout the
514 /// probe answers `has_blob: false` (safe — never slashable) and
515 /// the miss is memoised as absent for one TTL.
516 pub origin_probe_timeout_ms: Option<u64>,
517 /// Maximum distinct hashes held in the live-origin probe memo (#1130 pt3).
518 /// Absent => [`crate::config::DEFAULT_ORIGIN_PROBE_MEMO_CAPACITY`] (4096).
519 /// Bounds memo memory under a random-hash probe flood; the cache is
520 /// best-effort, so at capacity one arbitrary entry is dropped to admit a new
521 /// answer (expired entries reclaimed first).
522 pub origin_probe_memo_capacity: Option<u64>,
523 /// Cache eviction driver: percent of [`Self::cache_size_mb`] above which
524 /// the driver actively evicts (#1173, ADR 040). Absent =>
525 /// [`crate::config::DEFAULT_EVICTION_HIGH_WATER_PCT`] (90). Hard bounds
526 /// `[60, 95]`; set above the 25% probe-hold recommendation so a full hold
527 /// budget plus in-flight writes don't trip it, below 95% for write
528 /// headroom between sweeps.
529 pub eviction_high_water_pct: Option<u64>,
530 /// LRU eviction driver: percent of [`Self::cache_size_mb`] the driver
531 /// evicts down to before returning to idle (#1173). Absent =>
532 /// [`crate::config::DEFAULT_EVICTION_TARGET_PCT`] (80). Hard bounds
533 /// `[40, 90]`, and MUST be `<= eviction_high_water_pct - 5` — the 5-point
534 /// hysteresis gap is structural (prevents thrash on writes hovering near
535 /// the trigger), not a tunable nicety.
536 pub eviction_target_pct: Option<u64>,
537 /// LRU eviction driver: maximum candidates removed per tick before the
538 /// driver yields (#1173). Absent =>
539 /// [`crate::config::DEFAULT_EVICTION_PER_SWEEP_BUDGET`] (16). Hard bounds
540 /// `[1, 256]`. Bounds worst-case driver-induced latency on the cache hot
541 /// path (~1 ms filesystem-unlink per entry); the driver continues across
542 /// consecutive ticks until below target or candidates are exhausted.
543 pub eviction_per_sweep_budget: Option<u64>,
544 /// LRU eviction driver: seconds between driver wakeups (#1173). Absent =>
545 /// [`crate::config::DEFAULT_EVICTION_TICK_SECS`] (1). Hard bounds
546 /// `[1, 60]`.
547 ///
548 /// **Cost note:** every tick measures the on-disk footprint, which walks the
549 /// blob list and issues one `status()` per blob, plus an O(n) clone of the
550 /// pinned set — latched or not. An idle tick is not free: the driver keeps
551 /// no cached footprint, so it re-measures on every tick (ADR 040). On a
552 /// cache holding many blobs,
553 /// raise this to trade eviction latency for steady-state store load.
554 pub eviction_tick_secs: Option<u64>,
555 /// Maximum number of concurrently held (eviction-exempt) blobs for the
556 /// probe-triggered hold (ADR 005 §Hold budget, #318). Holds are
557 /// per-blob: multiple peers probing the same hash share one slot. When the
558 /// budget is exhausted, a probe for a blob that is present but gets no slot
559 /// still advertises (`has_blob: true`) and places no eviction hold — the
560 /// blob may be LRU-evicted before the pull, costing one wasted round trip
561 /// (never a slash). Absent => [`crate::config::DEFAULT_MAX_PROBE_HOLDS`]
562 /// (256). `0` is the operator opt-out: holds are disabled and probes answer
563 /// `has_blob: false` for store-backed content — content servable from a
564 /// configured origin takes no hold and is still advertised. Operators with
565 /// small caches SHOULD set this to ≤25% of cache capacity.
566 pub max_probe_holds: Option<u64>,
567 /// Number of probe-hold slots reserved for the **stake lane** —
568 /// registered operators issuing node-to-node cache-miss probes (#757,
569 /// ADR 003 §Admission and Priority). An end-client probe places no eviction
570 /// hold once usage reaches `max_probe_holds - stake_lane_reserved_holds` (it
571 /// still advertises `has_blob: true` if the blob is present), keeping the
572 /// last `stake_lane_reserved_holds` slots available for node-to-node probes
573 /// so end-client load cannot starve their holds. Absent / `0` (the default)
574 /// disables the reservation entirely — a single-lane node is unaffected.
575 /// Values `>= max_probe_holds` reserve the whole budget for the stake lane:
576 /// no end-client probe takes a hold slot, regardless of current usage.
577 pub stake_lane_reserved_holds: Option<u64>,
578 /// Enable node-to-node paid cache-miss pull-through (#831, ADR 001/022).
579 /// Absent / `false` (the default) → a cache miss serves `NotFound` as
580 /// before. When `true` *and* the buyer-channel service bootstrapped, a
581 /// miss triggers DHT provider discovery → probe → ranked paid pull from an
582 /// upstream node, which populates the cache and is then served. OFF by
583 /// default for the initial network: enabling it makes the node front USDC
584 /// egress to fill misses (bounded by `blockchain.buyer_working_deposit_micro_usdc`
585 /// and the upstream's per-MB rate), and the serving path only triggers it
586 /// behind a valid, pool-bound client so an unpaid request cannot drive
587 /// egress.
588 pub node_to_node_pull_through_enabled: Option<bool>,
589 /// When `false`, the node serves and seeds only content its own backend
590 /// holds; a foreign hash is declined like a miss. Absent resolves to a
591 /// role-derived default: `false` (origin-only) when an origin backend is
592 /// configured, `true` (relay) when none is. Set this explicitly to
593 /// override either default. Node-local; `cache.*` is restart-required.
594 pub relay_foreign_namespaces: Option<bool>,
595 /// Number of discovered providers to probe before ranking on a
596 /// node-to-node pull (#831). Absent =>
597 /// [`crate::config::DEFAULT_NODE_PULL_PROBE_FANOUT`] (5). Higher widens
598 /// provider choice at the cost of more probe round trips per miss; `0`
599 /// probes none, so no pull can succeed (a way to disable the pull while
600 /// keeping the feature flag on).
601 pub node_pull_probe_fanout: Option<usize>,
602 /// Wall-clock timeout in seconds for the STREAM-OPEN stage of a single upstream pull
603 /// on a node-to-node miss (#831) — connect, handshake, and the
604 /// signed `StreamResponse`. It does NOT bound the buyer-channel open, which precedes
605 /// it on its own 5 s budget. Absent =>
606 /// [`crate::config::DEFAULT_NODE_PULL_TIMEOUT_SEC`] (20). Bounds how long a
607 /// miss blocks the serving path on one upstream before falling through to
608 /// the next ranked candidate (or `NotFound`). This is the *per-upstream*
609 /// budget; the overall pull-through deadline is derived as roughly
610 /// `MAX_PROVIDER_ATTEMPTS × (channel open + it + stall)` plus a fixed discovery
611 /// allowance, so the fallback loop reaches every ranked candidate (#859).
612 ///
613 /// It does NOT bound the streaming stage (#1134) — that is
614 /// [`Self::node_pull_stall_window_sec`]. A wall clock over the bytes would
615 /// cap the blob size a node can pull through at roughly
616 /// `this × link speed`.
617 pub node_pull_timeout_sec: Option<u64>,
618 /// Throughput-floor window in seconds for the STREAMING stage of an upstream pull
619 /// (#1797). Absent => [`crate::config::DEFAULT_NODE_PULL_STALL_WINDOW_SEC`] (20). Bytes
620 /// read off the QUIC stream are counted sub-frame, and the pull aborts when the bytes
621 /// across the trailing window fall below [`Self::node_pull_min_throughput_bps`] — so the
622 /// signal is frame-size-independent and indifferent to blob size. It is what makes a pull
623 /// of any size safe to leave uncapped.
624 ///
625 /// Tune it against upstream responsiveness, not content size: too short and a brief
626 /// network hiccup abandons a healthy transfer; too long and a dead upstream is held onto
627 /// for longer than necessary before the fallback loop moves on. The abort is
628 /// requester-local policy and does NOT score the upstream's reputation (#1797).
629 ///
630 /// "Longer" is multiplied, not added. A silent candidate costs one full window of this,
631 /// and the derived outer deadline budgets that for EVERY candidate, so a second here is
632 /// ~3 seconds of worst-case client wait on a total miss (167.5 s at defaults).
633 ///
634 /// It also sets the head start of the time budget for each own-origin range-pull read
635 /// (ADR 037): one `{H}.obao4` fetch or one data window of at most 4 MiB. That budget is
636 /// this window plus the read size at [`Self::node_pull_min_throughput_bps`], so it
637 /// scales with the read and never caps the blob size.
638 pub node_pull_stall_window_sec: Option<u64>,
639 /// Minimum sustained upstream throughput in bytes per second over
640 /// [`Self::node_pull_stall_window_sec`] (#1797). Absent =>
641 /// [`crate::config::DEFAULT_NODE_PULL_MIN_THROUGHPUT_BPS`] (4096). A stream whose
642 /// throughput stays below this floor for a full window is abandoned — catching both a
643 /// wedged upstream (throughput to zero) and a slow drip (a trickle that never trips a
644 /// bare idle timeout). `0` disables the throughput test and leaves pure idle detection:
645 /// at least one byte per window.
646 ///
647 /// It also sets the average throughput each own-origin range-pull read must sustain
648 /// after the [`Self::node_pull_stall_window_sec`] head start (ADR 037). A read past that
649 /// budget fails as an origin fault and bumps `decdn_cache_origin_range_timeouts_total`.
650 /// Such a read returns the whole body at once, so it has no idle form: `0` leaves
651 /// those reads unbounded.
652 pub node_pull_min_throughput_bps: Option<u64>,
653 /// Cache eviction policy selector (ADR 040). Absent =>
654 /// [`crate::config::DEFAULT_EVICTION_POLICY`] (`"lru"`). Must be `"lru"` or
655 /// `"tinylfu"`; an unrecognized name is rejected at config load rather than
656 /// silently falling back.
657 pub eviction_policy: Option<String>,
658 /// Cache admission policy selector (ADR 040). Absent =>
659 /// [`crate::config::DEFAULT_ADMISSION_POLICY`] (`"always"`). Must be
660 /// `"always"` or `"tinylfu"`; an unrecognized name is rejected at config
661 /// load rather than silently falling back.
662 pub admission_policy: Option<String>,
663 /// W-TinyLFU tuning knobs (ADR 040). Consulted whenever
664 /// [`Self::eviction_policy`] or [`Self::admission_policy`] is `"tinylfu"`;
665 /// otherwise unused. Absent => every field defaults.
666 pub tinylfu: Option<TinyLfuConfig>,
667 /// Refuse-to-serve economics tuning (ADR 041): the margin policy, its
668 /// discount, and per-source speculative-warming allowance. Absent =>
669 /// every field defaults.
670 pub serve_economics: Option<ServeEconomicsConfig>,
671}
672
673/// W-TinyLFU tuning knobs (ADR 040). See [`CacheConfig::tinylfu`].
674#[derive(Debug, Default, Serialize, Deserialize)]
675#[serde(deny_unknown_fields)]
676pub struct TinyLfuConfig {
677 /// Count-min sketch size in bytes, shared by admission and eviction.
678 /// Absent => [`crate::config::DEFAULT_TINYLFU_SKETCH_BYTES`] (262144).
679 /// Larger sketches reduce frequency-estimate collisions at the cost of
680 /// more resident memory.
681 pub sketch_bytes: Option<usize>,
682 /// Prior sightings a probation member needs before promotion to the main
683 /// segment. Absent => [`crate::config::DEFAULT_TINYLFU_PROMOTION_THRESHOLD`]
684 /// (2).
685 pub promotion_threshold: Option<u32>,
686 /// Percent of `cache.cache_size_mb` the probation segment is capped to.
687 /// Absent => [`crate::config::DEFAULT_TINYLFU_PROBATION_TARGET_PCT`] (10).
688 /// Hard bounds `[1, 50]`.
689 pub probation_target_pct: Option<u64>,
690 /// Reserved: half-life in seconds for aging the frequency sketch. Absent
691 /// => [`crate::config::DEFAULT_TINYLFU_AGING_HALFLIFE_SEC`] (600).
692 /// Currently resolved and stored but not consulted — the shipped sketch
693 /// ages on a fixed observation-count window rather than a wall-clock
694 /// half-life.
695 pub aging_halflife_sec: Option<u64>,
696}
697
698/// Refuse-to-serve economics tuning knobs (ADR 041). See
699/// [`CacheConfig::serve_economics`].
700#[derive(Debug, Default, Serialize, Deserialize)]
701#[serde(deny_unknown_fields)]
702pub struct ServeEconomicsConfig {
703 /// Refuse-to-serve policy selector. `"off"` or `"margin"`. Absent =>
704 /// [`crate::config::DEFAULT_SERVE_ECONOMICS_POLICY`] (`"margin"`). An
705 /// unrecognized name is rejected at config load rather than silently
706 /// falling back.
707 pub policy: Option<String>,
708 /// Discount applied to the sell price when deciding whether a
709 /// speculative pull clears its margin gate. Within `(0.0, 1.0]`. Absent
710 /// => [`crate::config::DEFAULT_SERVE_ECONOMICS_DISCOUNT_BPS`] (5000,
711 /// i.e. `0.5`).
712 pub discount: Option<f64>,
713 /// Maximum number of concurrent speculative-warming sources. `>= 1`.
714 /// Absent => [`crate::config::DEFAULT_SERVE_ECONOMICS_N_MAX`] (64).
715 pub n_max: Option<u32>,
716 /// Per-source speculative-warming allowance, in payment base units.
717 /// `> 0`. Absent =>
718 /// [`crate::config::DEFAULT_SERVE_ECONOMICS_WARMING_BUDGET`]
719 /// (5,000,000).
720 pub warming_budget: Option<u64>,
721 /// Per-source allowance refill rate, in payment base units per second.
722 /// `>= 0`; `0` disables time-based refill (allowance only resets on
723 /// restart or via the serve-vindicated upgrade). Absent =>
724 /// [`crate::config::DEFAULT_SERVE_ECONOMICS_WARMING_REFILL`] (58).
725 pub warming_refill: Option<u64>,
726}
727
728/// Origin backend selection (#437). Tagged on the inner `kind` field.
729///
730/// `deny_unknown_fields` is set on the enum and on each variant's
731/// payload so that an operator typo (`decompres = "auto"` on the Http
732/// variant, for instance) fails at config load instead of silently
733/// no-opping.
734///
735/// ```toml
736/// [cache.origin]
737/// kind = "http"
738/// url = "https://origin.example/"
739/// decompress = "auto" # optional; defaults to "auto"
740///
741/// # — or —
742/// [cache.origin]
743/// kind = "fs"
744/// path = "/var/lib/decdn/origin"
745///
746/// # — or —
747/// [cache.origin]
748/// kind = "s3"
749/// bucket = "decdn-blobs"
750/// region = "us-east-1"
751/// # endpoint_url = "https://<accountid>.r2.cloudflarestorage.com" # for R2/B2/MinIO
752/// # path_style = true # for MinIO
753/// # prefix = "blobs/"
754/// # [cache.origin.credentials] source = "static" / "default-chain"
755/// ```
756///
757/// For multi-origin fallback (#284) use the plural `[[cache.origins]]`
758/// array-of-tables form (mutually exclusive with `[cache.origin]`):
759///
760/// ```toml
761/// [[cache.origins]]
762/// kind = "http"
763/// url = "https://primary.example/"
764///
765/// [[cache.origins]]
766/// kind = "s3"
767/// bucket = "mirror-blobs"
768/// region = "us-east-1"
769///
770/// [[cache.origins]]
771/// kind = "fs"
772/// path = "/var/lib/decdn/archive"
773/// ```
774#[derive(Debug, Serialize, Deserialize)]
775#[serde(tag = "kind", rename_all = "lowercase", deny_unknown_fields)]
776pub enum OriginConfig {
777 /// HTTP(S) origin served at `{url}/{blake3_hex}`.
778 Http {
779 /// Base URL; must be `http`/`https` and is path-normalized to
780 /// end with `/` so `{base}.join(&hex)` produces `{base}/{hex}`.
781 url: String,
782 /// How to handle `Content-Encoding` on the HTTP response
783 /// (#312). `"auto"` (default) decompresses gzip/zstd
784 /// transparently; `"strict"` refuses any non-identity
785 /// encoding. The BLAKE3 content-address is computed over the
786 /// canonical (decompressed) form, so `"strict"` is only safe
787 /// for origins guaranteed to serve already-canonical bytes.
788 decompress: Option<decdn_config_types::DecompressMode>,
789 },
790 /// Local filesystem origin rooted at `path`; blobs live at
791 /// `{path}/{hex[0..2]}/{hex}`.
792 Fs {
793 /// Filesystem root.
794 path: PathBuf,
795 },
796 /// S3-compatible object storage (#437): AWS S3, Cloudflare R2,
797 /// Backblaze B2, `MinIO`, etc. Object key layout is
798 /// `{prefix?}{hex[0..2]}/{hex}` — sharded the same way as the
799 /// filesystem backend so operators can copy blobs between
800 /// backends without rewriting tooling.
801 S3(S3OriginConfig),
802}
803
804/// S3-compatible origin configuration (#437) — wire form.
805///
806/// Validation runs at config-resolution time via the (private)
807/// `resolve_s3_origin` helper, which produces the runtime form
808/// [`super::resolved::ResolvedS3Config`]. Unvalidated instances of
809/// this type cannot reach the cache wiring layer because
810/// [`super::resolved::ResolvedOrigin::S3`] holds the resolved form,
811/// not this one.
812#[derive(Debug, Clone, Serialize, Deserialize)]
813#[serde(deny_unknown_fields)]
814pub struct S3OriginConfig {
815 /// Bucket name. Validated DNS-safe at config resolution
816 /// (lowercase `[a-z0-9.-]`, 3–63 chars; see
817 /// `validate_s3_bucket_name`).
818 pub bucket: String,
819 /// AWS region (e.g. `"us-east-1"`). Required because the S3 SDK
820 /// uses it for `SigV4` signing even when a custom `endpoint_url`
821 /// is set. Validated non-empty at config resolution.
822 pub region: String,
823 /// Custom endpoint URL for non-AWS S3-compatible providers (R2,
824 /// B2, `MinIO`). Omit for AWS S3. Validated as `http`/`https` at
825 /// config resolution.
826 pub endpoint_url: Option<String>,
827 /// Use path-style addressing (`https://endpoint/bucket/key`)
828 /// instead of virtual-hosted-style (`https://bucket.endpoint/key`).
829 /// Required `true` for `MinIO` and many self-hosted providers;
830 /// AWS S3 and Cloudflare R2 use virtual-hosted-style by default.
831 /// Absent => fall back to the SDK default (virtual-hosted-style).
832 pub path_style: Option<bool>,
833 /// Optional key prefix prepended to every fetched object. Final
834 /// key is `{prefix}{hex[0..2]}/{hex}`. Validation rejects
835 /// `prefix` starting with `/`, containing `..`, containing
836 /// backslash, or containing ASCII control / whitespace
837 /// characters; auto-appends a trailing `/` if the prefix is
838 /// non-empty and missing one (mirrors `parse_origin_url`'s
839 /// trailing-slash normalization).
840 pub prefix: Option<String>,
841 /// Credential source. Absent => use the AWS default credential
842 /// chain (env vars, `~/.aws/credentials`, IAM role / instance
843 /// profile).
844 pub credentials: Option<S3Credentials>,
845 /// How to handle `Content-Encoding` on the S3 response (#804).
846 /// `"auto"` (default) decompresses gzip/zstd transparently;
847 /// `"strict"` refuses any non-identity encoding. The BLAKE3
848 /// content-address is computed over the canonical (decompressed)
849 /// form, so `"strict"` is only safe for origins guaranteed to serve
850 /// already-canonical bytes. Mirrors the HTTP origin's `decompress`
851 /// knob.
852 pub decompress: Option<decdn_config_types::DecompressMode>,
853}
854
855/// S3 credential source (#437). Tagged on the inner `source` field.
856///
857/// Static credential fields use [`SecretString`] so an incidental
858/// `tracing::debug!(?cfg)` or panic backtrace cannot leak the
859/// material — the wrapper redacts itself in `Debug` output, and
860/// its `Serialize` impl emits a non-reversible hashed placeholder
861/// rather than the cleartext, so any incidental serialization of the
862/// config is redacted too (see [`SecretString`] for the full
863/// contract). The codebase already redacts HTTP-origin URL credentials
864/// (see `decdn_config_types::redact_for_log`); this is the same pattern
865/// for TOML-borne secrets.
866///
867/// ```toml
868/// [cache.origin.credentials]
869/// source = "static"
870/// access_key_id = "AKIA..."
871/// secret_access_key = "..."
872/// # session_token = "..." # optional
873///
874/// # — or —
875/// [cache.origin.credentials]
876/// source = "default-chain"
877/// # profile = "my-profile" # optional, override default profile
878/// ```
879#[derive(Debug, Clone, Serialize, Deserialize)]
880#[serde(tag = "source", rename_all = "kebab-case", deny_unknown_fields)]
881pub enum S3Credentials {
882 /// Static IAM credentials embedded in the config file. Common
883 /// for R2/B2/MinIO operator deployments where simple key-pair
884 /// audit is preferred; for AWS S3, prefer the default credential
885 /// chain so credentials live outside the TOML.
886 Static {
887 /// AWS access key ID. Stored in [`SecretString`] so it
888 /// redacts in `Debug` output alongside the secret key.
889 access_key_id: SecretString,
890 /// AWS secret access key. Redacted in `Debug` output.
891 secret_access_key: SecretString,
892 /// Optional STS session token (for assume-role / SSO flows
893 /// where a static key isn't enough on its own). Redacted.
894 session_token: Option<SecretString>,
895 },
896 /// Use the standard AWS credential chain: `AWS_ACCESS_KEY_ID` /
897 /// `AWS_SECRET_ACCESS_KEY` env vars, then `~/.aws/credentials`
898 /// profile, then IAM role / instance profile.
899 DefaultChain {
900 /// Override the default profile name when reading
901 /// `~/.aws/credentials`. Threaded through to the SDK as the
902 /// equivalent of the `AWS_PROFILE` env var.
903 profile: Option<String>,
904 },
905}
906
907/// Payment section of the config file.
908#[derive(Debug, Default, Serialize, Deserialize)]
909#[serde(deny_unknown_fields)]
910pub struct PaymentConfig {
911 /// Rate per MB in USDC base units.
912 pub rate_per_mb: Option<u64>,
913 /// Downstream credit-window ceiling in bytes (ADR 003 §Credit window). The
914 /// per-stream window ramps toward this cap as the stream pays, bounding the
915 /// node's credit exposure (unbilled egress already on the wire) to
916 /// `paid / credit_ramp_divisor`; the client's exposure stays zero (vouchers
917 /// are cumulative over delivered bytes). Absent =>
918 /// [`crate::config::DEFAULT_CREDIT_MAX`] (64 MiB). Floored at one chunk
919 /// ([`decdn_protocol::client::CHUNK_BYTES`], 1 MiB); a value at or below one
920 /// chunk reproduces the strict stop-and-wait cadence — deliver a chunk, then
921 /// recoup it. It is node-local config, not a governance-owned parameter — no
922 /// contract holds a delivery-cadence value.
923 pub credit_max: Option<decdn_config_types::Bytes>,
924 /// Ramp divisor for the credit window (ADR 003 §Credit window): the window is
925 /// `paid / credit_ramp_divisor`, floored at one chunk and capped at
926 /// [`Self::credit_max`]. Absent => [`crate::config::DEFAULT_CREDIT_RAMP_DIVISOR`]
927 /// (2). `0` opens the full ceiling immediately, reproducing the flat-window
928 /// behavior. It is node-local config, not a governance-owned parameter.
929 pub credit_ramp_divisor: Option<u64>,
930 /// Target wire-frame size for the serve path, in bytes (ADR 005
931 /// §`cdn/client/v1`). Absent => [`crate::config::DEFAULT_FRAME_TARGET_BYTES`]
932 /// (1 MiB). Must be > 0.
933 ///
934 /// Node-local policy that is never negotiated: no message carries it, the payer
935 /// accepts any non-empty frame, and both bao verification and the payment meter
936 /// are defined over byte counts rather than frame boundaries. Raising it lowers
937 /// per-frame CPU per byte served, up to one `CHUNK_BYTES` payment interval
938 /// (1 MiB) — a frame never crosses a payment-chunk boundary, so a larger value
939 /// is rejected rather than silently ignored. The serve loop also clamps each
940 /// frame to the credit window's remaining room, so a large value does not widen
941 /// credit exposure.
942 pub frame_target_bytes: Option<decdn_config_types::Bytes>,
943 /// Background flush period for the lane store, in ms. See
944 /// [`crate::config::DEFAULT_VOUCHER_COMMIT_INTERVAL_MS`] (5 s). Must be > 0.
945 pub voucher_commit_interval_ms: Option<u64>,
946}
947
948/// Security / rate-limiting section of the config file.
949///
950/// All fields are optional; defaults produce a safe configuration out of
951/// the box.
952///
953/// **All fields are hot-reloadable** on SIGHUP and via `admin_v1_reload`.
954/// The live `ConnectionLimiter` rebuilds its keyed `governor` rate
955/// limiter from the new quota and swaps it under an `RwLock`. The
956/// `Arc<Semaphore>` identity is preserved across cap resizes via
957/// `add_permits` / `acquire_many_owned(...).forget()` so every
958/// in-flight `OwnedSemaphorePermit` keeps draining into the same
959/// semaphore on `Drop`.
960///
961/// **Caveats:** during a shrink the live concurrency cap is `>= new`
962/// until enough handlers drain; under racing reloads (N→N+1→N) the
963/// post-task permit count may briefly land anywhere in `[N, N+1]`
964/// until the next reload reconciles. Accepted as a known trade-off:
965/// strict resize semantics would require a full handler-drain barrier.
966///
967/// Token-bucket state is *not* preserved across a reload — the keyed
968/// limiter is rebuilt from scratch. Operators tuning the per-source
969/// quota live should expect the next acquire after a reload to start
970/// with a fresh burst budget.
971///
972/// **`0` means "disable this layer":**
973/// - `max_concurrent_handlers = 0`: no global concurrency cap.
974/// - `per_source_rate_per_sec = 0`: per-source rate-limit disabled. The
975/// paired `per_source_burst` field is ignored — the limiter
976/// short-circuits before the keyed map is consulted.
977/// - `max_tracked_sources = 0`: rate-limit bookkeeping map is unbounded.
978/// **Warning:** an attacker churning source addresses can grow the
979/// map without bound in this mode — operator opt-in only.
980///
981/// `burst > 0` is required only when paired with a positive rate. Setting
982/// `rate > 0` together with `burst = 0` would deny every request after
983/// the first burst-many — the resolver rejects that combination as a
984/// likely-typo. Setting `rate = 0` together with any `burst` value is
985/// fine; burst is unused once the layer is disabled.
986#[derive(Debug, Default, Serialize, Deserialize)]
987#[serde(deny_unknown_fields)]
988pub struct SecurityConfig {
989 /// Maximum number of concurrently in-flight QUIC handler tasks across
990 /// all deCDN-authored ALPNs. New connections beyond this limit are
991 /// closed immediately with `APP_ERR_RATE_LIMITED`. Default: 256.
992 /// `0` disables the global cap (no concurrency limit).
993 pub max_concurrent_handlers: Option<u32>,
994 /// Refill rate for per-source rate limiting (cells per second).
995 /// More generous than older per-NodeID-only schemes because a single
996 /// source IP may host a legitimate fleet. Default: 100. `0.0`
997 /// disables the per-source layer (the paired `per_source_burst` is
998 /// then ignored).
999 ///
1000 /// The current implementation keys on the remote `IpAddr`. IPv6
1001 /// addresses are bucketed by their `/64` prefix, not the full
1002 /// 128-bit address. A customer-grade IPv6 allocation is typically
1003 /// `/64` or larger, so without this prefix grouping an attacker can
1004 /// rotate through `2^64` distinct source addresses inside one
1005 /// allocation and trivially defeat the layer. IPv4 addresses are
1006 /// used in full.
1007 ///
1008 /// "Per-source" rather than "per-IP" because the keying axis may
1009 /// extend in the future (e.g. `NodeID`) without renaming the field.
1010 pub per_source_rate_per_sec: Option<f64>,
1011 /// Burst capacity for per-source limiting. Default: 200 (2× the
1012 /// rate, providing headroom for jitter so well-behaved peers don't
1013 /// trip the limit on naturally-clumped requests). Required `> 0`
1014 /// when `per_source_rate_per_sec > 0`; ignored when the layer is
1015 /// disabled (`per_source_rate_per_sec = 0`).
1016 pub per_source_burst: Option<u32>,
1017 /// Hard cap on the number of distinct sources tracked in the
1018 /// rate-limit state. When the live keyed map exceeds this size the
1019 /// limiter prunes entries whose state has refilled to a fresh
1020 /// baseline. Default: 4096. `0` makes the map unbounded — see the
1021 /// type-level docs for the operator-opt-in warning.
1022 pub max_tracked_sources: Option<usize>,
1023}
1024
1025/// `[load_shed]` section — node-local load-shedding thresholds (overload
1026/// protection). All fields optional; unset falls back to the resolved
1027/// defaults.
1028#[derive(Debug, Default, Serialize, Deserialize)]
1029#[serde(deny_unknown_fields)]
1030pub struct LoadShedConfig {
1031 /// `resource-pressure` (default) or `always-admit`.
1032 pub policy: Option<String>,
1033 /// Serving egress budget in Mbps; `0` disables the egress ceiling.
1034 pub egress_budget_mbps: Option<u64>,
1035 /// Concurrency high-water mark (start shedding misses at/above).
1036 pub max_concurrent_serves_high: Option<u32>,
1037 /// Concurrency low-water mark (resume at/below).
1038 pub max_concurrent_serves_low: Option<u32>,
1039 /// Per-client concurrent-serve cap, enforced only under pressure; `0` off.
1040 pub per_client_serve_cap: Option<u32>,
1041}
1042
1043/// `[dht]` section — `cdn/dht/v1` settings (ADR 022).
1044///
1045/// The Kademlia routing-table parameters (k, α, bucket count, refresh
1046/// interval) are pinned by the protocol and not exposed here. The
1047/// rate-limit knobs nest under `[dht.rate_limit]` to match ADR 022 §DHT
1048/// Rate Limiting and to leave room for other future `dht.*` top-level knobs (e.g.
1049/// bootstrap peers, republish overrides) without breaking the operator
1050/// key path.
1051#[derive(Debug, Default, Serialize, Deserialize)]
1052#[serde(deny_unknown_fields)]
1053pub struct DhtConfig {
1054 /// Rate-limiter settings (ADR 022 §DHT Rate Limiting). Absent =>
1055 /// the defaults from the ADR (20/100/1000 req/s with 40/200/2000
1056 /// bursts).
1057 pub rate_limit: Option<DhtRateLimitConfig>,
1058}
1059
1060/// `[dht.rate_limit]` — three-layer token-bucket settings (ADR 022 §DHT
1061/// Rate Limiting).
1062///
1063/// Defaults match the ADR's conservative ceilings on adversarial load;
1064/// they are NOT steady-state operating targets — the ADR's bandwidth
1065/// analysis (§DHT Bandwidth Analysis "Headroom against rate limits")
1066/// shows realistic node traffic is two orders of magnitude below the
1067/// per-peer cap.
1068///
1069/// Setting any `*_rate_per_sec` to `0.0` disables that layer (operator
1070/// opt-out); the matching `*_burst` must also be `0` to avoid a deny-all
1071/// configuration. The resolver enforces this pairing the same way
1072/// `security.per_source_rate_per_sec` / `per_source_burst` are paired.
1073#[derive(Debug, Default, Serialize, Deserialize)]
1074#[serde(deny_unknown_fields)]
1075pub struct DhtRateLimitConfig {
1076 /// Per-peer (source `NodeId`) sustained rate. Absent => 20 req/s
1077 /// (ADR 022 §DHT Rate Limiting). `0.0` disables the layer.
1078 pub per_peer_rate_per_sec: Option<f64>,
1079 /// Per-peer burst capacity. Absent => 40. Required `> 0` when
1080 /// `per_peer_rate_per_sec > 0`.
1081 pub per_peer_burst: Option<u32>,
1082 /// Per-IP sustained rate. Absent => 100 req/s. `0.0` disables.
1083 pub per_ip_rate_per_sec: Option<f64>,
1084 /// Per-IP burst capacity. Absent => 200.
1085 pub per_ip_burst: Option<u32>,
1086 /// Global inbound DHT sustained rate. Absent => 1000 req/s.
1087 pub global_rate_per_sec: Option<f64>,
1088 /// Global inbound DHT burst capacity. Absent => 2000.
1089 pub global_burst: Option<u32>,
1090 /// Hard cap on the per-IP keyed-limiter map. Absent => 4096. `0`
1091 /// makes the map unbounded — operator opt-in (#645). Mirrors
1092 /// `security.max_tracked_sources` for the dispatch layer.
1093 pub max_tracked_per_ip: Option<usize>,
1094 /// Hard cap on the per-peer (`NodeId`) keyed-limiter map. Absent =>
1095 /// 4096. `0` makes the map unbounded (#645).
1096 pub max_tracked_per_peer: Option<usize>,
1097}
1098
1099/// `[probe]` section — `cdn/probe/v1` settings (ADR 005).
1100///
1101/// The rate-limit knobs nest under `[probe.rate_limit]` to match ADR 005
1102/// §Probe rate limiting and to leave room for other future `probe.*` top-level knobs without
1103/// breaking the operator key path.
1104#[derive(Debug, Default, Serialize, Deserialize)]
1105#[serde(deny_unknown_fields)]
1106pub struct ProbeConfig {
1107 /// Rate-limiter settings (ADR 005 §Probe rate limiting). Absent =>
1108 /// the defaults from the ADR (5/50/1000 req/s with 5/200/2000 bursts).
1109 pub rate_limit: Option<ProbeRateLimitConfig>,
1110}
1111
1112/// `[probe.rate_limit]` — three-layer token-bucket settings (ADR 005 §Probe
1113/// rate limiting).
1114///
1115/// Mirrors `[dht.rate_limit]` in shape; only the defaults differ (ADR 005
1116/// specifies a tighter per-peer cap than ADR 022's DHT layer because a probe
1117/// is unauthenticated and cheaper to flood). Setting any `*_rate_per_sec` to
1118/// `0.0` disables that layer (operator opt-out); the matching `*_burst` must
1119/// also be `0` to avoid a deny-all configuration — the resolver enforces this
1120/// pairing, the same way `security.per_source_*` and `dht.rate_limit.*` are
1121/// paired.
1122#[derive(Debug, Default, Serialize, Deserialize)]
1123#[serde(deny_unknown_fields)]
1124pub struct ProbeRateLimitConfig {
1125 /// Per-peer (source `NodeId`) sustained rate. Absent => 5 req/s
1126 /// (ADR 005 §Probe rate limiting). `0.0` disables the layer.
1127 pub per_peer_rate_per_sec: Option<f64>,
1128 /// Per-peer burst capacity. Absent => 5. Required `> 0` when
1129 /// `per_peer_rate_per_sec > 0`.
1130 pub per_peer_burst: Option<u32>,
1131 /// Per-IP sustained rate. Absent => 50 req/s. `0.0` disables.
1132 pub per_ip_rate_per_sec: Option<f64>,
1133 /// Per-IP burst capacity. Absent => 200.
1134 pub per_ip_burst: Option<u32>,
1135 /// Global inbound probe sustained rate. Absent => 1000 req/s.
1136 pub global_rate_per_sec: Option<f64>,
1137 /// Global inbound probe burst capacity. Absent => 2000.
1138 pub global_burst: Option<u32>,
1139 /// Hard cap on the per-IP keyed-limiter map. Absent => 4096. `0`
1140 /// makes the map unbounded — operator opt-in (#645).
1141 pub max_tracked_per_ip: Option<usize>,
1142 /// Hard cap on the per-peer (`NodeId`) keyed-limiter map. Absent =>
1143 /// 4096. `0` makes the map unbounded (#645).
1144 pub max_tracked_per_peer: Option<usize>,
1145}
1146
1147/// Observability section of the config file.
1148#[derive(Debug, Default, Serialize, Deserialize)]
1149#[serde(deny_unknown_fields)]
1150pub struct ObservabilityConfig {
1151 /// Log verbosity level.
1152 pub log_level: Option<LogLevel>,
1153 /// Log output format.
1154 pub log_format: Option<LogFormat>,
1155 /// Prometheus metrics HTTP port.
1156 pub metrics_port: Option<u16>,
1157 /// IP address to bind the metrics HTTP server on. Defaults to
1158 /// `127.0.0.1`. Set to `0.0.0.0` for containerised deployments.
1159 pub metrics_bind: Option<std::net::IpAddr>,
1160 /// Loopback admin HTTP port (`appendix-local-admin-http.md`). Explicit `0` disables the
1161 /// admin server; if the key is absent, resolution defaults to
1162 /// `9191`. Any positive value binds on `127.0.0.1:<port>`.
1163 pub admin_port: Option<u16>,
1164 /// OTLP collector endpoint URL.
1165 pub otlp_endpoint: Option<String>,
1166}
1167
1168/// Download-receipt audit-log retention section of the config file (#802).
1169///
1170/// Bounds the otherwise-unbounded `download_receipts.jsonl` (one line per
1171/// accepted payment proof, ~1 per MiB delivered) with size-based rotation:
1172/// once the live file reaches `max_file_bytes` it is rotated to a numbered
1173/// backup (`download_receipts.jsonl.1`, `.2`, …) and a fresh file is opened;
1174/// the oldest backup beyond `retained_files` is deleted. Both fields are
1175/// optional; the defaults bound disk to roughly `(retained_files + 1) *
1176/// max_file_bytes` while keeping a useful audit window.
1177#[derive(Debug, Default, Serialize, Deserialize)]
1178#[serde(deny_unknown_fields)]
1179pub struct ReceiptsConfig {
1180 /// Rotate the live receipt log once it reaches this many bytes. Absent =>
1181 /// default 128 MiB. Validated against a floor (1 MiB) and ceiling (1 GiB)
1182 /// so a typo cannot rotate every line or defeat rotation entirely.
1183 pub max_file_bytes: Option<u64>,
1184 /// Number of rotated backup files to retain (`.1`..=`.N`). Absent =>
1185 /// default 4. `0` keeps no backups (the live file is truncated in place on
1186 /// rotation). Capped at 100.
1187 pub retained_files: Option<u32>,
1188}
1189
1190/// Local content-denylist section of the config file (ADR 011 §Local Denylist).
1191///
1192/// The operator's own removal lever, independent of governance: entries take
1193/// effect on the next reload and bind only this node. ADR 011 §One-hour
1194/// removal orders makes this the only mechanism sized to a sub-day statutory
1195/// deadline (the EU TCO one-hour clock), because it is the only one entirely
1196/// within the order recipient's control.
1197#[derive(Debug, Default, Serialize, Deserialize)]
1198#[serde(deny_unknown_fields)]
1199pub struct ContentConfig {
1200 /// Blob hashes this node refuses to serve. Bare 64-character lowercase hex,
1201 /// the same spelling as `cache.pinned_hashes`. An invalid entry fails
1202 /// resolution rather than being skipped, so a typo in a takedown cannot
1203 /// silently leave content served. Duplicates are de-duplicated, not
1204 /// rejected — a repeated deny is still a deny.
1205 pub denied_hashes: Option<Vec<String>>,
1206 /// Operator addresses whose payment pools this node refuses to serve.
1207 /// `0x`-prefixed hex, checksum-agnostic (any case accepted); the zero
1208 /// address is rejected.
1209 pub denied_origins: Option<Vec<String>>,
1210}
1211
1212#[cfg(test)]
1213#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] // tests
1214mod tests;