subcoin_mempool/
types.rs

1//! Core type definitions for the mempool.
2
3use bitcoin::{Amount, BlockHash, Transaction, Txid};
4use slotmap::DefaultKey;
5use std::collections::HashSet;
6use std::sync::Arc;
7
8/// Handle to entry in mempool arena (not an iterator).
9#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
10pub struct EntryId(pub(crate) DefaultKey);
11
12/// Fee rate in satoshis per virtual kilobyte.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
14pub struct FeeRate(pub u64);
15
16impl FeeRate {
17    /// Create fee rate from satoshis per virtual byte.
18    pub fn from_sat_per_vb(sat_vb: u64) -> Self {
19        Self(sat_vb.checked_mul(1000).expect("Fee rate overflow"))
20    }
21
22    /// Create fee rate from satoshis per kilovirtual byte.
23    pub fn from_sat_per_kvb(sat_kvb: u64) -> Self {
24        Self(sat_kvb)
25    }
26
27    /// Calculate fee rate from amount and vsize with overflow protection.
28    ///
29    /// Returns fee rate in sat/kvB.
30    pub fn from_amount_and_vsize(fee: Amount, vsize: i64) -> Result<Self, &'static str> {
31        if vsize <= 0 {
32            return Err("vsize must be positive");
33        }
34
35        let fee_sat = fee.to_sat();
36        let vsize_u64 = vsize as u64;
37
38        // Calculate fee_sat * 1000 / vsize with overflow protection
39        let numerator = fee_sat
40            .checked_mul(1000)
41            .ok_or("Fee rate calculation overflow")?;
42
43        Ok(Self(numerator / vsize_u64))
44    }
45
46    /// Get fee for given virtual size.
47    pub fn get_fee(&self, vsize: i64) -> Amount {
48        let fee_sat = (self
49            .0
50            .checked_mul(vsize as u64)
51            .expect("Fee calculation overflow"))
52        .checked_div(1000)
53        .unwrap_or(0);
54        Amount::from_sat(fee_sat)
55    }
56
57    /// Get the fee rate in satoshis per kilovirtual byte.
58    pub fn as_sat_per_kvb(&self) -> u64 {
59        self.0
60    }
61}
62
63/// Lock points for BIP68/BIP112 validation.
64#[derive(Debug, Clone, Default)]
65pub struct LockPoints {
66    /// Height at which transaction becomes valid.
67    pub height: i32,
68    /// Time at which transaction becomes valid.
69    pub time: i64,
70    /// Highest block containing an input of this transaction.
71    pub max_input_block: Option<BlockHash>,
72}
73
74/// Result of transaction pre-validation.
75pub struct ValidationResult {
76    /// Base fee paid by transaction.
77    pub base_fee: Amount,
78    /// Signature operation cost.
79    pub sigop_cost: i64,
80    /// Lock points for BIP68/112.
81    pub lock_points: LockPoints,
82    /// Set of ancestor entry IDs in mempool.
83    pub ancestors: HashSet<EntryId>,
84    /// Set of conflicting transaction IDs.
85    pub conflicts: HashSet<Txid>,
86    /// Whether this transaction spends a coinbase output.
87    pub spends_coinbase: bool,
88}
89
90/// Reason for removing transactions from mempool.
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub enum RemovalReason {
93    /// Included in a block.
94    Block,
95    /// Chain reorganization.
96    Reorg,
97    /// Conflicted with another transaction.
98    Conflict,
99    /// Replaced by higher-fee transaction (RBF).
100    Replaced,
101    /// Evicted due to mempool size limit.
102    SizeLimit,
103    /// Expired (too old).
104    Expiry,
105}
106
107impl RemovalReason {
108    pub fn as_str(&self) -> &'static str {
109        match self {
110            Self::Block => "block",
111            Self::Reorg => "reorg",
112            Self::Conflict => "conflict",
113            Self::Replaced => "replaced",
114            Self::SizeLimit => "sizelimit",
115            Self::Expiry => "expiry",
116        }
117    }
118}
119
120/// Set of transactions being replaced by RBF.
121#[derive(Debug, Clone)]
122pub struct ConflictSet {
123    /// Direct conflicts (txs spending same outputs).
124    pub direct_conflicts: HashSet<EntryId>,
125
126    /// All affected (conflicts + descendants).
127    pub all_conflicts: HashSet<EntryId>,
128
129    /// Transactions to remove (for coins cache cleanup).
130    /// Must capture BEFORE calling remove_staged().
131    pub removed_transactions: Vec<Arc<Transaction>>,
132
133    /// Total fees of all replaced transactions.
134    pub replaced_fees: bitcoin::Amount,
135
136    /// Total size of all replaced transactions.
137    pub replaced_size: i64,
138}
139
140/// A package of related transactions to be validated together.
141#[derive(Debug, Clone)]
142pub struct Package {
143    pub transactions: Vec<Arc<Transaction>>,
144}
145
146/// Package validation result.
147#[derive(Debug)]
148pub struct PackageValidationResult {
149    pub accepted: Vec<Txid>,
150    pub package_feerate: FeeRate,
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    #[test]
158    fn test_fee_rate_from_amount_and_vsize() {
159        // 1000 sat fee, 250 vbytes = 4000 sat/kvB
160        let fee = Amount::from_sat(1000);
161        assert_eq!(
162            FeeRate::from_amount_and_vsize(fee, 250)
163                .unwrap()
164                .as_sat_per_kvb(),
165            4000
166        );
167
168        // 500 sat fee, 200 vbytes = 2500 sat/kvB
169        let fee = Amount::from_sat(500);
170        assert_eq!(
171            FeeRate::from_amount_and_vsize(fee, 200)
172                .unwrap()
173                .as_sat_per_kvb(),
174            2500
175        );
176
177        // Zero vsize should error
178        let fee = Amount::from_sat(1000);
179        assert!(FeeRate::from_amount_and_vsize(fee, 0).is_err());
180
181        // Negative vsize should error
182        assert!(FeeRate::from_amount_and_vsize(fee, -1).is_err());
183    }
184}