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#[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 #[arg(long, short = 's', env = "OPENMINA_P2P_SEC_KEY")]
36 pub p2p_secret_key: Option<SecretKey>,
37
38 #[arg(long)]
41 pub libp2p_keypair: Option<String>,
42
43 #[arg(env = "MINA_LIBP2P_PASS")]
46 pub libp2p_password: Option<String>,
47
48 #[arg(long)]
50 pub libp2p_external_ip: Vec<String>,
51
52 #[arg(long, short, env, default_value = "3000")]
54 pub port: u16,
55
56 #[arg(long, env, default_value = "8302")]
58 pub libp2p_port: u16,
59
60 #[arg(long, short, env, default_value = "info")]
62 pub verbosity: Level,
63
64 #[arg(
66 long,
67 env = "OPENMINA_DISABLE_FILESYSTEM_LOGGING",
68 default_value_t = false
69 )]
70 pub disable_filesystem_logging: bool,
71
72 #[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 #[arg(long, env)]
83 pub peer_list_file: Option<PathBuf>,
84
85 #[arg(long, env)]
89 pub peer_list_url: Option<Url>,
90
91 #[arg(long, default_value = "100")]
92 pub max_peers: usize,
93
94 #[arg(long, env)]
96 pub seed: bool,
97
98 #[arg(long, env, group = "snarker")]
102 pub run_snarker: Option<AccountSecretKey>,
103
104 #[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 #[arg(long, env, group = "producer")]
115 pub producer_key: Option<PathBuf>,
116
117 #[arg(env = "MINA_PRIVKEY_PASS", default_value = "")]
119 pub producer_key_password: String,
120
121 #[arg(long, requires = "producer")]
128 pub coinbase_receiver: Option<AccountPublicKey>,
129
130 #[arg(long, default_value = "none", env)]
131 pub record: String,
132
133 #[arg(long)]
135 pub no_peers_discovery: bool,
136
137 #[arg(short = 'c', long, env)]
140 pub config: Option<PathBuf>,
141
142 #[arg(long, env)]
147 pub archive_local_storage: bool,
148
149 #[arg(long, env)]
154 pub archive_archiver_process: bool,
155
156 #[arg(long, env)]
163 pub archive_gcp_storage: bool,
164
165 #[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 if let Some(sec_key) = self.p2p_secret_key {
252 node_builder.p2p_sec_key(sec_key);
253 }
254
255 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}