/ ledger / src / lib.rs
lib.rs
  1  // Copyright (c) 2025 ADnet Contributors
  2  // This file is part of the AlphaVM 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  
  8  // http://www.apache.org/licenses/LICENSE-2.0
  9  
 10  // Unless required by applicable law or agreed to in writing, software
 11  // distributed under the License is distributed on an "AS IS" BASIS,
 12  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 13  // See the License for the specific language governing permissions and
 14  // limitations under the License.
 15  
 16  #![forbid(unsafe_code)]
 17  #![warn(clippy::cast_possible_truncation)]
 18  
 19  extern crate alphavm_console as console;
 20  
 21  #[macro_use]
 22  extern crate tracing;
 23  
 24  pub use alphavm_ledger_authority as authority;
 25  pub use alphavm_ledger_block as block;
 26  pub use alphavm_ledger_committee as committee;
 27  pub use alphavm_ledger_governor as governor;
 28  pub use alphavm_ledger_narwhal as narwhal;
 29  pub use alphavm_ledger_puzzle as puzzle;
 30  pub use alphavm_ledger_query as query;
 31  pub use alphavm_ledger_store as store;
 32  
 33  #[cfg(any(test, feature = "test-helpers"))]
 34  pub mod test_helpers;
 35  
 36  mod helpers;
 37  pub use helpers::*;
 38  
 39  pub use crate::block::*;
 40  
 41  mod check_next_block;
 42  pub use check_next_block::{CheckBlockError, PendingBlock};
 43  
 44  mod advance;
 45  mod check_transaction_basic;
 46  mod contains;
 47  mod find;
 48  mod get;
 49  mod is_solution_limit_reached;
 50  mod iterators;
 51  
 52  #[cfg(test)]
 53  mod tests;
 54  
 55  use alphavm_ledger_authority::Authority;
 56  use alphavm_ledger_committee::Committee;
 57  use alphavm_ledger_narwhal::{BatchCertificate, Subdag, Transmission, TransmissionID};
 58  use alphavm_ledger_puzzle::{Puzzle, PuzzleSolutions, Solution, SolutionID};
 59  use alphavm_ledger_query::QueryTrait;
 60  use alphavm_ledger_store::{ConsensusStorage, ConsensusStore};
 61  use alphavm_synthesizer::{
 62      program::{FinalizeGlobalState, Program},
 63      vm::VM,
 64  };
 65  use console::{
 66      account::{Address, GraphKey, PrivateKey, ViewKey},
 67      network::prelude::*,
 68      program::{Ciphertext, Entry, Identifier, Literal, Plaintext, ProgramID, Record, StatePath, Value},
 69      types::{Field, Group},
 70  };
 71  
 72  use alpha_std::{
 73      StorageMode,
 74      prelude::{finish, lap, timer},
 75  };
 76  use anyhow::{Context, Result};
 77  use core::ops::Range;
 78  use indexmap::IndexMap;
 79  #[cfg(feature = "locktick")]
 80  use locktick::parking_lot::{Mutex, RwLock};
 81  use lru::LruCache;
 82  #[cfg(not(feature = "locktick"))]
 83  use parking_lot::{Mutex, RwLock};
 84  use rand::{prelude::IteratorRandom, rngs::OsRng};
 85  use std::{borrow::Cow, collections::HashSet, sync::Arc};
 86  use time::OffsetDateTime;
 87  
 88  #[cfg(not(feature = "serial"))]
 89  use rayon::prelude::*;
 90  
 91  pub type RecordMap<N> = IndexMap<Field<N>, Record<N, Plaintext<N>>>;
 92  
 93  /// The capacity of the LRU cache holding the recently queried committees.
 94  const COMMITTEE_CACHE_SIZE: usize = 16;
 95  
 96  #[derive(Copy, Clone, Debug)]
 97  pub enum RecordsFilter<N: Network> {
 98      /// Returns all records associated with the account.
 99      All,
100      /// Returns only records associated with the account that are **spent** with the graph key.
101      Spent,
102      /// Returns only records associated with the account that are **not spent** with the graph key.
103      Unspent,
104      /// Returns all records associated with the account that are **spent** with the given private key.
105      SlowSpent(PrivateKey<N>),
106      /// Returns all records associated with the account that are **not spent** with the given private key.
107      SlowUnspent(PrivateKey<N>),
108  }
109  
110  /// State of the entire chain.
111  ///
112  /// All stored state is held in the `VM`, while Ledger holds the `VM` and relevant cache data.
113  ///
114  /// The constructor is [`Ledger::load`],
115  /// which loads the ledger from storage,
116  /// or initializes it with the genesis block if the storage is empty
117  #[derive(Clone)]
118  pub struct Ledger<N: Network, C: ConsensusStorage<N>>(Arc<InnerLedger<N, C>>);
119  
120  impl<N: Network, C: ConsensusStorage<N>> Deref for Ledger<N, C> {
121      type Target = InnerLedger<N, C>;
122  
123      fn deref(&self) -> &Self::Target {
124          &self.0
125      }
126  }
127  
128  #[doc(hidden)]
129  pub struct InnerLedger<N: Network, C: ConsensusStorage<N>> {
130      /// The VM state.
131      vm: VM<N, C>,
132      /// The genesis block.
133      genesis_block: Block<N>,
134      /// The current epoch hash.
135      current_epoch_hash: RwLock<Option<N::BlockHash>>,
136      /// The committee resulting from all the on-chain staking activity.
137      ///
138      /// This includes any bonding and unbonding transactions in the latest block.
139      /// The starting point, in the genesis block, is the genesis committee.
140      /// If the latest block has round `R`, `current_committee` is
141      /// the committee bonded for rounds `R+1`, `R+2`, and perhaps others
142      /// (unless a block at round `R+2` changes the committee).
143      /// Note that this committee is not active (i.e. in charge of running consensus)
144      /// until round `R + 1 + L`, where `L` is the lookback round distance.
145      ///
146      /// This committee is always well-defined
147      /// (in particular, it is the genesis committee when the `Ledger` is empty, or only has the genesis block).
148      /// So the `Option` should always be `Some`,
149      /// but there are cases in which it is `None`,
150      /// probably only temporarily when loading/initializing the ledger,
151      current_committee: RwLock<Option<Committee<N>>>,
152  
153      /// The latest block that was added to the ledger.
154      ///
155      /// This lock is also used as a way to prevent concurrent updates to the ledger, and to ensure that
156      /// the ledger does not advance while certain check happen.
157      current_block: RwLock<Block<N>>,
158      /// The recent committees of interest paired with their applicable rounds.
159      ///
160      /// Each entry consisting of a round `R` and a committee `C`,
161      /// says that `C` is the bonded committee at round `R`,
162      /// i.e. resulting from all the bonding and unbonding transactions before `R`.
163      /// If `L` is the lookback round distance, `C` is the active committee at round `R + L`
164      /// (i.e. the committee in charge of running consensus at round `R + L`).
165      committee_cache: Mutex<LruCache<u64, Committee<N>>>,
166      /// The cache that holds the provers and the number of solutions they have submitted for the current epoch.
167      epoch_provers_cache: Arc<RwLock<IndexMap<Address<N>, u32>>>,
168  }
169  
170  impl<N: Network, C: ConsensusStorage<N>> Ledger<N, C> {
171      /// Loads the ledger from storage.
172      pub fn load(genesis_block: Block<N>, storage_mode: StorageMode) -> Result<Self> {
173          let timer = timer!("Ledger::load");
174  
175          // Retrieve the genesis hash.
176          let genesis_hash = genesis_block.hash();
177          // Initialize the ledger.
178          let ledger = Self::load_unchecked(genesis_block, storage_mode)?;
179  
180          // Ensure the ledger contains the correct genesis block.
181          if !ledger.contains_block_hash(&genesis_hash)? {
182              bail!("Incorrect genesis block (run 'snarkos clean' and try again)")
183          }
184  
185          // Spot check the integrity of `NUM_BLOCKS` random blocks upon bootup.
186          const NUM_BLOCKS: usize = 10;
187          // Retrieve the latest height.
188          let latest_height = ledger.current_block.read().height();
189          debug_assert_eq!(latest_height, ledger.vm.block_store().max_height().unwrap(), "Mismatch in latest height");
190          // Sample random block heights.
191          let block_heights: Vec<u32> =
192              (0..=latest_height).choose_multiple(&mut OsRng, (latest_height as usize).min(NUM_BLOCKS));
193          cfg_into_iter!(block_heights).try_for_each(|height| {
194              ledger.get_block(height)?;
195              Ok::<_, Error>(())
196          })?;
197          lap!(timer, "Check existence of {NUM_BLOCKS} random blocks");
198  
199          finish!(timer);
200          Ok(ledger)
201      }
202  
203      /// Loads the ledger from storage, without performing integrity checks.
204      pub fn load_unchecked(genesis_block: Block<N>, storage_mode: StorageMode) -> Result<Self> {
205          let timer = timer!("Ledger::load_unchecked");
206  
207          info!("Loading the ledger from storage...");
208          // Initialize the consensus store.
209          let store = match ConsensusStore::<N, C>::open(storage_mode) {
210              Ok(store) => store,
211              Err(e) => bail!("Failed to load ledger (run 'snarkos clean' and try again)\n\n{e}\n"),
212          };
213          lap!(timer, "Load consensus store");
214  
215          // Initialize a new VM.
216          let vm = VM::from(store)?;
217          lap!(timer, "Initialize a new VM");
218  
219          // Retrieve the current committee.
220          let current_committee = vm.finalize_store().committee_store().current_committee().ok();
221  
222          // Create a committee cache.
223          let committee_cache = Mutex::new(LruCache::new(COMMITTEE_CACHE_SIZE.try_into().unwrap()));
224  
225          // Initialize the ledger.
226          let ledger = Self(Arc::new(InnerLedger {
227              vm,
228              genesis_block: genesis_block.clone(),
229              current_epoch_hash: Default::default(),
230              current_committee: RwLock::new(current_committee),
231              current_block: RwLock::new(genesis_block.clone()),
232              committee_cache,
233              epoch_provers_cache: Default::default(),
234          }));
235  
236          // Attempt to obtain the maximum height from the storage.
237          let max_stored_height = ledger.vm.block_store().max_height();
238  
239          // If the block store is empty, add the genesis block.
240          let latest_height = if let Some(max_height) = max_stored_height {
241              max_height
242          } else {
243              ledger.advance_to_next_block(&genesis_block)?;
244              0
245          };
246          lap!(timer, "Initialize genesis");
247  
248          // Ensure that the greatest stored height matches that of the block tree.
249          ensure!(
250              latest_height == ledger.vm().block_store().current_block_height(),
251              "The stored height is different than the one in the block tree; \
252              please ensure that the cached block tree is valid or delete the \
253              'block_tree' file from the ledger folder"
254          );
255  
256          // Verify that the root of the cached block tree matches the one in the storage.
257          let tree_root = <N::StateRoot>::from(ledger.vm().block_store().get_block_tree_root());
258          let state_root = ledger
259              .vm()
260              .block_store()
261              .get_state_root(latest_height)?
262              .ok_or_else(|| anyhow!("Missing state root in the storage"))?;
263          ensure!(
264              tree_root == state_root,
265              "The stored state root is different than the one in the block tree;
266              please ensure that the cached block tree is valid or delete the \
267              'block_tree' file from the ledger folder"
268          );
269  
270          // Fetch the latest block.
271          let block = ledger
272              .get_block(latest_height)
273              .with_context(|| format!("Failed to load block {latest_height} from the ledger"))?;
274  
275          // Set the current block.
276          *ledger.current_block.write() = block;
277          // Set the current committee (and ensures the latest committee exists).
278          *ledger.current_committee.write() = Some(ledger.latest_committee()?);
279          // Set the current epoch hash.
280          *ledger.current_epoch_hash.write() = Some(ledger.get_epoch_hash(latest_height)?);
281          // Set the epoch prover cache.
282          *ledger.epoch_provers_cache.write() = ledger.load_epoch_provers();
283  
284          finish!(timer, "Initialize ledger");
285          Ok(ledger)
286      }
287  }
288  
289  impl<N: Network, C: ConsensusStorage<N>> Ledger<N, C> {
290      /// Creates a rocksdb checkpoint in the specified directory, which needs to not exist at the
291      /// moment of calling. The checkpoints are based on hard links, which means they can both be
292      /// incremental (i.e. they aren't full physical copies), and used as full rollback points
293      /// (a checkpoint can be used to completely replace the original ledger).
294      #[cfg(feature = "rocks")]
295      pub fn backup_database<P: AsRef<std::path::Path>>(&self, path: P) -> Result<()> {
296          self.vm.block_store().backup_database(path).map_err(|err| anyhow!(err))
297      }
298  
299      #[cfg(feature = "rocks")]
300      pub fn cache_block_tree(&self) -> Result<()> {
301          self.vm.block_store().cache_block_tree()
302      }
303  
304      /// Loads the provers and the number of solutions they have submitted for the current epoch.
305      pub fn load_epoch_provers(&self) -> IndexMap<Address<N>, u32> {
306          // Fetch the block heights that belong to the current epoch.
307          let current_block_height = self.vm().block_store().current_block_height();
308          let start_of_epoch = current_block_height.saturating_sub(current_block_height % N::NUM_BLOCKS_PER_EPOCH);
309          let existing_epoch_blocks: Vec<_> = (start_of_epoch..=current_block_height).collect();
310  
311          // Collect the addresses of the solutions submitted in the current epoch.
312          let solution_addresses = cfg_iter!(existing_epoch_blocks)
313              .flat_map(|height| match self.get_solutions(*height).as_deref() {
314                  Ok(Some(solutions)) => solutions.iter().map(|(_, s)| s.address()).collect::<Vec<_>>(),
315                  _ => vec![],
316              })
317              .collect::<Vec<_>>();
318  
319          // Count the number of occurrences of each address in the epoch blocks.
320          let mut epoch_provers = IndexMap::new();
321          for address in solution_addresses {
322              epoch_provers.entry(address).and_modify(|e| *e += 1).or_insert(1);
323          }
324          epoch_provers
325      }
326  
327      /// Returns the VM.
328      pub fn vm(&self) -> &VM<N, C> {
329          &self.vm
330      }
331  
332      /// Returns the puzzle.
333      pub fn puzzle(&self) -> &Puzzle<N> {
334          self.vm.puzzle()
335      }
336  
337      /// Returns the size of the block cache (or `None` if the block cache is not enabled).
338      pub fn block_cache_size(&self) -> Option<u32> {
339          self.vm.block_store().cache_size()
340      }
341  
342      /// Returns the provers and the number of solutions they have submitted for the current epoch.
343      pub fn epoch_provers(&self) -> Arc<RwLock<IndexMap<Address<N>, u32>>> {
344          self.epoch_provers_cache.clone()
345      }
346  
347      /// Returns the latest committee,
348      /// i.e. the committee resulting from all the on-chain staking activity.
349      pub fn latest_committee(&self) -> Result<Committee<N>> {
350          match self.current_committee.read().as_ref() {
351              Some(committee) => Ok(committee.clone()),
352              None => self.vm.finalize_store().committee_store().current_committee(),
353          }
354      }
355  
356      /// Returns the latest state root.
357      pub fn latest_state_root(&self) -> N::StateRoot {
358          self.vm.block_store().current_state_root()
359      }
360  
361      /// Returns the latest epoch number.
362      pub fn latest_epoch_number(&self) -> u32 {
363          self.current_block.read().height() / N::NUM_BLOCKS_PER_EPOCH
364      }
365  
366      /// Returns the latest epoch hash.
367      pub fn latest_epoch_hash(&self) -> Result<N::BlockHash> {
368          match self.current_epoch_hash.read().as_ref() {
369              Some(epoch_hash) => Ok(*epoch_hash),
370              None => self.get_epoch_hash(self.latest_height()),
371          }
372      }
373  
374      /// Returns the latest block.
375      pub fn latest_block(&self) -> Block<N> {
376          self.current_block.read().clone()
377      }
378  
379      /// Returns the latest round number.
380      pub fn latest_round(&self) -> u64 {
381          self.current_block.read().round()
382      }
383  
384      /// Returns the latest block height.
385      pub fn latest_height(&self) -> u32 {
386          self.current_block.read().height()
387      }
388  
389      /// Returns the latest block hash.
390      pub fn latest_hash(&self) -> N::BlockHash {
391          self.current_block.read().hash()
392      }
393  
394      /// Returns the latest block header.
395      pub fn latest_header(&self) -> Header<N> {
396          *self.current_block.read().header()
397      }
398  
399      /// Returns the latest block cumulative weight.
400      pub fn latest_cumulative_weight(&self) -> u128 {
401          self.current_block.read().cumulative_weight()
402      }
403  
404      /// Returns the latest block cumulative proof target.
405      pub fn latest_cumulative_proof_target(&self) -> u128 {
406          self.current_block.read().cumulative_proof_target()
407      }
408  
409      /// Returns the latest block solutions root.
410      pub fn latest_solutions_root(&self) -> Field<N> {
411          self.current_block.read().header().solutions_root()
412      }
413  
414      /// Returns the latest block coinbase target.
415      pub fn latest_coinbase_target(&self) -> u64 {
416          self.current_block.read().coinbase_target()
417      }
418  
419      /// Returns the latest block proof target.
420      pub fn latest_proof_target(&self) -> u64 {
421          self.current_block.read().proof_target()
422      }
423  
424      /// Returns the last coinbase target.
425      pub fn last_coinbase_target(&self) -> u64 {
426          self.current_block.read().last_coinbase_target()
427      }
428  
429      /// Returns the last coinbase timestamp.
430      pub fn last_coinbase_timestamp(&self) -> i64 {
431          self.current_block.read().last_coinbase_timestamp()
432      }
433  
434      /// Returns the latest block timestamp.
435      pub fn latest_timestamp(&self) -> i64 {
436          self.current_block.read().timestamp()
437      }
438  
439      /// Returns the latest block transactions.
440      pub fn latest_transactions(&self) -> Transactions<N> {
441          self.current_block.read().transactions().clone()
442      }
443  }
444  
445  impl<N: Network, C: ConsensusStorage<N>> Ledger<N, C> {
446      /// Returns the unspent `credits.alpha` records.
447      pub fn find_unspent_credits_records(&self, view_key: &ViewKey<N>) -> Result<RecordMap<N>> {
448          let microcredits = Identifier::from_str("microcredits")?;
449          Ok(self
450              .find_records(view_key, RecordsFilter::Unspent)?
451              .filter(|(_, record)| {
452                  // TODO (raychu86): Find cleaner approach and check that the record is associated with the `credits.alpha` program
453                  match record.data().get(&microcredits) {
454                      Some(Entry::Private(Plaintext::Literal(Literal::U64(amount), _))) => !amount.is_zero(),
455                      _ => false,
456                  }
457              })
458              .collect::<IndexMap<_, _>>())
459      }
460  
461      /// Creates a deploy transaction.
462      ///
463      /// The `priority_fee_in_microcredits` is an additional fee **on top** of the deployment fee.
464      pub fn create_deploy<R: Rng + CryptoRng>(
465          &self,
466          private_key: &PrivateKey<N>,
467          program: &Program<N>,
468          priority_fee_in_microcredits: u64,
469          query: Option<&dyn QueryTrait<N>>,
470          rng: &mut R,
471      ) -> Result<Transaction<N>> {
472          // Fetch the unspent records.
473          let records = self.find_unspent_credits_records(&ViewKey::try_from(private_key)?)?;
474          ensure!(!records.len().is_zero(), "The Aleo account has no records to spend.");
475          let mut records = records.values();
476  
477          // Prepare the fee record.
478          let fee_record = Some(records.next().unwrap().clone());
479  
480          // Create a new deploy transaction.
481          self.vm.deploy(private_key, program, fee_record, priority_fee_in_microcredits, query, rng)
482      }
483  
484      /// Creates a transfer transaction.
485      ///
486      /// The `priority_fee_in_microcredits` is an additional fee **on top** of the execution fee.
487      pub fn create_transfer<R: Rng + CryptoRng>(
488          &self,
489          private_key: &PrivateKey<N>,
490          to: Address<N>,
491          amount_in_microcredits: u64,
492          priority_fee_in_microcredits: u64,
493          query: Option<&dyn QueryTrait<N>>,
494          rng: &mut R,
495      ) -> Result<Transaction<N>> {
496          // Fetch the unspent records.
497          let records = self.find_unspent_credits_records(&ViewKey::try_from(private_key)?)?;
498          ensure!(records.len() >= 2, "The Aleo account does not have enough records to spend.");
499          let mut records = records.values();
500  
501          // Prepare the inputs.
502          let inputs = [
503              Value::Record(records.next().unwrap().clone()),
504              Value::from_str(&format!("{to}"))?,
505              Value::from_str(&format!("{amount_in_microcredits}u64"))?,
506          ];
507  
508          // Prepare the fee.
509          let fee_record = Some(records.next().unwrap().clone());
510  
511          // Create a new execute transaction.
512          self.vm.execute(
513              private_key,
514              ("credits.alpha", "transfer_private"),
515              inputs.iter(),
516              fee_record,
517              priority_fee_in_microcredits,
518              query,
519              rng,
520          )
521      }
522  }
523  
524  #[cfg(feature = "rocks")]
525  impl<N: Network, C: ConsensusStorage<N>> Drop for InnerLedger<N, C> {
526      fn drop(&mut self) {
527          // Cache the block tree in order to speed up the next startup; this operation
528          // is guaranteed to conclude as long as the destructors are allowed to run
529          // (a clean shutdown, panic = "unwind", an explicit call to `drop`, etc.).
530          // At the moment this code is executed, the Ledger is guaranteed to be owned
531          // exclusively by this method, so no other activity may interrupt it.
532          if let Err(e) = self.vm.block_store().cache_block_tree() {
533              error!("Couldn't cache the block tree: {e}");
534          }
535      }
536  }
537  
538  pub mod prelude {
539      pub use crate::{Ledger, authority, block, block::*, committee, helpers::*, narwhal, puzzle, query, store};
540  }