Skip to main content

tempo/
tempo_cmd.rs

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/// Passthrough args for extension management commands.
40///
41/// These commands are defined here so they appear in `tempo --help`, but
42/// the actual implementation lives in `tempo_ext::run()`. We capture all
43/// trailing arguments and re-dispatch.
44#[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/// Tempo-specific subcommands that extend the reth CLI.
51#[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    /// Consensus-related commands.
58    #[command(subcommand)]
59    Consensus(ConsensusSubcommand),
60
61    /// Initialize state from a binary dump file.
62    ///
63    /// Loads TIP20 storage slots from a binary file generated by `tempo-xtask generate-state-bloat`
64    /// and applies them to the genesis state.
65    InitFromBinaryDump(Box<init_state::InitFromBinaryDump<TempoChainSpecParser>>),
66
67    /// Install an extension (e.g., `tempo add wallet`).
68    #[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    /// Update tempo and/or extensions.
75    #[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    /// Remove an extension.
82    #[command(
83        override_usage = "tempo remove <EXT>",
84        after_help = "Example: tempo remove wallet"
85    )]
86    Remove(ExtArgs),
87
88    /// List installed extensions.
89    #[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    /// Add a new validator to the validator config contract.
118    AddValidator(AddValidator),
119    /// Create an ed25519 signature for `addValidator`.
120    CreateAddValidatorSignature(CreateAddValidatorSignatureArgs),
121    /// Rotate a validator to a new identity.
122    RotateValidator(RotateValidator),
123    /// Create an ed25519 signature for `rotateValidator`.
124    CreateRotateValidatorSignature(CreateRotateValidatorSignatureArgs),
125    /// Generates an ed25519 signing key pair to be used in consensus.
126    GeneratePrivateKey(GeneratePrivateKey),
127    /// Calculates the public key from an ed25519 signing key.
128    CalculatePublicKey(CalculatePublicKey),
129    /// Query validator info from the previous epoch's DKG outcome and current contract state.
130    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/// Shared validator identity arguments used across add/rotate/sign commands.
148#[derive(Debug, clap::Args)]
149pub(crate) struct ValidatorIdentityArgs {
150    /// The validator's address
151    #[arg(long, value_name = "ETHEREUM_ADDRESS")]
152    validator_address: Address,
153    /// The identity key of the validator (0x-prefixed hex).
154    #[arg(long, value_name = "IDENTITY_KEY")]
155    public_key: B256,
156    /// The inbound address for the validator.
157    #[arg(long, value_name = "IP:PORT")]
158    ingress: SocketAddr,
159    /// The outbound address for the validator.
160    #[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/// Either a pre-computed signature or a signing key to compute it from.
177#[derive(Debug, clap::Args)]
178#[group(required = true, multiple = false)]
179pub(crate) struct SignatureArgs {
180    /// A pre-computed ed25519 signature over the validator identity.
181    #[arg(long, value_name = "SIGNATURE")]
182    signature: Option<Bytes>,
183
184    /// Path to the ed25519 signing key file. The signature is computed
185    /// automatically so a separate `create-*-signature` step is not needed.
186    #[arg(long, value_name = "FILE")]
187    signing_key: Option<PathBuf>,
188}
189
190/// Shared arguments for commands that update the validator config contract.
191#[derive(Debug, clap::Args)]
192pub(crate) struct ValidatorTransactionArgs {
193    #[command(flatten)]
194    sig: SignatureArgs,
195
196    /// Path to the file holding the Ethereum private key.
197    #[arg(long, value_name = "FILE")]
198    private_key: PathBuf,
199
200    /// The RPC URL to submit the transaction to.
201    #[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    /// The fee recipient address
212    #[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
374/// Resolves the ed25519 signature for a validator config transaction.
375///
376/// If a pre-computed `signature` is provided, it is returned as-is.
377/// Otherwise, the signature is computed from the `signing_key` file.
378fn 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    /// The fee recipient address
403    #[arg(long, value_name = "ETHEREUM_ADDRESS")]
404    fee_recipient: Address,
405    /// RPC used to fetch the chain id
406    #[arg(long, value_name = "RPC_URL")]
407    chain_id_from_rpc_url: String,
408    /// Path to the ed25519 signing key file.
409    #[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    /// RPC used to fetch the chain id
452    #[arg(long, value_name = "RPC_URL")]
453    chain_id_from_rpc_url: String,
454    /// Path to the ed25519 signing key file.
455    #[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    /// Destination of the generated signing key.
496    #[arg(long, short, value_name = "FILE")]
497    output: PathBuf,
498
499    /// Whether to override `output`, if it already exists.
500    #[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    /// Private key to calculate the public key from.
530    #[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/// Validator info output structure
550#[derive(Debug, Serialize)]
551struct ValidatorInfoOutput {
552    /// The current epoch (at the time of query)
553    current_epoch: u64,
554    /// The current height (at the time of query)
555    current_height: u64,
556    // The boundary height from which the DKG outcome was read
557    last_boundary: u64,
558    // The epoch length as set in the chain spec
559    epoch_length: u64,
560    /// Whether this is a full DKG (new polynomial) or reshare
561    is_next_full_dkg: bool,
562    /// The epoch at which the next full DKG ceremony will be triggered (from contract)
563    next_full_dkg_epoch: u64,
564    /// List of validators participating in the DKG
565    validators: Vec<ValidatorEntry>,
566}
567
568/// Individual validator entry
569#[derive(Debug, Serialize)]
570struct ValidatorEntry {
571    /// onchain address of the validator
572    onchain_address: Address,
573    /// ed25519 public key (hex)
574    public_key: String,
575    /// Inbound IP address for p2p connections
576    inbound_address: String,
577    /// Outbound IP address
578    outbound_address: String,
579    /// Whether the validator is active in the current contract state
580    active: bool,
581    // Whether the validator is a dealer in th ecurrent epoch.
582    is_dkg_dealer: bool,
583    /// Whether the validator is a player in the current epoch.
584    is_dkg_player: bool,
585    /// Whether the validator is in the committee for the given epoch.
586    in_committee: bool,
587}
588
589#[derive(Debug, clap::Args)]
590pub(crate) struct ValidatorsInfo {
591    /// Chain to query (presto, testnet, moderato, or path to chainspec file)
592    #[arg(long, short, default_value = "mainnet", value_parser = tempo_chainspec::spec::chain_value_parser)]
593    chain: Arc<TempoChainSpec>,
594
595    /// RPC URL to query. Defaults to <https://rpc.presto.tempo.xyz>
596    #[arg(long, default_value = "https://rpc.presto.tempo.xyz")]
597    rpc_url: String,
598
599    /// Whethr to include historic validators (deactivated and not in the current committee).
600    #[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}