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}