Skip to main content

mina_cli/commands/node/
mod.rs

1use anyhow::Context;
2use ledger::proofs::provers::BlockProver;
3use mina_node::{
4    account::AccountSecretKey,
5    core::log::inner::Level,
6    p2p::{connection::outgoing::P2pConnectionOutgoingInitOpts, identity::SecretKey},
7    service::Recorder,
8    snark::{BlockVerifier, TransactionVerifier},
9    transition_frontier::genesis::GenesisConfig,
10    SnarkerStrategy,
11};
12use mina_node_account::AccountPublicKey;
13use mina_node_native::{archive::config::ArchiveStorageOptions, tracing, NodeBuilder};
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
77    ///
78    /// The node will serve its HTTP API 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    /// Override the chain ID used for P2P networking. (interop testing)
285    #[arg(long, env = "MINA_CHAIN_ID")]
286    pub chain_id: Option<String>,
287
288    /// Skip SNARK proof verification (interop testing).
289    /// WARNING: THIS DISABLES A CRITICAL SECURITY CHECK. ONLY USE
290    /// FOR INTEROP TESTING WITH OCAML NODES RUNNING --PROOF-LEVEL
291    /// NONE, WHICH PRODUCE BLOCKS WITH DUMMY PROOFS.
292    #[arg(long)]
293    pub skip_proof_verification: bool,
294}
295
296impl Node {
297    /// Runs the Mina node with the configured options.
298    ///
299    /// Starts the node and blocks until shutdown. Builds [`BlockVerifier`] and
300    /// [`TransactionVerifier`] at startup for SNARK proof verification. The
301    /// node behavior depends on the struct fields: a basic node syncs the
302    /// chain, while optional features like block production
303    /// ([`producer_key`](Self::producer_key)), SNARK worker
304    /// ([`run_snarker`](Self::run_snarker)), or archiving
305    /// ([`archive_local_storage`](Self::archive_local_storage)) are enabled
306    /// via their respective fields.
307    ///
308    /// # Panics
309    ///
310    /// Panics if an unknown [`record`](Self::record) strategy is provided.
311    ///
312    /// # Notes
313    ///
314    /// Building SNARK verification keys at startup may take several seconds.
315    /// Optional services are initialized based on their respective CLI flags.
316    ///
317    /// # Errors
318    ///
319    /// Returns an error if initialization fails (config loading, key
320    /// decryption, node building, etc.).
321    pub fn run(self) -> anyhow::Result<()> {
322        // Initialize logging and thread pool
323        let work_dir = shellexpand::full(&self.work_dir).unwrap().into_owned();
324
325        let _guard = if !self.disable_filesystem_logging {
326            let log_output_dir = if self.log_path == "$MINA_HOME" {
327                work_dir.clone()
328            } else {
329                self.log_path.clone()
330            };
331            Some(tracing::initialize_with_filesystem_output(
332                self.verbosity,
333                log_output_dir.into(),
334            ))
335        } else {
336            tracing::initialize(self.verbosity);
337            None
338        };
339
340        rayon::ThreadPoolBuilder::new()
341            .num_threads(num_cpus::get().max(2) - 1)
342            .thread_name(|i| format!("mina_rayon_{i}"))
343            .build_global()
344            .context("failed to initialize threadpool")?;
345
346        // Load configuration
347        let (daemon_conf, genesis_conf) = match self.config {
348            Some(config) => {
349                let reader = File::open(config).context("config file {config:?}")?;
350                let config: mina_node::daemon_json::DaemonJson =
351                    serde_json::from_reader(reader).context("config file {config:?}")?;
352                (
353                    config
354                        .daemon
355                        .clone()
356                        .unwrap_or(mina_node::daemon_json::Daemon::DEFAULT),
357                    Arc::new(GenesisConfig::DaemonJson(Box::new(config))),
358                )
359            }
360            None => (
361                mina_node::daemon_json::Daemon::DEFAULT,
362                mina_node::config::DEVNET_CONFIG.clone(),
363            ),
364        };
365
366        // Parse chain ID override (applied to builder below).
367        let chain_id_override = self
368            .chain_id
369            .as_deref()
370            .map(mina_node::core::ChainId::from_hex)
371            .transpose()
372            .context("invalid --chain-id hex string")?;
373
374        let custom_rng_seed = match self.rng_seed {
375            None => None,
376            Some(v) => match hex::decode(v)
377                .map_err(anyhow::Error::from)
378                .and_then(|bytes| {
379                    <[u8; 32]>::try_from(bytes.as_slice()).map_err(anyhow::Error::from)
380                }) {
381                Ok(v) => Some(v),
382                Err(err) => {
383                    mina_node::core::error!(
384                        mina_node::core::log::system_time();
385                        summary = "bad rng seed",
386                        err = err.to_string(),
387                    );
388                    return Err(err);
389                }
390            },
391        };
392
393        let mut node_builder: NodeBuilder =
394            NodeBuilder::new(custom_rng_seed, daemon_conf, genesis_conf);
395
396        if let Some(chain_id) = chain_id_override {
397            node_builder.chain_id_override(chain_id);
398        }
399        if self.skip_proof_verification {
400            node_builder.skip_proof_verification(true);
401        }
402
403        // Configure P2P identity
404        if let Some(sec_key) = self.p2p_secret_key {
405            node_builder.p2p_sec_key(sec_key);
406        }
407
408        // Load libp2p keypair from file (overrides MINA_P2P_SEC_KEY)
409        if let (Some(key_file), Some(password)) = (&self.libp2p_keypair, &self.libp2p_password) {
410            match SecretKey::from_encrypted_file(key_file, password) {
411                Ok(sk) => {
412                    node_builder.p2p_sec_key(sk.clone());
413                    mina_node::core::info!(
414                        mina_node::core::log::system_time();
415                        summary = "read sercret key from file",
416                        file_name = key_file,
417                        pk = sk.public_key().to_string(),
418                    )
419                }
420                Err(err) => {
421                    mina_node::core::error!(
422                        mina_node::core::log::system_time();
423                        summary = "failed to read secret key",
424                        file_name = key_file,
425                        err = err.to_string(),
426                    );
427                    return Err(err.into());
428                }
429            }
430        } else if self.libp2p_keypair.is_some() && self.libp2p_password.is_none() {
431            let error = "keyfile is specified, but `MINA_LIBP2P_PASS` is not set";
432            mina_node::core::error!(
433                mina_node::core::log::system_time();
434                summary = error,
435            );
436            return Err(anyhow::anyhow!(error));
437        }
438
439        // Configure P2P networking
440        node_builder.p2p_libp2p_port(self.libp2p_port);
441
442        node_builder.external_addrs(
443            self.libp2p_external_ip
444                .into_iter()
445                .filter_map(|s| s.parse().ok()),
446        );
447
448        node_builder.p2p_max_peers(self.max_peers);
449        self.seed.then(|| node_builder.p2p_seed_node());
450        self.no_peers_discovery
451            .then(|| node_builder.p2p_no_discovery());
452
453        node_builder.initial_peers(self.peers);
454        if let Some(path) = self.peer_list_file {
455            node_builder.initial_peers_from_file(path)?;
456        }
457        if let Some(url) = self.peer_list_url {
458            node_builder.initial_peers_from_url(url)?;
459        }
460
461        // Build SNARK verifiers
462        let block_verifier_index = BlockVerifier::make();
463        let work_verifier_index = TransactionVerifier::make();
464        node_builder
465            .block_verifier_index(block_verifier_index.clone())
466            .work_verifier_index(work_verifier_index.clone());
467
468        // Initialize block producer (optional)
469        if let Some(producer_key_path) = self.producer_key {
470            let password = &self.producer_key_password;
471            mina_core::thread::spawn(|| {
472                mina_node::core::info!(mina_node::core::log::system_time(); summary = "loading provers index");
473                BlockProver::make(Some(block_verifier_index), Some(work_verifier_index));
474                mina_node::core::info!(mina_node::core::log::system_time(); summary = "loaded provers index");
475            });
476            node_builder.block_producer_from_file(producer_key_path, password, None)?;
477
478            if let Some(pub_key) = self.coinbase_receiver {
479                node_builder
480                    .custom_coinbase_receiver(pub_key.into())
481                    .unwrap();
482            }
483        }
484
485        // Initialize archive service (optional)
486        let archive_storage_options = ArchiveStorageOptions::from_iter(
487            [
488                (
489                    self.archive_local_storage,
490                    ArchiveStorageOptions::LOCAL_PRECOMPUTED_STORAGE,
491                ),
492                (
493                    self.archive_archiver_process,
494                    ArchiveStorageOptions::ARCHIVER_PROCESS,
495                ),
496                (
497                    self.archive_gcp_storage,
498                    ArchiveStorageOptions::GCP_PRECOMPUTED_STORAGE,
499                ),
500                (
501                    self.archive_aws_storage,
502                    ArchiveStorageOptions::AWS_PRECOMPUTED_STORAGE,
503                ),
504            ]
505            .iter()
506            .filter(|(enabled, _)| *enabled)
507            .map(|(_, option)| option.clone()),
508        );
509
510        if archive_storage_options.is_enabled() {
511            mina_node::core::info!(
512                summary = "Archive mode enabled",
513                local_storage = archive_storage_options.uses_local_precomputed_storage(),
514                archiver_process = archive_storage_options.uses_archiver_process(),
515                gcp_storage = archive_storage_options.uses_gcp_precomputed_storage(),
516                aws_storage = archive_storage_options.uses_aws_precomputed_storage(),
517            );
518
519            archive_storage_options
520                .validate_env_vars()
521                .map_err(|e| anyhow::anyhow!(e))?;
522
523            node_builder.archive(archive_storage_options, work_dir.clone());
524        }
525
526        // Initialize SNARK worker (optional)
527        if let Some(sec_key) = self.run_snarker {
528            node_builder.snarker(sec_key, self.snarker_fee, self.snarker_strategy);
529        }
530
531        // Build and run the node
532        mina_core::set_work_dir(work_dir.clone().into());
533
534        node_builder
535            .http_server(self.port)
536            .gather_stats()
537            .record(match self.record.trim() {
538                "none" => Recorder::None,
539                "state-with-input-actions" => Recorder::only_input_actions(work_dir),
540                _ => panic!("unknown --record strategy"),
541            });
542
543        let mut node = node_builder.build().context("node build failed!")?;
544
545        // Start event loop
546        let runtime = tokio::runtime::Builder::new_current_thread()
547            .enable_all()
548            .thread_stack_size(64 * 1024 * 1024)
549            .build()
550            .unwrap();
551
552        runtime.block_on(node.run_forever());
553
554        Ok(())
555    }
556}