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
552 lines
16 KiB
Rust
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);
|
|
}
|
|
}
|