synor/crates/synor-storage/src/cache.rs
Gulshan Yadav 48949ebb3f Initial commit: Synor blockchain monorepo
A complete blockchain implementation featuring:
- synord: Full node with GHOSTDAG consensus
- explorer-web: Modern React blockchain explorer with 3D DAG visualization
- CLI wallet and tools
- Smart contract SDK and example contracts (DEX, NFT, token)
- WASM crypto library for browser/mobile
2026-01-08 05:22:17 +05:30

552 lines
16 KiB
Rust

//! Caching layer for Synor storage.
//!
//! Provides LRU caches for frequently accessed data to reduce disk I/O.
//!
//! OPTIMIZATION: Uses the `lru` crate for O(1) get/put operations instead of
//! the O(n) Vec-based implementation. This significantly improves performance
//! for high-throughput scenarios.
use parking_lot::RwLock;
use std::hash::Hash;
use std::num::NonZeroUsize;
use synor_types::{BlockHeader, BlockId, Hash256, Transaction, TransactionId};
use crate::stores::{StoredGhostdagData, StoredRelations, StoredUtxo};
/// A wrapper around the `lru` crate's LruCache with our API.
///
/// This provides O(1) get and put operations using a doubly-linked list
/// with a HashMap for fast lookups.
pub struct LruCache<K, V> {
inner: lru::LruCache<K, V>,
}
impl<K: Hash + Eq, V: Clone> LruCache<K, V> {
/// Creates a new LRU cache with the given capacity.
pub fn new(capacity: usize) -> Self {
let cap = NonZeroUsize::new(capacity.max(1)).unwrap();
LruCache {
inner: lru::LruCache::new(cap),
}
}
/// Gets a value from the cache, updating access order.
/// O(1) operation.
pub fn get(&mut self, key: &K) -> Option<V> {
self.inner.get(key).cloned()
}
/// Peeks at a value without updating access order.
/// O(1) operation.
pub fn peek(&self, key: &K) -> Option<&V> {
self.inner.peek(key)
}
/// Inserts a value into the cache.
/// O(1) operation. Returns the evicted value if cache was at capacity.
pub fn insert(&mut self, key: K, value: V) -> Option<V> {
self.inner.push(key, value).map(|(_, v)| v)
}
/// Removes a value from the cache.
/// O(1) operation.
pub fn remove(&mut self, key: &K) -> Option<V> {
self.inner.pop(key)
}
/// Returns the number of entries in the cache.
pub fn len(&self) -> usize {
self.inner.len()
}
/// Returns true if the cache is empty.
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
/// Clears the cache.
pub fn clear(&mut self) {
self.inner.clear();
}
/// Returns true if the key is in the cache.
pub fn contains(&self, key: &K) -> bool {
self.inner.contains(key)
}
/// Returns the capacity of the cache.
pub fn capacity(&self) -> usize {
self.inner.cap().get()
}
}
/// Configuration for the storage cache.
#[derive(Clone, Debug)]
pub struct CacheConfig {
/// Maximum number of headers to cache.
pub header_cache_size: usize,
/// Maximum number of block bodies to cache.
pub block_cache_size: usize,
/// Maximum number of transactions to cache.
pub tx_cache_size: usize,
/// Maximum number of UTXOs to cache.
pub utxo_cache_size: usize,
/// Maximum number of GHOSTDAG entries to cache.
pub ghostdag_cache_size: usize,
/// Maximum number of relations to cache.
pub relations_cache_size: usize,
}
impl Default for CacheConfig {
fn default() -> Self {
CacheConfig {
header_cache_size: 10_000,
block_cache_size: 1_000,
tx_cache_size: 50_000,
utxo_cache_size: 100_000,
ghostdag_cache_size: 50_000,
relations_cache_size: 50_000,
}
}
}
impl CacheConfig {
/// Creates a configuration for low-memory environments.
pub fn low_memory() -> Self {
CacheConfig {
header_cache_size: 1_000,
block_cache_size: 100,
tx_cache_size: 5_000,
utxo_cache_size: 10_000,
ghostdag_cache_size: 5_000,
relations_cache_size: 5_000,
}
}
/// Creates a configuration for high-memory environments.
pub fn high_memory() -> Self {
CacheConfig {
header_cache_size: 100_000,
block_cache_size: 10_000,
tx_cache_size: 500_000,
utxo_cache_size: 1_000_000,
ghostdag_cache_size: 500_000,
relations_cache_size: 500_000,
}
}
}
/// UTXO cache key (txid + index).
#[derive(Clone, Hash, Eq, PartialEq)]
pub struct UtxoKey {
pub txid: TransactionId,
pub index: u32,
}
impl UtxoKey {
pub fn new(txid: TransactionId, index: u32) -> Self {
UtxoKey { txid, index }
}
}
/// Cache statistics.
#[derive(Clone, Debug, Default)]
pub struct CacheStats {
/// Number of cache hits.
pub hits: u64,
/// Number of cache misses.
pub misses: u64,
/// Number of insertions.
pub insertions: u64,
/// Number of evictions.
pub evictions: u64,
}
impl CacheStats {
/// Returns the hit ratio (0.0 to 1.0).
pub fn hit_ratio(&self) -> f64 {
let total = self.hits + self.misses;
if total == 0 {
0.0
} else {
self.hits as f64 / total as f64
}
}
}
/// Combined storage cache for all data types.
pub struct StorageCache {
/// Header cache.
headers: RwLock<LruCache<Hash256, BlockHeader>>,
/// Transaction cache.
transactions: RwLock<LruCache<TransactionId, Transaction>>,
/// UTXO cache.
utxos: RwLock<LruCache<UtxoKey, StoredUtxo>>,
/// GHOSTDAG data cache.
ghostdag: RwLock<LruCache<BlockId, StoredGhostdagData>>,
/// Relations cache.
relations: RwLock<LruCache<BlockId, StoredRelations>>,
/// Cache statistics.
stats: RwLock<CacheStats>,
/// Configuration.
config: CacheConfig,
}
impl StorageCache {
/// Creates a new storage cache with the given configuration.
pub fn new(config: CacheConfig) -> Self {
StorageCache {
headers: RwLock::new(LruCache::new(config.header_cache_size)),
transactions: RwLock::new(LruCache::new(config.tx_cache_size)),
utxos: RwLock::new(LruCache::new(config.utxo_cache_size)),
ghostdag: RwLock::new(LruCache::new(config.ghostdag_cache_size)),
relations: RwLock::new(LruCache::new(config.relations_cache_size)),
stats: RwLock::new(CacheStats::default()),
config,
}
}
/// Creates a new storage cache with default configuration.
pub fn default_config() -> Self {
Self::new(CacheConfig::default())
}
// Header cache operations
/// Gets a header from the cache.
pub fn get_header(&self, hash: &Hash256) -> Option<BlockHeader> {
let mut cache = self.headers.write();
let result = cache.get(hash);
let mut stats = self.stats.write();
if result.is_some() {
stats.hits += 1;
} else {
stats.misses += 1;
}
result
}
/// Inserts a header into the cache.
pub fn insert_header(&self, header: BlockHeader) {
let hash = header.block_id();
let mut cache = self.headers.write();
let was_full = cache.len() >= cache.capacity();
cache.insert(hash, header);
let mut stats = self.stats.write();
stats.insertions += 1;
if was_full {
stats.evictions += 1;
}
}
/// Removes a header from the cache.
pub fn remove_header(&self, hash: &Hash256) {
let mut cache = self.headers.write();
cache.remove(hash);
}
// Transaction cache operations
/// Gets a transaction from the cache.
pub fn get_transaction(&self, txid: &TransactionId) -> Option<Transaction> {
let mut cache = self.transactions.write();
let result = cache.get(txid);
let mut stats = self.stats.write();
if result.is_some() {
stats.hits += 1;
} else {
stats.misses += 1;
}
result
}
/// Inserts a transaction into the cache.
pub fn insert_transaction(&self, tx: Transaction) {
let txid = tx.txid();
let mut cache = self.transactions.write();
cache.insert(txid, tx);
self.stats.write().insertions += 1;
}
/// Removes a transaction from the cache.
pub fn remove_transaction(&self, txid: &TransactionId) {
let mut cache = self.transactions.write();
cache.remove(txid);
}
// UTXO cache operations
/// Gets a UTXO from the cache.
pub fn get_utxo(&self, txid: &TransactionId, index: u32) -> Option<StoredUtxo> {
let key = UtxoKey::new(*txid, index);
let mut cache = self.utxos.write();
let result = cache.get(&key);
let mut stats = self.stats.write();
if result.is_some() {
stats.hits += 1;
} else {
stats.misses += 1;
}
result
}
/// Inserts a UTXO into the cache.
pub fn insert_utxo(&self, txid: TransactionId, index: u32, utxo: StoredUtxo) {
let key = UtxoKey::new(txid, index);
let mut cache = self.utxos.write();
cache.insert(key, utxo);
self.stats.write().insertions += 1;
}
/// Removes a UTXO from the cache.
pub fn remove_utxo(&self, txid: &TransactionId, index: u32) {
let key = UtxoKey::new(*txid, index);
let mut cache = self.utxos.write();
cache.remove(&key);
}
// GHOSTDAG cache operations
/// Gets GHOSTDAG data from the cache.
pub fn get_ghostdag(&self, block_id: &BlockId) -> Option<StoredGhostdagData> {
let mut cache = self.ghostdag.write();
let result = cache.get(block_id);
let mut stats = self.stats.write();
if result.is_some() {
stats.hits += 1;
} else {
stats.misses += 1;
}
result
}
/// Inserts GHOSTDAG data into the cache.
pub fn insert_ghostdag(&self, block_id: BlockId, data: StoredGhostdagData) {
let mut cache = self.ghostdag.write();
cache.insert(block_id, data);
self.stats.write().insertions += 1;
}
/// Removes GHOSTDAG data from the cache.
pub fn remove_ghostdag(&self, block_id: &BlockId) {
let mut cache = self.ghostdag.write();
cache.remove(block_id);
}
// Relations cache operations
/// Gets relations from the cache.
pub fn get_relations(&self, block_id: &BlockId) -> Option<StoredRelations> {
let mut cache = self.relations.write();
let result = cache.get(block_id);
let mut stats = self.stats.write();
if result.is_some() {
stats.hits += 1;
} else {
stats.misses += 1;
}
result
}
/// Inserts relations into the cache.
pub fn insert_relations(&self, block_id: BlockId, relations: StoredRelations) {
let mut cache = self.relations.write();
cache.insert(block_id, relations);
self.stats.write().insertions += 1;
}
/// Removes relations from the cache.
pub fn remove_relations(&self, block_id: &BlockId) {
let mut cache = self.relations.write();
cache.remove(block_id);
}
// Statistics and management
/// Returns cache statistics.
pub fn stats(&self) -> CacheStats {
self.stats.read().clone()
}
/// Resets cache statistics.
pub fn reset_stats(&self) {
*self.stats.write() = CacheStats::default();
}
/// Returns the total number of cached entries.
pub fn total_entries(&self) -> usize {
self.headers.read().len()
+ self.transactions.read().len()
+ self.utxos.read().len()
+ self.ghostdag.read().len()
+ self.relations.read().len()
}
/// Clears all caches.
pub fn clear(&self) {
self.headers.write().clear();
self.transactions.write().clear();
self.utxos.write().clear();
self.ghostdag.write().clear();
self.relations.write().clear();
}
/// Returns the configuration.
pub fn config(&self) -> &CacheConfig {
&self.config
}
/// Returns cache size information.
pub fn size_info(&self) -> CacheSizeInfo {
CacheSizeInfo {
headers: self.headers.read().len(),
headers_capacity: self.config.header_cache_size,
transactions: self.transactions.read().len(),
transactions_capacity: self.config.tx_cache_size,
utxos: self.utxos.read().len(),
utxos_capacity: self.config.utxo_cache_size,
ghostdag: self.ghostdag.read().len(),
ghostdag_capacity: self.config.ghostdag_cache_size,
relations: self.relations.read().len(),
relations_capacity: self.config.relations_cache_size,
}
}
}
impl Default for StorageCache {
fn default() -> Self {
Self::default_config()
}
}
/// Cache size information.
#[derive(Clone, Debug)]
pub struct CacheSizeInfo {
pub headers: usize,
pub headers_capacity: usize,
pub transactions: usize,
pub transactions_capacity: usize,
pub utxos: usize,
pub utxos_capacity: usize,
pub ghostdag: usize,
pub ghostdag_capacity: usize,
pub relations: usize,
pub relations_capacity: usize,
}
impl CacheSizeInfo {
/// Returns the total number of cached entries.
pub fn total_entries(&self) -> usize {
self.headers + self.transactions + self.utxos + self.ghostdag + self.relations
}
/// Returns the total capacity.
pub fn total_capacity(&self) -> usize {
self.headers_capacity
+ self.transactions_capacity
+ self.utxos_capacity
+ self.ghostdag_capacity
+ self.relations_capacity
}
/// Returns the overall fill ratio.
pub fn fill_ratio(&self) -> f64 {
self.total_entries() as f64 / self.total_capacity() as f64
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lru_cache_basic() {
let mut cache: LruCache<i32, String> = LruCache::new(3);
cache.insert(1, "one".to_string());
cache.insert(2, "two".to_string());
cache.insert(3, "three".to_string());
assert_eq!(cache.get(&1), Some("one".to_string()));
assert_eq!(cache.get(&2), Some("two".to_string()));
assert_eq!(cache.len(), 3);
}
#[test]
fn test_lru_cache_eviction() {
let mut cache: LruCache<i32, String> = LruCache::new(3);
cache.insert(1, "one".to_string());
cache.insert(2, "two".to_string());
cache.insert(3, "three".to_string());
cache.insert(4, "four".to_string()); // Should evict 1
assert_eq!(cache.get(&1), None);
assert_eq!(cache.get(&4), Some("four".to_string()));
assert_eq!(cache.len(), 3);
}
#[test]
fn test_lru_cache_access_order() {
let mut cache: LruCache<i32, String> = LruCache::new(3);
cache.insert(1, "one".to_string());
cache.insert(2, "two".to_string());
cache.insert(3, "three".to_string());
// Access 1, making it most recently used
cache.get(&1);
// Insert 4, should evict 2 (least recently used)
cache.insert(4, "four".to_string());
assert_eq!(cache.get(&1), Some("one".to_string()));
assert_eq!(cache.get(&2), None);
assert_eq!(cache.get(&3), Some("three".to_string()));
assert_eq!(cache.get(&4), Some("four".to_string()));
}
#[test]
fn test_storage_cache_stats() {
let cache = StorageCache::new(CacheConfig {
header_cache_size: 10,
block_cache_size: 10,
tx_cache_size: 10,
utxo_cache_size: 10,
ghostdag_cache_size: 10,
relations_cache_size: 10,
});
// Miss
let _ = cache.get_utxo(&TransactionId::from_bytes([1u8; 32]), 0);
assert_eq!(cache.stats().misses, 1);
// Insert and hit
cache.insert_utxo(
TransactionId::from_bytes([1u8; 32]),
0,
StoredUtxo {
amount: 1000,
script_pubkey: vec![],
block_daa_score: 0,
is_coinbase: false,
},
);
let _ = cache.get_utxo(&TransactionId::from_bytes([1u8; 32]), 0);
let stats = cache.stats();
assert_eq!(stats.hits, 1);
assert_eq!(stats.misses, 1);
assert_eq!(stats.insertions, 1);
}
#[test]
fn test_cache_size_info() {
let cache = StorageCache::default_config();
let info = cache.size_info();
assert_eq!(info.total_entries(), 0);
assert!(info.total_capacity() > 0);
assert_eq!(info.fill_ratio(), 0.0);
}
}