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}