mithril_aggregator/commands/
genesis_command.rs

1use std::{collections::HashMap, path::PathBuf, sync::Arc};
2
3use anyhow::Context;
4use clap::{Parser, Subcommand};
5use config::{ConfigBuilder, Map, Value, builder::DefaultState};
6use serde::{Deserialize, Serialize};
7use slog::{Logger, debug};
8
9use mithril_cardano_node_chain::chain_observer::ChainObserverType;
10use mithril_common::{
11    StdResult,
12    crypto_helper::{
13        ProtocolGenesisSecretKey, ProtocolGenesisSigner, ProtocolGenesisVerificationKey,
14    },
15    entities::{HexEncodedGenesisSecretKey, HexEncodedGenesisVerificationKey},
16};
17use mithril_doc::{Documenter, StructDoc};
18
19use crate::{
20    ConfigurationSource, ExecutionEnvironment, dependency_injection::DependenciesBuilder,
21    extract_all, tools::GenesisTools,
22};
23
24#[derive(Debug, Clone, Serialize, Deserialize, Documenter)]
25pub struct GenesisCommandConfiguration {
26    /// Cardano CLI tool path
27    #[example = "`cardano-cli`"]
28    pub cardano_cli_path: Option<PathBuf>,
29
30    /// Path of the socket opened by the Cardano node
31    #[example = "`/ipc/node.socket`"]
32    pub cardano_node_socket_path: PathBuf,
33
34    /// Cardano Network Magic number
35    ///
36    /// useful for TestNet & DevNet
37    #[example = "`1097911063` or `42`"]
38    pub network_magic: Option<u64>,
39
40    /// Cardano network
41    #[example = "`testnet` or `mainnet` or `devnet`"]
42    network: String,
43
44    /// Cardano chain observer type
45    pub chain_observer_type: ChainObserverType,
46
47    /// Directory to store aggregator data (Certificates, Snapshots, Protocol Parameters, ...)
48    #[example = "`./mithril-aggregator/stores`"]
49    pub data_stores_directory: PathBuf,
50}
51
52impl ConfigurationSource for GenesisCommandConfiguration {
53    fn environment(&self) -> ExecutionEnvironment {
54        ExecutionEnvironment::Production
55    }
56
57    fn cardano_cli_path(&self) -> PathBuf {
58        match self.chain_observer_type {
59            ChainObserverType::CardanoCli => {
60                if let Some(path) = &self.cardano_cli_path {
61                    path.clone()
62                } else {
63                    panic!("Cardano CLI path must be set when using Cardano CLI chain observer")
64                }
65            }
66            _ => PathBuf::new(),
67        }
68    }
69
70    fn cardano_node_socket_path(&self) -> PathBuf {
71        self.cardano_node_socket_path.clone()
72    }
73
74    fn network_magic(&self) -> Option<u64> {
75        self.network_magic
76    }
77
78    fn network(&self) -> String {
79        self.network.clone()
80    }
81
82    fn chain_observer_type(&self) -> ChainObserverType {
83        self.chain_observer_type.clone()
84    }
85
86    fn data_stores_directory(&self) -> PathBuf {
87        self.data_stores_directory.clone()
88    }
89
90    fn store_retention_limit(&self) -> Option<usize> {
91        None
92    }
93}
94
95/// Genesis tools
96#[derive(Parser, Debug, Clone)]
97pub struct GenesisCommand {
98    /// commands
99    #[clap(subcommand)]
100    pub genesis_subcommand: GenesisSubCommand,
101}
102
103impl GenesisCommand {
104    pub async fn execute(
105        &self,
106        root_logger: Logger,
107        config_builder: ConfigBuilder<DefaultState>,
108    ) -> StdResult<()> {
109        self.genesis_subcommand.execute(root_logger, config_builder).await
110    }
111
112    pub fn extract_config(command_path: String) -> HashMap<String, StructDoc> {
113        extract_all!(
114            command_path,
115            GenesisSubCommand,
116            Export = { ExportGenesisSubCommand },
117            Import = { ImportGenesisSubCommand },
118            Sign = { SignGenesisSubCommand },
119            Bootstrap = { BootstrapGenesisSubCommand },
120            GenerateKeypair = { GenerateKeypairGenesisSubCommand },
121        )
122    }
123}
124
125/// Genesis tools commands.
126#[derive(Debug, Clone, Subcommand)]
127pub enum GenesisSubCommand {
128    /// Genesis certificate export command.
129    Export(ExportGenesisSubCommand),
130
131    /// Genesis certificate import command.
132    Import(ImportGenesisSubCommand),
133
134    /// Genesis certificate sign command.
135    Sign(SignGenesisSubCommand),
136
137    /// Genesis certificate bootstrap command.
138    Bootstrap(BootstrapGenesisSubCommand),
139
140    /// Genesis keypair generation command.
141    GenerateKeypair(GenerateKeypairGenesisSubCommand),
142}
143
144impl GenesisSubCommand {
145    pub async fn execute(
146        &self,
147        root_logger: Logger,
148        config_builder: ConfigBuilder<DefaultState>,
149    ) -> StdResult<()> {
150        match self {
151            Self::Bootstrap(cmd) => cmd.execute(root_logger, config_builder).await,
152            Self::Export(cmd) => cmd.execute(root_logger, config_builder).await,
153            Self::Import(cmd) => cmd.execute(root_logger, config_builder).await,
154            Self::Sign(cmd) => cmd.execute(root_logger).await,
155            Self::GenerateKeypair(cmd) => cmd.execute(root_logger).await,
156        }
157    }
158}
159
160/// Genesis certificate export command
161#[derive(Parser, Debug, Clone)]
162pub struct ExportGenesisSubCommand {
163    /// Target path
164    #[clap(long)]
165    target_path: PathBuf,
166}
167
168impl ExportGenesisSubCommand {
169    pub async fn execute(
170        &self,
171        root_logger: Logger,
172        config_builder: ConfigBuilder<DefaultState>,
173    ) -> StdResult<()> {
174        let config: GenesisCommandConfiguration = config_builder
175            .build()
176            .with_context(|| "configuration build error")?
177            .try_deserialize()
178            .with_context(|| "configuration deserialize error")?;
179        debug!(root_logger, "EXPORT GENESIS command"; "config" => format!("{config:?}"));
180        println!(
181            "Genesis export payload to sign to {}",
182            self.target_path.display()
183        );
184        let mut dependencies_builder =
185            DependenciesBuilder::new(root_logger.clone(), Arc::new(config.clone()));
186        let dependencies = dependencies_builder.create_genesis_container().await.with_context(
187            || "Dependencies Builder can not create genesis command dependencies container",
188        )?;
189
190        let genesis_tools = GenesisTools::from_dependencies(dependencies)
191            .await
192            .with_context(|| "genesis-tools: initialization error")?;
193        genesis_tools
194            .export_payload_to_sign(&self.target_path)
195            .with_context(|| "genesis-tools: export error")?;
196        Ok(())
197    }
198
199    pub fn extract_config(command_path: String) -> HashMap<String, StructDoc> {
200        HashMap::from([(command_path, GenesisCommandConfiguration::extract())])
201    }
202}
203
204#[derive(Parser, Debug, Clone)]
205pub struct ImportGenesisSubCommand {
206    /// Signed Payload Path
207    #[clap(long)]
208    signed_payload_path: PathBuf,
209
210    /// Genesis Verification Key
211    #[clap(long)]
212    genesis_verification_key: HexEncodedGenesisVerificationKey,
213}
214
215impl ImportGenesisSubCommand {
216    pub async fn execute(
217        &self,
218        root_logger: Logger,
219        config_builder: ConfigBuilder<DefaultState>,
220    ) -> StdResult<()> {
221        let config: GenesisCommandConfiguration = config_builder
222            .build()
223            .with_context(|| "configuration build error")?
224            .try_deserialize()
225            .with_context(|| "configuration deserialize error")?;
226        debug!(root_logger, "IMPORT GENESIS command"; "config" => format!("{config:?}"));
227        println!(
228            "Genesis import signed payload from {}",
229            self.signed_payload_path.to_string_lossy()
230        );
231        let mut dependencies_builder =
232            DependenciesBuilder::new(root_logger.clone(), Arc::new(config.clone()));
233        let dependencies = dependencies_builder.create_genesis_container().await.with_context(
234            || "Dependencies Builder can not create genesis command dependencies container",
235        )?;
236
237        let genesis_tools = GenesisTools::from_dependencies(dependencies)
238            .await
239            .with_context(|| "genesis-tools: initialization error")?;
240        genesis_tools
241            .import_payload_signature(
242                &self.signed_payload_path,
243                &ProtocolGenesisVerificationKey::from_json_hex(&self.genesis_verification_key)?,
244            )
245            .await
246            .with_context(|| "genesis-tools: import error")?;
247        Ok(())
248    }
249
250    pub fn extract_config(command_path: String) -> HashMap<String, StructDoc> {
251        HashMap::from([(command_path, GenesisCommandConfiguration::extract())])
252    }
253}
254
255#[derive(Parser, Debug, Clone)]
256pub struct SignGenesisSubCommand {
257    /// To Sign Payload Path
258    #[clap(long)]
259    to_sign_payload_path: PathBuf,
260
261    /// Target Signed Payload Path
262    #[clap(long)]
263    target_signed_payload_path: PathBuf,
264
265    /// Genesis Secret Key Path
266    #[clap(long)]
267    genesis_secret_key_path: PathBuf,
268}
269
270impl SignGenesisSubCommand {
271    pub async fn execute(&self, root_logger: Logger) -> StdResult<()> {
272        debug!(root_logger, "SIGN GENESIS command");
273        println!(
274            "Genesis sign payload from {} to {}",
275            self.to_sign_payload_path.to_string_lossy(),
276            self.target_signed_payload_path.to_string_lossy()
277        );
278
279        GenesisTools::sign_genesis_certificate(
280            &self.to_sign_payload_path,
281            &self.target_signed_payload_path,
282            &self.genesis_secret_key_path,
283        )
284        .await
285        .with_context(|| "genesis-tools: sign error")?;
286
287        Ok(())
288    }
289
290    pub fn extract_config(_command_path: String) -> HashMap<String, StructDoc> {
291        HashMap::new()
292    }
293}
294#[derive(Parser, Debug, Clone)]
295pub struct BootstrapGenesisSubCommand {
296    /// Genesis Secret Key (test only)
297    #[clap(long, env = "GENESIS_SECRET_KEY")]
298    genesis_secret_key: HexEncodedGenesisSecretKey,
299}
300
301impl BootstrapGenesisSubCommand {
302    pub async fn execute(
303        &self,
304        root_logger: Logger,
305        config_builder: ConfigBuilder<DefaultState>,
306    ) -> StdResult<()> {
307        let config: GenesisCommandConfiguration = config_builder
308            .build()
309            .with_context(|| "configuration build error")?
310            .try_deserialize()
311            .with_context(|| "configuration deserialize error")?;
312        debug!(root_logger, "BOOTSTRAP GENESIS command"; "config" => format!("{config:?}"));
313        println!("Genesis bootstrap for test only!");
314        let mut dependencies_builder =
315            DependenciesBuilder::new(root_logger.clone(), Arc::new(config.clone()));
316        let dependencies = dependencies_builder.create_genesis_container().await.with_context(
317            || "Dependencies Builder can not create genesis command dependencies container",
318        )?;
319
320        let genesis_tools = GenesisTools::from_dependencies(dependencies)
321            .await
322            .with_context(|| "genesis-tools: initialization error")?;
323        let genesis_secret_key = ProtocolGenesisSecretKey::from_json_hex(&self.genesis_secret_key)
324            .with_context(|| "json hex decode of genesis secret key failure")?;
325        let genesis_signer = ProtocolGenesisSigner::from_secret_key(genesis_secret_key);
326        genesis_tools
327            .bootstrap_test_genesis_certificate(genesis_signer)
328            .await
329            .with_context(|| "genesis-tools: bootstrap error")?;
330        Ok(())
331    }
332
333    pub fn extract_config(command_path: String) -> HashMap<String, StructDoc> {
334        HashMap::from([(command_path, GenesisCommandConfiguration::extract())])
335    }
336}
337
338/// Genesis keypair generation command.
339#[derive(Parser, Debug, Clone)]
340pub struct GenerateKeypairGenesisSubCommand {
341    /// Target path for the generated keypair
342    #[clap(long)]
343    target_path: PathBuf,
344}
345
346impl GenerateKeypairGenesisSubCommand {
347    pub async fn execute(&self, root_logger: Logger) -> StdResult<()> {
348        debug!(root_logger, "GENERATE KEYPAIR GENESIS command");
349        println!(
350            "Genesis generate keypair to {}",
351            self.target_path.to_string_lossy()
352        );
353
354        GenesisTools::create_and_save_genesis_keypair(&self.target_path)
355            .with_context(|| "genesis-tools: keypair generation error")?;
356
357        Ok(())
358    }
359
360    pub fn extract_config(_parent: String) -> HashMap<String, StructDoc> {
361        HashMap::new()
362    }
363}
364
365#[cfg(test)]
366mod tests {
367    use std::sync::Arc;
368
369    use mithril_common::temp_dir;
370
371    use crate::test_tools::TestLogger;
372
373    use super::*;
374
375    #[tokio::test]
376    async fn create_container_does_not_panic() {
377        let config = GenesisCommandConfiguration {
378            cardano_cli_path: None,
379            cardano_node_socket_path: PathBuf::new(),
380            network_magic: Some(42),
381            network: "devnet".to_string(),
382            chain_observer_type: ChainObserverType::Fake,
383            data_stores_directory: temp_dir!().join("stores"),
384        };
385        let mut dependencies_builder =
386            DependenciesBuilder::new(TestLogger::stdout(), Arc::new(config));
387
388        dependencies_builder
389            .create_genesis_container()
390            .await
391            .expect("Expected container creation to succeed without panicking");
392    }
393}