subcoin_primitives/
tx_pool.rs

1//! Bitcoin transaction pool abstraction for network integration.
2
3use bitcoin::{Transaction, Txid};
4use std::sync::Arc;
5
6/// Result of transaction validation.
7#[derive(Debug, Clone)]
8pub enum TxValidationResult {
9    /// Transaction accepted into mempool.
10    Accepted {
11        txid: Txid,
12        /// Fee rate in sat/kvB for relay decisions.
13        fee_rate: u64,
14    },
15    /// Transaction rejected.
16    Rejected { txid: Txid, reason: RejectionReason },
17}
18
19/// Classification of rejection reasons for peer penalty policy.
20#[derive(Debug, Clone)]
21pub enum RejectionReason {
22    /// Soft rejection - don't penalize peer.
23    Soft(SoftRejection),
24    /// Hard rejection - penalize peer for protocol violation.
25    Hard(HardRejection),
26}
27
28impl RejectionReason {
29    /// Returns true if the peer should be penalized for this rejection.
30    pub fn should_penalize_peer(&self) -> bool {
31        matches!(self, Self::Hard(_))
32    }
33}
34
35/// Soft rejections - legitimate reasons that don't indicate misbehavior.
36#[derive(Debug, Clone)]
37pub enum SoftRejection {
38    /// Transaction already in mempool.
39    AlreadyInMempool,
40    /// Missing parent transactions (might arrive later).
41    MissingInputs {
42        parents: Vec<Txid>,
43    },
44    /// Fee rate too low for relay.
45    FeeTooLow {
46        min_kvb: u64,
47        actual_kvb: u64,
48    },
49    /// Mempool is full.
50    MempoolFull,
51    /// Too many ancestor/descendant transactions.
52    TooManyAncestors(usize),
53    TooManyDescendants(usize),
54    /// Transaction conflicts with mempool.
55    TxConflict(String),
56    /// RBF-related issues.
57    NoConflictToReplace,
58    TxNotReplaceable,
59    TooManyReplacements(usize),
60    NewUnconfirmedInput,
61    InsufficientFee(String),
62    /// Package relay issues.
63    PackageTooLarge(usize, usize),
64    PackageSizeTooLarge(u64),
65    PackageCyclicDependencies,
66    PackageFeeTooLow(String),
67    PackageTxValidationFailed(Txid, String),
68    PackageRelayDisabled,
69}
70
71/// Hard rejections - indicate protocol violations or malformed transactions.
72#[derive(Debug, Clone)]
73pub enum HardRejection {
74    /// Coinbase transaction not allowed in mempool.
75    Coinbase,
76    /// Transaction is non-standard.
77    NotStandard(String),
78    TxVersionNotStandard,
79    TxSizeTooSmall,
80    /// Transaction is non-final.
81    NonFinal,
82    NonBIP68Final,
83    /// Too many signature operations.
84    TooManySigops(i64),
85    /// Fee calculation errors.
86    NegativeFee,
87    FeeOverflow,
88    InvalidFeeRate(String),
89    /// Ancestor/descendant size limits.
90    AncestorSizeTooLarge(i64),
91    DescendantSizeTooLarge(i64),
92    /// Script validation failed.
93    ScriptValidationFailed(String),
94    /// Other validation errors.
95    TxError(String),
96    RuntimeApi(String),
97}
98
99/// Mempool statistics.
100#[derive(Debug, Clone)]
101pub struct TxPoolInfo {
102    /// Number of transactions in mempool.
103    pub size: usize,
104    /// Total virtual size of all transactions.
105    pub bytes: u64,
106    /// Total fees of all transactions.
107    pub usage: u64,
108    /// Current minimum relay fee rate in sat/kvB.
109    pub min_fee_rate: u64,
110}
111
112/// Bitcoin transaction pool trait for network integration.
113///
114/// This trait abstracts mempool operations needed by the network layer,
115/// avoiding circular dependencies and enabling testing with mock implementations.
116///
117/// All methods are synchronous - the caller can decide whether to run them
118/// on a blocking executor (e.g., Substrate's `spawn_blocking`) or inline.
119pub trait TxPool: Send + Sync + 'static {
120    /// Validate and potentially accept a transaction into the mempool.
121    ///
122    /// This is a blocking operation that holds internal locks and performs
123    /// script validation. The caller should run this on a blocking executor
124    /// if needed (e.g., `task_manager.spawn_blocking()`).
125    fn validate_transaction(&self, tx: Transaction) -> TxValidationResult;
126
127    /// Check if transaction is already in mempool.
128    fn contains(&self, txid: &Txid) -> bool;
129
130    /// Get transaction from mempool if present.
131    fn get(&self, txid: &Txid) -> Option<Arc<Transaction>>;
132
133    /// Get transactions pending broadcast to peers.
134    /// Returns (txid, fee_rate) pairs.
135    fn pending_broadcast(&self) -> Vec<(Txid, u64)>;
136
137    /// Mark transactions as broadcast to peers.
138    fn mark_broadcast(&self, txids: &[Txid]);
139
140    /// Iterate over all transaction IDs with their fee rates.
141    /// Returns (txid, fee_rate) pairs sorted by mining priority.
142    fn iter_txids(&self) -> Box<dyn Iterator<Item = (Txid, u64)> + Send>;
143
144    /// Get mempool statistics.
145    fn info(&self) -> TxPoolInfo;
146}
147
148/// No-op transaction pool for backward compatibility.
149///
150/// This default implementation allows existing code to compile without
151/// requiring immediate mempool integration.
152#[derive(Debug, Default, Clone)]
153pub struct NoTxPool;
154
155impl TxPool for NoTxPool {
156    fn validate_transaction(&self, tx: Transaction) -> TxValidationResult {
157        TxValidationResult::Rejected {
158            txid: tx.compute_txid(),
159            reason: RejectionReason::Soft(SoftRejection::PackageRelayDisabled),
160        }
161    }
162
163    fn contains(&self, _txid: &Txid) -> bool {
164        false
165    }
166
167    fn get(&self, _txid: &Txid) -> Option<Arc<Transaction>> {
168        None
169    }
170
171    fn pending_broadcast(&self) -> Vec<(Txid, u64)> {
172        Vec::new()
173    }
174
175    fn mark_broadcast(&self, _txids: &[Txid]) {}
176
177    fn iter_txids(&self) -> Box<dyn Iterator<Item = (Txid, u64)> + Send> {
178        Box::new(std::iter::empty())
179    }
180
181    fn info(&self) -> TxPoolInfo {
182        TxPoolInfo {
183            size: 0,
184            bytes: 0,
185            usage: 0,
186            min_fee_rate: 0,
187        }
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194    use bitcoin::hashes::Hash;
195
196    #[test]
197    fn test_noop_pool() {
198        let pool = NoTxPool;
199        let tx = Transaction {
200            version: bitcoin::transaction::Version::TWO,
201            lock_time: bitcoin::absolute::LockTime::ZERO,
202            input: vec![],
203            output: vec![],
204        };
205
206        let result = pool.validate_transaction(tx);
207        assert!(matches!(
208            result,
209            TxValidationResult::Rejected {
210                reason: RejectionReason::Soft(SoftRejection::PackageRelayDisabled),
211                ..
212            }
213        ));
214        assert!(!pool.contains(&Txid::all_zeros()));
215        assert!(pool.get(&Txid::all_zeros()).is_none());
216        assert_eq!(pool.pending_broadcast().len(), 0);
217        assert_eq!(pool.info().size, 0);
218    }
219
220    #[test]
221    fn test_rejection_reason_penalize() {
222        let soft = RejectionReason::Soft(SoftRejection::PackageRelayDisabled);
223        assert!(!soft.should_penalize_peer());
224
225        let hard = RejectionReason::Hard(HardRejection::Coinbase);
226        assert!(hard.should_penalize_peer());
227    }
228
229    #[test]
230    fn test_rejection_reason_penalty() {
231        let soft = RejectionReason::Soft(SoftRejection::AlreadyInMempool);
232        assert!(!soft.should_penalize_peer());
233
234        let hard = RejectionReason::Hard(HardRejection::Coinbase);
235        assert!(hard.should_penalize_peer());
236    }
237}