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
32#[cfg(any(test, feature = "test-helpers"))]
33pub mod test_helpers;
34
35mod error;
36pub use error::*;
37
38mod helpers;
39pub use helpers::*;
40
41pub use crate::block::*;
42
43mod check_next_block;
44pub use check_next_block::{CheckBlockError, PendingBlock};
45
46mod advance;
47mod check_transaction_basic;
48mod contains;
49mod find;
50mod get;
51mod is_solution_limit_reached;
52mod iterators;
53
54#[cfg(test)]
55mod tests;
56
57use console::{
58 account::{Address, GraphKey, PrivateKey, ViewKey},
59 network::prelude::*,
60 program::{Ciphertext, Entry, Identifier, Literal, Plaintext, ProgramID, Record, StatePath, Value},
61 types::{Field, Group},
62};
63use snarkvm_ledger_authority::Authority;
64use snarkvm_ledger_committee::Committee;
65use snarkvm_ledger_narwhal::{BatchCertificate, Subdag, Transmission, TransmissionID};
66use snarkvm_ledger_puzzle::{Puzzle, PuzzleSolutions, Solution, SolutionID};
67use snarkvm_ledger_query::QueryTrait;
68use snarkvm_ledger_store::{ConsensusStorage, ConsensusStore};
69use snarkvm_synthesizer::{
70 program::{FinalizeGlobalState, Program},
71 vm::VM,
72};
73
74use aleo_std::{
75 StorageMode,
76 prelude::{finish, lap, timer},
77};
78use anyhow::{Context, Result};
79use cfg_if::cfg_if;
80use core::ops::Range;
81use indexmap::IndexMap;
82#[cfg(feature = "locktick")]
83use locktick::parking_lot::{Mutex, RwLock};
84use lru::LruCache;
85#[cfg(not(feature = "locktick"))]
86use parking_lot::{Mutex, RwLock};
87use rand::prelude::IteratorRandom;
88use std::{borrow::Cow, collections::HashSet, sync::Arc};
89use time::OffsetDateTime;
90
91#[cfg(not(feature = "serial"))]
92use rayon::prelude::*;
93
94pub type RecordMap<N> = IndexMap<Field<N>, Record<N, Plaintext<N>>>;
95
96const COMMITTEE_CACHE_SIZE: usize = 16;
98
99#[cfg(feature = "dev-committee")]
111#[derive(Clone, Copy, Debug)]
112pub struct DevCommitteeOptions {
113 pub start_round: Option<u64>,
116 pub dev_num_validators: u16,
118 pub seed: u64,
124}
125
126#[cfg(feature = "dev-committee")]
132pub fn build_dev_committee<N: Network>(start_round: u64, dev_num_validators: u16, seed: u64) -> Result<Committee<N>> {
133 use rand::SeedableRng;
134 let mut rng = rand_chacha::ChaChaRng::seed_from_u64(seed);
135 let dev_keys = (0..dev_num_validators).map(|_| PrivateKey::<N>::new(&mut rng)).collect::<Result<Vec<_>>>()?;
136 let members = dev_keys
137 .iter()
138 .map(Address::<N>::try_from)
139 .collect::<Result<Vec<_>>>()?
140 .into_iter()
141 .map(|address| (address, (snarkvm_ledger_committee::MIN_VALIDATOR_STAKE, true, 0)))
142 .collect::<IndexMap<_, _>>();
143 Committee::new(start_round, members)
144}
145
146#[derive(Copy, Clone, Debug)]
147pub enum RecordsFilter<N: Network> {
148 All,
150 Spent,
152 Unspent,
154 SlowSpent(PrivateKey<N>),
156 SlowUnspent(PrivateKey<N>),
158}
159
160#[derive(Clone)]
168pub struct Ledger<N: Network, C: ConsensusStorage<N>>(Arc<InnerLedger<N, C>>);
169
170impl<N: Network, C: ConsensusStorage<N>> Deref for Ledger<N, C> {
171 type Target = InnerLedger<N, C>;
172
173 fn deref(&self) -> &Self::Target {
174 &self.0
175 }
176}
177
178#[doc(hidden)]
179pub struct InnerLedger<N: Network, C: ConsensusStorage<N>> {
180 vm: VM<N, C>,
182 genesis_block: Block<N>,
184
185 current_epoch_hash: RwLock<Option<N::BlockHash>>,
187
188 current_committee: RwLock<Option<Committee<N>>>,
204
205 current_block: RwLock<Block<N>>,
210
211 committee_cache: Mutex<LruCache<u64, Committee<N>>>,
219 epoch_provers_cache: Arc<RwLock<IndexMap<Address<N>, u32>>>,
221
222 #[cfg(feature = "dev-committee")]
228 dev_committee: Option<Committee<N>>,
229}
230
231impl<N: Network, C: ConsensusStorage<N>> Ledger<N, C> {
232 pub fn load(genesis_block: Block<N>, storage_mode: StorageMode) -> Result<Self> {
234 let timer = timer!("Ledger::load");
235
236 let genesis_hash = genesis_block.hash();
238 let ledger = Self::load_unchecked(genesis_block, storage_mode)?;
240
241 if !ledger.contains_block_hash(&genesis_hash)? {
243 bail!("Incorrect genesis block (run 'snarkos clean' and try again)")
244 }
245
246 const NUM_BLOCKS: usize = 10;
248 let latest_height = ledger.current_block.read().height();
250 debug_assert_eq!(latest_height, ledger.vm.block_store().max_height().unwrap(), "Mismatch in latest height");
251 let block_heights: Vec<u32> =
253 (0..=latest_height).sample(&mut rand::rng(), (latest_height as usize).min(NUM_BLOCKS));
254 cfg_into_iter!(block_heights).try_for_each(|height| {
255 ledger.get_block(height)?;
256 Ok::<_, Error>(())
257 })?;
258 lap!(timer, "Check existence of {NUM_BLOCKS} random blocks");
259
260 finish!(timer);
261 Ok(ledger)
262 }
263
264 pub fn load_unchecked(genesis_block: Block<N>, storage_mode: StorageMode) -> Result<Self> {
266 cfg_if! {
267 if #[cfg(feature="dev-committee")] {
268 Self::load_unchecked_inner(genesis_block, storage_mode, None)
269 } else {
270 Self::load_unchecked_inner(genesis_block, storage_mode)
271 }
272 }
273 }
274
275 #[cfg(feature = "dev-committee")]
280 pub fn load_with_dev_committee(
281 genesis_block: Block<N>,
282 storage_mode: StorageMode,
283 dev_committee_opts: DevCommitteeOptions,
284 ) -> Result<Self> {
285 let timer = timer!("Ledger::load_with_dev_committee");
286
287 let genesis_hash = genesis_block.hash();
289 let ledger = Self::load_unchecked_with_dev_committee(genesis_block, storage_mode, dev_committee_opts)?;
291
292 if !ledger.contains_block_hash(&genesis_hash)? {
294 bail!("Incorrect genesis block (run 'snarkos clean' and try again)")
295 }
296
297 const NUM_BLOCKS: usize = 10;
299 let latest_height = ledger.current_block.read().height();
300 debug_assert_eq!(latest_height, ledger.vm.block_store().max_height().unwrap(), "Mismatch in latest height");
301 let block_heights: Vec<u32> =
302 (0..=latest_height).sample(&mut rand::rng(), (latest_height as usize).min(NUM_BLOCKS));
303 cfg_into_iter!(block_heights).try_for_each(|height| {
304 ledger.get_block(height)?;
305 Ok::<_, Error>(())
306 })?;
307 lap!(timer, "Check existence of {NUM_BLOCKS} random blocks");
308
309 finish!(timer);
310 Ok(ledger)
311 }
312
313 #[cfg(feature = "dev-committee")]
315 pub fn load_unchecked_with_dev_committee(
316 genesis_block: Block<N>,
317 storage_mode: StorageMode,
318 dev_committee_opts: DevCommitteeOptions,
319 ) -> Result<Self> {
320 Self::load_unchecked_inner(genesis_block, storage_mode, Some(dev_committee_opts))
321 }
322
323 fn load_unchecked_inner(
324 genesis_block: Block<N>,
325 storage_mode: StorageMode,
326 #[cfg(feature = "dev-committee")] dev_committee_opts: Option<DevCommitteeOptions>,
327 ) -> Result<Self> {
328 let timer = timer!("Ledger::load_unchecked");
329
330 info!("Loading the ledger from storage...");
331 let store = match ConsensusStore::<N, C>::open(storage_mode) {
333 Ok(store) => store,
334 Err(e) => bail!("Failed to load ledger (run 'snarkos clean' and try again)\n\n{e}\n"),
335 };
336 lap!(timer, "Load consensus store");
337
338 let vm = VM::from(store)?;
340 lap!(timer, "Initialize a new VM");
341
342 let current_committee = vm.finalize_store().committee_store().current_committee().ok();
344 let committee_cache = Mutex::new(LruCache::new(COMMITTEE_CACHE_SIZE.try_into().unwrap()));
346
347 #[cfg(feature = "dev-committee")]
352 let dev_committee = match dev_committee_opts {
353 Some(opts) => {
354 let start_round = if let Some(round) = opts.start_round {
355 round
356 } else {
357 match vm.block_store().max_height() {
358 Some(h) => {
359 let hash = vm
360 .block_store()
361 .get_block_hash(h)?
362 .ok_or_else(|| anyhow!("Missing block hash at height {h}"))?;
363 let block = vm
364 .block_store()
365 .get_block(&hash)?
366 .ok_or_else(|| anyhow!("Missing block at height {h}"))?;
367 block.round()
368 }
369 None => genesis_block.round(),
370 }
371 };
372
373 Some(build_dev_committee::<N>(start_round, opts.dev_num_validators, opts.seed)?)
374 }
375 None => None,
376 };
377
378 let ledger = Self(Arc::new(InnerLedger {
380 vm,
381 genesis_block: genesis_block.clone(),
382 current_epoch_hash: Default::default(),
383 current_committee: RwLock::new(current_committee),
384 current_block: RwLock::new(genesis_block.clone()),
385 committee_cache,
386 epoch_provers_cache: Default::default(),
387 #[cfg(feature = "dev-committee")]
388 dev_committee,
389 }));
390
391 let max_stored_height = ledger.vm.block_store().max_height();
393
394 let latest_height = if let Some(max_height) = max_stored_height {
396 max_height
397 } else {
398 ledger.advance_to_next_block(&genesis_block)?;
399 0
400 };
401 lap!(timer, "Initialize genesis");
402
403 let tree_derived_block_height = ledger.vm().block_store().current_block_height();
405 ensure!(
406 latest_height == tree_derived_block_height,
407 "The stored height ({latest_height}) is different than the one in \
408 the block tree ({tree_derived_block_height}); please ensure that \
409 the cached block tree is valid or delete the 'block_tree' file from \
410 the ledger folder"
411 );
412
413 let tree_root = <N::StateRoot>::from(ledger.vm().block_store().get_block_tree_root());
415 let state_root = ledger
416 .vm()
417 .block_store()
418 .get_state_root(latest_height)?
419 .ok_or_else(|| anyhow!("Missing state root in the storage"))?;
420 ensure!(
421 tree_root == state_root,
422 "The stored state root is different than the one in the block tree;
423 please ensure that the cached block tree is valid or delete the \
424 'block_tree' file from the ledger folder"
425 );
426
427 let block = ledger
429 .get_block(latest_height)
430 .with_context(|| format!("Failed to load block {latest_height} from the ledger"))?;
431
432 *ledger.current_block.write() = block;
434 *ledger.current_committee.write() = Some(ledger.latest_committee()?);
436 *ledger.current_epoch_hash.write() = Some(ledger.get_epoch_hash(latest_height)?);
438 *ledger.epoch_provers_cache.write() = ledger.load_epoch_provers();
440
441 finish!(timer, "Initialize ledger");
442 Ok(ledger)
443 }
444}
445
446impl<N: Network, C: ConsensusStorage<N>> Ledger<N, C> {
447 #[cfg(feature = "dev-committee")]
453 pub fn dev_committee(&self) -> Option<&Committee<N>> {
454 self.dev_committee.as_ref()
455 }
456
457 #[cfg(feature = "rocks")]
462 pub fn backup_database<P: AsRef<std::path::Path>>(&self, path: P) -> Result<()> {
463 self.vm.block_store().backup_database(path).map_err(|err| anyhow!(err))
464 }
465
466 #[cfg(feature = "rocks")]
467 pub fn cache_block_tree(&self) -> Result<()> {
468 self.vm.block_store().cache_block_tree()
469 }
470
471 pub fn load_epoch_provers(&self) -> IndexMap<Address<N>, u32> {
473 let current_block_height = self.vm().block_store().current_block_height();
475
476 let next_block_height = current_block_height.saturating_add(1);
480 let start = next_block_height.saturating_sub(current_block_height % N::NUM_BLOCKS_PER_EPOCH);
481
482 if start > current_block_height {
484 return IndexMap::new();
485 }
486
487 let existing_epoch_blocks: Vec<_> = (start..=current_block_height).collect();
489 let solution_addresses = cfg_iter!(existing_epoch_blocks)
490 .flat_map(|height| match self.get_solutions(*height).as_deref() {
491 Ok(Some(solutions)) => solutions.iter().map(|(_, s)| s.address()).collect::<Vec<_>>(),
492 _ => vec![],
493 })
494 .collect::<Vec<_>>();
495
496 let mut epoch_provers = IndexMap::new();
498 for address in solution_addresses {
499 epoch_provers.entry(address).and_modify(|e| *e += 1).or_insert(1);
500 }
501 epoch_provers
502 }
503
504 pub fn vm(&self) -> &VM<N, C> {
506 &self.vm
507 }
508
509 pub fn puzzle(&self) -> &Puzzle<N> {
511 self.vm.puzzle()
512 }
513
514 pub fn block_cache_size(&self) -> Option<u32> {
516 self.vm.block_store().cache_size()
517 }
518
519 pub fn epoch_provers(&self) -> Arc<RwLock<IndexMap<Address<N>, u32>>> {
521 self.epoch_provers_cache.clone()
522 }
523
524 pub fn latest_committee(&self) -> Result<Committee<N>> {
527 match self.current_committee.read().as_ref() {
528 Some(committee) => Ok(committee.clone()),
529 None => self.vm.finalize_store().committee_store().current_committee(),
530 }
531 }
532
533 pub fn latest_state_root(&self) -> N::StateRoot {
535 self.vm.block_store().current_state_root()
536 }
537
538 pub fn latest_epoch_number(&self) -> u32 {
540 self.current_block.read().height() / N::NUM_BLOCKS_PER_EPOCH
541 }
542
543 pub fn latest_epoch_hash(&self) -> Result<N::BlockHash> {
545 match self.current_epoch_hash.read().as_ref() {
546 Some(epoch_hash) => Ok(*epoch_hash),
547 None => self.get_epoch_hash(self.latest_height()),
548 }
549 }
550
551 pub fn latest_block(&self) -> Block<N> {
553 self.current_block.read().clone()
554 }
555
556 pub fn latest_round(&self) -> u64 {
558 self.current_block.read().round()
559 }
560
561 pub fn latest_height(&self) -> u32 {
563 self.current_block.read().height()
564 }
565
566 pub fn latest_hash(&self) -> N::BlockHash {
568 self.current_block.read().hash()
569 }
570
571 pub fn latest_header(&self) -> Header<N> {
573 *self.current_block.read().header()
574 }
575
576 pub fn latest_cumulative_weight(&self) -> u128 {
578 self.current_block.read().cumulative_weight()
579 }
580
581 pub fn latest_cumulative_proof_target(&self) -> u128 {
583 self.current_block.read().cumulative_proof_target()
584 }
585
586 pub fn latest_solutions_root(&self) -> Field<N> {
588 self.current_block.read().header().solutions_root()
589 }
590
591 pub fn latest_coinbase_target(&self) -> u64 {
593 self.current_block.read().coinbase_target()
594 }
595
596 pub fn latest_proof_target(&self) -> u64 {
598 self.current_block.read().proof_target()
599 }
600
601 pub fn last_coinbase_target(&self) -> u64 {
603 self.current_block.read().last_coinbase_target()
604 }
605
606 pub fn last_coinbase_timestamp(&self) -> i64 {
608 self.current_block.read().last_coinbase_timestamp()
609 }
610
611 pub fn latest_timestamp(&self) -> i64 {
613 self.current_block.read().timestamp()
614 }
615
616 pub fn latest_transactions(&self) -> Transactions<N> {
618 self.current_block.read().transactions().clone()
619 }
620}
621
622impl<N: Network, C: ConsensusStorage<N>> Ledger<N, C> {
623 pub fn find_unspent_credits_records(&self, view_key: &ViewKey<N>) -> Result<RecordMap<N>> {
625 let microcredits = Identifier::from_str("microcredits")?;
626 Ok(self
627 .find_records(view_key, RecordsFilter::Unspent)?
628 .filter(|(_, record)| {
629 match record.data().get(µcredits) {
631 Some(Entry::Private(Plaintext::Literal(Literal::U64(amount), _))) => !amount.is_zero(),
632 _ => false,
633 }
634 })
635 .collect::<IndexMap<_, _>>())
636 }
637
638 pub fn create_deploy<R: Rng + CryptoRng>(
642 &self,
643 private_key: &PrivateKey<N>,
644 program: &Program<N>,
645 priority_fee_in_microcredits: u64,
646 query: Option<&dyn QueryTrait<N>>,
647 rng: &mut R,
648 ) -> Result<Transaction<N>, CreateDeployError> {
649 let records = self.find_unspent_credits_records(&ViewKey::try_from(private_key)?)?;
651 if records.len().is_zero() {
652 return Err(anyhow!("The Aleo account has no records to spend.").into());
653 }
654 let mut records = records.values();
655
656 let fee_record = Some(records.next().unwrap().clone());
658
659 Ok(self.vm.deploy(private_key, program, fee_record, priority_fee_in_microcredits, query, rng)?)
661 }
662
663 pub fn create_transfer<R: Rng + CryptoRng>(
667 &self,
668 private_key: &PrivateKey<N>,
669 to: Address<N>,
670 amount_in_microcredits: u64,
671 priority_fee_in_microcredits: u64,
672 query: Option<&dyn QueryTrait<N>>,
673 rng: &mut R,
674 ) -> Result<Transaction<N>, CreateTransferError> {
675 let records = self.find_unspent_credits_records(&ViewKey::try_from(private_key)?)?;
677 if records.len() < 2 {
678 return Err(anyhow!("The Aleo account does not have enough records to spend.").into());
679 }
680 let mut records = records.values();
681
682 let inputs = [
684 Value::Record(records.next().unwrap().clone()),
685 Value::from_str(&format!("{to}"))?,
686 Value::from_str(&format!("{amount_in_microcredits}u64"))?,
687 ];
688
689 let fee_record = Some(records.next().unwrap().clone());
691
692 Ok(self.vm.execute(
694 private_key,
695 ("credits.aleo", "transfer_private"),
696 inputs.iter(),
697 fee_record,
698 priority_fee_in_microcredits,
699 query,
700 rng,
701 )?)
702 }
703}
704
705#[cfg(feature = "rocks")]
706impl<N: Network, C: ConsensusStorage<N>> Drop for InnerLedger<N, C> {
707 fn drop(&mut self) {
708 if let Err(e) = self.vm.block_store().cache_block_tree() {
714 error!("Couldn't cache the block tree: {e}");
715 }
716 }
717}
718
719pub mod prelude {
720 pub use crate::{Ledger, authority, block, block::*, committee, helpers::*, narwhal, puzzle, query, store};
721}