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,
240 pub height: u64,
242 pub time: u64,
244 pub previous_block_hash: Option<BlockHash>,
246}
247
248pub struct BlockHeaderCache {
250 by_hash: Arc<RwLock<HashMap<BlockHash, CachedEntry<BlockHeader>>>>,
251 by_height: Arc<RwLock<HashMap<u64, CachedEntry<BlockHeader>>>>,
252 config: CacheConfig,
253}
254
255impl BlockHeaderCache {
256 pub fn new(config: CacheConfig) -> Self {
258 Self {
259 by_hash: Arc::new(RwLock::new(HashMap::new())),
260 by_height: Arc::new(RwLock::new(HashMap::new())),
261 config,
262 }
263 }
264
265 pub async fn get_by_hash(&self, hash: &BlockHash) -> Option<BlockHeader> {
267 let cache = self.by_hash.read().await;
268 if let Some(entry) = cache.get(hash) {
269 if !entry.is_expired() {
270 trace!(hash = %hash, "Block header cache hit (by hash)");
271 return Some(entry.value.clone());
272 }
273 }
274 trace!(hash = %hash, "Block header cache miss (by hash)");
275 None
276 }
277
278 pub async fn get_by_height(&self, height: u64) -> Option<BlockHeader> {
280 let cache = self.by_height.read().await;
281 if let Some(entry) = cache.get(&height) {
282 if !entry.is_expired() {
283 trace!(height = height, "Block header cache hit (by height)");
284 return Some(entry.value.clone());
285 }
286 }
287 trace!(height = height, "Block header cache miss (by height)");
288 None
289 }
290
291 pub async fn insert(&self, header: BlockHeader) {
293 let mut by_hash = self.by_hash.write().await;
294 let mut by_height = self.by_height.write().await;
295
296 if by_hash.len() >= self.config.max_block_headers {
298 Self::evict_expired(&mut by_hash);
299 Self::evict_expired_height(&mut by_height);
300
301 if by_hash.len() >= self.config.max_block_headers {
303 if let Some(oldest_key) = by_hash.keys().next().copied() {
304 by_hash.remove(&oldest_key);
305 }
306 }
307 if by_height.len() >= self.config.max_block_headers {
308 if let Some(oldest_key) = by_height.keys().next().copied() {
309 by_height.remove(&oldest_key);
310 }
311 }
312 }
313
314 let hash = header.hash;
315 let height = header.height;
316
317 by_hash.insert(
318 hash,
319 CachedEntry::new(header.clone(), self.config.block_header_ttl),
320 );
321 by_height.insert(
322 height,
323 CachedEntry::new(header, self.config.block_header_ttl),
324 );
325
326 trace!(hash = %hash, height = height, "Block header cached");
327 }
328
329 pub async fn invalidate(&self, hash: &BlockHash, height: u64) {
331 let mut by_hash = self.by_hash.write().await;
332 let mut by_height = self.by_height.write().await;
333
334 by_hash.remove(hash);
335 by_height.remove(&height);
336
337 debug!(hash = %hash, height = height, "Block header cache invalidated");
338 }
339
340 pub async fn clear(&self) {
342 let mut by_hash = self.by_hash.write().await;
343 let mut by_height = self.by_height.write().await;
344
345 by_hash.clear();
346 by_height.clear();
347
348 debug!("Block header cache cleared");
349 }
350
351 fn evict_expired(cache: &mut HashMap<BlockHash, CachedEntry<BlockHeader>>) {
353 cache.retain(|_, entry| !entry.is_expired());
354 }
355
356 fn evict_expired_height(cache: &mut HashMap<u64, CachedEntry<BlockHeader>>) {
358 cache.retain(|_, entry| !entry.is_expired());
359 }
360
361 pub async fn stats(&self) -> CacheStats {
363 let by_hash = self.by_hash.read().await;
364 let expired_count = by_hash.values().filter(|e| e.is_expired()).count();
365
366 CacheStats {
367 total_entries: by_hash.len(),
368 expired_entries: expired_count,
369 active_entries: by_hash.len() - expired_count,
370 max_entries: self.config.max_block_headers,
371 }
372 }
373}
374
375#[derive(Debug, Clone)]
377pub struct CacheStats {
378 pub total_entries: usize,
380 pub expired_entries: usize,
382 pub active_entries: usize,
384 pub max_entries: usize,
386}
387
388impl CacheStats {
389 pub fn utilization(&self) -> f64 {
391 if self.max_entries == 0 {
392 0.0
393 } else {
394 self.total_entries as f64 / self.max_entries as f64
395 }
396 }
397}
398
399pub struct CacheManager {
401 pub transactions: TransactionCache,
403 pub utxos: UtxoCache,
405 pub block_headers: BlockHeaderCache,
407 #[allow(dead_code)]
408 config: CacheConfig,
409}
410
411impl CacheManager {
412 pub fn new(config: CacheConfig) -> Self {
414 Self {
415 transactions: TransactionCache::new(config.clone()),
416 utxos: UtxoCache::new(config.clone()),
417 block_headers: BlockHeaderCache::new(config.clone()),
418 config,
419 }
420 }
421
422 pub fn with_defaults() -> Self {
424 Self::new(CacheConfig::default())
425 }
426
427 pub async fn clear_all(&self) {
429 self.transactions.clear().await;
430 self.utxos.clear().await;
431 self.block_headers.clear().await;
432 debug!("All caches cleared");
433 }
434
435 pub async fn overall_stats(&self) -> OverallCacheStats {
437 OverallCacheStats {
438 transaction_stats: self.transactions.stats().await,
439 utxo_stats: self.utxos.stats().await,
440 block_header_stats: self.block_headers.stats().await,
441 }
442 }
443}
444
445#[derive(Debug, Clone)]
447pub struct OverallCacheStats {
448 pub transaction_stats: CacheStats,
450 pub utxo_stats: CacheStats,
452 pub block_header_stats: CacheStats,
454}
455
456#[cfg(test)]
457mod tests {
458 use super::*;
459 use bitcoin::hashes::Hash;
460
461 #[tokio::test]
462 async fn test_cache_config_defaults() {
463 let config = CacheConfig::default();
464 assert!(config.transaction_ttl.as_secs() > 0);
465 assert!(config.max_transactions > 0);
466 }
467
468 #[tokio::test]
469 async fn test_transaction_cache() {
470 let config = CacheConfig {
471 transaction_ttl: Duration::from_secs(1),
472 max_transactions: 2,
473 ..Default::default()
474 };
475
476 let cache = TransactionCache::new(config);
477 let txid = Txid::all_zeros();
478
479 assert!(cache.get(&txid).await.is_none());
481
482 }
485
486 #[tokio::test]
487 async fn test_utxo_cache() {
488 let config = CacheConfig {
489 utxo_ttl: Duration::from_secs(1),
490 max_utxos: 2,
491 ..Default::default()
492 };
493
494 let cache = UtxoCache::new(config);
495 let address = "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh";
496
497 assert!(cache.get(address).await.is_none());
499
500 let utxos = vec![];
502 cache.insert(address.to_string(), utxos.clone()).await;
503 assert_eq!(cache.get(address).await, Some(utxos));
504
505 cache.invalidate(address).await;
507 assert!(cache.get(address).await.is_none());
508 }
509
510 #[tokio::test]
511 async fn test_block_header_cache() {
512 let config = CacheConfig {
513 block_header_ttl: Duration::from_secs(1),
514 max_block_headers: 2,
515 ..Default::default()
516 };
517
518 let cache = BlockHeaderCache::new(config);
519 let hash = BlockHash::all_zeros();
520 let header = BlockHeader {
521 hash,
522 height: 100,
523 time: 1234567890,
524 previous_block_hash: None,
525 };
526
527 assert!(cache.get_by_hash(&hash).await.is_none());
529 assert!(cache.get_by_height(100).await.is_none());
530
531 cache.insert(header.clone()).await;
533 assert!(cache.get_by_hash(&hash).await.is_some());
534 assert!(cache.get_by_height(100).await.is_some());
535
536 cache.invalidate(&hash, 100).await;
538 assert!(cache.get_by_hash(&hash).await.is_none());
539 assert!(cache.get_by_height(100).await.is_none());
540 }
541
542 #[tokio::test]
543 async fn test_cache_manager() {
544 let manager = CacheManager::with_defaults();
545 manager.clear_all().await;
546
547 let stats = manager.overall_stats().await;
548 assert_eq!(stats.transaction_stats.total_entries, 0);
549 assert_eq!(stats.utxo_stats.total_entries, 0);
550 assert_eq!(stats.block_header_stats.total_entries, 0);
551 }
552
553 #[test]
554 fn test_cache_stats_utilization() {
555 let stats = CacheStats {
556 total_entries: 50,
557 expired_entries: 10,
558 active_entries: 40,
559 max_entries: 100,
560 };
561
562 assert_eq!(stats.utilization(), 0.5);
563 }
564}