mina_cli/commands/
misc.rs

1use libp2p_identity::PeerId;
2use mina_node::{account::AccountSecretKey, p2p::identity::SecretKey};
3use std::{fs::File, io::Write};
4
5#[derive(Debug, clap::Args)]
6pub struct Misc {
7    #[command(subcommand)]
8    command: MiscCommand,
9}
10
11impl Misc {
12    pub fn run(self) -> anyhow::Result<()> {
13        match self.command {
14            MiscCommand::MinaEncryptedKey(command) => command.run(),
15            MiscCommand::MinaKeyPair(command) => command.run(),
16            MiscCommand::P2PKeyPair(command) => command.run(),
17        }
18    }
19}
20
21#[derive(Clone, Debug, clap::Subcommand)]
22pub enum MiscCommand {
23    MinaEncryptedKey(MinaEncryptedKey),
24    MinaKeyPair(MinaKeyPair),
25    P2PKeyPair(P2PKeyPair),
26}
27
28#[derive(Debug, Clone, clap::Args)]
29pub struct P2PKeyPair {
30    #[arg(long, short = 's', env = "MINA_P2P_SEC_KEY")]
31    p2p_secret_key: Option<SecretKey>,
32}
33
34impl P2PKeyPair {
35    pub fn run(self) -> anyhow::Result<()> {
36        let secret_key = self.p2p_secret_key.unwrap_or_else(SecretKey::rand);
37        let public_key = secret_key.public_key();
38        let peer_id = public_key.peer_id();
39        let libp2p_peer_id = PeerId::try_from(peer_id)?;
40        println!("secret key: {secret_key}");
41        println!("public key: {public_key}");
42        println!("peer_id:    {peer_id}");
43        println!("libp2p_id:  {libp2p_peer_id}");
44
45        Ok(())
46    }
47}
48
49/// Generates an unencrypted Mina key pair.
50///
51/// Outputs the public and private keys in a human-readable format.
52/// For encrypted key storage suitable for block production, use
53/// the `mina-encrypted-key` command instead.
54#[derive(Debug, Clone, Default, clap::Args)]
55pub struct MinaKeyPair {
56    #[arg(long, short = 's', env = "MINA_SEC_KEY")]
57    secret_key: Option<AccountSecretKey>,
58}
59
60impl MinaKeyPair {
61    pub fn run(self) -> anyhow::Result<()> {
62        let private_key = self.secret_key.unwrap_or_else(AccountSecretKey::rand);
63        let public_key = private_key.public_key();
64
65        println!("secret key: {private_key}");
66        println!("public key: {public_key}");
67
68        Ok(())
69    }
70}
71
72/// Generate a new Mina key pair and save it as an encrypted JSON file
73///
74/// This command generates a new random Mina key pair (or uses a provided secret key)
75/// and saves it to an encrypted JSON file format compatible with key generation
76/// from the OCaml implementation.
77/// The encrypted file can be used as a producer key for block production.
78///
79/// This command replicates the secret box functionality from `src/lib/secret_box`
80/// in the OCaml implementation, providing compatible encrypted key storage.
81///
82/// # Examples
83///
84/// Generate a new encrypted key with password:
85/// ```bash
86/// mina misc mina-encrypted-key --password mypassword --file producer-key
87/// ```
88///
89/// Generate a new encrypted key using environment variable for password:
90/// ```bash
91/// MINA_PRIVKEY_PASS=mypassword mina misc mina-encrypted-key --file producer-key
92/// ```
93///
94/// Use an existing secret key:
95/// ```bash
96/// mina misc mina-encrypted-key --secret-key EKE... --password mypassword
97/// ```
98#[derive(Debug, Clone, clap::Args)]
99pub struct MinaEncryptedKey {
100    /// Optional existing secret key to encrypt. If not provided, generates a
101    /// new random key
102    #[arg(long, short = 's', env = "MINA_ENC_KEY")]
103    secret_key: Option<AccountSecretKey>,
104
105    /// Password to encrypt the key file with. Can be provided via
106    /// MINA_PRIVKEY_PASS environment variable
107    #[arg(env = "MINA_PRIVKEY_PASS", default_value = "")]
108    password: String,
109
110    /// Output file path for the encrypted key (default: mina_encrypted_key.json)
111    #[arg(long, short = 'f', default_value = "mina_encrypted_key.json")]
112    file: String,
113}
114
115impl MinaEncryptedKey {
116    /// Execute the mina-encrypted-key command
117    ///
118    /// Generates a new Mina key pair (or uses provided secret key) and saves it
119    /// as an encrypted JSON file that can be used for block production.
120    ///
121    /// It will also save the public key to the filename suffixed with `.pub`.
122    ///
123    /// # Returns
124    ///
125    /// * `Ok(())` - On successful key generation and file creation
126    /// * `Err(anyhow::Error)` - If key encryption or file writing fails
127    ///
128    /// # Output
129    ///
130    /// Prints the secret key and public key to stdout, and creates an encrypted
131    /// JSON file at the specified path.
132    pub fn run(self) -> anyhow::Result<()> {
133        let secret_key = self.secret_key.unwrap_or_else(AccountSecretKey::rand);
134        let public_key = secret_key.public_key();
135
136        // Save the public key to a separate file
137        let public_key_file = format!("{}.pub", self.file);
138
139        if File::open(&public_key_file).is_ok() {
140            return Err(anyhow::anyhow!(
141                "Public key file '{}' already exists. Please choose a different file name.",
142                public_key_file
143            ));
144        }
145
146        secret_key
147            .to_encrypted_file(&self.file, &self.password)
148            .map_err(|e| {
149                anyhow::anyhow!("Failed to encrypt key: {} into path '{}'", e, self.file,)
150            })?;
151        // Write the public key to the file
152        let mut public_key_file = File::create(public_key_file)
153            .map_err(|e| anyhow::anyhow!("Failed to create public key file: {}", e))?;
154        public_key_file
155            .write_all(public_key.to_string().as_bytes())
156            .map_err(|e| anyhow::anyhow!("Failed to write public key: {}", e))?;
157
158        println!("secret key: {secret_key}");
159        println!("public key: {public_key}");
160        Ok(())
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167    use std::fs;
168    use tempfile::TempDir;
169
170    #[test]
171    fn test_mina_encrypted_key_generates_random_key() {
172        let temp_dir = TempDir::new().unwrap();
173        let file_path = temp_dir.path().join("test_key.json");
174        let file_path_str = file_path.to_str().unwrap().to_string();
175
176        let cmd = MinaEncryptedKey {
177            secret_key: None,
178            password: "test_password".to_string(),
179            file: file_path_str.clone(),
180        };
181
182        let result = cmd.run();
183        assert!(result.is_ok());
184
185        // Verify the file was created
186        assert!(file_path.exists());
187
188        // Verify the file contains encrypted data (should be JSON)
189        let file_content = fs::read_to_string(&file_path).unwrap();
190        assert!(file_content.starts_with('{'));
191        assert!(file_content.ends_with('}'));
192    }
193
194    #[test]
195    fn test_mina_encrypted_key_with_provided_secret_key() {
196        let temp_dir = TempDir::new().unwrap();
197        let file_path = temp_dir.path().join("test_key_provided.json");
198        let file_path_str = file_path.to_str().unwrap().to_string();
199
200        let secret_key = AccountSecretKey::rand();
201        let expected_public_key = secret_key.public_key();
202
203        let cmd = MinaEncryptedKey {
204            secret_key: Some(secret_key),
205            password: "test_password".to_string(),
206            file: file_path_str.clone(),
207        };
208
209        let result = cmd.run();
210        assert!(result.is_ok());
211
212        // Verify the file was created
213        assert!(file_path.exists());
214
215        // Verify we can load the key back and it matches
216        let loaded_key = AccountSecretKey::from_encrypted_file(&file_path_str, "test_password");
217        assert!(loaded_key.is_ok());
218        let loaded_key = loaded_key.unwrap();
219        assert_eq!(loaded_key.public_key(), expected_public_key);
220    }
221
222    #[test]
223    fn test_mina_encrypted_key_with_empty_password() {
224        let temp_dir = TempDir::new().unwrap();
225        let file_path = temp_dir.path().join("test_key_no_pass.json");
226        let file_path_str = file_path.to_str().unwrap().to_string();
227
228        let cmd = MinaEncryptedKey {
229            secret_key: None,
230            password: "".to_string(),
231            file: file_path_str.clone(),
232        };
233
234        let result = cmd.run();
235        assert!(result.is_ok());
236
237        // Verify the file was created
238        assert!(file_path.exists());
239
240        // Verify we can load the key back with empty password
241        let loaded_key = AccountSecretKey::from_encrypted_file(&file_path_str, "");
242        assert!(loaded_key.is_ok());
243    }
244
245    #[test]
246    fn test_mina_encrypted_key_wrong_password_fails() {
247        let temp_dir = TempDir::new().unwrap();
248        let file_path = temp_dir.path().join("test_key_wrong_pass.json");
249        let file_path_str = file_path.to_str().unwrap().to_string();
250
251        let cmd = MinaEncryptedKey {
252            secret_key: None,
253            password: "correct_password".to_string(),
254            file: file_path_str.clone(),
255        };
256
257        let result = cmd.run();
258        assert!(result.is_ok());
259
260        // Verify loading with wrong password fails
261        let loaded_key = AccountSecretKey::from_encrypted_file(&file_path_str, "wrong_password");
262        assert!(loaded_key.is_err());
263    }
264
265    #[test]
266    fn test_mina_encrypted_key_invalid_file_path_fails() {
267        let cmd = MinaEncryptedKey {
268            secret_key: None,
269            password: "test_password".to_string(),
270            file: "/invalid/path/that/does/not/exist/key.json".to_string(),
271        };
272
273        let result = cmd.run();
274        assert!(result.is_err());
275    }
276
277    #[test]
278    fn test_mina_encrypted_key_roundtrip_compatibility() {
279        let temp_dir = TempDir::new().unwrap();
280        let file_path = temp_dir.path().join("test_roundtrip.json");
281        let file_path_str = file_path.to_str().unwrap().to_string();
282
283        // Generate a key with our command
284        let original_secret_key = AccountSecretKey::rand();
285        let original_public_key = original_secret_key.public_key();
286        let password = "roundtrip_test_password";
287
288        let cmd = MinaEncryptedKey {
289            secret_key: Some(original_secret_key.clone()),
290            password: password.to_string(),
291            file: file_path_str.clone(),
292        };
293
294        let result = cmd.run();
295        assert!(result.is_ok());
296
297        // Load the key back using the secret key methods directly
298        let loaded_secret_key = AccountSecretKey::from_encrypted_file(&file_path_str, password);
299        assert!(loaded_secret_key.is_ok());
300        let loaded_secret_key = loaded_secret_key.unwrap();
301        let loaded_public_key = loaded_secret_key.public_key();
302
303        // Verify the keys match exactly
304        assert_eq!(original_public_key, loaded_public_key);
305        assert_eq!(
306            original_secret_key.to_string(),
307            loaded_secret_key.to_string()
308        );
309    }
310
311    #[test]
312    fn test_mina_key_pair_generates_random_key() {
313        let cmd = MinaKeyPair::default();
314
315        let result = cmd.run();
316        assert!(result.is_ok());
317    }
318
319    #[test]
320    fn test_mina_key_pair_with_provided_secret_key() {
321        let secret_key = AccountSecretKey::rand();
322        let cmd = MinaKeyPair {
323            secret_key: Some(secret_key),
324        };
325
326        let result = cmd.run();
327        assert!(result.is_ok());
328    }
329
330    #[test]
331    fn test_p2p_key_pair_generates_random_key() {
332        let cmd = P2PKeyPair {
333            p2p_secret_key: None,
334        };
335
336        let result = cmd.run();
337        assert!(result.is_ok());
338    }
339
340    #[test]
341    fn test_p2p_key_pair_with_provided_secret_key() {
342        let secret_key = SecretKey::rand();
343        let cmd = P2PKeyPair {
344            p2p_secret_key: Some(secret_key),
345        };
346
347        let result = cmd.run();
348        assert!(result.is_ok());
349    }
350}