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