lumina_node/node/
builder.rs

1use std::any::TypeId;
2use std::time::Duration;
3
4use blockstore::Blockstore;
5use libp2p::Multiaddr;
6use libp2p::identity::Keypair;
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, StoreError};
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    /// Error decoding libp2p identity from the store
56    #[error(transparent)]
57    IdentityDecodingError(#[from] libp2p::identity::DecodingError),
58
59    /// Error propagated from the store
60    #[error(transparent)]
61    StoreError(#[from] StoreError),
62}
63
64impl NodeBuilder<InMemoryBlockstore, InMemoryStore> {
65    /// Creates a new [`NodeBuilder`] which uses in-memory stores.
66    ///
67    /// After the creation you can call [`NodeBuilder::blockstore`]
68    /// and [`NodeBuilder::store`] to use other stores.
69    ///
70    /// # Example
71    ///
72    /// ```no_run
73    /// # use lumina_node::network::Network;
74    /// # use lumina_node::NodeBuilder;
75    /// #
76    /// # async fn example() {
77    /// let node = NodeBuilder::new()
78    ///     .network(Network::Mainnet)
79    ///     .start()
80    ///     .await
81    ///     .unwrap();
82    /// # }
83    /// ```
84    pub fn new() -> Self {
85        NodeBuilder {
86            blockstore: InMemoryBlockstore::new(),
87            store: InMemoryStore::new(),
88            keypair: None,
89            network: None,
90            bootnodes: Vec::new(),
91            listen: Vec::new(),
92            sync_batch_size: None,
93            pruning_window: None,
94        }
95    }
96}
97
98impl Default for NodeBuilder<InMemoryBlockstore, InMemoryStore> {
99    fn default() -> Self {
100        NodeBuilder::new()
101    }
102}
103
104impl<B, S> NodeBuilder<B, S>
105where
106    B: Blockstore + 'static,
107    S: Store + 'static,
108{
109    /// Creates and starts a new Celestia [`Node`].
110    pub async fn start(self) -> Result<Node<B, S>> {
111        let (node, _) = self.start_subscribed().await?;
112        Ok(node)
113    }
114
115    /// Creates and starts a new Celestia [`Node`].
116    ///
117    /// Returns [`Node`] along with [`EventSubscriber`]. Use this to avoid missing
118    /// any events that will be generated on the construction of the node.
119    pub async fn start_subscribed(self) -> Result<(Node<B, S>, EventSubscriber)> {
120        let config = self.build_config().await?;
121        Node::start(config).await
122    }
123
124    /// Set the [`Blockstore`] for Bitswap.
125    ///
126    /// **Default:** [`InMemoryBlockstore`]
127    pub fn blockstore<B2>(self, blockstore: B2) -> NodeBuilder<B2, S>
128    where
129        B2: Blockstore + 'static,
130    {
131        NodeBuilder {
132            blockstore,
133            store: self.store,
134            keypair: self.keypair,
135            network: self.network,
136            bootnodes: self.bootnodes,
137            listen: self.listen,
138            sync_batch_size: self.sync_batch_size,
139            pruning_window: self.pruning_window,
140        }
141    }
142
143    /// Set the [`Store`] for headers.
144    ///
145    /// **Default:** [`InMemoryStore`]
146    pub fn store<S2>(self, store: S2) -> NodeBuilder<B, S2>
147    where
148        S2: Store + 'static,
149    {
150        NodeBuilder {
151            blockstore: self.blockstore,
152            store,
153            keypair: self.keypair,
154            network: self.network,
155            bootnodes: self.bootnodes,
156            listen: self.listen,
157            sync_batch_size: self.sync_batch_size,
158            pruning_window: self.pruning_window,
159        }
160    }
161
162    /// The [`Network`] to connect to.
163    pub fn network(self, network: Network) -> Self {
164        NodeBuilder {
165            network: Some(network),
166            ..self
167        }
168    }
169
170    /// Set the keypair to be used as [`Node`]s identity.
171    ///
172    /// This enforces given libp2p identity to be used by the node.
173    ///
174    /// By default, random identity is created by the store and persisted.
175    /// Running more than one node with the same identiity is undefined behaviour,
176    /// because all nodes with the same `peer_id` will be treated as a singular
177    /// instance. Network can randomly choose a connection used to send a message
178    /// to the node, so with multiple instances, each message from the network could
179    /// be received by a random node with the same keypair. This can potentially
180    /// lead to broken state and rejecting valid messages. Please make sure that
181    /// when you use it, only one node has this keypair.
182    ///
183    /// Special care needs to be taken in wasm, as there if your app has hardcoded key,
184    /// user opening multiple tabs with it may lead to undefined behaviour.
185    pub fn keypair(self, keypair: Keypair) -> Self {
186        NodeBuilder {
187            keypair: Some(keypair),
188            ..self
189        }
190    }
191
192    /// Set the bootstrap nodes to connect and trust.
193    ///
194    /// **Default:** [`Network::canonical_bootnodes`]
195    pub fn bootnodes<I>(self, addrs: I) -> Self
196    where
197        I: IntoIterator<Item = Multiaddr>,
198    {
199        NodeBuilder {
200            bootnodes: addrs.into_iter().collect(),
201            ..self
202        }
203    }
204
205    /// Set the addresses where [`Node`] will listen for incoming connections.
206    pub fn listen<I>(self, addrs: I) -> Self
207    where
208        I: IntoIterator<Item = Multiaddr>,
209    {
210        NodeBuilder {
211            listen: addrs.into_iter().collect(),
212            ..self
213        }
214    }
215
216    /// Maximum number of headers in batch while syncing.
217    ///
218    /// **Default:** 512
219    pub fn sync_batch_size(self, batch_size: u64) -> Self {
220        NodeBuilder {
221            sync_batch_size: Some(batch_size),
222            ..self
223        }
224    }
225
226    /// Set pruning window.
227    ///
228    /// Pruning window defines maximum age of a block for it to be retained in store.
229    ///
230    /// If pruning window is smaller than sampling window, then blocks will be pruned
231    /// right after they get sampled. This is useful when you want to keep low
232    /// memory footprint but still validate the blockchain.
233    ///
234    /// **Default if [`InMemoryStore`]/[`InMemoryBlockstore`] are used:** 0 seconds.\
235    /// **Default:** 7 days plus 1 hour.\
236    pub fn pruning_window(self, dur: Duration) -> Self {
237        NodeBuilder {
238            pruning_window: Some(dur),
239            ..self
240        }
241    }
242
243    async fn build_config(self) -> Result<NodeConfig<B, S>, NodeBuilderError> {
244        let network = self.network.ok_or(NodeBuilderError::NetworkNotSpecified)?;
245
246        let bootnodes = if self.bootnodes.is_empty() {
247            network.canonical_bootnodes().collect()
248        } else {
249            self.bootnodes
250        };
251
252        if bootnodes.is_empty() && self.listen.is_empty() {
253            // It is a valid scenario for user to create a node without any bootnodes
254            // and listening addresses. However it may not be what they wanted. Because
255            // of that we display a warning.
256            warn!(
257                "Node has empty bootnodes and listening addresses. It will never connect to another peer."
258            );
259        }
260
261        #[cfg(target_arch = "wasm32")]
262        let bootnodes = {
263            let bootnodes_was_empty = bootnodes.is_empty();
264            let bootnodes = resolve_bootnode_addresses(bootnodes).await;
265
266            // If we had some bootnodes but resolving them failed for all of them,
267            // then we fail with an error.
268            if bootnodes.is_empty() && !bootnodes_was_empty {
269                return Err(NodeBuilderError::FailedResolvingBootnodes);
270            }
271
272            bootnodes
273        };
274
275        // `Node` is memory hungry when in-memory stores are used and the user may not
276        // expect they should set a smaller sampling window to reduce that. For user-friendliness
277        // sake, use smaller default sampling window, if we're running in memory.
278        let in_memory_stores_used = TypeId::of::<S>() == TypeId::of::<InMemoryStore>()
279            || TypeId::of::<B>() == TypeId::of::<InMemoryBlockstore>();
280
281        let pruning_window = if let Some(dur) = self.pruning_window {
282            dur
283        } else if in_memory_stores_used {
284            DEFAULT_PRUNING_WINDOW_IN_MEMORY
285        } else {
286            DEFAULT_PRUNING_WINDOW
287        };
288
289        info!("Sampling window: {SAMPLING_WINDOW:?}, Pruning window: {pruning_window:?}");
290
291        let p2p_local_keypair = if let Some(keypair) = self.keypair {
292            keypair
293        } else {
294            self.store.get_identity().await?
295        };
296
297        Ok(NodeConfig {
298            blockstore: self.blockstore,
299            store: self.store,
300            network_id: network.id().to_owned(),
301            p2p_local_keypair,
302            p2p_bootnodes: bootnodes,
303            p2p_listen_on: self.listen,
304            sync_batch_size: self.sync_batch_size.unwrap_or(512),
305            sampling_window: SAMPLING_WINDOW,
306            pruning_window,
307        })
308    }
309}