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}