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(µcredits) { 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 }