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