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