1use std::{
2 fs::OpenOptions,
3 net::{IpAddr, SocketAddr},
4 path::PathBuf,
5 sync::Arc,
6};
7
8use alloy_network::EthereumWallet;
9use alloy_primitives::{Address, B256, Bytes};
10use alloy_provider::{Provider, ProviderBuilder};
11use alloy_rpc_types_eth::TransactionRequest;
12use alloy_signer_local::PrivateKeySigner;
13use alloy_sol_types::SolCall;
14use clap::Subcommand;
15use commonware_codec::{DecodeExt as _, Encode as _, ReadExt as _};
16use commonware_consensus::types::{Epocher as _, FixedEpocher, Height};
17use commonware_cryptography::{
18 Signer as _,
19 ed25519::{PrivateKey, PublicKey},
20};
21use commonware_math::algebra::Random as _;
22use commonware_utils::NZU64;
23use eyre::{OptionExt as _, Report, WrapErr as _, eyre};
24use reth_cli_runner::CliRunner;
25use reth_ethereum_cli::ExtendedCommand;
26use serde::Serialize;
27use tempo_alloy::TempoNetwork;
28use tempo_chainspec::spec::{TempoChainSpec, TempoChainSpecParser};
29use tempo_commonware_node_config::SigningKey;
30use tempo_contracts::precompiles::{
31 IValidatorConfig, IValidatorConfigV2, VALIDATOR_CONFIG_ADDRESS, VALIDATOR_CONFIG_V2_ADDRESS,
32};
33use tempo_dkg_onchain_artifacts::OnchainDkgOutcome;
34use tempo_precompiles::validator_config_v2::{VALIDATOR_NS_ADD, VALIDATOR_NS_ROTATE};
35use tempo_validator_config::ValidatorConfig;
36
37use crate::init_state;
38
39#[derive(Debug, clap::Args)]
45pub(crate) struct ExtArgs {
46 #[arg(trailing_var_arg = true, allow_hyphen_values = true, hide = true)]
47 args: Vec<String>,
48}
49
50#[derive(Debug, Subcommand)]
52#[expect(
53 clippy::large_enum_variant,
54 reason = "one-off commands; size doesn't matter"
55)]
56pub(crate) enum TempoSubcommand {
57 #[command(subcommand)]
59 Consensus(ConsensusSubcommand),
60
61 InitFromBinaryDump(Box<init_state::InitFromBinaryDump<TempoChainSpecParser>>),
66
67 #[command(
69 override_usage = "tempo add <EXT> [VERSION]",
70 after_help = "Examples:\n tempo add wallet\n tempo add wallet 0.2.0"
71 )]
72 Add(ExtArgs),
73
74 #[command(
76 override_usage = "tempo update [EXT]",
77 after_help = "Examples:\n tempo update # update tempo + all extensions\n tempo update wallet # update a single extension"
78 )]
79 Update(ExtArgs),
80
81 #[command(
83 override_usage = "tempo remove <EXT>",
84 after_help = "Example: tempo remove wallet"
85 )]
86 Remove(ExtArgs),
87
88 #[command(override_usage = "tempo list")]
90 List(ExtArgs),
91}
92
93impl ExtendedCommand for TempoSubcommand {
94 fn execute(self, runner: CliRunner) -> eyre::Result<()> {
95 match self {
96 Self::Consensus(cmd) => cmd.run(),
97 Self::InitFromBinaryDump(cmd) => {
98 let runtime = runner.runtime();
99 runner.run_blocking_until_ctrl_c(
100 cmd.execute::<tempo_node::node::TempoNode>(runtime),
101 )?;
102 Ok(())
103 }
104 Self::Add(_) | Self::Update(_) | Self::Remove(_) | Self::List(_) => {
105 let code = tempo_ext::run(std::env::args_os()).map_err(|e| eyre!("{e}"))?;
106 if code != 0 {
107 std::process::exit(code);
108 }
109 Ok(())
110 }
111 }
112 }
113}
114
115#[derive(Debug, Subcommand)]
116pub(crate) enum ConsensusSubcommand {
117 AddValidator(AddValidator),
119 CreateAddValidatorSignature(CreateAddValidatorSignatureArgs),
121 RotateValidator(RotateValidator),
123 CreateRotateValidatorSignature(CreateRotateValidatorSignatureArgs),
125 GeneratePrivateKey(GeneratePrivateKey),
127 CalculatePublicKey(CalculatePublicKey),
129 ValidatorsInfo(ValidatorsInfo),
131}
132
133impl ConsensusSubcommand {
134 fn run(self) -> eyre::Result<()> {
135 match self {
136 Self::AddValidator(args) => args.run(),
137 Self::RotateValidator(args) => args.run(),
138 Self::CreateAddValidatorSignature(args) => args.run(),
139 Self::CreateRotateValidatorSignature(args) => args.run(),
140 Self::GeneratePrivateKey(args) => args.run(),
141 Self::CalculatePublicKey(args) => args.run(),
142 Self::ValidatorsInfo(args) => args.run(),
143 }
144 }
145}
146
147#[derive(Debug, clap::Args)]
149pub(crate) struct ValidatorIdentityArgs {
150 #[arg(long, value_name = "ETHEREUM_ADDRESS")]
152 validator_address: Address,
153 #[arg(long, value_name = "IDENTITY_KEY")]
155 public_key: B256,
156 #[arg(long, value_name = "IP:PORT")]
158 ingress: SocketAddr,
159 #[arg(long, value_name = "IP")]
161 egress: IpAddr,
162}
163
164impl ValidatorIdentityArgs {
165 fn to_config(&self, chain_id: u64) -> ValidatorConfig {
166 ValidatorConfig {
167 chain_id,
168 validator_address: self.validator_address,
169 public_key: self.public_key,
170 ingress: self.ingress,
171 egress: self.egress,
172 }
173 }
174}
175
176#[derive(Debug, clap::Args)]
178#[group(required = true, multiple = false)]
179pub(crate) struct SignatureArgs {
180 #[arg(long, value_name = "SIGNATURE")]
182 signature: Option<Bytes>,
183
184 #[arg(long, value_name = "FILE")]
187 signing_key: Option<PathBuf>,
188}
189
190#[derive(Debug, clap::Args)]
192pub(crate) struct ValidatorTransactionArgs {
193 #[command(flatten)]
194 sig: SignatureArgs,
195
196 #[arg(long, value_name = "FILE")]
198 private_key: PathBuf,
199
200 #[arg(long, value_name = "RPC_URL")]
202 rpc_url: String,
203}
204
205#[derive(Debug, clap::Args)]
206pub(crate) struct AddValidator {
207 #[command(flatten)]
208 identity: ValidatorIdentityArgs,
209 #[command(flatten)]
210 submit: ValidatorTransactionArgs,
211 #[arg(long, value_name = "ETHEREUM_ADDRESS")]
213 fee_recipient: Address,
214}
215
216impl AddValidator {
217 fn run(self) -> eyre::Result<()> {
218 tokio::runtime::Builder::new_current_thread()
219 .enable_all()
220 .build()
221 .wrap_err("failed constructing async runtime")?
222 .block_on(self.run_async())
223 }
224
225 async fn run_async(self) -> eyre::Result<()> {
226 let private_key_bytes =
227 std::fs::read(&self.submit.private_key).wrap_err("failed reading private key")?;
228 let private_key =
229 B256::try_from(private_key_bytes.as_slice()).wrap_err("invalid private key")?;
230
231 let signer = PrivateKeySigner::from_bytes(&private_key)?;
232 let provider = ProviderBuilder::new_with_network::<TempoNetwork>()
233 .fetch_chain_id()
234 .with_gas_estimation()
235 .wallet(signer)
236 .connect(&self.submit.rpc_url)
237 .await
238 .wrap_err("failed to connect to RPC")?;
239
240 let chain_id = provider
241 .get_chain_id()
242 .await
243 .wrap_err("failed to get chain id")?;
244
245 let config = self.identity.to_config(chain_id);
246
247 let signature = resolve_signature(
248 self.submit.sig.signature,
249 self.submit.sig.signing_key.as_deref(),
250 VALIDATOR_NS_ADD,
251 &config.add_validator_message_hash(self.fee_recipient),
252 )?;
253
254 config
255 .check_add_validator_signature(self.fee_recipient, signature.as_ref())
256 .wrap_err("add-validator signature check failed")?;
257
258 let calldata = IValidatorConfigV2::addValidatorCall {
259 validatorAddress: self.identity.validator_address,
260 publicKey: self.identity.public_key,
261 ingress: self.identity.ingress.to_string(),
262 egress: self.identity.egress.to_string(),
263 signature,
264 feeRecipient: self.fee_recipient,
265 };
266
267 let tx = TransactionRequest::default()
268 .to(VALIDATOR_CONFIG_V2_ADDRESS)
269 .input(calldata.abi_encode().into());
270
271 let pending = provider
272 .send_transaction(tx.into())
273 .await
274 .wrap_err("failed to send transaction")?;
275
276 let tx_hash = pending.tx_hash();
277 println!("transaction submitted: {tx_hash}");
278
279 Ok(())
280 }
281}
282
283#[derive(Debug, clap::Args)]
284pub(crate) struct RotateValidator {
285 #[command(flatten)]
286 identity: ValidatorIdentityArgs,
287 #[command(flatten)]
288 submit: ValidatorTransactionArgs,
289}
290
291impl RotateValidator {
292 fn run(self) -> eyre::Result<()> {
293 tokio::runtime::Builder::new_current_thread()
294 .enable_all()
295 .build()
296 .wrap_err("failed constructing async runtime")?
297 .block_on(self.run_async())
298 }
299
300 async fn run_async(self) -> eyre::Result<()> {
301 let private_key_bytes =
302 std::fs::read(&self.submit.private_key).wrap_err("failed reading private key")?;
303 let private_key =
304 B256::try_from(private_key_bytes.as_slice()).wrap_err("invalid private key")?;
305
306 let signer = PrivateKeySigner::from_bytes(&private_key)?;
307 let provider = ProviderBuilder::new_with_network::<TempoNetwork>()
308 .fetch_chain_id()
309 .with_gas_estimation()
310 .wallet(EthereumWallet::from(signer))
311 .connect(&self.submit.rpc_url)
312 .await
313 .wrap_err("failed to connect to RPC")?;
314
315 let chain_id = provider
316 .get_chain_id()
317 .await
318 .wrap_err("failed to get chain id")?;
319
320 let config = self.identity.to_config(chain_id);
321
322 let signature = resolve_signature(
323 self.submit.sig.signature,
324 self.submit.sig.signing_key.as_deref(),
325 VALIDATOR_NS_ROTATE,
326 &config.rotate_validator_message_hash(),
327 )?;
328
329 config
330 .check_rotate_validator_signature(signature.as_ref())
331 .wrap_err("rotate-validator signature check failed")?;
332
333 let validator_call_args = IValidatorConfigV2::validatorByAddressCall {
334 validatorAddress: self.identity.validator_address,
335 };
336 let validator_call_resp = provider
337 .call(
338 TransactionRequest::default()
339 .to(VALIDATOR_CONFIG_V2_ADDRESS)
340 .input(validator_call_args.abi_encode().into())
341 .into(),
342 )
343 .await
344 .wrap_err("failed to call validatorByAddress")?;
345
346 let validator =
347 IValidatorConfigV2::validatorByAddressCall::abi_decode_returns(&validator_call_resp)
348 .wrap_err("failed to decode validatorByAddress response")?;
349
350 let calldata = IValidatorConfigV2::rotateValidatorCall {
351 idx: validator.index,
352 publicKey: self.identity.public_key,
353 ingress: self.identity.ingress.to_string(),
354 egress: self.identity.egress.to_string(),
355 signature,
356 };
357
358 let tx = TransactionRequest::default()
359 .to(VALIDATOR_CONFIG_V2_ADDRESS)
360 .input(calldata.abi_encode().into());
361
362 let pending = provider
363 .send_transaction(tx.into())
364 .await
365 .wrap_err("failed to send transaction")?;
366
367 let tx_hash = pending.tx_hash();
368 println!("transaction submitted: {tx_hash}");
369
370 Ok(())
371 }
372}
373
374fn resolve_signature(
379 signature: Option<Bytes>,
380 signing_key: Option<&std::path::Path>,
381 namespace: &[u8],
382 message: &B256,
383) -> eyre::Result<Bytes> {
384 match (signature, signing_key) {
385 (Some(sig), _) => Ok(sig),
386 (None, Some(path)) => {
387 let key = SigningKey::read_from_file(path).wrap_err("failed reading signing key")?;
388 let private_key = key.into_inner();
389 let sig = private_key.sign(namespace, message.as_slice());
390 Ok(sig.encode().into())
391 }
392 (None, None) => Err(eyre!(
393 "either --signature or --signing-key must be provided"
394 )),
395 }
396}
397
398#[derive(Debug, clap::Args)]
399pub(crate) struct CreateAddValidatorSignatureArgs {
400 #[command(flatten)]
401 identity: ValidatorIdentityArgs,
402 #[arg(long, value_name = "ETHEREUM_ADDRESS")]
404 fee_recipient: Address,
405 #[arg(long, value_name = "RPC_URL")]
407 chain_id_from_rpc_url: String,
408 #[arg(long, value_name = "FILE")]
410 signing_key: PathBuf,
411}
412
413impl CreateAddValidatorSignatureArgs {
414 fn run(self) -> eyre::Result<()> {
415 tokio::runtime::Builder::new_current_thread()
416 .enable_all()
417 .build()
418 .wrap_err("failed constructing async runtime")?
419 .block_on(self.run_async())
420 }
421
422 async fn run_async(self) -> eyre::Result<()> {
423 let signing_key =
424 SigningKey::read_from_file(&self.signing_key).wrap_err("failed reading signing key")?;
425
426 let provider = ProviderBuilder::new_with_network::<TempoNetwork>()
427 .connect(&self.chain_id_from_rpc_url)
428 .await
429 .wrap_err("failed to connect to RPC")?;
430
431 let chain_id = provider
432 .get_chain_id()
433 .await
434 .wrap_err("failed to get chain id")?;
435
436 let config = self.identity.to_config(chain_id);
437 let message = config.add_validator_message_hash(self.fee_recipient);
438
439 let private_key = signing_key.into_inner();
440 let signature = private_key.sign(VALIDATOR_NS_ADD, message.as_slice());
441 let encoded = signature.encode();
442 println!("{}", alloy_primitives::hex::encode_prefixed(encoded));
443 Ok(())
444 }
445}
446
447#[derive(Debug, clap::Args)]
448pub(crate) struct CreateRotateValidatorSignatureArgs {
449 #[command(flatten)]
450 identity: ValidatorIdentityArgs,
451 #[arg(long, value_name = "RPC_URL")]
453 chain_id_from_rpc_url: String,
454 #[arg(long, value_name = "FILE")]
456 signing_key: PathBuf,
457}
458
459impl CreateRotateValidatorSignatureArgs {
460 fn run(self) -> eyre::Result<()> {
461 tokio::runtime::Builder::new_current_thread()
462 .enable_all()
463 .build()
464 .wrap_err("failed constructing async runtime")?
465 .block_on(self.run_async())
466 }
467
468 async fn run_async(self) -> eyre::Result<()> {
469 let signing_key =
470 SigningKey::read_from_file(&self.signing_key).wrap_err("failed reading signing key")?;
471
472 let provider = ProviderBuilder::new_with_network::<TempoNetwork>()
473 .connect(&self.chain_id_from_rpc_url)
474 .await
475 .wrap_err("failed to connect to RPC")?;
476
477 let chain_id = provider
478 .get_chain_id()
479 .await
480 .wrap_err("failed to get chain id")?;
481
482 let config = self.identity.to_config(chain_id);
483 let message = config.rotate_validator_message_hash();
484
485 let private_key = signing_key.into_inner();
486 let signature = private_key.sign(VALIDATOR_NS_ROTATE, message.as_slice());
487 let encoded = signature.encode();
488 println!("{}", alloy_primitives::hex::encode_prefixed(encoded));
489 Ok(())
490 }
491}
492
493#[derive(Debug, clap::Args)]
494pub(crate) struct GeneratePrivateKey {
495 #[arg(long, short, value_name = "FILE")]
497 output: PathBuf,
498
499 #[arg(long, short)]
501 force: bool,
502}
503
504impl GeneratePrivateKey {
505 fn run(self) -> eyre::Result<()> {
506 let Self { output, force } = self;
507 let signing_key = PrivateKey::random(&mut rand_08::thread_rng());
508 let public_key = signing_key.public_key();
509 let signing_key = SigningKey::from(signing_key);
510 OpenOptions::new()
511 .write(true)
512 .create_new(!force)
513 .create(force)
514 .truncate(force)
515 .open(&output)
516 .map_err(Report::new)
517 .and_then(|f| signing_key.to_writer(f).map_err(Report::new))
518 .wrap_err_with(|| format!("failed writing private key to `{}`", output.display()))?;
519 eprintln!(
520 "wrote private key to: {}\npublic key: {public_key}",
521 output.display()
522 );
523 Ok(())
524 }
525}
526
527#[derive(Debug, clap::Args)]
528pub(crate) struct CalculatePublicKey {
529 #[arg(long, short, value_name = "FILE")]
531 private_key: PathBuf,
532}
533
534impl CalculatePublicKey {
535 fn run(self) -> eyre::Result<()> {
536 let Self { private_key } = self;
537 let private_key = SigningKey::read_from_file(&private_key).wrap_err_with(|| {
538 format!(
539 "failed reading private key from `{}`",
540 private_key.display()
541 )
542 })?;
543 let validating_key = private_key.public_key();
544 println!("public key: {validating_key}");
545 Ok(())
546 }
547}
548
549#[derive(Debug, Serialize)]
551struct ValidatorInfoOutput {
552 current_epoch: u64,
554 current_height: u64,
556 last_boundary: u64,
558 epoch_length: u64,
560 is_next_full_dkg: bool,
562 next_full_dkg_epoch: u64,
564 validators: Vec<ValidatorEntry>,
566}
567
568#[derive(Debug, Serialize)]
570struct ValidatorEntry {
571 onchain_address: Address,
573 public_key: String,
575 inbound_address: String,
577 outbound_address: String,
579 active: bool,
581 is_dkg_dealer: bool,
583 is_dkg_player: bool,
585 in_committee: bool,
587}
588
589#[derive(Debug, clap::Args)]
590pub(crate) struct ValidatorsInfo {
591 #[arg(long, short, default_value = "mainnet", value_parser = tempo_chainspec::spec::chain_value_parser)]
593 chain: Arc<TempoChainSpec>,
594
595 #[arg(long, default_value = "https://rpc.presto.tempo.xyz")]
597 rpc_url: String,
598
599 #[arg(long)]
601 with_historic: bool,
602}
603
604impl ValidatorsInfo {
605 fn run(self) -> eyre::Result<()> {
606 tokio::runtime::Builder::new_current_thread()
607 .enable_all()
608 .build()
609 .wrap_err("failed constructing async runtime")?
610 .block_on(self.run_async())
611 }
612
613 async fn run_async(self) -> eyre::Result<()> {
614 use alloy_consensus::BlockHeader;
615 use alloy_provider::ProviderBuilder;
616
617 let epoch_length = self
618 .chain
619 .info
620 .epoch_length()
621 .ok_or_eyre("epochLength not found in chainspec")?;
622
623 let provider = ProviderBuilder::new_with_network::<TempoNetwork>()
624 .connect(&self.rpc_url)
625 .await
626 .wrap_err("failed to connect to RPC")?;
627
628 let latest_block_number = provider
629 .get_block_number()
630 .await
631 .wrap_err("failed to get latest block number")?;
632
633 let epoch_strategy = FixedEpocher::new(NZU64!(epoch_length));
634 let current_height = Height::new(latest_block_number);
635 let current_epoch_info = epoch_strategy
636 .containing(current_height)
637 .ok_or_else(|| eyre!("failed to determine epoch for height {latest_block_number}"))?;
638
639 let current_epoch = current_epoch_info.epoch();
640 let boundary_height = current_epoch
641 .previous()
642 .map(|epoch| epoch_strategy.last(epoch).expect("valid epoch"))
643 .unwrap_or_default();
644
645 let boundary_block = provider
646 .get_block_by_number(boundary_height.get().into())
647 .hashes()
648 .await
649 .wrap_err_with(|| {
650 format!(
651 "failed to get block header at height {}",
652 boundary_height.get()
653 )
654 })?
655 .ok_or_eyre("boundary block not found")?;
656
657 let extra_data = boundary_block.header.extra_data();
658 if extra_data.is_empty() {
659 return Err(eyre!(
660 "boundary block at height {} has no DKG outcome in extra_data",
661 boundary_height.get()
662 ));
663 }
664
665 let dkg_outcome = OnchainDkgOutcome::read(&mut extra_data.as_ref())
666 .wrap_err("failed to decode DKG outcome from extra_data")?;
667
668 let validators_result = provider
669 .call(
670 TransactionRequest::default()
671 .to(VALIDATOR_CONFIG_ADDRESS)
672 .input(IValidatorConfig::getValidatorsCall {}.abi_encode().into())
673 .into(),
674 )
675 .await
676 .wrap_err("failed to call getValidators")?;
677
678 let decoded_validators =
679 IValidatorConfig::getValidatorsCall::abi_decode_returns(&validators_result)
680 .wrap_err("failed to decode getValidators response")?;
681
682 let next_dkg_result = provider
683 .call(
684 TransactionRequest::default()
685 .to(VALIDATOR_CONFIG_ADDRESS)
686 .input(
687 IValidatorConfig::getNextFullDkgCeremonyCall {}
688 .abi_encode()
689 .into(),
690 )
691 .into(),
692 )
693 .await
694 .wrap_err("failed to call getNextFullDkgCeremony")?;
695 let decoded_next_dkg =
696 IValidatorConfig::getNextFullDkgCeremonyCall::abi_decode_returns(&next_dkg_result)
697 .wrap_err("failed to decode getNextFullDkgCeremony response")?;
698
699 let mut validator_entries = Vec::with_capacity(decoded_validators.len());
700 for validator in decoded_validators.into_iter() {
701 let pubkey_bytes = validator.publicKey.0;
702 let key = PublicKey::decode(&mut &validator.publicKey.0[..])
703 .wrap_err("failed decoding on-chain ed25519 key")?;
704
705 let in_committee = dkg_outcome.players().position(&key).is_some();
706
707 if self.with_historic || (validator.active || in_committee) {
708 validator_entries.push(ValidatorEntry {
709 onchain_address: validator.validatorAddress,
710 public_key: alloy_primitives::hex::encode(pubkey_bytes),
711 inbound_address: validator.inboundAddress,
712 outbound_address: validator.outboundAddress,
713 active: validator.active,
714 is_dkg_dealer: dkg_outcome.players().position(&key).is_some(),
715 is_dkg_player: dkg_outcome.next_players().position(&key).is_some(),
716 in_committee,
717 });
718 }
719 }
720
721 let output = ValidatorInfoOutput {
722 current_epoch: current_epoch.get(),
723 current_height: current_height.get(),
724 last_boundary: boundary_height.get(),
725 epoch_length,
726 is_next_full_dkg: dkg_outcome.is_next_full_dkg,
727 next_full_dkg_epoch: decoded_next_dkg,
728 validators: validator_entries,
729 };
730
731 println!("{}", serde_json::to_string_pretty(&output)?);
732 Ok(())
733 }
734}