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