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