use std::path::PathBuf;
use clap::{Args, Parser, Subcommand};
pub const ROOT_GENESIS_LONG_HELP: &str = "\
Root genesis creates a 7-of-13 institutional root authority. Genesis DKG \
requires all 13 rostered certifiers to complete the ceremony; if any \
certifier fails, abort and restart with a new signed roster.
Certifier rules: keep private material offline, maintain an offline backup, \
never submit plaintext shares, encrypt round-two payloads per recipient, and \
run verify-bundle before trusting the result. Secret-producing commands require \
explicit unique --output files and refuse stdout.";
pub const DEFAULT_ROUND_TIMEOUT_MS: u64 = 5_000;
pub const MIN_ROUND_TIMEOUT_MS: u64 = 250;
pub const MAX_ROUND_TIMEOUT_MS: u64 = 300_000;
fn parse_round_timeout_ms(value: &str) -> Result<u64, String> {
let timeout_ms = value
.parse::<u64>()
.map_err(|error| format!("round timeout must be a millisecond integer: {error}"))?;
if !(MIN_ROUND_TIMEOUT_MS..=MAX_ROUND_TIMEOUT_MS).contains(&timeout_ms) {
return Err(format!(
"round timeout must be between {MIN_ROUND_TIMEOUT_MS} and {MAX_ROUND_TIMEOUT_MS} milliseconds"
));
}
Ok(timeout_ms)
}
#[derive(Parser)]
#[command(
name = "exochain",
about = "EXOCHAIN distributed constitutional governance node",
version,
propagate_version = true
)]
pub struct Cli {
#[command(subcommand)]
pub command: Command,
}
#[derive(Subcommand)]
pub enum Command {
Start {
#[arg(long, default_value = None)]
api_port: Option<u16>,
#[arg(long, default_value = "127.0.0.1")]
api_host: String,
#[arg(long, default_value = None)]
p2p_port: Option<u16>,
#[arg(
long,
default_value_t = DEFAULT_ROUND_TIMEOUT_MS,
value_parser = parse_round_timeout_ms
)]
round_timeout_ms: u64,
#[arg(long)]
data_dir: Option<PathBuf>,
#[arg(long, default_value_t = false)]
validator: bool,
#[arg(long, value_delimiter = ',')]
validators: Option<Vec<String>>,
#[arg(long = "validator-public-key", value_delimiter = ',')]
validator_public_keys: Option<Vec<String>>,
},
Join {
#[arg(long, required = true, num_args = 1..)]
seed: Vec<String>,
#[arg(long, default_value = None)]
api_port: Option<u16>,
#[arg(long, default_value = "127.0.0.1")]
api_host: String,
#[arg(long, default_value = None)]
p2p_port: Option<u16>,
#[arg(
long,
default_value_t = DEFAULT_ROUND_TIMEOUT_MS,
value_parser = parse_round_timeout_ms
)]
round_timeout_ms: u64,
#[arg(long)]
data_dir: Option<PathBuf>,
#[arg(long, default_value_t = false)]
validator: bool,
#[arg(long, value_delimiter = ',')]
validators: Option<Vec<String>>,
#[arg(long = "validator-public-key", value_delimiter = ',')]
validator_public_keys: Option<Vec<String>>,
},
Status {
#[arg(long)]
data_dir: Option<PathBuf>,
},
Peers {
#[arg(long)]
data_dir: Option<PathBuf>,
},
Mcp {
#[arg(long)]
data_dir: Option<PathBuf>,
#[arg(long)]
actor_did: Option<String>,
#[arg(long)]
sse: Option<String>,
},
Genesis {
#[command(subcommand)]
command: GenesisCommand,
},
}
#[derive(Subcommand)]
#[command(after_long_help = ROOT_GENESIS_LONG_HELP)]
pub enum GenesisCommand {
Certifier {
#[command(subcommand)]
command: GenesisCertifierCommand,
},
Ceremony {
#[command(subcommand)]
command: GenesisCeremonyCommand,
},
Portal(GenesisPortalArgs),
Round1(GenesisIoArgs),
Round2(GenesisIoArgs),
#[command(name = "finalize-dkg")]
FinalizeDkg(GenesisIoArgs),
#[command(name = "build-final-key-confirmation")]
BuildFinalKeyConfirmation(GenesisIoArgs),
#[command(name = "sign-root-artifact")]
SignRootArtifact(GenesisIoArgs),
#[command(name = "assemble-bundle")]
AssembleBundle(GenesisIoArgs),
#[command(name = "verify-bundle")]
VerifyBundle(GenesisIoArgs),
#[command(name = "seal-share")]
SealShare(GenesisIoArgs),
#[command(name = "unseal-share")]
UnsealShare(GenesisIoArgs),
#[command(name = "sign-envelope")]
SignEnvelope(GenesisSignEnvelopeArgs),
#[command(name = "encrypt-pairwise")]
EncryptPairwise(GenesisIoArgs),
#[command(name = "decrypt-pairwise")]
DecryptPairwise(GenesisIoArgs),
#[command(name = "emit-artifact-bytes")]
EmitArtifactBytes(GenesisIoArgs),
#[command(name = "submit-envelope")]
SubmitEnvelope(GenesisSubmitEnvelopeArgs),
#[command(name = "pull-envelopes")]
PullEnvelopes(GenesisPullEnvelopesArgs),
#[command(name = "compute-dkg-transcript-hash")]
ComputeDkgTranscriptHash(GenesisIoArgs),
#[command(name = "compute-final-transcript-hash")]
ComputeFinalTranscriptHash(GenesisIoArgs),
#[command(name = "encode-encrypted-payload")]
EncodeEncryptedPayload(GenesisIoArgs),
#[command(name = "decode-encrypted-payload")]
DecodeEncryptedPayload(GenesisIoArgs),
#[command(name = "sign-commit")]
SignCommit(GenesisSignCommitArgs),
#[command(name = "build-signing-package")]
BuildSigningPackage(GenesisIoArgs),
#[command(name = "sign-share")]
SignShare(GenesisSignShareArgs),
#[command(name = "aggregate-signature")]
AggregateSignature(GenesisIoArgs),
}
#[derive(Subcommand)]
pub enum GenesisCertifierCommand {
Init(GenesisCertifierInitArgs),
}
#[derive(Subcommand)]
pub enum GenesisCeremonyCommand {
Init(GenesisCeremonyInitArgs),
}
#[derive(Args)]
pub struct GenesisCertifierInitArgs {
#[arg(long)]
pub did: String,
#[arg(long)]
pub frost_identifier: u16,
#[arg(long)]
pub certifier_out: PathBuf,
#[arg(long)]
pub private_out: PathBuf,
}
#[derive(Args)]
pub struct GenesisCeremonyInitArgs {
#[arg(long)]
pub ceremony_id: String,
#[arg(long)]
pub network_id: String,
#[arg(long)]
pub repo_commit: String,
#[arg(long)]
pub constitution_hash: String,
#[arg(long)]
pub created_physical_ms: u64,
#[arg(long)]
pub roster: PathBuf,
#[arg(long, value_delimiter = ',')]
pub signing_set: Vec<u16>,
#[arg(long)]
pub out: PathBuf,
}
#[derive(Args)]
pub struct GenesisPortalArgs {
#[arg(long)]
pub config: PathBuf,
#[arg(long, default_value = "127.0.0.1:3017")]
pub bind: String,
}
#[derive(Args)]
pub struct GenesisIoArgs {
#[arg(long)]
pub input: Option<PathBuf>,
#[arg(long)]
pub output: Option<PathBuf>,
}
#[derive(Args)]
pub struct GenesisSignEnvelopeArgs {
#[arg(long)]
pub input: Option<PathBuf>,
#[arg(long)]
pub output: Option<PathBuf>,
#[arg(long)]
pub private_input: PathBuf,
}
#[derive(Args)]
pub struct GenesisPullEnvelopesArgs {
#[arg(long)]
pub portal_url: String,
#[arg(long)]
pub phase: Option<String>,
#[arg(long)]
pub payload_kind: Option<String>,
#[arg(long)]
pub recipient_did: Option<String>,
#[arg(long)]
pub output: Option<PathBuf>,
}
#[derive(Args)]
pub struct GenesisSubmitEnvelopeArgs {
#[arg(long)]
pub portal_url: String,
#[arg(long)]
pub input: Option<PathBuf>,
}
#[derive(Args)]
pub struct GenesisSignCommitArgs {
#[arg(long)]
pub input: Option<PathBuf>,
#[arg(long)]
pub commitment_out: PathBuf,
#[arg(long)]
pub nonces_out: PathBuf,
}
#[derive(Args)]
pub struct GenesisSignShareArgs {
#[arg(long)]
pub input: Option<PathBuf>,
#[arg(long)]
pub nonces: PathBuf,
#[arg(long)]
pub output: Option<PathBuf>,
}
#[cfg(test)]
mod tests {
use clap::CommandFactory;
use super::Cli;
fn long_help_for(path: &[&str]) -> String {
let mut command = Cli::command();
let mut current = &mut command;
for segment in path {
current = current
.find_subcommand_mut(segment)
.expect("subcommand should exist");
}
let mut help = Vec::new();
current
.write_long_help(&mut help)
.expect("help should render");
String::from_utf8(help).expect("help should be utf8")
}
#[test]
fn genesis_cli_exposes_complete_operator_command_set() {
let help = long_help_for(&["genesis"]);
for command in [
"certifier",
"ceremony",
"portal",
"round1",
"round2",
"finalize-dkg",
"build-final-key-confirmation",
"sign-root-artifact",
"assemble-bundle",
"verify-bundle",
"seal-share",
"unseal-share",
"sign-envelope",
"encrypt-pairwise",
"decrypt-pairwise",
"emit-artifact-bytes",
"submit-envelope",
"pull-envelopes",
"compute-dkg-transcript-hash",
"compute-final-transcript-hash",
"encode-encrypted-payload",
"decode-encrypted-payload",
"sign-commit",
"build-signing-package",
"sign-share",
"aggregate-signature",
] {
assert!(help.contains(command), "missing genesis command {command}");
}
}
#[test]
fn genesis_build_round1_attestation_command_is_absent_pending_ratification() {
let help = long_help_for(&["genesis"]);
assert!(!help.contains("build-round1-attestation"));
}
#[test]
fn genesis_sign_envelope_takes_no_signing_secret_through_argv() {
use clap::Parser;
let with_file = Cli::try_parse_from([
"exochain",
"genesis",
"sign-envelope",
"--input",
"draft.json",
"--private-input",
"certifier-01.private.json",
"--output",
"envelope.json",
]);
assert!(
with_file.is_ok(),
"private-material file path must be accepted"
);
let with_argv_secret = Cli::try_parse_from([
"exochain",
"genesis",
"sign-envelope",
"--input",
"draft.json",
"--signing-key-hex",
&"ab".repeat(32),
]);
assert!(
with_argv_secret.is_err(),
"no CLI argument may carry 32-byte signing-secret material"
);
}
#[test]
fn genesis_cli_help_warns_certifiers_about_secret_handling_and_restart_rules() {
let help = long_help_for(&["genesis"]);
for required in [
"7-of-13",
"all 13 rostered certifiers",
"abort and restart",
"offline backup",
"never submit plaintext shares",
"explicit unique --output files",
"verify-bundle",
] {
assert!(help.contains(required), "missing help text {required}");
}
}
}