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 / 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).
543    /// See [`crate::config::DEFAULT_CREDIT_MAX`].
544    pub credit_max: u64,
545    /// Ramp divisor for the credit window; `0` opens the full ceiling immediately.
546    pub credit_ramp_divisor: u64,
547    /// Target wire-frame size for the serve path, in bytes (ADR 005
548    /// §`cdn/client/v1`). See [`crate::config::DEFAULT_FRAME_TARGET_BYTES`] (1 MiB).
549    /// Clamped down to the credit window's remaining room per frame.
550    pub frame_target_bytes: u64,
551    /// Background flush period for the lane store, in ms. See
552    /// [`crate::config::DEFAULT_VOUCHER_COMMIT_INTERVAL_MS`] (5 s). Must be > 0.
553    pub voucher_commit_interval_ms: u64,
554}
555
556/// Resolved observability fields.
557#[derive(Debug)]
558pub struct ResolvedObservability {
559    /// Log verbosity level.
560    pub log_level: LogLevel,
561    /// Log output format.
562    pub log_format: LogFormat,
563    /// Prometheus metrics HTTP port.
564    pub metrics_port: u16,
565    /// IP address to bind the metrics HTTP server on.
566    pub metrics_bind: std::net::IpAddr,
567    /// Loopback admin HTTP port (`appendix-local-admin-http.md`). `None` disables the admin
568    /// server entirely; `Some(port)` binds on `127.0.0.1:<port>`.
569    pub admin_port: Option<u16>,
570    /// OTLP gRPC collector endpoint (if set, span export is enabled). Resolution
571    /// guarantees the `http://host:port` shape; see `validate_otlp_endpoint`.
572    pub otlp_endpoint: Option<String>,
573}
574
575/// Resolved `cdn/dht/v1` settings (ADR 022).
576///
577/// Each `*_rate_per_sec == 0.0` and matching `*_burst == 0` disables that
578/// layer. The resolver guarantees every rate is finite and `>= 0`, and that
579/// each `*_burst > 0` whenever its rate is `> 0` (no deny-all).
580///
581/// `Default` returns the ADR 022 §DHT Rate Limiting defaults so test sites
582/// that build a `ResolvedConfig` by hand can write `ResolvedDht::default()`
583/// instead of restating the constants. The production `resolve_dht_into`
584/// path threads the defaults through the resolver bag and is what
585/// `decdn-common`'s integration tests cover.
586#[derive(Debug, Clone)]
587pub struct ResolvedDht {
588    /// Token refill rate for the per-peer (`NodeId`) limiter, in requests/second.
589    pub per_peer_rate_per_sec: f64,
590    /// Bucket capacity for the per-peer limiter: how many requests one
591    /// peer may send back to back before the refill rate binds. `0` alongside
592    /// a `0.0` rate disables the layer; `0` against a nonzero rate is rejected
593    /// as a deny-all. The same pairing rule holds for the two below.
594    pub per_peer_burst: u32,
595    /// Token refill rate for the per-source-IP limiter, in requests/second.
596    pub per_ip_rate_per_sec: f64,
597    /// Bucket capacity for the per-IP limiter.
598    pub per_ip_burst: u32,
599    /// Token refill rate for the handler-wide limiter, in requests/second. Applies
600    /// across all peers and all source IPs together.
601    pub global_rate_per_sec: f64,
602    /// Bucket capacity for the handler-wide limiter.
603    pub global_burst: u32,
604    /// Hard cap on the per-IP keyed-limiter map (#645). `0` => unbounded.
605    pub max_tracked_per_ip: usize,
606    /// Hard cap on the per-peer (`NodeId`) keyed-limiter map (#645). `0`
607    /// => unbounded.
608    pub max_tracked_per_peer: usize,
609}
610
611impl Default for ResolvedDht {
612    fn default() -> Self {
613        Self {
614            per_peer_rate_per_sec: 20.0,
615            per_peer_burst: 40,
616            per_ip_rate_per_sec: 100.0,
617            per_ip_burst: 200,
618            global_rate_per_sec: 1000.0,
619            global_burst: 2000,
620            max_tracked_per_ip: 4096,
621            max_tracked_per_peer: 4096,
622        }
623    }
624}
625
626/// Resolved `cdn/probe/v1` settings (ADR 005 §Probe rate limiting).
627///
628/// Mirrors [`ResolvedDht`] in shape; `Default` returns the ADR 005 defaults
629/// (a tighter per-peer cap than the DHT layer, since probes are
630/// unauthenticated and cheaper to flood) so hand-built `ResolvedConfig` test
631/// sites can write `ResolvedProbe::default()`. The production
632/// `resolve_probe_into` path threads the defaults through the resolver bag.
633#[derive(Debug, Clone)]
634pub struct ResolvedProbe {
635    /// Token refill rate for the per-peer (`NodeId`) limiter, in probes/second.
636    pub per_peer_rate_per_sec: f64,
637    /// Bucket capacity for the per-peer limiter: how many probes one
638    /// peer may send back to back before the refill rate binds. `0` alongside
639    /// a `0.0` rate disables the layer; `0` against a nonzero rate is rejected
640    /// as a deny-all. The same pairing rule holds for the two below.
641    pub per_peer_burst: u32,
642    /// Token refill rate for the per-source-IP limiter, in probes/second.
643    pub per_ip_rate_per_sec: f64,
644    /// Bucket capacity for the per-IP limiter.
645    pub per_ip_burst: u32,
646    /// Token refill rate for the handler-wide limiter, in probes/second. Applies
647    /// across all peers and all source IPs together.
648    pub global_rate_per_sec: f64,
649    /// Bucket capacity for the handler-wide limiter.
650    pub global_burst: u32,
651    /// Hard cap on the per-IP keyed-limiter map (#645). `0` => unbounded.
652    pub max_tracked_per_ip: usize,
653    /// Hard cap on the per-peer (`NodeId`) keyed-limiter map (#645). `0`
654    /// => unbounded.
655    pub max_tracked_per_peer: usize,
656}
657
658impl Default for ResolvedProbe {
659    fn default() -> Self {
660        Self {
661            per_peer_rate_per_sec: 5.0,
662            per_peer_burst: 5,
663            per_ip_rate_per_sec: 50.0,
664            per_ip_burst: 200,
665            global_rate_per_sec: 1000.0,
666            global_burst: 2000,
667            max_tracked_per_ip: 4096,
668            max_tracked_per_peer: 4096,
669        }
670    }
671}
672
673/// Resolved security / rate-limiting fields.
674#[derive(Debug, Clone)]
675pub struct ResolvedSecurity {
676    /// Global cap on concurrent in-flight QUIC handler tasks.
677    pub max_concurrent_handlers: u32,
678    /// Per-source rate-limit refill (cells/second). `0.0` disables the
679    /// layer.
680    pub per_source_rate_per_sec: f64,
681    /// Per-source rate-limit burst capacity. Ignored when
682    /// `per_source_rate_per_sec == 0.0`.
683    pub per_source_burst: u32,
684    /// Hard cap on the number of tracked sources in the keyed limiter.
685    pub max_tracked_sources: usize,
686}
687
688/// Which load-shed policy the node runs.
689#[derive(Debug, Clone, Copy, PartialEq, Eq)]
690pub enum LoadShedPolicyKind {
691    /// `resource-pressure` (the default): admit against the live egress,
692    /// concurrency, and per-client budgets below.
693    ResourcePressure,
694    /// `always-admit`: never shed. The budgets below are still resolved and
695    /// range-checked, but the policy discards them and refuses nothing.
696    AlwaysAdmit,
697}
698
699/// Resolved load-shedding thresholds.
700#[derive(Debug, Clone)]
701pub struct ResolvedLoadShed {
702    /// Which policy decides admission.
703    pub policy: LoadShedPolicyKind,
704    /// Serving egress budget in Mbps. `0` disables the egress ceiling.
705    pub egress_budget_mbps: u64,
706    /// Concurrency high-water mark: at or above it, the node starts shedding
707    /// misses. The resolver rejects a value below
708    /// [`Self::max_concurrent_serves_low`].
709    pub max_concurrent_serves_high: u32,
710    /// Concurrency low-water mark: at or below it, the node resumes admitting.
711    /// The gap to the high mark is what stops the decision flapping.
712    pub max_concurrent_serves_low: u32,
713    /// Per-client concurrent-serve cap, enforced only while under pressure.
714    /// `0` disables it.
715    pub per_client_serve_cap: u32,
716}
717
718impl Default for ResolvedLoadShed {
719    /// Reuses the `DEFAULT_LOAD_SHED_*` resolver constants so hand-built
720    /// `ResolvedConfig`s in tests cannot drift from the production defaults.
721    fn default() -> Self {
722        Self {
723            policy: LoadShedPolicyKind::ResourcePressure,
724            egress_budget_mbps: super::DEFAULT_LOAD_SHED_EGRESS_BUDGET_MBPS,
725            max_concurrent_serves_high: super::DEFAULT_LOAD_SHED_SERVES_HIGH,
726            max_concurrent_serves_low: super::DEFAULT_LOAD_SHED_SERVES_LOW,
727            per_client_serve_cap: super::DEFAULT_LOAD_SHED_PER_CLIENT_CAP,
728        }
729    }
730}
731
732/// Resolved download-receipt audit-log retention fields (#802).
733///
734/// `max_file_bytes` is always within `[MIN_RECEIPT_MAX_FILE_BYTES,
735/// MAX_RECEIPT_MAX_FILE_BYTES]` and `retained_files` within
736/// `[0, MAX_RECEIPT_RETAINED_FILES]` (the resolver rejects out-of-range
737/// values), so the runtime can bound disk to roughly `(retained_files + 1) *
738/// max_file_bytes`.
739#[derive(Debug, Clone, Copy)]
740pub struct ResolvedReceipts {
741    /// Rotate the live receipt log once it reaches this many bytes.
742    pub max_file_bytes: u64,
743    /// Number of rotated backup files retained (`.1`..=`.N`). `0` keeps none.
744    pub retained_files: u32,
745}
746
747impl Default for ResolvedReceipts {
748    /// Reuses the `DEFAULT_RECEIPT_*` resolver constants so hand-built
749    /// `ResolvedConfig`s in tests cannot drift from the production defaults.
750    fn default() -> Self {
751        Self {
752            max_file_bytes: super::DEFAULT_RECEIPT_MAX_FILE_BYTES,
753            retained_files: super::DEFAULT_RECEIPT_RETAINED_FILES,
754        }
755    }
756}
757
758/// Fully resolved node configuration.
759///
760/// Every field has a value determined by the three-layer merge:
761/// CLI flag > config file > built-in default.
762///
763/// Required fields that have no default (`rpc_url`,
764/// `payment_pool_address`, `capacity_bond_address`) cause
765/// [`super::resolve_config`] to return an error if not provided.
766#[derive(Debug)]
767pub struct ResolvedConfig {
768    /// Node key and data directory.
769    pub identity: ResolvedIdentity,
770    /// Listen addresses, relays, and NAT-traversal settings.
771    pub network: ResolvedNetwork,
772    /// Chain RPC endpoint and the contract addresses the node talks to.
773    pub blockchain: ResolvedBlockchain,
774    /// Blob store, origin backend, and admission/eviction policy.
775    pub cache: ResolvedCache,
776    /// Pricing, voucher, and settlement settings.
777    pub payment: ResolvedPayment,
778    /// Logging, metrics, and tracing endpoints.
779    pub observability: ResolvedObservability,
780    /// Connection and dispatch limits for the serving path.
781    pub security: ResolvedSecurity,
782    /// Overload-protection thresholds.
783    pub load_shed: ResolvedLoadShed,
784    /// `cdn/dht/v1` settings (ADR 022).
785    pub dht: ResolvedDht,
786    /// `cdn/probe/v1` settings (ADR 005).
787    pub probe: ResolvedProbe,
788    /// Download-receipt log settings.
789    pub receipts: ResolvedReceipts,
790    /// Local content denylist (ADR 011).
791    pub content: ResolvedContent,
792}
793
794/// Resolved local content denylist (ADR 011 §Local Denylist).
795///
796/// Both sets are hot-reloadable: a takedown order with a sub-day deadline must
797/// not require a restart to discharge.
798#[derive(Debug, Clone, Default)]
799pub struct ResolvedContent {
800    /// Blob hashes refused at the delivery path. A distinct type from
801    /// [`ResolvedCache::pinned_hashes`]'s `PinnedHashes` on purpose — the two
802    /// are both hash sets meaning opposite things, and confusing them would pin
803    /// content the operator was ordered to remove.
804    pub denied_hashes: DeniedHashes,
805    /// Operator addresses whose channels are refused. Unioned with the on-chain
806    /// origin blacklist at the gate, so the wire refusal cannot distinguish a
807    /// local entry from a governance one (ADR 011 §`StreamRequest` Response).
808    pub denied_origins: HashSet<Address>,
809}