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