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 advance;
41mod check_next_block;
42mod check_transaction_basic;
43mod contains;
44mod find;
45mod get;
46mod is_solution_limit_reached;
47mod iterators;
48
49#[cfg(test)]
50mod tests;
51
52use console::{
53 account::{Address, GraphKey, PrivateKey, ViewKey},
54 network::prelude::*,
55 program::{Ciphertext, Entry, Identifier, Literal, Plaintext, ProgramID, Record, StatePath, Value},
56 types::{Field, Group},
57};
58use snarkvm_ledger_authority::Authority;
59use snarkvm_ledger_committee::Committee;
60use snarkvm_ledger_narwhal::{BatchCertificate, Subdag, Transmission, TransmissionID};
61use snarkvm_ledger_puzzle::{Puzzle, PuzzleSolutions, Solution, SolutionID};
62use snarkvm_ledger_query::QueryTrait;
63use snarkvm_ledger_store::{ConsensusStorage, ConsensusStore};
64use snarkvm_synthesizer::{
65 program::{FinalizeGlobalState, Program},
66 vm::VM,
67};
68
69use aleo_std::{
70 StorageMode,
71 prelude::{finish, lap, timer},
72};
73use anyhow::Result;
74use core::ops::Range;
75use indexmap::IndexMap;
76#[cfg(feature = "locktick")]
77use locktick::parking_lot::{Mutex, RwLock};
78use lru::LruCache;
79#[cfg(not(feature = "locktick"))]
80use parking_lot::{Mutex, RwLock};
81use rand::{prelude::IteratorRandom, rngs::OsRng};
82use std::{borrow::Cow, collections::HashSet, sync::Arc};
83use time::OffsetDateTime;
84
85#[cfg(not(feature = "serial"))]
86use rayon::prelude::*;
87
88pub type RecordMap<N> = IndexMap<Field<N>, Record<N, Plaintext<N>>>;
89
90const COMMITTEE_CACHE_SIZE: usize = 16;
92
93#[derive(Copy, Clone, Debug)]
94pub enum RecordsFilter<N: Network> {
95 All,
97 Spent,
99 Unspent,
101 SlowSpent(PrivateKey<N>),
103 SlowUnspent(PrivateKey<N>),
105}
106
107#[derive(Clone)]
115pub struct Ledger<N: Network, C: ConsensusStorage<N>> {
116 vm: VM<N, C>,
118 genesis_block: Block<N>,
120 current_epoch_hash: Arc<RwLock<Option<N::BlockHash>>>,
122 current_committee: Arc<RwLock<Option<Committee<N>>>>,
138 current_block: Arc<RwLock<Block<N>>>,
140 committee_cache: Arc<Mutex<LruCache<u64, Committee<N>>>>,
148 epoch_provers_cache: Arc<RwLock<IndexMap<Address<N>, u32>>>,
150}
151
152impl<N: Network, C: ConsensusStorage<N>> Ledger<N, C> {
153 pub fn load(genesis_block: Block<N>, storage_mode: StorageMode) -> Result<Self> {
155 let timer = timer!("Ledger::load");
156
157 let genesis_hash = genesis_block.hash();
159 let ledger = Self::load_unchecked(genesis_block, storage_mode)?;
161
162 if !ledger.contains_block_hash(&genesis_hash)? {
164 bail!("Incorrect genesis block (run 'snarkos clean' and try again)")
165 }
166
167 const NUM_BLOCKS: usize = 10;
169 let latest_height = ledger.current_block.read().height();
171 debug_assert_eq!(latest_height, ledger.vm.block_store().max_height().unwrap(), "Mismatch in latest height");
172 let block_heights: Vec<u32> =
174 (0..=latest_height).choose_multiple(&mut OsRng, (latest_height as usize).min(NUM_BLOCKS));
175 cfg_into_iter!(block_heights).try_for_each(|height| {
176 ledger.get_block(height)?;
177 Ok::<_, Error>(())
178 })?;
179 lap!(timer, "Check existence of {NUM_BLOCKS} random blocks");
180
181 finish!(timer);
182 Ok(ledger)
183 }
184
185 pub fn load_unchecked(genesis_block: Block<N>, storage_mode: StorageMode) -> Result<Self> {
187 let timer = timer!("Ledger::load_unchecked");
188
189 info!("Loading the ledger from storage...");
190 let store = match ConsensusStore::<N, C>::open(storage_mode) {
192 Ok(store) => store,
193 Err(e) => bail!("Failed to load ledger (run 'snarkos clean' and try again)\n\n{e}\n"),
194 };
195 lap!(timer, "Load consensus store");
196
197 let vm = VM::from(store)?;
199 lap!(timer, "Initialize a new VM");
200
201 let current_committee = vm.finalize_store().committee_store().current_committee().ok();
203
204 let committee_cache = Arc::new(Mutex::new(LruCache::new(COMMITTEE_CACHE_SIZE.try_into().unwrap())));
206
207 let mut ledger = Self {
209 vm,
210 genesis_block: genesis_block.clone(),
211 current_epoch_hash: Default::default(),
212 current_committee: Arc::new(RwLock::new(current_committee)),
213 current_block: Arc::new(RwLock::new(genesis_block.clone())),
214 committee_cache,
215 epoch_provers_cache: Default::default(),
216 };
217
218 if ledger.vm.block_store().max_height().is_none() {
220 ledger.advance_to_next_block(&genesis_block)?;
222 }
223 lap!(timer, "Initialize genesis");
224
225 let latest_height =
227 ledger.vm.block_store().max_height().ok_or_else(|| anyhow!("Failed to load blocks from the ledger"))?;
228 let block = ledger
230 .get_block(latest_height)
231 .map_err(|_| anyhow!("Failed to load block {latest_height} from the ledger"))?;
232
233 ledger.current_block = Arc::new(RwLock::new(block));
235 ledger.current_committee = Arc::new(RwLock::new(Some(ledger.latest_committee()?)));
237 ledger.current_epoch_hash = Arc::new(RwLock::new(Some(ledger.get_epoch_hash(latest_height)?)));
239 ledger.epoch_provers_cache = Arc::new(RwLock::new(ledger.load_epoch_provers()));
241
242 finish!(timer, "Initialize ledger");
243 Ok(ledger)
244 }
245
246 #[cfg(feature = "rocks")]
251 pub fn backup_database<P: AsRef<std::path::Path>>(&self, path: P) -> Result<()> {
252 self.vm.block_store().backup_database(path).map_err(|err| anyhow!(err))
253 }
254
255 pub fn load_epoch_provers(&self) -> IndexMap<Address<N>, u32> {
257 let current_block_height = self.vm().block_store().current_block_height();
259 let start_of_epoch = current_block_height.saturating_sub(current_block_height % N::NUM_BLOCKS_PER_EPOCH);
260 let existing_epoch_blocks: Vec<_> = (start_of_epoch..=current_block_height).collect();
261
262 let solution_addresses = cfg_iter!(existing_epoch_blocks)
264 .flat_map(|height| match self.get_solutions(*height).as_deref() {
265 Ok(Some(solutions)) => solutions.iter().map(|(_, s)| s.address()).collect::<Vec<_>>(),
266 _ => vec![],
267 })
268 .collect::<Vec<_>>();
269
270 let mut epoch_provers = IndexMap::new();
272 for address in solution_addresses {
273 epoch_provers.entry(address).and_modify(|e| *e += 1).or_insert(1);
274 }
275 epoch_provers
276 }
277
278 pub const fn vm(&self) -> &VM<N, C> {
280 &self.vm
281 }
282
283 pub const fn puzzle(&self) -> &Puzzle<N> {
285 self.vm.puzzle()
286 }
287
288 pub fn epoch_provers(&self) -> Arc<RwLock<IndexMap<Address<N>, u32>>> {
290 self.epoch_provers_cache.clone()
291 }
292
293 pub fn latest_committee(&self) -> Result<Committee<N>> {
296 match self.current_committee.read().as_ref() {
297 Some(committee) => Ok(committee.clone()),
298 None => self.vm.finalize_store().committee_store().current_committee(),
299 }
300 }
301
302 pub fn latest_state_root(&self) -> N::StateRoot {
304 self.vm.block_store().current_state_root()
305 }
306
307 pub fn latest_epoch_number(&self) -> u32 {
309 self.current_block.read().height() / N::NUM_BLOCKS_PER_EPOCH
310 }
311
312 pub fn latest_epoch_hash(&self) -> Result<N::BlockHash> {
314 match self.current_epoch_hash.read().as_ref() {
315 Some(epoch_hash) => Ok(*epoch_hash),
316 None => self.get_epoch_hash(self.latest_height()),
317 }
318 }
319
320 pub fn latest_block(&self) -> Block<N> {
322 self.current_block.read().clone()
323 }
324
325 pub fn latest_round(&self) -> u64 {
327 self.current_block.read().round()
328 }
329
330 pub fn latest_height(&self) -> u32 {
332 self.current_block.read().height()
333 }
334
335 pub fn latest_hash(&self) -> N::BlockHash {
337 self.current_block.read().hash()
338 }
339
340 pub fn latest_header(&self) -> Header<N> {
342 *self.current_block.read().header()
343 }
344
345 pub fn latest_cumulative_weight(&self) -> u128 {
347 self.current_block.read().cumulative_weight()
348 }
349
350 pub fn latest_cumulative_proof_target(&self) -> u128 {
352 self.current_block.read().cumulative_proof_target()
353 }
354
355 pub fn latest_solutions_root(&self) -> Field<N> {
357 self.current_block.read().header().solutions_root()
358 }
359
360 pub fn latest_coinbase_target(&self) -> u64 {
362 self.current_block.read().coinbase_target()
363 }
364
365 pub fn latest_proof_target(&self) -> u64 {
367 self.current_block.read().proof_target()
368 }
369
370 pub fn last_coinbase_target(&self) -> u64 {
372 self.current_block.read().last_coinbase_target()
373 }
374
375 pub fn last_coinbase_timestamp(&self) -> i64 {
377 self.current_block.read().last_coinbase_timestamp()
378 }
379
380 pub fn latest_timestamp(&self) -> i64 {
382 self.current_block.read().timestamp()
383 }
384
385 pub fn latest_transactions(&self) -> Transactions<N> {
387 self.current_block.read().transactions().clone()
388 }
389}
390
391impl<N: Network, C: ConsensusStorage<N>> Ledger<N, C> {
392 pub fn find_unspent_credits_records(&self, view_key: &ViewKey<N>) -> Result<RecordMap<N>> {
394 let microcredits = Identifier::from_str("microcredits")?;
395 Ok(self
396 .find_records(view_key, RecordsFilter::Unspent)?
397 .filter(|(_, record)| {
398 match record.data().get(µcredits) {
400 Some(Entry::Private(Plaintext::Literal(Literal::U64(amount), _))) => !amount.is_zero(),
401 _ => false,
402 }
403 })
404 .collect::<IndexMap<_, _>>())
405 }
406
407 pub fn create_deploy<R: Rng + CryptoRng>(
411 &self,
412 private_key: &PrivateKey<N>,
413 program: &Program<N>,
414 priority_fee_in_microcredits: u64,
415 query: Option<&dyn QueryTrait<N>>,
416 rng: &mut R,
417 ) -> Result<Transaction<N>> {
418 let records = self.find_unspent_credits_records(&ViewKey::try_from(private_key)?)?;
420 ensure!(!records.len().is_zero(), "The Aleo account has no records to spend.");
421 let mut records = records.values();
422
423 let fee_record = Some(records.next().unwrap().clone());
425
426 self.vm.deploy(private_key, program, fee_record, priority_fee_in_microcredits, query, rng)
428 }
429
430 pub fn create_transfer<R: Rng + CryptoRng>(
434 &self,
435 private_key: &PrivateKey<N>,
436 to: Address<N>,
437 amount_in_microcredits: u64,
438 priority_fee_in_microcredits: u64,
439 query: Option<&dyn QueryTrait<N>>,
440 rng: &mut R,
441 ) -> Result<Transaction<N>> {
442 let records = self.find_unspent_credits_records(&ViewKey::try_from(private_key)?)?;
444 ensure!(!records.len().is_zero(), "The Aleo account has no records to spend.");
445 let mut records = records.values();
446
447 let inputs = [
449 Value::Record(records.next().unwrap().clone()),
450 Value::from_str(&format!("{to}"))?,
451 Value::from_str(&format!("{amount_in_microcredits}u64"))?,
452 ];
453
454 let fee_record = Some(records.next().unwrap().clone());
456
457 self.vm.execute(
459 private_key,
460 ("credits.aleo", "transfer_private"),
461 inputs.iter(),
462 fee_record,
463 priority_fee_in_microcredits,
464 query,
465 rng,
466 )
467 }
468}
469
470#[cfg(test)]
471pub(crate) mod test_helpers {
472 use crate::Ledger;
473 use aleo_std::StorageMode;
474 use console::{
475 account::{Address, PrivateKey, ViewKey},
476 network::MainnetV0,
477 prelude::*,
478 };
479 use snarkvm_circuit::network::AleoV0;
480 use snarkvm_ledger_store::ConsensusStore;
481 use snarkvm_synthesizer::vm::VM;
482
483 pub(crate) type CurrentNetwork = MainnetV0;
484 pub(crate) type CurrentAleo = AleoV0;
485
486 #[cfg(not(feature = "rocks"))]
487 pub(crate) type CurrentLedger =
488 Ledger<CurrentNetwork, snarkvm_ledger_store::helpers::memory::ConsensusMemory<CurrentNetwork>>;
489 #[cfg(feature = "rocks")]
490 pub(crate) type CurrentLedger =
491 Ledger<CurrentNetwork, snarkvm_ledger_store::helpers::rocksdb::ConsensusDB<CurrentNetwork>>;
492
493 #[cfg(not(feature = "rocks"))]
494 pub(crate) type CurrentConsensusStore =
495 ConsensusStore<CurrentNetwork, snarkvm_ledger_store::helpers::memory::ConsensusMemory<CurrentNetwork>>;
496 #[cfg(feature = "rocks")]
497 pub(crate) type CurrentConsensusStore =
498 ConsensusStore<CurrentNetwork, snarkvm_ledger_store::helpers::rocksdb::ConsensusDB<CurrentNetwork>>;
499
500 #[cfg(not(feature = "rocks"))]
501 pub(crate) type CurrentConsensusStorage = snarkvm_ledger_store::helpers::memory::ConsensusMemory<CurrentNetwork>;
502 #[cfg(feature = "rocks")]
503 pub(crate) type CurrentConsensusStorage = snarkvm_ledger_store::helpers::rocksdb::ConsensusDB<CurrentNetwork>;
504
505 #[allow(dead_code)]
506 pub(crate) struct TestEnv {
507 pub ledger: CurrentLedger,
508 pub private_key: PrivateKey<CurrentNetwork>,
509 pub view_key: ViewKey<CurrentNetwork>,
510 pub address: Address<CurrentNetwork>,
511 }
512
513 pub(crate) fn sample_test_env(rng: &mut (impl Rng + CryptoRng)) -> TestEnv {
514 let private_key = PrivateKey::<CurrentNetwork>::new(rng).unwrap();
516 let view_key = ViewKey::try_from(&private_key).unwrap();
517 let address = Address::try_from(&private_key).unwrap();
518 let ledger = sample_ledger(private_key, rng);
520 TestEnv { ledger, private_key, view_key, address }
522 }
523
524 pub(crate) fn sample_ledger(
525 private_key: PrivateKey<CurrentNetwork>,
526 rng: &mut (impl Rng + CryptoRng),
527 ) -> CurrentLedger {
528 let store = CurrentConsensusStore::open(StorageMode::new_test(None)).unwrap();
530 let genesis = VM::from(store).unwrap().genesis_beacon(&private_key, rng).unwrap();
532 let ledger = CurrentLedger::load(genesis.clone(), StorageMode::new_test(None)).unwrap();
534 assert_eq!(genesis, ledger.get_block(0).unwrap());
536 ledger
538 }
539}