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#[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 pub hash: Hash256,
33}
34
35impl ScriptHash {
36 #[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#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
48pub enum MempoolError {
49 #[error("transaction already exists in mempool")]
51 DuplicateTransaction,
52 #[error("mempool entry id space exhausted")]
54 TooManyEntries,
55 #[error(transparent)]
57 Policy(#[from] PolicyError),
58}
59
60#[derive(Debug)]
62pub struct Mempool {
63 pub entries: Slab<MempoolEntry>,
65 pub by_txid: HashMap<Txid, EntryId>,
67 pub funding: std::collections::BTreeSet<(ScriptHash, EntryId)>,
69 pub spending: std::collections::BTreeSet<(OutPoint, EntryId)>,
71 pub pareto: ParetoFront,
73 pub limits: MempoolLimits,
75 sequence: core::sync::atomic::AtomicU64,
76}
77#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
80pub struct MempoolStats {
81 pub txs: u64,
83 pub bytes: u64,
85 pub total_fee: u64,
87}
88
89impl Mempool {
90 #[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 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 #[must_use]
117 pub fn sequence_number(&self) -> u64 {
118 self.sequence.load(core::sync::atomic::Ordering::Acquire)
119 }
120
121 #[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 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 #[must_use]
181 pub fn len(&self) -> usize {
182 self.entries.len()
183 }
184
185 #[must_use]
190 pub fn contains_txid(&self, txid: &bitcoin::Txid) -> bool {
191 self.by_txid.contains_key(txid)
192 }
193
194 #[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 #[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 #[must_use]
217 pub fn is_empty(&self) -> bool {
218 self.entries.is_empty()
219 }
220
221 #[must_use]
223 pub fn tx_count(&self) -> usize {
224 self.entries.len()
225 }
226
227 #[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 #[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 #[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 pub fn enforce_size_limit(&mut self, max_bytes: u64) -> Vec<EntryId> {
267 crate::evict_lowest_fee_packages(self, max_bytes)
268 }
269
270 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[must_use]
373 pub fn is_outpoint_spent(&self, outpoint: &bitcoin::OutPoint) -> bool {
374 self.find_by_outpoint(outpoint).is_some()
375 }
376
377 #[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 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 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 #[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 #[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 #[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 #[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 #[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 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 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)); 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)); 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 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 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}