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