snarkos_node/validator/mod.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
16mod router;
17
18use crate::traits::NodeInterface;
19
20use snarkos_account::Account;
21#[cfg(feature = "test_network")]
22use snarkos_node_bft::ledger_service::{persist_dev_committee_start_round_if_unwritten, prepare_dev_committee_options};
23use snarkos_node_bft::{ledger_service::CoreLedgerService, spawn_blocking};
24use snarkos_node_cdn::CdnBlockSync;
25use snarkos_node_consensus::Consensus;
26use snarkos_node_network::{ConnectionMode, NodeType, PeerPoolHandling};
27use snarkos_node_rest::Rest;
28use snarkos_node_router::{
29 Heartbeat,
30 Inbound,
31 Outbound,
32 Router,
33 Routing,
34 messages::{PuzzleResponse, UnconfirmedSolution, UnconfirmedTransaction},
35};
36use snarkos_node_sync::{BlockSync, Ping};
37use snarkos_node_tcp::{
38 P2P,
39 protocols::{Disconnect, Handshake, OnConnect, Reading},
40};
41use snarkos_utilities::{DevHotswapConfig, NodeDataDir, SignalHandler};
42
43use snarkvm::prelude::{
44 Ledger,
45 Network,
46 block::{Block, Header},
47 puzzle::Solution,
48 store::ConsensusStorage,
49};
50
51use aleo_std::StorageMode;
52use anyhow::{Context, Result};
53use cfg_if::cfg_if;
54use core::future::Future;
55#[cfg(feature = "locktick")]
56use locktick::parking_lot::Mutex;
57#[cfg(not(feature = "locktick"))]
58use parking_lot::Mutex;
59use std::{net::SocketAddr, sync::Arc, time::Duration};
60use tokio::task::JoinHandle;
61
62/// A validator is a full node, capable of validating blocks.
63#[derive(Clone)]
64pub struct Validator<N: Network, C: ConsensusStorage<N>> {
65 /// The ledger of the node.
66 ledger: Ledger<N, C>,
67 /// The consensus module of the node.
68 consensus: Consensus<N>,
69 /// The router of the node.
70 router: Router<N>,
71 /// The REST server of the node.
72 rest: Option<Rest<N, C, Self>>,
73 /// The block synchronization logic (used in the Router impl).
74 sync: Arc<BlockSync<N>>,
75 /// The spawned handles.
76 pub(crate) handles: Arc<Mutex<Vec<JoinHandle<()>>>>,
77 /// Keeps track of sending pings.
78 ping: Arc<Ping<N>>,
79}
80
81impl<N: Network, C: ConsensusStorage<N>> Validator<N, C> {
82 /// Initializes a new validator node.
83 pub async fn new(
84 node_ip: SocketAddr,
85 bft_ip: Option<SocketAddr>,
86 rest_ip: Option<SocketAddr>,
87 rest_rps: u32,
88 account: Account<N>,
89 trusted_peers: &[SocketAddr],
90 trusted_validators: &[SocketAddr],
91 genesis: Block<N>,
92 cdn: Option<http::Uri>,
93 storage_mode: StorageMode,
94 node_data_dir: NodeDataDir,
95 trusted_peers_only: bool,
96 dev_txs: bool,
97 dev: Option<u16>,
98 _slipstream_configs: &[std::path::PathBuf],
99 #[cfg(feature = "test_network")] dev_hotswap_config: Option<DevHotswapConfig>,
100 #[cfg(not(feature = "test_network"))] _dev_hotswap_config: Option<DevHotswapConfig>,
101 signal_handler: Arc<SignalHandler>,
102 ) -> Result<Self> {
103 // Initialize the ledger, installing the dev committee override (when configured)
104 // so that snarkVM's block-sync path honors the hotswap.
105 let ledger = {
106 let storage_mode = storage_mode.clone();
107 let genesis = genesis.clone();
108
109 cfg_if! {
110 if #[cfg(feature = "test_network")] {
111 if let Some(config) = dev_hotswap_config
112 .map(|cfg| prepare_dev_committee_options(&node_data_dir, cfg)) {
113 spawn_blocking!(Ledger::<N, C>::load_with_dev_committee(genesis, storage_mode, config?))
114 } else {
115 spawn_blocking!(Ledger::<N, C>::load(genesis, storage_mode))
116 }
117 } else {
118 spawn_blocking!(Ledger::<N, C>::load(genesis, storage_mode))
119 }
120 }
121 }
122 .with_context(|| "Failed to initialize the ledger")?;
123
124 // Initialize the Slipstream plugin manager (if any config files were provided).
125 #[cfg(feature = "slipstream-plugins")]
126 if !_slipstream_configs.is_empty() {
127 let manager =
128 snarkvm::slipstream_plugin_manager::SlipstreamPluginManager::from_config_files(_slipstream_configs)
129 .context("Failed to initialize Slipstream plugin manager")?;
130 ledger.vm().finalize_store().set_slipstream_plugin_manager(manager);
131 let num_plugins = _slipstream_configs.len();
132 tracing::info!(target: "slipstream", "Slipstream plugin manager registered ({num_plugins} plugin(s))");
133 }
134
135 // If snarkVM picked the start round itself (no CLI flag and no persisted file),
136 // record it now so subsequent restarts are stable.
137 #[cfg(feature = "test_network")]
138 if let Some(committee) = ledger.dev_committee() {
139 persist_dev_committee_start_round_if_unwritten(&node_data_dir, committee.starting_round())?;
140 }
141
142 // Initialize the ledger service.
143 let ledger_service = Arc::new(CoreLedgerService::new(ledger.clone(), signal_handler.clone()));
144
145 // Initialize the node router.
146 let router = Router::new(
147 node_ip,
148 NodeType::Validator,
149 account.clone(),
150 ledger_service.clone(),
151 trusted_peers,
152 Self::MAXIMUM_NUMBER_OF_PEERS as u16,
153 trusted_peers_only,
154 node_data_dir.clone(),
155 dev.is_some(),
156 )
157 .await?;
158
159 // Initialize the block synchronization logic.
160 let sync = Arc::new(BlockSync::new(ledger_service.clone(), ConnectionMode::Gateway));
161 let locators = sync.get_block_locators()?;
162 let ping = Arc::new(Ping::new(router.clone(), locators));
163
164 // Initialize the consensus layer.
165 let consensus = Consensus::new(
166 account.clone(),
167 ledger_service.clone(),
168 sync.clone(),
169 bft_ip,
170 trusted_validators,
171 trusted_peers_only,
172 storage_mode.clone(),
173 node_data_dir.clone(),
174 ping.clone(),
175 dev,
176 )
177 .await?;
178
179 // Initialize the node.
180 let mut node = Self {
181 ledger: ledger.clone(),
182 consensus: consensus.clone(),
183 router,
184 rest: None,
185 sync: sync.clone(),
186 ping,
187 handles: Default::default(),
188 };
189
190 // Perform sync with CDN (if enabled).
191 let cdn_sync = cdn.map(|base_url| Arc::new(CdnBlockSync::new(base_url, ledger.clone(), signal_handler)));
192
193 // Initialize the transaction pool.
194 node.initialize_transaction_pool(dev, dev_txs)?;
195
196 // Initialize the REST server.
197 if let Some(rest_ip) = rest_ip {
198 node.rest = Some(
199 Rest::start(
200 rest_ip,
201 rest_rps,
202 Some(consensus),
203 ledger.clone(),
204 Arc::new(node.clone()),
205 cdn_sync.clone(),
206 sync,
207 )
208 .await?,
209 );
210 }
211
212 // Set up everything else after CDN sync is done.
213 if let Some(cdn_sync) = cdn_sync {
214 if let Err(error) = cdn_sync.wait().await.with_context(|| "Failed to synchronize from the CDN") {
215 crate::log_clean_error(&storage_mode);
216 node.shut_down().await;
217 return Err(error);
218 }
219 }
220
221 // Start the BFT and consensus handlers now that CDN sync is complete. This ensures that
222 // committed subdags are only processed after the initial sync from the CDN has finished.
223 node.start_consensus_handlers().await?;
224 // Initialize the routing.
225 node.initialize_routing().await;
226 // Initialize the notification message loop.
227 node.handles.lock().push(crate::start_notification_message_loop());
228
229 // Return the node.
230 Ok(node)
231 }
232
233 /// Returns the ledger.
234 pub fn ledger(&self) -> &Ledger<N, C> {
235 &self.ledger
236 }
237
238 /// Returns the REST server.
239 pub fn rest(&self) -> &Option<Rest<N, C, Self>> {
240 &self.rest
241 }
242
243 /// Returns the router.
244 pub fn router(&self) -> &Router<N> {
245 &self.router
246 }
247
248 /// Starts the BFT and consensus handlers.
249 async fn start_consensus_handlers(&self) -> Result<()> {
250 self.consensus.start_consensus_handlers().await
251 }
252
253 // /// Initialize the transaction pool.
254 // fn initialize_transaction_pool(&self, dev: Option<u16>) -> Result<()> {
255 // use snarkvm::{
256 // console::{
257 // account::ViewKey,
258 // program::{Identifier, Literal, Plaintext, ProgramID, Record, Value},
259 // types::U64,
260 // },
261 // ledger::block::transition::Output,
262 // };
263 // use std::str::FromStr;
264 //
265 // // Initialize the locator.
266 // let locator = (ProgramID::from_str("credits.aleo")?, Identifier::from_str("split")?);
267 // // Initialize the record name.
268 // let record_name = Identifier::from_str("credits")?;
269 //
270 // /// Searches the genesis block for the mint record.
271 // fn search_genesis_for_mint<N: Network>(
272 // block: Block<N>,
273 // view_key: &ViewKey<N>,
274 // ) -> Option<Record<N, Plaintext<N>>> {
275 // for transition in block.transitions().filter(|t| t.is_mint()) {
276 // if let Output::Record(_, _, Some(ciphertext)) = &transition.outputs()[0] {
277 // if ciphertext.is_owner(view_key) {
278 // match ciphertext.decrypt(view_key) {
279 // Ok(record) => return Some(record),
280 // Err(error) => {
281 // error!("Failed to decrypt the mint output record - {error}");
282 // return None;
283 // }
284 // }
285 // }
286 // }
287 // }
288 // None
289 // }
290 //
291 // /// Searches the block for the split record.
292 // fn search_block_for_split<N: Network>(
293 // block: Block<N>,
294 // view_key: &ViewKey<N>,
295 // ) -> Option<Record<N, Plaintext<N>>> {
296 // let mut found = None;
297 // // TODO (howardwu): Switch to the iterator when DoubleEndedIterator is supported.
298 // // block.transitions().rev().for_each(|t| {
299 // let splits = block.transitions().filter(|t| t.is_split()).collect::<Vec<_>>();
300 // splits.iter().rev().for_each(|t| {
301 // if found.is_some() {
302 // return;
303 // }
304 // let Output::Record(_, _, Some(ciphertext)) = &t.outputs()[1] else {
305 // error!("Failed to find the split output record");
306 // return;
307 // };
308 // if ciphertext.is_owner(view_key) {
309 // match ciphertext.decrypt(view_key) {
310 // Ok(record) => found = Some(record),
311 // Err(error) => {
312 // error!("Failed to decrypt the split output record - {error}");
313 // }
314 // }
315 // }
316 // });
317 // found
318 // }
319 //
320 // let self_ = self.clone();
321 // self.spawn(async move {
322 // // Retrieve the view key.
323 // let view_key = self_.view_key();
324 // // Initialize the record.
325 // let mut record = {
326 // let mut found = None;
327 // let mut height = self_.ledger.latest_height();
328 // while found.is_none() && height > 0 {
329 // // Retrieve the block.
330 // let Ok(block) = self_.ledger.get_block(height) else {
331 // error!("Failed to get block at height {}", height);
332 // break;
333 // };
334 // // Search for the latest split record.
335 // if let Some(record) = search_block_for_split(block, view_key) {
336 // found = Some(record);
337 // }
338 // // Decrement the height.
339 // height = height.saturating_sub(1);
340 // }
341 // match found {
342 // Some(record) => record,
343 // None => {
344 // // Retrieve the genesis block.
345 // let Ok(block) = self_.ledger.get_block(0) else {
346 // error!("Failed to get the genesis block");
347 // return;
348 // };
349 // // Search the genesis block for the mint record.
350 // if let Some(record) = search_genesis_for_mint(block, view_key) {
351 // found = Some(record);
352 // }
353 // found.expect("Failed to find the split output record")
354 // }
355 // }
356 // };
357 // info!("Starting transaction pool...");
358 // // Start the transaction loop.
359 // loop {
360 // tokio::time::sleep(Duration::from_secs(1)).await;
361 // // If the node is running in development mode, only generate if you are allowed.
362 // if let Some(dev) = dev {
363 // if dev != 0 {
364 // continue;
365 // }
366 // }
367 //
368 // // Prepare the inputs.
369 // let inputs = [Value::from(record.clone()), Value::from(Literal::U64(U64::new(1)))].into_iter();
370 // // Execute the transaction.
371 // let transaction = match self_.ledger.vm().execute(
372 // self_.private_key(),
373 // locator,
374 // inputs,
375 // None,
376 // None,
377 // &mut rand::rng(),
378 // ) {
379 // Ok(transaction) => transaction,
380 // Err(error) => {
381 // error!("Transaction pool encountered an execution error - {error}");
382 // continue;
383 // }
384 // };
385 // // Retrieve the transition.
386 // let Some(transition) = transaction.transitions().next() else {
387 // error!("Transaction pool encountered a missing transition");
388 // continue;
389 // };
390 // // Retrieve the second output.
391 // let Output::Record(_, _, Some(ciphertext)) = &transition.outputs()[1] else {
392 // error!("Transaction pool encountered a missing output");
393 // continue;
394 // };
395 // // Save the second output record.
396 // let Ok(next_record) = ciphertext.decrypt(view_key) else {
397 // error!("Transaction pool encountered a decryption error");
398 // continue;
399 // };
400 // // Broadcast the transaction.
401 // if self_
402 // .unconfirmed_transaction(
403 // self_.router.local_ip(),
404 // UnconfirmedTransaction::from(transaction.clone()),
405 // transaction.clone(),
406 // )
407 // .await
408 // {
409 // info!("Transaction pool broadcasted the transaction");
410 // let commitment = next_record.to_commitment(&locator.0, &record_name).unwrap();
411 // while !self_.ledger.contains_commitment(&commitment).unwrap_or(false) {
412 // tokio::time::sleep(Duration::from_secs(1)).await;
413 // }
414 // info!("Transaction accepted by the ledger");
415 // }
416 // // Save the record.
417 // record = next_record;
418 // }
419 // });
420 // Ok(())
421 // }
422
423 /// Initializes the transaction pool (if in development mode).
424 ///
425 /// Spawns a background task that periodically issues transactions to the network.
426 fn initialize_transaction_pool(&self, dev: Option<u16>, dev_txs: bool) -> Result<()> {
427 use snarkvm::console::{
428 program::{Identifier, Literal, ProgramID, Value},
429 types::U64,
430 };
431 use std::str::FromStr;
432
433 // Initialize the locator.
434 let locator = (ProgramID::from_str("credits.aleo")?, Identifier::from_str("transfer_public")?);
435
436 // Determine whether to start the loop.
437 match dev {
438 // If the node is running in development mode, only generate if you are allowed.
439 Some(id) => {
440 // If the node is not the first node, or if we should not create dev traffic, do not start the loop.
441 if id != 0 || !dev_txs {
442 return Ok(());
443 }
444 }
445 // If the node is not running in development mode, do not generate dev traffic.
446 _ => return Ok(()),
447 }
448
449 let self_ = self.clone();
450 self.spawn(async move {
451 tokio::time::sleep(Duration::from_secs(3)).await;
452 info!("Starting transaction pool...");
453
454 // Start the transaction loop.
455 loop {
456 tokio::time::sleep(Duration::from_millis(500)).await;
457
458 // Prepare the inputs.
459 let inputs = [Value::from(Literal::Address(self_.address())), Value::from(Literal::U64(U64::new(1)))];
460 // Execute the transaction.
461 let self__ = self_.clone();
462 let transaction = match tokio::task::spawn_blocking(move || {
463 self__.ledger.vm().execute(
464 self__.private_key(),
465 locator,
466 inputs.into_iter(),
467 None,
468 10_000,
469 None,
470 &mut rand::rng(),
471 )
472 })
473 .await
474 {
475 Ok(Ok(transaction)) => transaction,
476 Ok(Err(error)) => {
477 error!("Transaction pool encountered an execution error - {error}");
478 continue;
479 }
480 Err(error) => {
481 error!("Transaction pool encountered an execution error - blocking task failed - {error}");
482 continue;
483 }
484 };
485 // Broadcast the transaction.
486 if self_
487 .unconfirmed_transaction(
488 self_.router.local_ip(),
489 UnconfirmedTransaction::from(transaction.clone()),
490 transaction.clone(),
491 )
492 .await
493 {
494 info!("Transaction pool broadcasted the transaction");
495 }
496 }
497 });
498 Ok(())
499 }
500
501 /// Spawns a task with the given future; it should only be used for long-running tasks.
502 pub fn spawn<T: Future<Output = ()> + Send + 'static>(&self, future: T) {
503 self.handles.lock().push(tokio::spawn(future));
504 }
505}
506
507#[async_trait]
508impl<N: Network, C: ConsensusStorage<N>> NodeInterface<N> for Validator<N, C> {
509 /// Shuts down the node.
510 async fn shut_down(&self) {
511 info!("Shutting down...");
512
513 // Shut down the node.
514 trace!("Shutting down the node...");
515
516 // Shut down the Slipstream plugin manager.
517 #[cfg(feature = "slipstream-plugins")]
518 if let Some(manager) = self.ledger.vm().finalize_store().slipstream_plugin_manager().write().as_mut() {
519 manager.unload();
520 }
521
522 // Shut down the REST instance.
523 if let Some(rest) = &self.rest {
524 trace!("Shutting down the REST server...");
525 rest.shut_down();
526 }
527
528 // Abort the tasks.
529 trace!("Shutting down the validator...");
530 self.handles.lock().iter().for_each(|handle| handle.abort());
531
532 // Shut down the router.
533 self.router.shut_down().await;
534
535 // Shut down consensus.
536 trace!("Shutting down consensus...");
537 self.consensus.shut_down().await;
538
539 info!("Node has shut down.");
540 }
541}
542
543#[cfg(test)]
544mod tests {
545 use super::*;
546 use snarkvm::prelude::{
547 MainnetV0,
548 VM,
549 store::{ConsensusStore, helpers::memory::ConsensusMemory},
550 };
551
552 use anyhow::bail;
553 use rand::SeedableRng;
554 use rand_chacha::ChaChaRng;
555 use std::str::FromStr;
556
557 type CurrentNetwork = MainnetV0;
558
559 /// Use `RUST_MIN_STACK=67108864 cargo test --release profiler --features timer` to run this test.
560 #[ignore]
561 #[tokio::test]
562 async fn test_profiler() -> Result<()> {
563 // Specify the node attributes.
564 let node = SocketAddr::from_str("0.0.0.0:4130").unwrap();
565 let rest = SocketAddr::from_str("0.0.0.0:3030").unwrap();
566 let storage_mode = StorageMode::Development(0);
567 let node_data_dir = NodeDataDir::new_development(CurrentNetwork::ID, 0);
568 let dev_txs = true;
569
570 // Initialize an (insecure) fixed RNG.
571 let mut rng = ChaChaRng::seed_from_u64(snarkos_utilities::DEVELOPMENT_MODE_RNG_SEED);
572 // Initialize the account.
573 let account = Account::<CurrentNetwork>::new(&mut rng).unwrap();
574 // Initialize a new VM.
575 let vm = VM::from(ConsensusStore::<CurrentNetwork, ConsensusMemory<CurrentNetwork>>::open(
576 StorageMode::new_test(None),
577 )?)?;
578 // Initialize the genesis block.
579 let genesis = vm.genesis_beacon(account.private_key(), &mut rng)?;
580
581 println!("Initializing validator node...");
582
583 let validator = Validator::<CurrentNetwork, ConsensusMemory<CurrentNetwork>>::new(
584 node,
585 None,
586 Some(rest),
587 10,
588 account,
589 &[],
590 &[],
591 genesis,
592 None,
593 storage_mode,
594 node_data_dir,
595 false,
596 dev_txs,
597 None,
598 &[],
599 None,
600 SignalHandler::new(None),
601 )
602 .await
603 .unwrap();
604
605 println!("Loaded validator node with {} blocks", validator.ledger.latest_height(),);
606
607 bail!("\n\nRemember to #[ignore] this test!\n\n")
608 }
609}