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
24#[derive(Debug, clap::Args)]
25pub struct Node {
26    #[arg(
27        long,
28        short = 'd',
29        default_value = "~/.openmina",
30        env = "OPENMINA_HOME"
31    )]
32    pub work_dir: String,
33
34    /// Peer secret key
35    #[arg(long, short = 's', env = "OPENMINA_P2P_SEC_KEY")]
36    pub p2p_secret_key: Option<SecretKey>,
37
38    // warning, this overrides `OPENMINA_P2P_SEC_KEY`
39    /// Compatibility with OCaml Mina node
40    #[arg(long)]
41    pub libp2p_keypair: Option<String>,
42
43    // warning, this overrides `OPENMINA_P2P_SEC_KEY`
44    /// Compatibility with OCaml Mina node
45    #[arg(env = "MINA_LIBP2P_PASS")]
46    pub libp2p_password: Option<String>,
47
48    /// List of external addresses at which this node is accessible
49    #[arg(long)]
50    pub libp2p_external_ip: Vec<String>,
51
52    /// Http port to listen on
53    #[arg(long, short, env, default_value = "3000")]
54    pub port: u16,
55
56    /// LibP2P port to listen on
57    #[arg(long, env, default_value = "8302")]
58    pub libp2p_port: u16,
59
60    /// Verbosity level (options: trace, debug, info, warn, error)
61    #[arg(long, short, env, default_value = "info")]
62    pub verbosity: Level,
63
64    /// Disable filesystem logging
65    #[arg(
66        long,
67        env = "OPENMINA_DISABLE_FILESYSTEM_LOGGING",
68        default_value_t = false
69    )]
70    pub disable_filesystem_logging: bool,
71
72    /// Specify custom path for log files
73    #[arg(long, env = "OPENMINA_LOG_PATH", default_value = "$OPENMINA_HOME")]
74    pub log_path: String,
75
76    #[arg(long, short = 'P', alias = "peer")]
77    pub peers: Vec<P2pConnectionOutgoingInitOpts>,
78
79    /// File containing initial peers.
80    ///
81    /// Each line should contain peer's multiaddr.
82    #[arg(long, env)]
83    pub peer_list_file: Option<PathBuf>,
84
85    /// File containing initial peers.
86    ///
87    /// Each line should contain peer's multiaddr.
88    #[arg(long, env)]
89    pub peer_list_url: Option<Url>,
90
91    #[arg(long, default_value = "100")]
92    pub max_peers: usize,
93
94    /// Run the node in seed mode. No default peers will be added.
95    #[arg(long, env)]
96    pub seed: bool,
97
98    /// Run Snark Worker.
99    ///
100    /// Pass snarker private key as an argument.
101    #[arg(long, env, group = "snarker")]
102    pub run_snarker: Option<AccountSecretKey>,
103
104    /// Snark fee, in Mina
105    #[arg(long, env, default_value_t = 1_000_000, requires = "snarker")]
106    pub snarker_fee: u64,
107
108    #[arg(long, env, default_value = "seq", requires = "snarker")]
109    pub snarker_strategy: SnarkerStrategy,
110
111    /// Enable block producer with this key file
112    ///
113    /// MINA_PRIVKEY_PASS must be set to decrypt the keyfile if it is password-protected
114    #[arg(long, env, group = "producer")]
115    pub producer_key: Option<PathBuf>,
116
117    /// Password used to decrypt the producer key file.
118    #[arg(env = "MINA_PRIVKEY_PASS", default_value = "")]
119    pub producer_key_password: String,
120
121    /// Address to send coinbase rewards to (if this node is producing blocks).
122    /// If not provided, coinbase rewards will be sent to the producer
123    /// of a block.
124    ///
125    /// Warning: If the key is from a zkApp account, the account's
126    /// receive permission must be None.
127    #[arg(long, requires = "producer")]
128    pub coinbase_receiver: Option<AccountPublicKey>,
129
130    #[arg(long, default_value = "none", env)]
131    pub record: String,
132
133    /// Do not use peers discovery.
134    #[arg(long)]
135    pub no_peers_discovery: bool,
136
137    /// Config JSON file to load at startup.
138    // TODO: make this argument required.
139    #[arg(short = 'c', long, env)]
140    pub config: Option<PathBuf>,
141
142    /// Enable local precomputed storage.
143    ///
144    /// This option requires the following environment variables to be set:
145    /// - OPENMINA_ARCHIVE_LOCAL_STORAGE_PATH (otherwise the path to the working directory will be used)
146    #[arg(long, env)]
147    pub archive_local_storage: bool,
148
149    /// Enable archiver process.
150    ///
151    /// This requires the following environment variables to be set:
152    /// - OPENMINA_ARCHIVE_ADDRESS
153    #[arg(long, env)]
154    pub archive_archiver_process: bool,
155
156    /// Enable GCP precomputed storage.
157    ///
158    /// This requires the following environment variables to be set:
159    /// - GCP_CREDENTIALS_JSON
160    /// - GCP_BUCKET_NAME
161    ///
162    #[arg(long, env)]
163    pub archive_gcp_storage: bool,
164
165    /// Enable AWS precomputed storage.
166    ///
167    /// This requires the following environment variables to be set:
168    /// - AWS_ACCESS_KEY_ID
169    /// - AWS_SECRET_ACCESS_KEY
170    /// - AWS_SESSION_TOKEN
171    /// - AWS_DEFAULT_REGION
172    /// - OPENMINA_AWS_BUCKET_NAME
173    #[arg(long, env)]
174    pub archive_aws_storage: bool,
175
176    #[arg(long, env)]
177    pub rng_seed: Option<String>,
178}
179
180impl Node {
181    pub fn run(self) -> anyhow::Result<()> {
182        let work_dir = shellexpand::full(&self.work_dir).unwrap().into_owned();
183
184        let _guard = if !self.disable_filesystem_logging {
185            let log_output_dir = if self.log_path == "$OPENMINA_HOME" {
186                work_dir.clone()
187            } else {
188                self.log_path.clone()
189            };
190            Some(tracing::initialize_with_filesystem_output(
191                self.verbosity,
192                log_output_dir.into(),
193            ))
194        } else {
195            tracing::initialize(self.verbosity);
196            None
197        };
198
199        rayon::ThreadPoolBuilder::new()
200            .num_threads(num_cpus::get().max(2) - 1)
201            .thread_name(|i| format!("openmina_rayon_{i}"))
202            .build_global()
203            .context("failed to initialize threadpool")?;
204
205        let (daemon_conf, genesis_conf) = match self.config {
206            Some(config) => {
207                let reader = File::open(config).context("config file {config:?}")?;
208                let config: node::daemon_json::DaemonJson =
209                    serde_json::from_reader(reader).context("config file {config:?}")?;
210                (
211                    config
212                        .daemon
213                        .clone()
214                        .unwrap_or(node::daemon_json::Daemon::DEFAULT),
215                    Arc::new(GenesisConfig::DaemonJson(Box::new(config))),
216                )
217            }
218            None => (
219                node::daemon_json::Daemon::DEFAULT,
220                node::config::DEVNET_CONFIG.clone(),
221            ),
222        };
223
224        let custom_rng_seed = match self.rng_seed {
225            None => None,
226            Some(v) => match hex::decode(v)
227                .map_err(anyhow::Error::from)
228                .and_then(|bytes| {
229                    <[u8; 32]>::try_from(bytes.as_slice()).map_err(anyhow::Error::from)
230                }) {
231                Ok(v) => Some(v),
232                Err(err) => {
233                    node::core::error!(
234                        node::core::log::system_time();
235                        summary = "bad rng seed",
236                        err = err.to_string(),
237                    );
238                    return Err(err);
239                }
240            },
241        };
242        let mut node_builder: NodeBuilder =
243            NodeBuilder::new(custom_rng_seed, daemon_conf, genesis_conf);
244
245        // let genesis_config = match self.config {
246        //     Some(config_path) => GenesisConfig::DaemonJsonFile(config_path).into(),
247        //     None => node::config::DEVNET_CONFIG.clone(),
248        // };
249        // let mut node_builder: NodeBuilder = NodeBuilder::new(None, genesis_config);
250
251        if let Some(sec_key) = self.p2p_secret_key {
252            node_builder.p2p_sec_key(sec_key);
253        }
254
255        // warning, this overrides `OPENMINA_P2P_SEC_KEY`
256        if let (Some(key_file), Some(password)) = (&self.libp2p_keypair, &self.libp2p_password) {
257            match SecretKey::from_encrypted_file(key_file, password) {
258                Ok(sk) => {
259                    node_builder.p2p_sec_key(sk.clone());
260                    node::core::info!(
261                        node::core::log::system_time();
262                        summary = "read sercret key from file",
263                        file_name = key_file,
264                        pk = sk.public_key().to_string(),
265                    )
266                }
267                Err(err) => {
268                    node::core::error!(
269                        node::core::log::system_time();
270                        summary = "failed to read secret key",
271                        file_name = key_file,
272                        err = err.to_string(),
273                    );
274                    return Err(err.into());
275                }
276            }
277        } else if self.libp2p_keypair.is_some() && self.libp2p_password.is_none() {
278            let error = "keyfile is specified, but `MINA_LIBP2P_PASS` is not set";
279            node::core::error!(
280                node::core::log::system_time();
281                summary = error,
282            );
283            return Err(anyhow::anyhow!(error));
284        }
285
286        node_builder.p2p_libp2p_port(self.libp2p_port);
287
288        node_builder.external_addrs(
289            self.libp2p_external_ip
290                .into_iter()
291                .filter_map(|s| s.parse().ok()),
292        );
293
294        node_builder.p2p_max_peers(self.max_peers);
295        self.seed.then(|| node_builder.p2p_seed_node());
296        self.no_peers_discovery
297            .then(|| node_builder.p2p_no_discovery());
298
299        node_builder.initial_peers(self.peers);
300        if let Some(path) = self.peer_list_file {
301            node_builder.initial_peers_from_file(path)?;
302        }
303        if let Some(url) = self.peer_list_url {
304            node_builder.initial_peers_from_url(url)?;
305        }
306
307        let block_verifier_index = BlockVerifier::make();
308        let work_verifier_index = TransactionVerifier::make();
309        node_builder
310            .block_verifier_index(block_verifier_index.clone())
311            .work_verifier_index(work_verifier_index.clone());
312
313        if let Some(producer_key_path) = self.producer_key {
314            let password = &self.producer_key_password;
315            openmina_core::thread::spawn(|| {
316                node::core::info!(node::core::log::system_time(); summary = "loading provers index");
317                BlockProver::make(Some(block_verifier_index), Some(work_verifier_index));
318                node::core::info!(node::core::log::system_time(); summary = "loaded provers index");
319            });
320            node_builder.block_producer_from_file(producer_key_path, password, None)?;
321
322            if let Some(pub_key) = self.coinbase_receiver {
323                node_builder
324                    .custom_coinbase_receiver(pub_key.into())
325                    .unwrap();
326            }
327        }
328
329        let archive_storage_options = ArchiveStorageOptions::from_iter(
330            [
331                (
332                    self.archive_local_storage,
333                    ArchiveStorageOptions::LOCAL_PRECOMPUTED_STORAGE,
334                ),
335                (
336                    self.archive_archiver_process,
337                    ArchiveStorageOptions::ARCHIVER_PROCESS,
338                ),
339                (
340                    self.archive_gcp_storage,
341                    ArchiveStorageOptions::GCP_PRECOMPUTED_STORAGE,
342                ),
343                (
344                    self.archive_aws_storage,
345                    ArchiveStorageOptions::AWS_PRECOMPUTED_STORAGE,
346                ),
347            ]
348            .iter()
349            .filter(|(enabled, _)| *enabled)
350            .map(|(_, option)| option.clone()),
351        );
352
353        if archive_storage_options.is_enabled() {
354            node::core::info!(
355                summary = "Archive mode enabled",
356                local_storage = archive_storage_options.uses_local_precomputed_storage(),
357                archiver_process = archive_storage_options.uses_archiver_process(),
358                gcp_storage = archive_storage_options.uses_gcp_precomputed_storage(),
359                aws_storage = archive_storage_options.uses_aws_precomputed_storage(),
360            );
361
362            archive_storage_options
363                .validate_env_vars()
364                .map_err(|e| anyhow::anyhow!(e))?;
365
366            node_builder.archive(archive_storage_options, work_dir.clone());
367        }
368
369        if let Some(sec_key) = self.run_snarker {
370            node_builder.snarker(sec_key, self.snarker_fee, self.snarker_strategy);
371        }
372
373        openmina_core::set_work_dir(work_dir.clone().into());
374
375        node_builder
376            .http_server(self.port)
377            .gather_stats()
378            .record(match self.record.trim() {
379                "none" => Recorder::None,
380                "state-with-input-actions" => Recorder::only_input_actions(work_dir),
381                _ => panic!("unknown --record strategy"),
382            });
383
384        let mut node = node_builder.build().context("node build failed!")?;
385
386        let runtime = tokio::runtime::Builder::new_current_thread()
387            .enable_all()
388            .thread_stack_size(64 * 1024 * 1024)
389            .build()
390            .unwrap();
391
392        runtime.block_on(node.run_forever());
393
394        Ok(())
395    }
396}