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
32pub use crate::block::*;
33
34#[cfg(feature = "test-helpers")]
35pub use snarkvm_ledger_test_helpers;
36
37mod helpers;
38pub use helpers::*;
39
40mod check_next_block;
41pub use check_next_block::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::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>> {
118    /// The VM state.
119    vm: VM<N, C>,
120    /// The genesis block.
121    genesis_block: Block<N>,
122    /// The current epoch hash.
123    current_epoch_hash: Arc<RwLock<Option<N::BlockHash>>>,
124    /// The committee resulting from all the on-chain staking activity.
125    ///
126    /// This includes any bonding and unbonding transactions in the latest block.
127    /// The starting point, in the genesis block, is the genesis committee.
128    /// If the latest block has round `R`, `current_committee` is
129    /// the committee bonded for rounds `R+1`, `R+2`, and perhaps others
130    /// (unless a block at round `R+2` changes the committee).
131    /// Note that this committee is not active (i.e. in charge of running consensus)
132    /// until round `R + 1 + L`, where `L` is the lookback round distance.
133    ///
134    /// This committee is always well-defined
135    /// (in particular, it is the genesis committee when the `Ledger` is empty, or only has the genesis block).
136    /// So the `Option` should always be `Some`,
137    /// but there are cases in which it is `None`,
138    /// probably only temporarily when loading/initializing the ledger,
139    current_committee: Arc<RwLock<Option<Committee<N>>>>,
140    /// The latest block.
141    current_block: Arc<RwLock<Block<N>>>,
142    /// The recent committees of interest paired with their applicable rounds.
143    ///
144    /// Each entry consisting of a round `R` and a committee `C`,
145    /// says that `C` is the bonded committee at round `R`,
146    /// i.e. resulting from all the bonding and unbonding transactions before `R`.
147    /// If `L` is the lookback round distance, `C` is the active committee at round `R + L`
148    /// (i.e. the committee in charge of running consensus at round `R + L`).
149    committee_cache: Arc<Mutex<LruCache<u64, Committee<N>>>>,
150    /// The cache that holds the provers and the number of solutions they have submitted for the current epoch.
151    epoch_provers_cache: Arc<RwLock<IndexMap<Address<N>, u32>>>,
152}
153
154impl<N: Network, C: ConsensusStorage<N>> Ledger<N, C> {
155    /// Loads the ledger from storage.
156    pub fn load(genesis_block: Block<N>, storage_mode: StorageMode) -> Result<Self> {
157        let timer = timer!("Ledger::load");
158
159        // Retrieve the genesis hash.
160        let genesis_hash = genesis_block.hash();
161        // Initialize the ledger.
162        let ledger = Self::load_unchecked(genesis_block, storage_mode)?;
163
164        // Ensure the ledger contains the correct genesis block.
165        if !ledger.contains_block_hash(&genesis_hash)? {
166            bail!("Incorrect genesis block (run 'snarkos clean' and try again)")
167        }
168
169        // Spot check the integrity of `NUM_BLOCKS` random blocks upon bootup.
170        const NUM_BLOCKS: usize = 10;
171        // Retrieve the latest height.
172        let latest_height = ledger.current_block.read().height();
173        debug_assert_eq!(latest_height, ledger.vm.block_store().max_height().unwrap(), "Mismatch in latest height");
174        // Sample random block heights.
175        let block_heights: Vec<u32> =
176            (0..=latest_height).choose_multiple(&mut OsRng, (latest_height as usize).min(NUM_BLOCKS));
177        cfg_into_iter!(block_heights).try_for_each(|height| {
178            ledger.get_block(height)?;
179            Ok::<_, Error>(())
180        })?;
181        lap!(timer, "Check existence of {NUM_BLOCKS} random blocks");
182
183        finish!(timer);
184        Ok(ledger)
185    }
186
187    /// Loads the ledger from storage, without performing integrity checks.
188    pub fn load_unchecked(genesis_block: Block<N>, storage_mode: StorageMode) -> Result<Self> {
189        let timer = timer!("Ledger::load_unchecked");
190
191        info!("Loading the ledger from storage...");
192        // Initialize the consensus store.
193        let store = match ConsensusStore::<N, C>::open(storage_mode) {
194            Ok(store) => store,
195            Err(e) => bail!("Failed to load ledger (run 'snarkos clean' and try again)\n\n{e}\n"),
196        };
197        lap!(timer, "Load consensus store");
198
199        // Initialize a new VM.
200        let vm = VM::from(store)?;
201        lap!(timer, "Initialize a new VM");
202
203        // Retrieve the current committee.
204        let current_committee = vm.finalize_store().committee_store().current_committee().ok();
205
206        // Create a committee cache.
207        let committee_cache = Arc::new(Mutex::new(LruCache::new(COMMITTEE_CACHE_SIZE.try_into().unwrap())));
208
209        // Initialize the ledger.
210        let mut ledger = Self {
211            vm,
212            genesis_block: genesis_block.clone(),
213            current_epoch_hash: Default::default(),
214            current_committee: Arc::new(RwLock::new(current_committee)),
215            current_block: Arc::new(RwLock::new(genesis_block.clone())),
216            committee_cache,
217            epoch_provers_cache: Default::default(),
218        };
219
220        // If the block store is empty, add the genesis block.
221        if ledger.vm.block_store().max_height().is_none() {
222            // Add the genesis block.
223            ledger.advance_to_next_block(&genesis_block)?;
224        }
225        lap!(timer, "Initialize genesis");
226
227        // Retrieve the latest height.
228        let latest_height =
229            ledger.vm.block_store().max_height().ok_or_else(|| anyhow!("Failed to load blocks from the ledger"))?;
230        // Fetch the latest block.
231        let block = ledger
232            .get_block(latest_height)
233            .map_err(|_| anyhow!("Failed to load block {latest_height} from the ledger"))?;
234
235        // Set the current block.
236        ledger.current_block = Arc::new(RwLock::new(block));
237        // Set the current committee (and ensures the latest committee exists).
238        ledger.current_committee = Arc::new(RwLock::new(Some(ledger.latest_committee()?)));
239        // Set the current epoch hash.
240        ledger.current_epoch_hash = Arc::new(RwLock::new(Some(ledger.get_epoch_hash(latest_height)?)));
241        // Set the epoch prover cache.
242        ledger.epoch_provers_cache = Arc::new(RwLock::new(ledger.load_epoch_provers()));
243
244        finish!(timer, "Initialize ledger");
245        Ok(ledger)
246    }
247
248    /// Creates a rocksdb checkpoint in the specified directory, which needs to not exist at the
249    /// moment of calling. The checkpoints are based on hard links, which means they can both be
250    /// incremental (i.e. they aren't full physical copies), and used as full rollback points
251    /// (a checkpoint can be used to completely replace the original ledger).
252    #[cfg(feature = "rocks")]
253    pub fn backup_database<P: AsRef<std::path::Path>>(&self, path: P) -> Result<()> {
254        self.vm.block_store().backup_database(path).map_err(|err| anyhow!(err))
255    }
256
257    /// Loads the provers and the number of solutions they have submitted for the current epoch.
258    pub fn load_epoch_provers(&self) -> IndexMap<Address<N>, u32> {
259        // Fetch the block heights that belong to the current epoch.
260        let current_block_height = self.vm().block_store().current_block_height();
261        let start_of_epoch = current_block_height.saturating_sub(current_block_height % N::NUM_BLOCKS_PER_EPOCH);
262        let existing_epoch_blocks: Vec<_> = (start_of_epoch..=current_block_height).collect();
263
264        // Collect the addresses of the solutions submitted in the current epoch.
265        let solution_addresses = cfg_iter!(existing_epoch_blocks)
266            .flat_map(|height| match self.get_solutions(*height).as_deref() {
267                Ok(Some(solutions)) => solutions.iter().map(|(_, s)| s.address()).collect::<Vec<_>>(),
268                _ => vec![],
269            })
270            .collect::<Vec<_>>();
271
272        // Count the number of occurrences of each address in the epoch blocks.
273        let mut epoch_provers = IndexMap::new();
274        for address in solution_addresses {
275            epoch_provers.entry(address).and_modify(|e| *e += 1).or_insert(1);
276        }
277        epoch_provers
278    }
279
280    /// Returns the VM.
281    pub const fn vm(&self) -> &VM<N, C> {
282        &self.vm
283    }
284
285    /// Returns the puzzle.
286    pub const fn puzzle(&self) -> &Puzzle<N> {
287        self.vm.puzzle()
288    }
289
290    /// Returns the provers and the number of solutions they have submitted for the current epoch.
291    pub fn epoch_provers(&self) -> Arc<RwLock<IndexMap<Address<N>, u32>>> {
292        self.epoch_provers_cache.clone()
293    }
294
295    /// Returns the latest committee,
296    /// i.e. the committee resulting from all the on-chain staking activity.
297    pub fn latest_committee(&self) -> Result<Committee<N>> {
298        match self.current_committee.read().as_ref() {
299            Some(committee) => Ok(committee.clone()),
300            None => self.vm.finalize_store().committee_store().current_committee(),
301        }
302    }
303
304    /// Returns the latest state root.
305    pub fn latest_state_root(&self) -> N::StateRoot {
306        self.vm.block_store().current_state_root()
307    }
308
309    /// Returns the latest epoch number.
310    pub fn latest_epoch_number(&self) -> u32 {
311        self.current_block.read().height() / N::NUM_BLOCKS_PER_EPOCH
312    }
313
314    /// Returns the latest epoch hash.
315    pub fn latest_epoch_hash(&self) -> Result<N::BlockHash> {
316        match self.current_epoch_hash.read().as_ref() {
317            Some(epoch_hash) => Ok(*epoch_hash),
318            None => self.get_epoch_hash(self.latest_height()),
319        }
320    }
321
322    /// Returns the latest block.
323    pub fn latest_block(&self) -> Block<N> {
324        self.current_block.read().clone()
325    }
326
327    /// Returns the latest round number.
328    pub fn latest_round(&self) -> u64 {
329        self.current_block.read().round()
330    }
331
332    /// Returns the latest block height.
333    pub fn latest_height(&self) -> u32 {
334        self.current_block.read().height()
335    }
336
337    /// Returns the latest block hash.
338    pub fn latest_hash(&self) -> N::BlockHash {
339        self.current_block.read().hash()
340    }
341
342    /// Returns the latest block header.
343    pub fn latest_header(&self) -> Header<N> {
344        *self.current_block.read().header()
345    }
346
347    /// Returns the latest block cumulative weight.
348    pub fn latest_cumulative_weight(&self) -> u128 {
349        self.current_block.read().cumulative_weight()
350    }
351
352    /// Returns the latest block cumulative proof target.
353    pub fn latest_cumulative_proof_target(&self) -> u128 {
354        self.current_block.read().cumulative_proof_target()
355    }
356
357    /// Returns the latest block solutions root.
358    pub fn latest_solutions_root(&self) -> Field<N> {
359        self.current_block.read().header().solutions_root()
360    }
361
362    /// Returns the latest block coinbase target.
363    pub fn latest_coinbase_target(&self) -> u64 {
364        self.current_block.read().coinbase_target()
365    }
366
367    /// Returns the latest block proof target.
368    pub fn latest_proof_target(&self) -> u64 {
369        self.current_block.read().proof_target()
370    }
371
372    /// Returns the last coinbase target.
373    pub fn last_coinbase_target(&self) -> u64 {
374        self.current_block.read().last_coinbase_target()
375    }
376
377    /// Returns the last coinbase timestamp.
378    pub fn last_coinbase_timestamp(&self) -> i64 {
379        self.current_block.read().last_coinbase_timestamp()
380    }
381
382    /// Returns the latest block timestamp.
383    pub fn latest_timestamp(&self) -> i64 {
384        self.current_block.read().timestamp()
385    }
386
387    /// Returns the latest block transactions.
388    pub fn latest_transactions(&self) -> Transactions<N> {
389        self.current_block.read().transactions().clone()
390    }
391}
392
393impl<N: Network, C: ConsensusStorage<N>> Ledger<N, C> {
394    /// Returns the unspent `credits.aleo` records.
395    pub fn find_unspent_credits_records(&self, view_key: &ViewKey<N>) -> Result<RecordMap<N>> {
396        let microcredits = Identifier::from_str("microcredits")?;
397        Ok(self
398            .find_records(view_key, RecordsFilter::Unspent)?
399            .filter(|(_, record)| {
400                // TODO (raychu86): Find cleaner approach and check that the record is associated with the `credits.aleo` program
401                match record.data().get(&microcredits) {
402                    Some(Entry::Private(Plaintext::Literal(Literal::U64(amount), _))) => !amount.is_zero(),
403                    _ => false,
404                }
405            })
406            .collect::<IndexMap<_, _>>())
407    }
408
409    /// Creates a deploy transaction.
410    ///
411    /// The `priority_fee_in_microcredits` is an additional fee **on top** of the deployment fee.
412    pub fn create_deploy<R: Rng + CryptoRng>(
413        &self,
414        private_key: &PrivateKey<N>,
415        program: &Program<N>,
416        priority_fee_in_microcredits: u64,
417        query: Option<&dyn QueryTrait<N>>,
418        rng: &mut R,
419    ) -> Result<Transaction<N>> {
420        // Fetch the unspent records.
421        let records = self.find_unspent_credits_records(&ViewKey::try_from(private_key)?)?;
422        ensure!(!records.len().is_zero(), "The Aleo account has no records to spend.");
423        let mut records = records.values();
424
425        // Prepare the fee record.
426        let fee_record = Some(records.next().unwrap().clone());
427
428        // Create a new deploy transaction.
429        self.vm.deploy(private_key, program, fee_record, priority_fee_in_microcredits, query, rng)
430    }
431
432    /// Creates a transfer transaction.
433    ///
434    /// The `priority_fee_in_microcredits` is an additional fee **on top** of the execution fee.
435    pub fn create_transfer<R: Rng + CryptoRng>(
436        &self,
437        private_key: &PrivateKey<N>,
438        to: Address<N>,
439        amount_in_microcredits: u64,
440        priority_fee_in_microcredits: u64,
441        query: Option<&dyn QueryTrait<N>>,
442        rng: &mut R,
443    ) -> Result<Transaction<N>> {
444        // Fetch the unspent records.
445        let records = self.find_unspent_credits_records(&ViewKey::try_from(private_key)?)?;
446        ensure!(!records.len().is_zero(), "The Aleo account has no records to spend.");
447        let mut records = records.values();
448
449        // Prepare the inputs.
450        let inputs = [
451            Value::Record(records.next().unwrap().clone()),
452            Value::from_str(&format!("{to}"))?,
453            Value::from_str(&format!("{amount_in_microcredits}u64"))?,
454        ];
455
456        // Prepare the fee.
457        let fee_record = Some(records.next().unwrap().clone());
458
459        // Create a new execute transaction.
460        self.vm.execute(
461            private_key,
462            ("credits.aleo", "transfer_private"),
463            inputs.iter(),
464            fee_record,
465            priority_fee_in_microcredits,
466            query,
467            rng,
468        )
469    }
470}
471
472#[cfg(test)]
473pub(crate) mod test_helpers {
474    use crate::Ledger;
475    use aleo_std::StorageMode;
476    use console::{
477        account::{Address, PrivateKey, ViewKey},
478        network::MainnetV0,
479        prelude::*,
480    };
481    use snarkvm_circuit::network::AleoV0;
482    use snarkvm_ledger_store::ConsensusStore;
483    use snarkvm_synthesizer::vm::VM;
484
485    pub(crate) type CurrentNetwork = MainnetV0;
486    pub(crate) type CurrentAleo = AleoV0;
487
488    #[cfg(not(feature = "rocks"))]
489    pub(crate) type CurrentLedger =
490        Ledger<CurrentNetwork, snarkvm_ledger_store::helpers::memory::ConsensusMemory<CurrentNetwork>>;
491    #[cfg(feature = "rocks")]
492    pub(crate) type CurrentLedger =
493        Ledger<CurrentNetwork, snarkvm_ledger_store::helpers::rocksdb::ConsensusDB<CurrentNetwork>>;
494
495    #[cfg(not(feature = "rocks"))]
496    pub(crate) type CurrentConsensusStore =
497        ConsensusStore<CurrentNetwork, snarkvm_ledger_store::helpers::memory::ConsensusMemory<CurrentNetwork>>;
498    #[cfg(feature = "rocks")]
499    pub(crate) type CurrentConsensusStore =
500        ConsensusStore<CurrentNetwork, snarkvm_ledger_store::helpers::rocksdb::ConsensusDB<CurrentNetwork>>;
501
502    #[cfg(not(feature = "rocks"))]
503    pub(crate) type CurrentConsensusStorage = snarkvm_ledger_store::helpers::memory::ConsensusMemory<CurrentNetwork>;
504    #[cfg(feature = "rocks")]
505    pub(crate) type CurrentConsensusStorage = snarkvm_ledger_store::helpers::rocksdb::ConsensusDB<CurrentNetwork>;
506
507    #[allow(dead_code)]
508    pub(crate) struct TestEnv {
509        pub ledger: CurrentLedger,
510        pub private_key: PrivateKey<CurrentNetwork>,
511        pub view_key: ViewKey<CurrentNetwork>,
512        pub address: Address<CurrentNetwork>,
513    }
514
515    pub(crate) fn sample_test_env(rng: &mut (impl Rng + CryptoRng)) -> TestEnv {
516        // Sample the genesis private key.
517        let private_key = PrivateKey::<CurrentNetwork>::new(rng).unwrap();
518        let view_key = ViewKey::try_from(&private_key).unwrap();
519        let address = Address::try_from(&private_key).unwrap();
520        // Sample the ledger.
521        let ledger = sample_ledger(private_key, rng);
522        // Return the test environment.
523        TestEnv { ledger, private_key, view_key, address }
524    }
525
526    pub(crate) fn sample_ledger(
527        private_key: PrivateKey<CurrentNetwork>,
528        rng: &mut (impl Rng + CryptoRng),
529    ) -> CurrentLedger {
530        // Initialize the store.
531        let store = CurrentConsensusStore::open(StorageMode::new_test(None)).unwrap();
532        // Create a genesis block.
533        let genesis = VM::from(store).unwrap().genesis_beacon(&private_key, rng).unwrap();
534        // Initialize the ledger with the genesis block.
535        let ledger = CurrentLedger::load(genesis.clone(), StorageMode::new_test(None)).unwrap();
536        // Ensure the genesis block is correct.
537        assert_eq!(genesis, ledger.get_block(0).unwrap());
538        // Return the ledger.
539        ledger
540    }
541}