Skip to main content

pot_o_extensions/
mempool.rs

1use crate::ledger::Ledger;
2use crate::tx::{verify_transfer_sig, TransferTransaction, TxError};
3use serde::{Deserialize, Serialize};
4use std::collections::{BTreeMap, HashMap, HashSet};
5use std::sync::RwLock;
6use tokio::sync::RwLock as AsyncRwLock;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
9struct MempoolEntry {
10    tx: TransferTransaction,
11    pending_nonces: HashMap<String, u64>,
12}
13
14/// Pending transaction pool
15pub struct Mempool {
16    txs: RwLock<HashMap<[u8; 32], TransferTransaction>>,
17    address_nonces: RwLock<HashMap<String, u64>>,
18    by_fee: RwLock<BTreeMap<(u64, u64), [u8; 32]>>, // (fee, timestamp_ns) → tx_hash
19    max_size: usize,
20    min_fee: u64,
21    path: RwLock<String>,
22    modified: RwLock<bool>,
23}
24
25impl Mempool {
26    pub fn new(max_size: usize, min_fee: u64) -> Self {
27        Mempool {
28            txs: RwLock::new(HashMap::new()),
29            address_nonces: RwLock::new(HashMap::new()),
30            by_fee: RwLock::new(BTreeMap::new()),
31            max_size,
32            min_fee,
33            path: RwLock::new(String::new()),
34            modified: RwLock::new(false),
35        }
36    }
37
38    pub fn set_path(&self, path: &str) {
39        *self.path.write().unwrap() = path.to_string();
40    }
41
42    pub fn load_from_file(&self, path: &str) {
43        let content = std::fs::read_to_string(path).ok();
44        if let Some(ref s) = content {
45            if let Ok(entries) = serde_json::from_str::<Vec<MempoolEntry>>(s) {
46                let mut txs = self.txs.write().unwrap();
47                let mut by_fee = self.by_fee.write().unwrap();
48                let mut nonces = self.address_nonces.write().unwrap();
49                let base_time = std::time::SystemTime::now()
50                    .duration_since(std::time::UNIX_EPOCH)
51                    .unwrap_or_default()
52                    .as_nanos() as u64;
53                for (i, entry) in entries.into_iter().enumerate() {
54                    let hash = entry.tx.tx_hash;
55                    txs.insert(hash, entry.tx);
56                    by_fee.insert((0, base_time + i as u64), hash);
57                    for (addr, n) in entry.pending_nonces {
58                        nonces.insert(addr, n);
59                    }
60                }
61            }
62        }
63        *self.path.write().unwrap() = path.to_string();
64    }
65
66    pub fn save_to_file(&self) -> Result<(), String> {
67        let (path, entries) = {
68            let path = self.path.read().unwrap().clone();
69            if path.is_empty() {
70                return Ok(());
71            }
72            let txs = self.txs.read().unwrap();
73            let nonces = self.address_nonces.read().unwrap();
74            let entries: Vec<MempoolEntry> = txs
75                .values()
76                .map(|tx| MempoolEntry {
77                    tx: tx.clone(),
78                    pending_nonces: nonces.clone(),
79                })
80                .collect();
81            (path, entries)
82        };
83        let json = serde_json::to_string_pretty(&entries).map_err(|e| e.to_string())?;
84        let tmp_path = format!("{}.tmp", path);
85        std::fs::write(&tmp_path, &json).map_err(|e| e.to_string())?;
86        std::fs::rename(&tmp_path, &path).map_err(|e| e.to_string())?;
87        *self.modified.write().unwrap() = false;
88        Ok(())
89    }
90
91    pub fn is_modified(&self) -> bool {
92        *self.modified.read().unwrap()
93    }
94
95    /// Submit a transaction to the mempool after validation.
96    /// Uses `RwLock::read()` for non-blocking reads since this is called from async context.
97    pub async fn submit(
98        &self,
99        tx: TransferTransaction,
100        ledger: &AsyncRwLock<Ledger>,
101    ) -> Result<[u8; 32], TxError> {
102        // 1. Fee check
103        if tx.fee < self.min_fee {
104            return Err(TxError::FeeTooLow);
105        }
106
107        // 2. Amount check
108        if tx.amount == 0 {
109            return Err(TxError::AmountZero);
110        }
111
112        // 3. Self-transfer check
113        if tx.from == tx.to {
114            return Err(TxError::SelfTransfer);
115        }
116
117        // 4. Signature verification
118        verify_transfer_sig(&tx)?;
119
120        // 5. Duplicate check
121        if self.txs.read().unwrap().contains_key(&tx.tx_hash) {
122            return Err(TxError::DuplicateTransaction);
123        }
124
125        // 6. Nonce and balance check against ledger + pending
126        // Scope ledger read lock so it drops before acquiring mempool write locks (avoid ABBA deadlock)
127        let (ledger_nonce, balance) = {
128            let guard = ledger.read().await;
129            (
130                guard.current_nonce(&tx.from),
131                guard.balance_of(&tx.from, &tx.token),
132            )
133        };
134        let pending_count = self
135            .address_nonces
136            .read()
137            .unwrap()
138            .get(&tx.from)
139            .copied()
140            .unwrap_or(0);
141        let expected_nonce = ledger_nonce + pending_count;
142        if tx.nonce != expected_nonce {
143            return Err(TxError::InvalidNonce);
144        }
145
146        let pending_amount: u64 = self
147            .txs
148            .read()
149            .unwrap()
150            .values()
151            .filter(|t| t.from == tx.from && t.token == tx.token)
152            .map(|t| t.amount + t.fee)
153            .sum();
154        if balance < tx.amount + tx.fee + pending_amount {
155            return Err(TxError::InsufficientBalance);
156        }
157
158        // 7. Insert (ledger lock dropped, safe to acquire mempool locks)
159        let tx_hash = tx.tx_hash;
160        let fee = tx.fee;
161        let from = tx.from.clone();
162        self.txs.write().unwrap().insert(tx_hash, tx);
163        *self
164            .address_nonces
165            .write()
166            .unwrap()
167            .entry(from)
168            .or_insert(0) += 1;
169
170        // Use current time nanos as tiebreaker for same-fee ordering
171        let ts = std::time::SystemTime::now()
172            .duration_since(std::time::UNIX_EPOCH)
173            .unwrap_or_default()
174            .as_nanos() as u64;
175        self.by_fee.write().unwrap().insert((fee, ts), tx_hash);
176
177        // Evict if over capacity
178        self.evict_over_capacity();
179
180        *self.modified.write().unwrap() = true;
181
182        Ok(tx_hash)
183    }
184
185    /// Drain highest-fee transactions for block inclusion
186    pub fn drain_for_block(&self, max_count: usize, min_fee: u64) -> Vec<TransferTransaction> {
187        let mut result = Vec::new();
188        let mut to_remove = Vec::new();
189
190        let by_fee = self.by_fee.read().unwrap();
191        for ((fee, ts), hash) in by_fee.iter().rev() {
192            if *fee < min_fee {
193                break;
194            }
195            if result.len() >= max_count {
196                break;
197            }
198            if let Some(tx) = self.txs.read().unwrap().get(hash) {
199                result.push(tx.clone());
200                to_remove.push((*fee, *ts, *hash));
201            }
202        }
203        drop(by_fee);
204
205        // Remove drained txs (single retain pass using HashSet)
206        let mut txs = self.txs.write().unwrap();
207        let mut by_fee_map = self.by_fee.write().unwrap();
208        let mut nonces = self.address_nonces.write().unwrap();
209        let remove_set: HashSet<[u8; 32]> = to_remove.iter().map(|(_, _, h)| *h).collect();
210        for hash in &remove_set {
211            if let Some(tx) = txs.remove(hash) {
212                let addr_count = nonces.get(&tx.from).copied().unwrap_or(1).saturating_sub(1);
213                if addr_count == 0 {
214                    nonces.remove(&tx.from);
215                } else {
216                    nonces.insert(tx.from, addr_count);
217                }
218            }
219        }
220        by_fee_map.retain(|_k, v| !remove_set.contains(v));
221
222        *self.modified.write().unwrap() = true;
223        result
224    }
225
226    /// Remove confirmed transactions (after block acceptance)
227    pub fn remove(&self, tx_hashes: &[[u8; 32]]) {
228        let mut txs = self.txs.write().unwrap();
229        let mut by_fee_map = self.by_fee.write().unwrap();
230        let mut nonces = self.address_nonces.write().unwrap();
231
232        let remove_set: HashSet<[u8; 32]> = tx_hashes.iter().copied().collect();
233        for hash in &remove_set {
234            if let Some(tx) = txs.remove(hash) {
235                let addr_count = nonces.get(&tx.from).copied().unwrap_or(1).saturating_sub(1);
236                if addr_count == 0 {
237                    nonces.remove(&tx.from);
238                } else {
239                    nonces.insert(tx.from, addr_count);
240                }
241            }
242        }
243        by_fee_map.retain(|_k, v| !remove_set.contains(v));
244        *self.modified.write().unwrap() = true;
245    }
246
247    /// Revalidate all pending txs against current ledger state
248    pub fn revalidate(&self, ledger: &AsyncRwLock<Ledger>) {
249        let (to_remove, _balance_map) = {
250            let ledger = ledger.blocking_read();
251            let mut to_remove = Vec::new();
252            let mut balance_map = HashMap::new();
253            let txs = self.txs.read().unwrap();
254            for (hash, tx) in txs.iter() {
255                let ledger_nonce = ledger.current_nonce(&tx.from);
256                if tx.nonce < ledger_nonce {
257                    to_remove.push(*hash);
258                    continue;
259                }
260                let balance = ledger.balance_of(&tx.from, &tx.token);
261                balance_map.insert(tx.from.clone(), balance);
262                if balance < tx.amount + tx.fee {
263                    to_remove.push(*hash);
264                }
265            }
266            drop(txs);
267            (to_remove, balance_map)
268        };
269
270        let mut txs = self.txs.write().unwrap();
271        let mut by_fee_map = self.by_fee.write().unwrap();
272        let mut nonces = self.address_nonces.write().unwrap();
273
274        let remove_set: HashSet<[u8; 32]> = to_remove.iter().copied().collect();
275        for hash in &remove_set {
276            if let Some(tx) = txs.remove(hash) {
277                let addr_count = nonces.get(&tx.from).copied().unwrap_or(1).saturating_sub(1);
278                if addr_count == 0 {
279                    nonces.remove(&tx.from);
280                } else {
281                    nonces.insert(tx.from, addr_count);
282                }
283            }
284        }
285        by_fee_map.retain(|_k, v| !remove_set.contains(v));
286        *self.modified.write().unwrap() = true;
287    }
288
289    pub fn pending(&self) -> Vec<TransferTransaction> {
290        self.txs.read().unwrap().values().cloned().collect()
291    }
292
293    pub fn hashes(&self) -> Vec<[u8; 32]> {
294        self.txs.read().unwrap().keys().copied().collect()
295    }
296
297    pub fn get_tx(&self, hash: &[u8; 32]) -> Option<TransferTransaction> {
298        self.txs.read().unwrap().get(hash).cloned()
299    }
300
301    pub fn len(&self) -> usize {
302        self.txs.read().unwrap().len()
303    }
304
305    pub fn is_empty(&self) -> bool {
306        self.len() == 0
307    }
308
309    fn evict_over_capacity(&self) {
310        let count = self.txs.read().unwrap().len();
311        if count <= self.max_size {
312            return;
313        }
314        let excess = count - self.max_size;
315        let lowest_fee: Vec<[u8; 32]> = self
316            .by_fee
317            .read()
318            .unwrap()
319            .iter()
320            .take(excess)
321            .map(|(_, hash)| *hash)
322            .collect();
323        self.remove(&lowest_fee);
324    }
325}
326
327#[cfg(test)]
328mod tests {
329    use super::*;
330    use crate::ledger::Ledger;
331    use crate::tx::{hash_transfer, TokenType};
332
333    async fn test_mempool_submit_and_drain_impl(ledger: &AsyncRwLock<Ledger>) {
334        use ed25519_dalek::{Signer, SigningKey};
335
336        let signing_key = SigningKey::from_bytes(&[42u8; 32]);
337        let from = bs58::encode(signing_key.verifying_key().to_bytes()).into_string();
338        let to = bs58::encode([99u8; 32]).into_string();
339
340        ledger
341            .write()
342            .await
343            .issue(&from, &TokenType::TribeChain, 1000);
344        let mempool = Mempool::new(1000, 0);
345
346        let nonce = 0u64;
347        let amount = 100u64;
348        let fee = 1u64;
349        let timestamp = 0u64;
350
351        let tx_hash = hash_transfer(
352            &from,
353            nonce,
354            &to,
355            &TokenType::TribeChain,
356            amount,
357            fee,
358            timestamp,
359        );
360        let signature = signing_key.sign(&tx_hash).to_bytes().to_vec();
361
362        let tx = TransferTransaction {
363            tx_hash,
364            nonce,
365            from: from.clone(),
366            to,
367            token: TokenType::TribeChain,
368            amount,
369            fee,
370            signature,
371            timestamp,
372        };
373
374        let result = mempool.submit(tx, ledger).await;
375        assert!(result.is_ok(), "submit should succeed: {:?}", result);
376        assert_eq!(mempool.len(), 1);
377
378        let drained = mempool.drain_for_block(100, 0);
379        assert_eq!(drained.len(), 1);
380        assert_eq!(drained[0].from, from);
381        assert_eq!(mempool.len(), 0);
382    }
383
384    fn test_mempool_revalidate_impl(ledger: &AsyncRwLock<Ledger>) {
385        let mempool = Mempool::new(1000, 0);
386        assert_eq!(mempool.len(), 0);
387        mempool.revalidate(ledger);
388        assert_eq!(mempool.len(), 0);
389    }
390
391    #[tokio::test]
392    async fn test_mempool_submit_and_drain() {
393        let ledger = AsyncRwLock::new(Ledger::new("protocol".to_string()));
394        test_mempool_submit_and_drain_impl(&ledger).await;
395    }
396
397    #[test]
398    fn test_mempool_revalidate() {
399        let ledger = AsyncRwLock::new(Ledger::new("protocol".to_string()));
400        test_mempool_revalidate_impl(&ledger);
401    }
402}