subcoin_mempool/
arena.rs

1//! Arena-based mempool entry storage with multi-index support.
2//!
3//! The arena uses SlotMap for handle-based entry storage, avoiding reference cycles
4//! and enabling safe mutation. Index keys are cached in entries to solve the
5//! remove-before-mutate problem when updating BTreeSet indices.
6
7use crate::types::{EntryId, LockPoints};
8use bitcoin::{Amount, Transaction, Txid, Weight, Wtxid};
9use slotmap::{DefaultKey, SlotMap};
10use std::collections::{BTreeSet, HashMap, HashSet};
11use std::sync::Arc;
12
13/// Comparable key for ancestor score index.
14///
15/// Transactions are prioritized by the minimum of their own feerate and
16/// their ancestor feerate (ensures ancestors are mined first).
17#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
18pub struct AncestorScoreKey {
19    /// Negated feerate for reverse sort (higher fee = lower value = earlier in BTreeSet).
20    /// Uses fraction (fee * 1_000_000, size) for exact comparison without floats.
21    pub neg_feerate_frac: (i64, i64),
22    /// Tie-breaker (deterministic ordering).
23    pub txid: Txid,
24}
25
26/// Comparable key for descendant score index.
27///
28/// Transactions are evicted based on the maximum of their own feerate and
29/// their descendant feerate (evict lowest-paying clusters first).
30#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
31pub struct DescendantScoreKey {
32    /// Negated feerate for reverse sort (higher fee = lower value = earlier in BTreeSet).
33    pub neg_feerate_frac: (i64, i64),
34    /// Secondary sort by entry time (older first for same feerate).
35    pub time: i64,
36}
37
38/// Mempool entry with cached ancestor/descendant state.
39///
40/// **CRITICAL:** Index keys are cached to enable correct reindexing.
41/// When mutating ancestor/descendant state, we must:
42/// 1. Capture the old cached key
43/// 2. Remove from indices using the old key
44/// 3. Mutate the entry
45/// 4. Recompute and cache the new key
46/// 5. Reinsert using the new key
47pub struct TxMemPoolEntry {
48    /// Transaction data.
49    pub tx: Arc<Transaction>,
50
51    /// Base fee (without priority adjustments).
52    pub fee: Amount,
53
54    /// Modified fee (includes priority delta from prioritise_transaction).
55    pub modified_fee: Amount,
56
57    /// Cached transaction weight.
58    pub tx_weight: Weight,
59
60    /// Entry timestamp (seconds since epoch).
61    pub time: i64,
62
63    /// Block height when transaction entered mempool.
64    pub entry_height: u32,
65
66    /// Median Time Past of the best block when transaction entered mempool.
67    pub entry_block_mtp: i64,
68
69    /// Best block hash when transaction entered mempool (for reorg detection).
70    pub entry_block_hash: bitcoin::BlockHash,
71
72    /// Sequence number for replay protection.
73    pub entry_sequence: u64,
74
75    /// Whether this transaction spends a coinbase output.
76    pub spends_coinbase: bool,
77
78    /// Signature operation cost.
79    pub sigop_cost: i64,
80
81    /// Lock points for BIP68/BIP112 validation.
82    pub lock_points: LockPoints,
83
84    // === Mutable ancestor/descendant state ===
85    /// Number of ancestors (including this tx).
86    pub count_with_ancestors: u64,
87
88    /// Total size of ancestors in virtual bytes (including this tx).
89    pub size_with_ancestors: i64,
90
91    /// Total fees of ancestors (including this tx).
92    pub fees_with_ancestors: Amount,
93
94    /// Total sigop cost of ancestors (including this tx).
95    pub sigops_with_ancestors: i64,
96
97    /// Number of descendants (including this tx).
98    pub count_with_descendants: u64,
99
100    /// Total size of descendants in virtual bytes (including this tx).
101    pub size_with_descendants: i64,
102
103    /// Total fees of descendants (including this tx).
104    pub fees_with_descendants: Amount,
105
106    // === Graph links (handles only, no direct references) ===
107    /// Parent entries (in-mempool dependencies).
108    pub parents: HashSet<EntryId>,
109
110    /// Child entries (in-mempool dependents).
111    pub children: HashSet<EntryId>,
112
113    // === Cached index keys (updated atomically with state) ===
114    /// Cached ancestor score key for efficient reindexing.
115    pub cached_ancestor_key: AncestorScoreKey,
116
117    /// Cached descendant score key for efficient reindexing.
118    pub cached_descendant_key: DescendantScoreKey,
119
120    /// Position in randomized transaction vector (for relay).
121    pub idx_randomized: Option<usize>,
122}
123
124impl TxMemPoolEntry {
125    /// Get transaction virtual size in bytes.
126    pub fn vsize(&self) -> i64 {
127        self.tx_weight.to_vbytes_ceil() as i64
128    }
129
130    /// Get transaction feerate (modified fee / vsize).
131    pub fn feerate(&self) -> i64 {
132        (self.modified_fee.to_sat() as i64 * 1_000_000) / self.vsize()
133    }
134
135    /// Get ancestor feerate.
136    pub fn ancestor_feerate(&self) -> i64 {
137        (self.fees_with_ancestors.to_sat() as i64 * 1_000_000) / self.size_with_ancestors
138    }
139
140    /// Get descendant feerate.
141    pub fn descendant_feerate(&self) -> i64 {
142        (self.fees_with_descendants.to_sat() as i64 * 1_000_000) / self.size_with_descendants
143    }
144
145    /// Check if transaction signals RBF per BIP125.
146    ///
147    /// Returns true if any input has nSequence < 0xfffffffe.
148    /// BIP125 Rule: Any nSequence value less than 0xfffffffe signals replaceability.
149    pub fn signals_rbf(&self) -> bool {
150        // BIP125: Any nSequence value less than 0xfffffffe signals RBF
151        const RBF_THRESHOLD: u32 = 0xfffffffe;
152
153        self.tx
154            .input
155            .iter()
156            .any(|txin| txin.sequence.to_consensus_u32() < RBF_THRESHOLD)
157    }
158}
159
160/// Arena holding all mempool entries with multi-index support.
161///
162/// Provides O(1) lookups by txid/wtxid and sorted iteration by various criteria.
163pub struct MemPoolArena {
164    /// Primary storage: handle -> entry.
165    entries: SlotMap<DefaultKey, TxMemPoolEntry>,
166
167    // === Secondary indices (O(1) lookups) ===
168    /// Index by transaction ID.
169    by_txid: HashMap<Txid, EntryId>,
170
171    /// Index by witness transaction ID.
172    by_wtxid: HashMap<Wtxid, EntryId>,
173
174    // === Sorted indices (for mining, eviction, iteration) ===
175    /// Sorted by ancestor score (mining order: highest fee ancestors first).
176    /// Key: (AncestorScoreKey, EntryId)
177    by_ancestor_score: BTreeSet<(AncestorScoreKey, EntryId)>,
178
179    /// Sorted by descendant score (eviction order: lowest fee descendants first).
180    /// Key: (DescendantScoreKey, EntryId)
181    by_descendant_score: BTreeSet<(DescendantScoreKey, EntryId)>,
182
183    /// Sorted by entry time (for expiration).
184    /// Key: (time, EntryId)
185    by_entry_time: BTreeSet<(i64, EntryId)>,
186}
187
188impl MemPoolArena {
189    /// Create a new empty arena.
190    pub fn new() -> Self {
191        Self {
192            entries: SlotMap::new(),
193            by_txid: HashMap::new(),
194            by_wtxid: HashMap::new(),
195            by_ancestor_score: BTreeSet::new(),
196            by_descendant_score: BTreeSet::new(),
197            by_entry_time: BTreeSet::new(),
198        }
199    }
200
201    /// Insert a new entry into the arena.
202    ///
203    /// Returns the handle to the inserted entry.
204    pub fn insert(&mut self, mut entry: TxMemPoolEntry) -> EntryId {
205        // Compute and cache index keys
206        let anc_key = compute_ancestor_key(&entry);
207        let desc_key = compute_descendant_key(&entry);
208        entry.cached_ancestor_key = anc_key;
209        entry.cached_descendant_key = desc_key;
210
211        let txid = entry.tx.compute_txid();
212        let wtxid = entry.tx.compute_wtxid();
213        let time = entry.time;
214
215        // Insert into primary storage
216        let id = EntryId(self.entries.insert(entry));
217
218        // Update all indices
219        self.by_txid.insert(txid, id);
220        self.by_wtxid.insert(wtxid, id);
221        self.by_ancestor_score.insert((anc_key, id));
222        self.by_descendant_score.insert((desc_key, id));
223        self.by_entry_time.insert((time, id));
224
225        id
226    }
227
228    /// Update ancestor state and reindex.
229    ///
230    /// **CRITICAL:** Uses cached keys to remove old entries before mutation.
231    pub fn update_ancestor_state(
232        &mut self,
233        id: EntryId,
234        size_delta: i64,
235        fee_delta: bitcoin::SignedAmount,
236        count_delta: i64,
237        sigops_delta: i64,
238    ) {
239        let entry = &self.entries[id.0];
240
241        // CAPTURE old key BEFORE mutation
242        let old_anc_key = entry.cached_ancestor_key;
243        let old_desc_key = entry.cached_descendant_key;
244
245        // Remove from indices using OLD keys
246        self.by_ancestor_score.remove(&(old_anc_key, id));
247        self.by_descendant_score.remove(&(old_desc_key, id));
248
249        // Mutate entry
250        let entry = &mut self.entries[id.0];
251        entry.size_with_ancestors += size_delta;
252        entry.fees_with_ancestors =
253            (bitcoin::SignedAmount::from_sat(entry.fees_with_ancestors.to_sat() as i64)
254                + fee_delta)
255                .to_unsigned()
256                .expect("Ancestor fees cannot go negative");
257        entry.count_with_ancestors = (entry.count_with_ancestors as i64 + count_delta) as u64;
258        entry.sigops_with_ancestors += sigops_delta;
259
260        // Recompute keys
261        let new_anc_key = compute_ancestor_key(entry);
262        let new_desc_key = compute_descendant_key(entry);
263
264        // Cache new keys in entry
265        entry.cached_ancestor_key = new_anc_key;
266        entry.cached_descendant_key = new_desc_key;
267
268        // Reinsert with NEW keys
269        self.by_ancestor_score.insert((new_anc_key, id));
270        self.by_descendant_score.insert((new_desc_key, id));
271    }
272
273    /// Update descendant state and reindex.
274    pub fn update_descendant_state(
275        &mut self,
276        id: EntryId,
277        size_delta: i64,
278        fee_delta: bitcoin::SignedAmount,
279        count_delta: i64,
280    ) {
281        let entry = &self.entries[id.0];
282
283        // CAPTURE old key
284        let old_desc_key = entry.cached_descendant_key;
285
286        // Remove using old key
287        self.by_descendant_score.remove(&(old_desc_key, id));
288
289        // Mutate
290        let entry = &mut self.entries[id.0];
291        entry.size_with_descendants += size_delta;
292        entry.fees_with_descendants =
293            (bitcoin::SignedAmount::from_sat(entry.fees_with_descendants.to_sat() as i64)
294                + fee_delta)
295                .to_unsigned()
296                .expect("Descendant fees cannot go negative");
297        entry.count_with_descendants = (entry.count_with_descendants as i64 + count_delta) as u64;
298
299        // Recompute and cache
300        let new_desc_key = compute_descendant_key(entry);
301        entry.cached_descendant_key = new_desc_key;
302
303        // Reinsert
304        self.by_descendant_score.insert((new_desc_key, id));
305    }
306
307    /// Update modified fee (priority adjustment) and reindex.
308    pub fn update_modified_fee(&mut self, id: EntryId, new_modified_fee: Amount) {
309        let entry = &self.entries[id.0];
310
311        // CAPTURE old keys
312        let old_anc_key = entry.cached_ancestor_key;
313        let old_desc_key = entry.cached_descendant_key;
314
315        // Remove from sorted indices
316        self.by_ancestor_score.remove(&(old_anc_key, id));
317        self.by_descendant_score.remove(&(old_desc_key, id));
318
319        // Mutate
320        let entry = &mut self.entries[id.0];
321        let old_fee = entry.modified_fee;
322        entry.modified_fee = new_modified_fee;
323
324        // Update ancestor/descendant fees
325        let fee_delta =
326            Amount::from_sat((new_modified_fee.to_sat() as i64 - old_fee.to_sat() as i64) as u64);
327        entry.fees_with_ancestors = Amount::from_sat(
328            (entry.fees_with_ancestors.to_sat() as i64 + fee_delta.to_sat() as i64) as u64,
329        );
330        entry.fees_with_descendants = Amount::from_sat(
331            (entry.fees_with_descendants.to_sat() as i64 + fee_delta.to_sat() as i64) as u64,
332        );
333
334        // Recompute and cache
335        let new_anc_key = compute_ancestor_key(entry);
336        let new_desc_key = compute_descendant_key(entry);
337        entry.cached_ancestor_key = new_anc_key;
338        entry.cached_descendant_key = new_desc_key;
339
340        // Reinsert
341        self.by_ancestor_score.insert((new_anc_key, id));
342        self.by_descendant_score.insert((new_desc_key, id));
343    }
344
345    /// Get entry by ID (immutable).
346    pub fn get(&self, id: EntryId) -> Option<&TxMemPoolEntry> {
347        self.entries.get(id.0)
348    }
349
350    /// Get entry by ID (mutable).
351    pub fn get_mut(&mut self, id: EntryId) -> Option<&mut TxMemPoolEntry> {
352        self.entries.get_mut(id.0)
353    }
354
355    /// Lookup entry ID by txid.
356    pub fn get_by_txid(&self, txid: &Txid) -> Option<EntryId> {
357        self.by_txid.get(txid).copied()
358    }
359
360    /// Lookup entry ID by wtxid.
361    pub fn get_by_wtxid(&self, wtxid: &Wtxid) -> Option<EntryId> {
362        self.by_wtxid.get(wtxid).copied()
363    }
364
365    /// Remove entry from arena.
366    ///
367    /// Returns the removed entry if it existed.
368    pub fn remove(&mut self, id: EntryId) -> Option<TxMemPoolEntry> {
369        let entry = self.entries.remove(id.0)?;
370
371        // Remove from all indices
372        self.by_txid.remove(&entry.tx.compute_txid());
373        self.by_wtxid.remove(&entry.tx.compute_wtxid());
374        self.by_ancestor_score
375            .remove(&(entry.cached_ancestor_key, id));
376        self.by_descendant_score
377            .remove(&(entry.cached_descendant_key, id));
378        self.by_entry_time.remove(&(entry.time, id));
379
380        Some(entry)
381    }
382
383    /// Iterate entries sorted by ancestor score (highest fee first).
384    ///
385    /// This is the mining order.
386    pub fn iter_by_ancestor_score(&self) -> impl Iterator<Item = (EntryId, &TxMemPoolEntry)> {
387        self.by_ancestor_score
388            .iter()
389            .map(|(_, id)| (*id, &self.entries[id.0]))
390    }
391
392    /// Iterate entries sorted by descendant score (lowest fee first).
393    ///
394    /// This is the eviction order.
395    pub fn iter_by_descendant_score(&self) -> impl Iterator<Item = (EntryId, &TxMemPoolEntry)> {
396        self.by_descendant_score
397            .iter()
398            .map(|(_, id)| (*id, &self.entries[id.0]))
399    }
400
401    /// Iterate entries sorted by entry time (oldest first).
402    pub fn iter_by_entry_time(&self) -> impl Iterator<Item = (EntryId, &TxMemPoolEntry)> {
403        self.by_entry_time
404            .iter()
405            .map(|(_, id)| (*id, &self.entries[id.0]))
406    }
407
408    /// Get total number of entries.
409    pub fn len(&self) -> usize {
410        self.entries.len()
411    }
412
413    /// Check if arena is empty.
414    pub fn is_empty(&self) -> bool {
415        self.entries.is_empty()
416    }
417}
418
419impl Default for MemPoolArena {
420    fn default() -> Self {
421        Self::new()
422    }
423}
424
425fn compute_ancestor_key(entry: &TxMemPoolEntry) -> AncestorScoreKey {
426    let entry_feerate = entry.feerate();
427    let ancestor_feerate = entry.ancestor_feerate();
428    let min_feerate = std::cmp::min(entry_feerate, ancestor_feerate);
429
430    AncestorScoreKey {
431        neg_feerate_frac: (-min_feerate, entry.size_with_ancestors),
432        txid: entry.tx.compute_txid(),
433    }
434}
435
436fn compute_descendant_key(entry: &TxMemPoolEntry) -> DescendantScoreKey {
437    let entry_feerate = entry.feerate();
438    let desc_feerate = entry.descendant_feerate();
439    let max_feerate = std::cmp::max(entry_feerate, desc_feerate);
440
441    DescendantScoreKey {
442        neg_feerate_frac: (-max_feerate, entry.size_with_descendants),
443        time: entry.time,
444    }
445}
446
447#[cfg(test)]
448mod tests {
449    #[allow(unused_imports)]
450    use super::*;
451
452    // Tests will be added once we have the full entry construction logic
453}