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}