cache.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 use crate::block::{Block, Network}; 17 18 use alphavm_utilities::ensure_equals; 19 20 use std::collections::VecDeque; 21 22 use anyhow::{Result, ensure}; 23 24 /// Helper struct for caching the most recent blocks. 25 pub(super) struct BlockCache<N: Network> { 26 /// Contains the most recent blocks ordered by height. 27 /// We do not use a BTreeMap here as the cache is small and updates to a vector are more efficient 28 /// 29 /// Invariant: all entries in this vector (except the first) a height equal to h+1 (where h` is the previous block entry) 30 blocks: VecDeque<Block<N>>, 31 } 32 33 impl<N: Network> BlockCache<N> { 34 /// The maximum size of the cache in blocks. 35 pub(super) const BLOCK_CACHE_SIZE: u32 = 10; 36 37 /// Initialize the cache with the given blocks. 38 pub fn new(blocks: Vec<Block<N>>) -> Result<Self> { 39 ensure!(blocks.len() <= Self::BLOCK_CACHE_SIZE as usize, "Too many blocks to fit in the cache"); 40 41 if let Some(block) = blocks.first() { 42 ensure!(block.height() != 0, "Cannot cache the genesis block"); 43 } 44 for idx in 1..blocks.len() { 45 ensure!(blocks[idx - 1].height() + 1 == blocks[idx].height(), "Not a continuous chain of blocks"); 46 } 47 48 Ok(Self { blocks: VecDeque::from(blocks) }) 49 } 50 51 /// Insert a new block into the cache. 52 /// Must be the successor of the last block inserted into the cache. 53 #[inline] 54 pub fn insert(&mut self, block: Block<N>) -> Result<()> { 55 ensure!(block.height() != 0, "Cannot cache the genesis block"); 56 57 if let Some(prev) = self.blocks.back() { 58 ensure_equals!( 59 prev.height() + 1, 60 block.height(), 61 "Block is not the successor of the last block inserted into the cache" 62 ); 63 } 64 65 self.blocks.push_back(block.clone()); 66 if self.blocks.len() > (Self::BLOCK_CACHE_SIZE as usize) { 67 self.blocks.pop_front(); 68 } 69 70 Ok(()) 71 } 72 73 /// Return the block at the given height if it is in the cache.i 74 #[inline] 75 pub fn get_block(&self, block_height: u32) -> Option<&Block<N>> { 76 let first_block = self.blocks.front()?; 77 78 // Determine to location of the cached block (if any). 79 // This returns `None` if `block_height` is lower than the height of the first cached block. 80 let offset = block_height.checked_sub(first_block.height())?; 81 82 self.blocks.get(offset as usize) 83 } 84 85 /// Return the block with the given hash if it is in the cache. 86 #[inline] 87 pub fn get_block_by_hash(&self, block_hash: &N::BlockHash) -> Option<&Block<N>> { 88 // Perform a linear search through the cache. 89 // This is cheap, as the cache is very small. 90 self.blocks.iter().find(|block| &block.hash() == block_hash) 91 } 92 93 /// Remove the last `n` blocks from the cache. 94 #[inline] 95 pub fn remove_last_n(&mut self, n: u32) -> Result<()> { 96 for _ in 0..n { 97 self.blocks.pop_back(); 98 } 99 Ok(()) 100 } 101 } 102 103 #[cfg(test)] 104 mod tests { 105 use super::*; 106 107 use crate::test_helpers::CurrentNetwork; 108 109 use alphavm_console::{ 110 account::{Field, PrivateKey}, 111 prelude::{Rng, TestRng}, 112 }; 113 use alphavm_ledger_authority::Authority; 114 use alphavm_ledger_block::{Header, Metadata, Ratifications, Transactions}; 115 116 type BlockCache = super::BlockCache<CurrentNetwork>; 117 118 #[test] 119 fn eviction() { 120 // The number of blocks to insert during the test 121 // (must be more than the cache size) 122 const NUM_BLOCKS: u32 = 15; 123 const { assert!(NUM_BLOCKS > BlockCache::BLOCK_CACHE_SIZE) }; 124 125 let rng = &mut TestRng::default(); 126 let private_key = PrivateKey::<CurrentNetwork>::new(rng).unwrap(); 127 128 // Construct a chain of blocks. 129 let mut previous_hash = None; 130 let blocks: Vec<_> = (0..NUM_BLOCKS) 131 .map(|h| { 132 let transactions = Transactions::from(&[]); 133 let ratifications = Ratifications::try_from(vec![]).unwrap(); 134 let header = if h == 0 { 135 Header::genesis(&ratifications, &transactions, vec![]).unwrap() 136 } else { 137 // Use mock metadata to save compute time. 138 let metadata = Metadata::new( 139 CurrentNetwork::ID, 140 (h * 2) as u64, 141 h, 142 0, 143 (h * 1000) as u128, 144 CurrentNetwork::GENESIS_COINBASE_TARGET, 145 CurrentNetwork::GENESIS_PROOF_TARGET + 1, 146 CurrentNetwork::GENESIS_COINBASE_TARGET, 147 CurrentNetwork::GENESIS_TIMESTAMP + ((h - 1) * 100) as i64, 148 CurrentNetwork::GENESIS_TIMESTAMP + (h * 100) as i64, 149 ) 150 .unwrap(); 151 152 let previous_state_root: <CurrentNetwork as Network>::StateRoot = rng.r#gen(); 153 Header::<CurrentNetwork>::from( 154 previous_state_root, 155 Field::from_u32(1), 156 Field::from_u32(1), 157 Field::from_u32(1), 158 Field::from_u32(1), 159 Field::from_u32(1), 160 metadata, 161 ) 162 .unwrap() 163 }; 164 let block_hash = rng.r#gen(); 165 let authority = Authority::<CurrentNetwork>::new_beacon(&private_key, block_hash, rng).unwrap(); 166 167 let block = Block::from_unchecked( 168 block_hash.into(), 169 previous_hash.unwrap_or_default(), 170 header, 171 authority, 172 ratifications, 173 None.into(), 174 vec![], 175 transactions, 176 vec![], 177 ) 178 .unwrap(); 179 180 previous_hash = Some(block.hash()); 181 block 182 }) 183 .collect(); 184 185 let mut cache = BlockCache::new(vec![]).unwrap(); 186 let mut blocks = blocks.into_iter().skip(1); 187 188 // First, fill up the cache. 189 while cache.blocks.len() < (BlockCache::BLOCK_CACHE_SIZE as usize) { 190 cache.insert(blocks.next().unwrap()).unwrap(); 191 } 192 193 // Then, continue insertions and check that old blocks are evicted. 194 for block in blocks { 195 let hash = block.hash(); 196 let height = block.height(); 197 198 cache.insert(block).unwrap(); 199 200 // Ensure eviction work. 201 assert_eq!(cache.blocks.len(), BlockCache::BLOCK_CACHE_SIZE as usize); 202 203 // Ensure the correct block is returned. 204 let ret1 = cache.get_block(height).unwrap(); 205 let ret2 = cache.get_block_by_hash(&hash).unwrap(); 206 207 assert_eq!(ret1.hash(), hash); 208 assert_eq!(ret2.hash(), hash); 209 assert_eq!(ret1.height(), height); 210 assert_eq!(ret2.height(), height); 211 212 assert_eq!(cache.blocks[0].height(), height - BlockCache::BLOCK_CACHE_SIZE + 1); 213 } 214 215 // Fetch something that isn't the last block. 216 let block = cache.get_block(10).unwrap(); 217 assert_eq!(block.height(), 10); 218 219 // Fetch the last thing that must be in the cache. 220 assert!(cache.get_block(NUM_BLOCKS - BlockCache::BLOCK_CACHE_SIZE).is_some()); 221 // Fetch something that must not be in the cache. 222 assert!(cache.get_block(NUM_BLOCKS - BlockCache::BLOCK_CACHE_SIZE - 1).is_none()); 223 } 224 }