Skip to main content

tempo/
tempo_cmd.rs

1use std::{
2    fs::OpenOptions,
3    io::Write as _,
4    net::{IpAddr, SocketAddr},
5    path::{Path, PathBuf},
6    str::FromStr,
7    sync::Arc,
8};
9
10use alloy::hex::ToHexExt;
11use alloy_network::EthereumWallet;
12use alloy_primitives::{Address, B256, Bytes};
13use alloy_provider::{Provider, ProviderBuilder};
14use alloy_rpc_types_eth::TransactionRequest;
15use alloy_signer_aws::{AwsSigner, aws_config, aws_sdk_kms};
16use alloy_signer_gcp::{GcpKeyRingRef, GcpSigner, KeySpecifier, gcloud_sdk};
17use alloy_signer_ledger::{HDPath as LedgerHDPath, LedgerSigner};
18use alloy_signer_local::PrivateKeySigner;
19use alloy_signer_trezor::{HDPath as TrezorHDPath, TrezorSigner};
20use alloy_sol_types::SolCall;
21use clap::Subcommand;
22use commonware_codec::{DecodeExt as _, Encode as _, ReadExt as _};
23use commonware_consensus::types::{Epocher as _, FixedEpocher, Height};
24use commonware_cryptography::{
25    Signer as _,
26    ed25519::{PrivateKey, PublicKey},
27};
28use commonware_math::algebra::Random as _;
29use commonware_utils::{NZU64, ordered};
30use eyre::{OptionExt as _, Report, WrapErr as _, bail, eyre};
31use reth_chainspec::EthChainSpec;
32use reth_cli_runner::CliRunner;
33use reth_ethereum_cli::ExtendedCommand;
34use serde::Serialize;
35use tempo_alloy::TempoNetwork;
36use tempo_chainspec::spec::{TempoChainSpec, TempoChainSpecParser};
37use tempo_consensus_config::{SigningKey, SigningKeyPassphrase};
38use tempo_contracts::precompiles::{
39    IValidatorConfigV2::{self, Validator},
40    VALIDATOR_CONFIG_V2_ADDRESS,
41};
42use tempo_dkg_onchain_artifacts::OnchainDkgOutcome;
43use tempo_precompiles::validator_config_v2::{VALIDATOR_NS_ADD, VALIDATOR_NS_ROTATE};
44use tempo_validator_config::ValidatorConfig;
45
46use crate::{init_state, p2p_proxy::P2pProxyArgs, regenesis};
47
48fn get_env(key: &str) -> eyre::Result<String> {
49    std::env::var(key).wrap_err_with(|| format!("failed reading environment variable `{key}`"))
50}
51
52/// Passthrough args for extension management commands.
53///
54/// These commands are defined here so they appear in `tempo --help`, but
55/// the actual implementation lives in `tempo_ext::run()`. We capture all
56/// trailing arguments and re-dispatch.
57#[derive(Debug, clap::Args)]
58pub struct ExtArgs {
59    #[arg(trailing_var_arg = true, allow_hyphen_values = true, hide = true)]
60    args: Vec<String>,
61}
62
63/// Tempo-specific subcommands that extend the reth CLI.
64#[derive(Debug, Subcommand)]
65pub enum TempoSubcommand {
66    /// Consensus-related commands.
67    #[command(subcommand)]
68    Consensus(ConsensusSubcommand),
69
70    /// Run a proxy P2P node that serves cached block data fetched from an RPC endpoint.
71    P2pProxy(P2pProxyArgs),
72
73    /// Initialize state from a binary dump file.
74    ///
75    /// Loads TIP20 storage slots from a binary file generated by `tempo-xtask generate-state-bloat`
76    /// and applies them to the genesis state.
77    InitFromBinaryDump(Box<init_state::InitFromBinaryDump<TempoChainSpecParser>>),
78
79    /// Patch a virgin block-0 database to use a new genesis header.
80    Regenesis(Box<regenesis::Regenesis<TempoChainSpecParser>>),
81
82    /// Install an extension (e.g., `tempo add wallet`).
83    #[command(
84        override_usage = "tempo add <EXT> [VERSION]",
85        after_help = "Examples:\n  tempo add wallet\n  tempo add wallet 0.2.0"
86    )]
87    Add(ExtArgs),
88
89    /// Update tempo and/or extensions.
90    #[command(
91        override_usage = "tempo update [EXT]",
92        after_help = "Examples:\n  tempo update          # update tempo + all extensions\n  tempo update wallet   # update a single extension"
93    )]
94    Update(ExtArgs),
95
96    /// Remove an extension.
97    #[command(
98        override_usage = "tempo remove <EXT>",
99        after_help = "Example: tempo remove wallet"
100    )]
101    Remove(ExtArgs),
102
103    /// List installed extensions.
104    #[command(override_usage = "tempo list")]
105    List(ExtArgs),
106}
107
108impl ExtendedCommand for TempoSubcommand {
109    fn execute(self, runner: CliRunner) -> eyre::Result<()> {
110        match self {
111            Self::Consensus(cmd) => {
112                runner.run_blocking_until_ctrl_c(cmd.run())?;
113                Ok(())
114            }
115            Self::P2pProxy(cmd) => runner.run_command_until_exit(|_| cmd.run()),
116            Self::InitFromBinaryDump(cmd) => {
117                let runtime = runner.runtime();
118                runner.run_blocking_until_ctrl_c(
119                    cmd.execute::<tempo_node::node::TempoNode>(runtime),
120                )?;
121                Ok(())
122            }
123            Self::Regenesis(cmd) => {
124                let runtime = runner.runtime();
125                runner.run_blocking_until_ctrl_c(
126                    cmd.execute::<tempo_node::node::TempoNode>(runtime),
127                )?;
128                Ok(())
129            }
130            Self::Add(_) | Self::Update(_) | Self::Remove(_) | Self::List(_) => {
131                let code = tempo_ext::run(std::env::args_os()).map_err(|e| eyre!("{e}"))?;
132                if code != 0 {
133                    std::process::exit(code);
134                }
135                Ok(())
136            }
137        }
138    }
139}
140
141#[derive(Debug, Subcommand)]
142pub enum ConsensusSubcommand {
143    /// Add a new validator to the validator config contract.
144    AddValidator(AddValidator),
145    /// Shows the verification key for an ed25519 signing key.
146    #[command(alias = "calculate-public-key")]
147    ShowVerificationKey(ShowVerificationKey),
148    /// Create an ed25519 signature for `addValidator`.
149    CreateAddValidatorSignature(CreateAddValidatorSignatureArgs),
150    /// Create an ed25519 signature for `rotateValidator`.
151    CreateRotateValidatorSignature(CreateRotateValidatorSignatureArgs),
152    /// Deactivate a validator
153    DeactivateValidator(DeactivateValidator),
154    /// Encrypt an existing ed25519 signing key using a passphrase.
155    EncryptSigningKey(EncryptSigningKey),
156    /// Generates an ed25519 signing key pair to be used in consensus.
157    #[command(alias = "generate-private-key")]
158    GenerateSigningKey(GenerateSigningKey),
159    /// Rotate a validator to a new identity.
160    RotateValidator(RotateValidator),
161    /// Set the validator Ip Address
162    SetValidatorIpAddress(SetValidatorIpAddress),
163    /// Set the validator fee recipient
164    SetValidatorFeeRecipient(SetValidatorFeeRecipient),
165    /// Transfer validator ownership
166    TransferValidatorOwnership(TransferValidatorOwnership),
167    /// Look up a validator by etheruem address, e25519 public key, or index.
168    Validator(ValidatorInfo),
169    /// Query current committee information from the previous epoch's DKG outcome and current contract state.
170    #[command(alias = "validators-info")]
171    Info(Info),
172}
173
174impl ConsensusSubcommand {
175    async fn run(self) -> eyre::Result<()> {
176        match self {
177            Self::AddValidator(args) => args.run().await,
178            Self::DeactivateValidator(args) => args.run().await,
179            Self::TransferValidatorOwnership(args) => args.run().await,
180            Self::RotateValidator(args) => args.run().await,
181            Self::CreateAddValidatorSignature(args) => args.run().await,
182            Self::CreateRotateValidatorSignature(args) => args.run().await,
183            Self::SetValidatorIpAddress(args) => args.run().await,
184            Self::SetValidatorFeeRecipient(args) => args.run().await,
185            Self::EncryptSigningKey(args) => args.run(),
186            Self::GenerateSigningKey(args) => args.run(),
187            Self::ShowVerificationKey(args) => args.run(),
188            Self::Validator(args) => args.run().await,
189            Self::Info(args) => args.run().await,
190        }
191    }
192}
193
194#[derive(Clone, Debug)]
195enum ValidatorId {
196    Address(Address),
197    Index(u64),
198    PublicKey(B256),
199}
200
201impl FromStr for ValidatorId {
202    type Err = eyre::Report;
203
204    fn from_str(s: &str) -> Result<Self, Self::Err> {
205        if let Ok(idx) = s.parse::<u64>() {
206            Ok(Self::Index(idx))
207        } else if let Ok(address) = s.parse::<Address>() {
208            Ok(Self::Address(address))
209        } else if let Ok(pubkey) = s.parse::<B256>() {
210            Ok(Self::PublicKey(pubkey))
211        } else {
212            Err(eyre!(
213                "validator identifier must be an index, ethereum address, or ed25519 public key"
214            ))
215        }
216    }
217}
218
219async fn read_validator_from_contract(
220    provider: &impl Provider<TempoNetwork>,
221    lookup: ValidatorId,
222) -> eyre::Result<Validator> {
223    let calldata: Vec<u8> = match lookup {
224        ValidatorId::Address(addr) => IValidatorConfigV2::validatorByAddressCall {
225            validatorAddress: addr,
226        }
227        .abi_encode(),
228        ValidatorId::Index(idx) => {
229            IValidatorConfigV2::validatorByIndexCall { index: idx }.abi_encode()
230        }
231        ValidatorId::PublicKey(pubkey) => {
232            IValidatorConfigV2::validatorByPublicKeyCall { publicKey: pubkey }.abi_encode()
233        }
234    };
235
236    let tx = TransactionRequest::default()
237        .to(VALIDATOR_CONFIG_V2_ADDRESS)
238        .input(calldata.into());
239
240    let resp = provider
241        .call(tx.into())
242        .await
243        .wrap_err("failed to read contract")?;
244
245    let validator = match lookup {
246        ValidatorId::Address(_) => {
247            IValidatorConfigV2::validatorByAddressCall::abi_decode_returns(&resp)
248        }
249        ValidatorId::Index(_) => {
250            IValidatorConfigV2::validatorByIndexCall::abi_decode_returns(&resp)
251        }
252        ValidatorId::PublicKey(_) => {
253            IValidatorConfigV2::validatorByPublicKeyCall::abi_decode_returns(&resp)
254        }
255    }
256    .wrap_err("failed to decode validator")?;
257
258    Ok(validator)
259}
260
261/// Shared validator identity arguments used across add/rotate/sign commands.
262#[derive(Debug, clap::Args)]
263pub struct ValidatorIdentityArgs {
264    /// The validator's Ethereum address
265    #[arg(long, value_name = "ETHEREUM_ADDRESS")]
266    validator_address: Address,
267    /// The validator's signing key address (0x-prefixed hex).
268    #[arg(
269        long = "consensus.public-key",
270        value_name = "IDENTITY_PUBLIC_KEY_ADDRESS"
271    )]
272    public_key: B256,
273    /// The inbound address for the validator.
274    #[arg(long, value_name = "IP:PORT")]
275    ingress: SocketAddr,
276    /// The outbound address for the validator.
277    #[arg(long, value_name = "IP")]
278    egress: IpAddr,
279}
280
281impl ValidatorIdentityArgs {
282    fn to_config(&self, chain_id: u64) -> ValidatorConfig {
283        ValidatorConfig {
284            chain_id,
285            validator_address: self.validator_address,
286            public_key: self.public_key,
287            ingress: self.ingress,
288            egress: self.egress,
289        }
290    }
291}
292
293/// Either a pre-computed signature or a signing key to compute it from.
294#[derive(Debug, clap::Args)]
295#[group(required = true, multiple = false, args = ["signature", "signing_key"])]
296pub struct ValidatorSignatureArgs {
297    /// A pre-computed ed25519 signature over the validator identity.
298    #[arg(long, value_name = "SIGNATURE")]
299    signature: Option<Bytes>,
300
301    #[command(flatten)]
302    signing_key: Option<SigningKeyArgs>,
303}
304
305impl ValidatorSignatureArgs {
306    fn resolve(self, namespace: &[u8], message: &B256) -> eyre::Result<Bytes> {
307        let Self {
308            signature,
309            signing_key,
310        } = self;
311
312        match (signature, signing_key) {
313            (Some(sig), _) => Ok(sig),
314            (None, Some(signing_key)) => {
315                let key = signing_key.read()?;
316                let private_key = key.into_inner();
317                let sig = private_key.sign(namespace, message.as_slice());
318                Ok(sig.encode().into())
319            }
320            (None, None) => unreachable!("clap requires either --signature or --signing-key"),
321        }
322    }
323}
324
325#[derive(Debug, clap::Args)]
326pub struct SigningKeyArgs {
327    /// Passphrase source for decrypting `--signing-key`.
328    ///
329    /// A FIFO path, including shell process substitution like `<(...)`, is preferred.
330    /// If omitted, the signing key is read as plaintext hex.
331    #[arg(long, alias = "consensus.secret", value_name = "PATH")]
332    secret: Option<PathBuf>,
333
334    /// Path to the ed25519 signing private key file.
335    ///
336    /// In commands that also accept `--signature`, the signature is computed
337    /// automatically so a separate `create-*-signature` step is not needed.
338    #[arg(long, alias = "consensus.signing-key", value_name = "FILE")]
339    signing_key: PathBuf,
340}
341
342impl SigningKeyArgs {
343    fn read(self) -> eyre::Result<SigningKey> {
344        let Self {
345            signing_key,
346            secret,
347        } = self;
348        read_signing_key(signing_key, secret)
349    }
350}
351
352#[derive(Debug, clap::Args)]
353pub struct WalletArgs {
354    /// Path to the file holding the validator's Ethereum private key.
355    #[arg(long, value_name = "FILE", help_heading = "Wallet options - raw")]
356    wallet_key: Option<PathBuf>,
357
358    /// Use a Ledger hardware wallet.
359    #[arg(long, help_heading = "Wallet options - hardware wallet")]
360    ledger: bool,
361
362    /// Use a Trezor hardware wallet.
363    #[arg(long, help_heading = "Wallet options - hardware wallet")]
364    trezor: bool,
365
366    /// Use AWS KMS. Requires AWS_KMS_KEY_ID env var
367    #[arg(long, help_heading = "Wallet options - remote")]
368    aws: bool,
369
370    /// Use GCP KMS. Requires GCP_PROJECT_ID, GCP_LOCATION, GCP_KEY_RING, GCP_KEY_NAME,
371    /// GCP_KEY_VERSION env vars
372    #[arg(long, help_heading = "Wallet options - remote")]
373    gcp: bool,
374}
375
376impl WalletArgs {
377    async fn build(&self) -> eyre::Result<EthereumWallet> {
378        if self.ledger {
379            let signer = LedgerSigner::new(LedgerHDPath::LedgerLive(0), None)
380                .await
381                .wrap_err("failed to connect to Ledger device")?;
382
383            Ok(EthereumWallet::new(signer))
384        } else if self.trezor {
385            let signer = TrezorSigner::new(TrezorHDPath::TrezorLive(0), None)
386                .await
387                .wrap_err("failed to connect to Trezor device")?;
388
389            Ok(EthereumWallet::new(signer))
390        } else if self.aws {
391            let key_id = get_env("AWS_KMS_KEY_ID")?;
392            let config = aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await;
393            let client = aws_sdk_kms::Client::new(&config);
394            let signer = AwsSigner::new(client, key_id, None)
395                .await
396                .wrap_err("failed to create AWS KMS signer")?;
397
398            Ok(EthereumWallet::new(signer))
399        } else if self.gcp {
400            let project = get_env("GCP_PROJECT_ID")?;
401            let location = get_env("GCP_LOCATION")?;
402            let keyring = get_env("GCP_KEY_RING")?;
403            let key_name = get_env("GCP_KEY_NAME")?;
404            let key_version: u64 = get_env("GCP_KEY_VERSION")?
405                .parse()
406                .wrap_err("GCP_KEY_VERSION must be a valid u64")?;
407
408            let keyring_ref = GcpKeyRingRef::new(&project, &location, &keyring);
409            let specifier = KeySpecifier::new(keyring_ref, &key_name, key_version);
410
411            let client = gcloud_sdk::GoogleApi::from_function(
412                gcloud_sdk::google::cloud::kms::v1::key_management_service_client::KeyManagementServiceClient::new,
413                "https://cloudkms.googleapis.com",
414                None,
415            )
416            .await
417            .wrap_err("failed to create GCP KMS client")?;
418
419            let signer = GcpSigner::new(client, specifier, None)
420                .await
421                .wrap_err("failed to create GCP KMS signer")?;
422
423            Ok(EthereumWallet::new(signer))
424        } else if let Some(path) = &self.wallet_key {
425            let signer = key_from_file(path).wrap_err_with(|| {
426                format!("failed reading private key from file `{}`", path.display())
427            })?;
428
429            Ok(EthereumWallet::new(signer))
430        } else {
431            bail!("no wallet provided")
432        }
433    }
434}
435
436/// Shared arguments for commands that update the validator config contract.
437#[derive(Debug, clap::Args)]
438pub struct ValidatorTransactionArgs {
439    #[command(flatten)]
440    wallet: WalletArgs,
441
442    /// The RPC URL to submit the transaction to.
443    #[arg(long, default_value = "https://rpc.presto.tempo.xyz")]
444    rpc_url: String,
445
446    /// Skip the interactive confirmation prompt.
447    #[arg(long, short = 'y')]
448    yes: bool,
449
450    /// Prints transaction and exits. Does not send sign or send the transaction.
451    #[arg(long)]
452    dry_run: bool,
453}
454
455impl ValidatorTransactionArgs {
456    async fn call<T: SolCall + Serialize>(&self, call: &T) -> eyre::Result<()> {
457        let tx = TransactionRequest::default()
458            .to(VALIDATOR_CONFIG_V2_ADDRESS)
459            .input(call.abi_encode().into());
460
461        let mut output: Box<dyn std::io::Write + Send> = if self.yes {
462            Box::new(std::io::stderr())
463        } else {
464            Box::new(std::io::stdout())
465        };
466
467        writeln!(output, "{}", serde_json::json!(tx))?;
468        if self.dry_run {
469            return Ok(());
470        }
471
472        if !self.yes {
473            write!(output, "\nSubmit this transaction? [y/N] ")?;
474            output.flush()?;
475
476            let mut input = String::new();
477            std::io::stdin().read_line(&mut input)?;
478
479            if !matches!(input.trim(), "y" | "Y" | "yes" | "YES") {
480                bail!("transaction cancelled by user")
481            }
482        }
483
484        let wallet = self
485            .wallet
486            .build()
487            .await
488            .wrap_err("failed to open wallet to send transaction")?;
489
490        let provider = ProviderBuilder::new_with_network::<TempoNetwork>()
491            .with_gas_estimation()
492            .wallet(wallet)
493            .connect(&self.rpc_url)
494            .await
495            .wrap_err("failed to connect to RPC")?;
496
497        let pending = provider
498            .send_transaction(tx.into())
499            .await
500            .wrap_err("failed to send transaction")?;
501
502        let tx_hash = pending.tx_hash();
503        writeln!(output, "{tx_hash}")?;
504
505        Ok(())
506    }
507
508    async fn provider(&self) -> eyre::Result<impl Provider<TempoNetwork>> {
509        let provider = ProviderBuilder::new_with_network::<TempoNetwork>()
510            .fetch_chain_id()
511            .connect(&self.rpc_url)
512            .await
513            .wrap_err("failed to connect to RPC")?;
514
515        Ok(provider)
516    }
517}
518
519#[derive(Debug, clap::Args)]
520pub struct AddValidator {
521    #[command(flatten)]
522    identity: ValidatorIdentityArgs,
523    #[command(flatten)]
524    sig: ValidatorSignatureArgs,
525
526    /// The fee recipient address
527    #[arg(long, value_name = "ETHEREUM_ADDRESS")]
528    fee_recipient: Address,
529
530    #[command(flatten)]
531    submit: ValidatorTransactionArgs,
532}
533
534impl AddValidator {
535    async fn run(self) -> eyre::Result<()> {
536        let provider = self.submit.provider().await?;
537
538        let chain_id = provider
539            .get_chain_id()
540            .await
541            .wrap_err("failed to get chain id")?;
542
543        let config = self.identity.to_config(chain_id);
544        let signature = self.sig.resolve(
545            VALIDATOR_NS_ADD,
546            &config.add_validator_message_hash(self.fee_recipient),
547        )?;
548
549        config
550            .check_add_validator_signature(self.fee_recipient, signature.as_ref())
551            .wrap_err("add-validator signature check failed")?;
552
553        let call = IValidatorConfigV2::addValidatorCall {
554            validatorAddress: self.identity.validator_address,
555            publicKey: self.identity.public_key,
556            ingress: self.identity.ingress.to_string(),
557            egress: self.identity.egress.to_string(),
558            signature,
559            feeRecipient: self.fee_recipient,
560        };
561
562        self.submit.call(&call).await?;
563        Ok(())
564    }
565}
566
567#[derive(Debug, clap::Args)]
568pub struct TransferValidatorOwnership {
569    /// Validator ethereum address, ed25519 public key, or index
570    #[arg()]
571    id: ValidatorId,
572    /// Path to the file holding the private key of the new validator address
573    #[arg(long, value_name = "FILE")]
574    new_private_key: PathBuf,
575
576    #[command(flatten)]
577    submit: ValidatorTransactionArgs,
578}
579
580impl TransferValidatorOwnership {
581    async fn run(self) -> eyre::Result<()> {
582        let provider = self.submit.provider().await?;
583
584        let new_signer = key_from_file(&self.new_private_key).wrap_err_with(|| {
585            format!(
586                "failed reading private key from file `{}`",
587                self.new_private_key.display()
588            )
589        })?;
590
591        let new_validator_address = new_signer.address();
592
593        let validator = read_validator_from_contract(&provider, self.id).await?;
594
595        let call = IValidatorConfigV2::transferValidatorOwnershipCall {
596            idx: validator.index,
597            newAddress: new_validator_address,
598        };
599
600        self.submit.call(&call).await?;
601        Ok(())
602    }
603}
604
605#[derive(Debug, clap::Args)]
606pub struct RotateValidator {
607    #[command(flatten)]
608    identity: ValidatorIdentityArgs,
609    #[command(flatten)]
610    sig: ValidatorSignatureArgs,
611    #[command(flatten)]
612    submit: ValidatorTransactionArgs,
613}
614
615impl RotateValidator {
616    async fn run(self) -> eyre::Result<()> {
617        let provider = self.submit.provider().await?;
618
619        let chain_id = provider
620            .get_chain_id()
621            .await
622            .wrap_err("failed to get chain id")?;
623
624        let config = self.identity.to_config(chain_id);
625
626        let signature = self
627            .sig
628            .resolve(VALIDATOR_NS_ROTATE, &config.rotate_validator_message_hash())?;
629
630        config
631            .check_rotate_validator_signature(signature.as_ref())
632            .wrap_err("rotate-validator signature check failed")?;
633
634        let lookup = ValidatorId::Address(self.identity.validator_address);
635        let validator = read_validator_from_contract(&provider, lookup).await?;
636
637        let call = IValidatorConfigV2::rotateValidatorCall {
638            idx: validator.index,
639            publicKey: self.identity.public_key,
640            ingress: self.identity.ingress.to_string(),
641            egress: self.identity.egress.to_string(),
642            signature,
643        };
644
645        self.submit.call(&call).await?;
646        Ok(())
647    }
648}
649
650#[derive(Debug, clap::Args)]
651#[group(required = true, args = ["signing_key"])]
652pub struct CreateAddValidatorSignatureArgs {
653    #[command(flatten)]
654    identity: ValidatorIdentityArgs,
655    /// The fee recipient address
656    #[arg(long, value_name = "ETHEREUM_ADDRESS")]
657    fee_recipient: Address,
658    /// RPC used to fetch the chain id
659    #[arg(
660        long,
661        value_name = "RPC_URL",
662        default_value = "https://rpc.presto.tempo.xyz"
663    )]
664    chain_id_from_rpc_url: String,
665    #[command(flatten)]
666    signing_key: SigningKeyArgs,
667}
668
669impl CreateAddValidatorSignatureArgs {
670    async fn run(self) -> eyre::Result<()> {
671        let signing_key = self.signing_key.read()?;
672
673        let provider = ProviderBuilder::new_with_network::<TempoNetwork>()
674            .connect(&self.chain_id_from_rpc_url)
675            .await
676            .wrap_err("failed to connect to RPC")?;
677
678        let chain_id = provider
679            .get_chain_id()
680            .await
681            .wrap_err("failed to get chain id")?;
682
683        let config = self.identity.to_config(chain_id);
684        let message = config.add_validator_message_hash(self.fee_recipient);
685
686        let private_key = signing_key.into_inner();
687        let signature = private_key.sign(VALIDATOR_NS_ADD, message.as_slice());
688        let encoded = signature.encode();
689        println!("{}", alloy_primitives::hex::encode_prefixed(encoded));
690        Ok(())
691    }
692}
693
694#[derive(Debug, clap::Args)]
695#[group(required = true, args = ["signing_key"])]
696pub struct CreateRotateValidatorSignatureArgs {
697    #[command(flatten)]
698    identity: ValidatorIdentityArgs,
699    /// RPC used to fetch the chain id
700    #[arg(
701        long,
702        value_name = "RPC_URL",
703        default_value = "https://rpc.presto.tempo.xyz"
704    )]
705    chain_id_from_rpc_url: String,
706    #[command(flatten)]
707    signing_key: SigningKeyArgs,
708}
709
710impl CreateRotateValidatorSignatureArgs {
711    async fn run(self) -> eyre::Result<()> {
712        let signing_key = self.signing_key.read()?;
713
714        let provider = ProviderBuilder::new_with_network::<TempoNetwork>()
715            .connect(&self.chain_id_from_rpc_url)
716            .await
717            .wrap_err("failed to connect to RPC")?;
718
719        let chain_id = provider
720            .get_chain_id()
721            .await
722            .wrap_err("failed to get chain id")?;
723
724        let config = self.identity.to_config(chain_id);
725        let message = config.rotate_validator_message_hash();
726
727        let private_key = signing_key.into_inner();
728        let signature = private_key.sign(VALIDATOR_NS_ROTATE, message.as_slice());
729        let encoded = signature.encode();
730        println!("{}", alloy_primitives::hex::encode_prefixed(encoded));
731        Ok(())
732    }
733}
734
735#[derive(Debug, clap::Args)]
736pub struct SetValidatorIpAddress {
737    /// Validator ethereum address, ed25519 public key, or index
738    #[arg()]
739    id: ValidatorId,
740    /// The inbound address for the validator.
741    #[arg(long, value_name = "IP:PORT")]
742    ingress: Option<SocketAddr>,
743    /// The outbound address for the validator.
744    #[arg(long, value_name = "IP")]
745    egress: Option<IpAddr>,
746
747    #[command(flatten)]
748    submit: ValidatorTransactionArgs,
749}
750
751impl SetValidatorIpAddress {
752    async fn run(self) -> eyre::Result<()> {
753        let provider = self.submit.provider().await?;
754
755        if self.ingress.is_none() && self.egress.is_none() {
756            return Err(eyre!("at least one of --ingress or --egress must be set"));
757        }
758
759        let validator = read_validator_from_contract(&provider, self.id).await?;
760
761        let call = IValidatorConfigV2::setIpAddressesCall {
762            idx: validator.index,
763            ingress: self.ingress.map_or(validator.ingress, |v| v.to_string()),
764            egress: self.egress.map_or(validator.egress, |v| v.to_string()),
765        };
766
767        self.submit.call(&call).await?;
768        Ok(())
769    }
770}
771
772#[derive(Debug, clap::Args)]
773pub struct DeactivateValidator {
774    /// Validator ethereum address, ed25519 public key, or index
775    #[arg()]
776    id: ValidatorId,
777
778    #[command(flatten)]
779    submit: ValidatorTransactionArgs,
780}
781
782impl DeactivateValidator {
783    async fn run(self) -> eyre::Result<()> {
784        let provider = self.submit.provider().await?;
785
786        let validator = read_validator_from_contract(&provider, self.id).await?;
787
788        let call = IValidatorConfigV2::deactivateValidatorCall {
789            idx: validator.index,
790        };
791
792        self.submit.call(&call).await?;
793        Ok(())
794    }
795}
796
797#[derive(Debug, clap::Args)]
798pub struct SetValidatorFeeRecipient {
799    /// Validator ethereum address, ed25519 public key, or index
800    #[arg()]
801    id: ValidatorId,
802    /// The fee recipient address
803    #[arg(long, value_name = "ETHEREUM_ADDRESS")]
804    fee_recipient: Address,
805
806    #[command(flatten)]
807    submit: ValidatorTransactionArgs,
808}
809
810impl SetValidatorFeeRecipient {
811    async fn run(self) -> eyre::Result<()> {
812        let provider = self.submit.provider().await?;
813        let validator = read_validator_from_contract(&provider, self.id).await?;
814
815        let call = IValidatorConfigV2::setFeeRecipientCall {
816            idx: validator.index,
817            feeRecipient: self.fee_recipient,
818        };
819
820        self.submit.call(&call).await?;
821        Ok(())
822    }
823}
824
825#[derive(Debug, clap::Args)]
826pub struct GenerateSigningKey {
827    /// Destination of the generated signing key.
828    #[arg(long, short, value_name = "FILE")]
829    output: PathBuf,
830
831    /// Passphrase source for encrypting the generated signing key.
832    ///
833    /// A FIFO path, including shell process substitution like `<(...)`, is preferred.
834    /// If omitted, the signing key is written unencrypted for compatibility.
835    #[arg(long, value_name = "PATH")]
836    secret: Option<PathBuf>,
837
838    /// Whether to override `output`, if it already exists.
839    #[arg(long, short)]
840    force: bool,
841}
842
843impl GenerateSigningKey {
844    fn run(self) -> eyre::Result<()> {
845        let Self {
846            output,
847            secret,
848            force,
849        } = self;
850        let signing_key = PrivateKey::random(&mut rand_08::thread_rng());
851        let public_key = signing_key.public_key();
852        let signing_key = SigningKey::from(signing_key);
853        let passphrase = secret
854            .as_ref()
855            .map(|secret| {
856                read_secret(secret).wrap_err_with(|| {
857                    format!(
858                        "failed reading signing-key encryption passphrase from `{}`",
859                        secret.display()
860                    )
861                })
862            })
863            .transpose()?;
864
865        if passphrase.is_none() {
866            warn_unencrypted_signing_key_deprecation();
867        }
868
869        OpenOptions::new()
870            .write(true)
871            .create_new(!force)
872            .create(force)
873            .truncate(force)
874            .open(&output)
875            .map_err(Report::new)
876            .and_then(|f| match passphrase {
877                Some(passphrase) => signing_key
878                    .write_encrypted(f, passphrase)
879                    .map_err(Report::new),
880                None => signing_key.to_writer_unencrypted(f).map_err(Report::new),
881            })
882            .wrap_err_with(|| format!("failed writing signing key to `{}`", output.display()))?;
883        eprintln!(
884            "wrote signing key to: {}\npublic key: {public_key}",
885            output.display()
886        );
887
888        Ok(())
889    }
890}
891
892fn warn_unencrypted_signing_key_deprecation() {
893    eprintln!(
894        "\
895!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
896WARNING: generated consensus signing key will be written UNENCRYPTED.
897This compatibility mode is deprecated and will be removed in a future release.
898Pass `--secret <PATH>` (preferably a FIFO, for example `--secret <(cmd)`) to encrypt the key at rest.
899!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
900    );
901}
902
903fn read_secret<P: AsRef<Path>>(path: P) -> eyre::Result<SigningKeyPassphrase> {
904    let path = path.as_ref();
905    let (passphrase, is_fifo) =
906        tempo_consensus_config::read_secret(path).wrap_err("failed reading from secret path")?;
907    if !is_fifo {
908        eprintln!(
909            "WARNING: signing-key passphrase was read from a non-FIFO path `{}`; prefer a FIFO to avoid persisting the passphrase on disk.",
910            path.display()
911        );
912    }
913    Ok(passphrase)
914}
915
916fn read_signing_key<P1: AsRef<Path>, P2: AsRef<Path>>(
917    key_path: P1,
918    secret_path: Option<P2>,
919) -> eyre::Result<SigningKey> {
920    let key_path = key_path.as_ref();
921    match secret_path.as_ref().map(AsRef::<Path>::as_ref) {
922        Some(secret_path) => {
923            let passphrase = read_secret(secret_path).wrap_err_with(|| {
924                format!(
925                    "failed reading signing-key passphrase from `{}`",
926                    secret_path.display()
927                )
928            })?;
929
930            SigningKey::read_from_file_encrypted(key_path, passphrase).wrap_err_with(|| {
931                format!(
932                    "failed decrypting signing key from `{}`",
933                    key_path.display()
934                )
935            })
936        }
937        None => SigningKey::read_from_file_unencrypted(key_path)
938            .wrap_err_with(|| format!("failed reading signing key from `{}`", key_path.display())),
939    }
940}
941
942#[derive(Debug, clap::Args)]
943pub struct EncryptSigningKey {
944    /// Existing plaintext ed25519 signing key file.
945    #[arg(long, short, value_name = "FILE")]
946    input: PathBuf,
947
948    /// Destination for the passphrase-encrypted signing key.
949    #[arg(long, short, value_name = "FILE")]
950    output: PathBuf,
951
952    /// Passphrase source. A FIFO path, including shell process substitution like `<(...)`, is preferred.
953    #[arg(long, value_name = "PATH")]
954    secret: PathBuf,
955
956    /// Whether to override `output`, if it already exists.
957    #[arg(long, short)]
958    force: bool,
959}
960
961impl EncryptSigningKey {
962    fn run(self) -> eyre::Result<()> {
963        let Self {
964            input,
965            output,
966            secret,
967            force,
968        } = self;
969
970        let signing_key = SigningKey::read_from_file_unencrypted(&input).wrap_err_with(|| {
971            format!(
972                "failed reading plaintext signing key from `{}`",
973                input.display()
974            )
975        })?;
976        let passphrase = read_secret(&secret).wrap_err_with(|| {
977            format!(
978                "failed reading signing-key encryption passphrase from `{}`",
979                secret.display()
980            )
981        })?;
982
983        OpenOptions::new()
984            .write(true)
985            .create_new(!force)
986            .create(force)
987            .truncate(force)
988            .open(&output)
989            .map_err(Report::new)
990            .and_then(|f| {
991                signing_key
992                    .write_encrypted(f, passphrase)
993                    .map_err(Report::new)
994            })
995            .wrap_err_with(|| {
996                format!(
997                    "failed writing encrypted signing key to `{}`",
998                    output.display()
999                )
1000            })?;
1001
1002        eprintln!("wrote encrypted signing key to: {}", output.display());
1003        Ok(())
1004    }
1005}
1006
1007#[derive(Debug, clap::Args)]
1008pub struct ShowVerificationKey {
1009    /// Signing key to show the verification key for.
1010    #[arg(long, short, value_name = "FILE")]
1011    private_key: PathBuf,
1012
1013    /// Passphrase source for decrypting the signing key.
1014    ///
1015    /// A FIFO path, including shell process substitution like `<(...)`, is preferred.
1016    /// If omitted, the signing key is read as plaintext hex.
1017    #[arg(long, value_name = "PATH")]
1018    secret: Option<PathBuf>,
1019}
1020
1021impl ShowVerificationKey {
1022    fn run(self) -> eyre::Result<()> {
1023        let Self {
1024            private_key,
1025            secret,
1026        } = self;
1027        let private_key = read_signing_key(&private_key, secret.as_deref())?;
1028
1029        let validating_key = private_key.public_key();
1030        println!("public key: {validating_key}");
1031        Ok(())
1032    }
1033}
1034
1035#[derive(Debug, clap::Args)]
1036pub struct ValidatorInfo {
1037    /// Validator ethereum address, ed25519 public key, or index
1038    #[arg()]
1039    id: ValidatorId,
1040
1041    /// RPC URL to query.
1042    #[arg(long, default_value = "https://rpc.presto.tempo.xyz")]
1043    rpc_url: String,
1044
1045    /// Chain spec override for local/unknown chains (mainnet, testnet, moderato, or path to
1046    /// chainspec file). Resolved automatically from the RPC chain id when omitted.
1047    #[arg(long, short, value_parser = tempo_chainspec::spec::chain_value_parser)]
1048    chain: Option<Arc<TempoChainSpec>>,
1049
1050    /// Skip crosschecking the validator with the last DKG round.
1051    #[arg(long)]
1052    no_dkg_information: bool,
1053}
1054
1055#[derive(Debug, Serialize)]
1056struct ValidatorOutput {
1057    #[serde(skip_serializing_if = "Option::is_none")]
1058    is_dkg_player: Option<bool>,
1059    #[serde(skip_serializing_if = "Option::is_none")]
1060    is_dkg_dealer: Option<bool>,
1061    #[serde(skip_serializing_if = "Option::is_none")]
1062    in_committee: Option<bool>,
1063
1064    #[serde(flatten)]
1065    validator: Validator,
1066}
1067
1068/// Output for the single-validator lookup enriched with DKG role and epoch context.
1069#[derive(Debug, Serialize)]
1070struct ValidatorInfoOutput {
1071    current_epoch: u64,
1072    current_height: u64,
1073    #[serde(flatten)]
1074    validator: ValidatorOutput,
1075}
1076
1077impl ValidatorInfo {
1078    async fn run(self) -> eyre::Result<()> {
1079        let provider = ProviderBuilder::new_with_network::<TempoNetwork>()
1080            .connect(&self.rpc_url)
1081            .await
1082            .wrap_err("failed to connect to RPC")?;
1083
1084        let chain_id = provider
1085            .get_chain_id()
1086            .await
1087            .wrap_err("failed to get chain id")?;
1088
1089        let chain = match self.chain {
1090            Some(chain) => {
1091                let spec_chain_id = chain.chain().id();
1092                if spec_chain_id != chain_id {
1093                    eprintln!(
1094                        "warning: --chain spec has chain id {spec_chain_id} but RPC returned {chain_id}"
1095                    );
1096                }
1097                chain
1098            }
1099            None => tempo_chainspec::spec::chainspec_from_chain_id(chain_id)
1100                .ok_or_else(|| eyre!("unknown chain id {chain_id}, pass --chain explicitly"))?,
1101        };
1102
1103        let epoch_length = chain
1104            .info
1105            .epoch_length()
1106            .ok_or_eyre("epochLength not found in chainspec")?;
1107
1108        let validator = read_validator_from_contract(&provider, self.id).await?;
1109        let pubkey_bytes = validator.publicKey.0;
1110
1111        let latest_block_number = provider
1112            .get_block_number()
1113            .await
1114            .wrap_err("failed to get latest block number")?;
1115
1116        let epoch_strategy = FixedEpocher::new(NZU64!(epoch_length));
1117        let current_height = Height::new(latest_block_number);
1118        let current_epoch_info = epoch_strategy
1119            .containing(current_height)
1120            .ok_or_else(|| eyre!("failed to determine epoch for height {latest_block_number}"))?;
1121        let current_epoch = current_epoch_info.epoch();
1122
1123        let mut is_dkg_dealer = None;
1124        let mut is_dkg_player = None;
1125        let mut in_committee = None;
1126
1127        if !self.no_dkg_information {
1128            use alloy_consensus::BlockHeader;
1129
1130            let boundary_height = current_epoch
1131                .previous()
1132                .map(|epoch| epoch_strategy.last(epoch).expect("valid epoch"))
1133                .unwrap_or_default();
1134
1135            let boundary_block = provider
1136                .get_block_by_number(boundary_height.get().into())
1137                .hashes()
1138                .await
1139                .wrap_err_with(|| {
1140                    format!(
1141                        "failed to get block header at height {}",
1142                        boundary_height.get()
1143                    )
1144                })?
1145                .ok_or_eyre("boundary block not found")?;
1146
1147            let extra_data = boundary_block.header.extra_data();
1148            if extra_data.is_empty() {
1149                return Err(eyre!(
1150                    "boundary block at height {} has no DKG outcome in extra_data",
1151                    boundary_height.get()
1152                ));
1153            }
1154
1155            let dkg_outcome = OnchainDkgOutcome::read(&mut extra_data.as_ref())
1156                .wrap_err("failed to decode DKG outcome from extra_data")?;
1157
1158            let key = PublicKey::decode(&mut &pubkey_bytes[..])
1159                .wrap_err("failed decoding on-chain ed25519 key")?;
1160
1161            let committee = dkg_outcome.players().position(&key).is_some();
1162            is_dkg_dealer = Some(committee);
1163            is_dkg_player = Some(dkg_outcome.next_players().position(&key).is_some());
1164            in_committee = Some(committee);
1165        }
1166
1167        let output = ValidatorInfoOutput {
1168            current_epoch: current_epoch.get(),
1169            current_height: current_height.get(),
1170            validator: ValidatorOutput {
1171                validator,
1172                is_dkg_dealer,
1173                is_dkg_player,
1174                in_committee,
1175            },
1176        };
1177
1178        println!("{}", serde_json::to_string_pretty(&output)?);
1179        Ok(())
1180    }
1181}
1182
1183/// Validator info output structure
1184#[derive(Debug, Serialize)]
1185struct InfoOutput {
1186    /// The current epoch (at the time of query)
1187    current_epoch: u64,
1188    /// The current height (at the time of query)
1189    current_height: u64,
1190    // The boundary height from which the DKG outcome was read
1191    last_boundary: u64,
1192    // The epoch length as set in the chain spec
1193    epoch_length: u64,
1194    /// Whether this is a full DKG (new polynomial) or reshare
1195    is_next_full_dkg: bool,
1196    /// The epoch at which the next full DKG ceremony will be triggered (from contract)
1197    next_full_dkg_epoch: u64,
1198    /// List of validators participating in the DKG
1199    validators: Vec<ValidatorOutput>,
1200}
1201
1202#[derive(Debug, clap::Args)]
1203pub struct Info {
1204    /// RPC URL to query. Defaults to <https://rpc.presto.tempo.xyz>
1205    #[arg(long, default_value = "https://rpc.presto.tempo.xyz")]
1206    rpc_url: String,
1207
1208    /// Chain spec override for local/unknown chains (mainnet, testnet, moderato, or path to
1209    /// chainspec file). Resolved automatically from the RPC chain id when omitted.
1210    #[arg(long, short, value_parser = tempo_chainspec::spec::chain_value_parser)]
1211    chain: Option<Arc<TempoChainSpec>>,
1212}
1213
1214impl Info {
1215    async fn run(self) -> eyre::Result<()> {
1216        use alloy_consensus::BlockHeader;
1217        use alloy_provider::ProviderBuilder;
1218
1219        let provider = ProviderBuilder::new_with_network::<TempoNetwork>()
1220            .connect(&self.rpc_url)
1221            .await
1222            .wrap_err("failed to connect to RPC")?;
1223
1224        let chain_id = provider
1225            .get_chain_id()
1226            .await
1227            .wrap_err("failed to get chain id")?;
1228
1229        let chain = match self.chain {
1230            Some(chain) => {
1231                let spec_chain_id = chain.chain().id();
1232                if spec_chain_id != chain_id {
1233                    eprintln!(
1234                        "warning: --chain spec has chain id {spec_chain_id} but RPC returned {chain_id}"
1235                    );
1236                }
1237                chain
1238            }
1239            None => tempo_chainspec::spec::chainspec_from_chain_id(chain_id)
1240                .ok_or_else(|| eyre!("unknown chain id {chain_id}, pass --chain explicitly"))?,
1241        };
1242
1243        let epoch_length = chain
1244            .info
1245            .epoch_length()
1246            .ok_or_eyre("epochLength not found in chainspec")?;
1247
1248        let latest_block_number = provider
1249            .get_block_number()
1250            .await
1251            .wrap_err("failed to get latest block number")?;
1252
1253        let epoch_strategy = FixedEpocher::new(NZU64!(epoch_length));
1254        let current_height = Height::new(latest_block_number);
1255        let current_epoch_info = epoch_strategy
1256            .containing(current_height)
1257            .ok_or_else(|| eyre!("failed to determine epoch for height {latest_block_number}"))?;
1258
1259        let current_epoch = current_epoch_info.epoch();
1260        let boundary_height = current_epoch
1261            .previous()
1262            .map(|epoch| epoch_strategy.last(epoch).expect("valid epoch"))
1263            .unwrap_or_default();
1264
1265        let boundary_block = provider
1266            .get_block_by_number(boundary_height.get().into())
1267            .hashes()
1268            .await
1269            .wrap_err_with(|| {
1270                format!(
1271                    "failed to get block header at height {}",
1272                    boundary_height.get()
1273                )
1274            })?
1275            .ok_or_eyre("boundary block not found")?;
1276
1277        let extra_data = boundary_block.header.extra_data();
1278        if extra_data.is_empty() {
1279            return Err(eyre!(
1280                "boundary block at height {} has no DKG outcome in extra_data",
1281                boundary_height.get()
1282            ));
1283        }
1284
1285        let dkg_outcome = OnchainDkgOutcome::read(&mut extra_data.as_ref())
1286            .wrap_err("failed to decode DKG outcome from extra_data")?;
1287
1288        let next_dkg_result = provider
1289            .call(
1290                TransactionRequest::default()
1291                    .to(VALIDATOR_CONFIG_V2_ADDRESS)
1292                    .input(
1293                        IValidatorConfigV2::getNextNetworkIdentityRotationEpochCall {}
1294                            .abi_encode()
1295                            .into(),
1296                    )
1297                    .into(),
1298            )
1299            .number(latest_block_number)
1300            .await
1301            .wrap_err("failed to call getNextNetworkIdentityRotationEpoch")?;
1302
1303        let next_full_dkg_epoch =
1304            IValidatorConfigV2::getNextNetworkIdentityRotationEpochCall::abi_decode_returns(
1305                &next_dkg_result,
1306            )
1307            .wrap_err("failed to decode getNextNetworkIdentityRotationEpoch response")?;
1308
1309        let active_validators_result = provider
1310            .call(
1311                TransactionRequest::default()
1312                    .to(VALIDATOR_CONFIG_V2_ADDRESS)
1313                    .input(
1314                        IValidatorConfigV2::getActiveValidatorsCall {}
1315                            .abi_encode()
1316                            .into(),
1317                    )
1318                    .into(),
1319            )
1320            .number(latest_block_number)
1321            .await
1322            .wrap_err("failed to call getActiveValidators")?;
1323
1324        let active_validators = IValidatorConfigV2::getActiveValidatorsCall::abi_decode_returns(
1325            &active_validators_result,
1326        )
1327        .wrap_err("failed to decode getActiveValidators response")?;
1328
1329        let active_validators_by_public_key = active_validators
1330            .into_iter()
1331            .map(|v| (v.publicKey, v))
1332            .collect::<std::collections::BTreeMap<_, _>>();
1333
1334        let players = dkg_outcome.players();
1335        let next_players = dkg_outcome.next_players();
1336        let dkg_players = ordered::Set::from_iter_dedup(players.iter().chain(next_players));
1337
1338        // Add validators that are active onchain
1339        let mut validators_by_public_key = active_validators_by_public_key.clone();
1340
1341        // Add validators that are in the dkg outcome but no longer active onchain
1342        for public_key in dkg_players {
1343            let key = B256::from_slice(public_key.as_ref());
1344            if let std::collections::btree_map::Entry::Vacant(e) =
1345                validators_by_public_key.entry(key)
1346            {
1347                let id = ValidatorId::PublicKey(key);
1348                match read_validator_from_contract(&provider, id).await {
1349                    Ok(v) => _ = e.insert(v),
1350                    Err(e) => eprintln!("failed to lookup validator {}: {e}", key.encode_hex()),
1351                }
1352            }
1353        }
1354
1355        let mut validators: Vec<ValidatorOutput> = Vec::new();
1356        for (key, validator) in validators_by_public_key {
1357            let public_key = match PublicKey::decode(key.as_ref()) {
1358                Ok(key) => key,
1359                Err(e) => {
1360                    let index = validator.index;
1361                    eprintln!("invalid ed25519 public key found on validator index {index}: {e}",);
1362                    continue;
1363                }
1364            };
1365
1366            validators.push(ValidatorOutput {
1367                validator,
1368                is_dkg_dealer: Some(players.position(&public_key).is_some()),
1369                is_dkg_player: Some(next_players.position(&public_key).is_some()),
1370                in_committee: Some(players.position(&public_key).is_some()),
1371            })
1372        }
1373
1374        let output = InfoOutput {
1375            validators,
1376            current_epoch: current_epoch.get(),
1377            current_height: current_height.get(),
1378            last_boundary: boundary_height.get(),
1379            epoch_length,
1380            is_next_full_dkg: dkg_outcome.is_next_full_dkg,
1381            next_full_dkg_epoch,
1382        };
1383
1384        println!("{}", serde_json::to_string_pretty(&output)?);
1385        Ok(())
1386    }
1387}
1388
1389fn key_from_file<P: AsRef<Path>>(p: P) -> eyre::Result<PrivateKeySigner> {
1390    let raw = std::fs::read(p).wrap_err("failed reading key from file")?;
1391    let bytes = alloy::hex::decode(&raw).wrap_err("failed decoding file contents from hex")?;
1392    PrivateKeySigner::from_slice(&bytes)
1393        .wrap_err("failed converting file decoded hex bytes to private key")
1394}
1395
1396#[cfg(test)]
1397mod tests {
1398    use super::*;
1399    use clap::Parser;
1400    use reth_ethereum_cli::Cli;
1401    use reth_rpc_server_types::{RethRpcModule, RpcModuleSelection, RpcModuleValidator};
1402    use tempo_chainspec::spec::TempoChainSpecParser;
1403
1404    type TempoCli = Cli<
1405        TempoChainSpecParser,
1406        crate::TempoArgs,
1407        crate::TempoRpcModuleValidator,
1408        TempoSubcommand,
1409    >;
1410
1411    const TEST_VALIDATOR_ADDRESS: &str = "0x0000000000000000000000000000000000000001";
1412    const TEST_FEE_RECIPIENT: &str = "0x0000000000000000000000000000000000000002";
1413    const TEST_PUBLIC_KEY: &str =
1414        "0x1111111111111111111111111111111111111111111111111111111111111111";
1415    const TEST_SIGNATURE: &str = "0x11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111";
1416    const TEST_INGRESS: &str = "127.0.0.1:8000";
1417    const TEST_EGRESS: &str = "127.0.0.1";
1418
1419    #[test]
1420    fn parse_p2p_proxy_defaults() {
1421        let cli = TempoCli::try_parse_from([
1422            "tempo",
1423            "p2p-proxy",
1424            "--rpc-url",
1425            "https://rpc.moderato.tempo.xyz",
1426        ])
1427        .unwrap();
1428
1429        assert!(matches!(
1430            cli.command,
1431            reth_ethereum::cli::Commands::Ext(TempoSubcommand::P2pProxy(_))
1432        ));
1433    }
1434
1435    #[test]
1436    fn parse_p2p_proxy_all_args() {
1437        let cli = TempoCli::try_parse_from([
1438            "tempo",
1439            "p2p-proxy",
1440            "--rpc-url",
1441            "ws://localhost:8546",
1442            "--chain",
1443            "moderato",
1444            "--port",
1445            "9999",
1446            "--discovery-port",
1447            "9998",
1448            "--max-inbound",
1449            "50",
1450            "--max-concurrent-inbound",
1451            "10",
1452            "--cache-blocks",
1453            "1000",
1454            "--p2p-secret-key",
1455            "/tmp/test-enode.key",
1456        ])
1457        .unwrap();
1458
1459        assert!(matches!(
1460            cli.command,
1461            reth_ethereum::cli::Commands::Ext(TempoSubcommand::P2pProxy(_))
1462        ));
1463    }
1464
1465    #[test]
1466    fn parse_p2p_proxy_missing_rpc_url_fails() {
1467        let result = TempoCli::try_parse_from(["tempo", "p2p-proxy"]);
1468        assert!(result.is_err());
1469    }
1470
1471    #[test]
1472    fn parse_encrypt_signing_key() {
1473        let cli = TempoCli::try_parse_from([
1474            "tempo",
1475            "consensus",
1476            "encrypt-signing-key",
1477            "--input",
1478            "/tmp/signing.key",
1479            "--output",
1480            "/tmp/signing.key.age",
1481            "--secret",
1482            "/dev/fd/11",
1483        ])
1484        .unwrap();
1485
1486        let EncryptSigningKey {
1487            input,
1488            output,
1489            secret,
1490            force,
1491        } = match cli.command {
1492            reth_ethereum::cli::Commands::Ext(TempoSubcommand::Consensus(
1493                ConsensusSubcommand::EncryptSigningKey(cmd),
1494            )) => cmd,
1495            other => panic!("expected variant EncryptSigningKey, got `{other:?}`"),
1496        };
1497        assert_eq!(&input, "/tmp/signing.key");
1498        assert_eq!(&output, "/tmp/signing.key.age");
1499        assert_eq!(&secret, "/dev/fd/11");
1500        assert!(!force);
1501    }
1502
1503    #[test]
1504    fn parse_show_verification_key_with_secret() {
1505        assert_parse_show_verification_key("show-verification-key");
1506    }
1507
1508    #[test]
1509    fn parse_calculate_public_key_alias_with_secret() {
1510        assert_parse_show_verification_key("calculate-public-key");
1511    }
1512
1513    #[track_caller]
1514    fn assert_parse_show_verification_key(command: &str) {
1515        let cli = TempoCli::try_parse_from([
1516            "tempo",
1517            "consensus",
1518            command,
1519            "--private-key",
1520            "/tmp/signing.key.age",
1521            "--secret",
1522            "/dev/fd/11",
1523        ])
1524        .unwrap();
1525
1526        let ShowVerificationKey {
1527            private_key,
1528            secret,
1529        } = match cli.command {
1530            reth_ethereum::cli::Commands::Ext(TempoSubcommand::Consensus(
1531                ConsensusSubcommand::ShowVerificationKey(cmd),
1532            )) => cmd,
1533            other => panic!("expected ShowVerificationKey, got `{other:?}`"),
1534        };
1535        assert_eq!(&private_key, "/tmp/signing.key.age");
1536        assert_eq!(&secret.unwrap(), "/dev/fd/11");
1537    }
1538
1539    #[test]
1540    fn parse_generate_signing_key_command() {
1541        assert_parse_generate_signing_key("generate-signing-key");
1542    }
1543
1544    #[test]
1545    fn parse_generate_private_key_alias() {
1546        assert_parse_generate_signing_key("generate-private-key");
1547    }
1548
1549    #[test]
1550    fn parse_generate_signing_key_with_secret() {
1551        let cli = TempoCli::try_parse_from([
1552            "tempo",
1553            "consensus",
1554            "generate-signing-key",
1555            "--output",
1556            "/tmp/signing.key",
1557            "--secret",
1558            "/dev/fd/11",
1559        ])
1560        .unwrap();
1561
1562        let GenerateSigningKey {
1563            output,
1564            secret,
1565            force,
1566        } = match cli.command {
1567            reth_ethereum::cli::Commands::Ext(TempoSubcommand::Consensus(
1568                ConsensusSubcommand::GenerateSigningKey(cmd),
1569            )) => cmd,
1570            other => panic!("expected GenerateSigningKey, got `{other:?}`"),
1571        };
1572        assert!(!force);
1573        assert_eq!(&output, "/tmp/signing.key");
1574        assert_eq!(&secret.unwrap(), "/dev/fd/11");
1575    }
1576
1577    #[track_caller]
1578    fn assert_parse_generate_signing_key(command: &str) {
1579        let cli = TempoCli::try_parse_from([
1580            "tempo",
1581            "consensus",
1582            command,
1583            "--output",
1584            "/tmp/signing.key",
1585        ])
1586        .unwrap();
1587
1588        let GenerateSigningKey { output, .. } = match cli.command {
1589            reth_ethereum::cli::Commands::Ext(TempoSubcommand::Consensus(
1590                ConsensusSubcommand::GenerateSigningKey(cmd),
1591            )) => cmd,
1592            other => panic!("expected GenerateSigningKey, got `{other:?}`"),
1593        };
1594        assert_eq!(&output, "/tmp/signing.key");
1595    }
1596
1597    #[test]
1598    fn parse_create_add_validator_signature_with_secret() {
1599        let cli = TempoCli::try_parse_from([
1600            "tempo",
1601            "consensus",
1602            "create-add-validator-signature",
1603            "--validator-address",
1604            TEST_VALIDATOR_ADDRESS,
1605            "--consensus.public-key",
1606            TEST_PUBLIC_KEY,
1607            "--ingress",
1608            TEST_INGRESS,
1609            "--egress",
1610            TEST_EGRESS,
1611            "--fee-recipient",
1612            TEST_FEE_RECIPIENT,
1613            "--signing-key",
1614            "/tmp/signing.key.age",
1615            "--secret",
1616            "/dev/fd/11",
1617        ])
1618        .unwrap();
1619
1620        let CreateAddValidatorSignatureArgs { signing_key, .. } = match cli.command {
1621            reth_ethereum::cli::Commands::Ext(TempoSubcommand::Consensus(
1622                ConsensusSubcommand::CreateAddValidatorSignature(cmd),
1623            )) => cmd,
1624            other => panic!("expected CreateAddValidatorSignature, got `{other:?}`"),
1625        };
1626        assert_eq!(&signing_key.signing_key, "/tmp/signing.key.age");
1627        assert_eq!(&signing_key.secret.unwrap(), "/dev/fd/11");
1628    }
1629
1630    #[test]
1631    fn parse_create_rotate_validator_signature_with_secret() {
1632        let cli = TempoCli::try_parse_from([
1633            "tempo",
1634            "consensus",
1635            "create-rotate-validator-signature",
1636            "--validator-address",
1637            TEST_VALIDATOR_ADDRESS,
1638            "--consensus.public-key",
1639            TEST_PUBLIC_KEY,
1640            "--ingress",
1641            TEST_INGRESS,
1642            "--egress",
1643            TEST_EGRESS,
1644            "--signing-key",
1645            "/tmp/signing.key.age",
1646            "--secret",
1647            "/dev/fd/11",
1648        ])
1649        .unwrap();
1650
1651        let CreateRotateValidatorSignatureArgs { signing_key, .. } = match cli.command {
1652            reth_ethereum::cli::Commands::Ext(TempoSubcommand::Consensus(
1653                ConsensusSubcommand::CreateRotateValidatorSignature(cmd),
1654            )) => cmd,
1655            other => panic!("expected CreateRotateValidatorSignature, got `{other:?}`"),
1656        };
1657        assert_eq!(&signing_key.signing_key, "/tmp/signing.key.age");
1658        assert_eq!(&signing_key.secret.unwrap(), "/dev/fd/11");
1659    }
1660
1661    #[test]
1662    fn parse_add_validator_with_signature_does_not_require_signing_key() {
1663        let cli = TempoCli::try_parse_from([
1664            "tempo",
1665            "consensus",
1666            "add-validator",
1667            "--validator-address",
1668            TEST_VALIDATOR_ADDRESS,
1669            "--consensus.public-key",
1670            TEST_PUBLIC_KEY,
1671            "--ingress",
1672            TEST_INGRESS,
1673            "--egress",
1674            TEST_EGRESS,
1675            "--fee-recipient",
1676            TEST_FEE_RECIPIENT,
1677            "--signature",
1678            TEST_SIGNATURE,
1679        ])
1680        .unwrap();
1681
1682        let AddValidator { sig, .. } = match cli.command {
1683            reth_ethereum::cli::Commands::Ext(TempoSubcommand::Consensus(
1684                ConsensusSubcommand::AddValidator(cmd),
1685            )) => cmd,
1686            other => panic!("expected AddValidator, got `{other:?}`"),
1687        };
1688        assert!(sig.signing_key.is_none());
1689    }
1690
1691    #[test]
1692    fn parse_add_validator_rejects_signature_and_signing_key() {
1693        let err = TempoCli::try_parse_from([
1694            "tempo",
1695            "consensus",
1696            "add-validator",
1697            "--validator-address",
1698            TEST_VALIDATOR_ADDRESS,
1699            "--consensus.public-key",
1700            TEST_PUBLIC_KEY,
1701            "--ingress",
1702            TEST_INGRESS,
1703            "--egress",
1704            TEST_EGRESS,
1705            "--fee-recipient",
1706            TEST_FEE_RECIPIENT,
1707            "--signature",
1708            TEST_SIGNATURE,
1709            "--signing-key",
1710            "/tmp/signing.key.age",
1711        ])
1712        .unwrap_err();
1713
1714        assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
1715    }
1716
1717    #[test]
1718    fn parse_add_validator_with_consensus_signing_key_secret() {
1719        let cli = TempoCli::try_parse_from([
1720            "tempo",
1721            "consensus",
1722            "add-validator",
1723            "--validator-address",
1724            TEST_VALIDATOR_ADDRESS,
1725            "--consensus.public-key",
1726            TEST_PUBLIC_KEY,
1727            "--ingress",
1728            TEST_INGRESS,
1729            "--egress",
1730            TEST_EGRESS,
1731            "--fee-recipient",
1732            TEST_FEE_RECIPIENT,
1733            "--consensus.signing-key",
1734            "/tmp/signing.key.age",
1735            "--consensus.secret",
1736            "/dev/fd/11",
1737        ])
1738        .unwrap();
1739
1740        let AddValidator { sig, .. } = match cli.command {
1741            reth_ethereum::cli::Commands::Ext(TempoSubcommand::Consensus(
1742                ConsensusSubcommand::AddValidator(cmd),
1743            )) => cmd,
1744            other => panic!("expected AddValidator, got `{other:?}`"),
1745        };
1746        let signing_key = sig.signing_key.unwrap();
1747        assert_eq!(&signing_key.signing_key, "/tmp/signing.key.age");
1748        assert_eq!(&signing_key.secret.unwrap(), "/dev/fd/11");
1749    }
1750
1751    #[test]
1752    fn parse_rotate_validator_with_consensus_signing_key_secret() {
1753        let cli = TempoCli::try_parse_from([
1754            "tempo",
1755            "consensus",
1756            "rotate-validator",
1757            "--validator-address",
1758            TEST_VALIDATOR_ADDRESS,
1759            "--consensus.public-key",
1760            TEST_PUBLIC_KEY,
1761            "--ingress",
1762            TEST_INGRESS,
1763            "--egress",
1764            TEST_EGRESS,
1765            "--consensus.signing-key",
1766            "/tmp/signing.key.age",
1767            "--consensus.secret",
1768            "/dev/fd/11",
1769        ])
1770        .unwrap();
1771
1772        let RotateValidator { sig, .. } = match cli.command {
1773            reth_ethereum::cli::Commands::Ext(TempoSubcommand::Consensus(
1774                ConsensusSubcommand::RotateValidator(cmd),
1775            )) => cmd,
1776            other => panic!("expected RotateValidator, got `{other:?}`"),
1777        };
1778        let signing_key = sig.signing_key.unwrap();
1779        assert_eq!(&signing_key.signing_key, "/tmp/signing.key.age");
1780        assert_eq!(&signing_key.secret.unwrap(), "/dev/fd/11");
1781    }
1782
1783    #[test]
1784    fn tempo_rpc_module_validator_allows_tempo_custom_modules() {
1785        for module in ["consensus", "operator", "tempo", "token"] {
1786            let selection = crate::TempoRpcModuleValidator::parse_selection(module).unwrap();
1787
1788            assert_eq!(
1789                selection,
1790                RpcModuleSelection::from([RethRpcModule::Other(module.to_string())])
1791            );
1792        }
1793    }
1794
1795    #[test]
1796    fn tempo_rpc_module_validator_rejects_unknown_modules() {
1797        let err = crate::TempoRpcModuleValidator::parse_selection("not-a-real-module").unwrap_err();
1798
1799        assert!(err.contains("Unknown RPC module: 'not-a-real-module'"));
1800    }
1801}