Skip to main content

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