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