Skip to main content

snarkvm_ledger/
lib.rs

1// Copyright (c) 2019-2025 Provable Inc.
2// This file is part of the snarkVM 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
16#![forbid(unsafe_code)]
17#![warn(clippy::cast_possible_truncation)]
18
19extern crate snarkvm_console as console;
20
21#[macro_use]
22extern crate tracing;
23
24pub use snarkvm_ledger_authority as authority;
25pub use snarkvm_ledger_block as block;
26pub use snarkvm_ledger_committee as committee;
27pub use snarkvm_ledger_narwhal as narwhal;
28pub use snarkvm_ledger_puzzle as puzzle;
29pub use snarkvm_ledger_query as query;
30pub use snarkvm_ledger_store as store;
31
32#[cfg(any(test, feature = "test-helpers"))]
33pub mod test_helpers;
34
35mod helpers;
36pub use helpers::*;
37
38pub use crate::block::*;
39
40mod check_next_block;
41pub use check_next_block::{CheckBlockError, PendingBlock};
42
43mod advance;
44mod check_transaction_basic;
45mod contains;
46mod find;
47mod get;
48mod is_solution_limit_reached;
49mod iterators;
50
51#[cfg(test)]
52mod tests;
53
54use console::{
55    account::{Address, GraphKey, PrivateKey, ViewKey},
56    network::prelude::*,
57    program::{Ciphertext, Entry, Identifier, Literal, Plaintext, ProgramID, Record, StatePath, Value},
58    types::{Field, Group},
59};
60use snarkvm_ledger_authority::Authority;
61use snarkvm_ledger_committee::Committee;
62use snarkvm_ledger_narwhal::{BatchCertificate, Subdag, Transmission, TransmissionID};
63use snarkvm_ledger_puzzle::{Puzzle, PuzzleSolutions, Solution, SolutionID};
64use snarkvm_ledger_query::QueryTrait;
65use snarkvm_ledger_store::{ConsensusStorage, ConsensusStore};
66use snarkvm_synthesizer::{
67    program::{FinalizeGlobalState, Program},
68    vm::VM,
69};
70
71use aleo_std::{
72    StorageMode,
73    prelude::{finish, lap, timer},
74};
75use anyhow::{Context, Result};
76use core::ops::Range;
77use indexmap::IndexMap;
78#[cfg(feature = "locktick")]
79use locktick::parking_lot::{Mutex, RwLock};
80use lru::LruCache;
81#[cfg(not(feature = "locktick"))]
82use parking_lot::{Mutex, RwLock};
83use rand::{prelude::IteratorRandom, rngs::OsRng};
84use std::{borrow::Cow, collections::HashSet, sync::Arc};
85use time::OffsetDateTime;
86
87#[cfg(not(feature = "serial"))]
88use rayon::prelude::*;
89
90pub type RecordMap<N> = IndexMap<Field<N>, Record<N, Plaintext<N>>>;
91
92/// The capacity of the LRU cache holding the recently queried committees.
93const COMMITTEE_CACHE_SIZE: usize = 16;
94
95#[derive(Copy, Clone, Debug)]
96pub enum RecordsFilter<N: Network> {
97    /// Returns all records associated with the account.
98    All,
99    /// Returns only records associated with the account that are **spent** with the graph key.
100    Spent,
101    /// Returns only records associated with the account that are **not spent** with the graph key.
102    Unspent,
103    /// Returns all records associated with the account that are **spent** with the given private key.
104    SlowSpent(PrivateKey<N>),
105    /// Returns all records associated with the account that are **not spent** with the given private key.
106    SlowUnspent(PrivateKey<N>),
107}
108
109/// State of the entire chain.
110///
111/// All stored state is held in the `VM`, while Ledger holds the `VM` and relevant cache data.
112///
113/// The constructor is [`Ledger::load`],
114/// which loads the ledger from storage,
115/// or initializes it with the genesis block if the storage is empty
116#[derive(Clone)]
117pub struct Ledger<N: Network, C: ConsensusStorage<N>>(Arc<InnerLedger<N, C>>);
118
119impl<N: Network, C: ConsensusStorage<N>> Deref for Ledger<N, C> {
120    type Target = InnerLedger<N, C>;
121
122    fn deref(&self) -> &Self::Target {
123        &self.0
124    }
125}
126
127#[doc(hidden)]
128pub struct InnerLedger<N: Network, C: ConsensusStorage<N>> {
129    /// The VM state.
130    vm: VM<N, C>,
131    /// The genesis block.
132    genesis_block: Block<N>,
133    /// The current epoch hash.
134    current_epoch_hash: RwLock<Option<N::BlockHash>>,
135    /// The committee resulting from all the on-chain staking activity.
136    ///
137    /// This includes any bonding and unbonding transactions in the latest block.
138    /// The starting point, in the genesis block, is the genesis committee.
139    /// If the latest block has round `R`, `current_committee` is
140    /// the committee bonded for rounds `R+1`, `R+2`, and perhaps others
141    /// (unless a block at round `R+2` changes the committee).
142    /// Note that this committee is not active (i.e. in charge of running consensus)
143    /// until round `R + 1 + L`, where `L` is the lookback round distance.
144    ///
145    /// This committee is always well-defined
146    /// (in particular, it is the genesis committee when the `Ledger` is empty, or only has the genesis block).
147    /// So the `Option` should always be `Some`,
148    /// but there are cases in which it is `None`,
149    /// probably only temporarily when loading/initializing the ledger,
150    current_committee: RwLock<Option<Committee<N>>>,
151
152    /// The latest block that was added to the ledger.
153    ///
154    /// This lock is also used as a way to prevent concurrent updates to the ledger, and to ensure that
155    /// the ledger does not advance while certain check happen.
156    current_block: RwLock<Block<N>>,
157    /// The recent committees of interest paired with their applicable rounds.
158    ///
159    /// Each entry consisting of a round `R` and a committee `C`,
160    /// says that `C` is the bonded committee at round `R`,
161    /// i.e. resulting from all the bonding and unbonding transactions before `R`.
162    /// If `L` is the lookback round distance, `C` is the active committee at round `R + L`
163    /// (i.e. the committee in charge of running consensus at round `R + L`).
164    committee_cache: Mutex<LruCache<u64, Committee<N>>>,
165    /// The cache that holds the provers and the number of solutions they have submitted for the current epoch.
166    epoch_provers_cache: Arc<RwLock<IndexMap<Address<N>, u32>>>,
167}
168
169impl<N: Network, C: ConsensusStorage<N>> Ledger<N, C> {
170    /// Loads the ledger from storage.
171    pub fn load(genesis_block: Block<N>, storage_mode: StorageMode) -> Result<Self> {
172        let timer = timer!("Ledger::load");
173
174        // Retrieve the genesis hash.
175        let genesis_hash = genesis_block.hash();
176        // Initialize the ledger.
177        let ledger = Self::load_unchecked(genesis_block, storage_mode)?;
178
179        // Ensure the ledger contains the correct genesis block.
180        if !ledger.contains_block_hash(&genesis_hash)? {
181            bail!("Incorrect genesis block (run 'snarkos clean' and try again)")
182        }
183
184        // Spot check the integrity of `NUM_BLOCKS` random blocks upon bootup.
185        const NUM_BLOCKS: usize = 10;
186        // Retrieve the latest height.
187        let latest_height = ledger.current_block.read().height();
188        debug_assert_eq!(latest_height, ledger.vm.block_store().max_height().unwrap(), "Mismatch in latest height");
189        // Sample random block heights.
190        let block_heights: Vec<u32> =
191            (0..=latest_height).choose_multiple(&mut OsRng, (latest_height as usize).min(NUM_BLOCKS));
192        cfg_into_iter!(block_heights).try_for_each(|height| {
193            ledger.get_block(height)?;
194            Ok::<_, Error>(())
195        })?;
196        lap!(timer, "Check existence of {NUM_BLOCKS} random blocks");
197
198        finish!(timer);
199        Ok(ledger)
200    }
201
202    /// Loads the ledger from storage, without performing integrity checks.
203    pub fn load_unchecked(genesis_block: Block<N>, storage_mode: StorageMode) -> Result<Self> {
204        let timer = timer!("Ledger::load_unchecked");
205
206        info!("Loading the ledger from storage...");
207        // Initialize the consensus store.
208        let store = match ConsensusStore::<N, C>::open(storage_mode) {
209            Ok(store) => store,
210            Err(e) => bail!("Failed to load ledger (run 'snarkos clean' and try again)\n\n{e}\n"),
211        };
212        lap!(timer, "Load consensus store");
213
214        // Initialize a new VM.
215        let vm = VM::from(store)?;
216        lap!(timer, "Initialize a new VM");
217
218        // Retrieve the current committee.
219        let current_committee = vm.finalize_store().committee_store().current_committee().ok();
220
221        // Create a committee cache.
222        let committee_cache = Mutex::new(LruCache::new(COMMITTEE_CACHE_SIZE.try_into().unwrap()));
223
224        // Initialize the ledger.
225        let ledger = Self(Arc::new(InnerLedger {
226            vm,
227            genesis_block: genesis_block.clone(),
228            current_epoch_hash: Default::default(),
229            current_committee: RwLock::new(current_committee),
230            current_block: RwLock::new(genesis_block.clone()),
231            committee_cache,
232            epoch_provers_cache: Default::default(),
233        }));
234
235        // Attempt to obtain the maximum height from the storage.
236        let max_stored_height = ledger.vm.block_store().max_height();
237
238        // If the block store is empty, add the genesis block.
239        let latest_height = if let Some(max_height) = max_stored_height {
240            max_height
241        } else {
242            ledger.advance_to_next_block(&genesis_block)?;
243            0
244        };
245        lap!(timer, "Initialize genesis");
246
247        // Ensure that the greatest stored height matches that of the block tree.
248        ensure!(
249            latest_height == ledger.vm().block_store().current_block_height(),
250            "The stored height is different than the one in the block tree; \
251            please ensure that the cached block tree is valid or delete the \
252            'block_tree' file from the ledger folder"
253        );
254
255        // Verify that the root of the cached block tree matches the one in the storage.
256        let tree_root = <N::StateRoot>::from(ledger.vm().block_store().get_block_tree_root());
257        let state_root = ledger
258            .vm()
259            .block_store()
260            .get_state_root(latest_height)?
261            .ok_or_else(|| anyhow!("Missing state root in the storage"))?;
262        ensure!(
263            tree_root == state_root,
264            "The stored state root is different than the one in the block tree;
265            please ensure that the cached block tree is valid or delete the \
266            'block_tree' file from the ledger folder"
267        );
268
269        // Fetch the latest block.
270        let block = ledger
271            .get_block(latest_height)
272            .with_context(|| format!("Failed to load block {latest_height} from the ledger"))?;
273
274        // Set the current block.
275        *ledger.current_block.write() = block;
276        // Set the current committee (and ensures the latest committee exists).
277        *ledger.current_committee.write() = Some(ledger.latest_committee()?);
278        // Set the current epoch hash.
279        *ledger.current_epoch_hash.write() = Some(ledger.get_epoch_hash(latest_height)?);
280        // Set the epoch prover cache.
281        *ledger.epoch_provers_cache.write() = ledger.load_epoch_provers();
282
283        finish!(timer, "Initialize ledger");
284        Ok(ledger)
285    }
286}
287
288impl<N: Network, C: ConsensusStorage<N>> Ledger<N, C> {
289    /// Creates a rocksdb checkpoint in the specified directory, which needs to not exist at the
290    /// moment of calling. The checkpoints are based on hard links, which means they can both be
291    /// incremental (i.e. they aren't full physical copies), and used as full rollback points
292    /// (a checkpoint can be used to completely replace the original ledger).
293    #[cfg(feature = "rocks")]
294    pub fn backup_database<P: AsRef<std::path::Path>>(&self, path: P) -> Result<()> {
295        self.vm.block_store().backup_database(path).map_err(|err| anyhow!(err))
296    }
297
298    #[cfg(feature = "rocks")]
299    pub fn cache_block_tree(&self) -> Result<()> {
300        self.vm.block_store().cache_block_tree()
301    }
302
303    /// Loads the provers and the number of solutions they have submitted for the current epoch.
304    pub fn load_epoch_provers(&self) -> IndexMap<Address<N>, u32> {
305        // Fetch the current block height.
306        let current_block_height = self.vm().block_store().current_block_height();
307
308        // Determine the first block to start checking.
309        // Note that the epoch boundary (where current_block_height % N::NUM_BLOCKS_PER_EPOCH == 0) can contain solutions
310        // for the previous epoch X. The subsequent block is the first block to contain solutions for the current epoch X+1.
311        let next_block_height = current_block_height.saturating_add(1);
312        let start = next_block_height.saturating_sub(current_block_height % N::NUM_BLOCKS_PER_EPOCH);
313
314        // If the epoch contains no blocks that have solutions for the epoch.
315        if start > current_block_height {
316            return IndexMap::new();
317        }
318
319        // Collect the addresses of the solutions submitted in the current epoch.
320        let existing_epoch_blocks: Vec<_> = (start..=current_block_height).collect();
321        let solution_addresses = cfg_iter!(existing_epoch_blocks)
322            .flat_map(|height| match self.get_solutions(*height).as_deref() {
323                Ok(Some(solutions)) => solutions.iter().map(|(_, s)| s.address()).collect::<Vec<_>>(),
324                _ => vec![],
325            })
326            .collect::<Vec<_>>();
327
328        // Count the number of occurrences of each address in the epoch blocks.
329        let mut epoch_provers = IndexMap::new();
330        for address in solution_addresses {
331            epoch_provers.entry(address).and_modify(|e| *e += 1).or_insert(1);
332        }
333        epoch_provers
334    }
335
336    /// Returns the VM.
337    pub fn vm(&self) -> &VM<N, C> {
338        &self.vm
339    }
340
341    /// Returns the puzzle.
342    pub fn puzzle(&self) -> &Puzzle<N> {
343        self.vm.puzzle()
344    }
345
346    /// Returns the size of the block cache (or `None` if the block cache is not enabled).
347    pub fn block_cache_size(&self) -> Option<u32> {
348        self.vm.block_store().cache_size()
349    }
350
351    /// Returns the provers and the number of solutions they have submitted for the current epoch.
352    pub fn epoch_provers(&self) -> Arc<RwLock<IndexMap<Address<N>, u32>>> {
353        self.epoch_provers_cache.clone()
354    }
355
356    /// Returns the latest committee,
357    /// i.e. the committee resulting from all the on-chain staking activity.
358    pub fn latest_committee(&self) -> Result<Committee<N>> {
359        match self.current_committee.read().as_ref() {
360            Some(committee) => Ok(committee.clone()),
361            None => self.vm.finalize_store().committee_store().current_committee(),
362        }
363    }
364
365    /// Returns the latest state root.
366    pub fn latest_state_root(&self) -> N::StateRoot {
367        self.vm.block_store().current_state_root()
368    }
369
370    /// Returns the latest epoch number.
371    pub fn latest_epoch_number(&self) -> u32 {
372        self.current_block.read().height() / N::NUM_BLOCKS_PER_EPOCH
373    }
374
375    /// Returns the latest epoch hash.
376    pub fn latest_epoch_hash(&self) -> Result<N::BlockHash> {
377        match self.current_epoch_hash.read().as_ref() {
378            Some(epoch_hash) => Ok(*epoch_hash),
379            None => self.get_epoch_hash(self.latest_height()),
380        }
381    }
382
383    /// Returns the latest block.
384    pub fn latest_block(&self) -> Block<N> {
385        self.current_block.read().clone()
386    }
387
388    /// Returns the latest round number.
389    pub fn latest_round(&self) -> u64 {
390        self.current_block.read().round()
391    }
392
393    /// Returns the latest block height.
394    pub fn latest_height(&self) -> u32 {
395        self.current_block.read().height()
396    }
397
398    /// Returns the latest block hash.
399    pub fn latest_hash(&self) -> N::BlockHash {
400        self.current_block.read().hash()
401    }
402
403    /// Returns the latest block header.
404    pub fn latest_header(&self) -> Header<N> {
405        *self.current_block.read().header()
406    }
407
408    /// Returns the latest block cumulative weight.
409    pub fn latest_cumulative_weight(&self) -> u128 {
410        self.current_block.read().cumulative_weight()
411    }
412
413    /// Returns the latest block cumulative proof target.
414    pub fn latest_cumulative_proof_target(&self) -> u128 {
415        self.current_block.read().cumulative_proof_target()
416    }
417
418    /// Returns the latest block solutions root.
419    pub fn latest_solutions_root(&self) -> Field<N> {
420        self.current_block.read().header().solutions_root()
421    }
422
423    /// Returns the latest block coinbase target.
424    pub fn latest_coinbase_target(&self) -> u64 {
425        self.current_block.read().coinbase_target()
426    }
427
428    /// Returns the latest block proof target.
429    pub fn latest_proof_target(&self) -> u64 {
430        self.current_block.read().proof_target()
431    }
432
433    /// Returns the last coinbase target.
434    pub fn last_coinbase_target(&self) -> u64 {
435        self.current_block.read().last_coinbase_target()
436    }
437
438    /// Returns the last coinbase timestamp.
439    pub fn last_coinbase_timestamp(&self) -> i64 {
440        self.current_block.read().last_coinbase_timestamp()
441    }
442
443    /// Returns the latest block timestamp.
444    pub fn latest_timestamp(&self) -> i64 {
445        self.current_block.read().timestamp()
446    }
447
448    /// Returns the latest block transactions.
449    pub fn latest_transactions(&self) -> Transactions<N> {
450        self.current_block.read().transactions().clone()
451    }
452}
453
454impl<N: Network, C: ConsensusStorage<N>> Ledger<N, C> {
455    /// Returns the unspent `credits.aleo` records.
456    pub fn find_unspent_credits_records(&self, view_key: &ViewKey<N>) -> Result<RecordMap<N>> {
457        let microcredits = Identifier::from_str("microcredits")?;
458        Ok(self
459            .find_records(view_key, RecordsFilter::Unspent)?
460            .filter(|(_, record)| {
461                // TODO (raychu86): Find cleaner approach and check that the record is associated with the `credits.aleo` program
462                match record.data().get(&microcredits) {
463                    Some(Entry::Private(Plaintext::Literal(Literal::U64(amount), _))) => !amount.is_zero(),
464                    _ => false,
465                }
466            })
467            .collect::<IndexMap<_, _>>())
468    }
469
470    /// Creates a deploy transaction.
471    ///
472    /// The `priority_fee_in_microcredits` is an additional fee **on top** of the deployment fee.
473    pub fn create_deploy<R: Rng + CryptoRng>(
474        &self,
475        private_key: &PrivateKey<N>,
476        program: &Program<N>,
477        priority_fee_in_microcredits: u64,
478        query: Option<&dyn QueryTrait<N>>,
479        rng: &mut R,
480    ) -> Result<Transaction<N>> {
481        // Fetch the unspent records.
482        let records = self.find_unspent_credits_records(&ViewKey::try_from(private_key)?)?;
483        ensure!(!records.len().is_zero(), "The Aleo account has no records to spend.");
484        let mut records = records.values();
485
486        // Prepare the fee record.
487        let fee_record = Some(records.next().unwrap().clone());
488
489        // Create a new deploy transaction.
490        self.vm.deploy(private_key, program, fee_record, priority_fee_in_microcredits, query, rng)
491    }
492
493    /// Creates a transfer transaction.
494    ///
495    /// The `priority_fee_in_microcredits` is an additional fee **on top** of the execution fee.
496    pub fn create_transfer<R: Rng + CryptoRng>(
497        &self,
498        private_key: &PrivateKey<N>,
499        to: Address<N>,
500        amount_in_microcredits: u64,
501        priority_fee_in_microcredits: u64,
502        query: Option<&dyn QueryTrait<N>>,
503        rng: &mut R,
504    ) -> Result<Transaction<N>> {
505        // Fetch the unspent records.
506        let records = self.find_unspent_credits_records(&ViewKey::try_from(private_key)?)?;
507        ensure!(records.len() >= 2, "The Aleo account does not have enough records to spend.");
508        let mut records = records.values();
509
510        // Prepare the inputs.
511        let inputs = [
512            Value::Record(records.next().unwrap().clone()),
513            Value::from_str(&format!("{to}"))?,
514            Value::from_str(&format!("{amount_in_microcredits}u64"))?,
515        ];
516
517        // Prepare the fee.
518        let fee_record = Some(records.next().unwrap().clone());
519
520        // Create a new execute transaction.
521        self.vm.execute(
522            private_key,
523            ("credits.aleo", "transfer_private"),
524            inputs.iter(),
525            fee_record,
526            priority_fee_in_microcredits,
527            query,
528            rng,
529        )
530    }
531}
532
533#[cfg(feature = "rocks")]
534impl<N: Network, C: ConsensusStorage<N>> Drop for InnerLedger<N, C> {
535    fn drop(&mut self) {
536        // Cache the block tree in order to speed up the next startup; this operation
537        // is guaranteed to conclude as long as the destructors are allowed to run
538        // (a clean shutdown, panic = "unwind", an explicit call to `drop`, etc.).
539        // At the moment this code is executed, the Ledger is guaranteed to be owned
540        // exclusively by this method, so no other activity may interrupt it.
541        if let Err(e) = self.vm.block_store().cache_block_tree() {
542            error!("Couldn't cache the block tree: {e}");
543        }
544    }
545}
546
547pub mod prelude {
548    pub use crate::{Ledger, authority, block, block::*, committee, helpers::*, narwhal, puzzle, query, store};
549}