Skip to main content

snarkos_node/
node.rs

1// Copyright (c) 2019-2026 Provable Inc.
2// This file is part of the snarkOS library.
3
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at:
7
8// http://www.apache.org/licenses/LICENSE-2.0
9
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16use crate::{
17    BootstrapClient,
18    Client,
19    Prover,
20    Validator,
21    network::{NodeType, Peer, PeerPoolHandling},
22    router::Outbound,
23    traits::NodeInterface,
24};
25
26use snarkos_account::Account;
27use snarkos_utilities::{DevHotswapConfig, NodeDataDir, SignalHandler};
28
29use snarkvm::prelude::{
30    Address,
31    Header,
32    Ledger,
33    Network,
34    PrivateKey,
35    ViewKey,
36    block::Block,
37    store::helpers::{memory::ConsensusMemory, rocksdb::ConsensusDB},
38};
39
40use aleo_std::{StorageMode, aleo_ledger_dir};
41use anyhow::{Result, bail};
42
43#[cfg(feature = "locktick")]
44use locktick::parking_lot::RwLock;
45#[cfg(not(feature = "locktick"))]
46use parking_lot::RwLock;
47use std::{
48    cmp,
49    collections::HashMap,
50    fs,
51    net::SocketAddr,
52    path::{Path, PathBuf},
53    str::FromStr,
54    sync::Arc,
55    time::Duration,
56};
57use tokio::task;
58
59/// The number of blocks between automatic database checkpoints.
60const CHECKPOINT_BLOCK_FREQUENCY: u32 = 1000;
61
62/// The maximum number of automatic database checkpoints kept at any time.
63const MAX_AUTO_CHECKPOINTS: usize = 5;
64
65/// How often to publish RocksDB internal metrics to Prometheus.
66#[cfg(feature = "metrics")]
67const ROCKSDB_METRICS_INTERVAL: Duration = Duration::from_secs(15);
68
69fn existing_startup_checkpoint_height(auto_checkpoint_path: &Path, startup_height: u32) -> Option<u32> {
70    let mut checkpoint_path = auto_checkpoint_path.to_path_buf();
71    checkpoint_path.push(format!("checkpoint_{startup_height}"));
72    checkpoint_path.is_dir().then_some(startup_height)
73}
74
75#[derive(Clone)]
76pub enum Node<N: Network> {
77    /// A validator is a full node, capable of validating blocks.
78    Validator(Arc<Validator<N, ConsensusDB<N>>>),
79    /// A prover is a light node, capable of producing proofs for consensus.
80    Prover(Arc<Prover<N, ConsensusMemory<N>>>),
81    /// A client node is a full node, capable of querying with the network.
82    Client(Arc<Client<N, ConsensusDB<N>>>),
83    /// A bootstrap client node is a light node dedicated to serving lists of peers.
84    BootstrapClient(BootstrapClient<N>),
85}
86
87impl<N: Network> Node<N> {
88    /// Initializes a new validator node.
89    pub async fn new_validator(
90        node_ip: SocketAddr,
91        bft_ip: Option<SocketAddr>,
92        rest_ip: Option<SocketAddr>,
93        rest_rps: u32,
94        account: Account<N>,
95        trusted_peers: &[SocketAddr],
96        trusted_validators: &[SocketAddr],
97        genesis: Block<N>,
98        cdn: Option<http::Uri>,
99        storage_mode: StorageMode,
100        node_data_dir: NodeDataDir,
101        trusted_peers_only: bool,
102        auto_db_checkpoints: Option<PathBuf>,
103        dev_txs: bool,
104        dev: Option<u16>,
105        slipstream_configs: &[PathBuf],
106        dev_hotswap_config: Option<DevHotswapConfig>,
107        signal_handler: Arc<SignalHandler>,
108    ) -> Result<Self> {
109        let validator = Arc::new(
110            Validator::new(
111                node_ip,
112                bft_ip,
113                rest_ip,
114                rest_rps,
115                account,
116                trusted_peers,
117                trusted_validators,
118                genesis,
119                cdn,
120                storage_mode,
121                node_data_dir,
122                trusted_peers_only,
123                dev_txs,
124                dev,
125                slipstream_configs,
126                dev_hotswap_config,
127                signal_handler,
128            )
129            .await?,
130        );
131
132        let node = Self::Validator(validator.clone());
133
134        // Perform automatic ledger checkpoints.
135        if let Some(path) = auto_db_checkpoints {
136            if let Some(handle) = node.perform_auto_checkpoints(path)? {
137                validator.handles.lock().push(handle);
138            }
139        }
140
141        #[cfg(feature = "metrics")]
142        if let Some(handle) = node.spawn_rocksdb_metrics_polling() {
143            validator.handles.lock().push(handle);
144        }
145
146        Ok(node)
147    }
148
149    /// Initializes a new prover node.
150    pub async fn new_prover(
151        node_ip: SocketAddr,
152        account: Account<N>,
153        trusted_peers: &[SocketAddr],
154        genesis: Block<N>,
155        node_data_dir: NodeDataDir,
156        trusted_peers_only: bool,
157        dev: Option<u16>,
158        signal_handler: Arc<SignalHandler>,
159    ) -> Result<Self> {
160        Ok(Self::Prover(Arc::new(
161            Prover::new(
162                node_ip,
163                account,
164                trusted_peers,
165                genesis,
166                node_data_dir,
167                trusted_peers_only,
168                dev,
169                signal_handler,
170            )
171            .await?,
172        )))
173    }
174
175    /// Initializes a new client node.
176    pub async fn new_client(
177        node_ip: SocketAddr,
178        rest_ip: Option<SocketAddr>,
179        rest_rps: u32,
180        account: Account<N>,
181        trusted_peers: &[SocketAddr],
182        genesis: Block<N>,
183        cdn: Option<http::Uri>,
184        storage_mode: StorageMode,
185        node_data_dir: NodeDataDir,
186        trusted_peers_only: bool,
187        auto_db_checkpoints: Option<PathBuf>,
188        dev: Option<u16>,
189        slipstream_configs: &[PathBuf],
190        signal_handler: Arc<SignalHandler>,
191    ) -> Result<Self> {
192        let client = Arc::new(
193            Client::new(
194                node_ip,
195                rest_ip,
196                rest_rps,
197                account,
198                trusted_peers,
199                genesis,
200                cdn,
201                storage_mode,
202                node_data_dir,
203                trusted_peers_only,
204                dev,
205                slipstream_configs,
206                signal_handler,
207            )
208            .await?,
209        );
210
211        let node = Self::Client(client.clone());
212
213        // Perform automatic ledger checkpoints.
214        if let Some(path) = auto_db_checkpoints {
215            if let Some(handle) = node.perform_auto_checkpoints(path)? {
216                client.handles.lock().push(handle);
217            }
218        }
219
220        #[cfg(feature = "metrics")]
221        if let Some(handle) = node.spawn_rocksdb_metrics_polling() {
222            client.handles.lock().push(handle);
223        }
224
225        Ok(node)
226    }
227
228    /// Initializes a new bootstrap client node.
229    pub async fn new_bootstrap_client(
230        listener_addr: SocketAddr,
231        account: Account<N>,
232        genesis_header: Header<N>,
233        dev: Option<u16>,
234    ) -> Result<Self> {
235        Ok(Self::BootstrapClient(BootstrapClient::new(listener_addr, account, genesis_header, dev).await?))
236    }
237
238    /// Returns the node type.
239    pub fn node_type(&self) -> NodeType {
240        match self {
241            Self::Validator(validator) => validator.node_type(),
242            Self::Prover(prover) => prover.node_type(),
243            Self::Client(client) => client.node_type(),
244            Self::BootstrapClient(_) => NodeType::BootstrapClient,
245        }
246    }
247
248    /// Returns the account private key of the node.
249    pub fn private_key(&self) -> &PrivateKey<N> {
250        match self {
251            Self::Validator(node) => node.private_key(),
252            Self::Prover(node) => node.private_key(),
253            Self::Client(node) => node.private_key(),
254            Self::BootstrapClient(node) => node.private_key(),
255        }
256    }
257
258    /// Returns the account view key of the node.
259    pub fn view_key(&self) -> &ViewKey<N> {
260        match self {
261            Self::Validator(node) => node.view_key(),
262            Self::Prover(node) => node.view_key(),
263            Self::Client(node) => node.view_key(),
264            Self::BootstrapClient(node) => node.view_key(),
265        }
266    }
267
268    /// Returns the account address of the node.
269    pub fn address(&self) -> Address<N> {
270        match self {
271            Self::Validator(node) => node.address(),
272            Self::Prover(node) => node.address(),
273            Self::Client(node) => node.address(),
274            Self::BootstrapClient(node) => node.address(),
275        }
276    }
277
278    /// Returns `true` if the node is in development mode.
279    pub fn is_dev(&self) -> bool {
280        match self {
281            Self::Validator(node) => node.is_dev(),
282            Self::Prover(node) => node.is_dev(),
283            Self::Client(node) => node.is_dev(),
284            Self::BootstrapClient(node) => node.is_dev(),
285        }
286    }
287
288    /// Returns a reference to the underlying peer pool.
289    pub fn peer_pool(&self) -> &RwLock<HashMap<SocketAddr, Peer<N>>> {
290        match self {
291            Self::Validator(validator) => validator.router().peer_pool(),
292            Self::Prover(prover) => prover.router().peer_pool(),
293            Self::Client(client) => client.router().peer_pool(),
294            Self::BootstrapClient(client) => client.peer_pool(),
295        }
296    }
297
298    /// Get the underlying ledger (if any).
299    pub fn ledger(&self) -> Option<&Ledger<N, ConsensusDB<N>>> {
300        match self {
301            Self::Validator(node) => Some(node.ledger()),
302            Self::Prover(_) => None,
303            Self::Client(node) => Some(node.ledger()),
304            Self::BootstrapClient(_) => None,
305        }
306    }
307
308    /// Returns `true` if the node is synced up to the latest block (within the given tolerance).
309    pub fn is_block_synced(&self) -> bool {
310        match self {
311            Self::Validator(node) => node.is_block_synced(),
312            Self::Prover(node) => node.is_block_synced(),
313            Self::Client(node) => node.is_block_synced(),
314            Self::BootstrapClient(_) => true,
315        }
316    }
317
318    /// Returns the number of blocks this node is behind the greatest peer height,
319    /// or `None` if not connected to peers yet.
320    pub fn num_blocks_behind(&self) -> Option<u32> {
321        match self {
322            Self::Validator(node) => node.num_blocks_behind(),
323            Self::Prover(node) => node.num_blocks_behind(),
324            Self::Client(node) => node.num_blocks_behind(),
325            Self::BootstrapClient(_) => Some(0),
326        }
327    }
328
329    /// Calculates the current sync speed in blocks per second.
330    /// Returns None if sync speed cannot be calculated (e.g., not syncing or insufficient data).
331    pub fn get_sync_speed(&self) -> f64 {
332        match self {
333            Self::Validator(node) => node.get_sync_speed(),
334            Self::Prover(node) => node.get_sync_speed(),
335            Self::Client(node) => node.get_sync_speed(),
336            Self::BootstrapClient(_) => 0.0,
337        }
338    }
339
340    /// Shuts down the node.
341    pub async fn shut_down(&self) {
342        match self {
343            Self::Validator(node) => node.shut_down().await,
344            Self::Prover(node) => node.shut_down().await,
345            Self::Client(node) => node.shut_down().await,
346            Self::BootstrapClient(node) => node.shut_down().await,
347        }
348    }
349
350    /// Waits until the node receives a signal.
351    pub async fn wait_for_signals(&self, signal_handler: &SignalHandler) {
352        match self {
353            Self::Validator(node) => node.wait_for_signals(signal_handler).await,
354            Self::Prover(node) => node.wait_for_signals(signal_handler).await,
355            Self::Client(node) => node.wait_for_signals(signal_handler).await,
356            Self::BootstrapClient(node) => node.wait_for_signals(signal_handler).await,
357        }
358    }
359
360    /// Spawns a background task that periodically publishes RocksDB internal metrics.
361    #[cfg(feature = "metrics")]
362    pub fn spawn_rocksdb_metrics_polling(&self) -> Option<task::JoinHandle<()>> {
363        let ledger = self.ledger()?.clone();
364        Some(tokio::spawn(async move {
365            let mut interval = tokio::time::interval(ROCKSDB_METRICS_INTERVAL);
366            loop {
367                interval.tick().await;
368                ledger.vm().block_store().export_rocksdb_metrics();
369            }
370        }))
371    }
372
373    /// Periodically creates automated ledger checkpoints.
374    pub fn perform_auto_checkpoints(&self, auto_checkpoint_path: PathBuf) -> Result<Option<task::JoinHandle<()>>> {
375        // Only perform checkpoints if there's a database involved.
376        let Some(ledger) = self.ledger().cloned() else {
377            return Ok(None);
378        };
379
380        // Ensure that the target path exists as a folder or create it.
381        if !auto_checkpoint_path.exists() {
382            if let Err(e) = fs::create_dir_all(&auto_checkpoint_path) {
383                bail!("Couldn't create the specified path for the automatic ledger checkpoints: {e}");
384            }
385        } else if auto_checkpoint_path.exists() && !auto_checkpoint_path.is_dir() {
386            bail!("The specified path for automatic ledger checkpoints is not a directory");
387        }
388
389        // Spawn a loop that will periodically create the checkpoints.
390        let handle = tokio::spawn(async move {
391            info!("Starting the automatic ledger checkpoint routine...");
392
393            // Prepare some object that will be useful throughout the routine.
394            let startup_height = ledger.vm().block_store().current_block_height();
395            let mut last_checkpoint_height =
396                existing_startup_checkpoint_height(auto_checkpoint_path.as_path(), startup_height);
397            let mut existing_checkpoints = Vec::with_capacity(MAX_AUTO_CHECKPOINTS + 1);
398            let mut block_tree_path = aleo_ledger_dir(N::ID, ledger.vm().block_store().storage_mode());
399            block_tree_path.push("block_tree");
400
401            loop {
402                // A small delay that's smaller than block time. There are technically situations when
403                // blocks can be inserted one after the other more quickly (syncing, multiple blocks in
404                // a Subdag), those are edge cases unlikely to be encountered under normal conditions.
405                tokio::time::sleep(Duration::from_millis(500)).await;
406
407                // Skip if we've already created a checkpoint during this run, and the
408                // number of blocks baked since then is lower than the configured threshold.
409                let current_height = ledger.vm().block_store().current_block_height();
410                if last_checkpoint_height.is_some_and(|checkpoint_height| {
411                    current_height.saturating_sub(checkpoint_height) < CHECKPOINT_BLOCK_FREQUENCY
412                }) {
413                    continue;
414                }
415
416                // Create a checkpoint.
417                let mut checkpoint_path = auto_checkpoint_path.clone();
418                checkpoint_path.push(format!("checkpoint_{current_height}"));
419                if let Err(e) = ledger.backup_database(&checkpoint_path) {
420                    warn!("Couldn't automatically store a checkpoint at {}: {e}", checkpoint_path.display());
421                    continue;
422                }
423                last_checkpoint_height = Some(current_height);
424
425                // Immediately procure and copy the applicable block tree in the background.
426                let ledger_clone = ledger.clone();
427                let source_block_tree_path = block_tree_path.clone();
428                tokio::spawn(async move {
429                    if let Err(e) = ledger_clone.cache_block_tree() {
430                        warn!("Couldn't cache the block tree for a ledger checkpoint: {e}");
431                        return;
432                    }
433
434                    // Copy the block tree file to the new checkpoint.
435                    checkpoint_path.push("block_tree");
436                    if let Err(e) = fs::copy(source_block_tree_path, checkpoint_path) {
437                        warn!("Couldn't copy the block tree file to a ledger checkpoint: {e}");
438                    }
439                });
440
441                // Count the existing auto checkpoints.
442                existing_checkpoints.clear();
443                let checkpoint_dir = match auto_checkpoint_path.read_dir() {
444                    Ok(dir) => dir,
445                    Err(e) => {
446                        warn!("IO error while accessing the automatic checkpoints: {e}");
447                        continue;
448                    }
449                };
450                for entry in checkpoint_dir {
451                    // Handle possible IO errors.
452                    let entry = match entry {
453                        Ok(entry) => entry,
454                        Err(e) => {
455                            warn!("IO error while counting the automatic checkpoints: {e}");
456                            continue;
457                        }
458                    };
459
460                    // Skip non-directories.
461                    let path = entry.path();
462                    if !path.is_dir() {
463                        continue;
464                    }
465
466                    // Recognize checkpoints by the "checkpoint_height" name.
467                    let file_name = entry.file_name().into_string().unwrap(); // can't fail - we create Unicode filenames
468                    let mut name_iter = file_name.split("_");
469                    if name_iter.next() != Some("checkpoint") {
470                        continue;
471                    }
472                    let Some(height) = name_iter.next() else {
473                        continue;
474                    };
475                    let Ok(height) = u32::from_str(height) else {
476                        continue;
477                    };
478                    existing_checkpoints.push((path, height));
479                }
480                existing_checkpoints.sort_unstable_by_key(|(_, height)| cmp::Reverse(*height));
481
482                // If we have a sufficient number of checkpoints, delete the oldest one(s).
483                let surplus_checkpoints = existing_checkpoints.len().saturating_sub(MAX_AUTO_CHECKPOINTS);
484                for _ in 0..surplus_checkpoints {
485                    if let Some((checkpoint_path, _)) = existing_checkpoints.pop() {
486                        if let Err(e) = fs::remove_dir_all(checkpoint_path) {
487                            warn!("Couldn't remove an automatic ledger checkpoint: {e}");
488                        }
489                    }
490                }
491            }
492        });
493
494        Ok(Some(handle))
495    }
496}
497
498#[cfg(test)]
499mod tests {
500    use super::existing_startup_checkpoint_height;
501    use std::{
502        fs,
503        time::{SystemTime, UNIX_EPOCH},
504    };
505
506    #[test]
507    fn seeds_last_checkpoint_height_when_startup_checkpoint_directory_exists() {
508        let startup_height = 42;
509        let unique = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos();
510        let base_path = std::env::temp_dir().join(format!("snarkos_checkpoint_seed_test_{unique}"));
511        let checkpoint_path = base_path.join(format!("checkpoint_{startup_height}"));
512        fs::create_dir_all(&checkpoint_path).unwrap();
513
514        let seeded_height = existing_startup_checkpoint_height(base_path.as_path(), startup_height);
515        assert_eq!(seeded_height, Some(startup_height));
516
517        fs::remove_dir_all(base_path).unwrap();
518    }
519
520    #[test]
521    fn does_not_seed_last_checkpoint_height_when_startup_checkpoint_directory_missing() {
522        let startup_height = 42;
523        let unique = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos();
524        let base_path = std::env::temp_dir().join(format!("snarkos_checkpoint_seed_test_{unique}"));
525        fs::create_dir_all(&base_path).unwrap();
526
527        let seeded_height = existing_startup_checkpoint_height(base_path.as_path(), startup_height);
528        assert_eq!(seeded_height, None);
529
530        fs::remove_dir_all(base_path).unwrap();
531    }
532}