1use 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
59const CHECKPOINT_BLOCK_FREQUENCY: u32 = 1000;
61
62const MAX_AUTO_CHECKPOINTS: usize = 5;
64
65#[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 Validator(Arc<Validator<N, ConsensusDB<N>>>),
79 Prover(Arc<Prover<N, ConsensusMemory<N>>>),
81 Client(Arc<Client<N, ConsensusDB<N>>>),
83 BootstrapClient(BootstrapClient<N>),
85}
86
87impl<N: Network> Node<N> {
88 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 #[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 pub fn perform_auto_checkpoints(&self, auto_checkpoint_path: PathBuf) -> Result<Option<task::JoinHandle<()>>> {
375 let Some(ledger) = self.ledger().cloned() else {
377 return Ok(None);
378 };
379
380 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 let handle = tokio::spawn(async move {
391 info!("Starting the automatic ledger checkpoint routine...");
392
393 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 tokio::time::sleep(Duration::from_millis(500)).await;
406
407 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 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 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 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 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 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 let path = entry.path();
462 if !path.is_dir() {
463 continue;
464 }
465
466 let file_name = entry.file_name().into_string().unwrap(); 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 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}