Skip to main content

decdn_common/config/
resolved.rs

1//! Fully resolved configuration with concrete types.
2//!
3//! Most fields are non-optional. `region` remains `Option` because it has no
4//! universal default; `relay_urls` is a (possibly empty) list — empty means
5//! "fall back to the n0 default relays".
6
7use std::collections::HashSet;
8use std::path::PathBuf;
9
10use alloy::primitives::Address;
11use decdn_config_types::DeniedHashes;
12
13use crate::cli::common::{LogFormat, LogLevel};
14
15/// Resolved identity fields.
16#[derive(Debug)]
17pub struct ResolvedIdentity {
18    /// Node data directory.
19    pub data_dir: PathBuf,
20    /// Region code (e.g. `"US"`).
21    pub region: Option<String>,
22}
23
24/// Resolved network fields.
25#[derive(Debug)]
26pub struct ResolvedNetwork {
27    /// QUIC bind port.
28    pub bind_port: u16,
29    /// iroh relay URLs for NAT traversal. Empty => use the n0 default relays;
30    /// non-empty => swap in these self-hosted relays (`RelayMode::Custom`).
31    /// Relay connectivity is logged after bring-up but never fatal: when no
32    /// relay connects the node warns and proceeds (iroh retries in the
33    /// background).
34    pub relay_urls: Vec<String>,
35    /// Operator-configurable address discovery (#818 scope 1). Empty
36    /// ([`ResolvedDiscovery::is_empty`]) => the node keeps the n0-hosted
37    /// pkarr/DNS default (`presets::N0`); otherwise the node builds on
38    /// `presets::Minimal` and composes only the providers configured here.
39    pub discovery: ResolvedDiscovery,
40}
41
42/// Resolved discovery providers (#818). Strings are already shape-validated at
43/// resolution (`pkarr_url` a parseable URL, `dns_origin` non-empty, each peer
44/// `node_id` a valid iroh `NodeId`, each `addr` a `SocketAddr`); the node wiring
45/// layer parses them into iroh types — the discovery-provider seam, per
46/// `adr/appendix-poc-production-seams.md`.
47#[derive(Debug, Default, Clone)]
48pub struct ResolvedDiscovery {
49    /// pkarr relay URL to publish this node's address record to.
50    pub pkarr_url: Option<String>,
51    /// DNS origin domain to resolve peers from.
52    pub dns_origin: Option<String>,
53    /// Static peer address book, sorted by `node_id` for a deterministic build.
54    pub peers: Vec<ResolvedDiscoveryPeer>,
55}
56
57impl ResolvedDiscovery {
58    /// No discovery overrides configured => the node uses `presets::N0`.
59    pub const fn is_empty(&self) -> bool {
60        self.pkarr_url.is_none() && self.dns_origin.is_none() && self.peers.is_empty()
61    }
62}
63
64/// One resolved static peer ([`ResolvedDiscovery::peers`]).
65#[derive(Debug, Clone)]
66pub struct ResolvedDiscoveryPeer {
67    /// Peer `NodeId` (the canonical 64-char lowercase-hex form; validated at
68    /// resolution).
69    pub node_id: String,
70    /// Peer home relay URL, if configured.
71    pub relay_url: Option<String>,
72    /// Peer direct socket addresses (`host:port`).
73    pub addrs: Vec<String>,
74}
75
76/// Resolved blockchain fields.
77#[derive(Debug)]
78pub struct ResolvedBlockchain {
79    /// JSON-RPC endpoint URL.
80    pub rpc_url: String,
81    /// Ethereum keystore file path.
82    pub eth_keystore: PathBuf,
83    /// Optional path to a file holding the keystore password. CLI/env-only;
84    /// not surfaced via the TOML schema (passwords don't belong in config
85    /// files even by reference). The runtime first consults the
86    /// `DECDN_KEYSTORE_PASSWORD` env var, then this file, then prompts on
87    /// stdin if connected to a TTY. Each source is chosen by presence, so a
88    /// set-but-empty env var and an empty file both mean an empty password.
89    pub keystore_password_file: Option<PathBuf>,
90    /// `PaymentPool` contract address — the shared payment pool this node
91    /// registers against for buyer and seller flows alike.
92    pub payment_pool_address: String,
93    /// `CapacityBond` contract address.
94    pub capacity_bond_address: String,
95    /// `OriginAssignment` contract address. `Some` only when the operator opts
96    /// into the chain-backed origin directory; `None` => empty directory, so
97    /// the `FIND_VALUE` routing fallback (ADR 022) resolves nothing.
98    pub origin_assignment_address: Option<String>,
99    /// Positive-hit TTL for the lazy origin directory cache, in seconds: how
100    /// long a resolved, non-empty `getOrigins(namespaceId)` set is served
101    /// before a re-read. Defaults to
102    /// [`super::DEFAULT_ORIGIN_DIRECTORY_POSITIVE_TTL_SEC`].
103    pub origin_directory_positive_ttl_sec: u64,
104    /// Negative-hit TTL for the lazy origin directory cache, in seconds: how
105    /// long "this namespace has no origins / does not exist" is cached.
106    /// Defaults to [`super::DEFAULT_ORIGIN_DIRECTORY_NEGATIVE_TTL_SEC`].
107    pub origin_directory_negative_ttl_sec: u64,
108    /// Max distinct namespaces held in the lazy origin directory cache (LRU
109    /// eviction). Defaults to
110    /// [`super::DEFAULT_ORIGIN_DIRECTORY_CACHE_CAPACITY`].
111    pub origin_directory_cache_capacity: usize,
112    /// `PublisherRegistry` contract address. Independent of the origin directory
113    /// (the publish CLI's `namespace create` target); not consumed by the node.
114    pub publisher_registry_address: Option<String>,
115    /// `SlashJudge` contract address — the EIP-712 `verifyingContract` for
116    /// `slash_sig` signatures (ADR 014). Required (no default).
117    pub slash_judge_address: String,
118    /// Required `ContentBlacklist` contract address (ADR 011/019/031). Successful
119    /// node config resolution always produces `Some`; the runtime rejects `None`.
120    pub content_blacklist_address: Option<String>,
121    /// Seconds between the blacklist watcher's periodic replay + re-scope pass.
122    /// Defaults to [`super::DEFAULT_CONTENT_BLACKLIST_POLL_INTERVAL_SEC`]. Only
123    /// used when `content_blacklist_address` is set.
124    pub content_blacklist_poll_interval_sec: u64,
125    /// Seconds the node may go without a successful chain read before the serve
126    /// and probe paths refuse (ADR 011 § Serving while chain-stale). Defaults to
127    /// [`super::DEFAULT_CHAIN_STALENESS_GRACE_SEC`]. Always evaluated; a large
128    /// value opts out. Only used when `content_blacklist_address` is set.
129    pub chain_staleness_grace_sec: u64,
130    /// EIP-712 `chainId` for the `slash_sig` domain separator. Defaults to
131    /// [`super::DEFAULT_CHAIN_ID`] (Arbitrum Sepolia) when unset.
132    pub chain_id: u64,
133    /// Seconds between RPC connectivity watchdog probes. `0` disables the
134    /// watchdog entirely; otherwise the resolver enforces a minimum (see
135    /// `MIN_RPC_WATCHDOG_INTERVAL_SEC`).
136    pub rpc_watchdog_interval_sec: u64,
137    /// Milliseconds between chain-event poll ticks (#1011, #1106). Two unrelated
138    /// consumers read this one value:
139    /// - the `MultiplexedPoller`'s `poll_interval` — the `eth_getLogs` tick
140    ///   cadence shared by every chain watcher's `Route`.
141    /// - alloy's pending-transaction receipt heartbeat, applied to every provider
142    ///   via the node's `with_poll_interval` (`client().set_poll_interval`) and
143    ///   consumed by `PendingTransactionBuilder::get_receipt` when the node awaits
144    ///   a mined settlement / pool tx. This is also what overrides alloy's
145    ///   localhost-detected 250 ms default.
146    ///
147    /// Defaults to 7000 ms (`DEFAULT_EVENT_POLL_INTERVAL_MS`); the resolver
148    /// enforces a minimum (see `MIN_EVENT_POLL_INTERVAL_MS`).
149    pub event_poll_interval_ms: u64,
150    /// Ceiling on the block span of one chain-watcher `eth_getLogs` request
151    /// (the `MultiplexedPoller`'s window). The poller starts here. When the
152    /// provider rejects a range, it sets its span to half the rejected window's
153    /// length, and it doubles back toward this ceiling after a streak of
154    /// accepted windows. Defaults to [`super::DEFAULT_GET_LOGS_MAX_BLOCK_SPAN`];
155    /// always `>= 1`.
156    pub get_logs_max_block_span: u64,
157    /// Seconds between authoritative `FeeRouter.getShares()` re-reads by the
158    /// fee-shares watcher (ADR 041 / ADR 016 § Tunable Economics). Safety-net
159    /// cadence alongside the `SharesUpdated` event subscription. Defaults to
160    /// 3600s (`DEFAULT_FEE_SHARES_POLL_INTERVAL_SEC`); the resolver rejects
161    /// `0`.
162    pub fee_shares_poll_interval_sec: u64,
163    /// Per-chunk redemption floor (base units, `µUSDC`). The seller
164    /// settlement path submits an on-chain redemption transaction for a
165    /// chunk of lanes only once the aggregate un-redeemed value across that
166    /// chunk's lanes reaches this floor; every lane in a submitted chunk
167    /// settles. Defaults to 1 USDC (`1_000_000` `µUSDC`) when unset.
168    pub redeem_threshold_micro_usdc: u64,
169    /// Maximum vouchers per `redeemMany` transaction (the redemption chunk size).
170    /// Defaults to `300` (`DEFAULT_REDEEM_MAX_VOUCHERS_PER_TX`); the resolver
171    /// rejects `0`.
172    pub redeem_max_vouchers_per_tx: u64,
173    /// Seconds between the redeemer self-tick sweeps (#327, #751): the
174    /// low-frequency backstop that sweeps every lane and redeems the chunks
175    /// that clear the redemption floor, independent of the advisory per-voucher
176    /// hints. Defaults to 300s
177    /// (`DEFAULT_REDEEM_INTERVAL_SECS`); the resolver rejects `0`.
178    pub redeem_interval_secs: u64,
179    /// Deposit (base units, `µUSDC`) the buyer path escrows when it opens a
180    /// pool, and the target every top-up restores the pool balance toward.
181    /// Defaults to 10 USDC (`10_000_000`). Guaranteed nonzero (the resolver
182    /// rejects `0`; the pool's `openPool` reverts on a zero deposit).
183    pub buyer_working_deposit_micro_usdc: u64,
184    /// Whether the buyer path issues a one-time max USDC approval for the
185    /// `PaymentPool` contract at startup (#744, ADR 003 § Deposit
186    /// Economics). Defaults to `true`; set `false` to manage the allowance
187    /// out-of-band (e.g. a tighter per-pool approval policy).
188    pub buyer_max_approve: bool,
189    /// The node's refundable floor `M` (base units, `µUSDC`), sized per ADR
190    /// 003 § Sizing (`M = k·ρ·B·Δ`). The node stops serving a pool once its
191    /// remaining on-chain balance minus `M` can no longer cover the next
192    /// credit window. Defaults to 1 USDC (`1_000_000` `µUSDC`).
193    pub pool_min_remaining_deposit_micro_usdc: u64,
194    /// Per-signer LIVE concurrency cap `k`, in ramp-start credit windows (ADR 003
195    /// § Pool solvency, per-signer floor isolation): the most live un-vouchered floor
196    /// reservation any one capability signer may hold against a pool, `k · one credit
197    /// window`. Lower-clamped to one window at use. Defaults to `8` — see
198    /// `decdn_common::config::DEFAULT_POOL_FLOOR_SIGNER_LIVE_WINDOWS`.
199    pub pool_floor_signer_live_windows: u64,
200}
201
202/// Resolved cache fields.
203#[derive(Debug)]
204pub struct ResolvedCache {
205    /// Blob cache directory.
206    pub cache_dir: PathBuf,
207    /// Upper bound on the cache ceiling in megabytes. The eviction driver clamps
208    /// it down each tick so the cache never grows into the last
209    /// [`Self::disk_headroom_mb`] of free space on the `cache_dir` volume
210    /// (#1930).
211    pub cache_size_mb: u64,
212    /// Free disk in megabytes the eviction driver keeps unused on the
213    /// `cache_dir` volume, defended against any process (#1930). Default
214    /// [`crate::config::DEFAULT_DISK_HEADROOM_MB`]; `0` opts out of the disk
215    /// clamp so only [`Self::cache_size_mb`] binds.
216    pub disk_headroom_mb: u64,
217    /// Cap on the bytes of one blob the node holds, in megabytes. A blob from
218    /// the node's own origin larger than it is refused; a pull-through serves a
219    /// range at any offset of a larger blob while the bytes held for it fit.
220    pub max_blob_size_mb: u64,
221    /// Buyer-side ABSOLUTE per-MB rate ceiling for paid pulls (#1375), in the same
222    /// per-MB units as the wire `StreamResponse.rate_per_mb`; `0` = unlimited
223    /// (the default). A buyer-side ceiling: it bounds what this node, acting as a
224    /// BUYER on a cache-miss pull, will accept a provider to quote. The node also
225    /// always applies a probe-relative bound (a quote may not exceed the rate the
226    /// chosen candidate advertised at probe), so this is the additional absolute
227    /// backstop. The node never bounds its own sell rate.
228    pub max_rate_per_mb: u64,
229    /// Resolved ordered list of origin backends (#437, #284). Empty
230    /// vec => no pull-through; cache misses return `NoOrigin`. A
231    /// single-element vec is the single-origin case and is also the
232    /// form produced from a `[cache.origin]` (singular) TOML table —
233    /// both wire forms (singular and plural `[[cache.origins]]`)
234    /// collapse here so downstream wiring sees
235    /// one canonical representation. Per-variant validation (URL
236    /// parse, S3 bucket/region/prefix shape) has already run at
237    /// resolution time — the wiring layer can construct the concrete
238    /// `Origin` impl without re-validating. Order is significant: the
239    /// engine tries entries in this order on a cache miss and falls
240    /// back to the next on `NotFound`, permanent error, or transient
241    /// retry exhaustion.
242    pub origins: Vec<ResolvedOrigin>,
243    /// Operator-pinned blob hashes (#276). Hashes here are excluded from
244    /// LRU eviction candidates by the cache engine. Resolved
245    /// from the hex-encoded TOML form at load time, so any wrong-length
246    /// or non-hex entry fails config loading rather than turning into a
247    /// silent "this hash will be ignored" surprise.
248    ///
249    /// Held as [`decdn_config_types::PinnedHashes`] (a typed wrapper around
250    /// `Arc<HashSet<Hash>>`) so the engine, runtime, and resolver
251    /// share one nominal type — a future "blocklist" or similar
252    /// `HashSet<Hash>`-shaped feature can't be silently swapped into
253    /// the pinning slot.
254    pub pinned_hashes: decdn_config_types::PinnedHashes,
255    /// Origin pull-through retry policy (#285). Set once at startup;
256    /// changes require a process restart.
257    pub origin_retry: decdn_config_types::RetryPolicy,
258    /// Per-origin circuit-breaker policy (#963). Fronts each origin's
259    /// pull-through retry loop so a sustained outage fast-fails the
260    /// origin's misses (no retry/backoff incurred) and the cache sheds
261    /// load, then probes for recovery. Set once at startup; changes
262    /// require a process restart.
263    pub circuit_breaker: decdn_config_types::CircuitBreakerPolicy,
264    /// `User-Agent` header sent on every HTTP origin pull-through (#435).
265    /// Defaults to [`decdn_config_types::DEFAULT_USER_AGENT`] (which embeds
266    /// the `decdn-config-types` crate's `CARGO_PKG_VERSION`; the
267    /// `decdn-node/` prefix — not the version — is the stable contract,
268    /// #578); operators can override
269    /// via `cache.user_agent` to attribute CDN traffic in origin access
270    /// logs or to drive origin-side rate limits and routing policy.
271    pub user_agent: String,
272    /// Interval between iroh-blobs GC sweeps in seconds (#518). `0`
273    /// disables the periodic sweep; any positive value is forwarded to
274    /// iroh-blobs' built-in GC loop. Default
275    /// [`crate::config::DEFAULT_GC_INTERVAL_SEC`] when the TOML section
276    /// omits the field.
277    pub gc_interval_sec: u64,
278    /// Interval between origin-held-index rescans in seconds (#1130). `0`
279    /// disables the periodic rescan (startup + `decdn node reload` still run
280    /// one). Default [`crate::config::DEFAULT_FS_RESCAN_INTERVAL_SEC`] when the
281    /// TOML section omits the field.
282    pub fs_rescan_interval_sec: u64,
283    /// TTL in seconds for a memoised positive live-origin probe answer (#1130
284    /// pt3). Default [`crate::config::DEFAULT_ORIGIN_PROBE_TTL_SEC`]. Backs the
285    /// per-probe `HEAD`/`HeadObject` fallback that discovers non-pinned http/s3
286    /// objects the enumeration index cannot see.
287    pub origin_probe_ttl_sec: u64,
288    /// Default [`crate::config::DEFAULT_ORIGIN_PROBE_NEGATIVE_TTL_SEC`]. The
289    /// negative-answer TTL for the origin-probe memo — the bound on how long a
290    /// stale `Absent` can hide newly-available own content.
291    pub origin_probe_negative_ttl_sec: u64,
292    /// Memo TTL in seconds for a `Fault` answer (#1130 pt3). Default
293    /// [`crate::config::DEFAULT_ORIGIN_PROBE_FAULT_TTL_SEC`]. Bounds how long a
294    /// hash that faulted is not re-probed, and is capped by
295    /// `origin_probe_ttl_sec` so a recovered origin is noticed soon.
296    pub origin_probe_fault_ttl_sec: u64,
297    /// Per-probe live-`HEAD` ceiling in milliseconds (#1130 pt3). Default
298    /// [`crate::config::DEFAULT_ORIGIN_PROBE_TIMEOUT_MS`]. Keeps a slow origin
299    /// off the probe hot path.
300    pub origin_probe_timeout_ms: u64,
301    /// Maximum distinct hashes in the live-origin probe memo (#1130 pt3).
302    /// Default [`crate::config::DEFAULT_ORIGIN_PROBE_MEMO_CAPACITY`]. Bounds
303    /// memo memory under a random-hash probe flood.
304    pub origin_probe_memo_capacity: u64,
305    /// LRU eviction driver high-water percent of [`Self::cache_size_mb`]
306    /// (#1173). Above this fraction the driver actively evicts. Validated to
307    /// `[60, 95]`. Default [`crate::config::DEFAULT_EVICTION_HIGH_WATER_PCT`].
308    pub eviction_high_water_pct: u64,
309    /// LRU eviction driver target percent of [`Self::cache_size_mb`] (#1173).
310    /// The driver evicts down to this fraction before idling. Validated to
311    /// `[40, 90]` and `<= eviction_high_water_pct - 5` (structural hysteresis
312    /// gap). Default [`crate::config::DEFAULT_EVICTION_TARGET_PCT`].
313    pub eviction_target_pct: u64,
314    /// LRU eviction driver max candidates removed per tick (#1173). Validated
315    /// to `[1, 256]`. Default
316    /// [`crate::config::DEFAULT_EVICTION_PER_SWEEP_BUDGET`].
317    pub eviction_per_sweep_budget: u64,
318    /// LRU eviction driver wakeup cadence in seconds (#1173). Validated to
319    /// `[1, 60]`. Default [`crate::config::DEFAULT_EVICTION_TICK_SECS`].
320    pub eviction_tick_secs: u64,
321    /// Maximum number of concurrently held (eviction-exempt) blobs for the
322    /// probe-triggered hold (ADR 005 §Hold budget, #318). Default
323    /// [`crate::config::DEFAULT_MAX_PROBE_HOLDS`]; `0` disables
324    /// `has_blob: true`.
325    pub max_probe_holds: usize,
326    /// Probe-hold slots reserved for the stake lane (registered node-to-node
327    /// requesters) under hold-budget pressure (#757, ADR 003 §Admission and
328    /// Priority). Default `0` disables the reservation (single-lane node).
329    /// The runtime only builds a `StakeLanePolicy` when this is `> 0`, so the
330    /// probe handler's hot path is unchanged for the default.
331    pub stake_lane_reserved_holds: usize,
332    /// Enable node-to-node paid cache-miss pull-through (#831). Default
333    /// `false`. When `true` and the buyer pool service bootstrapped, the
334    /// runtime provisions the `NodeOrigin` and the client handler triggers a
335    /// pull on a miss (behind a pull-authorized client lane). `cache.*` is
336    /// restart-required, so this is read once at bring-up.
337    pub node_to_node_pull_through_enabled: bool,
338    /// When `false`, the node serves and seeds only content its own backend
339    /// holds; a foreign hash is declined like a miss. The default is
340    /// role-derived: `false` (origin-only) when an origin backend is
341    /// configured — an origin is not a general proxy — and `true` (relay)
342    /// when no origin is configured, since a pure edge node's only function
343    /// is relaying. Explicit config overrides either default. Node-local;
344    /// `cache.*` is restart-required.
345    pub relay_foreign_namespaces: bool,
346    /// Providers probed before ranking on a node-to-node pull (#831). Default
347    /// [`crate::config::DEFAULT_NODE_PULL_PROBE_FANOUT`].
348    pub node_pull_probe_fanout: usize,
349    /// Per-pull wall-clock timeout in seconds for a node-to-node miss fill
350    /// (#831). Default [`crate::config::DEFAULT_NODE_PULL_TIMEOUT_SEC`].
351    ///
352    /// Bounds the OPEN stage only; the streaming stage is bounded by
353    /// [`Self::node_pull_stall_window_sec`] and [`Self::node_pull_min_throughput_bps`]
354    /// (#1797).
355    pub node_pull_timeout_sec: u64,
356    /// Throughput-floor window (seconds) on the streaming stage of an upstream pull; bytes
357    /// are counted off the QUIC stream sub-frame, so it trips only when throughput falls
358    /// below the floor, never on a large blob (#1797). Default
359    /// [`crate::config::DEFAULT_NODE_PULL_STALL_WINDOW_SEC`]. Also the head start of each
360    /// own-origin range-pull read's time budget (ADR 037).
361    pub node_pull_stall_window_sec: u64,
362    /// Minimum sustained upstream throughput (bytes/sec) over `node_pull_stall_window_sec`;
363    /// `0` = idle detection only (#1797). Default
364    /// [`crate::config::DEFAULT_NODE_PULL_MIN_THROUGHPUT_BPS`]. Also the average throughput
365    /// each own-origin range-pull read must sustain after the head start; `0` leaves those
366    /// reads unbounded (ADR 037).
367    pub node_pull_min_throughput_bps: u64,
368    /// Cache eviction policy selector (ADR 040). `"lru"` or `"tinylfu"`.
369    /// Default [`crate::config::DEFAULT_EVICTION_POLICY`].
370    pub eviction_policy: String,
371    /// Cache admission policy selector (ADR 040). `"always"` or `"tinylfu"`.
372    /// Default [`crate::config::DEFAULT_ADMISSION_POLICY`].
373    pub admission_policy: String,
374    /// W-TinyLFU tuning knobs (ADR 040), resolved regardless of whether either
375    /// policy selector above is `"tinylfu"` — cheap to resolve and keeps this
376    /// struct free of `Option`.
377    pub tinylfu: ResolvedTinyLfu,
378    /// Refuse-to-serve economics tuning (ADR 041): the margin policy, its
379    /// discount, and per-source speculative-warming allowance.
380    pub serve_economics: ResolvedServeEconomics,
381}
382
383/// Resolved W-TinyLFU tuning knobs. See [`ResolvedCache::tinylfu`].
384#[derive(Debug, Clone, Copy, PartialEq, Eq)]
385pub struct ResolvedTinyLfu {
386    /// Count-min sketch size in bytes. Default
387    /// [`crate::config::DEFAULT_TINYLFU_SKETCH_BYTES`].
388    pub sketch_bytes: usize,
389    /// Prior sightings before a probation member promotes to main. Default
390    /// [`crate::config::DEFAULT_TINYLFU_PROMOTION_THRESHOLD`].
391    pub promotion_threshold: u32,
392    /// Percent of `cache.cache_size_mb` the probation segment is capped to.
393    /// Validated to `[1, 50]`. Default
394    /// [`crate::config::DEFAULT_TINYLFU_PROBATION_TARGET_PCT`].
395    pub probation_target_pct: u64,
396    /// Reserved: half-life in seconds for aging the frequency sketch. Resolved
397    /// and stored but not currently consulted by the shipped sketch (fixed
398    /// sample-count reset, not wall-clock aging). Default
399    /// [`crate::config::DEFAULT_TINYLFU_AGING_HALFLIFE_SEC`].
400    pub aging_halflife_sec: u64,
401}
402
403/// Resolved refuse-to-serve economics tuning (ADR 041). See
404/// [`ResolvedCache::serve_economics`].
405#[derive(Debug, Clone, PartialEq, Eq)]
406pub struct ResolvedServeEconomics {
407    /// Refuse-to-serve policy. `"off"` or `"margin"`. Default
408    /// [`crate::config::DEFAULT_SERVE_ECONOMICS_POLICY`].
409    pub policy: String,
410    /// Discount applied to the sell price when deciding whether a
411    /// speculative pull clears its margin gate, in basis points. Validated
412    /// to `[1, 10_000]` (`5000` == `0.5`). Default
413    /// [`crate::config::DEFAULT_SERVE_ECONOMICS_DISCOUNT_BPS`].
414    pub discount_bps: u32,
415    /// Maximum number of concurrent speculative-warming sources. `>= 1`.
416    /// Default [`crate::config::DEFAULT_SERVE_ECONOMICS_N_MAX`].
417    pub n_max: u32,
418    /// Per-source speculative-warming allowance, in payment base units.
419    /// `> 0`. Default
420    /// [`crate::config::DEFAULT_SERVE_ECONOMICS_WARMING_BUDGET`].
421    pub warming_budget: u64,
422    /// Per-source allowance refill rate, in payment base units per second.
423    /// `0` disables time-based refill. Default
424    /// [`crate::config::DEFAULT_SERVE_ECONOMICS_WARMING_REFILL`].
425    pub warming_refill: u64,
426}
427
428/// Resolved + validated origin backend selection (#437). Mirrors
429/// [`crate::config::types::OriginConfig`] but carries pre-parsed types
430/// for the variants that need them (HTTP base URL, S3 endpoint URL).
431///
432/// The resolution layer (`crate::config::resolve_origin`) is the
433/// **intended** construction path — it's the only producer the
434/// runtime relies on, and the only path that runs the validators.
435/// The fields on each variant are `pub` for consistency with the
436/// other `Resolved*` types in this crate (see `ResolvedCache`,
437/// `ResolvedBlockchain`, etc.); Rust visibility doesn't *enforce*
438/// the validator-only contract, but the runtime never bypasses it.
439#[derive(Debug, Clone)]
440pub enum ResolvedOrigin {
441    /// HTTP(S) origin. The base URL has already been parsed by
442    /// [`decdn_config_types::parse_origin_url`] at resolution time, so
443    /// invariants (http/https scheme, trailing-slash path, no
444    /// query/fragment) are type-enforced — they cannot be reconstructed
445    /// outside the parser.
446    Http {
447        /// Validated base URL.
448        url: decdn_config_types::OriginUrl,
449        /// How to handle `Content-Encoding` on the HTTP origin response
450        /// (#312). Defaults to [`decdn_config_types::DecompressMode::Auto`].
451        decompress: decdn_config_types::DecompressMode,
452    },
453    /// Local filesystem origin root. Directory-existence is validated
454    /// when the runtime constructs the
455    /// cache engine's filesystem origin — config resolution carries
456    /// the raw path so resolution stays filesystem-free and testable
457    /// without real I/O.
458    Fs {
459        /// Filesystem root.
460        path: PathBuf,
461    },
462    /// S3-compatible origin. Field-shape validation (DNS-safe bucket
463    /// name, region non-empty, endpoint URL scheme, prefix shape) has
464    /// already run at resolution time and is encoded in the
465    /// [`ResolvedS3Config`] type.
466    S3(ResolvedS3Config),
467}
468
469/// Validated runtime form of an S3 origin (#437). The intended
470/// construction path is `crate::config::resolve_s3_origin`, which
471/// runs the validators and lifts the wire-form
472/// [`crate::config::types::S3OriginConfig`] into this resolved form
473/// with pre-parsed types (notably `endpoint_url: Option<OriginUrl>`).
474/// `Default` is intentionally not derived. The fields are `pub` for
475/// consistency with the other `Resolved*` types in this crate, so
476/// in-crate construction with field-init is technically possible —
477/// the runtime simply doesn't do that. Pattern of intent rather
478/// than visibility-enforced invariant.
479///
480/// Resolved-vs-wire-form differences:
481/// - `endpoint_url` is `Option<OriginUrl>` (parsed) instead of
482///   `Option<String>`, so the S3 backend cannot accidentally pass
483///   an un-normalized URL to the SDK and produce `SigV4` mismatches
484///   between, say, `http://minio:9000` and `http://minio:9000/`.
485/// - `path_style` is `bool` (collapsed from `Option<bool>`), with
486///   `None` mapped to the SDK default (virtual-hosted-style = false).
487/// - `prefix` is a `String` with the trailing-slash
488///   auto-append already applied — the runtime never sees the raw
489///   pre-normalised form.
490#[derive(Debug, Clone)]
491pub struct ResolvedS3Config {
492    /// Bucket name. Validated DNS-safe at construction time.
493    pub bucket: String,
494    /// AWS region, validated non-empty at construction time.
495    pub region: String,
496    /// Custom endpoint URL for non-AWS S3-compatible providers (R2,
497    /// B2, `MinIO`). Parsed and trailing-slash-normalized.
498    pub endpoint_url: Option<decdn_config_types::OriginUrl>,
499    /// Whether to use path-style addressing. `false` (the SDK
500    /// default) selects virtual-hosted-style addressing.
501    pub path_style: bool,
502    /// Optional key prefix prepended to every fetched object.
503    /// Trailing-slash-normalized so the runtime can build keys via
504    /// `format!("{prefix}{shard}/{hex}")` without re-checking the
505    /// trailing slash.
506    pub prefix: String,
507    /// Resolved credential source.
508    pub credentials: Option<ResolvedS3Credentials>,
509    /// How to handle `Content-Encoding` on the S3 origin response
510    /// (#804). Defaults to [`decdn_config_types::DecompressMode::Auto`].
511    pub decompress: decdn_config_types::DecompressMode,
512}
513
514/// Validated runtime form of S3 credentials (#437). See
515/// [`ResolvedS3Config`] for the constructor invariant.
516#[derive(Debug, Clone)]
517pub enum ResolvedS3Credentials {
518    /// Static IAM credentials.
519    Static {
520        /// AWS access key ID. Held in [`crate::config::secret::SecretString`]
521        /// so an incidental `Debug` print or panic backtrace cannot
522        /// leak it.
523        access_key_id: crate::config::secret::SecretString,
524        /// AWS secret access key. Same redaction discipline.
525        secret_access_key: crate::config::secret::SecretString,
526        /// Optional STS session token, redacted.
527        session_token: Option<crate::config::secret::SecretString>,
528    },
529    /// Use the AWS default credential chain. `profile` overrides the
530    /// default profile name for `~/.aws/credentials`.
531    DefaultChain {
532        /// Optional profile name override.
533        profile: Option<String>,
534    },
535}
536
537/// Resolved payment fields.
538#[derive(Debug)]
539pub struct ResolvedPayment {
540    /// Rate per MB in USDC base units (6 decimals).
541    pub rate_per_mb: u64,
542    /// Credit-window ceiling in bytes (ADR 003 §Credit window). The per-stream
543    /// window ramps toward this cap as the stream pays; floored at one chunk
544    /// ([`decdn_protocol::client::CHUNK_BYTES`], the payment quantum) plus one
545    /// 16 KiB chunk group, the leaf the payer verifies before it pays.
546    /// See [`crate::config::DEFAULT_CREDIT_MAX`].
547    pub credit_max: u64,
548    /// Ramp divisor for the credit window; `0` opens the full ceiling immediately.
549    pub credit_ramp_divisor: u64,
550    /// Target wire-frame size for the serve path, in bytes (ADR 005
551    /// §`cdn/client/v1`). See [`crate::config::DEFAULT_FRAME_TARGET_BYTES`] (1 MiB).
552    /// Clamped down to the credit window's remaining room per frame.
553    pub frame_target_bytes: u64,
554    /// Background flush period for the lane store, in ms. See
555    /// [`crate::config::DEFAULT_VOUCHER_COMMIT_INTERVAL_MS`] (5 s). Must be > 0.
556    pub voucher_commit_interval_ms: u64,
557}
558
559/// Resolved observability fields.
560#[derive(Debug)]
561pub struct ResolvedObservability {
562    /// Log verbosity level.
563    pub log_level: LogLevel,
564    /// Log output format.
565    pub log_format: LogFormat,
566    /// Prometheus metrics HTTP port.
567    pub metrics_port: u16,
568    /// IP address to bind the metrics HTTP server on.
569    pub metrics_bind: std::net::IpAddr,
570    /// Loopback admin HTTP port (`appendix-local-admin-http.md`). `None` disables the admin
571    /// server entirely; `Some(port)` binds on `127.0.0.1:<port>`.
572    pub admin_port: Option<u16>,
573    /// OTLP gRPC collector endpoint (if set, span export is enabled). Resolution
574    /// guarantees the `http://host:port` shape; see `validate_otlp_endpoint`.
575    pub otlp_endpoint: Option<String>,
576}
577
578/// Resolved `cdn/dht/v1` settings (ADR 022).
579///
580/// Each `*_rate_per_sec == 0.0` and matching `*_burst == 0` disables that
581/// layer. The resolver guarantees every rate is finite and `>= 0`, and that
582/// each `*_burst > 0` whenever its rate is `> 0` (no deny-all).
583///
584/// `Default` returns the ADR 022 §DHT Rate Limiting defaults so test sites
585/// that build a `ResolvedConfig` by hand can write `ResolvedDht::default()`
586/// instead of restating the constants. The production `resolve_dht_into`
587/// path threads the defaults through the resolver bag and is what
588/// `decdn-common`'s integration tests cover.
589#[derive(Debug, Clone)]
590pub struct ResolvedDht {
591    /// Token refill rate for the per-peer (`NodeId`) limiter, in requests/second.
592    pub per_peer_rate_per_sec: f64,
593    /// Bucket capacity for the per-peer limiter: how many requests one
594    /// peer may send back to back before the refill rate binds. `0` alongside
595    /// a `0.0` rate disables the layer; `0` against a nonzero rate is rejected
596    /// as a deny-all. The same pairing rule holds for the two below.
597    pub per_peer_burst: u32,
598    /// Token refill rate for the per-source-IP limiter, in requests/second.
599    pub per_ip_rate_per_sec: f64,
600    /// Bucket capacity for the per-IP limiter.
601    pub per_ip_burst: u32,
602    /// Token refill rate for the handler-wide limiter, in requests/second. Applies
603    /// across all peers and all source IPs together.
604    pub global_rate_per_sec: f64,
605    /// Bucket capacity for the handler-wide limiter.
606    pub global_burst: u32,
607    /// Hard cap on the per-IP keyed-limiter map (#645). `0` => unbounded.
608    pub max_tracked_per_ip: usize,
609    /// Hard cap on the per-peer (`NodeId`) keyed-limiter map (#645). `0`
610    /// => unbounded.
611    pub max_tracked_per_peer: usize,
612}
613
614impl Default for ResolvedDht {
615    fn default() -> Self {
616        Self {
617            per_peer_rate_per_sec: 20.0,
618            per_peer_burst: 40,
619            per_ip_rate_per_sec: 100.0,
620            per_ip_burst: 200,
621            global_rate_per_sec: 1000.0,
622            global_burst: 2000,
623            max_tracked_per_ip: 4096,
624            max_tracked_per_peer: 4096,
625        }
626    }
627}
628
629/// Resolved `cdn/probe/v1` settings (ADR 005 §Probe rate limiting).
630///
631/// Mirrors [`ResolvedDht`] in shape; `Default` returns the ADR 005 defaults
632/// (a tighter per-peer cap than the DHT layer, since probes are
633/// unauthenticated and cheaper to flood) so hand-built `ResolvedConfig` test
634/// sites can write `ResolvedProbe::default()`. The production
635/// `resolve_probe_into` path threads the defaults through the resolver bag.
636#[derive(Debug, Clone)]
637pub struct ResolvedProbe {
638    /// Token refill rate for the per-peer (`NodeId`) limiter, in probes/second.
639    pub per_peer_rate_per_sec: f64,
640    /// Bucket capacity for the per-peer limiter: how many probes one
641    /// peer may send back to back before the refill rate binds. `0` alongside
642    /// a `0.0` rate disables the layer; `0` against a nonzero rate is rejected
643    /// as a deny-all. The same pairing rule holds for the two below.
644    pub per_peer_burst: u32,
645    /// Token refill rate for the per-source-IP limiter, in probes/second.
646    pub per_ip_rate_per_sec: f64,
647    /// Bucket capacity for the per-IP limiter.
648    pub per_ip_burst: u32,
649    /// Token refill rate for the handler-wide limiter, in probes/second. Applies
650    /// across all peers and all source IPs together.
651    pub global_rate_per_sec: f64,
652    /// Bucket capacity for the handler-wide limiter.
653    pub global_burst: u32,
654    /// Hard cap on the per-IP keyed-limiter map (#645). `0` => unbounded.
655    pub max_tracked_per_ip: usize,
656    /// Hard cap on the per-peer (`NodeId`) keyed-limiter map (#645). `0`
657    /// => unbounded.
658    pub max_tracked_per_peer: usize,
659}
660
661impl Default for ResolvedProbe {
662    fn default() -> Self {
663        Self {
664            per_peer_rate_per_sec: 5.0,
665            per_peer_burst: 5,
666            per_ip_rate_per_sec: 50.0,
667            per_ip_burst: 200,
668            global_rate_per_sec: 1000.0,
669            global_burst: 2000,
670            max_tracked_per_ip: 4096,
671            max_tracked_per_peer: 4096,
672        }
673    }
674}
675
676/// Resolved security / rate-limiting fields.
677#[derive(Debug, Clone)]
678pub struct ResolvedSecurity {
679    /// Global cap on concurrent in-flight QUIC handler tasks.
680    pub max_concurrent_handlers: u32,
681    /// Per-source rate-limit refill (cells/second). `0.0` disables the
682    /// layer.
683    pub per_source_rate_per_sec: f64,
684    /// Per-source rate-limit burst capacity. Ignored when
685    /// `per_source_rate_per_sec == 0.0`.
686    pub per_source_burst: u32,
687    /// Hard cap on the number of tracked sources in the keyed limiter.
688    pub max_tracked_sources: usize,
689}
690
691/// Which load-shed policy the node runs.
692#[derive(Debug, Clone, Copy, PartialEq, Eq)]
693pub enum LoadShedPolicyKind {
694    /// `resource-pressure` (the default): admit against the live egress,
695    /// concurrency, and per-client budgets below.
696    ResourcePressure,
697    /// `always-admit`: never shed. The budgets below are still resolved and
698    /// range-checked, but the policy discards them and refuses nothing.
699    AlwaysAdmit,
700}
701
702/// Resolved load-shedding thresholds.
703#[derive(Debug, Clone)]
704pub struct ResolvedLoadShed {
705    /// Which policy decides admission.
706    pub policy: LoadShedPolicyKind,
707    /// Serving egress budget in Mbps. `0` disables the egress ceiling.
708    pub egress_budget_mbps: u64,
709    /// Concurrency high-water mark: at or above it, the node starts shedding
710    /// misses. The resolver rejects a value below
711    /// [`Self::max_concurrent_serves_low`].
712    pub max_concurrent_serves_high: u32,
713    /// Concurrency low-water mark: at or below it, the node resumes admitting.
714    /// The gap to the high mark is what stops the decision flapping.
715    pub max_concurrent_serves_low: u32,
716    /// Per-client concurrent-serve cap, enforced only while under pressure.
717    /// `0` disables it.
718    pub per_client_serve_cap: u32,
719}
720
721impl Default for ResolvedLoadShed {
722    /// Reuses the `DEFAULT_LOAD_SHED_*` resolver constants so hand-built
723    /// `ResolvedConfig`s in tests cannot drift from the production defaults.
724    fn default() -> Self {
725        Self {
726            policy: LoadShedPolicyKind::ResourcePressure,
727            egress_budget_mbps: super::DEFAULT_LOAD_SHED_EGRESS_BUDGET_MBPS,
728            max_concurrent_serves_high: super::DEFAULT_LOAD_SHED_SERVES_HIGH,
729            max_concurrent_serves_low: super::DEFAULT_LOAD_SHED_SERVES_LOW,
730            per_client_serve_cap: super::DEFAULT_LOAD_SHED_PER_CLIENT_CAP,
731        }
732    }
733}
734
735/// Resolved download-receipt audit-log retention fields (#802).
736///
737/// `max_file_bytes` is always within `[MIN_RECEIPT_MAX_FILE_BYTES,
738/// MAX_RECEIPT_MAX_FILE_BYTES]` and `retained_files` within
739/// `[0, MAX_RECEIPT_RETAINED_FILES]` (the resolver rejects out-of-range
740/// values), so the runtime can bound disk to roughly `(retained_files + 1) *
741/// max_file_bytes`.
742#[derive(Debug, Clone, Copy)]
743pub struct ResolvedReceipts {
744    /// Rotate the live receipt log once it reaches this many bytes.
745    pub max_file_bytes: u64,
746    /// Number of rotated backup files retained (`.1`..=`.N`). `0` keeps none.
747    pub retained_files: u32,
748}
749
750impl Default for ResolvedReceipts {
751    /// Reuses the `DEFAULT_RECEIPT_*` resolver constants so hand-built
752    /// `ResolvedConfig`s in tests cannot drift from the production defaults.
753    fn default() -> Self {
754        Self {
755            max_file_bytes: super::DEFAULT_RECEIPT_MAX_FILE_BYTES,
756            retained_files: super::DEFAULT_RECEIPT_RETAINED_FILES,
757        }
758    }
759}
760
761/// Fully resolved node configuration.
762///
763/// Every field has a value determined by the three-layer merge:
764/// CLI flag > config file > built-in default.
765///
766/// Required fields that have no default (`rpc_url`,
767/// `payment_pool_address`, `capacity_bond_address`) cause
768/// [`super::resolve_config`] to return an error if not provided.
769#[derive(Debug)]
770pub struct ResolvedConfig {
771    /// Node key and data directory.
772    pub identity: ResolvedIdentity,
773    /// Listen addresses, relays, and NAT-traversal settings.
774    pub network: ResolvedNetwork,
775    /// Chain RPC endpoint and the contract addresses the node talks to.
776    pub blockchain: ResolvedBlockchain,
777    /// Blob store, origin backend, and admission/eviction policy.
778    pub cache: ResolvedCache,
779    /// Pricing, voucher, and settlement settings.
780    pub payment: ResolvedPayment,
781    /// Logging, metrics, and tracing endpoints.
782    pub observability: ResolvedObservability,
783    /// Connection and dispatch limits for the serving path.
784    pub security: ResolvedSecurity,
785    /// Overload-protection thresholds.
786    pub load_shed: ResolvedLoadShed,
787    /// `cdn/dht/v1` settings (ADR 022).
788    pub dht: ResolvedDht,
789    /// `cdn/probe/v1` settings (ADR 005).
790    pub probe: ResolvedProbe,
791    /// Download-receipt log settings.
792    pub receipts: ResolvedReceipts,
793    /// Local content denylist (ADR 011).
794    pub content: ResolvedContent,
795}
796
797/// Resolved local content denylist (ADR 011 §Local Denylist).
798///
799/// Both sets are hot-reloadable: a takedown order with a sub-day deadline must
800/// not require a restart to discharge.
801#[derive(Debug, Clone, Default)]
802pub struct ResolvedContent {
803    /// Blob hashes refused at the delivery path. A distinct type from
804    /// [`ResolvedCache::pinned_hashes`]'s `PinnedHashes` on purpose — the two
805    /// are both hash sets meaning opposite things, and confusing them would pin
806    /// content the operator was ordered to remove.
807    pub denied_hashes: DeniedHashes,
808    /// Operator addresses whose pools are refused. Unioned with the on-chain
809    /// origin blacklist at the gate, so the wire refusal cannot distinguish a
810    /// local entry from a governance one (ADR 011 §`StreamRequest` Response).
811    pub denied_origins: HashSet<Address>,
812}