1use bitcoin::{BlockHash, Txid};
4use bitcoincore_rpc::json::GetRawTransactionResult;
5use std::collections::HashMap;
6use std::sync::Arc;
7use std::time::{Duration, Instant};
8use tokio::sync::RwLock;
9use tracing::{debug, trace};
10
11use crate::utxo::Utxo;
12
13#[derive(Debug, Clone)]
15pub struct CacheConfig {
16 pub transaction_ttl: Duration,
18 pub utxo_ttl: Duration,
20 pub block_header_ttl: Duration,
22 pub max_transactions: usize,
24 pub max_utxos: usize,
26 pub max_block_headers: usize,
28}
29
30impl Default for CacheConfig {
31 fn default() -> Self {
32 Self {
33 transaction_ttl: Duration::from_secs(300), utxo_ttl: Duration::from_secs(60), block_header_ttl: Duration::from_secs(600), max_transactions: 1000,
37 max_utxos: 5000,
38 max_block_headers: 500,
39 }
40 }
41}
42
43#[derive(Debug, Clone)]
45struct CachedEntry<T> {
46 value: T,
47 expires_at: Instant,
48}
49
50impl<T> CachedEntry<T> {
51 fn new(value: T, ttl: Duration) -> Self {
52 Self {
53 value,
54 expires_at: Instant::now() + ttl,
55 }
56 }
57
58 fn is_expired(&self) -> bool {
59 Instant::now() > self.expires_at
60 }
61}
62
63pub struct TransactionCache {
65 cache: Arc<RwLock<HashMap<Txid, CachedEntry<GetRawTransactionResult>>>>,
66 config: CacheConfig,
67}
68
69impl TransactionCache {
70 pub fn new(config: CacheConfig) -> Self {
72 Self {
73 cache: Arc::new(RwLock::new(HashMap::new())),
74 config,
75 }
76 }
77
78 pub async fn get(&self, txid: &Txid) -> Option<GetRawTransactionResult> {
80 let cache = self.cache.read().await;
81 if let Some(entry) = cache.get(txid) {
82 if !entry.is_expired() {
83 trace!(txid = %txid, "Transaction cache hit");
84 return Some(entry.value.clone());
85 }
86 }
87 trace!(txid = %txid, "Transaction cache miss");
88 None
89 }
90
91 pub async fn insert(&self, txid: Txid, tx: GetRawTransactionResult) {
93 let mut cache = self.cache.write().await;
94
95 if cache.len() >= self.config.max_transactions {
97 self.evict_expired(&mut cache);
98
99 if cache.len() >= self.config.max_transactions {
101 if let Some(oldest_key) = cache.keys().next().copied() {
102 cache.remove(&oldest_key);
103 }
104 }
105 }
106
107 cache.insert(txid, CachedEntry::new(tx, self.config.transaction_ttl));
108 trace!(txid = %txid, "Transaction cached");
109 }
110
111 pub async fn invalidate(&self, txid: &Txid) {
113 let mut cache = self.cache.write().await;
114 cache.remove(txid);
115 debug!(txid = %txid, "Transaction cache invalidated");
116 }
117
118 pub async fn clear(&self) {
120 let mut cache = self.cache.write().await;
121 cache.clear();
122 debug!("Transaction cache cleared");
123 }
124
125 fn evict_expired(&self, cache: &mut HashMap<Txid, CachedEntry<GetRawTransactionResult>>) {
127 cache.retain(|_, entry| !entry.is_expired());
128 }
129
130 pub async fn stats(&self) -> CacheStats {
132 let cache = self.cache.read().await;
133 let expired_count = cache.values().filter(|e| e.is_expired()).count();
134
135 CacheStats {
136 total_entries: cache.len(),
137 expired_entries: expired_count,
138 active_entries: cache.len() - expired_count,
139 max_entries: self.config.max_transactions,
140 }
141 }
142}
143
144pub struct UtxoCache {
146 cache: Arc<RwLock<HashMap<String, CachedEntry<Vec<Utxo>>>>>,
147 config: CacheConfig,
148}
149
150impl UtxoCache {
151 pub fn new(config: CacheConfig) -> Self {
153 Self {
154 cache: Arc::new(RwLock::new(HashMap::new())),
155 config,
156 }
157 }
158
159 pub async fn get(&self, address: &str) -> Option<Vec<Utxo>> {
161 let cache = self.cache.read().await;
162 if let Some(entry) = cache.get(address) {
163 if !entry.is_expired() {
164 trace!(address = address, "UTXO cache hit");
165 return Some(entry.value.clone());
166 }
167 }
168 trace!(address = address, "UTXO cache miss");
169 None
170 }
171
172 pub async fn insert(&self, address: String, utxos: Vec<Utxo>) {
174 let mut cache = self.cache.write().await;
175
176 if cache.len() >= self.config.max_utxos {
178 self.evict_expired(&mut cache);
179
180 if cache.len() >= self.config.max_utxos {
182 if let Some(oldest_key) = cache.keys().next().cloned() {
183 cache.remove(&oldest_key);
184 }
185 }
186 }
187
188 cache.insert(
189 address.clone(),
190 CachedEntry::new(utxos, self.config.utxo_ttl),
191 );
192 trace!(address = address, "UTXOs cached");
193 }
194
195 pub async fn invalidate(&self, address: &str) {
197 let mut cache = self.cache.write().await;
198 cache.remove(address);
199 debug!(address = address, "UTXO cache invalidated");
200 }
201
202 pub async fn invalidate_all(&self) {
204 let mut cache = self.cache.write().await;
205 cache.clear();
206 debug!("All UTXO cache invalidated");
207 }
208
209 pub async fn clear(&self) {
211 let mut cache = self.cache.write().await;
212 cache.clear();
213 debug!("UTXO cache cleared");
214 }
215
216 fn evict_expired(&self, cache: &mut HashMap<String, CachedEntry<Vec<Utxo>>>) {
218 cache.retain(|_, entry| !entry.is_expired());
219 }
220
221 pub async fn stats(&self) -> CacheStats {
223 let cache = self.cache.read().await;
224 let expired_count = cache.values().filter(|e| e.is_expired()).count();
225
226 CacheStats {
227 total_entries: cache.len(),
228 expired_entries: expired_count,
229 active_entries: cache.len() - expired_count,
230 max_entries: self.config.max_utxos,
231 }
232 }
233}
234
235#[derive(Debug, Clone)]
237pub struct BlockHeader {
238 pub hash: BlockHash,
239 pub height: u64,
240 pub time: u64,
241 pub previous_block_hash: Option<BlockHash>,
242}
243
244pub struct BlockHeaderCache {
245 by_hash: Arc<RwLock<HashMap<BlockHash, CachedEntry<BlockHeader>>>>,
246 by_height: Arc<RwLock<HashMap<u64, CachedEntry<BlockHeader>>>>,
247 config: CacheConfig,
248}
249
250impl BlockHeaderCache {
251 pub fn new(config: CacheConfig) -> Self {
253 Self {
254 by_hash: Arc::new(RwLock::new(HashMap::new())),
255 by_height: Arc::new(RwLock::new(HashMap::new())),
256 config,
257 }
258 }
259
260 pub async fn get_by_hash(&self, hash: &BlockHash) -> Option<BlockHeader> {
262 let cache = self.by_hash.read().await;
263 if let Some(entry) = cache.get(hash) {
264 if !entry.is_expired() {
265 trace!(hash = %hash, "Block header cache hit (by hash)");
266 return Some(entry.value.clone());
267 }
268 }
269 trace!(hash = %hash, "Block header cache miss (by hash)");
270 None
271 }
272
273 pub async fn get_by_height(&self, height: u64) -> Option<BlockHeader> {
275 let cache = self.by_height.read().await;
276 if let Some(entry) = cache.get(&height) {
277 if !entry.is_expired() {
278 trace!(height = height, "Block header cache hit (by height)");
279 return Some(entry.value.clone());
280 }
281 }
282 trace!(height = height, "Block header cache miss (by height)");
283 None
284 }
285
286 pub async fn insert(&self, header: BlockHeader) {
288 let mut by_hash = self.by_hash.write().await;
289 let mut by_height = self.by_height.write().await;
290
291 if by_hash.len() >= self.config.max_block_headers {
293 Self::evict_expired(&mut by_hash);
294 Self::evict_expired_height(&mut by_height);
295
296 if by_hash.len() >= self.config.max_block_headers {
298 if let Some(oldest_key) = by_hash.keys().next().copied() {
299 by_hash.remove(&oldest_key);
300 }
301 }
302 if by_height.len() >= self.config.max_block_headers {
303 if let Some(oldest_key) = by_height.keys().next().copied() {
304 by_height.remove(&oldest_key);
305 }
306 }
307 }
308
309 let hash = header.hash;
310 let height = header.height;
311
312 by_hash.insert(
313 hash,
314 CachedEntry::new(header.clone(), self.config.block_header_ttl),
315 );
316 by_height.insert(
317 height,
318 CachedEntry::new(header, self.config.block_header_ttl),
319 );
320
321 trace!(hash = %hash, height = height, "Block header cached");
322 }
323
324 pub async fn invalidate(&self, hash: &BlockHash, height: u64) {
326 let mut by_hash = self.by_hash.write().await;
327 let mut by_height = self.by_height.write().await;
328
329 by_hash.remove(hash);
330 by_height.remove(&height);
331
332 debug!(hash = %hash, height = height, "Block header cache invalidated");
333 }
334
335 pub async fn clear(&self) {
337 let mut by_hash = self.by_hash.write().await;
338 let mut by_height = self.by_height.write().await;
339
340 by_hash.clear();
341 by_height.clear();
342
343 debug!("Block header cache cleared");
344 }
345
346 fn evict_expired(cache: &mut HashMap<BlockHash, CachedEntry<BlockHeader>>) {
348 cache.retain(|_, entry| !entry.is_expired());
349 }
350
351 fn evict_expired_height(cache: &mut HashMap<u64, CachedEntry<BlockHeader>>) {
353 cache.retain(|_, entry| !entry.is_expired());
354 }
355
356 pub async fn stats(&self) -> CacheStats {
358 let by_hash = self.by_hash.read().await;
359 let expired_count = by_hash.values().filter(|e| e.is_expired()).count();
360
361 CacheStats {
362 total_entries: by_hash.len(),
363 expired_entries: expired_count,
364 active_entries: by_hash.len() - expired_count,
365 max_entries: self.config.max_block_headers,
366 }
367 }
368}
369
370#[derive(Debug, Clone)]
372pub struct CacheStats {
373 pub total_entries: usize,
374 pub expired_entries: usize,
375 pub active_entries: usize,
376 pub max_entries: usize,
377}
378
379impl CacheStats {
380 pub fn utilization(&self) -> f64 {
382 if self.max_entries == 0 {
383 0.0
384 } else {
385 self.total_entries as f64 / self.max_entries as f64
386 }
387 }
388}
389
390pub struct CacheManager {
392 pub transactions: TransactionCache,
393 pub utxos: UtxoCache,
394 pub block_headers: BlockHeaderCache,
395 #[allow(dead_code)]
396 config: CacheConfig,
397}
398
399impl CacheManager {
400 pub fn new(config: CacheConfig) -> Self {
402 Self {
403 transactions: TransactionCache::new(config.clone()),
404 utxos: UtxoCache::new(config.clone()),
405 block_headers: BlockHeaderCache::new(config.clone()),
406 config,
407 }
408 }
409
410 pub fn with_defaults() -> Self {
412 Self::new(CacheConfig::default())
413 }
414
415 pub async fn clear_all(&self) {
417 self.transactions.clear().await;
418 self.utxos.clear().await;
419 self.block_headers.clear().await;
420 debug!("All caches cleared");
421 }
422
423 pub async fn overall_stats(&self) -> OverallCacheStats {
425 OverallCacheStats {
426 transaction_stats: self.transactions.stats().await,
427 utxo_stats: self.utxos.stats().await,
428 block_header_stats: self.block_headers.stats().await,
429 }
430 }
431}
432
433#[derive(Debug, Clone)]
435pub struct OverallCacheStats {
436 pub transaction_stats: CacheStats,
437 pub utxo_stats: CacheStats,
438 pub block_header_stats: CacheStats,
439}
440
441#[cfg(test)]
442mod tests {
443 use super::*;
444 use bitcoin::hashes::Hash;
445
446 #[tokio::test]
447 async fn test_cache_config_defaults() {
448 let config = CacheConfig::default();
449 assert!(config.transaction_ttl.as_secs() > 0);
450 assert!(config.max_transactions > 0);
451 }
452
453 #[tokio::test]
454 async fn test_transaction_cache() {
455 let config = CacheConfig {
456 transaction_ttl: Duration::from_secs(1),
457 max_transactions: 2,
458 ..Default::default()
459 };
460
461 let cache = TransactionCache::new(config);
462 let txid = Txid::all_zeros();
463
464 assert!(cache.get(&txid).await.is_none());
466
467 }
470
471 #[tokio::test]
472 async fn test_utxo_cache() {
473 let config = CacheConfig {
474 utxo_ttl: Duration::from_secs(1),
475 max_utxos: 2,
476 ..Default::default()
477 };
478
479 let cache = UtxoCache::new(config);
480 let address = "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh";
481
482 assert!(cache.get(address).await.is_none());
484
485 let utxos = vec![];
487 cache.insert(address.to_string(), utxos.clone()).await;
488 assert_eq!(cache.get(address).await, Some(utxos));
489
490 cache.invalidate(address).await;
492 assert!(cache.get(address).await.is_none());
493 }
494
495 #[tokio::test]
496 async fn test_block_header_cache() {
497 let config = CacheConfig {
498 block_header_ttl: Duration::from_secs(1),
499 max_block_headers: 2,
500 ..Default::default()
501 };
502
503 let cache = BlockHeaderCache::new(config);
504 let hash = BlockHash::all_zeros();
505 let header = BlockHeader {
506 hash,
507 height: 100,
508 time: 1234567890,
509 previous_block_hash: None,
510 };
511
512 assert!(cache.get_by_hash(&hash).await.is_none());
514 assert!(cache.get_by_height(100).await.is_none());
515
516 cache.insert(header.clone()).await;
518 assert!(cache.get_by_hash(&hash).await.is_some());
519 assert!(cache.get_by_height(100).await.is_some());
520
521 cache.invalidate(&hash, 100).await;
523 assert!(cache.get_by_hash(&hash).await.is_none());
524 assert!(cache.get_by_height(100).await.is_none());
525 }
526
527 #[tokio::test]
528 async fn test_cache_manager() {
529 let manager = CacheManager::with_defaults();
530 manager.clear_all().await;
531
532 let stats = manager.overall_stats().await;
533 assert_eq!(stats.transaction_stats.total_entries, 0);
534 assert_eq!(stats.utxo_stats.total_entries, 0);
535 assert_eq!(stats.block_header_stats.total_entries, 0);
536 }
537
538 #[test]
539 fn test_cache_stats_utilization() {
540 let stats = CacheStats {
541 total_entries: 50,
542 expired_entries: 10,
543 active_entries: 40,
544 max_entries: 100,
545 };
546
547 assert_eq!(stats.utilization(), 0.5);
548 }
549}