Skip to main content

bitcoin_rs_mempool/
pool.rs

1use alloc::sync::Arc;
2use alloc::vec::Vec;
3use core::ops::RangeInclusive;
4
5use bitcoin::hashes::{Hash as _, sha256d};
6use bitcoin::{OutPoint, ScriptBuf, Transaction, Txid};
7use bitcoin_rs_primitives::Hash256;
8use hashbrown::HashMap;
9use slab::Slab;
10use thiserror::Error;
11
12use crate::entry::fee_rate;
13use crate::{EntryId, MempoolEntry, MempoolLimits, ParetoFront, PolicyError};
14
15/// Electrum-compatible script hash key for funding index range scans.
16#[derive(
17    Clone,
18    Copy,
19    Debug,
20    Default,
21    Eq,
22    Hash,
23    Ord,
24    PartialEq,
25    PartialOrd,
26    bytemuck::Pod,
27    bytemuck::Zeroable,
28)]
29#[repr(transparent)]
30pub struct ScriptHash {
31    /// Double-SHA256 of the script bytes in consensus byte order.
32    pub hash: Hash256,
33}
34
35impl ScriptHash {
36    /// Hashes a script into an index key.
37    #[must_use]
38    pub fn from_script(script: &ScriptBuf) -> Self {
39        let hash = sha256d::Hash::hash(script.as_bytes());
40        Self {
41            hash: Hash256::from_le_bytes(hash.as_byte_array()),
42        }
43    }
44}
45
46/// Mempool insertion and mutation errors.
47#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
48pub enum MempoolError {
49    /// The transaction id already exists in the pool.
50    #[error("transaction already exists in mempool")]
51    DuplicateTransaction,
52    /// The slab index can no longer fit the public `u32` entry id.
53    #[error("mempool entry id space exhausted")]
54    TooManyEntries,
55    /// The transaction violates mempool policy limits.
56    #[error(transparent)]
57    Policy(#[from] PolicyError),
58}
59
60/// In-memory transaction pool with txid, funding, spending, and fee-priority indexes.
61#[derive(Debug)]
62pub struct Mempool {
63    /// Entry arena. Public ids are slab indices represented as `u32`.
64    pub entries: Slab<MempoolEntry>,
65    /// Transaction id to entry id lookup.
66    pub by_txid: HashMap<Txid, EntryId>,
67    /// Funding index keyed by script hash then entry id.
68    pub funding: std::collections::BTreeSet<(ScriptHash, EntryId)>,
69    /// Spending index keyed by spent outpoint then entry id.
70    pub spending: std::collections::BTreeSet<(OutPoint, EntryId)>,
71    /// Fee-priority index for mining and eviction consumers.
72    pub pareto: ParetoFront,
73    /// Active mempool policy limits.
74    pub limits: MempoolLimits,
75    sequence: core::sync::atomic::AtomicU64,
76}
77/// Aggregate mempool counters surfaced through the JSON-RPC `getmempoolinfo`
78/// and Electrum `mempool.get_fee_histogram` surfaces.
79#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
80pub struct MempoolStats {
81    /// Number of transactions in the mempool.
82    pub txs: u64,
83    /// Sum of virtual sizes in vbytes.
84    pub bytes: u64,
85    /// Sum of base fees in satoshis.
86    pub total_fee: u64,
87}
88
89impl Mempool {
90    /// Creates an empty mempool with the supplied limits.
91    #[must_use]
92    pub fn new(limits: MempoolLimits) -> Self {
93        Self {
94            entries: Slab::new(),
95            by_txid: HashMap::new(),
96            funding: std::collections::BTreeSet::new(),
97            spending: std::collections::BTreeSet::new(),
98            pareto: ParetoFront::new(),
99            limits,
100            sequence: core::sync::atomic::AtomicU64::new(0),
101        }
102    }
103
104    /// Removes all entries from the pool, clears every index, and bumps the
105    /// sequence counter to signal a wholesale invalidation to subscribers.
106    pub fn clear(&mut self) {
107        self.entries.clear();
108        self.by_txid.clear();
109        self.funding.clear();
110        self.spending.clear();
111        self.pareto = ParetoFront::new();
112        self.bump_sequence();
113    }
114
115    /// Returns the current sequence number. Increments on every insert/remove.
116    #[must_use]
117    pub fn sequence_number(&self) -> u64 {
118        self.sequence.load(core::sync::atomic::Ordering::Acquire)
119    }
120
121    /// Returns the configured min-relay-fee rate in sat/kvB.
122    #[must_use]
123    pub const fn min_relay_fee_sat_per_kvb(&self) -> u64 {
124        self.limits.min_relay_fee_sat_per_kvb
125    }
126
127    fn bump_sequence(&self) {
128        let _ = self
129            .sequence
130            .fetch_add(1, core::sync::atomic::Ordering::AcqRel);
131    }
132
133    /// Inserts an entry after applying ancestor and descendant policy checks.
134    pub fn insert_entry(&mut self, mut entry: MempoolEntry) -> Result<EntryId, MempoolError> {
135        let txid = entry.tx.compute_txid();
136        let min_rate = self.limits.min_relay_fee_sat_per_kvb;
137        if min_rate > 0 && entry.fee_rate < min_rate {
138            return Err(PolicyError::BelowMinRelayFee {
139                tx_rate: entry.fee_rate,
140                min_rate,
141            }
142            .into());
143        }
144
145        if self.by_txid.contains_key(&txid) {
146            return Err(MempoolError::DuplicateTransaction);
147        }
148
149        let ancestors = self.ancestor_ids_for_tx(&entry.tx);
150        self.check_ancestor_limits(&ancestors, &entry)?;
151        self.check_descendant_limits(&ancestors)?;
152
153        let ancestor_size = ancestors.iter().fold(u64::from(entry.vsize), |total, id| {
154            total.saturating_add(
155                self.entry(*id)
156                    .map_or(0, |ancestor| u64::from(ancestor.vsize)),
157            )
158        });
159        let ancestor_fee = ancestors.iter().fold(entry.fee, |total, id| {
160            total.saturating_add(self.entry(*id).map_or(0, |ancestor| ancestor.fee))
161        });
162        entry.ancestor_size = ancestor_size;
163        entry.ancestor_fee = ancestor_fee;
164        entry.descendant_size = u64::from(entry.vsize);
165        entry.descendant_fee = entry.fee;
166
167        let index = self.entries.insert(entry);
168        let id = EntryId::try_from(index).map_err(|_| MempoolError::TooManyEntries)?;
169        self.by_txid.insert(txid, id);
170        self.index_entry(id);
171        self.recompute_all_metadata();
172        self.bump_sequence();
173        if self.limits.max_total_bytes > 0 && self.total_vsize() > self.limits.max_total_bytes {
174            let _evicted = self.enforce_size_limit(self.limits.max_total_bytes);
175        }
176        Ok(id)
177    }
178
179    /// Returns the number of transactions in the mempool.
180    #[must_use]
181    pub fn len(&self) -> usize {
182        self.entries.len()
183    }
184
185    /// Returns `true` when `txid` is present in the mempool.
186    ///
187    /// Constant-time wrapper over `by_txid.contains_key`. Cheaper than `entry()`
188    /// for callers that only need a presence check.
189    #[must_use]
190    pub fn contains_txid(&self, txid: &bitcoin::Txid) -> bool {
191        self.by_txid.contains_key(txid)
192    }
193
194    /// Returns a reference to the `MempoolEntry` for `txid`, or `None` if the
195    /// transaction is not in the pool.
196    ///
197    /// Composite of `self.by_txid.get(txid)` and `self.entry(*id)`. Saves the
198    /// 2-step lookup pattern at electrum/rpc handler callsites.
199    #[must_use]
200    pub fn entry_by_txid(&self, txid: &bitcoin::Txid) -> Option<&MempoolEntry> {
201        let id = *self.by_txid.get(txid)?;
202        self.entry(id)
203    }
204
205    /// Returns a clone of the shared `Arc<Transaction>` for `txid`, or `None`
206    /// if the transaction is not in the pool.
207    ///
208    /// Cheaper than [`entry_by_txid`] when only the transaction body is needed
209    /// — no `MempoolEntry` indirection, just an `Arc::clone`.
210    #[must_use]
211    pub fn transaction_by_txid(&self, txid: &bitcoin::Txid) -> Option<Arc<bitcoin::Transaction>> {
212        self.entry_by_txid(txid).map(|entry| Arc::clone(&entry.tx))
213    }
214
215    /// Returns whether the pool currently holds zero entries.
216    #[must_use]
217    pub fn is_empty(&self) -> bool {
218        self.entries.is_empty()
219    }
220
221    /// Returns the count of in-pool transactions.
222    #[must_use]
223    pub fn tx_count(&self) -> usize {
224        self.entries.len()
225    }
226
227    /// Returns the txids of every entry in the pool.
228    ///
229    /// Order is the underlying slab iteration order (i.e., NOT fee-rate sorted;
230    /// use `iter_by_fee_rate_desc` for that).
231    #[must_use]
232    pub fn iter_txids(&self) -> Vec<bitcoin::Txid> {
233        self.entries
234            .iter()
235            .map(|(_id, entry)| entry.tx.compute_txid())
236            .collect()
237    }
238
239    /// Returns txids of mempool entries signalling BIP-125 RBF eligibility.
240    ///
241    /// An entry is replaceable when ANY of its inputs has `sequence < 0xFFFFFFFE`
242    /// (the BIP-125 opt-in convention). Used by fee-bumping and replacement-eligibility
243    /// queries.
244    #[must_use]
245    pub fn iter_replaceable_txids(&self) -> Vec<bitcoin::Txid> {
246        self.entries
247            .iter()
248            .filter(|(_id, entry)| entry.is_replaceable())
249            .map(|(_id, entry)| entry.tx.compute_txid())
250            .collect()
251    }
252
253    /// Returns the total virtual size of all entries.
254    #[must_use]
255    pub fn total_vsize(&self) -> u64 {
256        self.entries.iter().fold(0, |total, (_, entry)| {
257            total.saturating_add(u64::from(entry.vsize))
258        })
259    }
260
261    /// Evicts the lowest-fee packages until the pool's total vsize is at or below
262    /// `max_bytes`. Returns the evicted entry ids.
263    ///
264    /// Delegates to the free-function `evict_lowest_fee_packages`; removals bump
265    /// the sequence counter through `remove_entry_and_descendants`.
266    pub fn enforce_size_limit(&mut self, max_bytes: u64) -> Vec<EntryId> {
267        crate::evict_lowest_fee_packages(self, max_bytes)
268    }
269
270    /// Returns the sum of fees of all entries in the pool, in satoshis.
271    ///
272    /// Used by `getmempoolinfo.total_fee` (BTC = sats / 1e8). Wraps a single
273    /// linear pass over `self.entries`. Saturating add prevents overflow on a
274    /// pathological pool (would require ~92 quadrillion sat aggregate, well
275    /// above the 21M BTC monetary cap, so saturation here is purely defensive).
276    #[must_use]
277    pub fn aggregate_fees(&self) -> u64 {
278        self.entries
279            .iter()
280            .fold(0_u64, |acc, (_id, entry)| acc.saturating_add(entry.fee))
281    }
282
283    /// Returns aggregate counters for the current pool.
284    #[must_use]
285    pub fn stats(&self) -> MempoolStats {
286        let txs = u64::try_from(self.entries.len()).unwrap_or(u64::MAX);
287        let bytes = self.total_vsize();
288        let total_fee = self.aggregate_fees();
289        MempoolStats {
290            txs,
291            bytes,
292            total_fee,
293        }
294    }
295
296    /// Returns an entry by public id.
297    #[must_use]
298    pub fn entry(&self, id: EntryId) -> Option<&MempoolEntry> {
299        usize::try_from(id)
300            .ok()
301            .and_then(|index| self.entries.get(index))
302    }
303
304    /// Returns mempool entry ids in order of descending `fee_rate` (sat/kvB).
305    ///
306    /// Walks `entries` and sorts; cost O(N log N) per call. Used by mining
307    /// template builders and fee estimators that want fee-ordered traversal
308    /// without going through `ParetoFront` (which uses ancestor-aware
309    /// scoring).
310    #[must_use]
311    pub fn iter_by_fee_rate_desc(&self) -> Vec<EntryId> {
312        let mut pairs: Vec<(u64, EntryId)> = self
313            .entries
314            .iter()
315            .filter_map(|(index, entry)| {
316                let id = EntryId::try_from(index).ok()?;
317                Some((entry.fee_rate, id))
318            })
319            .collect();
320        pairs.sort_by_key(|pair| core::cmp::Reverse(pair.0));
321        pairs.into_iter().map(|(_, id)| id).collect()
322    }
323
324    /// Returns the minimum `fee_rate` (sat/kvB) among all entries, or `None`
325    /// for an empty pool.
326    ///
327    /// Linear pass over `self.entries`. Used for `mempoolminfee` tightening
328    /// and fee-histogram tail bounds. The min is over `MempoolEntry.fee_rate`
329    /// which is already pre-computed at insert time.
330    #[must_use]
331    pub fn lowest_fee_rate(&self) -> Option<u64> {
332        self.entries
333            .iter()
334            .map(|(_index, entry)| entry.fee_rate)
335            .min()
336    }
337
338    /// Returns mempool entry ids whose `fee_rate` >= `threshold_sat_per_kvb`.
339    ///
340    /// Linear scan over `entries`. Used by mining template builders and eviction
341    /// strategies that want a fee-rate cohort without sorting.
342    #[must_use]
343    pub fn iter_above_fee_rate(&self, threshold_sat_per_kvb: u64) -> Vec<EntryId> {
344        self.entries
345            .iter()
346            .filter(|(_index, entry)| entry.fee_rate >= threshold_sat_per_kvb)
347            .filter_map(|(index, _entry)| EntryId::try_from(index).ok())
348            .collect()
349    }
350
351    /// Returns the txid of the in-mempool transaction that spends `outpoint`,
352    /// or `None` if no entry spends it. Linear scan over entries; acceptable
353    /// for early IBD where the pool is small, future strands may add a per-
354    /// outpoint index.
355    #[must_use]
356    pub fn find_by_outpoint(&self, outpoint: &bitcoin::OutPoint) -> Option<bitcoin::Txid> {
357        for (_id, entry) in &self.entries {
358            for input in &entry.tx.input {
359                if input.previous_output == *outpoint {
360                    return Some(entry.tx.compute_txid());
361                }
362            }
363        }
364        None
365    }
366
367    /// Returns whether any in-pool transaction spends `outpoint`.
368    ///
369    /// Composes `find_by_outpoint(...).is_some()`. Cheaper than
370    /// `find_by_outpoint` when only the presence answer matters — the wider
371    /// accessor returns the spending txid which callers may not need.
372    #[must_use]
373    pub fn is_outpoint_spent(&self, outpoint: &bitcoin::OutPoint) -> bool {
374        self.find_by_outpoint(outpoint).is_some()
375    }
376
377    /// Adjusts the effective fee of `txid` in the pool by `fee_delta` satoshis.
378    ///
379    /// The delta can be negative (saturating at 0). Bumps the entry's `fee`,
380    /// recomputes `fee_rate` against the existing `vsize`, and propagates the
381    /// realized delta into ancestor and descendant aggregate fees. Returns
382    /// `true` when the txid was present and the adjustment was applied; `false`
383    /// when the txid was not in the mempool.
384    #[must_use]
385    pub fn prioritise(&mut self, txid: Txid, fee_delta: i64) -> bool {
386        let Some(&id) = self.by_txid.get(&txid) else {
387            return false;
388        };
389
390        let actual_delta = {
391            let Some(entry) = self.entry_mut(id) else {
392                return false;
393            };
394            let new_fee = apply_fee_delta(entry.fee, fee_delta);
395            let actual_delta = i128::from(new_fee).saturating_sub(i128::from(entry.fee));
396            entry.fee = new_fee;
397            let denom = u64::from(entry.vsize).max(1);
398            entry.fee_rate = new_fee.saturating_mul(1_000) / denom;
399            entry.ancestor_fee = apply_delta_u64(entry.ancestor_fee, actual_delta);
400            actual_delta
401        };
402
403        let ancestor_ids = self.ancestor_ids_for_entry(id);
404        let descendant_ids = self.descendant_ids_for_entry(id);
405        for ancestor_id in ancestor_ids {
406            if let Some(ancestor) = self.entry_mut(ancestor_id) {
407                ancestor.descendant_fee = apply_delta_u64(ancestor.descendant_fee, actual_delta);
408            }
409        }
410        for descendant_id in descendant_ids {
411            if let Some(descendant) = self.entry_mut(descendant_id) {
412                descendant.ancestor_fee = apply_delta_u64(descendant.ancestor_fee, actual_delta);
413            }
414        }
415
416        let Some(entry) = self.entry(id).cloned() else {
417            return false;
418        };
419        self.pareto.insert(id, &entry);
420        self.bump_sequence();
421        true
422    }
423
424    /// Removes an entry and all descendants that spend its outputs.
425    pub fn remove_entry_and_descendants(&mut self, id: EntryId) -> Vec<EntryId> {
426        let mut ids = Vec::new();
427        self.collect_descendants_inclusive(id, &mut ids);
428        ids.sort_unstable();
429        ids.dedup();
430        self.remove_entries(&ids);
431        ids
432    }
433
434    /// Removes the entry for `txid` along with all descendants that spend
435    /// its outputs. Returns the set of removed entry ids in stable order.
436    ///
437    /// Returns an empty vector when the txid is not present in the pool.
438    pub fn remove_by_txid(&mut self, txid: &bitcoin::Txid) -> Vec<EntryId> {
439        let Some(id) = self.by_txid.get(txid).copied() else {
440            return Vec::new();
441        };
442        self.remove_entry_and_descendants(id)
443    }
444
445    /// Removes every entry whose `fee_rate` (sat/kvB) is strictly below
446    /// `threshold_sat_per_kvb`. Returns the evicted transaction ids.
447    ///
448    /// Bumps the sequence counter for each successful removal. Use this for
449    /// min-relay-fee tightening or size-bound eviction policies.
450    #[must_use]
451    pub fn evict_below_fee_rate(&mut self, threshold_sat_per_kvb: u64) -> Vec<bitcoin::Txid> {
452        let mut to_evict: Vec<bitcoin::Txid> = Vec::new();
453        for (_id, entry) in &self.entries {
454            if entry.fee_rate < threshold_sat_per_kvb {
455                to_evict.push(entry.tx.compute_txid());
456            }
457        }
458
459        let mut evicted = Vec::with_capacity(to_evict.len());
460        for txid in to_evict {
461            let removed = self.remove_by_txid(&txid);
462            if !removed.is_empty() {
463                evicted.push(txid);
464            }
465        }
466        evicted
467    }
468
469    pub(crate) fn conflicts_for(&self, tx: &Transaction) -> Vec<EntryId> {
470        let mut conflicts = Vec::new();
471        for input in &tx.input {
472            for (_, id) in self.spending.range(outpoint_range(input.previous_output)) {
473                conflicts.push(*id);
474            }
475        }
476        conflicts.sort_unstable();
477        conflicts.dedup();
478        conflicts
479    }
480
481    pub(crate) fn conflicts_with_descendants(&self, tx: &Transaction) -> Vec<EntryId> {
482        let mut conflicts = self.conflicts_for(tx);
483        let direct = conflicts.clone();
484        for id in direct {
485            self.collect_descendants_exclusive(id, &mut conflicts);
486        }
487        conflicts.sort_unstable();
488        conflicts.dedup();
489        conflicts
490    }
491
492    /// Returns all ancestor entry ids for `id`, excluding `id` itself.
493    #[must_use]
494    pub fn ancestor_ids_for_entry(&self, id: EntryId) -> Vec<EntryId> {
495        self.entry(id)
496            .map_or_else(Vec::new, |entry| self.ancestor_ids_for_tx(&entry.tx))
497    }
498
499    /// Returns all descendant entry ids for `id`, EXCLUDING `id` itself.
500    ///
501    /// Walks the spend graph forward via output references. Empty Vec when the
502    /// entry has no descendants or is unknown.
503    #[must_use]
504    pub fn descendant_ids_for_entry(&self, id: EntryId) -> Vec<EntryId> {
505        let mut ids = Vec::new();
506        self.collect_descendants_inclusive(id, &mut ids);
507        ids.retain(|other| *other != id);
508        ids.sort_unstable();
509        ids.dedup();
510        ids
511    }
512
513    pub(crate) fn signals_rbf_including_ancestors(&self, id: EntryId) -> bool {
514        self.entry_signals_rbf(id)
515            || self
516                .ancestor_ids_for_entry(id)
517                .into_iter()
518                .any(|ancestor| self.entry_signals_rbf(ancestor))
519    }
520
521    pub(crate) fn is_unconfirmed_outpoint(&self, outpoint: OutPoint) -> bool {
522        self.by_txid.contains_key(&outpoint.txid)
523    }
524
525    fn remove_entries(&mut self, ids: &[EntryId]) {
526        let mut removed_any = false;
527        for id in ids {
528            let Some(index) = usize::try_from(*id).ok() else {
529                continue;
530            };
531            if !self.entries.contains(index) {
532                continue;
533            }
534            let entry = self.entries.remove(index);
535            removed_any = true;
536            self.by_txid.remove(&entry.tx.compute_txid());
537            self.pareto.remove(*id);
538            for (vout, output) in entry.tx.output.iter().enumerate() {
539                let Ok(_) = EntryId::try_from(vout) else {
540                    continue;
541                };
542                let _ = self
543                    .funding
544                    .remove(&(ScriptHash::from_script(&output.script_pubkey), *id));
545            }
546            for input in &entry.tx.input {
547                let _ = self.spending.remove(&(input.previous_output, *id));
548            }
549        }
550        self.recompute_all_metadata();
551        if removed_any {
552            self.bump_sequence();
553        }
554    }
555
556    fn index_entry(&mut self, id: EntryId) {
557        let Some(entry) = self.entry(id) else {
558            return;
559        };
560        let funding_keys = entry
561            .tx
562            .output
563            .iter()
564            .map(|output| (ScriptHash::from_script(&output.script_pubkey), id))
565            .collect::<Vec<_>>();
566        let spending_keys = entry
567            .tx
568            .input
569            .iter()
570            .map(|input| (input.previous_output, id))
571            .collect::<Vec<_>>();
572        for key in funding_keys {
573            self.funding.insert(key);
574        }
575        for key in spending_keys {
576            self.spending.insert(key);
577        }
578    }
579
580    fn recompute_all_metadata(&mut self) {
581        let ids = self
582            .entries
583            .iter()
584            .filter_map(|(index, _)| EntryId::try_from(index).ok())
585            .collect::<Vec<_>>();
586        for id in &ids {
587            let ancestors = self.ancestor_ids_for_entry(*id);
588            let mut ancestor_size = self.entry(*id).map_or(0, |entry| u64::from(entry.vsize));
589            let mut ancestor_fee = self.entry(*id).map_or(0, |entry| entry.fee);
590            for ancestor in ancestors {
591                if let Some(entry) = self.entry(ancestor) {
592                    ancestor_size = ancestor_size.saturating_add(u64::from(entry.vsize));
593                    ancestor_fee = ancestor_fee.saturating_add(entry.fee);
594                }
595            }
596            if let Some(entry) = self.entry_mut(*id) {
597                entry.ancestor_size = ancestor_size;
598                entry.ancestor_fee = ancestor_fee;
599                entry.descendant_size = u64::from(entry.vsize);
600                entry.descendant_fee = entry.fee;
601            }
602        }
603
604        for id in &ids {
605            let Some(entry) = self.entry(*id) else {
606                continue;
607            };
608            let size = u64::from(entry.vsize);
609            let fee = entry.fee;
610            for ancestor in self.ancestor_ids_for_entry(*id) {
611                if let Some(ancestor_entry) = self.entry_mut(ancestor) {
612                    ancestor_entry.descendant_size =
613                        ancestor_entry.descendant_size.saturating_add(size);
614                    ancestor_entry.descendant_fee =
615                        ancestor_entry.descendant_fee.saturating_add(fee);
616                }
617            }
618        }
619
620        let pareto_entries = ids
621            .into_iter()
622            .filter_map(|id| self.entry(id).cloned().map(|entry| (id, entry)))
623            .collect::<Vec<_>>();
624        self.pareto = ParetoFront::new();
625        for (id, entry) in pareto_entries {
626            self.pareto.insert(id, &entry);
627        }
628    }
629
630    fn check_ancestor_limits(
631        &self,
632        ancestors: &[EntryId],
633        entry: &MempoolEntry,
634    ) -> Result<(), PolicyError> {
635        let ancestor_count = u32::try_from(ancestors.len())
636            .unwrap_or(u32::MAX)
637            .saturating_add(1);
638        if ancestor_count > self.limits.max_ancestors {
639            return Err(PolicyError::TooManyAncestors);
640        }
641        let ancestor_size = ancestors.iter().fold(u64::from(entry.vsize), |total, id| {
642            total.saturating_add(
643                self.entry(*id)
644                    .map_or(0, |ancestor| u64::from(ancestor.vsize)),
645            )
646        });
647        if ancestor_size > self.limits.max_ancestor_size {
648            return Err(PolicyError::AncestorSizeLimit);
649        }
650        Ok(())
651    }
652
653    fn check_descendant_limits(&self, ancestors: &[EntryId]) -> Result<(), PolicyError> {
654        for ancestor in ancestors {
655            let descendant_count = self.descendant_count_inclusive(*ancestor).saturating_add(1);
656            if descendant_count > self.limits.max_descendants {
657                return Err(PolicyError::TooManyDescendants);
658            }
659        }
660        Ok(())
661    }
662
663    fn ancestor_ids_for_tx(&self, tx: &Transaction) -> Vec<EntryId> {
664        let mut ancestors = Vec::new();
665        let mut stack = tx
666            .input
667            .iter()
668            .filter_map(|input| self.by_txid.get(&input.previous_output.txid).copied())
669            .collect::<Vec<_>>();
670        while let Some(id) = stack.pop() {
671            if ancestors.contains(&id) {
672                continue;
673            }
674            ancestors.push(id);
675            if let Some(entry) = self.entry(id) {
676                for input in &entry.tx.input {
677                    if let Some(parent) = self.by_txid.get(&input.previous_output.txid) {
678                        stack.push(*parent);
679                    }
680                }
681            }
682        }
683        ancestors.sort_unstable();
684        ancestors
685    }
686
687    fn collect_descendants_inclusive(&self, id: EntryId, out: &mut Vec<EntryId>) {
688        if out.contains(&id) {
689            return;
690        }
691        out.push(id);
692        self.collect_descendants_exclusive(id, out);
693    }
694
695    fn collect_descendants_exclusive(&self, id: EntryId, out: &mut Vec<EntryId>) {
696        for child in self.child_ids(id) {
697            if out.contains(&child) {
698                continue;
699            }
700            out.push(child);
701            self.collect_descendants_exclusive(child, out);
702        }
703    }
704
705    fn child_ids(&self, id: EntryId) -> Vec<EntryId> {
706        let Some(entry) = self.entry(id) else {
707            return Vec::new();
708        };
709        let txid = entry.tx.compute_txid();
710        let mut children = Vec::new();
711        for (vout, _) in entry.tx.output.iter().enumerate() {
712            let Ok(vout) = u32::try_from(vout) else {
713                continue;
714            };
715            let outpoint = OutPoint::new(txid, vout);
716            for (_, child) in self.spending.range(outpoint_range(outpoint)) {
717                children.push(*child);
718            }
719        }
720        children.sort_unstable();
721        children.dedup();
722        children
723    }
724
725    /// Returns the descendant-package count for `id` (inclusive of `id` itself).
726    ///
727    /// Saturates at `u32::MAX` for pathological packages.
728    #[must_use]
729    pub fn descendant_count_inclusive(&self, id: EntryId) -> u32 {
730        let mut descendants = Vec::new();
731        self.collect_descendants_inclusive(id, &mut descendants);
732        u32::try_from(descendants.len()).unwrap_or(u32::MAX)
733    }
734
735    /// Returns the ancestor-package count for `id` (inclusive of `id` itself).
736    ///
737    /// Saturates at `u32::MAX` for pathological packages. Composes
738    /// `ancestor_ids_for_entry` + plus-one (caller is itself an ancestor of
739    /// the inclusive count).
740    #[must_use]
741    pub fn ancestor_count_inclusive(&self, id: EntryId) -> u32 {
742        let ancestors = self.ancestor_ids_for_entry(id);
743        u32::try_from(ancestors.len())
744            .unwrap_or(u32::MAX)
745            .saturating_add(1)
746    }
747
748    fn entry_mut(&mut self, id: EntryId) -> Option<&mut MempoolEntry> {
749        usize::try_from(id)
750            .ok()
751            .and_then(|index| self.entries.get_mut(index))
752    }
753
754    fn entry_signals_rbf(&self, id: EntryId) -> bool {
755        self.entry(id)
756            .is_some_and(|entry| entry.tx.input.iter().any(|input| input.sequence.is_rbf()))
757    }
758}
759
760pub(crate) fn tx_fee_rate(fee: u64, vsize: u32) -> u64 {
761    fee_rate(fee, u64::from(vsize))
762}
763
764fn apply_fee_delta(fee: u64, delta: i64) -> u64 {
765    if delta >= 0 {
766        fee.saturating_add(delta.unsigned_abs())
767    } else {
768        fee.saturating_sub(delta.unsigned_abs())
769    }
770}
771
772fn apply_delta_u64(value: u64, delta: i128) -> u64 {
773    let magnitude = u64::try_from(delta.unsigned_abs()).unwrap_or(u64::MAX);
774    if delta >= 0 {
775        value.saturating_add(magnitude)
776    } else {
777        value.saturating_sub(magnitude)
778    }
779}
780
781const fn outpoint_range(outpoint: OutPoint) -> RangeInclusive<(OutPoint, EntryId)> {
782    (outpoint, EntryId::MIN)..=(outpoint, EntryId::MAX)
783}
784#[cfg(test)]
785mod tests {
786    use alloc::sync::Arc;
787    use alloc::vec::Vec;
788
789    use bitcoin::{Amount, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Witness};
790
791    use super::*;
792
793    #[test]
794    fn default_min_relay_fee_is_1_sat_per_vbyte() {
795        let pool = Mempool::new(MempoolLimits::default());
796        assert_eq!(pool.min_relay_fee_sat_per_kvb(), 1_000);
797    }
798
799    #[test]
800    fn custom_min_relay_fee_round_trips() {
801        let limits = MempoolLimits {
802            min_relay_fee_sat_per_kvb: 5_000,
803            ..MempoolLimits::default()
804        };
805        let pool = Mempool::new(limits);
806        assert_eq!(pool.min_relay_fee_sat_per_kvb(), 5_000);
807    }
808
809    #[test]
810    fn insert_entry_rejects_below_min_relay_fee() {
811        let limits = MempoolLimits {
812            min_relay_fee_sat_per_kvb: 5_000,
813            ..MempoolLimits::default()
814        };
815        let mut pool = Mempool::new(limits);
816        let tx = bitcoin::Transaction {
817            version: bitcoin::transaction::Version(2),
818            lock_time: bitcoin::absolute::LockTime::ZERO,
819            input: Vec::new(),
820            output: vec![bitcoin::TxOut {
821                value: bitcoin::Amount::from_sat(1_000),
822                script_pubkey: bitcoin::ScriptBuf::from_bytes(vec![0x51]),
823            }],
824        };
825        let entry = MempoolEntry::new(Arc::new(tx), 100, 100, 1, 7);
826        let result = pool.insert_entry(entry);
827
828        assert!(
829            matches!(
830                result,
831                Err(MempoolError::Policy(PolicyError::BelowMinRelayFee {
832                    tx_rate: 1_000,
833                    min_rate: 5_000
834                }))
835            ),
836            "expected BelowMinRelayFee rejection, got {result:?}"
837        );
838    }
839
840    #[test]
841    fn stats_reports_empty_and_inserted_entry_counters() -> Result<(), MempoolError> {
842        let mut pool = Mempool::new(MempoolLimits::default());
843        assert_eq!(pool.stats(), MempoolStats::default());
844
845        let tx = Transaction {
846            version: bitcoin::transaction::Version::TWO,
847            lock_time: bitcoin::absolute::LockTime::ZERO,
848            input: Vec::new(),
849            output: Vec::new(),
850        };
851        let entry = MempoolEntry::new(Arc::new(tx), 123, 4_567, 0, 0);
852        let expected_vsize = u64::from(entry.vsize);
853        let expected_fee = entry.fee;
854
855        pool.insert_entry(entry)?;
856
857        let stats = pool.stats();
858        assert_eq!(stats.txs, 1);
859        assert_eq!(stats.bytes, expected_vsize);
860        assert_eq!(stats.total_fee, expected_fee);
861        Ok(())
862    }
863
864    #[test]
865    fn is_empty_true_for_default_pool() {
866        let pool = Mempool::new(MempoolLimits::default());
867        assert!(pool.is_empty());
868        assert_eq!(pool.tx_count(), 0);
869    }
870
871    #[test]
872    fn tx_count_increments_with_insert() -> Result<(), MempoolError> {
873        let mut pool = Mempool::new(MempoolLimits {
874            min_relay_fee_sat_per_kvb: 0,
875            ..MempoolLimits::default()
876        });
877        let tx = bitcoin::Transaction {
878            version: bitcoin::transaction::Version(2),
879            lock_time: bitcoin::absolute::LockTime::ZERO,
880            input: vec![],
881            output: vec![],
882        };
883        pool.insert_entry(MempoolEntry::new(Arc::new(tx), 100, 10_000, 1, 7))?;
884        assert!(!pool.is_empty());
885        assert_eq!(pool.tx_count(), 1);
886        Ok(())
887    }
888
889    #[test]
890    fn aggregate_fees_sums_entry_fees() -> Result<(), MempoolError> {
891        let mut pool = Mempool::new(MempoolLimits::default());
892        assert_eq!(pool.aggregate_fees(), 0);
893
894        let entry_a = MempoolEntry::new(Arc::new(tx(1, Vec::new())), 400, 500, 1, 7);
895        let entry_b = MempoolEntry::new(Arc::new(tx(2, Vec::new())), 900, 1_000, 2, 7);
896        pool.insert_entry(entry_a)?;
897        pool.insert_entry(entry_b)?;
898
899        assert_eq!(pool.aggregate_fees(), 1_500);
900        Ok(())
901    }
902    #[test]
903    fn contains_txid_returns_true_after_insert() {
904        let mut pool = Mempool::new(MempoolLimits::default());
905        let tx = bitcoin::Transaction {
906            version: bitcoin::transaction::Version(2),
907            lock_time: bitcoin::absolute::LockTime::ZERO,
908            input: Vec::new(),
909            output: vec![bitcoin::TxOut {
910                value: bitcoin::Amount::from_sat(99_000),
911                script_pubkey: bitcoin::ScriptBuf::from_bytes(vec![0x51]),
912            }],
913        };
914        let txid = tx.compute_txid();
915        let _ = pool.insert_entry(MempoolEntry::new(Arc::new(tx), 100, 10_000, 1, 7));
916        assert!(pool.contains_txid(&txid));
917        let other = bitcoin::Txid::from_byte_array([0xff; 32]);
918        assert!(!pool.contains_txid(&other));
919    }
920
921    #[test]
922    fn entry_by_txid_returns_some_for_inserted_tx() -> Result<(), MempoolError> {
923        let mut pool = Mempool::new(MempoolLimits {
924            min_relay_fee_sat_per_kvb: 0,
925            ..MempoolLimits::default()
926        });
927        let tx = bitcoin::Transaction {
928            version: bitcoin::transaction::Version(2),
929            lock_time: bitcoin::absolute::LockTime::ZERO,
930            input: vec![],
931            output: vec![],
932        };
933        let txid = tx.compute_txid();
934        pool.insert_entry(MempoolEntry::new(Arc::new(tx), 100, 10_000, 1, 7))?;
935        let Some(entry) = pool.entry_by_txid(&txid) else {
936            panic!("entry_by_txid returned None for inserted tx");
937        };
938        assert_eq!(entry.tx.compute_txid(), txid);
939        Ok(())
940    }
941
942    #[test]
943    fn entry_by_txid_returns_none_for_absent_tx() {
944        let pool = Mempool::new(MempoolLimits::default());
945        let absent = bitcoin::Txid::from_byte_array([0xff; 32]);
946        assert!(pool.entry_by_txid(&absent).is_none());
947    }
948
949    #[test]
950    fn transaction_by_txid_returns_arc_for_inserted_tx() -> Result<(), MempoolError> {
951        let mut pool = Mempool::new(MempoolLimits {
952            min_relay_fee_sat_per_kvb: 0,
953            ..MempoolLimits::default()
954        });
955        let tx = bitcoin::Transaction {
956            version: bitcoin::transaction::Version(2),
957            lock_time: bitcoin::absolute::LockTime::ZERO,
958            input: vec![],
959            output: vec![],
960        };
961        let txid = tx.compute_txid();
962        let tx_arc = Arc::new(tx);
963        pool.insert_entry(MempoolEntry::new(Arc::clone(&tx_arc), 500, 100, 1, 7))?;
964        let Some(retrieved) = pool.transaction_by_txid(&txid) else {
965            panic!("transaction_by_txid returned None");
966        };
967        assert_eq!(retrieved.compute_txid(), txid);
968        // The retrieved Arc should be the same allocation as the inserted one.
969        assert!(Arc::ptr_eq(&tx_arc, &retrieved));
970        Ok(())
971    }
972
973    #[test]
974    fn transaction_by_txid_returns_none_for_absent_tx() {
975        let pool = Mempool::new(MempoolLimits::default());
976        let absent = bitcoin::Txid::from_byte_array([0xff; 32]);
977        assert!(pool.transaction_by_txid(&absent).is_none());
978    }
979
980    #[test]
981    fn iter_txids_returns_empty_for_empty_pool() {
982        let pool = Mempool::new(MempoolLimits::default());
983        assert!(pool.iter_txids().is_empty());
984    }
985
986    #[test]
987    fn iter_txids_returns_all_inserted_txids() -> Result<(), MempoolError> {
988        let mut pool = Mempool::new(MempoolLimits {
989            min_relay_fee_sat_per_kvb: 0,
990            ..MempoolLimits::default()
991        });
992        let tx_a = bitcoin::Transaction {
993            version: bitcoin::transaction::Version(2),
994            lock_time: bitcoin::absolute::LockTime::ZERO,
995            input: vec![],
996            output: vec![bitcoin::TxOut {
997                value: bitcoin::Amount::from_sat(100),
998                script_pubkey: bitcoin::ScriptBuf::new(),
999            }],
1000        };
1001        let txid_a = tx_a.compute_txid();
1002        let tx_b = bitcoin::Transaction {
1003            version: bitcoin::transaction::Version(2),
1004            lock_time: bitcoin::absolute::LockTime::ZERO,
1005            input: vec![],
1006            output: vec![bitcoin::TxOut {
1007                value: bitcoin::Amount::from_sat(200),
1008                script_pubkey: bitcoin::ScriptBuf::new(),
1009            }],
1010        };
1011        let txid_b = tx_b.compute_txid();
1012        pool.insert_entry(MempoolEntry::new(Arc::new(tx_a), 500, 100, 1, 7))?;
1013        pool.insert_entry(MempoolEntry::new(Arc::new(tx_b), 500, 100, 2, 7))?;
1014        let txids = pool.iter_txids();
1015        assert_eq!(txids.len(), 2);
1016        assert!(txids.contains(&txid_a));
1017        assert!(txids.contains(&txid_b));
1018        Ok(())
1019    }
1020
1021    #[test]
1022    fn iter_by_fee_rate_desc_orders_highest_first() {
1023        let mut pool = Mempool::new(MempoolLimits::default());
1024        // Two distinct txs with different fee rates.
1025        let low_tx = bitcoin::Transaction {
1026            version: bitcoin::transaction::Version(2),
1027            lock_time: bitcoin::absolute::LockTime::ZERO,
1028            input: Vec::new(),
1029            output: vec![bitcoin::TxOut {
1030                value: bitcoin::Amount::from_sat(1_000),
1031                script_pubkey: bitcoin::ScriptBuf::from_bytes(vec![0x51]),
1032            }],
1033        };
1034        let low_txid = low_tx.compute_txid();
1035        let _ = pool.insert_entry(MempoolEntry::new(Arc::new(low_tx), 100, 1_000, 1, 7));
1036        let high_tx = bitcoin::Transaction {
1037            version: bitcoin::transaction::Version(2),
1038            lock_time: bitcoin::absolute::LockTime::ZERO,
1039            input: Vec::new(),
1040            output: vec![bitcoin::TxOut {
1041                value: bitcoin::Amount::from_sat(99_000),
1042                script_pubkey: bitcoin::ScriptBuf::from_bytes(vec![0x52]),
1043            }],
1044        };
1045        let high_txid = high_tx.compute_txid();
1046        let _ = pool.insert_entry(MempoolEntry::new(Arc::new(high_tx), 100, 10_000, 1, 7));
1047        let ordered = pool.iter_by_fee_rate_desc();
1048        assert_eq!(ordered.len(), 2);
1049        let Some(&first_id) = ordered.first() else {
1050            panic!("expected at least one entry");
1051        };
1052        let Some(first_entry) = pool.entry(first_id) else {
1053            panic!("first entry missing");
1054        };
1055        assert_eq!(first_entry.tx.compute_txid(), high_txid);
1056        let Some(&second_id) = ordered.get(1) else {
1057            panic!("expected two entries");
1058        };
1059        let Some(second_entry) = pool.entry(second_id) else {
1060            panic!("second entry missing");
1061        };
1062        assert_eq!(second_entry.tx.compute_txid(), low_txid);
1063    }
1064
1065    #[test]
1066    fn lowest_fee_rate_returns_none_for_empty_pool() {
1067        let pool = Mempool::new(MempoolLimits::default());
1068        assert!(pool.lowest_fee_rate().is_none());
1069    }
1070
1071    #[test]
1072    fn lowest_fee_rate_returns_minimum_across_entries() -> Result<(), MempoolError> {
1073        let mut pool = Mempool::new(MempoolLimits {
1074            min_relay_fee_sat_per_kvb: 0,
1075            ..MempoolLimits::default()
1076        });
1077
1078        let high = MempoolEntry::new(Arc::new(tx(1, Vec::new())), 1_000, 5_000, 1, 7);
1079        let low = MempoolEntry::new(Arc::new(tx(2, Vec::new())), 1_000, 1_500, 1, 7);
1080        pool.insert_entry(high)?;
1081        pool.insert_entry(low)?;
1082
1083        assert_eq!(pool.lowest_fee_rate(), Some(1_500));
1084        Ok(())
1085    }
1086
1087    #[test]
1088    fn iter_above_fee_rate_filters_to_high_fee_only() {
1089        let mut pool = Mempool::new(MempoolLimits::default());
1090        let low_tx = bitcoin::Transaction {
1091            version: bitcoin::transaction::Version(2),
1092            lock_time: bitcoin::absolute::LockTime::ZERO,
1093            input: Vec::new(),
1094            output: vec![bitcoin::TxOut {
1095                value: bitcoin::Amount::from_sat(1_000),
1096                script_pubkey: bitcoin::ScriptBuf::from_bytes(vec![0x51]),
1097            }],
1098        };
1099        let _ = pool.insert_entry(MempoolEntry::new(Arc::new(low_tx), 100, 1_000, 1, 7)); // fee_rate = 1000
1100        let high_tx = bitcoin::Transaction {
1101            version: bitcoin::transaction::Version(2),
1102            lock_time: bitcoin::absolute::LockTime::ZERO,
1103            input: Vec::new(),
1104            output: vec![bitcoin::TxOut {
1105                value: bitcoin::Amount::from_sat(99_000),
1106                script_pubkey: bitcoin::ScriptBuf::from_bytes(vec![0x52]),
1107            }],
1108        };
1109        let _ = pool.insert_entry(MempoolEntry::new(Arc::new(high_tx), 100, 10_000, 1, 7)); // fee_rate = 100_000
1110        let high_only = pool.iter_above_fee_rate(50_000);
1111        assert_eq!(high_only.len(), 1);
1112        let both = pool.iter_above_fee_rate(500);
1113        assert_eq!(both.len(), 2);
1114        let none = pool.iter_above_fee_rate(200_000);
1115        assert_eq!(none.len(), 0);
1116    }
1117
1118    #[test]
1119    fn iter_replaceable_txids_returns_only_rbf_signaled_txs() {
1120        let mut pool = Mempool::new(MempoolLimits::default());
1121        // RBF-signalled tx (sequence < 0xFFFFFFFE).
1122        let rbf_tx = bitcoin::Transaction {
1123            version: bitcoin::transaction::Version(2),
1124            lock_time: bitcoin::absolute::LockTime::ZERO,
1125            input: vec![bitcoin::TxIn {
1126                previous_output: bitcoin::OutPoint {
1127                    txid: bitcoin::Txid::from_byte_array([0xaa; 32]),
1128                    vout: 0,
1129                },
1130                script_sig: bitcoin::ScriptBuf::new(),
1131                sequence: bitcoin::Sequence(0x0000_0001),
1132                witness: bitcoin::Witness::new(),
1133            }],
1134            output: Vec::new(),
1135        };
1136        let rbf_txid = rbf_tx.compute_txid();
1137        let _ = pool.insert_entry(MempoolEntry::new(Arc::new(rbf_tx), 100, 10_000, 1, 7));
1138        // Non-RBF tx (sequence = MAX = 0xFFFFFFFF).
1139        let non_rbf_tx = bitcoin::Transaction {
1140            version: bitcoin::transaction::Version(2),
1141            lock_time: bitcoin::absolute::LockTime::ZERO,
1142            input: vec![bitcoin::TxIn {
1143                previous_output: bitcoin::OutPoint {
1144                    txid: bitcoin::Txid::from_byte_array([0xbb; 32]),
1145                    vout: 0,
1146                },
1147                script_sig: bitcoin::ScriptBuf::new(),
1148                sequence: bitcoin::Sequence::MAX,
1149                witness: bitcoin::Witness::new(),
1150            }],
1151            output: Vec::new(),
1152        };
1153        let non_rbf_txid = non_rbf_tx.compute_txid();
1154        let _ = pool.insert_entry(MempoolEntry::new(Arc::new(non_rbf_tx), 100, 10_000, 1, 7));
1155        let replaceable = pool.iter_replaceable_txids();
1156        assert!(replaceable.contains(&rbf_txid));
1157        assert!(!replaceable.contains(&non_rbf_txid));
1158        assert_eq!(replaceable.len(), 1);
1159    }
1160
1161    #[test]
1162    fn sequence_number_bumps_on_successful_insert() -> Result<(), MempoolError> {
1163        let mut pool = Mempool::new(MempoolLimits::default());
1164        let before = pool.sequence_number();
1165        let tx = bitcoin::Transaction {
1166            version: bitcoin::transaction::Version(2),
1167            lock_time: bitcoin::absolute::LockTime::ZERO,
1168            input: Vec::new(),
1169            output: Vec::new(),
1170        };
1171        let entry = MempoolEntry::new(Arc::new(tx), 100, 1_000, 1, 7);
1172        pool.insert_entry(entry)?;
1173        let after = pool.sequence_number();
1174        assert!(after > before, "expected sequence to bump");
1175        Ok(())
1176    }
1177
1178    #[test]
1179    fn clear_removes_all_entries_and_bumps_sequence() -> Result<(), MempoolError> {
1180        let mut pool = Mempool::new(MempoolLimits::default());
1181        let tx = bitcoin::Transaction {
1182            version: bitcoin::transaction::Version(2),
1183            lock_time: bitcoin::absolute::LockTime::ZERO,
1184            input: Vec::new(),
1185            output: vec![bitcoin::TxOut {
1186                value: bitcoin::Amount::from_sat(99_000),
1187                script_pubkey: bitcoin::ScriptBuf::from_bytes(vec![0x51]),
1188            }],
1189        };
1190        let _id = pool.insert_entry(MempoolEntry::new(Arc::new(tx), 100, 10_000, 1, 7))?;
1191        let seq_before_clear = pool.sequence_number();
1192
1193        pool.clear();
1194
1195        assert_eq!(pool.len(), 0);
1196        assert!(pool.by_txid.is_empty());
1197        assert!(pool.funding.is_empty());
1198        assert!(pool.spending.is_empty());
1199        assert!(pool.pareto.is_empty());
1200        assert!(pool.sequence_number() > seq_before_clear);
1201        Ok(())
1202    }
1203
1204    #[test]
1205    fn find_by_outpoint_locates_spending_tx() {
1206        let mut pool = Mempool::new(MempoolLimits::default());
1207        let prev_txid = bitcoin::Txid::from_byte_array([0xaa; 32]);
1208        let outpoint = bitcoin::OutPoint {
1209            txid: prev_txid,
1210            vout: 7,
1211        };
1212        let spending = bitcoin::Transaction {
1213            version: bitcoin::transaction::Version(2),
1214            lock_time: bitcoin::absolute::LockTime::ZERO,
1215            input: vec![bitcoin::TxIn {
1216                previous_output: outpoint,
1217                script_sig: bitcoin::ScriptBuf::new(),
1218                sequence: bitcoin::Sequence::MAX,
1219                witness: bitcoin::Witness::new(),
1220            }],
1221            output: vec![bitcoin::TxOut {
1222                value: bitcoin::Amount::from_sat(99_000),
1223                script_pubkey: bitcoin::ScriptBuf::from_bytes(vec![0x51]),
1224            }],
1225        };
1226        let spending_txid = spending.compute_txid();
1227        let _ = pool.insert_entry(MempoolEntry::new(Arc::new(spending), 100, 10_000, 1, 7));
1228        assert_eq!(pool.find_by_outpoint(&outpoint), Some(spending_txid));
1229    }
1230
1231    #[test]
1232    fn find_by_outpoint_returns_none_for_unspent_outpoint() {
1233        let pool = Mempool::new(MempoolLimits::default());
1234        let outpoint = bitcoin::OutPoint {
1235            txid: bitcoin::Txid::from_byte_array([0xff; 32]),
1236            vout: 0,
1237        };
1238        assert_eq!(pool.find_by_outpoint(&outpoint), None);
1239    }
1240
1241    #[test]
1242    fn is_outpoint_spent_returns_true_after_insert() -> Result<(), MempoolError> {
1243        let mut pool = Mempool::new(MempoolLimits {
1244            min_relay_fee_sat_per_kvb: 0,
1245            ..MempoolLimits::default()
1246        });
1247        let prev_txid = bitcoin::Txid::from_byte_array([0xaa_u8; 32]);
1248        let outpoint = bitcoin::OutPoint {
1249            txid: prev_txid,
1250            vout: 1,
1251        };
1252        let spending = bitcoin::Transaction {
1253            version: bitcoin::transaction::Version(2),
1254            lock_time: bitcoin::absolute::LockTime::ZERO,
1255            input: vec![bitcoin::TxIn {
1256                previous_output: outpoint,
1257                script_sig: bitcoin::ScriptBuf::new(),
1258                sequence: bitcoin::Sequence(0xFFFF_FFFF),
1259                witness: bitcoin::Witness::new(),
1260            }],
1261            output: vec![],
1262        };
1263        pool.insert_entry(MempoolEntry::new(Arc::new(spending), 100, 10_000, 1, 7))?;
1264        assert!(pool.is_outpoint_spent(&outpoint));
1265        Ok(())
1266    }
1267
1268    #[test]
1269    fn is_outpoint_spent_returns_false_for_unspent_outpoint() {
1270        let pool = Mempool::new(MempoolLimits::default());
1271        let outpoint = bitcoin::OutPoint {
1272            txid: bitcoin::Txid::from_byte_array([0xff_u8; 32]),
1273            vout: 0,
1274        };
1275        assert!(!pool.is_outpoint_spent(&outpoint));
1276    }
1277
1278    #[test]
1279    fn remove_by_txid_returns_empty_for_unknown_txid() {
1280        let mut pool = Mempool::new(MempoolLimits::default());
1281
1282        let removed = pool.remove_by_txid(&bitcoin::Txid::all_zeros());
1283
1284        assert!(removed.is_empty());
1285        assert_eq!(pool.len(), 0);
1286    }
1287
1288    #[test]
1289    fn remove_by_txid_removes_entry_and_descendants_when_present() -> Result<(), MempoolError> {
1290        let mut pool = Mempool::new(MempoolLimits::default());
1291        let tx = Transaction {
1292            version: bitcoin::transaction::Version::TWO,
1293            lock_time: bitcoin::absolute::LockTime::ZERO,
1294            input: Vec::new(),
1295            output: Vec::new(),
1296        };
1297        let txid = tx.compute_txid();
1298        let entry = MempoolEntry::new(Arc::new(tx), 123, 4_567, 0, 0);
1299        let id = pool.insert_entry(entry)?;
1300
1301        let removed = pool.remove_by_txid(&txid);
1302
1303        assert_eq!(removed.len(), 1);
1304        assert_eq!(removed.first().copied(), Some(id));
1305        assert_eq!(pool.len(), 0);
1306        Ok(())
1307    }
1308
1309    #[test]
1310    fn descendant_ids_for_entry_returns_descendants_excluding_origin() -> Result<(), MempoolError> {
1311        let mut pool = Mempool::new(MempoolLimits::default());
1312        let parent = tx(1, Vec::new());
1313        let parent_txid = parent.compute_txid();
1314        let parent_id = pool.insert_entry(MempoolEntry::new(Arc::new(parent), 100, 1_000, 0, 0))?;
1315        let child = tx(2, vec![OutPoint::new(parent_txid, 0)]);
1316        let child_id = pool.insert_entry(MempoolEntry::new(Arc::new(child), 100, 1_000, 0, 0))?;
1317
1318        let descendants = pool.descendant_ids_for_entry(parent_id);
1319
1320        assert_eq!(descendants, vec![child_id]);
1321        Ok(())
1322    }
1323
1324    #[test]
1325    fn descendant_count_inclusive_returns_one_for_lone_tx() -> Result<(), MempoolError> {
1326        let mut pool = Mempool::new(MempoolLimits::default());
1327        let lone = tx(1, Vec::new());
1328        let lone_txid = lone.compute_txid();
1329        pool.insert_entry(MempoolEntry::new(Arc::new(lone), 500, 1_000, 1, 7))?;
1330        let Some(&id) = pool.by_txid.get(&lone_txid) else {
1331            panic!("insert failed");
1332        };
1333        assert_eq!(pool.descendant_count_inclusive(id), 1);
1334        assert_eq!(pool.ancestor_count_inclusive(id), 1);
1335        Ok(())
1336    }
1337
1338    #[test]
1339    fn prioritise_bumps_fee_and_rate() -> Result<(), MempoolError> {
1340        let mut pool = Mempool::new(MempoolLimits::default());
1341        let tx = tx(1, Vec::new());
1342        let txid = tx.compute_txid();
1343        let entry = MempoolEntry::new(Arc::new(tx), 100, 1_000, 1, 7);
1344        let _id = pool.insert_entry(entry)?;
1345
1346        assert!(pool.prioritise(txid, 500));
1347
1348        let Some(&id) = pool.by_txid.get(&txid) else {
1349            panic!("tx missing after prioritise");
1350        };
1351        let Some(entry) = pool.entry(id) else {
1352            panic!("entry missing");
1353        };
1354        assert_eq!(entry.fee, 1_500);
1355        assert_eq!(entry.fee_rate, 15_000);
1356        Ok(())
1357    }
1358
1359    #[test]
1360    fn prioritise_saturates_negative_delta_at_zero() -> Result<(), MempoolError> {
1361        let mut pool = Mempool::new(MempoolLimits::default());
1362        let tx = tx(2, Vec::new());
1363        let txid = tx.compute_txid();
1364        let entry = MempoolEntry::new(Arc::new(tx), 100, 1_000, 1, 7);
1365        let _id = pool.insert_entry(entry)?;
1366
1367        assert!(pool.prioritise(txid, -2_000));
1368
1369        let Some(&id) = pool.by_txid.get(&txid) else {
1370            panic!("tx missing after prioritise");
1371        };
1372        let Some(entry) = pool.entry(id) else {
1373            panic!("entry missing");
1374        };
1375        assert_eq!(entry.fee, 0);
1376        assert_eq!(entry.fee_rate, 0);
1377        Ok(())
1378    }
1379
1380    #[test]
1381    fn prioritise_propagates_delta_to_ancestor_descendant_fees() -> Result<(), MempoolError> {
1382        let mut pool = Mempool::new(MempoolLimits::default());
1383        let parent = tx(5, Vec::new());
1384        let parent_txid = parent.compute_txid();
1385        let parent_id = pool.insert_entry(MempoolEntry::new(Arc::new(parent), 100, 1_000, 0, 0))?;
1386        let child = tx(6, vec![OutPoint::new(parent_txid, 0)]);
1387        let child_txid = child.compute_txid();
1388        let child_id = pool.insert_entry(MempoolEntry::new(Arc::new(child), 100, 2_000, 0, 0))?;
1389        let grandchild = tx(7, vec![OutPoint::new(child_txid, 0)]);
1390        let grandchild_id =
1391            pool.insert_entry(MempoolEntry::new(Arc::new(grandchild), 100, 3_000, 0, 0))?;
1392
1393        let Some(parent_before) = pool.entry(parent_id) else {
1394            panic!("missing parent");
1395        };
1396        let parent_descendant_fee = parent_before.descendant_fee;
1397        let Some(child_before) = pool.entry(child_id) else {
1398            panic!("missing child");
1399        };
1400        let child_ancestor_fee = child_before.ancestor_fee;
1401        let Some(grandchild_before) = pool.entry(grandchild_id) else {
1402            panic!("missing grandchild");
1403        };
1404        let grandchild_ancestor_fee = grandchild_before.ancestor_fee;
1405
1406        assert!(pool.prioritise(child_txid, 500));
1407
1408        let Some(parent_after) = pool.entry(parent_id) else {
1409            panic!("missing parent");
1410        };
1411        assert_eq!(
1412            parent_after.descendant_fee,
1413            parent_descendant_fee.saturating_add(500)
1414        );
1415        let Some(child_after) = pool.entry(child_id) else {
1416            panic!("missing child");
1417        };
1418        assert_eq!(
1419            child_after.ancestor_fee,
1420            child_ancestor_fee.saturating_add(500)
1421        );
1422        let Some(grandchild_after) = pool.entry(grandchild_id) else {
1423            panic!("missing grandchild");
1424        };
1425        assert_eq!(
1426            grandchild_after.ancestor_fee,
1427            grandchild_ancestor_fee.saturating_add(500)
1428        );
1429        Ok(())
1430    }
1431
1432    #[test]
1433    fn prioritise_returns_false_for_unknown_txid() {
1434        let mut pool = Mempool::new(MempoolLimits::default());
1435
1436        assert!(!pool.prioritise(Txid::all_zeros(), 100));
1437    }
1438
1439    #[test]
1440    fn prioritise_reorders_priority_index() -> Result<(), MempoolError> {
1441        let mut pool = Mempool::new(MempoolLimits::default());
1442        let lower_fee_tx = tx(3, Vec::new());
1443        let lower_fee_txid = lower_fee_tx.compute_txid();
1444        let lower_fee_id =
1445            pool.insert_entry(MempoolEntry::new(Arc::new(lower_fee_tx), 100, 1_000, 1, 7))?;
1446        let higher_fee_tx = tx(4, Vec::new());
1447        let higher_fee_id =
1448            pool.insert_entry(MempoolEntry::new(Arc::new(higher_fee_tx), 100, 2_000, 2, 7))?;
1449
1450        assert_eq!(
1451            pool.pareto.top_n(1).collect::<Vec<_>>(),
1452            vec![higher_fee_id]
1453        );
1454        assert!(pool.prioritise(lower_fee_txid, 2_000));
1455
1456        assert_eq!(pool.pareto.top_n(1).collect::<Vec<_>>(), vec![lower_fee_id]);
1457        Ok(())
1458    }
1459
1460    #[test]
1461    fn evict_below_fee_rate_removes_low_fee_entries_only() -> Result<(), MempoolError> {
1462        let mut pool = Mempool::new(MempoolLimits {
1463            min_relay_fee_sat_per_kvb: 0,
1464            ..MempoolLimits::default()
1465        });
1466        let low = Transaction {
1467            version: bitcoin::transaction::Version::TWO,
1468            lock_time: bitcoin::absolute::LockTime::ZERO,
1469            input: Vec::new(),
1470            output: vec![TxOut {
1471                value: Amount::from_sat(1_000),
1472                script_pubkey: ScriptBuf::from_bytes(vec![0x51]),
1473            }],
1474        };
1475        let low_txid = low.compute_txid();
1476        pool.insert_entry(MempoolEntry::new(Arc::new(low), 100, 100, 1, 7))?;
1477
1478        let high = Transaction {
1479            version: bitcoin::transaction::Version::TWO,
1480            lock_time: bitcoin::absolute::LockTime::ZERO,
1481            input: Vec::new(),
1482            output: vec![TxOut {
1483                value: Amount::from_sat(99_000),
1484                script_pubkey: ScriptBuf::from_bytes(vec![0x52]),
1485            }],
1486        };
1487        let high_txid = high.compute_txid();
1488        pool.insert_entry(MempoolEntry::new(Arc::new(high), 100, 10_000, 1, 7))?;
1489
1490        let evicted = pool.evict_below_fee_rate(5_000);
1491
1492        assert_eq!(evicted, vec![low_txid]);
1493        assert!(!pool.by_txid.contains_key(&low_txid));
1494        assert!(pool.by_txid.contains_key(&high_txid));
1495        Ok(())
1496    }
1497
1498    #[test]
1499    fn enforce_size_limit_evicts_lowest_fee_until_below_target() -> Result<(), MempoolError> {
1500        let mut pool = Mempool::new(MempoolLimits {
1501            min_relay_fee_sat_per_kvb: 0,
1502            ..MempoolLimits::default()
1503        });
1504
1505        let low = Transaction {
1506            version: bitcoin::transaction::Version::TWO,
1507            lock_time: bitcoin::absolute::LockTime::ZERO,
1508            input: Vec::new(),
1509            output: vec![TxOut {
1510                value: Amount::from_sat(1_000),
1511                script_pubkey: ScriptBuf::from_bytes(vec![0x51]),
1512            }],
1513        };
1514        let low_txid = low.compute_txid();
1515        pool.insert_entry(MempoolEntry::new(Arc::new(low), 500, 100, 1, 7))?;
1516
1517        let high = Transaction {
1518            version: bitcoin::transaction::Version::TWO,
1519            lock_time: bitcoin::absolute::LockTime::ZERO,
1520            input: Vec::new(),
1521            output: vec![TxOut {
1522                value: Amount::from_sat(99_000),
1523                script_pubkey: ScriptBuf::from_bytes(vec![0x52]),
1524            }],
1525        };
1526        let high_txid = high.compute_txid();
1527        pool.insert_entry(MempoolEntry::new(Arc::new(high), 500, 10_000, 1, 7))?;
1528
1529        let evicted = pool.enforce_size_limit(600);
1530
1531        assert_eq!(evicted.len(), 1);
1532        assert!(!pool.by_txid.contains_key(&low_txid));
1533        assert!(pool.by_txid.contains_key(&high_txid));
1534        assert!(pool.total_vsize() <= 600);
1535        Ok(())
1536    }
1537
1538    #[test]
1539    fn insert_entry_triggers_size_limit_eviction_when_overflow() {
1540        let limits = MempoolLimits {
1541            max_total_bytes: 1_000,
1542            min_relay_fee_sat_per_kvb: 0,
1543            ..MempoolLimits::default()
1544        };
1545        let mut pool = Mempool::new(limits);
1546
1547        for nonce in 0..2_u32 {
1548            let tx = bitcoin::Transaction {
1549                version: bitcoin::transaction::Version(2),
1550                lock_time: bitcoin::absolute::LockTime::ZERO,
1551                input: Vec::new(),
1552                output: vec![bitcoin::TxOut {
1553                    value: bitcoin::Amount::from_sat(1_000 + u64::from(nonce)),
1554                    script_pubkey: bitcoin::ScriptBuf::from_bytes(vec![
1555                        0x51,
1556                        u8::try_from(nonce).unwrap_or(0),
1557                    ]),
1558                }],
1559            };
1560            let fee = 100_u64.saturating_add(u64::from(nonce).saturating_mul(50));
1561
1562            let _ = pool.insert_entry(MempoolEntry::new(Arc::new(tx), 600, fee, 1, 7));
1563        }
1564
1565        assert!(
1566            pool.total_vsize() <= 1_000,
1567            "size limit must hold after inserts: {}",
1568            pool.total_vsize()
1569        );
1570    }
1571
1572    fn tx(label: u8, previous_outputs: Vec<OutPoint>) -> Transaction {
1573        Transaction {
1574            version: bitcoin::transaction::Version::TWO,
1575            lock_time: bitcoin::absolute::LockTime::ZERO,
1576            input: previous_outputs
1577                .into_iter()
1578                .map(|previous_output| TxIn {
1579                    previous_output,
1580                    script_sig: ScriptBuf::new(),
1581                    sequence: Sequence::MAX,
1582                    witness: Witness::new(),
1583                })
1584                .collect(),
1585            output: vec![TxOut {
1586                value: Amount::from_sat(5_000 + u64::from(label)),
1587                script_pubkey: ScriptBuf::from_bytes(vec![label]),
1588            }],
1589        }
1590    }
1591}