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