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#[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#[derive(Debug, Subcommand)]
65pub enum TempoSubcommand {
66 #[command(subcommand)]
68 Consensus(ConsensusSubcommand),
69
70 P2pProxy(P2pProxyArgs),
72
73 InitFromBinaryDump(Box<init_state::InitFromBinaryDump<TempoChainSpecParser>>),
78
79 Regenesis(Box<regenesis::Regenesis<TempoChainSpecParser>>),
81
82 #[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 #[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 #[command(
98 override_usage = "tempo remove <EXT>",
99 after_help = "Example: tempo remove wallet"
100 )]
101 Remove(ExtArgs),
102
103 #[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 AddValidator(AddValidator),
145 #[command(alias = "calculate-public-key")]
147 ShowVerificationKey(ShowVerificationKey),
148 CreateAddValidatorSignature(CreateAddValidatorSignatureArgs),
150 CreateRotateValidatorSignature(CreateRotateValidatorSignatureArgs),
152 DeactivateValidator(DeactivateValidator),
154 EncryptSigningKey(EncryptSigningKey),
156 #[command(alias = "generate-private-key")]
158 GenerateSigningKey(GenerateSigningKey),
159 RotateValidator(RotateValidator),
161 SetValidatorIpAddress(SetValidatorIpAddress),
163 SetValidatorFeeRecipient(SetValidatorFeeRecipient),
165 TransferValidatorOwnership(TransferValidatorOwnership),
167 Validator(ValidatorInfo),
169 #[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#[derive(Debug, clap::Args)]
263pub struct ValidatorIdentityArgs {
264 #[arg(long, value_name = "ETHEREUM_ADDRESS")]
266 validator_address: Address,
267 #[arg(
269 long = "consensus.public-key",
270 value_name = "IDENTITY_PUBLIC_KEY_ADDRESS"
271 )]
272 public_key: B256,
273 #[arg(long, value_name = "IP:PORT")]
275 ingress: SocketAddr,
276 #[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#[derive(Debug, clap::Args)]
295#[group(required = true, multiple = false, args = ["signature", "signing_key"])]
296pub struct ValidatorSignatureArgs {
297 #[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 #[arg(long, alias = "consensus.secret", value_name = "PATH")]
332 secret: Option<PathBuf>,
333
334 #[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 #[arg(long, value_name = "FILE", help_heading = "Wallet options - raw")]
356 wallet_key: Option<PathBuf>,
357
358 #[arg(long, help_heading = "Wallet options - hardware wallet")]
360 ledger: bool,
361
362 #[arg(long, help_heading = "Wallet options - hardware wallet")]
364 trezor: bool,
365
366 #[arg(long, help_heading = "Wallet options - remote")]
368 aws: bool,
369
370 #[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#[derive(Debug, clap::Args)]
438pub struct ValidatorTransactionArgs {
439 #[command(flatten)]
440 wallet: WalletArgs,
441
442 #[arg(long, default_value = "https://rpc.presto.tempo.xyz")]
444 rpc_url: String,
445
446 #[arg(long, short = 'y')]
448 yes: bool,
449
450 #[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 #[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 #[arg()]
571 id: ValidatorId,
572 #[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 #[arg(long, value_name = "ETHEREUM_ADDRESS")]
657 fee_recipient: Address,
658 #[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 #[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 #[arg()]
739 id: ValidatorId,
740 #[arg(long, value_name = "IP:PORT")]
742 ingress: Option<SocketAddr>,
743 #[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 #[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 #[arg()]
801 id: ValidatorId,
802 #[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 #[arg(long, short, value_name = "FILE")]
829 output: PathBuf,
830
831 #[arg(long, value_name = "PATH")]
836 secret: Option<PathBuf>,
837
838 #[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 #[arg(long, short, value_name = "FILE")]
946 input: PathBuf,
947
948 #[arg(long, short, value_name = "FILE")]
950 output: PathBuf,
951
952 #[arg(long, value_name = "PATH")]
954 secret: PathBuf,
955
956 #[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 #[arg(long, short, value_name = "FILE")]
1011 private_key: PathBuf,
1012
1013 #[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 #[arg()]
1039 id: ValidatorId,
1040
1041 #[arg(long, default_value = "https://rpc.presto.tempo.xyz")]
1043 rpc_url: String,
1044
1045 #[arg(long, short, value_parser = tempo_chainspec::spec::chain_value_parser)]
1048 chain: Option<Arc<TempoChainSpec>>,
1049
1050 #[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#[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#[derive(Debug, Serialize)]
1185struct InfoOutput {
1186 current_epoch: u64,
1188 current_height: u64,
1190 last_boundary: u64,
1192 epoch_length: u64,
1194 is_next_full_dkg: bool,
1196 next_full_dkg_epoch: u64,
1198 validators: Vec<ValidatorOutput>,
1200}
1201
1202#[derive(Debug, clap::Args)]
1203pub struct Info {
1204 #[arg(long, default_value = "https://rpc.presto.tempo.xyz")]
1206 rpc_url: String,
1207
1208 #[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 let mut validators_by_public_key = active_validators_by_public_key.clone();
1340
1341 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}