1use std::{
2 fs::File,
3 io::{BufRead, BufReader, Read},
4 net::IpAddr,
5 path::Path,
6 sync::Arc,
7 time::Duration,
8};
9
10use anyhow::Context;
11use ledger::proofs::provers::BlockProver;
12use mina_p2p_messages::v2::{self, NonZeroCurvePoint};
13use node::{
14 account::AccountSecretKey,
15 daemon_json::Daemon,
16 p2p::{
17 channels::ChannelId, connection::outgoing::P2pConnectionOutgoingInitOpts,
18 identity::SecretKey as P2pSecretKey, P2pLimits, P2pMeshsubConfig, P2pTimeouts,
19 },
20 service::Recorder,
21 snark::{get_srs, BlockVerifier, TransactionVerifier, VerifierSRS},
22 transition_frontier::{archive::archive_config::ArchiveConfig, genesis::GenesisConfig},
23 BlockProducerConfig, GlobalConfig, LedgerConfig, P2pConfig, SnarkConfig, SnarkerConfig,
24 SnarkerStrategy, TransitionFrontierConfig,
25};
26use openmina_core::{consensus::ConsensusConstants, constants::constraint_constants};
27use openmina_node_common::{archive::config::ArchiveStorageOptions, p2p::TaskSpawner};
28use rand::Rng;
29
30use crate::NodeServiceBuilder;
31
32use super::Node;
33
34pub struct NodeBuilder {
35 rng_seed: [u8; 32],
36 custom_initial_time: Option<redux::Timestamp>,
37 genesis_config: Arc<GenesisConfig>,
38 p2p: P2pConfig,
39 p2p_sec_key: Option<P2pSecretKey>,
40 p2p_is_seed: bool,
41 p2p_is_started: bool,
42 block_producer: Option<BlockProducerConfig>,
43 archive: Option<ArchiveConfig>,
44 snarker: Option<SnarkerConfig>,
45 service: NodeServiceBuilder,
46 verifier_srs: Option<Arc<VerifierSRS>>,
47 block_verifier_index: Option<BlockVerifier>,
48 work_verifier_index: Option<TransactionVerifier>,
49 http_port: Option<u16>,
50 daemon_conf: Daemon,
51}
52
53impl NodeBuilder {
54 pub fn new(
55 custom_rng_seed: Option<[u8; 32]>,
56 daemon_conf: Daemon,
57 genesis_config: Arc<GenesisConfig>,
58 ) -> Self {
59 let rng_seed = custom_rng_seed.unwrap_or_else(|| {
60 let mut seed = [0; 32];
61 getrandom::getrandom(&mut seed).unwrap_or_else(|_| {
62 seed = rand::thread_rng().gen();
63 });
64 seed
65 });
66 Self {
67 rng_seed,
68 custom_initial_time: None,
69 genesis_config,
70 p2p: P2pConfig {
71 libp2p_port: None,
72 listen_port: None,
73 identity_pub_key: P2pSecretKey::deterministic(0).public_key(),
75 initial_peers: Vec::new(),
76 external_addrs: Vec::new(),
77 enabled_channels: ChannelId::iter_all().collect(),
78 peer_discovery: true,
79 meshsub: P2pMeshsubConfig {
80 initial_time: Duration::ZERO,
81 ..Default::default()
82 },
83 timeouts: P2pTimeouts::default(),
84 limits: P2pLimits::default().with_max_peers(Some(100)),
85 },
86 p2p_sec_key: None,
87 p2p_is_seed: false,
88 p2p_is_started: false,
89 block_producer: None,
90 archive: None,
91 snarker: None,
92 service: NodeServiceBuilder::new(rng_seed),
93 verifier_srs: None,
94 block_verifier_index: None,
95 work_verifier_index: None,
96 http_port: None,
97 daemon_conf,
98 }
99 }
100
101 pub fn custom_initial_time(&mut self, time: redux::Timestamp) -> &mut Self {
103 self.custom_initial_time = Some(time);
104 self
105 }
106
107 pub fn p2p_sec_key(&mut self, key: P2pSecretKey) -> &mut Self {
109 self.p2p.identity_pub_key = key.public_key();
110 self.p2p_sec_key = Some(key);
111 self
112 }
113
114 pub fn p2p_libp2p_port(&mut self, port: u16) -> &mut Self {
115 self.p2p.libp2p_port = Some(port);
116 self
117 }
118
119 pub fn p2p_seed_node(&mut self) -> &mut Self {
121 self.p2p_is_seed = true;
122 self
123 }
124
125 pub fn p2p_no_discovery(&mut self) -> &mut Self {
126 self.p2p.peer_discovery = false;
127 self
128 }
129
130 pub fn initial_peers(
132 &mut self,
133 peers: impl IntoIterator<Item = P2pConnectionOutgoingInitOpts>,
134 ) -> &mut Self {
135 self.p2p.initial_peers.extend(peers);
136 self
137 }
138
139 pub fn external_addrs(&mut self, v: impl Iterator<Item = IpAddr>) -> &mut Self {
140 self.p2p.external_addrs.extend(v);
141 self
142 }
143
144 pub fn initial_peers_from_file(&mut self, path: impl AsRef<Path>) -> anyhow::Result<&mut Self> {
146 peers_from_reader(
147 &mut self.p2p.initial_peers,
148 File::open(&path).context(anyhow::anyhow!(
149 "opening peer list file {:?}",
150 path.as_ref()
151 ))?,
152 )
153 .context(anyhow::anyhow!(
154 "reading peer list file {:?}",
155 path.as_ref()
156 ))?;
157
158 Ok(self)
159 }
160
161 pub fn initial_peers_from_url(
163 &mut self,
164 url: impl reqwest::IntoUrl,
165 ) -> anyhow::Result<&mut Self> {
166 let url = url.into_url().context("failed to parse peers url")?;
167 peers_from_reader(
168 &mut self.p2p.initial_peers,
169 reqwest::blocking::get(url.clone())
170 .context(anyhow::anyhow!("reading peer list url {url}"))?,
171 )
172 .context(anyhow::anyhow!("reading peer list url {url}"))?;
173 Ok(self)
174 }
175
176 pub fn p2p_max_peers(&mut self, limit: usize) -> &mut Self {
177 self.p2p.limits = self.p2p.limits.with_max_peers(Some(limit));
178 self
179 }
180
181 pub fn p2p_custom_task_spawner(
183 &mut self,
184 spawner: impl TaskSpawner,
185 ) -> anyhow::Result<&mut Self> {
186 let sec_key: P2pSecretKey = self.p2p_sec_key.clone().ok_or_else(|| anyhow::anyhow!("before calling `with_p2p_custom_task_spawner` method, p2p secret key needs to be set with `with_p2p_sec_key`."))?;
187 self.service
188 .p2p_init_with_custom_task_spawner(sec_key, spawner);
189 self.p2p_is_started = true;
190 Ok(self)
191 }
192
193 pub fn block_producer(
195 &mut self,
196 key: AccountSecretKey,
197 provers: Option<BlockProver>,
198 ) -> &mut Self {
199 let config = BlockProducerConfig {
200 pub_key: key.public_key().into(),
201 custom_coinbase_receiver: None,
202 proposed_protocol_version: None,
203 };
204 self.block_producer = Some(config);
205 self.service.block_producer_init(key, provers);
206 self
207 }
208
209 pub fn block_producer_from_file(
211 &mut self,
212 path: impl AsRef<Path>,
213 password: &str,
214 provers: Option<BlockProver>,
215 ) -> anyhow::Result<&mut Self> {
216 let key = AccountSecretKey::from_encrypted_file(&path, password).with_context(|| {
217 format!(
218 "Failed to decrypt secret key file: {}",
219 path.as_ref().display()
220 )
221 })?;
222 Ok(self.block_producer(key, provers))
223 }
224
225 pub fn archive(&mut self, options: ArchiveStorageOptions, work_dir: String) -> &mut Self {
226 self.archive = Some(ArchiveConfig::new(work_dir.clone()));
227 self.service.archive_init(options, work_dir.clone());
228 self
229 }
230
231 pub fn custom_coinbase_receiver(
233 &mut self,
234 addr: NonZeroCurvePoint,
235 ) -> anyhow::Result<&mut Self> {
236 let bp = self.block_producer.as_mut().ok_or_else(|| {
237 anyhow::anyhow!(
238 "can't set custom_coinbase_receiver when block producer is not initialized."
239 )
240 })?;
241 bp.custom_coinbase_receiver = Some(addr);
242 Ok(self)
243 }
244
245 pub fn custom_block_producer_config(
246 &mut self,
247 config: BlockProducerConfig,
248 ) -> anyhow::Result<&mut Self> {
249 *self.block_producer.as_mut().ok_or_else(|| {
250 anyhow::anyhow!("block producer not initialized! Call `block_producer` function first.")
251 })? = config;
252 Ok(self)
253 }
254
255 pub fn snarker(
256 &mut self,
257 sec_key: AccountSecretKey,
258 fee: u64,
259 strategy: SnarkerStrategy,
260 ) -> &mut Self {
261 let config = SnarkerConfig {
262 public_key: sec_key.public_key(),
263 fee: v2::CurrencyFeeStableV1(v2::UnsignedExtendedUInt64Int64ForVersionTagsStableV1(
264 fee.into(),
265 )),
266 strategy,
267 auto_commit: true,
268 };
269 self.snarker = Some(config);
270 self
271 }
272
273 pub fn verifier_srs(&mut self, srs: Arc<VerifierSRS>) -> &mut Self {
275 self.verifier_srs = Some(srs);
276 self
277 }
278
279 pub fn block_verifier_index(&mut self, index: BlockVerifier) -> &mut Self {
280 self.block_verifier_index = Some(index);
281 self
282 }
283
284 pub fn work_verifier_index(&mut self, index: TransactionVerifier) -> &mut Self {
285 self.work_verifier_index = Some(index);
286 self
287 }
288
289 pub fn gather_stats(&mut self) -> &mut Self {
290 self.service.gather_stats();
291 self
292 }
293
294 pub fn record(&mut self, recorder: Recorder) -> &mut Self {
295 self.service.record(recorder);
296 self
297 }
298
299 pub fn http_server(&mut self, port: u16) -> &mut Self {
300 self.http_port = Some(port);
301 self.service.http_server_init(port);
302 self
303 }
304
305 pub fn build(mut self) -> anyhow::Result<Node> {
306 let p2p_sec_key = self.p2p_sec_key.clone().unwrap_or_else(P2pSecretKey::rand);
307 self.p2p_sec_key(p2p_sec_key.clone());
308 if self.p2p.initial_peers.is_empty() && !self.p2p_is_seed {
309 self.p2p.initial_peers = default_peers();
310 }
311
312 self.p2p.initial_peers = self
313 .p2p
314 .initial_peers
315 .into_iter()
316 .filter(|opts| *opts.peer_id() != p2p_sec_key.public_key().peer_id())
317 .filter_map(|opts| match opts {
318 P2pConnectionOutgoingInitOpts::LibP2P(mut opts) => {
319 opts.host = opts.host.resolve()?;
320 Some(P2pConnectionOutgoingInitOpts::LibP2P(opts))
321 }
322 x => Some(x),
323 })
324 .collect();
325
326 let srs = self.verifier_srs.unwrap_or_else(get_srs);
327 let block_verifier_index = self
328 .block_verifier_index
329 .unwrap_or_else(BlockVerifier::make);
330 let work_verifier_index = self
331 .work_verifier_index
332 .unwrap_or_else(TransactionVerifier::make);
333
334 let initial_time = self
335 .custom_initial_time
336 .unwrap_or_else(redux::Timestamp::global_now);
337 self.p2p.meshsub.initial_time = initial_time
338 .checked_sub(redux::Timestamp::ZERO)
339 .unwrap_or_default();
340
341 let protocol_constants = self.genesis_config.protocol_constants()?;
342 let consensus_consts =
343 ConsensusConstants::create(constraint_constants(), &protocol_constants);
344
345 let node_config = node::Config {
347 global: GlobalConfig {
348 build: node::BuildEnv::get().into(),
349 snarker: self.snarker,
350 consensus_constants: consensus_consts.clone(),
351 testing_run: false,
352 client_port: self.http_port,
353 },
354 p2p: self.p2p,
355 ledger: LedgerConfig {},
356 snark: SnarkConfig {
357 block_verifier_index,
358 block_verifier_srs: srs.clone(),
359 work_verifier_index,
360 work_verifier_srs: srs,
361 },
362 transition_frontier: TransitionFrontierConfig::new(self.genesis_config),
363 block_producer: self.block_producer,
364 archive: self.archive,
365 tx_pool: ledger::transaction_pool::Config {
366 trust_system: (),
367 pool_max_size: self.daemon_conf.tx_pool_max_size(),
368 slot_tx_end: self.daemon_conf.slot_tx_end(),
369 },
370 };
371
372 let mut service = self.service;
374 service.ledger_init();
375
376 if !self.p2p_is_started {
377 service.p2p_init(p2p_sec_key);
378 }
379
380 let service = service.build()?;
381 let state = node::State::new(node_config, &consensus_consts, initial_time);
382
383 Ok(Node::new(self.rng_seed, state, service, None))
384 }
385}
386
387fn default_peers() -> Vec<P2pConnectionOutgoingInitOpts> {
388 openmina_core::NetworkConfig::global()
389 .default_peers
390 .iter()
391 .map(|s| s.parse().unwrap())
392 .collect()
393}
394
395fn peers_from_reader(
396 peers: &mut Vec<P2pConnectionOutgoingInitOpts>,
397 read: impl Read,
398) -> anyhow::Result<()> {
399 let reader = BufReader::new(read);
400 for line in reader.lines() {
401 let line = line.context("reading line")?;
402 let trimmed = line.trim();
403 if trimmed.is_empty() {
404 continue;
405 }
406 match trimmed.parse::<P2pConnectionOutgoingInitOpts>() {
407 Ok(opts) => {
408 if let Some(opts) = opts.with_host_resolved() {
409 peers.push(opts);
410 } else {
411 openmina_core::warn!(
412 "Peer address name resolution failed, skipping: {:?}",
413 trimmed
414 );
415 }
416 }
417 Err(e) => openmina_core::warn!("Peer address parse error: {:?}", e),
418 }
419 }
420 Ok(())
421}