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