lumina_node/node/
builder.rs

1use std::any::TypeId;
2use std::time::Duration;
3
4use blockstore::Blockstore;
5use libp2p::identity::Keypair;
6use libp2p::Multiaddr;
7use tracing::{info, warn};
8
9use crate::blockstore::InMemoryBlockstore;
10use crate::events::EventSubscriber;
11use crate::network::Network;
12use crate::node::{Node, NodeConfig, Result};
13use crate::store::{InMemoryStore, Store};
14#[cfg(target_arch = "wasm32")]
15use crate::utils::resolve_bootnode_addresses;
16
17const HOUR: u64 = 60 * 60;
18const DAY: u64 = 24 * HOUR;
19
20/// Default maximum age of blocks [`Node`] will synchronise, sample, and store.
21pub const SAMPLING_WINDOW: Duration = Duration::from_secs(7 * DAY);
22
23/// Default maximum age of blocks before they get pruned.
24pub const DEFAULT_PRUNING_WINDOW: Duration = Duration::from_secs(7 * DAY + HOUR);
25/// Default pruninig window for in-memory stores.
26pub const DEFAULT_PRUNING_WINDOW_IN_MEMORY: Duration = Duration::from_secs(0);
27
28/// [`Node`] builder.
29pub struct NodeBuilder<B, S>
30where
31    B: Blockstore + 'static,
32    S: Store + 'static,
33{
34    blockstore: B,
35    store: S,
36    keypair: Option<Keypair>,
37    network: Option<Network>,
38    bootnodes: Vec<Multiaddr>,
39    listen: Vec<Multiaddr>,
40    sync_batch_size: Option<u64>,
41    pruning_window: Option<Duration>,
42}
43
44/// Representation of all the errors that can occur when interacting with the [`NodeBuilder`].
45#[derive(Debug, thiserror::Error)]
46pub enum NodeBuilderError {
47    /// Network is not specified
48    #[error("Network is not specified")]
49    NetworkNotSpecified,
50
51    /// Builder failed to resolve dnsaddr multiaddresses for bootnodes
52    #[error("Could not resolve any of the bootnode addresses")]
53    FailedResolvingBootnodes,
54}
55
56impl NodeBuilder<InMemoryBlockstore, InMemoryStore> {
57    /// Creates a new [`NodeBuilder`] which uses in-memory stores.
58    ///
59    /// After the creation you can call [`NodeBuilder::blockstore`]
60    /// and [`NodeBuilder::store`] to use other stores.
61    ///
62    /// # Example
63    ///
64    /// ```no_run
65    /// # use lumina_node::network::Network;
66    /// # use lumina_node::NodeBuilder;
67    /// #
68    /// # async fn example() {
69    /// let node = NodeBuilder::new()
70    ///     .network(Network::Mainnet)
71    ///     .start()
72    ///     .await
73    ///     .unwrap();
74    /// # }
75    /// ```
76    pub fn new() -> Self {
77        NodeBuilder {
78            blockstore: InMemoryBlockstore::new(),
79            store: InMemoryStore::new(),
80            keypair: None,
81            network: None,
82            bootnodes: Vec::new(),
83            listen: Vec::new(),
84            sync_batch_size: None,
85            pruning_window: None,
86        }
87    }
88}
89
90impl Default for NodeBuilder<InMemoryBlockstore, InMemoryStore> {
91    fn default() -> Self {
92        NodeBuilder::new()
93    }
94}
95
96impl<B, S> NodeBuilder<B, S>
97where
98    B: Blockstore + 'static,
99    S: Store + 'static,
100{
101    /// Creates and starts a new Celestia [`Node`].
102    pub async fn start(self) -> Result<Node<B, S>> {
103        let (node, _) = self.start_subscribed().await?;
104        Ok(node)
105    }
106
107    /// Creates and starts a new Celestia [`Node`].
108    ///
109    /// Returns [`Node`] along with [`EventSubscriber`]. Use this to avoid missing
110    /// any events that will be generated on the construction of the node.
111    pub async fn start_subscribed(self) -> Result<(Node<B, S>, EventSubscriber)> {
112        let config = self.build_config().await?;
113        Node::start(config).await
114    }
115
116    /// Set the [`Blockstore`] for Bitswap.
117    ///
118    /// **Default:** [`InMemoryBlockstore`]
119    pub fn blockstore<B2>(self, blockstore: B2) -> NodeBuilder<B2, S>
120    where
121        B2: Blockstore + 'static,
122    {
123        NodeBuilder {
124            blockstore,
125            store: self.store,
126            keypair: self.keypair,
127            network: self.network,
128            bootnodes: self.bootnodes,
129            listen: self.listen,
130            sync_batch_size: self.sync_batch_size,
131            pruning_window: self.pruning_window,
132        }
133    }
134
135    /// Set the [`Store`] for headers.
136    ///
137    /// **Default:** [`InMemoryStore`]
138    pub fn store<S2>(self, store: S2) -> NodeBuilder<B, S2>
139    where
140        S2: Store + 'static,
141    {
142        NodeBuilder {
143            blockstore: self.blockstore,
144            store,
145            keypair: self.keypair,
146            network: self.network,
147            bootnodes: self.bootnodes,
148            listen: self.listen,
149            sync_batch_size: self.sync_batch_size,
150            pruning_window: self.pruning_window,
151        }
152    }
153
154    /// The [`Network`] to connect to.
155    pub fn network(self, network: Network) -> Self {
156        NodeBuilder {
157            network: Some(network),
158            ..self
159        }
160    }
161
162    /// Set the keypair to be used as [`Node`]s identity.
163    ///
164    /// **Default:** Random generated with [`Keypair::generate_ed25519`].
165    pub fn keypair(self, keypair: Keypair) -> Self {
166        NodeBuilder {
167            keypair: Some(keypair),
168            ..self
169        }
170    }
171
172    /// Set the bootstrap nodes to connect and trust.
173    ///
174    /// **Default:** [`Network::canonical_bootnodes`]
175    pub fn bootnodes<I>(self, addrs: I) -> Self
176    where
177        I: IntoIterator<Item = Multiaddr>,
178    {
179        NodeBuilder {
180            bootnodes: addrs.into_iter().collect(),
181            ..self
182        }
183    }
184
185    /// Set the addresses where [`Node`] will listen for incoming connections.
186    pub fn listen<I>(self, addrs: I) -> Self
187    where
188        I: IntoIterator<Item = Multiaddr>,
189    {
190        NodeBuilder {
191            listen: addrs.into_iter().collect(),
192            ..self
193        }
194    }
195
196    /// Maximum number of headers in batch while syncing.
197    ///
198    /// **Default:** 512
199    pub fn sync_batch_size(self, batch_size: u64) -> Self {
200        NodeBuilder {
201            sync_batch_size: Some(batch_size),
202            ..self
203        }
204    }
205
206    /// Set pruning window.
207    ///
208    /// Pruning window defines maximum age of a block for it to be retained in store.
209    ///
210    /// If pruning window is smaller than sampling window, then blocks will be pruned
211    /// right after they get sampled. This is useful when you want to keep low
212    /// memory footprint but still validate the blockchain.
213    ///
214    /// **Default if [`InMemoryStore`]/[`InMemoryBlockstore`] are used:** 0 seconds.\
215    /// **Default:** 7 days plus 1 hour.\
216    pub fn pruning_window(self, dur: Duration) -> Self {
217        NodeBuilder {
218            pruning_window: Some(dur),
219            ..self
220        }
221    }
222
223    async fn build_config(self) -> Result<NodeConfig<B, S>, NodeBuilderError> {
224        let network = self.network.ok_or(NodeBuilderError::NetworkNotSpecified)?;
225
226        let bootnodes = if self.bootnodes.is_empty() {
227            network.canonical_bootnodes().collect()
228        } else {
229            self.bootnodes
230        };
231
232        if bootnodes.is_empty() && self.listen.is_empty() {
233            // It is a valid scenario for user to create a node without any bootnodes
234            // and listening addresses. However it may not be what they wanted. Because
235            // of that we display a warning.
236            warn!("Node has empty bootnodes and listening addresses. It will never connect to another peer.");
237        }
238
239        #[cfg(target_arch = "wasm32")]
240        let bootnodes = {
241            let bootnodes_was_empty = bootnodes.is_empty();
242            let bootnodes = resolve_bootnode_addresses(bootnodes).await;
243
244            // If we had some bootnodes but resolving them failed for all of them,
245            // then we fail with an error.
246            if bootnodes.is_empty() && !bootnodes_was_empty {
247                return Err(NodeBuilderError::FailedResolvingBootnodes);
248            }
249
250            bootnodes
251        };
252
253        // `Node` is memory hungry when in-memory stores are used and the user may not
254        // expect they should set a smaller sampling window to reduce that. For user-friendliness
255        // sake, use smaller default sampling window, if we're running in memory.
256        let in_memory_stores_used = TypeId::of::<S>() == TypeId::of::<InMemoryStore>()
257            || TypeId::of::<B>() == TypeId::of::<InMemoryBlockstore>();
258
259        let pruning_window = if let Some(dur) = self.pruning_window {
260            dur
261        } else if in_memory_stores_used {
262            DEFAULT_PRUNING_WINDOW_IN_MEMORY
263        } else {
264            DEFAULT_PRUNING_WINDOW
265        };
266
267        info!("Sampling window: {SAMPLING_WINDOW:?}, Pruning window: {pruning_window:?}",);
268
269        Ok(NodeConfig {
270            blockstore: self.blockstore,
271            store: self.store,
272            network_id: network.id().to_owned(),
273            p2p_local_keypair: self.keypair.unwrap_or_else(Keypair::generate_ed25519),
274            p2p_bootnodes: bootnodes,
275            p2p_listen_on: self.listen,
276            sync_batch_size: self.sync_batch_size.unwrap_or(512),
277            sampling_window: SAMPLING_WINDOW,
278            pruning_window,
279        })
280    }
281}