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 / channel 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 /// Maximum single blob size in megabytes.
218 pub max_blob_size_mb: u64,
219 /// Buyer-side ABSOLUTE per-MB rate ceiling for paid pulls (#1375), in the same
220 /// per-MB units as the wire `StreamResponse.rate_per_mb`; `0` = unlimited
221 /// (the default). A buyer-side ceiling: it bounds what this node, acting as a
222 /// BUYER on a cache-miss pull, will accept a provider to quote. The node also
223 /// always applies a probe-relative bound (a quote may not exceed the rate the
224 /// chosen candidate advertised at probe), so this is the additional absolute
225 /// backstop. The node never bounds its own sell rate.
226 pub max_rate_per_mb: u64,
227 /// Resolved ordered list of origin backends (#437, #284). Empty
228 /// vec => no pull-through; cache misses return `NoOrigin`. A
229 /// single-element vec is the single-origin case and is also the
230 /// form produced from a `[cache.origin]` (singular) TOML table —
231 /// both wire forms (singular and plural `[[cache.origins]]`)
232 /// collapse here so downstream wiring sees
233 /// one canonical representation. Per-variant validation (URL
234 /// parse, S3 bucket/region/prefix shape) has already run at
235 /// resolution time — the wiring layer can construct the concrete
236 /// `Origin` impl without re-validating. Order is significant: the
237 /// engine tries entries in this order on a cache miss and falls
238 /// back to the next on `NotFound`, permanent error, or transient
239 /// retry exhaustion.
240 pub origins: Vec<ResolvedOrigin>,
241 /// Operator-pinned blob hashes (#276). Hashes here are excluded from
242 /// LRU eviction candidates by the cache engine. Resolved
243 /// from the hex-encoded TOML form at load time, so any wrong-length
244 /// or non-hex entry fails config loading rather than turning into a
245 /// silent "this hash will be ignored" surprise.
246 ///
247 /// Held as [`decdn_config_types::PinnedHashes`] (a typed wrapper around
248 /// `Arc<HashSet<Hash>>`) so the engine, runtime, and resolver
249 /// share one nominal type — a future "blocklist" or similar
250 /// `HashSet<Hash>`-shaped feature can't be silently swapped into
251 /// the pinning slot.
252 pub pinned_hashes: decdn_config_types::PinnedHashes,
253 /// Origin pull-through retry policy (#285). Set once at startup;
254 /// changes require a process restart.
255 pub origin_retry: decdn_config_types::RetryPolicy,
256 /// Per-origin circuit-breaker policy (#963). Fronts each origin's
257 /// pull-through retry loop so a sustained outage fast-fails the
258 /// origin's misses (no retry/backoff incurred) and the cache sheds
259 /// load, then probes for recovery. Set once at startup; changes
260 /// require a process restart.
261 pub circuit_breaker: decdn_config_types::CircuitBreakerPolicy,
262 /// `User-Agent` header sent on every HTTP origin pull-through (#435).
263 /// Defaults to [`decdn_config_types::DEFAULT_USER_AGENT`] (which embeds
264 /// the `decdn-config-types` crate's `CARGO_PKG_VERSION`; the
265 /// `decdn-node/` prefix — not the version — is the stable contract,
266 /// #578); operators can override
267 /// via `cache.user_agent` to attribute CDN traffic in origin access
268 /// logs or to drive origin-side rate limits and routing policy.
269 pub user_agent: String,
270 /// Interval between iroh-blobs GC sweeps in seconds (#518). `0`
271 /// disables the periodic sweep; any positive value is forwarded to
272 /// iroh-blobs' built-in GC loop. Default
273 /// [`crate::config::DEFAULT_GC_INTERVAL_SEC`] when the TOML section
274 /// omits the field.
275 pub gc_interval_sec: u64,
276 /// Interval between origin-held-index rescans in seconds (#1130). `0`
277 /// disables the periodic rescan (startup + `decdn node reload` still run
278 /// one). Default [`crate::config::DEFAULT_FS_RESCAN_INTERVAL_SEC`] when the
279 /// TOML section omits the field.
280 pub fs_rescan_interval_sec: u64,
281 /// TTL in seconds for a memoised positive live-origin probe answer (#1130
282 /// pt3). Default [`crate::config::DEFAULT_ORIGIN_PROBE_TTL_SEC`]. Backs the
283 /// per-probe `HEAD`/`HeadObject` fallback that discovers non-pinned http/s3
284 /// objects the enumeration index cannot see.
285 pub origin_probe_ttl_sec: u64,
286 /// Default [`crate::config::DEFAULT_ORIGIN_PROBE_NEGATIVE_TTL_SEC`]. The
287 /// negative-answer TTL for the origin-probe memo — the bound on how long a
288 /// stale `Absent` can hide newly-available own content.
289 pub origin_probe_negative_ttl_sec: u64,
290 /// Memo TTL in seconds for a `Fault` answer (#1130 pt3). Default
291 /// [`crate::config::DEFAULT_ORIGIN_PROBE_FAULT_TTL_SEC`]. Bounds how long a
292 /// hash that faulted is not re-probed, and is capped by
293 /// `origin_probe_ttl_sec` so a recovered origin is noticed soon.
294 pub origin_probe_fault_ttl_sec: u64,
295 /// Per-probe live-`HEAD` ceiling in milliseconds (#1130 pt3). Default
296 /// [`crate::config::DEFAULT_ORIGIN_PROBE_TIMEOUT_MS`]. Keeps a slow origin
297 /// off the probe hot path.
298 pub origin_probe_timeout_ms: u64,
299 /// Maximum distinct hashes in the live-origin probe memo (#1130 pt3).
300 /// Default [`crate::config::DEFAULT_ORIGIN_PROBE_MEMO_CAPACITY`]. Bounds
301 /// memo memory under a random-hash probe flood.
302 pub origin_probe_memo_capacity: u64,
303 /// LRU eviction driver high-water percent of [`Self::cache_size_mb`]
304 /// (#1173). Above this fraction the driver actively evicts. Validated to
305 /// `[60, 95]`. Default [`crate::config::DEFAULT_EVICTION_HIGH_WATER_PCT`].
306 pub eviction_high_water_pct: u64,
307 /// LRU eviction driver target percent of [`Self::cache_size_mb`] (#1173).
308 /// The driver evicts down to this fraction before idling. Validated to
309 /// `[40, 90]` and `<= eviction_high_water_pct - 5` (structural hysteresis
310 /// gap). Default [`crate::config::DEFAULT_EVICTION_TARGET_PCT`].
311 pub eviction_target_pct: u64,
312 /// LRU eviction driver max candidates removed per tick (#1173). Validated
313 /// to `[1, 256]`. Default
314 /// [`crate::config::DEFAULT_EVICTION_PER_SWEEP_BUDGET`].
315 pub eviction_per_sweep_budget: u64,
316 /// LRU eviction driver wakeup cadence in seconds (#1173). Validated to
317 /// `[1, 60]`. Default [`crate::config::DEFAULT_EVICTION_TICK_SECS`].
318 pub eviction_tick_secs: u64,
319 /// Maximum number of concurrently held (eviction-exempt) blobs for the
320 /// probe-triggered hold (ADR 005 §Hold budget, #318). Default
321 /// [`crate::config::DEFAULT_MAX_PROBE_HOLDS`]; `0` disables
322 /// `has_blob: true`.
323 pub max_probe_holds: usize,
324 /// Probe-hold slots reserved for the stake lane (registered node-to-node
325 /// requesters) under hold-budget pressure (#757, ADR 003 §Admission and
326 /// Priority). Default `0` disables the reservation (single-lane node).
327 /// The runtime only builds a `StakeLanePolicy` when this is `> 0`, so the
328 /// probe handler's hot path is unchanged for the default.
329 pub stake_lane_reserved_holds: usize,
330 /// Enable node-to-node paid cache-miss pull-through (#831). Default
331 /// `false`. When `true` and the buyer-channel service bootstrapped, the
332 /// runtime provisions the `NodeOrigin` and the client handler triggers a
333 /// pull on a miss (behind a valid client channel). `cache.*` is
334 /// restart-required, so this is read once at bring-up.
335 pub node_to_node_pull_through_enabled: bool,
336 /// When `false`, the node serves and seeds only content its own backend
337 /// holds; a foreign hash is declined like a miss. The default is
338 /// role-derived: `false` (origin-only) when an origin backend is
339 /// configured — an origin is not a general proxy — and `true` (relay)
340 /// when no origin is configured, since a pure edge node's only function
341 /// is relaying. Explicit config overrides either default. Node-local;
342 /// `cache.*` is restart-required.
343 pub relay_foreign_namespaces: bool,
344 /// Providers probed before ranking on a node-to-node pull (#831). Default
345 /// [`crate::config::DEFAULT_NODE_PULL_PROBE_FANOUT`].
346 pub node_pull_probe_fanout: usize,
347 /// Per-pull wall-clock timeout in seconds for a node-to-node miss fill
348 /// (#831). Default [`crate::config::DEFAULT_NODE_PULL_TIMEOUT_SEC`].
349 ///
350 /// Bounds the OPEN stage only; the streaming stage is bounded by
351 /// [`Self::node_pull_stall_window_sec`] and [`Self::node_pull_min_throughput_bps`]
352 /// (#1797).
353 pub node_pull_timeout_sec: u64,
354 /// Throughput-floor window (seconds) on the streaming stage of an upstream pull; bytes
355 /// are counted off the QUIC stream sub-frame, so it trips only when throughput falls
356 /// below the floor, never on a large blob (#1797). Default
357 /// [`crate::config::DEFAULT_NODE_PULL_STALL_WINDOW_SEC`]. Also the head start of each
358 /// own-origin range-pull read's time budget (ADR 037).
359 pub node_pull_stall_window_sec: u64,
360 /// Minimum sustained upstream throughput (bytes/sec) over `node_pull_stall_window_sec`;
361 /// `0` = idle detection only (#1797). Default
362 /// [`crate::config::DEFAULT_NODE_PULL_MIN_THROUGHPUT_BPS`]. Also the average throughput
363 /// each own-origin range-pull read must sustain after the head start; `0` leaves those
364 /// reads unbounded (ADR 037).
365 pub node_pull_min_throughput_bps: u64,
366 /// Cache eviction policy selector (ADR 040). `"lru"` or `"tinylfu"`.
367 /// Default [`crate::config::DEFAULT_EVICTION_POLICY`].
368 pub eviction_policy: String,
369 /// Cache admission policy selector (ADR 040). `"always"` or `"tinylfu"`.
370 /// Default [`crate::config::DEFAULT_ADMISSION_POLICY`].
371 pub admission_policy: String,
372 /// W-TinyLFU tuning knobs (ADR 040), resolved regardless of whether either
373 /// policy selector above is `"tinylfu"` — cheap to resolve and keeps this
374 /// struct free of `Option`.
375 pub tinylfu: ResolvedTinyLfu,
376 /// Refuse-to-serve economics tuning (ADR 041): the margin policy, its
377 /// discount, and per-source speculative-warming allowance.
378 pub serve_economics: ResolvedServeEconomics,
379}
380
381/// Resolved W-TinyLFU tuning knobs. See [`ResolvedCache::tinylfu`].
382#[derive(Debug, Clone, Copy, PartialEq, Eq)]
383pub struct ResolvedTinyLfu {
384 /// Count-min sketch size in bytes. Default
385 /// [`crate::config::DEFAULT_TINYLFU_SKETCH_BYTES`].
386 pub sketch_bytes: usize,
387 /// Prior sightings before a probation member promotes to main. Default
388 /// [`crate::config::DEFAULT_TINYLFU_PROMOTION_THRESHOLD`].
389 pub promotion_threshold: u32,
390 /// Percent of `cache.cache_size_mb` the probation segment is capped to.
391 /// Validated to `[1, 50]`. Default
392 /// [`crate::config::DEFAULT_TINYLFU_PROBATION_TARGET_PCT`].
393 pub probation_target_pct: u64,
394 /// Reserved: half-life in seconds for aging the frequency sketch. Resolved
395 /// and stored but not currently consulted by the shipped sketch (fixed
396 /// sample-count reset, not wall-clock aging). Default
397 /// [`crate::config::DEFAULT_TINYLFU_AGING_HALFLIFE_SEC`].
398 pub aging_halflife_sec: u64,
399}
400
401/// Resolved refuse-to-serve economics tuning (ADR 041). See
402/// [`ResolvedCache::serve_economics`].
403#[derive(Debug, Clone, PartialEq, Eq)]
404pub struct ResolvedServeEconomics {
405 /// Refuse-to-serve policy. `"off"` or `"margin"`. Default
406 /// [`crate::config::DEFAULT_SERVE_ECONOMICS_POLICY`].
407 pub policy: String,
408 /// Discount applied to the sell price when deciding whether a
409 /// speculative pull clears its margin gate, in basis points. Validated
410 /// to `[1, 10_000]` (`5000` == `0.5`). Default
411 /// [`crate::config::DEFAULT_SERVE_ECONOMICS_DISCOUNT_BPS`].
412 pub discount_bps: u32,
413 /// Maximum number of concurrent speculative-warming sources. `>= 1`.
414 /// Default [`crate::config::DEFAULT_SERVE_ECONOMICS_N_MAX`].
415 pub n_max: u32,
416 /// Per-source speculative-warming allowance, in payment base units.
417 /// `> 0`. Default
418 /// [`crate::config::DEFAULT_SERVE_ECONOMICS_WARMING_BUDGET`].
419 pub warming_budget: u64,
420 /// Per-source allowance refill rate, in payment base units per second.
421 /// `0` disables time-based refill. Default
422 /// [`crate::config::DEFAULT_SERVE_ECONOMICS_WARMING_REFILL`].
423 pub warming_refill: u64,
424}
425
426/// Resolved + validated origin backend selection (#437). Mirrors
427/// [`crate::config::types::OriginConfig`] but carries pre-parsed types
428/// for the variants that need them (HTTP base URL, S3 endpoint URL).
429///
430/// The resolution layer (`crate::config::resolve_origin`) is the
431/// **intended** construction path — it's the only producer the
432/// runtime relies on, and the only path that runs the validators.
433/// The fields on each variant are `pub` for consistency with the
434/// other `Resolved*` types in this crate (see `ResolvedCache`,
435/// `ResolvedBlockchain`, etc.); Rust visibility doesn't *enforce*
436/// the validator-only contract, but the runtime never bypasses it.
437#[derive(Debug, Clone)]
438pub enum ResolvedOrigin {
439 /// HTTP(S) origin. The base URL has already been parsed by
440 /// [`decdn_config_types::parse_origin_url`] at resolution time, so
441 /// invariants (http/https scheme, trailing-slash path, no
442 /// query/fragment) are type-enforced — they cannot be reconstructed
443 /// outside the parser.
444 Http {
445 /// Validated base URL.
446 url: decdn_config_types::OriginUrl,
447 /// How to handle `Content-Encoding` on the HTTP origin response
448 /// (#312). Defaults to [`decdn_config_types::DecompressMode::Auto`].
449 decompress: decdn_config_types::DecompressMode,
450 },
451 /// Local filesystem origin root. Directory-existence is validated
452 /// when the runtime constructs the
453 /// cache engine's filesystem origin — config resolution carries
454 /// the raw path so resolution stays filesystem-free and testable
455 /// without real I/O.
456 Fs {
457 /// Filesystem root.
458 path: PathBuf,
459 },
460 /// S3-compatible origin. Field-shape validation (DNS-safe bucket
461 /// name, region non-empty, endpoint URL scheme, prefix shape) has
462 /// already run at resolution time and is encoded in the
463 /// [`ResolvedS3Config`] type.
464 S3(ResolvedS3Config),
465}
466
467/// Validated runtime form of an S3 origin (#437). The intended
468/// construction path is `crate::config::resolve_s3_origin`, which
469/// runs the validators and lifts the wire-form
470/// [`crate::config::types::S3OriginConfig`] into this resolved form
471/// with pre-parsed types (notably `endpoint_url: Option<OriginUrl>`).
472/// `Default` is intentionally not derived. The fields are `pub` for
473/// consistency with the other `Resolved*` types in this crate, so
474/// in-crate construction with field-init is technically possible —
475/// the runtime simply doesn't do that. Pattern of intent rather
476/// than visibility-enforced invariant.
477///
478/// Resolved-vs-wire-form differences:
479/// - `endpoint_url` is `Option<OriginUrl>` (parsed) instead of
480/// `Option<String>`, so the S3 backend cannot accidentally pass
481/// an un-normalized URL to the SDK and produce `SigV4` mismatches
482/// between, say, `http://minio:9000` and `http://minio:9000/`.
483/// - `path_style` is `bool` (collapsed from `Option<bool>`), with
484/// `None` mapped to the SDK default (virtual-hosted-style = false).
485/// - `prefix` is a `String` with the trailing-slash
486/// auto-append already applied — the runtime never sees the raw
487/// pre-normalised form.
488#[derive(Debug, Clone)]
489pub struct ResolvedS3Config {
490 /// Bucket name. Validated DNS-safe at construction time.
491 pub bucket: String,
492 /// AWS region, validated non-empty at construction time.
493 pub region: String,
494 /// Custom endpoint URL for non-AWS S3-compatible providers (R2,
495 /// B2, `MinIO`). Parsed and trailing-slash-normalized.
496 pub endpoint_url: Option<decdn_config_types::OriginUrl>,
497 /// Whether to use path-style addressing. `false` (the SDK
498 /// default) selects virtual-hosted-style addressing.
499 pub path_style: bool,
500 /// Optional key prefix prepended to every fetched object.
501 /// Trailing-slash-normalized so the runtime can build keys via
502 /// `format!("{prefix}{shard}/{hex}")` without re-checking the
503 /// trailing slash.
504 pub prefix: String,
505 /// Resolved credential source.
506 pub credentials: Option<ResolvedS3Credentials>,
507 /// How to handle `Content-Encoding` on the S3 origin response
508 /// (#804). Defaults to [`decdn_config_types::DecompressMode::Auto`].
509 pub decompress: decdn_config_types::DecompressMode,
510}
511
512/// Validated runtime form of S3 credentials (#437). See
513/// [`ResolvedS3Config`] for the constructor invariant.
514#[derive(Debug, Clone)]
515pub enum ResolvedS3Credentials {
516 /// Static IAM credentials.
517 Static {
518 /// AWS access key ID. Held in [`crate::config::secret::SecretString`]
519 /// so an incidental `Debug` print or panic backtrace cannot
520 /// leak it.
521 access_key_id: crate::config::secret::SecretString,
522 /// AWS secret access key. Same redaction discipline.
523 secret_access_key: crate::config::secret::SecretString,
524 /// Optional STS session token, redacted.
525 session_token: Option<crate::config::secret::SecretString>,
526 },
527 /// Use the AWS default credential chain. `profile` overrides the
528 /// default profile name for `~/.aws/credentials`.
529 DefaultChain {
530 /// Optional profile name override.
531 profile: Option<String>,
532 },
533}
534
535/// Resolved payment fields.
536#[derive(Debug)]
537pub struct ResolvedPayment {
538 /// Rate per MB in USDC base units (6 decimals).
539 pub rate_per_mb: u64,
540 /// Credit-window ceiling in bytes (ADR 003 §Credit window). The per-stream
541 /// window ramps toward this cap as the stream pays; floored at one chunk
542 /// ([`decdn_protocol::client::CHUNK_BYTES`], the payment quantum) plus one
543 /// 16 KiB chunk group, the leaf the payer verifies before it pays.
544 /// See [`crate::config::DEFAULT_CREDIT_MAX`].
545 pub credit_max: u64,
546 /// Ramp divisor for the credit window; `0` opens the full ceiling immediately.
547 pub credit_ramp_divisor: u64,
548 /// Target wire-frame size for the serve path, in bytes (ADR 005
549 /// §`cdn/client/v1`). See [`crate::config::DEFAULT_FRAME_TARGET_BYTES`] (1 MiB).
550 /// Clamped down to the credit window's remaining room per frame.
551 pub frame_target_bytes: u64,
552 /// Background flush period for the lane store, in ms. See
553 /// [`crate::config::DEFAULT_VOUCHER_COMMIT_INTERVAL_MS`] (5 s). Must be > 0.
554 pub voucher_commit_interval_ms: u64,
555}
556
557/// Resolved observability fields.
558#[derive(Debug)]
559pub struct ResolvedObservability {
560 /// Log verbosity level.
561 pub log_level: LogLevel,
562 /// Log output format.
563 pub log_format: LogFormat,
564 /// Prometheus metrics HTTP port.
565 pub metrics_port: u16,
566 /// IP address to bind the metrics HTTP server on.
567 pub metrics_bind: std::net::IpAddr,
568 /// Loopback admin HTTP port (`appendix-local-admin-http.md`). `None` disables the admin
569 /// server entirely; `Some(port)` binds on `127.0.0.1:<port>`.
570 pub admin_port: Option<u16>,
571 /// OTLP gRPC collector endpoint (if set, span export is enabled). Resolution
572 /// guarantees the `http://host:port` shape; see `validate_otlp_endpoint`.
573 pub otlp_endpoint: Option<String>,
574}
575
576/// Resolved `cdn/dht/v1` settings (ADR 022).
577///
578/// Each `*_rate_per_sec == 0.0` and matching `*_burst == 0` disables that
579/// layer. The resolver guarantees every rate is finite and `>= 0`, and that
580/// each `*_burst > 0` whenever its rate is `> 0` (no deny-all).
581///
582/// `Default` returns the ADR 022 §DHT Rate Limiting defaults so test sites
583/// that build a `ResolvedConfig` by hand can write `ResolvedDht::default()`
584/// instead of restating the constants. The production `resolve_dht_into`
585/// path threads the defaults through the resolver bag and is what
586/// `decdn-common`'s integration tests cover.
587#[derive(Debug, Clone)]
588pub struct ResolvedDht {
589 /// Token refill rate for the per-peer (`NodeId`) limiter, in requests/second.
590 pub per_peer_rate_per_sec: f64,
591 /// Bucket capacity for the per-peer limiter: how many requests one
592 /// peer may send back to back before the refill rate binds. `0` alongside
593 /// a `0.0` rate disables the layer; `0` against a nonzero rate is rejected
594 /// as a deny-all. The same pairing rule holds for the two below.
595 pub per_peer_burst: u32,
596 /// Token refill rate for the per-source-IP limiter, in requests/second.
597 pub per_ip_rate_per_sec: f64,
598 /// Bucket capacity for the per-IP limiter.
599 pub per_ip_burst: u32,
600 /// Token refill rate for the handler-wide limiter, in requests/second. Applies
601 /// across all peers and all source IPs together.
602 pub global_rate_per_sec: f64,
603 /// Bucket capacity for the handler-wide limiter.
604 pub global_burst: u32,
605 /// Hard cap on the per-IP keyed-limiter map (#645). `0` => unbounded.
606 pub max_tracked_per_ip: usize,
607 /// Hard cap on the per-peer (`NodeId`) keyed-limiter map (#645). `0`
608 /// => unbounded.
609 pub max_tracked_per_peer: usize,
610}
611
612impl Default for ResolvedDht {
613 fn default() -> Self {
614 Self {
615 per_peer_rate_per_sec: 20.0,
616 per_peer_burst: 40,
617 per_ip_rate_per_sec: 100.0,
618 per_ip_burst: 200,
619 global_rate_per_sec: 1000.0,
620 global_burst: 2000,
621 max_tracked_per_ip: 4096,
622 max_tracked_per_peer: 4096,
623 }
624 }
625}
626
627/// Resolved `cdn/probe/v1` settings (ADR 005 §Probe rate limiting).
628///
629/// Mirrors [`ResolvedDht`] in shape; `Default` returns the ADR 005 defaults
630/// (a tighter per-peer cap than the DHT layer, since probes are
631/// unauthenticated and cheaper to flood) so hand-built `ResolvedConfig` test
632/// sites can write `ResolvedProbe::default()`. The production
633/// `resolve_probe_into` path threads the defaults through the resolver bag.
634#[derive(Debug, Clone)]
635pub struct ResolvedProbe {
636 /// Token refill rate for the per-peer (`NodeId`) limiter, in probes/second.
637 pub per_peer_rate_per_sec: f64,
638 /// Bucket capacity for the per-peer limiter: how many probes one
639 /// peer may send back to back before the refill rate binds. `0` alongside
640 /// a `0.0` rate disables the layer; `0` against a nonzero rate is rejected
641 /// as a deny-all. The same pairing rule holds for the two below.
642 pub per_peer_burst: u32,
643 /// Token refill rate for the per-source-IP limiter, in probes/second.
644 pub per_ip_rate_per_sec: f64,
645 /// Bucket capacity for the per-IP limiter.
646 pub per_ip_burst: u32,
647 /// Token refill rate for the handler-wide limiter, in probes/second. Applies
648 /// across all peers and all source IPs together.
649 pub global_rate_per_sec: f64,
650 /// Bucket capacity for the handler-wide limiter.
651 pub global_burst: u32,
652 /// Hard cap on the per-IP keyed-limiter map (#645). `0` => unbounded.
653 pub max_tracked_per_ip: usize,
654 /// Hard cap on the per-peer (`NodeId`) keyed-limiter map (#645). `0`
655 /// => unbounded.
656 pub max_tracked_per_peer: usize,
657}
658
659impl Default for ResolvedProbe {
660 fn default() -> Self {
661 Self {
662 per_peer_rate_per_sec: 5.0,
663 per_peer_burst: 5,
664 per_ip_rate_per_sec: 50.0,
665 per_ip_burst: 200,
666 global_rate_per_sec: 1000.0,
667 global_burst: 2000,
668 max_tracked_per_ip: 4096,
669 max_tracked_per_peer: 4096,
670 }
671 }
672}
673
674/// Resolved security / rate-limiting fields.
675#[derive(Debug, Clone)]
676pub struct ResolvedSecurity {
677 /// Global cap on concurrent in-flight QUIC handler tasks.
678 pub max_concurrent_handlers: u32,
679 /// Per-source rate-limit refill (cells/second). `0.0` disables the
680 /// layer.
681 pub per_source_rate_per_sec: f64,
682 /// Per-source rate-limit burst capacity. Ignored when
683 /// `per_source_rate_per_sec == 0.0`.
684 pub per_source_burst: u32,
685 /// Hard cap on the number of tracked sources in the keyed limiter.
686 pub max_tracked_sources: usize,
687}
688
689/// Which load-shed policy the node runs.
690#[derive(Debug, Clone, Copy, PartialEq, Eq)]
691pub enum LoadShedPolicyKind {
692 /// `resource-pressure` (the default): admit against the live egress,
693 /// concurrency, and per-client budgets below.
694 ResourcePressure,
695 /// `always-admit`: never shed. The budgets below are still resolved and
696 /// range-checked, but the policy discards them and refuses nothing.
697 AlwaysAdmit,
698}
699
700/// Resolved load-shedding thresholds.
701#[derive(Debug, Clone)]
702pub struct ResolvedLoadShed {
703 /// Which policy decides admission.
704 pub policy: LoadShedPolicyKind,
705 /// Serving egress budget in Mbps. `0` disables the egress ceiling.
706 pub egress_budget_mbps: u64,
707 /// Concurrency high-water mark: at or above it, the node starts shedding
708 /// misses. The resolver rejects a value below
709 /// [`Self::max_concurrent_serves_low`].
710 pub max_concurrent_serves_high: u32,
711 /// Concurrency low-water mark: at or below it, the node resumes admitting.
712 /// The gap to the high mark is what stops the decision flapping.
713 pub max_concurrent_serves_low: u32,
714 /// Per-client concurrent-serve cap, enforced only while under pressure.
715 /// `0` disables it.
716 pub per_client_serve_cap: u32,
717}
718
719impl Default for ResolvedLoadShed {
720 /// Reuses the `DEFAULT_LOAD_SHED_*` resolver constants so hand-built
721 /// `ResolvedConfig`s in tests cannot drift from the production defaults.
722 fn default() -> Self {
723 Self {
724 policy: LoadShedPolicyKind::ResourcePressure,
725 egress_budget_mbps: super::DEFAULT_LOAD_SHED_EGRESS_BUDGET_MBPS,
726 max_concurrent_serves_high: super::DEFAULT_LOAD_SHED_SERVES_HIGH,
727 max_concurrent_serves_low: super::DEFAULT_LOAD_SHED_SERVES_LOW,
728 per_client_serve_cap: super::DEFAULT_LOAD_SHED_PER_CLIENT_CAP,
729 }
730 }
731}
732
733/// Resolved download-receipt audit-log retention fields (#802).
734///
735/// `max_file_bytes` is always within `[MIN_RECEIPT_MAX_FILE_BYTES,
736/// MAX_RECEIPT_MAX_FILE_BYTES]` and `retained_files` within
737/// `[0, MAX_RECEIPT_RETAINED_FILES]` (the resolver rejects out-of-range
738/// values), so the runtime can bound disk to roughly `(retained_files + 1) *
739/// max_file_bytes`.
740#[derive(Debug, Clone, Copy)]
741pub struct ResolvedReceipts {
742 /// Rotate the live receipt log once it reaches this many bytes.
743 pub max_file_bytes: u64,
744 /// Number of rotated backup files retained (`.1`..=`.N`). `0` keeps none.
745 pub retained_files: u32,
746}
747
748impl Default for ResolvedReceipts {
749 /// Reuses the `DEFAULT_RECEIPT_*` resolver constants so hand-built
750 /// `ResolvedConfig`s in tests cannot drift from the production defaults.
751 fn default() -> Self {
752 Self {
753 max_file_bytes: super::DEFAULT_RECEIPT_MAX_FILE_BYTES,
754 retained_files: super::DEFAULT_RECEIPT_RETAINED_FILES,
755 }
756 }
757}
758
759/// Fully resolved node configuration.
760///
761/// Every field has a value determined by the three-layer merge:
762/// CLI flag > config file > built-in default.
763///
764/// Required fields that have no default (`rpc_url`,
765/// `payment_pool_address`, `capacity_bond_address`) cause
766/// [`super::resolve_config`] to return an error if not provided.
767#[derive(Debug)]
768pub struct ResolvedConfig {
769 /// Node key and data directory.
770 pub identity: ResolvedIdentity,
771 /// Listen addresses, relays, and NAT-traversal settings.
772 pub network: ResolvedNetwork,
773 /// Chain RPC endpoint and the contract addresses the node talks to.
774 pub blockchain: ResolvedBlockchain,
775 /// Blob store, origin backend, and admission/eviction policy.
776 pub cache: ResolvedCache,
777 /// Pricing, voucher, and settlement settings.
778 pub payment: ResolvedPayment,
779 /// Logging, metrics, and tracing endpoints.
780 pub observability: ResolvedObservability,
781 /// Connection and dispatch limits for the serving path.
782 pub security: ResolvedSecurity,
783 /// Overload-protection thresholds.
784 pub load_shed: ResolvedLoadShed,
785 /// `cdn/dht/v1` settings (ADR 022).
786 pub dht: ResolvedDht,
787 /// `cdn/probe/v1` settings (ADR 005).
788 pub probe: ResolvedProbe,
789 /// Download-receipt log settings.
790 pub receipts: ResolvedReceipts,
791 /// Local content denylist (ADR 011).
792 pub content: ResolvedContent,
793}
794
795/// Resolved local content denylist (ADR 011 §Local Denylist).
796///
797/// Both sets are hot-reloadable: a takedown order with a sub-day deadline must
798/// not require a restart to discharge.
799#[derive(Debug, Clone, Default)]
800pub struct ResolvedContent {
801 /// Blob hashes refused at the delivery path. A distinct type from
802 /// [`ResolvedCache::pinned_hashes`]'s `PinnedHashes` on purpose — the two
803 /// are both hash sets meaning opposite things, and confusing them would pin
804 /// content the operator was ordered to remove.
805 pub denied_hashes: DeniedHashes,
806 /// Operator addresses whose channels are refused. Unioned with the on-chain
807 /// origin blacklist at the gate, so the wire refusal cannot distinguish a
808 /// local entry from a governance one (ADR 011 §`StreamRequest` Response).
809 pub denied_origins: HashSet<Address>,
810}