1#![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
92const COMMITTEE_CACHE_SIZE: usize = 16;
94
95#[derive(Copy, Clone, Debug)]
96pub enum RecordsFilter<N: Network> {
97 All,
99 Spent,
101 Unspent,
103 SlowSpent(PrivateKey<N>),
105 SlowUnspent(PrivateKey<N>),
107}
108
109#[derive(Clone)]
117pub struct Ledger<N: Network, C: ConsensusStorage<N>> {
118 vm: VM<N, C>,
120 genesis_block: Block<N>,
122 current_epoch_hash: Arc<RwLock<Option<N::BlockHash>>>,
124 current_committee: Arc<RwLock<Option<Committee<N>>>>,
140 current_block: Arc<RwLock<Block<N>>>,
142 committee_cache: Arc<Mutex<LruCache<u64, Committee<N>>>>,
150 epoch_provers_cache: Arc<RwLock<IndexMap<Address<N>, u32>>>,
152}
153
154impl<N: Network, C: ConsensusStorage<N>> Ledger<N, C> {
155 pub fn load(genesis_block: Block<N>, storage_mode: StorageMode) -> Result<Self> {
157 let timer = timer!("Ledger::load");
158
159 let genesis_hash = genesis_block.hash();
161 let ledger = Self::load_unchecked(genesis_block, storage_mode)?;
163
164 if !ledger.contains_block_hash(&genesis_hash)? {
166 bail!("Incorrect genesis block (run 'snarkos clean' and try again)")
167 }
168
169 const NUM_BLOCKS: usize = 10;
171 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 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 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 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 let vm = VM::from(store)?;
201 lap!(timer, "Initialize a new VM");
202
203 let current_committee = vm.finalize_store().committee_store().current_committee().ok();
205
206 let committee_cache = Arc::new(Mutex::new(LruCache::new(COMMITTEE_CACHE_SIZE.try_into().unwrap())));
208
209 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 ledger.vm.block_store().max_height().is_none() {
222 ledger.advance_to_next_block(&genesis_block)?;
224 }
225 lap!(timer, "Initialize genesis");
226
227 let latest_height =
229 ledger.vm.block_store().max_height().ok_or_else(|| anyhow!("Failed to load blocks from the ledger"))?;
230 let block = ledger
232 .get_block(latest_height)
233 .map_err(|_| anyhow!("Failed to load block {latest_height} from the ledger"))?;
234
235 ledger.current_block = Arc::new(RwLock::new(block));
237 ledger.current_committee = Arc::new(RwLock::new(Some(ledger.latest_committee()?)));
239 ledger.current_epoch_hash = Arc::new(RwLock::new(Some(ledger.get_epoch_hash(latest_height)?)));
241 ledger.epoch_provers_cache = Arc::new(RwLock::new(ledger.load_epoch_provers()));
243
244 finish!(timer, "Initialize ledger");
245 Ok(ledger)
246 }
247
248 #[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 pub fn load_epoch_provers(&self) -> IndexMap<Address<N>, u32> {
259 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 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 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 pub const fn vm(&self) -> &VM<N, C> {
282 &self.vm
283 }
284
285 pub const fn puzzle(&self) -> &Puzzle<N> {
287 self.vm.puzzle()
288 }
289
290 pub fn epoch_provers(&self) -> Arc<RwLock<IndexMap<Address<N>, u32>>> {
292 self.epoch_provers_cache.clone()
293 }
294
295 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 pub fn latest_state_root(&self) -> N::StateRoot {
306 self.vm.block_store().current_state_root()
307 }
308
309 pub fn latest_epoch_number(&self) -> u32 {
311 self.current_block.read().height() / N::NUM_BLOCKS_PER_EPOCH
312 }
313
314 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 pub fn latest_block(&self) -> Block<N> {
324 self.current_block.read().clone()
325 }
326
327 pub fn latest_round(&self) -> u64 {
329 self.current_block.read().round()
330 }
331
332 pub fn latest_height(&self) -> u32 {
334 self.current_block.read().height()
335 }
336
337 pub fn latest_hash(&self) -> N::BlockHash {
339 self.current_block.read().hash()
340 }
341
342 pub fn latest_header(&self) -> Header<N> {
344 *self.current_block.read().header()
345 }
346
347 pub fn latest_cumulative_weight(&self) -> u128 {
349 self.current_block.read().cumulative_weight()
350 }
351
352 pub fn latest_cumulative_proof_target(&self) -> u128 {
354 self.current_block.read().cumulative_proof_target()
355 }
356
357 pub fn latest_solutions_root(&self) -> Field<N> {
359 self.current_block.read().header().solutions_root()
360 }
361
362 pub fn latest_coinbase_target(&self) -> u64 {
364 self.current_block.read().coinbase_target()
365 }
366
367 pub fn latest_proof_target(&self) -> u64 {
369 self.current_block.read().proof_target()
370 }
371
372 pub fn last_coinbase_target(&self) -> u64 {
374 self.current_block.read().last_coinbase_target()
375 }
376
377 pub fn last_coinbase_timestamp(&self) -> i64 {
379 self.current_block.read().last_coinbase_timestamp()
380 }
381
382 pub fn latest_timestamp(&self) -> i64 {
384 self.current_block.read().timestamp()
385 }
386
387 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 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 match record.data().get(µcredits) {
402 Some(Entry::Private(Plaintext::Literal(Literal::U64(amount), _))) => !amount.is_zero(),
403 _ => false,
404 }
405 })
406 .collect::<IndexMap<_, _>>())
407 }
408
409 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 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 let fee_record = Some(records.next().unwrap().clone());
427
428 self.vm.deploy(private_key, program, fee_record, priority_fee_in_microcredits, query, rng)
430 }
431
432 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 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 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 let fee_record = Some(records.next().unwrap().clone());
458
459 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 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 let ledger = sample_ledger(private_key, rng);
522 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 let store = CurrentConsensusStore::open(StorageMode::new_test(None)).unwrap();
532 let genesis = VM::from(store).unwrap().genesis_beacon(&private_key, rng).unwrap();
534 let ledger = CurrentLedger::load(genesis.clone(), StorageMode::new_test(None)).unwrap();
536 assert_eq!(genesis, ledger.get_block(0).unwrap());
538 ledger
540 }
541}