subcoin_mempool/
coins_view.rs

1//! UTXO cache layer for mempool validation.
2//!
3//! Implements a dual-layer cache:
4//! - Base layer: UTXOs from the blockchain (invalidated on block import)
5//! - Overlay: UTXOs created by pending mempool transactions (never cleared on block import)
6
7use crate::error::MempoolError;
8use bitcoin::{OutPoint as COutPoint, Transaction};
9use sc_client_api::HeaderBackend;
10use schnellru::{ByLength, LruMap};
11use sp_api::ProvideRuntimeApi;
12use sp_runtime::traits::Block as BlockT;
13use std::collections::{HashMap, HashSet};
14use std::marker::PhantomData;
15use std::sync::Arc;
16use subcoin_primitives::{MEMPOOL_HEIGHT, PoolCoin};
17use subcoin_runtime_primitives::SubcoinApi;
18
19/// In-memory UTXO cache with Substrate runtime backend.
20///
21/// Uses a dual-layer design to handle both blockchain UTXOs and mempool-created coins:
22/// - `base_cache`: LRU cache of UTXOs from the blockchain (cleared on block import)
23/// - `mempool_overlay`: Coins created by pending transactions (preserved across blocks)
24/// - `mempool_spends`: Outputs spent by pending transactions
25pub struct CoinsViewCache<Block: BlockT, Client> {
26    /// Base layer: coins from blockchain (invalidated on block import).
27    base_cache: LruMap<COutPoint, Option<PoolCoin>, ByLength>,
28
29    /// Overlay: coins created by mempool transactions (never cleared).
30    mempool_overlay: HashMap<COutPoint, PoolCoin>,
31
32    /// Outputs spent by mempool transactions.
33    mempool_spends: HashSet<COutPoint>,
34
35    /// Best block hash (for runtime queries).
36    best_block: Block::Hash,
37
38    /// Substrate client for runtime API calls.
39    client: Arc<Client>,
40
41    _phantom: PhantomData<Block>,
42}
43
44impl<Block, Client> CoinsViewCache<Block, Client>
45where
46    Block: BlockT,
47    Client: ProvideRuntimeApi<Block> + HeaderBackend<Block> + Send + Sync,
48    Client::Api: subcoin_runtime_primitives::SubcoinApi<Block>,
49{
50    /// Create a new coins view cache.
51    ///
52    /// # Arguments
53    /// * `client` - Substrate client for runtime API access
54    /// * `cache_size` - Maximum number of base layer entries to cache
55    pub fn new(client: Arc<Client>, cache_size: u32) -> Self {
56        let best_block = client.info().best_hash;
57
58        Self {
59            base_cache: LruMap::new(ByLength::new(cache_size)),
60            mempool_overlay: HashMap::new(),
61            mempool_spends: HashSet::new(),
62            best_block,
63            client,
64            _phantom: PhantomData,
65        }
66    }
67
68    /// Get coin with overlay priority.
69    ///
70    /// Query order:
71    /// 1. Check if spent by mempool
72    /// 2. Check mempool overlay (created by pending txs)
73    /// 3. Check base cache
74    /// 4. Query runtime and cache result
75    pub fn get_coin(&mut self, outpoint: &COutPoint) -> Result<Option<PoolCoin>, MempoolError> {
76        // 1. Check mempool spends first
77        if self.mempool_spends.contains(outpoint) {
78            return Ok(None);
79        }
80
81        // 2. Check mempool overlay (created by pending transactions)
82        if let Some(coin) = self.mempool_overlay.get(outpoint) {
83            return Ok(Some(coin.clone()));
84        }
85
86        // 3. Check base cache
87        if let Some(cached) = self.base_cache.peek(outpoint) {
88            return Ok(cached.clone());
89        }
90
91        // 4. Fetch from runtime (single query)
92        let coin = self.fetch_from_runtime(outpoint)?;
93        self.base_cache.insert(*outpoint, coin.clone());
94        Ok(coin)
95    }
96
97    /// Batch-prefetch coins (called at start of validation).
98    ///
99    /// This is critical for performance: queries all needed coins in a single
100    /// runtime call rather than making individual calls per input.
101    ///
102    /// Should be called with all transaction inputs before validation begins.
103    pub fn ensure_coins(&mut self, outpoints: &[COutPoint]) -> Result<(), MempoolError> {
104        let missing: Vec<COutPoint> = outpoints
105            .iter()
106            .copied()
107            .filter(|op| {
108                !self.mempool_spends.contains(op)
109                    && !self.mempool_overlay.contains_key(op)
110                    && self.base_cache.peek(op).is_none()
111            })
112            .collect();
113
114        if missing.is_empty() {
115            return Ok(());
116        }
117
118        // Single batched runtime call for all missing coins
119        let coins = self.batch_fetch_from_runtime(&missing)?;
120
121        for (outpoint, coin_opt) in missing.into_iter().zip(coins) {
122            self.base_cache.insert(outpoint, coin_opt);
123        }
124
125        Ok(())
126    }
127
128    /// Add coins from mempool transaction to overlay.
129    ///
130    /// These coins are marked with MEMPOOL_HEIGHT and are preserved across
131    /// block imports until the transaction is removed from the mempool.
132    pub fn add_mempool_coins(&mut self, tx: &Transaction) {
133        for (idx, output) in tx.output.iter().enumerate() {
134            let outpoint = COutPoint::new(tx.compute_txid(), idx as u32);
135            self.mempool_overlay.insert(
136                outpoint,
137                PoolCoin {
138                    output: output.clone(),
139                    height: MEMPOOL_HEIGHT,
140                    is_coinbase: false,
141                    median_time_past: 0, // Mempool coins don't have MTP yet
142                },
143            );
144        }
145    }
146
147    /// Mark coin as spent by mempool transaction.
148    ///
149    /// This creates an overlay that makes the coin appear spent even though
150    /// it exists in the blockchain UTXO set.
151    pub fn spend_coin(&mut self, outpoint: &COutPoint) {
152        self.mempool_spends.insert(*outpoint);
153    }
154
155    /// Remove mempool transaction's coins (on eviction/confirmation).
156    ///
157    /// Cleans up both the coins created by this transaction and the spends
158    /// it made from the overlay.
159    pub fn remove_mempool_tx(&mut self, tx: &Transaction) {
160        // Remove coins created by this transaction
161        for idx in 0..tx.output.len() {
162            let outpoint = COutPoint::new(tx.compute_txid(), idx as u32);
163            self.mempool_overlay.remove(&outpoint);
164        }
165
166        // Remove spends made by this transaction
167        for input in &tx.input {
168            self.mempool_spends.remove(&input.previous_output);
169        }
170    }
171
172    /// Update to new block tip.
173    ///
174    /// **CRITICAL:** Only clears the base cache, preserving the mempool overlay.
175    /// Mempool coins remain valid until their transactions are removed.
176    pub fn on_block_connected(&mut self, block_hash: Block::Hash) {
177        self.best_block = block_hash;
178        self.base_cache.clear(); // Mempool overlay preserved
179    }
180
181    /// Check if coin exists (for quick lookups without fetching).
182    pub fn have_coin(&self, outpoint: &COutPoint) -> bool {
183        if self.mempool_spends.contains(outpoint) {
184            return false;
185        }
186
187        self.mempool_overlay.contains_key(outpoint) || self.base_cache.peek(outpoint).is_some()
188    }
189
190    /// Synchronous runtime API call for single coin.
191    fn fetch_from_runtime(&self, outpoint: &COutPoint) -> Result<Option<PoolCoin>, MempoolError> {
192        let api = self.client.runtime_api();
193        let outpoint_prim = subcoin_runtime_primitives::OutPoint::from(*outpoint);
194
195        api.get_utxos(self.best_block, vec![outpoint_prim])
196            .map_err(|e| MempoolError::RuntimeApi(format!("Failed to fetch UTXO: {e:?}")))?
197            .into_iter()
198            .next()
199            .ok_or_else(|| MempoolError::RuntimeApi("Empty response from runtime API".into()))
200            .map(|opt_coin| opt_coin.map(|coin| coin.into()))
201    }
202
203    /// Batch fetch multiple coins from runtime.
204    fn batch_fetch_from_runtime(
205        &self,
206        outpoints: &[COutPoint],
207    ) -> Result<Vec<Option<PoolCoin>>, MempoolError> {
208        let api = self.client.runtime_api();
209        let outpoints_prim: Vec<_> = outpoints
210            .iter()
211            .map(|op| subcoin_runtime_primitives::OutPoint::from(*op))
212            .collect();
213
214        api.get_utxos(self.best_block, outpoints_prim)
215            .map(|coins| {
216                coins
217                    .into_iter()
218                    .map(|opt_coin| opt_coin.map(|coin| coin.into()))
219                    .collect()
220            })
221            .map_err(|e| MempoolError::RuntimeApi(format!("Batch fetch failed: {e:?}")))
222    }
223
224    /// Get the current best block hash.
225    pub fn best_block(&self) -> Block::Hash {
226        self.best_block
227    }
228
229    /// Get reference to the client.
230    pub fn client(&self) -> &Arc<Client> {
231        &self.client
232    }
233
234    /// Get statistics about the cache.
235    pub fn cache_stats(&self) -> CacheStats {
236        CacheStats {
237            base_cache_entries: self.base_cache.len(),
238            mempool_overlay_entries: self.mempool_overlay.len(),
239            mempool_spends_entries: self.mempool_spends.len(),
240        }
241    }
242}
243
244/// Statistics about the coins cache.
245#[derive(Debug, Clone)]
246pub struct CacheStats {
247    /// Number of entries in base cache.
248    pub base_cache_entries: usize,
249    /// Number of entries in mempool overlay.
250    pub mempool_overlay_entries: usize,
251    /// Number of mempool spends tracked.
252    pub mempool_spends_entries: usize,
253}
254
255#[cfg(test)]
256mod tests {
257    #[allow(unused_imports)]
258    use super::*;
259
260    // Note: Full tests require a mock runtime API implementation.
261    // These will be added when the runtime implementation is complete.
262}