Skip to main content

abtc_application/
orphan_pool.rs

1//! Orphan Transaction Pool
2//!
3//! Holds transactions whose parent inputs are not yet available (missing from
4//! both the UTXO set and the mempool). When a parent transaction arrives and
5//! is accepted, orphans that depend on it are re-evaluated.
6//!
7//! This corresponds to Bitcoin Core's `mapOrphanTransactions` /
8//! `OrphanageAddTx` logic in `txorphanage.cpp`.
9//!
10//! ## Design
11//!
12//! - Orphans are keyed by txid for O(1) lookup and removal.
13//! - A reverse index (`by_prev`) maps each missing parent outpoint to the set
14//!   of orphan txids that need it. This allows efficient re-evaluation when a
15//!   new transaction confirms.
16//! - Each orphan has an expiry timestamp; `expire_old_orphans()` prunes stale
17//!   entries that have lingered too long.
18//! - A hard cap (`MAX_ORPHAN_TRANSACTIONS`) prevents memory exhaustion from
19//!   flooding attacks. When full, a random existing orphan is evicted.
20
21use abtc_domain::primitives::{OutPoint, Transaction, Txid};
22use std::collections::{HashMap, HashSet};
23
24// ── Configuration ───────────────────────────────────────────────────
25
26/// Maximum number of orphan transactions we will hold.
27const MAX_ORPHAN_TRANSACTIONS: usize = 100;
28
29/// How long (in seconds) an orphan may linger before expiry.
30const ORPHAN_TX_EXPIRE_TIME: u64 = 20 * 60; // 20 minutes
31
32/// Maximum size (in bytes) of a single orphan transaction we accept.
33const MAX_ORPHAN_TX_SIZE: usize = 100_000; // 100 KB
34
35// ── Types ───────────────────────────────────────────────────────────
36
37/// An orphan transaction and its metadata.
38#[derive(Debug, Clone)]
39pub struct OrphanEntry {
40    /// The transaction itself.
41    pub tx: Transaction,
42    /// Unix timestamp when this orphan was added.
43    pub time_added: u64,
44    /// The peer that sent us this orphan (for eviction priority).
45    pub from_peer: u64,
46    /// Serialized size of the transaction.
47    pub size: usize,
48}
49
50/// Result of attempting to add an orphan.
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub enum AddOrphanResult {
53    /// Successfully added as a new orphan.
54    Added,
55    /// Already in the orphan pool.
56    AlreadyExists,
57    /// Rejected: transaction too large.
58    TooLarge,
59    /// Rejected: pool is full and eviction occurred; orphan was added.
60    AddedAfterEviction { evicted: Txid },
61}
62
63/// The orphan transaction pool.
64pub struct OrphanPool {
65    /// Orphan transactions keyed by txid.
66    orphans: HashMap<Txid, OrphanEntry>,
67    /// Reverse index: for each outpoint, which orphan txids need it.
68    by_prev: HashMap<OutPoint, HashSet<Txid>>,
69    /// Maximum number of orphans to hold.
70    max_orphans: usize,
71    /// Expiry time in seconds.
72    expire_time: u64,
73}
74
75impl OrphanPool {
76    /// Create a new orphan pool with default settings.
77    pub fn new() -> Self {
78        OrphanPool {
79            orphans: HashMap::new(),
80            by_prev: HashMap::new(),
81            max_orphans: MAX_ORPHAN_TRANSACTIONS,
82            expire_time: ORPHAN_TX_EXPIRE_TIME,
83        }
84    }
85
86    /// Create with custom limits (useful for testing).
87    pub fn with_config(max_orphans: usize, expire_time: u64) -> Self {
88        OrphanPool {
89            orphans: HashMap::new(),
90            by_prev: HashMap::new(),
91            max_orphans,
92            expire_time,
93        }
94    }
95
96    /// Add a transaction to the orphan pool.
97    ///
98    /// Returns the result of the add operation.
99    pub fn add_orphan(&mut self, tx: Transaction, from_peer: u64, now: u64) -> AddOrphanResult {
100        let txid = tx.txid();
101
102        // Already known?
103        if self.orphans.contains_key(&txid) {
104            return AddOrphanResult::AlreadyExists;
105        }
106
107        // Size check
108        let size = tx.serialize().len();
109        if size > MAX_ORPHAN_TX_SIZE {
110            return AddOrphanResult::TooLarge;
111        }
112
113        // Evict if at capacity
114        let evicted = if self.orphans.len() >= self.max_orphans {
115            self.evict_random()
116        } else {
117            None
118        };
119
120        // Build reverse index entries for all inputs
121        for input in &tx.inputs {
122            self.by_prev
123                .entry(input.previous_output)
124                .or_default()
125                .insert(txid);
126        }
127
128        self.orphans.insert(
129            txid,
130            OrphanEntry {
131                tx,
132                time_added: now,
133                from_peer,
134                size,
135            },
136        );
137
138        match evicted {
139            Some(evicted_txid) => AddOrphanResult::AddedAfterEviction {
140                evicted: evicted_txid,
141            },
142            None => AddOrphanResult::Added,
143        }
144    }
145
146    /// Remove an orphan by txid. Returns the entry if it was present.
147    pub fn remove_orphan(&mut self, txid: &Txid) -> Option<OrphanEntry> {
148        if let Some(entry) = self.orphans.remove(txid) {
149            // Clean up reverse index
150            for input in &entry.tx.inputs {
151                if let Some(set) = self.by_prev.get_mut(&input.previous_output) {
152                    set.remove(txid);
153                    if set.is_empty() {
154                        self.by_prev.remove(&input.previous_output);
155                    }
156                }
157            }
158            Some(entry)
159        } else {
160            None
161        }
162    }
163
164    /// Get all orphan txids that depend on a given outpoint.
165    ///
166    /// Call this when a new transaction is accepted to find orphans
167    /// that can now be re-evaluated.
168    pub fn get_orphans_by_prev(&self, outpoint: &OutPoint) -> Vec<Txid> {
169        self.by_prev
170            .get(outpoint)
171            .map(|set| set.iter().copied().collect())
172            .unwrap_or_default()
173    }
174
175    /// Get all orphan txids that depend on any output of a given transaction.
176    ///
177    /// This is the common pattern: when tx `parent_txid` is accepted, find
178    /// all orphans that spent one of its outputs.
179    pub fn get_children_of(&self, parent_txid: &Txid, output_count: u32) -> Vec<Txid> {
180        let mut children = HashSet::new();
181        for vout in 0..output_count {
182            let outpoint = OutPoint::new(*parent_txid, vout);
183            if let Some(set) = self.by_prev.get(&outpoint) {
184                children.extend(set.iter().copied());
185            }
186        }
187        children.into_iter().collect()
188    }
189
190    /// Remove all orphans that are older than the expiry time.
191    ///
192    /// Returns the number of orphans removed.
193    pub fn expire_old_orphans(&mut self, now: u64) -> usize {
194        let expired: Vec<Txid> = self
195            .orphans
196            .iter()
197            .filter(|(_, entry)| now.saturating_sub(entry.time_added) > self.expire_time)
198            .map(|(txid, _)| *txid)
199            .collect();
200
201        let count = expired.len();
202        for txid in expired {
203            self.remove_orphan(&txid);
204        }
205        count
206    }
207
208    /// Remove all orphans from a specific peer.
209    ///
210    /// Useful when a peer is banned or disconnected.
211    pub fn remove_for_peer(&mut self, peer_id: u64) -> usize {
212        let to_remove: Vec<Txid> = self
213            .orphans
214            .iter()
215            .filter(|(_, entry)| entry.from_peer == peer_id)
216            .map(|(txid, _)| *txid)
217            .collect();
218
219        let count = to_remove.len();
220        for txid in to_remove {
221            self.remove_orphan(&txid);
222        }
223        count
224    }
225
226    /// Get an orphan entry by txid.
227    pub fn get(&self, txid: &Txid) -> Option<&OrphanEntry> {
228        self.orphans.get(txid)
229    }
230
231    /// Check if we have an orphan with the given txid.
232    pub fn contains(&self, txid: &Txid) -> bool {
233        self.orphans.contains_key(txid)
234    }
235
236    /// Get the number of orphan transactions in the pool.
237    pub fn len(&self) -> usize {
238        self.orphans.len()
239    }
240
241    /// Check if the pool is empty.
242    pub fn is_empty(&self) -> bool {
243        self.orphans.is_empty()
244    }
245
246    /// Clear all orphans.
247    pub fn clear(&mut self) {
248        self.orphans.clear();
249        self.by_prev.clear();
250    }
251
252    /// Evict a random orphan (deterministic: pick the first by iteration order).
253    ///
254    /// In a real implementation this would use a random selection to prevent
255    /// targeted eviction attacks. For simplicity we pick the oldest by time_added.
256    fn evict_random(&mut self) -> Option<Txid> {
257        let oldest = self
258            .orphans
259            .iter()
260            .min_by_key(|(_, entry)| entry.time_added)
261            .map(|(txid, _)| *txid);
262
263        if let Some(txid) = oldest {
264            self.remove_orphan(&txid);
265            Some(txid)
266        } else {
267            None
268        }
269    }
270}
271
272impl Default for OrphanPool {
273    fn default() -> Self {
274        Self::new()
275    }
276}
277
278#[cfg(test)]
279mod tests {
280    use super::*;
281    use abtc_domain::primitives::{Amount, Hash256, OutPoint, TxIn, TxOut};
282    use abtc_domain::script::Script;
283
284    fn make_tx(prev_txid: Txid, prev_vout: u32, value: i64) -> Transaction {
285        Transaction::v1(
286            vec![TxIn::final_input(
287                OutPoint::new(prev_txid, prev_vout),
288                Script::new(),
289            )],
290            vec![TxOut::new(Amount::from_sat(value), Script::new())],
291            0,
292        )
293    }
294
295    fn make_txid(byte: u8) -> Txid {
296        Txid::from_hash(Hash256::from_bytes([byte; 32]))
297    }
298
299    #[test]
300    fn test_add_orphan() {
301        let mut pool = OrphanPool::new();
302        let parent_txid = make_txid(0x01);
303        let tx = make_tx(parent_txid, 0, 5000);
304        let txid = tx.txid();
305
306        let result = pool.add_orphan(tx, 1, 1000);
307        assert_eq!(result, AddOrphanResult::Added);
308        assert_eq!(pool.len(), 1);
309        assert!(pool.contains(&txid));
310    }
311
312    #[test]
313    fn test_add_duplicate() {
314        let mut pool = OrphanPool::new();
315        let parent_txid = make_txid(0x01);
316        let tx = make_tx(parent_txid, 0, 5000);
317
318        pool.add_orphan(tx.clone(), 1, 1000);
319        let result = pool.add_orphan(tx, 1, 1001);
320        assert_eq!(result, AddOrphanResult::AlreadyExists);
321        assert_eq!(pool.len(), 1);
322    }
323
324    #[test]
325    fn test_remove_orphan() {
326        let mut pool = OrphanPool::new();
327        let parent_txid = make_txid(0x01);
328        let tx = make_tx(parent_txid, 0, 5000);
329        let txid = tx.txid();
330
331        pool.add_orphan(tx, 1, 1000);
332        let removed = pool.remove_orphan(&txid);
333        assert!(removed.is_some());
334        assert_eq!(pool.len(), 0);
335        assert!(!pool.contains(&txid));
336    }
337
338    #[test]
339    fn test_get_orphans_by_prev() {
340        let mut pool = OrphanPool::new();
341        let parent_txid = make_txid(0x01);
342
343        // Two orphans that both spend different outputs of the same parent
344        let tx1 = make_tx(parent_txid, 0, 3000);
345        let tx2 = make_tx(parent_txid, 1, 2000);
346        let txid1 = tx1.txid();
347        let txid2 = tx2.txid();
348
349        pool.add_orphan(tx1, 1, 1000);
350        pool.add_orphan(tx2, 1, 1000);
351
352        let children1 = pool.get_orphans_by_prev(&OutPoint::new(parent_txid, 0));
353        assert_eq!(children1.len(), 1);
354        assert_eq!(children1[0], txid1);
355
356        let children2 = pool.get_orphans_by_prev(&OutPoint::new(parent_txid, 1));
357        assert_eq!(children2.len(), 1);
358        assert_eq!(children2[0], txid2);
359    }
360
361    #[test]
362    fn test_get_children_of() {
363        let mut pool = OrphanPool::new();
364        let parent_txid = make_txid(0x01);
365
366        let tx1 = make_tx(parent_txid, 0, 3000);
367        let tx2 = make_tx(parent_txid, 1, 2000);
368
369        pool.add_orphan(tx1, 1, 1000);
370        pool.add_orphan(tx2, 1, 1000);
371
372        let children = pool.get_children_of(&parent_txid, 2);
373        assert_eq!(children.len(), 2);
374    }
375
376    #[test]
377    fn test_expire_old_orphans() {
378        let mut pool = OrphanPool::with_config(100, 600); // 10 min expiry
379        let tx1 = make_tx(make_txid(0x01), 0, 3000);
380        let tx2 = make_tx(make_txid(0x02), 0, 2000);
381
382        pool.add_orphan(tx1, 1, 1000);
383        pool.add_orphan(tx2, 1, 1500);
384
385        // At t=1500 neither is expired (within 600s)
386        let removed = pool.expire_old_orphans(1500);
387        assert_eq!(removed, 0);
388        assert_eq!(pool.len(), 2);
389
390        // At t=1700 tx1 is expired (1700 - 1000 = 700 > 600) but tx2 is not (200 < 600)
391        let removed = pool.expire_old_orphans(1700);
392        assert_eq!(removed, 1);
393        assert_eq!(pool.len(), 1);
394    }
395
396    #[test]
397    fn test_eviction_when_full() {
398        let mut pool = OrphanPool::with_config(3, 600);
399
400        let tx1 = make_tx(make_txid(0x01), 0, 1000);
401        let tx2 = make_tx(make_txid(0x02), 0, 2000);
402        let tx3 = make_tx(make_txid(0x03), 0, 3000);
403        let tx4 = make_tx(make_txid(0x04), 0, 4000);
404
405        pool.add_orphan(tx1, 1, 100);
406        pool.add_orphan(tx2, 1, 200);
407        pool.add_orphan(tx3, 1, 300);
408        assert_eq!(pool.len(), 3);
409
410        // Adding a 4th should evict the oldest (tx1 at time 100)
411        let result = pool.add_orphan(tx4, 1, 400);
412        match result {
413            AddOrphanResult::AddedAfterEviction { .. } => {}
414            _ => panic!("Expected AddedAfterEviction"),
415        }
416        assert_eq!(pool.len(), 3); // Still at max
417    }
418
419    #[test]
420    fn test_remove_for_peer() {
421        let mut pool = OrphanPool::new();
422        let tx1 = make_tx(make_txid(0x01), 0, 1000);
423        let tx2 = make_tx(make_txid(0x02), 0, 2000);
424        let tx3 = make_tx(make_txid(0x03), 0, 3000);
425
426        pool.add_orphan(tx1, 1, 100); // peer 1
427        pool.add_orphan(tx2, 2, 200); // peer 2
428        pool.add_orphan(tx3, 1, 300); // peer 1
429
430        let removed = pool.remove_for_peer(1);
431        assert_eq!(removed, 2);
432        assert_eq!(pool.len(), 1);
433    }
434
435    #[test]
436    fn test_clear() {
437        let mut pool = OrphanPool::new();
438        pool.add_orphan(make_tx(make_txid(0x01), 0, 1000), 1, 100);
439        pool.add_orphan(make_tx(make_txid(0x02), 0, 2000), 1, 200);
440        assert_eq!(pool.len(), 2);
441
442        pool.clear();
443        assert_eq!(pool.len(), 0);
444        assert!(pool.is_empty());
445    }
446
447    #[test]
448    fn test_reverse_index_cleanup() {
449        let mut pool = OrphanPool::new();
450        let parent_txid = make_txid(0x01);
451        let tx = make_tx(parent_txid, 0, 5000);
452        let txid = tx.txid();
453
454        pool.add_orphan(tx, 1, 1000);
455        assert!(!pool
456            .get_orphans_by_prev(&OutPoint::new(parent_txid, 0))
457            .is_empty());
458
459        let _ = pool.remove_orphan(&txid);
460        assert!(pool
461            .get_orphans_by_prev(&OutPoint::new(parent_txid, 0))
462            .is_empty());
463    }
464
465    #[test]
466    fn test_default_trait() {
467        let pool = OrphanPool::default();
468        assert_eq!(pool.len(), 0);
469    }
470}