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