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