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 helpers;
36pub use helpers::*;
37
38pub use crate::block::*;
39
40mod check_next_block;
41pub use check_next_block::{CheckBlockError, 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::{Context, 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>>(Arc<InnerLedger<N, C>>);
118
119impl<N: Network, C: ConsensusStorage<N>> Deref for Ledger<N, C> {
120 type Target = InnerLedger<N, C>;
121
122 fn deref(&self) -> &Self::Target {
123 &self.0
124 }
125}
126
127#[doc(hidden)]
128pub struct InnerLedger<N: Network, C: ConsensusStorage<N>> {
129 vm: VM<N, C>,
131 genesis_block: Block<N>,
133 current_epoch_hash: RwLock<Option<N::BlockHash>>,
135 current_committee: RwLock<Option<Committee<N>>>,
151
152 current_block: RwLock<Block<N>>,
157 committee_cache: Mutex<LruCache<u64, Committee<N>>>,
165 epoch_provers_cache: Arc<RwLock<IndexMap<Address<N>, u32>>>,
167}
168
169impl<N: Network, C: ConsensusStorage<N>> Ledger<N, C> {
170 pub fn load(genesis_block: Block<N>, storage_mode: StorageMode) -> Result<Self> {
172 let timer = timer!("Ledger::load");
173
174 let genesis_hash = genesis_block.hash();
176 let ledger = Self::load_unchecked(genesis_block, storage_mode)?;
178
179 if !ledger.contains_block_hash(&genesis_hash)? {
181 bail!("Incorrect genesis block (run 'snarkos clean' and try again)")
182 }
183
184 const NUM_BLOCKS: usize = 10;
186 let latest_height = ledger.current_block.read().height();
188 debug_assert_eq!(latest_height, ledger.vm.block_store().max_height().unwrap(), "Mismatch in latest height");
189 let block_heights: Vec<u32> =
191 (0..=latest_height).choose_multiple(&mut OsRng, (latest_height as usize).min(NUM_BLOCKS));
192 cfg_into_iter!(block_heights).try_for_each(|height| {
193 ledger.get_block(height)?;
194 Ok::<_, Error>(())
195 })?;
196 lap!(timer, "Check existence of {NUM_BLOCKS} random blocks");
197
198 finish!(timer);
199 Ok(ledger)
200 }
201
202 pub fn load_unchecked(genesis_block: Block<N>, storage_mode: StorageMode) -> Result<Self> {
204 let timer = timer!("Ledger::load_unchecked");
205
206 info!("Loading the ledger from storage...");
207 let store = match ConsensusStore::<N, C>::open(storage_mode) {
209 Ok(store) => store,
210 Err(e) => bail!("Failed to load ledger (run 'snarkos clean' and try again)\n\n{e}\n"),
211 };
212 lap!(timer, "Load consensus store");
213
214 let vm = VM::from(store)?;
216 lap!(timer, "Initialize a new VM");
217
218 let current_committee = vm.finalize_store().committee_store().current_committee().ok();
220
221 let committee_cache = Mutex::new(LruCache::new(COMMITTEE_CACHE_SIZE.try_into().unwrap()));
223
224 let ledger = Self(Arc::new(InnerLedger {
226 vm,
227 genesis_block: genesis_block.clone(),
228 current_epoch_hash: Default::default(),
229 current_committee: RwLock::new(current_committee),
230 current_block: RwLock::new(genesis_block.clone()),
231 committee_cache,
232 epoch_provers_cache: Default::default(),
233 }));
234
235 let max_stored_height = ledger.vm.block_store().max_height();
237
238 let latest_height = if let Some(max_height) = max_stored_height {
240 max_height
241 } else {
242 ledger.advance_to_next_block(&genesis_block)?;
243 0
244 };
245 lap!(timer, "Initialize genesis");
246
247 ensure!(
249 latest_height == ledger.vm().block_store().current_block_height(),
250 "The stored height is different than the one in the block tree; \
251 please ensure that the cached block tree is valid or delete the \
252 'block_tree' file from the ledger folder"
253 );
254
255 let tree_root = <N::StateRoot>::from(ledger.vm().block_store().get_block_tree_root());
257 let state_root = ledger
258 .vm()
259 .block_store()
260 .get_state_root(latest_height)?
261 .ok_or_else(|| anyhow!("Missing state root in the storage"))?;
262 ensure!(
263 tree_root == state_root,
264 "The stored state root is different than the one in the block tree;
265 please ensure that the cached block tree is valid or delete the \
266 'block_tree' file from the ledger folder"
267 );
268
269 let block = ledger
271 .get_block(latest_height)
272 .with_context(|| format!("Failed to load block {latest_height} from the ledger"))?;
273
274 *ledger.current_block.write() = block;
276 *ledger.current_committee.write() = Some(ledger.latest_committee()?);
278 *ledger.current_epoch_hash.write() = Some(ledger.get_epoch_hash(latest_height)?);
280 *ledger.epoch_provers_cache.write() = ledger.load_epoch_provers();
282
283 finish!(timer, "Initialize ledger");
284 Ok(ledger)
285 }
286}
287
288impl<N: Network, C: ConsensusStorage<N>> Ledger<N, C> {
289 #[cfg(feature = "rocks")]
294 pub fn backup_database<P: AsRef<std::path::Path>>(&self, path: P) -> Result<()> {
295 self.vm.block_store().backup_database(path).map_err(|err| anyhow!(err))
296 }
297
298 #[cfg(feature = "rocks")]
299 pub fn cache_block_tree(&self) -> Result<()> {
300 self.vm.block_store().cache_block_tree()
301 }
302
303 pub fn load_epoch_provers(&self) -> IndexMap<Address<N>, u32> {
305 let current_block_height = self.vm().block_store().current_block_height();
307
308 let next_block_height = current_block_height.saturating_add(1);
312 let start = next_block_height.saturating_sub(current_block_height % N::NUM_BLOCKS_PER_EPOCH);
313
314 if start > current_block_height {
316 return IndexMap::new();
317 }
318
319 let existing_epoch_blocks: Vec<_> = (start..=current_block_height).collect();
321 let solution_addresses = cfg_iter!(existing_epoch_blocks)
322 .flat_map(|height| match self.get_solutions(*height).as_deref() {
323 Ok(Some(solutions)) => solutions.iter().map(|(_, s)| s.address()).collect::<Vec<_>>(),
324 _ => vec![],
325 })
326 .collect::<Vec<_>>();
327
328 let mut epoch_provers = IndexMap::new();
330 for address in solution_addresses {
331 epoch_provers.entry(address).and_modify(|e| *e += 1).or_insert(1);
332 }
333 epoch_provers
334 }
335
336 pub fn vm(&self) -> &VM<N, C> {
338 &self.vm
339 }
340
341 pub fn puzzle(&self) -> &Puzzle<N> {
343 self.vm.puzzle()
344 }
345
346 pub fn block_cache_size(&self) -> Option<u32> {
348 self.vm.block_store().cache_size()
349 }
350
351 pub fn epoch_provers(&self) -> Arc<RwLock<IndexMap<Address<N>, u32>>> {
353 self.epoch_provers_cache.clone()
354 }
355
356 pub fn latest_committee(&self) -> Result<Committee<N>> {
359 match self.current_committee.read().as_ref() {
360 Some(committee) => Ok(committee.clone()),
361 None => self.vm.finalize_store().committee_store().current_committee(),
362 }
363 }
364
365 pub fn latest_state_root(&self) -> N::StateRoot {
367 self.vm.block_store().current_state_root()
368 }
369
370 pub fn latest_epoch_number(&self) -> u32 {
372 self.current_block.read().height() / N::NUM_BLOCKS_PER_EPOCH
373 }
374
375 pub fn latest_epoch_hash(&self) -> Result<N::BlockHash> {
377 match self.current_epoch_hash.read().as_ref() {
378 Some(epoch_hash) => Ok(*epoch_hash),
379 None => self.get_epoch_hash(self.latest_height()),
380 }
381 }
382
383 pub fn latest_block(&self) -> Block<N> {
385 self.current_block.read().clone()
386 }
387
388 pub fn latest_round(&self) -> u64 {
390 self.current_block.read().round()
391 }
392
393 pub fn latest_height(&self) -> u32 {
395 self.current_block.read().height()
396 }
397
398 pub fn latest_hash(&self) -> N::BlockHash {
400 self.current_block.read().hash()
401 }
402
403 pub fn latest_header(&self) -> Header<N> {
405 *self.current_block.read().header()
406 }
407
408 pub fn latest_cumulative_weight(&self) -> u128 {
410 self.current_block.read().cumulative_weight()
411 }
412
413 pub fn latest_cumulative_proof_target(&self) -> u128 {
415 self.current_block.read().cumulative_proof_target()
416 }
417
418 pub fn latest_solutions_root(&self) -> Field<N> {
420 self.current_block.read().header().solutions_root()
421 }
422
423 pub fn latest_coinbase_target(&self) -> u64 {
425 self.current_block.read().coinbase_target()
426 }
427
428 pub fn latest_proof_target(&self) -> u64 {
430 self.current_block.read().proof_target()
431 }
432
433 pub fn last_coinbase_target(&self) -> u64 {
435 self.current_block.read().last_coinbase_target()
436 }
437
438 pub fn last_coinbase_timestamp(&self) -> i64 {
440 self.current_block.read().last_coinbase_timestamp()
441 }
442
443 pub fn latest_timestamp(&self) -> i64 {
445 self.current_block.read().timestamp()
446 }
447
448 pub fn latest_transactions(&self) -> Transactions<N> {
450 self.current_block.read().transactions().clone()
451 }
452}
453
454impl<N: Network, C: ConsensusStorage<N>> Ledger<N, C> {
455 pub fn find_unspent_credits_records(&self, view_key: &ViewKey<N>) -> Result<RecordMap<N>> {
457 let microcredits = Identifier::from_str("microcredits")?;
458 Ok(self
459 .find_records(view_key, RecordsFilter::Unspent)?
460 .filter(|(_, record)| {
461 match record.data().get(µcredits) {
463 Some(Entry::Private(Plaintext::Literal(Literal::U64(amount), _))) => !amount.is_zero(),
464 _ => false,
465 }
466 })
467 .collect::<IndexMap<_, _>>())
468 }
469
470 pub fn create_deploy<R: Rng + CryptoRng>(
474 &self,
475 private_key: &PrivateKey<N>,
476 program: &Program<N>,
477 priority_fee_in_microcredits: u64,
478 query: Option<&dyn QueryTrait<N>>,
479 rng: &mut R,
480 ) -> Result<Transaction<N>> {
481 let records = self.find_unspent_credits_records(&ViewKey::try_from(private_key)?)?;
483 ensure!(!records.len().is_zero(), "The Aleo account has no records to spend.");
484 let mut records = records.values();
485
486 let fee_record = Some(records.next().unwrap().clone());
488
489 self.vm.deploy(private_key, program, fee_record, priority_fee_in_microcredits, query, rng)
491 }
492
493 pub fn create_transfer<R: Rng + CryptoRng>(
497 &self,
498 private_key: &PrivateKey<N>,
499 to: Address<N>,
500 amount_in_microcredits: u64,
501 priority_fee_in_microcredits: u64,
502 query: Option<&dyn QueryTrait<N>>,
503 rng: &mut R,
504 ) -> Result<Transaction<N>> {
505 let records = self.find_unspent_credits_records(&ViewKey::try_from(private_key)?)?;
507 ensure!(records.len() >= 2, "The Aleo account does not have enough records to spend.");
508 let mut records = records.values();
509
510 let inputs = [
512 Value::Record(records.next().unwrap().clone()),
513 Value::from_str(&format!("{to}"))?,
514 Value::from_str(&format!("{amount_in_microcredits}u64"))?,
515 ];
516
517 let fee_record = Some(records.next().unwrap().clone());
519
520 self.vm.execute(
522 private_key,
523 ("credits.aleo", "transfer_private"),
524 inputs.iter(),
525 fee_record,
526 priority_fee_in_microcredits,
527 query,
528 rng,
529 )
530 }
531}
532
533#[cfg(feature = "rocks")]
534impl<N: Network, C: ConsensusStorage<N>> Drop for InnerLedger<N, C> {
535 fn drop(&mut self) {
536 if let Err(e) = self.vm.block_store().cache_block_tree() {
542 error!("Couldn't cache the block tree: {e}");
543 }
544 }
545}
546
547pub mod prelude {
548 pub use crate::{Ledger, authority, block, block::*, committee, helpers::*, narwhal, puzzle, query, store};
549}