cli/commands/node/
mod.rs

1use anyhow::Context;
2use ledger::proofs::provers::BlockProver;
3use mina_node_account::AccountPublicKey;
4use mina_node_native::{archive::config::ArchiveStorageOptions, tracing, NodeBuilder};
5use node::{
6    account::AccountSecretKey,
7    core::log::inner::Level,
8    p2p::{connection::outgoing::P2pConnectionOutgoingInitOpts, identity::SecretKey},
9    service::Recorder,
10    snark::{BlockVerifier, TransactionVerifier},
11    transition_frontier::genesis::GenesisConfig,
12    SnarkerStrategy,
13};
14use reqwest::Url;
15use std::{fs::File, path::PathBuf, sync::Arc};
16
17/// Mina node configuration and runtime options
18///
19/// This struct defines all available command-line parameters for running a Mina node.
20/// The node can operate in different modes (basic node, block producer, archive node)
21/// depending on the parameters provided.
22///
23/// # Basic Usage
24///
25/// ```bash
26/// # Run a basic node on devnet
27/// mina node --network devnet
28///
29/// # Run with custom ports and logging
30/// mina node --network devnet --port 3001 --libp2p-port 8303 --verbosity debug
31/// ```
32///
33/// # Block Producer Mode
34///
35/// ```bash
36/// # Run as block producer
37/// mina node --network devnet --producer-key /path/to/key --coinbase-receiver B62q...
38/// ```
39///
40/// # Archive Node Mode
41///
42/// ```bash
43/// # Run as archive node with local storage
44/// mina node --network devnet --archive-local-storage
45/// ```
46#[derive(Debug, clap::Args)]
47pub struct Node {
48    /// Working directory for node data, logs, and configuration files
49    ///
50    /// Can be set via MINA_HOME environment variable.
51    /// Defaults to ~/.mina
52    #[arg(long, short = 'd', default_value = "~/.mina", env = "MINA_HOME")]
53    pub work_dir: String,
54
55    /// P2P networking secret key for node identity
56    ///
57    /// If not provided, a new key will be generated automatically.
58    /// Can be set via MINA_P2P_SEC_KEY environment variable.
59    #[arg(long, short = 's', env = "MINA_P2P_SEC_KEY")]
60    pub p2p_secret_key: Option<SecretKey>,
61
62    // warning, this overrides `MINA_P2P_SEC_KEY`
63    /// Compatibility with OCaml Mina node
64    #[arg(long)]
65    pub libp2p_keypair: Option<String>,
66
67    // warning, this overrides `MINA_P2P_SEC_KEY`
68    /// Compatibility with OCaml Mina node
69    #[arg(env = "MINA_LIBP2P_PASS")]
70    pub libp2p_password: Option<String>,
71
72    /// List of external addresses at which this node is accessible
73    #[arg(long)]
74    pub libp2p_external_ip: Vec<String>,
75
76    /// HTTP server port for RPC API and web interface
77    ///
78    /// The node will serve its HTTP API and dashboard on this port.
79    /// Default: 3000
80    #[arg(long, short, env, default_value = "3000")]
81    pub port: u16,
82
83    /// LibP2P networking port for peer-to-peer communication
84    ///
85    /// This port is used for connecting to other nodes in the network.
86    /// Default: 8302
87    #[arg(long, env, default_value = "8302")]
88    pub libp2p_port: u16,
89
90    /// Logging verbosity level
91    ///
92    /// Controls the amount of logging output. Options in order of verbosity:
93    /// - error: Only show errors
94    /// - warn: Show warnings and errors
95    /// - info: Show informational messages, warnings, and errors (default)
96    /// - debug: Show debug information and all above
97    /// - trace: Show all possible logging output
98    #[arg(long, short, env, default_value = "info")]
99    pub verbosity: Level,
100
101    /// Disable filesystem logging
102    #[arg(long, env = "MINA_DISABLE_FILESYSTEM_LOGGING", default_value_t = false)]
103    pub disable_filesystem_logging: bool,
104
105    /// Specify custom path for log files
106    #[arg(long, env = "MINA_LOG_PATH", default_value = "$MINA_HOME")]
107    pub log_path: String,
108
109    /// Initial peers to connect to on startup
110    ///
111    /// Specify peer multiaddresses to connect to when the node starts.
112    /// Can be used multiple times to add multiple peers.
113    ///
114    /// # Multiaddr Format
115    ///
116    /// Multiaddresses follow the format: `/protocol/address/protocol/port/protocol/peer_id`
117    ///
118    /// **IPv4 Example:**
119    /// ```
120    /// /ip4/192.168.1.100/tcp/8302/p2p/12D3KooWABCDEF1234567890abcdef...
121    /// ```
122    ///
123    /// **IPv6 Example:**
124    /// ```
125    /// /ip6/2001:db8::1/tcp/8302/p2p/12D3KooWABCDEF1234567890abcdef...
126    /// ```
127    ///
128    /// **DNS Example:**
129    /// ```
130    /// /dns4/node.example.com/tcp/8302/p2p/12D3KooWABCDEF1234567890abcdef...
131    /// ```
132    ///
133    /// Where:
134    /// - `ip4/ip6/dns4` specifies the address type
135    /// - IP address or hostname
136    /// - `tcp` protocol with port number (typically 8302 for Mina)
137    /// - `p2p` protocol with the peer's public key identifier
138    #[arg(long, short = 'P', alias = "peer")]
139    pub peers: Vec<P2pConnectionOutgoingInitOpts>,
140
141    /// File containing initial peers to connect to
142    ///
143    /// Each line should contain a peer's multiaddr following the format described above.
144    ///
145    /// **Example file content:**
146    /// ```
147    /// /ip4/192.168.1.100/tcp/8302/p2p/12D3KooWABCDEF1234567890abcdef...
148    /// /ip4/10.0.0.50/tcp/8302/p2p/12D3KooWXYZ9876543210fedcba...
149    /// /dns4/bootstrap.example.com/tcp/8302/p2p/12D3KooW123ABC...
150    /// ```
151    ///
152    /// Empty lines and lines starting with `#` are ignored.
153    #[arg(long, env)]
154    pub peer_list_file: Option<PathBuf>,
155
156    /// URL to fetch initial peers list from
157    ///
158    /// The URL should return a text file with one peer multiaddr per line,
159    /// using the same format as described in `peer_list_file`.
160    /// Useful for dynamic peer discovery from a central bootstrap service.
161    ///
162    /// **Example URL response:**
163    /// ```
164    /// /ip4/bootstrap1.example.com/tcp/8302/p2p/12D3KooW...
165    /// /ip4/bootstrap2.example.com/tcp/8302/p2p/12D3KooX...
166    /// ```
167    #[arg(long, env)]
168    pub peer_list_url: Option<Url>,
169
170    /// Maximum number of peer connections to maintain
171    ///
172    /// The node will attempt to maintain up to this many connections
173    /// to other peers in the network. Default: 100
174    #[arg(long, default_value = "100")]
175    pub max_peers: usize,
176
177    /// Run the node in seed mode. No default peers will be added.
178    #[arg(long, env)]
179    pub seed: bool,
180
181    /// Run Snark Worker.
182    ///
183    /// Pass snarker private key as an argument.
184    #[arg(long, env, group = "snarker")]
185    pub run_snarker: Option<AccountSecretKey>,
186
187    /// Snark fee, in Mina
188    #[arg(long, env, default_value_t = 1_000_000, requires = "snarker")]
189    pub snarker_fee: u64,
190
191    #[arg(long, env, default_value = "seq", requires = "snarker")]
192    pub snarker_strategy: SnarkerStrategy,
193
194    /// Enable block producer with this key file
195    ///
196    /// MINA_PRIVKEY_PASS must be set to decrypt the keyfile if it is password-protected
197    #[arg(long, env, group = "producer")]
198    pub producer_key: Option<PathBuf>,
199
200    /// Password used to decrypt the producer key file.
201    #[arg(env = "MINA_PRIVKEY_PASS", default_value = "")]
202    pub producer_key_password: String,
203
204    /// Address to send coinbase rewards to (if this node is producing blocks).
205    /// If not provided, coinbase rewards will be sent to the producer
206    /// of a block.
207    ///
208    /// Warning: If the key is from a zkApp account, the account's
209    /// receive permission must be None.
210    #[arg(long, requires = "producer")]
211    pub coinbase_receiver: Option<AccountPublicKey>,
212
213    /// Enable recording of node state and actions for debugging and replay
214    ///
215    /// Recording captures the node's state transitions and input actions,
216    /// enabling deterministic replay for debugging and testing purposes.
217    ///
218    /// Available modes:
219    /// - `none`: No recording (default)
220    /// - `state-with-input-actions`: Records initial state and all input
221    ///   actions to the `recorder/` directory within the working directory
222    ///
223    /// Recorded data can be replayed using the `mina replay` command to
224    /// reproduce the exact sequence of state transitions for debugging.
225    ///
226    /// # Example
227    ///
228    /// ```bash
229    /// # Record node execution
230    /// mina node --network devnet --record state-with-input-actions
231    ///
232    /// # Replay recorded execution
233    /// mina replay state-with-input-actions ~/.mina/recorder
234    /// ```
235    #[arg(long, default_value = "none", env)]
236    pub record: String,
237
238    /// Do not use peers discovery.
239    #[arg(long)]
240    pub no_peers_discovery: bool,
241
242    /// Config JSON file to load at startup.
243    // TODO: make this argument required.
244    #[arg(short = 'c', long, env)]
245    pub config: Option<PathBuf>,
246
247    /// Enable local precomputed storage.
248    ///
249    /// This option requires the following environment variables to be set:
250    /// - MINA_ARCHIVE_LOCAL_STORAGE_PATH (otherwise the path to the working directory will be used)
251    #[arg(long, env)]
252    pub archive_local_storage: bool,
253
254    /// Enable archiver process.
255    ///
256    /// This requires the following environment variables to be set:
257    /// - MINA_ARCHIVE_ADDRESS
258    #[arg(long, env)]
259    pub archive_archiver_process: bool,
260
261    /// Enable GCP precomputed storage.
262    ///
263    /// This requires the following environment variables to be set:
264    /// - GCP_CREDENTIALS_JSON
265    /// - GCP_BUCKET_NAME
266    ///
267    #[arg(long, env)]
268    pub archive_gcp_storage: bool,
269
270    /// Enable AWS precomputed storage.
271    ///
272    /// This requires the following environment variables to be set:
273    /// - AWS_ACCESS_KEY_ID
274    /// - AWS_SECRET_ACCESS_KEY
275    /// - AWS_SESSION_TOKEN
276    /// - AWS_DEFAULT_REGION
277    /// - MINA_AWS_BUCKET_NAME
278    #[arg(long, env)]
279    pub archive_aws_storage: bool,
280
281    #[arg(long, env)]
282    pub rng_seed: Option<String>,
283}
284
285impl Node {
286    pub fn run(self) -> anyhow::Result<()> {
287        let work_dir = shellexpand::full(&self.work_dir).unwrap().into_owned();
288
289        let _guard = if !self.disable_filesystem_logging {
290            let log_output_dir = if self.log_path == "$MINA_HOME" {
291                work_dir.clone()
292            } else {
293                self.log_path.clone()
294            };
295            Some(tracing::initialize_with_filesystem_output(
296                self.verbosity,
297                log_output_dir.into(),
298            ))
299        } else {
300            tracing::initialize(self.verbosity);
301            None
302        };
303
304        rayon::ThreadPoolBuilder::new()
305            .num_threads(num_cpus::get().max(2) - 1)
306            .thread_name(|i| format!("mina_rayon_{i}"))
307            .build_global()
308            .context("failed to initialize threadpool")?;
309
310        let (daemon_conf, genesis_conf) = match self.config {
311            Some(config) => {
312                let reader = File::open(config).context("config file {config:?}")?;
313                let config: node::daemon_json::DaemonJson =
314                    serde_json::from_reader(reader).context("config file {config:?}")?;
315                (
316                    config
317                        .daemon
318                        .clone()
319                        .unwrap_or(node::daemon_json::Daemon::DEFAULT),
320                    Arc::new(GenesisConfig::DaemonJson(Box::new(config))),
321                )
322            }
323            None => (
324                node::daemon_json::Daemon::DEFAULT,
325                node::config::DEVNET_CONFIG.clone(),
326            ),
327        };
328
329        let custom_rng_seed = match self.rng_seed {
330            None => None,
331            Some(v) => match hex::decode(v)
332                .map_err(anyhow::Error::from)
333                .and_then(|bytes| {
334                    <[u8; 32]>::try_from(bytes.as_slice()).map_err(anyhow::Error::from)
335                }) {
336                Ok(v) => Some(v),
337                Err(err) => {
338                    node::core::error!(
339                        node::core::log::system_time();
340                        summary = "bad rng seed",
341                        err = err.to_string(),
342                    );
343                    return Err(err);
344                }
345            },
346        };
347        let mut node_builder: NodeBuilder =
348            NodeBuilder::new(custom_rng_seed, daemon_conf, genesis_conf);
349
350        // let genesis_config = match self.config {
351        //     Some(config_path) => GenesisConfig::DaemonJsonFile(config_path).into(),
352        //     None => node::config::DEVNET_CONFIG.clone(),
353        // };
354        // let mut node_builder: NodeBuilder = NodeBuilder::new(None, genesis_config);
355
356        if let Some(sec_key) = self.p2p_secret_key {
357            node_builder.p2p_sec_key(sec_key);
358        }
359
360        // warning, this overrides `MINA_P2P_SEC_KEY`
361        if let (Some(key_file), Some(password)) = (&self.libp2p_keypair, &self.libp2p_password) {
362            match SecretKey::from_encrypted_file(key_file, password) {
363                Ok(sk) => {
364                    node_builder.p2p_sec_key(sk.clone());
365                    node::core::info!(
366                        node::core::log::system_time();
367                        summary = "read sercret key from file",
368                        file_name = key_file,
369                        pk = sk.public_key().to_string(),
370                    )
371                }
372                Err(err) => {
373                    node::core::error!(
374                        node::core::log::system_time();
375                        summary = "failed to read secret key",
376                        file_name = key_file,
377                        err = err.to_string(),
378                    );
379                    return Err(err.into());
380                }
381            }
382        } else if self.libp2p_keypair.is_some() && self.libp2p_password.is_none() {
383            let error = "keyfile is specified, but `MINA_LIBP2P_PASS` is not set";
384            node::core::error!(
385                node::core::log::system_time();
386                summary = error,
387            );
388            return Err(anyhow::anyhow!(error));
389        }
390
391        node_builder.p2p_libp2p_port(self.libp2p_port);
392
393        node_builder.external_addrs(
394            self.libp2p_external_ip
395                .into_iter()
396                .filter_map(|s| s.parse().ok()),
397        );
398
399        node_builder.p2p_max_peers(self.max_peers);
400        self.seed.then(|| node_builder.p2p_seed_node());
401        self.no_peers_discovery
402            .then(|| node_builder.p2p_no_discovery());
403
404        node_builder.initial_peers(self.peers);
405        if let Some(path) = self.peer_list_file {
406            node_builder.initial_peers_from_file(path)?;
407        }
408        if let Some(url) = self.peer_list_url {
409            node_builder.initial_peers_from_url(url)?;
410        }
411
412        let block_verifier_index = BlockVerifier::make();
413        let work_verifier_index = TransactionVerifier::make();
414        node_builder
415            .block_verifier_index(block_verifier_index.clone())
416            .work_verifier_index(work_verifier_index.clone());
417
418        if let Some(producer_key_path) = self.producer_key {
419            let password = &self.producer_key_password;
420            mina_core::thread::spawn(|| {
421                node::core::info!(node::core::log::system_time(); summary = "loading provers index");
422                BlockProver::make(Some(block_verifier_index), Some(work_verifier_index));
423                node::core::info!(node::core::log::system_time(); summary = "loaded provers index");
424            });
425            node_builder.block_producer_from_file(producer_key_path, password, None)?;
426
427            if let Some(pub_key) = self.coinbase_receiver {
428                node_builder
429                    .custom_coinbase_receiver(pub_key.into())
430                    .unwrap();
431            }
432        }
433
434        let archive_storage_options = ArchiveStorageOptions::from_iter(
435            [
436                (
437                    self.archive_local_storage,
438                    ArchiveStorageOptions::LOCAL_PRECOMPUTED_STORAGE,
439                ),
440                (
441                    self.archive_archiver_process,
442                    ArchiveStorageOptions::ARCHIVER_PROCESS,
443                ),
444                (
445                    self.archive_gcp_storage,
446                    ArchiveStorageOptions::GCP_PRECOMPUTED_STORAGE,
447                ),
448                (
449                    self.archive_aws_storage,
450                    ArchiveStorageOptions::AWS_PRECOMPUTED_STORAGE,
451                ),
452            ]
453            .iter()
454            .filter(|(enabled, _)| *enabled)
455            .map(|(_, option)| option.clone()),
456        );
457
458        if archive_storage_options.is_enabled() {
459            node::core::info!(
460                summary = "Archive mode enabled",
461                local_storage = archive_storage_options.uses_local_precomputed_storage(),
462                archiver_process = archive_storage_options.uses_archiver_process(),
463                gcp_storage = archive_storage_options.uses_gcp_precomputed_storage(),
464                aws_storage = archive_storage_options.uses_aws_precomputed_storage(),
465            );
466
467            archive_storage_options
468                .validate_env_vars()
469                .map_err(|e| anyhow::anyhow!(e))?;
470
471            node_builder.archive(archive_storage_options, work_dir.clone());
472        }
473
474        if let Some(sec_key) = self.run_snarker {
475            node_builder.snarker(sec_key, self.snarker_fee, self.snarker_strategy);
476        }
477
478        mina_core::set_work_dir(work_dir.clone().into());
479
480        node_builder
481            .http_server(self.port)
482            .gather_stats()
483            .record(match self.record.trim() {
484                "none" => Recorder::None,
485                "state-with-input-actions" => Recorder::only_input_actions(work_dir),
486                _ => panic!("unknown --record strategy"),
487            });
488
489        let mut node = node_builder.build().context("node build failed!")?;
490
491        let runtime = tokio::runtime::Builder::new_current_thread()
492            .enable_all()
493            .thread_stack_size(64 * 1024 * 1024)
494            .build()
495            .unwrap();
496
497        runtime.block_on(node.run_forever());
498
499        Ok(())
500    }
501}