snarkvm_ledger_debug/
lib.rs

1// Copyright (C) 2019-2023 Aleo Systems 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// http://www.apache.org/licenses/LICENSE-2.0
8
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#![forbid(unsafe_code)]
16#![warn(clippy::cast_possible_truncation)]
17
18#[macro_use]
19extern crate tracing;
20
21pub use ledger_authority as authority;
22pub use ledger_block as block;
23pub use ledger_coinbase as coinbase;
24pub use ledger_committee as committee;
25pub use ledger_narwhal_debug as narwhal;
26pub use ledger_query as query;
27pub use ledger_store as store;
28
29pub use crate::block::*;
30
31#[cfg(feature = "test-helpers")]
32pub use ledger_test_helpers;
33
34mod helpers;
35pub use helpers::*;
36
37mod advance;
38mod check_next_block;
39mod check_transaction_basic;
40mod contains;
41mod find;
42mod get;
43mod iterators;
44
45#[cfg(test)]
46mod tests;
47
48use console::{
49    account::{Address, GraphKey, PrivateKey, ViewKey},
50    network::prelude::*,
51    program::{Ciphertext, Entry, Identifier, Literal, Plaintext, ProgramID, Record, StatePath, Value},
52    types::{Field, Group},
53};
54use ledger_authority::Authority;
55use ledger_block::{Block, ConfirmedTransaction, Header, Metadata, Ratify, Transaction, Transactions};
56use ledger_coinbase::{CoinbasePuzzle, CoinbaseSolution, EpochChallenge, ProverSolution, PuzzleCommitment};
57use ledger_committee::Committee;
58use ledger_narwhal_debug::{BatchCertificate, Subdag, Transmission, TransmissionID};
59use ledger_query::Query;
60use ledger_store::{ConsensusStorage, ConsensusStore};
61use synthesizer_debug::{
62    program::{FinalizeGlobalState, Program},
63    vm::VM,
64};
65
66use aleo_std::prelude::{finish, lap, timer};
67use anyhow::Result;
68use core::ops::Range;
69use indexmap::IndexMap;
70use parking_lot::RwLock;
71use rand::{prelude::IteratorRandom, rngs::OsRng};
72use std::{borrow::Cow, sync::Arc};
73use time::OffsetDateTime;
74
75#[cfg(not(feature = "serial"))]
76use rayon::prelude::*;
77
78pub type RecordMap<N> = IndexMap<Field<N>, Record<N, Plaintext<N>>>;
79
80#[derive(Copy, Clone, Debug)]
81pub enum RecordsFilter<N: Network> {
82    /// Returns all records associated with the account.
83    All,
84    /// Returns only records associated with the account that are **spent** with the graph key.
85    Spent,
86    /// Returns only records associated with the account that are **not spent** with the graph key.
87    Unspent,
88    /// Returns all records associated with the account that are **spent** with the given private key.
89    SlowSpent(PrivateKey<N>),
90    /// Returns all records associated with the account that are **not spent** with the given private key.
91    SlowUnspent(PrivateKey<N>),
92}
93
94#[derive(Clone)]
95pub struct Ledger<N: Network, C: ConsensusStorage<N>> {
96    /// The VM state.
97    vm: VM<N, C>,
98    /// The genesis block.
99    genesis_block: Block<N>,
100    /// The coinbase puzzle.
101    coinbase_puzzle: CoinbasePuzzle<N>,
102    /// The current epoch challenge.
103    current_epoch_challenge: Arc<RwLock<Option<EpochChallenge<N>>>>,
104    /// The current committee.
105    current_committee: Arc<RwLock<Option<Committee<N>>>>,
106    /// The current block.
107    current_block: Arc<RwLock<Block<N>>>,
108}
109
110impl<N: Network, C: ConsensusStorage<N>> Ledger<N, C> {
111    /// Loads the ledger from storage.
112    pub fn load(genesis_block: Block<N>, dev: Option<u16>) -> Result<Self> {
113        let timer = timer!("Ledger::load");
114
115        // Retrieve the genesis hash.
116        let genesis_hash = genesis_block.hash();
117        // Initialize the ledger.
118        let ledger = Self::load_unchecked(genesis_block, dev)?;
119
120        // Ensure the ledger contains the correct genesis block.
121        if !ledger.contains_block_hash(&genesis_hash)? {
122            bail!("Incorrect genesis block (run 'snarkos clean' and try again)")
123        }
124
125        // Spot check the integrity of `NUM_BLOCKS` random blocks upon bootup.
126        const NUM_BLOCKS: usize = 10;
127        // Retrieve the latest height.
128        let latest_height = ledger.current_block.read().height();
129        debug_assert_eq!(latest_height, *ledger.vm.block_store().heights().max().unwrap(), "Mismatch in latest height");
130        // Sample random block heights.
131        let block_heights: Vec<u32> =
132            (0..=latest_height).choose_multiple(&mut OsRng, (latest_height as usize).min(NUM_BLOCKS));
133        cfg_into_iter!(block_heights).try_for_each(|height| {
134            ledger.get_block(height)?;
135            Ok::<_, Error>(())
136        })?;
137        lap!(timer, "Check existence of {NUM_BLOCKS} random blocks");
138
139        finish!(timer);
140        Ok(ledger)
141    }
142
143    /// Loads the ledger from storage, without performing integrity checks.
144    pub fn load_unchecked(genesis_block: Block<N>, dev: Option<u16>) -> Result<Self> {
145        let timer = timer!("Ledger::load_unchecked");
146
147        info!("Loading the ledger from storage...");
148        // Initialize the consensus store.
149        let store = match ConsensusStore::<N, C>::open(dev) {
150            Ok(store) => store,
151            Err(e) => bail!("Failed to load ledger (run 'snarkos clean' and try again)\n\n{e}\n"),
152        };
153        lap!(timer, "Load consensus store");
154
155        // Initialize a new VM.
156        let vm = VM::from(store)?;
157        lap!(timer, "Initialize a new VM");
158
159        // Retrieve the current committee.
160        let current_committee = vm.finalize_store().committee_store().current_committee().ok();
161
162        // Initialize the ledger.
163        let mut ledger = Self {
164            vm,
165            genesis_block: genesis_block.clone(),
166            coinbase_puzzle: CoinbasePuzzle::<N>::load()?,
167            current_epoch_challenge: Default::default(),
168            current_committee: Arc::new(RwLock::new(current_committee)),
169            current_block: Arc::new(RwLock::new(genesis_block.clone())),
170        };
171
172        // If the block store is empty, initialize the genesis block.
173        if ledger.vm.block_store().heights().max().is_none() {
174            // Add the genesis block.
175            ledger.advance_to_next_block(&genesis_block)?;
176        }
177        lap!(timer, "Initialize genesis");
178
179        // Retrieve the latest height.
180        let latest_height =
181            *ledger.vm.block_store().heights().max().ok_or_else(|| anyhow!("Failed to load blocks from the ledger"))?;
182        // Fetch the latest block.
183        let block = ledger
184            .get_block(latest_height)
185            .map_err(|_| anyhow!("Failed to load block {latest_height} from the ledger"))?;
186
187        // Set the current block.
188        ledger.current_block = Arc::new(RwLock::new(block));
189        // Set the current committee (and ensures the latest committee exists).
190        ledger.current_committee = Arc::new(RwLock::new(Some(ledger.latest_committee()?)));
191        // Set the current epoch challenge.
192        ledger.current_epoch_challenge = Arc::new(RwLock::new(Some(ledger.get_epoch_challenge(latest_height)?)));
193
194        finish!(timer, "Initialize ledger");
195        Ok(ledger)
196    }
197
198    /// Returns the VM.
199    pub const fn vm(&self) -> &VM<N, C> {
200        &self.vm
201    }
202
203    /// Returns the coinbase puzzle.
204    pub const fn coinbase_puzzle(&self) -> &CoinbasePuzzle<N> {
205        &self.coinbase_puzzle
206    }
207
208    /// Returns the latest committee.
209    pub fn latest_committee(&self) -> Result<Committee<N>> {
210        match self.current_committee.read().as_ref() {
211            Some(committee) => Ok(committee.clone()),
212            None => self.vm.finalize_store().committee_store().current_committee(),
213        }
214    }
215
216    /// Returns the latest state root.
217    pub fn latest_state_root(&self) -> N::StateRoot {
218        self.vm.block_store().current_state_root()
219    }
220
221    /// Returns the latest epoch number.
222    pub fn latest_epoch_number(&self) -> u32 {
223        self.current_block.read().height() / N::NUM_BLOCKS_PER_EPOCH
224    }
225
226    /// Returns the latest epoch challenge.
227    pub fn latest_epoch_challenge(&self) -> Result<EpochChallenge<N>> {
228        match self.current_epoch_challenge.read().as_ref() {
229            Some(challenge) => Ok(challenge.clone()),
230            None => self.get_epoch_challenge(self.latest_height()),
231        }
232    }
233
234    /// Returns the latest block.
235    pub fn latest_block(&self) -> Block<N> {
236        self.current_block.read().clone()
237    }
238
239    /// Returns the latest round number.
240    pub fn latest_round(&self) -> u64 {
241        self.current_block.read().round()
242    }
243
244    /// Returns the latest block height.
245    pub fn latest_height(&self) -> u32 {
246        self.current_block.read().height()
247    }
248
249    /// Returns the latest block hash.
250    pub fn latest_hash(&self) -> N::BlockHash {
251        self.current_block.read().hash()
252    }
253
254    /// Returns the latest block header.
255    pub fn latest_header(&self) -> Header<N> {
256        *self.current_block.read().header()
257    }
258
259    /// Returns the latest block cumulative weight.
260    pub fn latest_cumulative_weight(&self) -> u128 {
261        self.current_block.read().cumulative_weight()
262    }
263
264    /// Returns the latest block cumulative proof target.
265    pub fn latest_cumulative_proof_target(&self) -> u128 {
266        self.current_block.read().cumulative_proof_target()
267    }
268
269    /// Returns the latest block solutions root.
270    pub fn latest_solutions_root(&self) -> Field<N> {
271        self.current_block.read().header().solutions_root()
272    }
273
274    /// Returns the latest block coinbase target.
275    pub fn latest_coinbase_target(&self) -> u64 {
276        self.current_block.read().coinbase_target()
277    }
278
279    /// Returns the latest block proof target.
280    pub fn latest_proof_target(&self) -> u64 {
281        self.current_block.read().proof_target()
282    }
283
284    /// Returns the last coinbase target.
285    pub fn last_coinbase_target(&self) -> u64 {
286        self.current_block.read().last_coinbase_target()
287    }
288
289    /// Returns the last coinbase timestamp.
290    pub fn last_coinbase_timestamp(&self) -> i64 {
291        self.current_block.read().last_coinbase_timestamp()
292    }
293
294    /// Returns the latest block timestamp.
295    pub fn latest_timestamp(&self) -> i64 {
296        self.current_block.read().timestamp()
297    }
298
299    /// Returns the latest block transactions.
300    pub fn latest_transactions(&self) -> Transactions<N> {
301        self.current_block.read().transactions().clone()
302    }
303}
304
305impl<N: Network, C: ConsensusStorage<N>> Ledger<N, C> {
306    /// Returns the unspent `credits.aleo` records.
307    pub fn find_unspent_credits_records(&self, view_key: &ViewKey<N>) -> Result<RecordMap<N>> {
308        let microcredits = Identifier::from_str("microcredits")?;
309        Ok(self
310            .find_records(view_key, RecordsFilter::Unspent)?
311            .filter(|(_, record)| {
312                // TODO (raychu86): Find cleaner approach and check that the record is associated with the `credits.aleo` program
313                match record.data().get(&microcredits) {
314                    Some(Entry::Private(Plaintext::Literal(Literal::U64(amount), _))) => !amount.is_zero(),
315                    _ => false,
316                }
317            })
318            .collect::<IndexMap<_, _>>())
319    }
320
321    /// Creates a deploy transaction.
322    ///
323    /// The `priority_fee_in_microcredits` is an additional fee **on top** of the deployment fee.
324    pub fn create_deploy<R: Rng + CryptoRng>(
325        &self,
326        private_key: &PrivateKey<N>,
327        program: &Program<N>,
328        priority_fee_in_microcredits: u64,
329        query: Option<Query<N, C::BlockStorage>>,
330        rng: &mut R,
331    ) -> Result<Transaction<N>> {
332        // Fetch the unspent records.
333        let records = self.find_unspent_credits_records(&ViewKey::try_from(private_key)?)?;
334        ensure!(!records.len().is_zero(), "The Aleo account has no records to spend.");
335        let mut records = records.values();
336
337        // Prepare the fee record.
338        let fee_record = Some(records.next().unwrap().clone());
339
340        // Create a new deploy transaction.
341        self.vm.deploy(private_key, program, fee_record, priority_fee_in_microcredits, query, rng)
342    }
343
344    /// Creates a transfer transaction.
345    ///
346    /// The `priority_fee_in_microcredits` is an additional fee **on top** of the execution fee.
347    pub fn create_transfer<R: Rng + CryptoRng>(
348        &self,
349        private_key: &PrivateKey<N>,
350        to: Address<N>,
351        amount_in_microcredits: u64,
352        priority_fee_in_microcredits: u64,
353        query: Option<Query<N, C::BlockStorage>>,
354        rng: &mut R,
355    ) -> Result<Transaction<N>> {
356        // Fetch the unspent records.
357        let records = self.find_unspent_credits_records(&ViewKey::try_from(private_key)?)?;
358        ensure!(!records.len().is_zero(), "The Aleo account has no records to spend.");
359        let mut records = records.values();
360
361        // Prepare the inputs.
362        let inputs = [
363            Value::Record(records.next().unwrap().clone()),
364            Value::from_str(&format!("{to}"))?,
365            Value::from_str(&format!("{amount_in_microcredits}u64"))?,
366        ];
367
368        // Prepare the fee.
369        let fee_record = Some(records.next().unwrap().clone());
370
371        // Create a new execute transaction.
372        self.vm.execute(
373            private_key,
374            ("credits.aleo", "transfer_private"),
375            inputs.iter(),
376            fee_record,
377            priority_fee_in_microcredits,
378            query,
379            rng,
380        )
381    }
382}
383
384#[cfg(test)]
385pub(crate) mod test_helpers {
386    use crate::Ledger;
387    use console::{
388        account::{Address, PrivateKey, ViewKey},
389        network::Testnet3,
390        prelude::*,
391    };
392    use ledger_block::Block;
393    use ledger_store::ConsensusStore;
394    use synthesizer::vm::VM;
395
396    pub(crate) type CurrentNetwork = Testnet3;
397
398    #[cfg(not(feature = "rocks"))]
399    pub(crate) type CurrentLedger =
400        Ledger<CurrentNetwork, ledger_store::helpers::memory::ConsensusMemory<CurrentNetwork>>;
401    #[cfg(feature = "rocks")]
402    pub(crate) type CurrentLedger = Ledger<CurrentNetwork, ledger_store::helpers::rocksdb::ConsensusDB<CurrentNetwork>>;
403
404    #[cfg(not(feature = "rocks"))]
405    pub(crate) type CurrentConsensusStore =
406        ConsensusStore<CurrentNetwork, ledger_store::helpers::memory::ConsensusMemory<CurrentNetwork>>;
407    #[cfg(feature = "rocks")]
408    pub(crate) type CurrentConsensusStore =
409        ConsensusStore<CurrentNetwork, ledger_store::helpers::rocksdb::ConsensusDB<CurrentNetwork>>;
410
411    #[allow(dead_code)]
412    pub(crate) struct TestEnv {
413        pub ledger: CurrentLedger,
414        pub private_key: PrivateKey<CurrentNetwork>,
415        pub view_key: ViewKey<CurrentNetwork>,
416        pub address: Address<CurrentNetwork>,
417    }
418
419    pub(crate) fn sample_test_env(rng: &mut (impl Rng + CryptoRng)) -> TestEnv {
420        // Sample the genesis private key.
421        let private_key = PrivateKey::<CurrentNetwork>::new(rng).unwrap();
422        let view_key = ViewKey::try_from(&private_key).unwrap();
423        let address = Address::try_from(&private_key).unwrap();
424        // Sample the ledger.
425        let ledger = sample_ledger(private_key, rng);
426        // Return the test environment.
427        TestEnv { ledger, private_key, view_key, address }
428    }
429
430    pub(crate) fn sample_genesis_block() -> Block<CurrentNetwork> {
431        Block::<CurrentNetwork>::from_bytes_le(CurrentNetwork::genesis_bytes()).unwrap()
432    }
433
434    pub(crate) fn sample_ledger(
435        private_key: PrivateKey<CurrentNetwork>,
436        rng: &mut (impl Rng + CryptoRng),
437    ) -> CurrentLedger {
438        // Initialize the store.
439        let store = CurrentConsensusStore::open(None).unwrap();
440        // Create a genesis block.
441        let genesis = VM::from(store).unwrap().genesis_beacon(&private_key, rng).unwrap();
442        // Initialize the ledger with the genesis block.
443        let ledger = CurrentLedger::load(genesis.clone(), None).unwrap();
444        // Ensure the genesis block is correct.
445        assert_eq!(genesis, ledger.get_block(0).unwrap());
446        // Return the ledger.
447        ledger
448    }
449}