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