decdn_common/cli/node.rs
1//! Arguments for the `decdn node` subcommand group.
2//!
3//! `node` is the operator-local admin namespace: subcommands talk to the
4//! loopback admin HTTP surface (`adr/appendix-local-admin-http.md`) exposed by a running node.
5
6use std::num::NonZeroU64;
7use std::path::PathBuf;
8
9use clap::{ArgGroup, Args, Subcommand};
10
11use super::common::CommonChainArgs;
12
13/// Default `--wait-timeout-secs`. Wrapped here as a `const` so the
14/// `NonZeroU64` constructor can be evaluated at compile time.
15const DEFAULT_WAIT_TIMEOUT_SECS: NonZeroU64 = match NonZeroU64::new(30) {
16 Some(v) => v,
17 None => unreachable!(),
18};
19
20/// Default `--wait-poll-ms`. Wrapped here as a `const` for the same
21/// reason as [`DEFAULT_WAIT_TIMEOUT_SECS`].
22const DEFAULT_WAIT_POLL_MS: NonZeroU64 = match NonZeroU64::new(250) {
23 Some(v) => v,
24 None => unreachable!(),
25};
26
27/// Operator-local admin commands that query a running deCDN node.
28#[derive(Args, Debug)]
29pub struct NodeArgs {
30 /// The `decdn node` subcommand to run.
31 #[command(subcommand)]
32 pub cmd: NodeCommand,
33}
34
35/// Subcommands under `decdn node`.
36#[derive(Subcommand, Debug)]
37pub enum NodeCommand {
38 /// Print a running node's identity and process uptime.
39 Health(HealthArgs),
40 /// Print a snapshot of the running node's DHT participation health
41 /// via `admin_v1_status` (issue #741): Kademlia routing-table bucket
42 /// fill rates, the network-wide last bucket-refresh time, the
43 /// active-staker count, provider-record store utilization, and
44 /// republish-scheduler depth. Lets operators diagnose cold-start or
45 /// routing-table degradation without scraping Prometheus or reading
46 /// logs.
47 Status(StatusArgs),
48 /// Print a live snapshot of the running node's lanes via
49 /// `admin_v1_lanes` (issue #749): per lane the outstanding accrued
50 /// claim (micro-USDC), time since the last voucher, and whether the
51 /// accrued claim has reached the configured redemption threshold.
52 /// Lets operators spot lanes approaching settlement, stale lanes, or
53 /// unusually high outstanding balances before they become a
54 /// liquidity risk — without scraping metrics or reading logs. Same
55 /// admin-URL resolution and timeout semantics as `decdn node health`.
56 Lanes(LanesArgs),
57 /// Print every slash the node's watcher has detected against its own
58 /// operator via `admin_v1_slashes` (#1032, G-NODE-05): per slash the
59 /// `slashId`, offense type, amount, evidence digest, block, and the
60 /// 30-day appeal-window close time. Lets operators (or keeper scripts)
61 /// notice a slash and file `decdn appeal slash` within the window
62 /// without watching the chain directly. Same admin-URL resolution and
63 /// timeout semantics as `decdn node health`.
64 Slashes(SlashesArgs),
65 /// Print the running node's buyer-side `PaymentPool` state via
66 /// `admin_v1_pools` (#2078): every pool the node's buy leg tracks, the
67 /// deposit it believes each holds, and the per-lane amount it has already
68 /// signed away to each provider. This is the node's *spending* side — the
69 /// mirror of `decdn node lanes`, which reports what it has earned.
70 ///
71 /// The only way to read this state on a running node: the daemon holds an
72 /// exclusive lock on its `buyer.redb` for its whole lifetime, so no
73 /// command can open that file from disk while the node is up. `decdn pool
74 /// list` manages a **separate**, client-owned store and says nothing about
75 /// this one. Same admin-URL resolution and timeout semantics as
76 /// `decdn node health`.
77 Pools(PoolsArgs),
78 /// Forcibly remove a single blob from the local cache (issue #279).
79 /// The removal is permanent: the node never serves or re-fetches the hash
80 /// again. Useful for DMCA takedown and storage reclamation.
81 Evict(EvictArgs),
82 /// Re-read the running node's config file and apply hot-reloadable
83 /// fields (issue #373). Equivalent to `kill -HUP <pid>` but goes
84 /// through the loopback admin surface, so operator tooling that
85 /// already speaks JSON-RPC doesn't need to also know which PID to
86 /// signal. `observability.log_level`, `cache.pinned_hashes`,
87 /// `security.*`, the `[content]` denylist, and `[load_shed]` are
88 /// reloadable; other fields are logged as ignored. The authoritative
89 /// list is the module doc of the node's `runtime/reload.rs` — when a
90 /// section is added there, update this help text too. Both paths
91 /// share the same internal mutex, so a concurrent SIGHUP and
92 /// `decdn node reload` queue rather than race. Requires the node to
93 /// have been started with `decdn-node run --config <path>` — without
94 /// a path on disk there's nothing to re-read.
95 Reload(ReloadArgs),
96 /// Trigger graceful shutdown of the running node via `admin_v1_drain`
97 /// (issue #244, `adr/appendix-local-admin-http.md`). Equivalent to `kill -TERM <pid>` but goes
98 /// through the loopback admin surface, so operator tooling that already
99 /// speaks JSON-RPC doesn't need to also know which PID to signal. The
100 /// runtime begins the same graceful shutdown sequence SIGTERM triggers
101 /// (stops accept loops, waits for in-flight transfers, flushes cache).
102 ///
103 /// **Fire-and-forget semantics**: this command returns `drain_initiated=true`
104 /// as soon as the trigger is queued. The response does *not* mean shutdown
105 /// is complete — it means the runtime has been asked to shut down, and
106 /// the connection may close before the node fully exits because the
107 /// admin server is one of the first surfaces to stop. Observe completion
108 /// via process exit (systemd/K8s will notice) or by polling `decdn
109 /// node health` until the connection is refused.
110 Drain(DrainArgs),
111 /// Render a live (every `--interval-ms`) view of node activity by
112 /// scraping the running node's `/metrics` HTTP endpoint (issue
113 /// #275). Shows active streams, cache hit rate, and cache bytes
114 /// returned — the operator-visible signal the issue calls "bytes
115 /// served" maps to `decdn_cache_bytes_returned_total` until the
116 /// network-side `cdn/client/v1` egress counter (#317) lands.
117 Top(TopArgs),
118 /// Register this node on-chain via `CapacityBond.registerNode` (ADR 019
119 /// § Step 2.3). Loads the local iroh node key and Ethereum keystore,
120 /// builds the EIP-712 `bindingSignature` and the ed25519 ownership
121 /// signature locally, and submits the registration transaction. This
122 /// is the only `node` subcommand that talks to the chain rather than a
123 /// running node's admin RPC.
124 ///
125 /// Phase 2.1/2.2 (`approve` + `bond` + `declareMbps`) are a
126 /// precondition: the contract reverts if the operator's bond does not
127 /// cover `minBond` / the declared-capacity curve. Pass `--dry-run` to
128 /// print the parameters and signatures without submitting.
129 Register(RegisterArgs),
130 /// Stake to a capacity tier on-chain via `CapacityBond.bond` +
131 /// `declareMbps` (ADR 019 § Step 2.1–2.2). Idempotent: tops the active
132 /// bond up to `max(minBond, bondRequired(mbps))`, approving and bonding
133 /// only the shortfall, so re-running after a partial failure converges
134 /// rather than over-bonding. Run this before `decdn node register`. Pass
135 /// `--dry-run` to print the plan without submitting.
136 Bond(BondArgs),
137 /// Lower the on-chain bond via `CapacityBond.requestUnbond` + `unbond`
138 /// (ADR 026 § Capacity-bond curve) — the reverse of `decdn node bond`.
139 /// State-aware: with no request in flight it declares the tier down (if
140 /// needed) and starts the unbonding window; while one is maturing it
141 /// reports the unlock time and exits non-zero; once matured it withdraws.
142 /// Note that a request in flight makes the node INACTIVE for the whole
143 /// window. Pass `--dry-run` to print the plan without submitting.
144 Unbond(UnbondArgs),
145 /// Leave the active node set on-chain via `CapacityBond.deregisterNode`
146 /// (ADR 003 § Node Registry) — the reverse of `decdn node register`, and
147 /// the first leg of a full bond exit. Clears the declared capacity tier,
148 /// which is what releases the `bondRequired(declaredMbps)` floor that
149 /// `decdn node unbond` enforces.
150 ///
151 /// This does NOT return the bond: it stays deposited and fully slashable.
152 /// Follow with `decdn node unbond --all` to start the unbonding window,
153 /// then re-run it after the window to withdraw. Pass `--dry-run` to print
154 /// the plan without submitting.
155 Deregister(DeregisterArgs),
156 /// Rotate an operator key and keep the on-chain binding in step
157 /// (`adr/appendix-operator-key-rotation.md`, #1034). `--key` chooses which
158 /// key, because the two paths are not comparable in cost or effect.
159 ///
160 /// `--key iroh` rebinds the wire node id through
161 /// `CapacityBond.bindNodeId`. One transaction, no downtime beyond a
162 /// restart, and the Ethereum address — with the bond, the declared tier,
163 /// `firstBondedAt`, and every lane funded against this operator — is untouched. The old
164 /// and new node ids swap slashability in the same block, so there is no
165 /// window in which the operator cannot be slashed.
166 ///
167 /// `--key eth` moves the whole on-chain identity, because no rebinding API
168 /// exists for the address: deregister, wait out the full unbonding window,
169 /// withdraw, then re-bond and re-register from a new address.
170 /// `firstBondedAt` — and with it the governance age-ramp — resets. Like
171 /// `decdn node unbond`, the phase is read from chain state rather than
172 /// passed as a flag, so one command drives the whole window.
173 ///
174 /// Neither path restarts the daemon: the iroh key is not hot-reloadable,
175 /// so drain (`decdn node drain`), stop, rotate, restart stays the
176 /// operator's sequence. Pass `--dry-run` to print the plan — and, on the
177 /// iroh path, both signatures — without submitting or touching any key
178 /// file.
179 RotateKey(RotateKeyArgs),
180 /// (Re)publish this node's dialable QUIC addresses on-chain via
181 /// `CapacityBond.updateMultiaddrs` (#1908). The one-command fix for a node
182 /// that registered an empty multiaddr set (`decdn node register` with no
183 /// `--multiaddr`) and is therefore stranded on the iroh relay path: it
184 /// promotes direct addresses without a destructive deregister/re-register
185 /// cycle (which `register` alone cannot do — a second `register` reverts
186 /// `NodeAlreadyRegistered`).
187 ///
188 /// Like the other on-chain `node` subcommands this talks to the chain, not
189 /// a running node's admin RPC, and signs with the operator Ethereum key.
190 /// The node must be active; the contract's guardrails — the
191 /// `maxMultiaddrSize` byte ceiling and the `multiaddrUpdateCooldown`
192 /// between updates — are pre-checked and reported before the send. Pass
193 /// `--dry-run` to print the plan (packed size, ceiling, cooldown state)
194 /// without submitting.
195 UpdateMultiaddrs(UpdateMultiaddrsArgs),
196 /// (Re)attest this node's region on-chain via `CapacityBond.updateRegion`
197 /// (ADR 030). The one-command fix for a node that registered the wrong
198 /// `regionHint` (`decdn node register --region`) or whose operator has
199 /// physically relocated: it corrects the region without a destructive
200 /// deregister/re-register cycle (which `register` alone cannot do — a second
201 /// `register` reverts `NodeAlreadyRegistered`).
202 ///
203 /// Like the other on-chain `node` subcommands this talks to the chain, not
204 /// a running node's admin RPC, and signs with the operator Ethereum key. The
205 /// region is validated as an ISO 3166-1 alpha-2 code CLI-side — the contract
206 /// only length-checks and would otherwise store a garbage string. The node
207 /// must be active; the contract's `regionStabilityWindow` cooldown between
208 /// updates is pre-checked and reported before the send. Pass `--dry-run` to
209 /// print the plan (current region, new region, cooldown state) without
210 /// submitting.
211 UpdateRegion(UpdateRegionArgs),
212 /// Unpaid client-side discovery of active nodes via
213 /// `CapacityBond.getRegisteredNodes` (#1481). Maps node-ids/regions to
214 /// operator Ethereum addresses — the input `decdn get
215 /// --provider-address` needs — without spending anything: it builds a
216 /// signer-less read-only provider and never loads a keystore, unlike
217 /// every other on-chain `node` subcommand above.
218 ///
219 /// With neither `--node-id` nor `--region`, lists every active node.
220 /// Pass `--probe` to additionally rank the (region-shortlisted)
221 /// candidates by measured `cdn/probe/v1` round-trip time; without it,
222 /// candidates are listed with no RTT.
223 Lookup(LookupArgs),
224 /// Diagnose config, on-disk state, disk budget, and reachability.
225 // Boxed to keep `NodeCommand`'s variants close in size (`DoctorArgs` is by
226 // far the largest); mirrors `Command::Run(Box<RunArgs>)`.
227 Doctor(Box<DoctorArgs>),
228}
229
230/// `decdn node health` — report identity (hex `node_id`) and process
231/// uptime via `admin_v1_health`.
232#[derive(Args, Debug)]
233pub struct HealthArgs {
234 /// Base URL of the node's admin HTTP surface.
235 ///
236 /// Also read from `DECDN_ADMIN_URL` when unset; clap folds the env
237 /// var into this field. If still unset, the admin port is derived
238 /// from `observability.admin_port` in the config file (see
239 /// `--config`). Example: `http://127.0.0.1:9191`.
240 #[arg(long, value_name = "URL", env = "DECDN_ADMIN_URL")]
241 pub admin_url: Option<String>,
242
243 /// Path to the TOML config file used to derive the admin URL when
244 /// `--admin-url` / `DECDN_ADMIN_URL` are unset. Takes precedence
245 /// over the top-level `decdn --config`; if neither is set,
246 /// resolution falls through to `~/.decdn/node.toml` and then the
247 /// built-in default port.
248 #[arg(long, value_name = "PATH")]
249 pub config: Option<PathBuf>,
250
251 /// Emit the admin response body as JSON instead of two human-
252 /// readable lines.
253 #[arg(long)]
254 pub json: bool,
255
256 /// Roundtrip timeout in milliseconds.
257 #[arg(long, value_name = "MS", default_value_t = 5_000)]
258 pub timeout_ms: u64,
259}
260
261/// `decdn node status` — report DHT participation health via
262/// `admin_v1_status` (issue #741). Same admin-URL resolution and timeout
263/// semantics as `decdn node health`.
264#[derive(Args, Debug)]
265pub struct StatusArgs {
266 /// Base URL of the node's admin HTTP surface.
267 ///
268 /// Also read from `DECDN_ADMIN_URL` when unset; clap folds the env
269 /// var into this field. If still unset, the admin port is derived
270 /// from `observability.admin_port` in the config file (see
271 /// `--config`). Example: `http://127.0.0.1:9191`.
272 #[arg(long, value_name = "URL", env = "DECDN_ADMIN_URL")]
273 pub admin_url: Option<String>,
274
275 /// Path to the TOML config file used to derive the admin URL when
276 /// `--admin-url` / `DECDN_ADMIN_URL` are unset. Takes precedence
277 /// over the top-level `decdn --config`; if neither is set,
278 /// resolution falls through to `~/.decdn/node.toml` and then the
279 /// built-in default port.
280 #[arg(long, value_name = "PATH")]
281 pub config: Option<PathBuf>,
282
283 /// Emit the admin response body as JSON instead of the human-readable
284 /// summary + bucket table.
285 #[arg(long)]
286 pub json: bool,
287
288 /// Roundtrip timeout in milliseconds.
289 #[arg(long, value_name = "MS", default_value_t = 5_000)]
290 pub timeout_ms: u64,
291}
292
293/// `decdn node lanes` — list open payment lanes via
294/// `admin_v1_lanes` (issue #749). Same admin-URL resolution and
295/// timeout semantics as `decdn node health`.
296#[derive(Args, Debug)]
297pub struct LanesArgs {
298 /// Base URL of the node's admin HTTP surface.
299 ///
300 /// Also read from `DECDN_ADMIN_URL` when unset; clap folds the env
301 /// var into this field. If still unset, the admin port is derived
302 /// from `observability.admin_port` in the config file (see
303 /// `--config`). Example: `http://127.0.0.1:9191`.
304 #[arg(long, value_name = "URL", env = "DECDN_ADMIN_URL")]
305 pub admin_url: Option<String>,
306
307 /// Path to the TOML config file used to derive the admin URL when
308 /// `--admin-url` / `DECDN_ADMIN_URL` are unset. Takes precedence
309 /// over the top-level `decdn --config`; if neither is set,
310 /// resolution falls through to `~/.decdn/node.toml` and then the
311 /// built-in default port.
312 #[arg(long, value_name = "PATH")]
313 pub config: Option<PathBuf>,
314
315 /// Emit the admin response body as JSON instead of the human-readable
316 /// summary + lane table.
317 #[arg(long)]
318 pub json: bool,
319
320 /// Roundtrip timeout in milliseconds.
321 #[arg(long, value_name = "MS", default_value_t = 5_000)]
322 pub timeout_ms: u64,
323}
324
325/// `decdn node slashes` — list detected slashes against this node's
326/// operator via `admin_v1_slashes` (#1032). Same admin-URL resolution and
327/// timeout semantics as `decdn node health`.
328#[derive(Args, Debug)]
329pub struct SlashesArgs {
330 /// Base URL of the node's admin HTTP surface.
331 ///
332 /// Also read from `DECDN_ADMIN_URL` when unset; clap folds the env
333 /// var into this field. If still unset, the admin port is derived
334 /// from `observability.admin_port` in the config file (see
335 /// `--config`). Example: `http://127.0.0.1:9191`.
336 #[arg(long, value_name = "URL", env = "DECDN_ADMIN_URL")]
337 pub admin_url: Option<String>,
338
339 /// Path to the TOML config file used to derive the admin URL when
340 /// `--admin-url` / `DECDN_ADMIN_URL` are unset. Takes precedence
341 /// over the top-level `decdn --config`; if neither is set,
342 /// resolution falls through to `~/.decdn/node.toml` and then the
343 /// built-in default port.
344 #[arg(long, value_name = "PATH")]
345 pub config: Option<PathBuf>,
346
347 /// Emit the admin response body as JSON instead of the human-readable
348 /// summary + slash table.
349 #[arg(long)]
350 pub json: bool,
351
352 /// Roundtrip timeout in milliseconds.
353 #[arg(long, value_name = "MS", default_value_t = 5_000)]
354 pub timeout_ms: u64,
355}
356
357/// `decdn node pools` — read the node's buyer-side `PaymentPool` state via
358/// `admin_v1_pools` (#2078). Same admin-URL resolution and timeout semantics as
359/// `decdn node health`.
360#[derive(Args, Debug)]
361pub struct PoolsArgs {
362 /// Base URL of the node's admin HTTP surface.
363 ///
364 /// Also read from `DECDN_ADMIN_URL` when unset; clap folds the env
365 /// var into this field. If still unset, the admin port is derived
366 /// from `observability.admin_port` in the config file (see
367 /// `--config`). Example: `http://127.0.0.1:9191`.
368 #[arg(long, value_name = "URL", env = "DECDN_ADMIN_URL")]
369 pub admin_url: Option<String>,
370
371 /// Path to the TOML config file used to derive the admin URL when
372 /// `--admin-url` / `DECDN_ADMIN_URL` are unset. Takes precedence
373 /// over the top-level `decdn --config`; if neither is set,
374 /// resolution falls through to `~/.decdn/node.toml` and then the
375 /// built-in default port.
376 #[arg(long, value_name = "PATH")]
377 pub config: Option<PathBuf>,
378
379 /// Emit the admin response body as JSON instead of the human-readable
380 /// summary + pool table.
381 #[arg(long)]
382 pub json: bool,
383
384 /// Roundtrip timeout in milliseconds.
385 #[arg(long, value_name = "MS", default_value_t = 5_000)]
386 pub timeout_ms: u64,
387}
388
389/// `decdn node evict` — forcibly remove a blob from the local cache via
390/// `admin_v1_evict` (issue #279).
391///
392/// The eviction is *logical*: the iroh-blobs store still holds the bytes
393/// until the next periodic GC sweep reclaims them (#518; cadence controlled
394/// by `cache.gc_interval_sec`). The takedown is persisted to
395/// `<cache_dir>/evicted.log` so it survives restarts even if the next sweep
396/// hasn't run yet.
397/// Operators using this for DMCA takedowns can rely on the takedown
398/// being durable across `decdn-node run` invocations.
399///
400/// Pass `--dry-run` (issue #379) to preview what the evict would touch
401/// — blob size, last-access elapsed time, pin status, and already-
402/// evicted flag — without mutating any cache state. Useful as a
403/// pre-flight check before a DMCA takedown so operators can confirm
404/// the right blob and notice cases like "this hash is pinned" or
405/// "this is an idempotent re-run".
406#[derive(Args, Debug)]
407pub struct EvictArgs {
408 /// BLAKE3 hash of the blob to evict, encoded as 64 hex characters.
409 /// Optional `0x` / `0X` prefix is tolerated; mixed case is accepted.
410 /// Operators paste this from access logs / takedown notices.
411 #[arg(value_name = "HASH")]
412 pub hash: String,
413
414 /// Report what *would* be evicted (blob size, last-access elapsed
415 /// time, pin status, already-evicted flag) without mutating cache
416 /// state (issue #379). Use this as a pre-flight check before
417 /// running the real takedown — operators want to confirm the right
418 /// blob and spot cases like "this hash is pinned" or "this is an
419 /// idempotent re-run" before committing to the durable eviction.
420 #[arg(long)]
421 pub dry_run: bool,
422
423 /// Base URL of the node's admin HTTP surface. See `health --admin-url`
424 /// for resolution precedence (flag → env → config → default).
425 #[arg(long, value_name = "URL", env = "DECDN_ADMIN_URL")]
426 pub admin_url: Option<String>,
427
428 /// Path to the TOML config file used to derive the admin URL when
429 /// `--admin-url` / `DECDN_ADMIN_URL` are unset.
430 #[arg(long, value_name = "PATH")]
431 pub config: Option<PathBuf>,
432
433 /// Emit the admin response body as JSON instead of a human-readable line.
434 #[arg(long)]
435 pub json: bool,
436
437 /// Roundtrip timeout in milliseconds.
438 #[arg(long, value_name = "MS", default_value_t = 5_000)]
439 pub timeout_ms: u64,
440}
441
442/// `decdn node reload` — re-read the running node's config file via
443/// `admin_v1_reload` (issue #373) and print the post-reload `log_level`.
444#[derive(Args, Debug)]
445pub struct ReloadArgs {
446 /// Base URL of the node's admin HTTP surface. See `health --admin-url`
447 /// for resolution precedence (flag → env → config → default).
448 #[arg(long, value_name = "URL", env = "DECDN_ADMIN_URL")]
449 pub admin_url: Option<String>,
450
451 /// Path to the TOML config file used to derive the admin URL when
452 /// `--admin-url` / `DECDN_ADMIN_URL` are unset. Note: this argument
453 /// only locates the *running node's admin port*; the node re-reads
454 /// the path it was started with, not this one — passing a different
455 /// file here will not redirect the reload.
456 #[arg(long, value_name = "PATH")]
457 pub config: Option<PathBuf>,
458
459 /// Emit the admin response as JSON instead of the `log_level=` line.
460 #[arg(long)]
461 pub json: bool,
462
463 /// Roundtrip timeout in milliseconds.
464 #[arg(long, value_name = "MS", default_value_t = 5_000)]
465 pub timeout_ms: u64,
466}
467
468/// `decdn node drain` — trigger graceful shutdown of the running node via
469/// `admin_v1_drain` (issue #244, `adr/appendix-local-admin-http.md`). Fires the same runtime shutdown
470/// path as SIGTERM without needing the process PID.
471///
472/// Without `--wait`, the command is fire-and-forget: it returns
473/// `drain_initiated=true` as soon as the trigger lands and the admin
474/// server begins its early-stop sequence (metrics, then admin, both
475/// before `router.shutdown`). The connection will close before the
476/// node fully exits — observe completion via process exit
477/// (systemd/K8s) or `decdn node health` until ECONNREFUSED.
478///
479/// Pass `--wait` (issue #604) to opt the runtime into keeping the
480/// admin server alive *through* `router.shutdown` and have the CLI
481/// poll `admin_v1_health.in_flight_streams` until it reaches 0 (or
482/// the wait budget expires). On a clean drain the command returns
483/// success once the count is zero; on overrun it prints
484/// `drain_timeout=true in_flight_streams=N` to stderr and exits
485/// non-zero so operator scripts can fail closed during a stuck
486/// rolling upgrade.
487#[derive(Args, Debug)]
488pub struct DrainArgs {
489 /// Base URL of the node's admin HTTP surface. See `health --admin-url`
490 /// for resolution precedence (flag → env → config → default).
491 #[arg(long, value_name = "URL", env = "DECDN_ADMIN_URL")]
492 pub admin_url: Option<String>,
493
494 /// Path to the TOML config file used to derive the admin URL when
495 /// `--admin-url` / `DECDN_ADMIN_URL` are unset.
496 #[arg(long, value_name = "PATH")]
497 pub config: Option<PathBuf>,
498
499 /// Emit the admin response as JSON instead of a one-line confirmation.
500 #[arg(long)]
501 pub json: bool,
502
503 /// Roundtrip timeout in milliseconds for each individual RPC
504 /// (the initial `drain` call and each poll under `--wait`).
505 #[arg(long, value_name = "MS", default_value_t = 5_000)]
506 pub timeout_ms: u64,
507
508 /// Block until all in-flight client streams complete (or the wait
509 /// budget expires) before returning. Opt-in seam for #604: when set,
510 /// the server keeps admin alive through `router.shutdown` so the CLI
511 /// can poll `admin_v1_health.in_flight_streams` to observe
512 /// completion. Without it, the original SIGTERM-equivalent
513 /// ordering applies.
514 #[arg(long)]
515 pub wait: bool,
516
517 /// Wall-clock budget (seconds) for the `--wait` polling loop. On
518 /// overrun the CLI exits non-zero and prints `drain_timeout=true`.
519 /// Ignored without `--wait`. Default 30s covers the runtime's 15s
520 /// `SHUTDOWN_DEADLINE` plus typical settle time. `NonZeroU64` so
521 /// clap rejects `0` at parse time — a zero-second budget would
522 /// deadline-overrun on the first iteration with no signal of why.
523 #[arg(long, value_name = "SECS", default_value_t = DEFAULT_WAIT_TIMEOUT_SECS)]
524 pub wait_timeout_secs: NonZeroU64,
525
526 /// Cadence (milliseconds) at which `--wait` polls
527 /// `admin_v1_health`. Lower values converge faster on short
528 /// drains; higher values reduce admin churn on long ones. Ignored
529 /// without `--wait`. `NonZeroU64` so clap rejects `0` at parse
530 /// time — a zero-millisecond poll interval would spin a busy loop
531 /// against the loopback admin port.
532 #[arg(long, value_name = "MS", default_value_t = DEFAULT_WAIT_POLL_MS)]
533 pub wait_poll_ms: NonZeroU64,
534}
535
536/// Shared blockchain coordinates + keys for the on-chain `node` subcommands
537/// (`register`, `bond`). Flattened into each command's args so they expose
538/// an identical flag group. The common coordinates live in
539/// [`CommonChainArgs`]; this struct adds the `CapacityBond` address. Each field
540/// is taken from a flag when present, otherwise the `[blockchain]` /
541/// `[identity]` tables of the TOML config (same file the daemon reads).
542/// `rpc_url` and `capacity_bond_address` are required (no default — the command
543/// errors if neither flag nor config supplies them); `chain_id`, `keystore`,
544/// and `data_dir` fall back to built-in defaults.
545#[derive(Args, Debug)]
546pub struct ChainArgs {
547 /// RPC endpoint, chain id, keystore, and data-dir flags shared by every
548 /// chain-touching command.
549 #[command(flatten)]
550 pub common: CommonChainArgs,
551
552 /// `CapacityBond` contract address. Overrides
553 /// `blockchain.capacity_bond_address`.
554 #[arg(long, value_name = "ADDR")]
555 pub capacity_bond_address: Option<String>,
556}
557
558/// `decdn node register` — submit `CapacityBond.registerNode` (ADR 019
559/// § Step 2.3). Performs an on-chain transaction rather than talking to a
560/// running node's admin RPC.
561#[derive(Args, Debug)]
562pub struct RegisterArgs {
563 /// ISO 3166-1 alpha-2 country code submitted as the on-chain
564 /// `regionHint` (ADR 030). Self-reported, accepted at face value.
565 #[arg(long, value_name = "CODE")]
566 pub region: String,
567
568 /// QUIC multiaddr to register, e.g.
569 /// `/ip4/203.0.113.10/udp/4433/quic-v1`. Repeatable. The node listens on
570 /// the same port over IPv4 and IPv6, so a dual-stack host also registers
571 /// `/ip6/2001:db8::10/udp/4433/quic-v1`. Omitted, it defaults to the
572 /// host's public address on `network.bind_port` when the default route
573 /// has one. A host behind NAT registers none, and peers reach it through
574 /// the relay; pass the forwarded address here if UDP `network.bind_port`
575 /// is forwarded, or add it later with `decdn node update-multiaddrs`.
576 #[arg(long = "multiaddr", value_name = "MA")]
577 pub multiaddrs: Vec<String>,
578
579 /// Accept the network's current operator terms non-interactively (ADR 019
580 /// § Terms Acceptance). On a terminal the terms are shown and confirmed
581 /// interactively; in automation/headless contexts this flag records the
582 /// operator's acceptance (registration will not proceed without it).
583 #[arg(long = "accept-terms")]
584 pub accept_terms: bool,
585
586 /// Chain coordinates: RPC endpoint, contract addresses, and keystore.
587 #[command(flatten)]
588 pub chain: ChainArgs,
589}
590
591/// Which of the operator's keys `decdn node rotate-key` rotates.
592///
593/// A `ValueEnum` with **no default**: the two paths differ by orders of
594/// magnitude in cost and are not interchangeable, so the operator names one.
595/// These are the operator's only two keys. There is no separate slash-sig
596/// signing key: an operator serves from the EOA it registered, so `slash_sig`
597/// is signed by the Ethereum key above. Serving from a smart account while a
598/// hot key signs `slash_sig` is unimplemented
599/// (`adr/024-account-abstraction.md` § Unimplemented — Operator Custody While
600/// Serving), so there is no third variant to rotate.
601#[derive(Clone, Copy, Debug, PartialEq, Eq, clap::ValueEnum)]
602pub enum RotateKeyTarget {
603 /// The iroh Ed25519 node key — the wire `NodeId`. One `bindNodeId`
604 /// transaction; the Ethereum address, bond, declared tier,
605 /// `firstBondedAt`, and every lane funded against this operator survive untouched.
606 #[value(name = "iroh")]
607 Iroh,
608 /// The secp256k1 Ethereum signing key. No rebinding API exists for the
609 /// on-chain address, so this moves the whole identity: deregister →
610 /// unbond window → re-bond and re-register from a new address. Costs the
611 /// full unbonding window of downtime and resets `firstBondedAt`.
612 #[value(name = "eth")]
613 Eth,
614}
615
616/// `decdn node rotate-key` — the operator key-rotation runbook
617/// (`adr/appendix-operator-key-rotation.md`) as a command (#1034).
618///
619/// Rotation is the one operator procedure where a half-completed run is worse
620/// than no run: a node whose local key is not the on-chain-bound one is
621/// **un-slashable**, which is a protocol-level fault, not merely an outage. So
622/// both paths order their steps to make that state unreachable — the iroh path
623/// commits `node.secret` only after `bindNodeId` confirms, and the eth path
624/// reads chain state to pick its next phase rather than trusting a flag.
625///
626/// Neither path restarts the daemon. The node key is not hot-reloadable
627/// (`admin_v1_reload` and SIGHUP cover mutable config fields — see the
628/// node's `runtime/reload.rs` for the set — not the iroh endpoint), so the
629/// runbook's drain → stop → rotate → restart sequence stays the operator's
630/// to drive, with `decdn node drain` for the first step.
631#[derive(Args, Debug)]
632pub struct RotateKeyArgs {
633 /// Which key to rotate: `iroh` or `eth`. Required, because rotating the
634 /// wrong one is not recoverable in the same afternoon.
635 #[arg(long = "key", value_name = "KEY", value_enum)]
636 pub key: RotateKeyTarget,
637
638 /// (`--key iroh`) Bind the node key ALREADY ON DISK instead of
639 /// generating a fresh one.
640 ///
641 /// Two jobs, both from the runbook's § Failure modes and rollback: repair a
642 /// node whose `node.secret` was replaced by hand (leaving it un-slashable),
643 /// and roll back a rotation by re-binding a restored `.bak` archive. Since
644 /// nothing is generated, nothing is archived — this only submits the
645 /// transaction.
646 #[arg(long = "bind-existing")]
647 pub bind_existing: bool,
648
649 /// (`--key eth`) Keystore holding the NEW Ethereum key to migrate onto.
650 /// Required to reach the re-onboarding phase; the earlier phases
651 /// (deregister, unbond request, window wait, withdraw) all run against the
652 /// current keystore and do not need it. Passing it earlier is still useful:
653 /// the address is validated against the one being migrated away from on
654 /// every invocation, so a same-address mistake is caught on the first run
655 /// rather than after the unbonding window.
656 ///
657 /// Both keystores are unlocked from the same password source
658 /// (`DECDN_KEYSTORE_PASSWORD`, then `--keystore-password-file`, then a
659 /// prompt), so a headless migration needs them to share a password.
660 #[arg(long = "new-keystore", value_name = "PATH")]
661 pub new_keystore: Option<PathBuf>,
662
663 /// (`--key eth`) Capacity tier to re-declare on the new address. Required
664 /// at the re-onboarding phase: `deregisterNode` clears the declared tier and
665 /// it cannot be read back afterwards, so record the value the deregister
666 /// phase prints and pass it here.
667 #[arg(long = "mbps", value_name = "MBPS")]
668 pub mbps: Option<u64>,
669
670 /// (`--key eth`) `regionHint` for the re-registration. Defaults to the old
671 /// address's registered region.
672 #[arg(long, value_name = "CODE")]
673 pub region: Option<String>,
674
675 /// (`--key eth`) QUIC multiaddr for the re-registration. Repeatable.
676 /// Defaults to the multiaddrs the old address registered.
677 #[arg(long = "multiaddr", value_name = "MA")]
678 pub multiaddrs: Vec<String>,
679
680 /// (`--key eth`) Accept the network's current operator terms
681 /// non-interactively. Re-registration from a new address is a fresh
682 /// registration, so it re-accepts terms (ADR 019 § Terms Acceptance);
683 /// `--key iroh` never does, because rebinding signs the terms-free
684 /// `BindNodeId` payload.
685 #[arg(long = "accept-terms")]
686 pub accept_terms: bool,
687
688 /// Skip the interactive confirmation. Required from a non-interactive
689 /// shell. The consequences are printed either way — this suppresses the
690 /// prompt, not the warning.
691 #[arg(long = "yes", short = 'y')]
692 pub yes: bool,
693
694 // `--dry-run` and `--json` come from the flattened `ChainArgs`
695 // (`CommonChainArgs`); declaring them here too would collide.
696 /// Chain coordinates: RPC endpoint, contract addresses, and keystore.
697 #[command(flatten)]
698 pub chain: ChainArgs,
699}
700
701/// `decdn node bond` — stake to a capacity tier via `CapacityBond.bond` +
702/// `declareMbps` (ADR 019 § Step 2.1–2.2). Idempotent: it tops the active
703/// bond up to `max(minBond, bondRequired(mbps))`, approving and bonding only
704/// the shortfall, so a re-run after a partial failure converges instead of
705/// over-bonding. A precondition for `decdn node register`.
706#[derive(Args, Debug)]
707pub struct BondArgs {
708 /// Declared serving capacity in Mbps. The TOKEN bond is computed on-chain
709 /// as `max(minBond, bondRequired(mbps))` — you do not pass a token amount.
710 /// Must fall within the governable `[minCapacityMbps, maxCapacityMbps]`
711 /// band or the on-chain `declareMbps` reverts.
712 #[arg(long, value_name = "MBPS")]
713 pub mbps: u64,
714
715 /// Chain coordinates: RPC endpoint, contract addresses, and keystore.
716 #[command(flatten)]
717 pub chain: ChainArgs,
718}
719
720/// `decdn node unbond` — lower the bond via `CapacityBond.requestUnbond` +
721/// `unbond` (ADR 026 § Capacity-bond curve). One command covers all three
722/// phases of the window; which one runs is read from chain state, not from a
723/// flag. A run that lost its `requestUnbond` after `declareMbps` landed resumes
724/// on re-run, the way [`BondArgs`] converges; once `requestUnbond` HAS landed a
725/// re-run with an amount flag is a deliberate error, since the request can no
726/// longer be changed.
727///
728/// The amount flags apply only when starting a request — passing one while a
729/// request is already in flight is an error rather than a silent no-op. All
730/// three are mutually exclusive; exactly one is required to start a request.
731#[derive(Args, Debug)]
732#[command(group(ArgGroup::new("unbond_amount").args(["to_mbps", "all", "amount"]).multiple(false)))]
733pub struct UnbondArgs {
734 /// Reduce the declared capacity tier to MBPS and release the surplus
735 /// bond. The released amount is derived from the on-chain
736 /// `bondRequired` curve — you do not pass a token amount. The retained
737 /// bond is `max(minBond, bondRequired(MBPS))`, so the node stays
738 /// eligible at the new tier. Must not exceed the current declared tier
739 /// (raise with `decdn node bond --mbps`); passing the tier the node is
740 /// already at is accepted and skips the redundant `declareMbps`, which is
741 /// how a partially-failed run resumes. A tier that will actually be
742 /// declared must be inside the governable
743 /// `[minCapacityMbps, maxCapacityMbps]` band.
744 #[arg(long = "to-mbps", value_name = "MBPS")]
745 pub to_mbps: Option<u64>,
746
747 /// Release everything the bond curve permits, retaining only
748 /// `bondRequired(declaredMbps)`. Unlike `--to-mbps` this ignores the
749 /// `minBond` floor, so at low declared tiers the retained bond can land
750 /// below `minBond` and the node stays inactive until it re-bonds. Whether
751 /// it does is tier-dependent; the command reports `below_min_bond` before
752 /// submitting.
753 ///
754 /// For an operator that is NOT in the registered set — ejected, or bonded
755 /// and declared but never registered — this additionally clears the
756 /// declared tier (`declareMbps(0)`), making it a full exit: nothing is
757 /// retained. That is the only route to the bond those operators have, since
758 /// `decdn node deregister` requires an active registration. A registered
759 /// node exits by running `decdn node deregister` first.
760 #[arg(long)]
761 pub all: bool,
762
763 /// Release exactly this many TOKEN base units (1 TOKEN = 1e18).
764 /// Escape hatch for an exact figure; the curve is still pre-checked, so
765 /// an amount the contract would reject fails with the `decdn node bond
766 /// --mbps` invocation needed to make it legal.
767 #[arg(long, value_name = "BASE_UNITS")]
768 pub amount: Option<u128>,
769
770 /// Skip the interactive confirmation. Required when *starting* a request
771 /// from a non-interactive shell, since that deactivates the node for the
772 /// full window; the withdrawal phase never prompts. The consequences are
773 /// printed either way — this suppresses the prompt, not the warning.
774 #[arg(long = "yes", short = 'y')]
775 pub yes: bool,
776
777 /// Chain coordinates: RPC endpoint, contract addresses, and keystore.
778 #[command(flatten)]
779 pub chain: ChainArgs,
780}
781
782/// `decdn node deregister` — leave the active set via
783/// `CapacityBond.deregisterNode` (ADR 003 § Node Registry, #1359).
784///
785/// Deliberately flagless beyond the confirmation gate: `deregisterNode()` takes
786/// no arguments and needs no EIP-712 or ed25519 signature — unlike
787/// [`RegisterArgs`], which builds both — so the operator signer is the whole
788/// input. There is no state-driven phase selection either, the way
789/// [`UnbondArgs`] has: deregistration is a single transaction that either
790/// applies or reverts `NodeNotActive`.
791///
792/// It is nonetheless at least as consequential as starting an unbonding window,
793/// which is why it carries the same `--yes` gate: it drops the node from the
794/// active set, bumps `registrationNonce[nodeId]` (invalidating any
795/// previously-signed registration signature), and clears the declared tier, so
796/// re-entry costs a fresh `declareMbps` + `registerNode`.
797#[derive(Args, Debug)]
798pub struct DeregisterArgs {
799 /// Skip the interactive confirmation. Required from a non-interactive
800 /// shell, since deregistration takes the node out of the active set
801 /// immediately. The consequences are printed either way — this suppresses
802 /// the prompt, not the warning.
803 #[arg(long = "yes", short = 'y')]
804 pub yes: bool,
805
806 /// Chain coordinates: RPC endpoint, contract addresses, and keystore.
807 #[command(flatten)]
808 pub chain: ChainArgs,
809}
810
811/// `decdn node update-multiaddrs` — (re)publish the operator's dialable QUIC
812/// addresses via `CapacityBond.updateMultiaddrs` (#1908). See
813/// [`NodeCommand::UpdateMultiaddrs`] for the full description. Performs an
814/// on-chain transaction rather than talking to a running node's admin RPC.
815#[derive(Args, Debug)]
816pub struct UpdateMultiaddrsArgs {
817 /// QUIC multiaddr to publish, e.g. `/ip4/203.0.113.10/udp/4433/quic-v1`,
818 /// plus `/ip6/2001:db8::10/udp/4433/quic-v1` on a dual-stack host.
819 /// Repeatable; at least one is required — the whole point of this command
820 /// is to advertise a non-empty set, so clearing addresses back to empty is
821 /// deliberately not offered here (it would re-create the relay-pinned
822 /// footgun). The set fully replaces whatever is on-chain (the contract
823 /// stores the field verbatim, it does not merge).
824 #[arg(long = "multiaddr", value_name = "MA", required = true)]
825 pub multiaddrs: Vec<String>,
826
827 /// Chain coordinates: RPC endpoint, contract addresses, and keystore.
828 #[command(flatten)]
829 pub chain: ChainArgs,
830}
831
832/// `decdn node update-region` — (re)attest the operator's region via
833/// `CapacityBond.updateRegion` (ADR 030). See [`NodeCommand::UpdateRegion`] for
834/// the full description. Performs an on-chain transaction rather than talking to
835/// a running node's admin RPC.
836#[derive(Args, Debug)]
837pub struct UpdateRegionArgs {
838 /// New region to attest, as an ISO 3166-1 alpha-2 country code (e.g. `DE`).
839 /// Validated CLI-side and normalized (trimmed, uppercased) before the send —
840 /// the contract only length-checks, so an unvalidated code would be stored
841 /// verbatim and read back as "no locality information" by every downstream
842 /// consumer. The value fully replaces the on-chain `regionHint`.
843 #[arg(long = "region", value_name = "CODE")]
844 pub region: String,
845
846 /// Chain coordinates: RPC endpoint, contract addresses, and keystore.
847 #[command(flatten)]
848 pub chain: ChainArgs,
849}
850
851/// `decdn node lookup` — unpaid client-side discovery of active nodes via
852/// `CapacityBond.getRegisteredNodes` (#1481). See [`NodeCommand::Lookup`] for the
853/// full description.
854#[derive(Args, Debug)]
855pub struct LookupArgs {
856 /// Filter to the single node with this exact iroh node id (the same form
857 /// `decdn probe --node-id` accepts). Combinable with `--region`; with
858 /// neither flag every active node is listed.
859 #[arg(long = "node-id", value_name = "ID")]
860 pub node_id: Option<String>,
861
862 /// Filter to nodes whose self-attested region hint (ADR 030) matches this
863 /// ISO 3166-1 alpha-2 code exactly (case-insensitive — normalized the
864 /// same way as `decdn node register --region`).
865 #[arg(long, value_name = "CODE")]
866 pub region: Option<String>,
867
868 /// Probe each matching candidate over `cdn/probe/v1` and sort ascending
869 /// by measured round-trip time. Candidates that don't answer are kept at
870 /// the end (rather than dropped) with no RTT. Without this flag, no
871 /// network probing happens and candidates carry no RTT.
872 #[arg(long)]
873 pub probe: bool,
874
875 /// Roundtrip timeout in milliseconds for each `--probe` probe. Ignored
876 /// without `--probe`.
877 #[arg(long, value_name = "MS", default_value_t = 5_000)]
878 pub timeout_ms: u64,
879
880 // `--json` is supplied by the flattened `ChainArgs` (`CommonChainArgs.json`);
881 // declaring it here too would collide (clap requires unique arg names).
882 /// Chain coordinates: RPC endpoint, contract addresses, and keystore.
883 #[command(flatten)]
884 pub chain: ChainArgs,
885}
886
887/// `decdn node doctor` arguments.
888#[derive(Args, Debug)]
889pub struct DoctorArgs {
890 /// Config overrides, resolved exactly as `decdn-node run` would.
891 #[command(flatten)]
892 pub run: crate::cli::run::RunArgs,
893
894 /// Emit the full report as JSON instead of the checklist.
895 #[arg(long)]
896 pub json: bool,
897
898 /// Treat warnings as failures for the exit code.
899 #[arg(long)]
900 pub strict: bool,
901
902 /// Skip all network and live-daemon probes (config + disk + files only).
903 #[arg(long)]
904 pub offline: bool,
905
906 /// Per-probe timeout for network checks, in milliseconds.
907 #[arg(long, value_name = "MS", default_value_t = 5000)]
908 pub timeout_ms: u64,
909
910 /// Admin RPC URL for live enrichment [default: from config / `DECDN_ADMIN_PORT`].
911 #[arg(long, env = "DECDN_ADMIN_URL", value_name = "URL")]
912 pub admin_url: Option<String>,
913}
914
915/// `decdn node top` — live metrics view (issue #275).
916///
917/// Polls the daemon's loopback `/metrics` HTTP endpoint every
918/// `--interval-ms` and redraws a fixed-column table with current
919/// gauges, per-second counter deltas, and a derived cache hit-rate.
920/// The metrics port is loopback-only by default
921/// (`observability.metrics_bind = 127.0.0.1`); operators who exposed
922/// it elsewhere should pass `--metrics-url`.
923#[derive(Args, Debug)]
924pub struct TopArgs {
925 /// Base URL of the node's metrics HTTP endpoint, e.g.
926 /// `http://127.0.0.1:9090`. Also read from `DECDN_METRICS_URL`
927 /// when unset. If still unset, the port is derived from
928 /// `observability.metrics_port` in the config file (see
929 /// `--config`); the host is always `127.0.0.1` because the
930 /// metrics endpoint is loopback-only by default.
931 #[arg(long, value_name = "URL", env = "DECDN_METRICS_URL")]
932 pub metrics_url: Option<String>,
933
934 /// Path to the TOML config file used to derive the metrics URL
935 /// when `--metrics-url` / `DECDN_METRICS_URL` are unset. Same
936 /// resolution semantics as `decdn node health --config`.
937 #[arg(long, value_name = "PATH")]
938 pub config: Option<PathBuf>,
939
940 /// Refresh interval in milliseconds. Default 1000 (one Hz, matches
941 /// the issue title). Values below 100ms are accepted but unlikely
942 /// to outpace the operator's terminal redraw budget.
943 #[arg(long, value_name = "MS", default_value_t = 1_000)]
944 pub interval_ms: u64,
945
946 /// Single-shot mode: fetch once, emit the snapshot as pretty JSON,
947 /// and exit. Useful for scripts or one-off captures. Mutually
948 /// exclusive with the live redraw loop (which runs forever in
949 /// plain mode until Ctrl-C).
950 #[arg(long)]
951 pub json: bool,
952
953 /// HTTP roundtrip timeout in milliseconds for each `/metrics`
954 /// scrape. A scrape that times out is logged and the loop
955 /// continues — the displayed counters keep their last value
956 /// rather than the table going blank on a single hiccup.
957 #[arg(long, value_name = "MS", default_value_t = 5_000)]
958 pub timeout_ms: u64,
959}