Skip to main content

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}