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