Skip to main content

cory_core/
cache.rs

1//! In-memory caches for decoded transactions and resolved prevout data.
2//!
3//! The cache is shared across concurrent graph-building tasks via
4//! `Arc<Cache>`.
5
6use bitcoin::Txid;
7use quick_cache::sync::Cache as QuickCache;
8
9use crate::types::{TxNode, TxOutput};
10
11// ==============================================================================
12// Default Capacity
13// ==============================================================================
14
15/// Default maximum number of cached transactions.
16const DEFAULT_TX_CAPACITY: usize = 20_000;
17
18/// Default maximum number of cached prevout entries.
19const DEFAULT_PREVOUT_CAPACITY: usize = 100_000;
20
21// ==============================================================================
22// Cache
23// ==============================================================================
24
25/// In-memory caches for decoded transactions and resolved prevouts.
26///
27/// Shared across the graph builder and server via `Arc<Cache>`.
28/// Uses a concurrent quick cache implementation so lookups and inserts
29/// do not require an external async mutex.
30/// Entries are evicted automatically once the configured capacities are reached.
31pub struct Cache {
32    transactions: QuickCache<Txid, TxNode>,
33    prevouts: QuickCache<(Txid, u32), TxOutput>,
34}
35
36impl Cache {
37    /// Create a cache with the default capacities
38    pub fn new() -> Self {
39        Self::with_capacity(DEFAULT_TX_CAPACITY, DEFAULT_PREVOUT_CAPACITY)
40    }
41
42    /// Create a cache with explicit capacities. Both values must be > 0.
43    pub fn with_capacity(tx_cap: usize, prevout_cap: usize) -> Self {
44        assert!(tx_cap > 0, "tx capacity must be > 0");
45        assert!(prevout_cap > 0, "prevout capacity must be > 0");
46
47        Self {
48            transactions: QuickCache::new(tx_cap),
49            prevouts: QuickCache::new(prevout_cap),
50        }
51    }
52
53    /// Look up a cached transaction by txid.
54    pub async fn get_tx(&self, txid: &Txid) -> Option<TxNode> {
55        self.transactions.get(txid)
56    }
57
58    /// Insert a decoded transaction into the cache.
59    pub async fn insert_tx(&self, txid: Txid, node: TxNode) {
60        self.transactions.insert(txid, node);
61    }
62
63    /// Look up cached prevout info for a specific outpoint.
64    pub async fn get_prevout(&self, txid: &Txid, vout: u32) -> Option<TxOutput> {
65        self.prevouts.get(&(*txid, vout))
66    }
67
68    /// Cache resolved prevout data for a specific outpoint.
69    pub async fn insert_prevout(&self, txid: Txid, vout: u32, info: TxOutput) {
70        self.prevouts.insert((txid, vout), info);
71    }
72}
73
74impl Default for Cache {
75    fn default() -> Self {
76        Self::new()
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83    use crate::test_util::{make_output, make_tx_node, txid_from_byte};
84
85    #[tokio::test]
86    async fn cache_returns_none_for_unknown_txid() {
87        let cache = Cache::new();
88        assert!(cache.get_tx(&txid_from_byte(1)).await.is_none());
89    }
90
91    #[tokio::test]
92    async fn cache_returns_inserted_tx() {
93        let cache = Cache::new();
94        let txid = txid_from_byte(1);
95        let node = make_tx_node(vec![], vec![make_output(1000)], 100);
96        cache.insert_tx(txid, node.clone()).await;
97
98        let cached = cache.get_tx(&txid).await.expect("should be cached");
99        assert_eq!(cached.txid, node.txid);
100    }
101
102    #[tokio::test]
103    async fn cache_evicts_lru_entry() {
104        // Capacity of 2: inserting a third entry should evict one older entry.
105        // quick_cache does not guarantee strict LRU victim selection.
106        let cache = Cache::with_capacity(2, 1);
107        let txid_a = txid_from_byte(1);
108        let txid_b = txid_from_byte(2);
109        let txid_c = txid_from_byte(3);
110
111        let node = make_tx_node(vec![], vec![make_output(1000)], 100);
112        cache.insert_tx(txid_a, node.clone()).await;
113        cache.insert_tx(txid_b, node.clone()).await;
114        cache.insert_tx(txid_c, node.clone()).await;
115
116        assert!(
117            cache.get_tx(&txid_a).await.is_none() || cache.get_tx(&txid_b).await.is_none(),
118            "one of the two older entries should be evicted"
119        );
120        assert!(cache.get_tx(&txid_c).await.is_some());
121    }
122
123    #[tokio::test]
124    async fn prevout_cache_hit_and_miss() {
125        let cache = Cache::new();
126        let txid = txid_from_byte(1);
127
128        assert!(cache.get_prevout(&txid, 0).await.is_none());
129
130        let info = make_output(5000);
131        cache.insert_prevout(txid, 0, info.clone()).await;
132
133        let cached = cache.get_prevout(&txid, 0).await.expect("should be cached");
134        assert_eq!(cached.value, info.value);
135
136        // Different vout should miss.
137        assert!(cache.get_prevout(&txid, 1).await.is_none());
138    }
139}