Skip to main content

mina_node_native/node/
builder.rs

1//! Node builder for configuring and constructing native Mina nodes.
2//!
3//! See [`NodeBuilder`] for the main entry point.
4
5use 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
38/// Builder for constructing a native Mina node with fluent API.
39pub struct NodeBuilder {
40    /// Seed for RNG, used in `Node::new()` for replay. If `None` in constructor,
41    /// a random seed is generated.
42    rng_seed: [u8; 32],
43    /// If `Some`, overrides system time (for testing). If `None`, uses real time.
44    custom_initial_time: Option<redux::Timestamp>,
45    /// Genesis block configuration.
46    genesis_config: Arc<GenesisConfig>,
47    /// P2P networking configuration.
48    p2p: P2pConfig,
49    /// If `Some`, uses provided key. If `None`, `P2pSecretKey::rand()` in `build()`.
50    p2p_sec_key: Option<P2pSecretKey>,
51    /// If `true`, node acts as seed (no initial peers needed).
52    p2p_is_seed: bool,
53    /// If `true`, P2P service already spawned via custom task spawner.
54    p2p_is_started: bool,
55    /// If `Some`, enables block production. If `None`, node is non-producing.
56    block_producer: Option<BlockProducerConfig>,
57    /// If `Some`, enables archive mode. If `None`, no archiving.
58    archive: Option<ArchiveConfig>,
59    /// If `Some`, enables SNARK worker. If `None`, no SNARK work.
60    snarker: Option<SnarkerConfig>,
61    /// Service builder for I/O components.
62    service: NodeServiceBuilder,
63    /// If `Some`, uses provided SRS. If `None`, `get_srs()` in `build()`.
64    verifier_srs: Option<Arc<VerifierSRS>>,
65    /// If `Some`, uses provided index. If `None`, `BlockVerifier::make()` in `build()`.
66    block_verifier_index: Option<BlockVerifier>,
67    /// If `Some`, uses provided. If `None`, `TransactionVerifier::make()` in `build()`.
68    work_verifier_index: Option<TransactionVerifier>,
69    /// If `Some`, starts HTTP RPC server on port. If `None`, no RPC server.
70    http_port: Option<u16>,
71    /// Daemon JSON configuration.
72    daemon_conf: Daemon,
73    /// If `Some`, overrides computed chain ID (for interop testing).
74    chain_id_override: Option<mina_core::ChainId>,
75    /// If `true`, skips SNARK proof verification (for interop testing).
76    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                // Must be replaced with builder api.
100                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    /// Override chain ID for P2P networking (interop testing).
130    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    /// Skip SNARK proof verification (interop testing).
136    pub fn skip_proof_verification(&mut self, skip: bool) -> &mut Self {
137        self.skip_proof_verification = skip;
138        self
139    }
140
141    /// Set custom initial time. Used for testing.
142    pub fn custom_initial_time(&mut self, time: redux::Timestamp) -> &mut Self {
143        self.custom_initial_time = Some(time);
144        self
145    }
146
147    /// If not called, random one will be generated and used instead.
148    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    /// Set up node as a seed node.
160    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    /// Extend p2p initial peers from an iterable.
171    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    /// Extend p2p initial peers from file.
185    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    /// Extend p2p initial peers by opening the url.
202    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    /// Override default p2p task spawner.
222    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    /// Set up block producer.
234    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    /// Set up block producer using keys from file.
250    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    /// Receive block producer's coinbase reward to another account.
272    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    /// Set verifier srs. If not set, default will be used.
314    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        // build config
386        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        // build service
415        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}