/ ledger / tests / block_cache.rs
block_cache.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  //! Integration tests for the ledger block cache functionality.
 17  
 18  use alphavm_ledger::{
 19      Ledger,
 20      test_helpers::{CurrentConsensusStore, CurrentLedger, CurrentNetwork, LedgerType},
 21  };
 22  
 23  use alpha_std::StorageMode;
 24  use alphavm_console::{account::PrivateKey, prelude::*};
 25  use alphavm_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  }