block_cache.rs
1 // Copyright (c) 2019-2025 Alpha-Delta Network Inc. 2 // This file is part of the deltavm 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 //! Integration tests for the ledger block cache functionality. 17 18 use deltavm_ledger::{ 19 Ledger, 20 test_helpers::{CurrentConsensusStore, CurrentLedger, CurrentNetwork, LedgerType}, 21 }; 22 23 use alphastd::StorageMode; 24 use deltavm_console::{account::PrivateKey, prelude::*}; 25 use deltavm_synthesizer::vm::VM; 26 27 /// Tests that the block cache is properly initialized with existing blocks from storage. 28 #[test] 29 fn test_block_cache_initialization() { 30 let rng = &mut TestRng::default(); 31 32 // Initialize a ledger without block cache and add some blocks. 33 let private_key = PrivateKey::<CurrentNetwork>::new(rng).unwrap(); 34 let store = CurrentConsensusStore::open(StorageMode::new_test(None)).unwrap(); 35 let genesis = VM::from(store).unwrap().genesis_beacon(&private_key, rng).unwrap(); 36 37 const NUM_BLOCKS: u32 = 15; // More than cache size (10) to test initialization logic 38 39 // Generate some block in the storage. 40 let temp_ledger = CurrentLedger::load(genesis.clone(), StorageMode::new_test(None)).unwrap(); 41 for _ in 1..=NUM_BLOCKS { 42 let transactions = vec![]; 43 let block = 44 temp_ledger.prepare_advance_to_next_beacon_block(&private_key, vec![], vec![], transactions, rng).unwrap(); 45 temp_ledger.advance_to_next_block(&block).unwrap(); 46 } 47 48 // Now load a new ledger and ensure that the block cache is correclty populated. 49 let ledger = Ledger::<CurrentNetwork, LedgerType>::load(genesis.clone(), StorageMode::new_test(None)).unwrap(); 50 51 // Verify that recent blocks can be retrieved quickly (should be in cache). 52 let latest_height = ledger.latest_height(); 53 54 // Test retrieval of the 10 most recent blocks (should be in cache). 55 for height in (latest_height.saturating_sub(9))..=latest_height { 56 let block = ledger.get_block(height).unwrap(); 57 assert_eq!(block.height(), height); 58 } 59 60 // Test retrieval of older blocks (should not be in cache but still accessible). 61 for height in 0..=(latest_height.saturating_sub(10)) { 62 let block = ledger.get_block(height).unwrap(); 63 assert_eq!(block.height(), height); 64 } 65 } 66 67 /// Tests cache hit and miss behavior when retrieving blocks. 68 #[test] 69 fn test_block_cache_hit_miss_behavior() { 70 let rng = &mut TestRng::default(); 71 72 // Create a ledger with block cache enabled. 73 let private_key = PrivateKey::<CurrentNetwork>::new(rng).unwrap(); 74 let store = CurrentConsensusStore::open(StorageMode::new_test(None)).unwrap(); 75 let genesis = VM::from(store).unwrap().genesis_beacon(&private_key, rng).unwrap(); 76 77 let ledger = Ledger::<CurrentNetwork, LedgerType>::load(genesis.clone(), StorageMode::new_test(None)).unwrap(); 78 79 let cache_size = ledger.block_cache_size().expect("No cache size found"); 80 assert!(cache_size > 0, "Cache size is 0"); 81 82 // Add blocks to fill and exceed the cache. 83 let mut block_hashes = vec![genesis.hash()]; 84 85 for _ in 1..=(cache_size + 5) { 86 let transactions = vec![]; 87 let block = 88 ledger.prepare_advance_to_next_beacon_block(&private_key, vec![], vec![], transactions, rng).unwrap(); 89 let block_hash = block.hash(); 90 ledger.advance_to_next_block(&block).unwrap(); 91 block_hashes.push(block_hash); 92 } 93 94 let latest_height = ledger.latest_height(); 95 96 // Test cache hits: Recent blocks should be fast to retrieve. 97 for height in (latest_height.saturating_sub(cache_size - 1))..=latest_height { 98 let start = std::time::Instant::now(); 99 let block = ledger.get_block(height).unwrap(); 100 let duration = start.elapsed(); 101 102 assert_eq!(block.height(), height); 103 assert_eq!(block.hash(), block_hashes[height as usize]); 104 105 // Recent blocks should be retrieved quickly from cache. 106 // This is a rough performance check - exact timing depends on system. 107 assert!(duration.as_micros() < 1000, "Block {height} retrieval took too long: {duration:?}"); 108 } 109 110 // Test cache misses: Older blocks should still be accessible but may be slower. 111 for height in 0..=(latest_height.saturating_sub(cache_size)) { 112 let block = ledger.get_block(height).unwrap(); 113 114 assert_eq!(block.height(), height); 115 assert_eq!(block.hash(), block_hashes[height as usize]); 116 } 117 } 118 119 /// Tests that the cache properly evicts old blocks when new ones are added. 120 #[test] 121 fn test_block_cache_eviction() { 122 let rng = &mut TestRng::default(); 123 124 // Create a ledger with block cache enabled. 125 let private_key = PrivateKey::<CurrentNetwork>::new(rng).unwrap(); 126 let store = CurrentConsensusStore::open(StorageMode::new_test(None)).unwrap(); 127 let genesis = VM::from(store).unwrap().genesis_beacon(&private_key, rng).unwrap(); 128 129 let ledger = Ledger::<CurrentNetwork, LedgerType>::load(genesis.clone(), StorageMode::new_test(None)).unwrap(); 130 131 // Add exactly 10 blocks to fill the cache. 132 for i in 1..=10 { 133 let transactions = vec![]; 134 let block = 135 ledger.prepare_advance_to_next_beacon_block(&private_key, vec![], vec![], transactions, rng).unwrap(); 136 ledger.advance_to_next_block(&block).unwrap(); 137 138 // Verify the block was added correctly. 139 assert_eq!(ledger.latest_height(), i); 140 } 141 142 // At this point, cache should contain blocks 1-10 (genesis block 0 may or may not be cached). 143 let initial_height = ledger.latest_height(); 144 145 // Add 5 more blocks, which should cause eviction of the oldest cached blocks. 146 for i in 1..=5 { 147 let transactions = vec![]; 148 let block = 149 ledger.prepare_advance_to_next_beacon_block(&private_key, vec![], vec![], transactions, rng).unwrap(); 150 ledger.advance_to_next_block(&block).unwrap(); 151 152 assert_eq!(ledger.latest_height(), initial_height + i); 153 } 154 155 let final_height = ledger.latest_height(); 156 157 // Verify that all blocks are still accessible, regardless of cache state. 158 for height in 0..=final_height { 159 let block = ledger.get_block(height).unwrap(); 160 assert_eq!(block.height(), height); 161 } 162 163 // Verify that the most recent 10 blocks should be the fastest to access. 164 let cache_start = final_height.saturating_sub(9); 165 for height in cache_start..=final_height { 166 let start = std::time::Instant::now(); 167 let _block = ledger.get_block(height).unwrap(); 168 let duration = start.elapsed(); 169 170 // Recent blocks should be retrieved quickly from cache. 171 assert!(duration.as_micros() < 1000, "Cached block {height} retrieval took too long: {duration:?}"); 172 } 173 } 174 175 /// Tests performance comparison between ledgers with and without block cache. 176 #[test] 177 fn test_block_cache_performance_comparison() { 178 let rng = &mut TestRng::default(); 179 180 // Create two identical ledgers: one with cache, one without. 181 let private_key = PrivateKey::<CurrentNetwork>::new(rng).unwrap(); 182 183 // Create a common genesis block for both ledgers. 184 let store = CurrentConsensusStore::open(StorageMode::new_test(None)).unwrap(); 185 let genesis = VM::from(store).unwrap().genesis_beacon(&private_key, rng).unwrap(); 186 187 // Ledger without cache. 188 let ledger_no_cache = CurrentLedger::load(genesis.clone(), StorageMode::new_test(None)).unwrap(); 189 190 // Ledger with cache. 191 let ledger_with_cache = 192 Ledger::<CurrentNetwork, LedgerType>::load(genesis.clone(), StorageMode::new_test(None)).unwrap(); 193 194 // Add the same blocks to both ledgers. 195 const NUM_BLOCKS: u32 = 20; 196 for _ in 1..=NUM_BLOCKS { 197 let transactions = vec![]; 198 199 // Add to non-cached ledger. 200 let block = ledger_no_cache 201 .prepare_advance_to_next_beacon_block(&private_key, vec![], vec![], transactions.clone(), rng) 202 .unwrap(); 203 ledger_no_cache.advance_to_next_block(&block).unwrap(); 204 205 // Add the same block to cached ledger. 206 ledger_with_cache.advance_to_next_block(&block).unwrap(); 207 } 208 209 // Test retrieval performance for recent blocks (should be cached). 210 let latest_height = ledger_with_cache.latest_height(); 211 let test_heights: Vec<u32> = (latest_height.saturating_sub(9)..=latest_height).collect(); 212 213 // Measure performance without cache. 214 let start_no_cache = std::time::Instant::now(); 215 for &height in &test_heights { 216 let _block = ledger_no_cache.get_block(height).unwrap(); 217 } 218 let duration_no_cache = start_no_cache.elapsed(); 219 220 // Measure performance with cache. 221 let start_with_cache = std::time::Instant::now(); 222 for &height in &test_heights { 223 let _block = ledger_with_cache.get_block(height).unwrap(); 224 } 225 let duration_with_cache = start_with_cache.elapsed(); 226 227 // Cache should provide some performance benefit for recent blocks. 228 // Note: In memory storage, the difference might be minimal, but with RocksDB it should be more significant. 229 println!("No cache: {duration_no_cache:?}, With cache: {duration_with_cache:?}"); 230 231 // Verify both ledgers return the same blocks. 232 for height in 0..=latest_height { 233 let block_no_cache = ledger_no_cache.get_block(height).unwrap(); 234 let block_with_cache = ledger_with_cache.get_block(height).unwrap(); 235 assert_eq!(block_no_cache.hash(), block_with_cache.hash(), "different block hashes at height {height}"); 236 assert_eq!(block_no_cache.height(), block_with_cache.height()); 237 } 238 } 239 240 /// Tests cache behavior during block advancement operations. 241 #[test] 242 fn test_block_cache_during_advancement() { 243 let rng = &mut TestRng::default(); 244 245 // Create a ledger with block cache enabled. 246 let private_key = PrivateKey::<CurrentNetwork>::new(rng).unwrap(); 247 let store = CurrentConsensusStore::open(StorageMode::new_test(None)).unwrap(); 248 let genesis = VM::from(store).unwrap().genesis_beacon(&private_key, rng).unwrap(); 249 250 let ledger = Ledger::<CurrentNetwork, LedgerType>::load(genesis.clone(), StorageMode::new_test(None)).unwrap(); 251 252 // Test cache behavior during sequential block additions. 253 const NUM_BLOCKS: u32 = 25; // Exceeds cache size to test eviction 254 let mut added_blocks = vec![genesis]; 255 256 for i in 1..=NUM_BLOCKS { 257 let transactions = vec![]; 258 let block = 259 ledger.prepare_advance_to_next_beacon_block(&private_key, vec![], vec![], transactions, rng).unwrap(); 260 261 // Test that we can retrieve the current latest block before advancement. 262 let current_latest = ledger.get_block(ledger.latest_height()).unwrap(); 263 assert_eq!(current_latest.height(), i - 1); 264 265 // Advance to the next block. 266 ledger.advance_to_next_block(&block).unwrap(); 267 added_blocks.push(block.clone()); 268 269 // Test that the new block is immediately available. 270 let new_latest = ledger.get_block(ledger.latest_height()).unwrap(); 271 assert_eq!(new_latest.height(), i); 272 assert_eq!(new_latest.hash(), block.hash()); 273 274 // Test that previous blocks are still accessible. 275 if i >= 10 { 276 // Test both cached and potentially non-cached blocks. 277 let recent_block = ledger.get_block(i - 5).unwrap(); 278 assert_eq!(recent_block.height(), i - 5); 279 280 if i >= 15 { 281 let older_block = ledger.get_block(i - 15).unwrap(); 282 assert_eq!(older_block.height(), i - 15); 283 } 284 } 285 } 286 287 // Final verification: all blocks should be accessible. 288 for (expected_height, expected_block) in added_blocks.iter().enumerate() { 289 let retrieved_block = ledger.get_block(expected_height as u32).unwrap(); 290 assert_eq!(retrieved_block.hash(), expected_block.hash()); 291 assert_eq!(retrieved_block.height(), expected_height as u32); 292 } 293 294 // Test batch retrieval of recent blocks (should leverage cache). 295 let latest_height = ledger.latest_height(); 296 let recent_heights: Vec<u32> = (latest_height.saturating_sub(9)..=latest_height).collect(); 297 298 let start = std::time::Instant::now(); 299 let recent_blocks = ledger.get_blocks(recent_heights[0]..recent_heights[recent_heights.len() - 1] + 1).unwrap(); 300 let duration = start.elapsed(); 301 302 assert_eq!(recent_blocks.len(), recent_heights.len()); 303 for (block, &expected_height) in recent_blocks.iter().zip(&recent_heights) { 304 assert_eq!(block.height(), expected_height); 305 } 306 307 // Recent block retrieval should be fast. 308 assert!(duration.as_millis() < 100, "Batch retrieval of recent blocks took too long: {duration:?}"); 309 }