openmina_node_native/node/
builder.rs

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                // Must be replaced with builder api.
74                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    /// Set custom initial time. Used for testing.
102    pub fn custom_initial_time(&mut self, time: redux::Timestamp) -> &mut Self {
103        self.custom_initial_time = Some(time);
104        self
105    }
106
107    /// If not called, random one will be generated and used instead.
108    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    /// Set up node as a seed node.
120    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    /// Extend p2p initial peers from an iterable.
131    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    /// Extend p2p initial peers from file.
145    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    /// Extend p2p initial peers by opening the url.
162    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    /// Override default p2p task spawner.
182    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    /// Set up block producer.
194    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    /// Set up block producer using keys from file.
210    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    /// Receive block producer's coinbase reward to another account.
232    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    /// Set verifier srs. If not set, default will be used.
274    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        // build config
346        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        // build service
373        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}