use crate::ledger::Ledger;
use crate::tx::{verify_transfer_sig, TransferTransaction, TxError};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashMap, HashSet};
use std::sync::RwLock;
use tokio::sync::RwLock as AsyncRwLock;
#[derive(Debug, Clone, Serialize, Deserialize)]
struct MempoolEntry {
tx: TransferTransaction,
pending_nonces: HashMap<String, u64>,
}
pub struct Mempool {
txs: RwLock<HashMap<[u8; 32], TransferTransaction>>,
address_nonces: RwLock<HashMap<String, u64>>,
by_fee: RwLock<BTreeMap<(u64, u64), [u8; 32]>>, max_size: usize,
min_fee: u64,
path: RwLock<String>,
modified: RwLock<bool>,
}
impl Mempool {
pub fn new(max_size: usize, min_fee: u64) -> Self {
Mempool {
txs: RwLock::new(HashMap::new()),
address_nonces: RwLock::new(HashMap::new()),
by_fee: RwLock::new(BTreeMap::new()),
max_size,
min_fee,
path: RwLock::new(String::new()),
modified: RwLock::new(false),
}
}
pub fn set_path(&self, path: &str) {
*self.path.write().unwrap() = path.to_string();
}
pub fn load_from_file(&self, path: &str) {
let content = std::fs::read_to_string(path).ok();
if let Some(ref s) = content {
if let Ok(entries) = serde_json::from_str::<Vec<MempoolEntry>>(s) {
let mut txs = self.txs.write().unwrap();
let mut by_fee = self.by_fee.write().unwrap();
let mut nonces = self.address_nonces.write().unwrap();
let base_time = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos() as u64;
for (i, entry) in entries.into_iter().enumerate() {
let hash = entry.tx.tx_hash;
txs.insert(hash, entry.tx);
by_fee.insert((0, base_time + i as u64), hash);
for (addr, n) in entry.pending_nonces {
nonces.insert(addr, n);
}
}
}
}
*self.path.write().unwrap() = path.to_string();
}
pub fn save_to_file(&self) -> Result<(), String> {
let (path, entries) = {
let path = self.path.read().unwrap().clone();
if path.is_empty() {
return Ok(());
}
let txs = self.txs.read().unwrap();
let nonces = self.address_nonces.read().unwrap();
let entries: Vec<MempoolEntry> = txs
.values()
.map(|tx| MempoolEntry {
tx: tx.clone(),
pending_nonces: nonces.clone(),
})
.collect();
(path, entries)
};
let json = serde_json::to_string_pretty(&entries).map_err(|e| e.to_string())?;
let tmp_path = format!("{}.tmp", path);
std::fs::write(&tmp_path, &json).map_err(|e| e.to_string())?;
std::fs::rename(&tmp_path, &path).map_err(|e| e.to_string())?;
*self.modified.write().unwrap() = false;
Ok(())
}
pub fn is_modified(&self) -> bool {
*self.modified.read().unwrap()
}
pub async fn submit(
&self,
tx: TransferTransaction,
ledger: &AsyncRwLock<Ledger>,
) -> Result<[u8; 32], TxError> {
if tx.fee < self.min_fee {
return Err(TxError::FeeTooLow);
}
if tx.amount == 0 {
return Err(TxError::AmountZero);
}
if tx.from == tx.to {
return Err(TxError::SelfTransfer);
}
verify_transfer_sig(&tx)?;
if self.txs.read().unwrap().contains_key(&tx.tx_hash) {
return Err(TxError::DuplicateTransaction);
}
let (ledger_nonce, balance) = {
let guard = ledger.read().await;
(
guard.current_nonce(&tx.from),
guard.balance_of(&tx.from, &tx.token),
)
};
let pending_count = self
.address_nonces
.read()
.unwrap()
.get(&tx.from)
.copied()
.unwrap_or(0);
let expected_nonce = ledger_nonce + pending_count;
if tx.nonce != expected_nonce {
return Err(TxError::InvalidNonce);
}
let pending_amount: u64 = self
.txs
.read()
.unwrap()
.values()
.filter(|t| t.from == tx.from && t.token == tx.token)
.map(|t| t.amount + t.fee)
.sum();
if balance < tx.amount + tx.fee + pending_amount {
return Err(TxError::InsufficientBalance);
}
let tx_hash = tx.tx_hash;
let fee = tx.fee;
let from = tx.from.clone();
self.txs.write().unwrap().insert(tx_hash, tx);
*self
.address_nonces
.write()
.unwrap()
.entry(from)
.or_insert(0) += 1;
let ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos() as u64;
self.by_fee.write().unwrap().insert((fee, ts), tx_hash);
self.evict_over_capacity();
*self.modified.write().unwrap() = true;
Ok(tx_hash)
}
pub fn drain_for_block(&self, max_count: usize, min_fee: u64) -> Vec<TransferTransaction> {
let mut result = Vec::new();
let mut to_remove = Vec::new();
let by_fee = self.by_fee.read().unwrap();
for ((fee, ts), hash) in by_fee.iter().rev() {
if *fee < min_fee {
break;
}
if result.len() >= max_count {
break;
}
if let Some(tx) = self.txs.read().unwrap().get(hash) {
result.push(tx.clone());
to_remove.push((*fee, *ts, *hash));
}
}
drop(by_fee);
let mut txs = self.txs.write().unwrap();
let mut by_fee_map = self.by_fee.write().unwrap();
let mut nonces = self.address_nonces.write().unwrap();
let remove_set: HashSet<[u8; 32]> = to_remove.iter().map(|(_, _, h)| *h).collect();
for hash in &remove_set {
if let Some(tx) = txs.remove(hash) {
let addr_count = nonces.get(&tx.from).copied().unwrap_or(1).saturating_sub(1);
if addr_count == 0 {
nonces.remove(&tx.from);
} else {
nonces.insert(tx.from, addr_count);
}
}
}
by_fee_map.retain(|_k, v| !remove_set.contains(v));
*self.modified.write().unwrap() = true;
result
}
pub fn remove(&self, tx_hashes: &[[u8; 32]]) {
let mut txs = self.txs.write().unwrap();
let mut by_fee_map = self.by_fee.write().unwrap();
let mut nonces = self.address_nonces.write().unwrap();
let remove_set: HashSet<[u8; 32]> = tx_hashes.iter().copied().collect();
for hash in &remove_set {
if let Some(tx) = txs.remove(hash) {
let addr_count = nonces.get(&tx.from).copied().unwrap_or(1).saturating_sub(1);
if addr_count == 0 {
nonces.remove(&tx.from);
} else {
nonces.insert(tx.from, addr_count);
}
}
}
by_fee_map.retain(|_k, v| !remove_set.contains(v));
*self.modified.write().unwrap() = true;
}
pub fn revalidate(&self, ledger: &AsyncRwLock<Ledger>) {
let (to_remove, _balance_map) = {
let ledger = ledger.blocking_read();
let mut to_remove = Vec::new();
let mut balance_map = HashMap::new();
let txs = self.txs.read().unwrap();
for (hash, tx) in txs.iter() {
let ledger_nonce = ledger.current_nonce(&tx.from);
if tx.nonce < ledger_nonce {
to_remove.push(*hash);
continue;
}
let balance = ledger.balance_of(&tx.from, &tx.token);
balance_map.insert(tx.from.clone(), balance);
if balance < tx.amount + tx.fee {
to_remove.push(*hash);
}
}
drop(txs);
(to_remove, balance_map)
};
let mut txs = self.txs.write().unwrap();
let mut by_fee_map = self.by_fee.write().unwrap();
let mut nonces = self.address_nonces.write().unwrap();
let remove_set: HashSet<[u8; 32]> = to_remove.iter().copied().collect();
for hash in &remove_set {
if let Some(tx) = txs.remove(hash) {
let addr_count = nonces.get(&tx.from).copied().unwrap_or(1).saturating_sub(1);
if addr_count == 0 {
nonces.remove(&tx.from);
} else {
nonces.insert(tx.from, addr_count);
}
}
}
by_fee_map.retain(|_k, v| !remove_set.contains(v));
*self.modified.write().unwrap() = true;
}
pub fn pending(&self) -> Vec<TransferTransaction> {
self.txs.read().unwrap().values().cloned().collect()
}
pub fn hashes(&self) -> Vec<[u8; 32]> {
self.txs.read().unwrap().keys().copied().collect()
}
pub fn get_tx(&self, hash: &[u8; 32]) -> Option<TransferTransaction> {
self.txs.read().unwrap().get(hash).cloned()
}
pub fn len(&self) -> usize {
self.txs.read().unwrap().len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
fn evict_over_capacity(&self) {
let count = self.txs.read().unwrap().len();
if count <= self.max_size {
return;
}
let excess = count - self.max_size;
let lowest_fee: Vec<[u8; 32]> = self
.by_fee
.read()
.unwrap()
.iter()
.take(excess)
.map(|(_, hash)| *hash)
.collect();
self.remove(&lowest_fee);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ledger::Ledger;
use crate::tx::{hash_transfer, TokenType};
async fn test_mempool_submit_and_drain_impl(ledger: &AsyncRwLock<Ledger>) {
use ed25519_dalek::{Signer, SigningKey};
let signing_key = SigningKey::from_bytes(&[42u8; 32]);
let from = bs58::encode(signing_key.verifying_key().to_bytes()).into_string();
let to = bs58::encode([99u8; 32]).into_string();
ledger
.write()
.await
.issue(&from, &TokenType::TribeChain, 1000);
let mempool = Mempool::new(1000, 0);
let nonce = 0u64;
let amount = 100u64;
let fee = 1u64;
let timestamp = 0u64;
let tx_hash = hash_transfer(
&from,
nonce,
&to,
&TokenType::TribeChain,
amount,
fee,
timestamp,
);
let signature = signing_key.sign(&tx_hash).to_bytes().to_vec();
let tx = TransferTransaction {
tx_hash,
nonce,
from: from.clone(),
to,
token: TokenType::TribeChain,
amount,
fee,
signature,
timestamp,
};
let result = mempool.submit(tx, ledger).await;
assert!(result.is_ok(), "submit should succeed: {:?}", result);
assert_eq!(mempool.len(), 1);
let drained = mempool.drain_for_block(100, 0);
assert_eq!(drained.len(), 1);
assert_eq!(drained[0].from, from);
assert_eq!(mempool.len(), 0);
}
fn test_mempool_revalidate_impl(ledger: &AsyncRwLock<Ledger>) {
let mempool = Mempool::new(1000, 0);
assert_eq!(mempool.len(), 0);
mempool.revalidate(ledger);
assert_eq!(mempool.len(), 0);
}
#[tokio::test]
async fn test_mempool_submit_and_drain() {
let ledger = AsyncRwLock::new(Ledger::new("protocol".to_string()));
test_mempool_submit_and_drain_impl(&ledger).await;
}
#[test]
fn test_mempool_revalidate() {
let ledger = AsyncRwLock::new(Ledger::new("protocol".to_string()));
test_mempool_revalidate_impl(&ledger);
}
}