Skip to main content

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}