use std::{
cmp::Reverse,
collections::{BTreeMap, VecDeque, hash_map::Entry},
sync::RwLock,
sync::atomic::{AtomicU64, Ordering},
time::{Duration, Instant},
};
use rustc_hash::{FxHashMap, FxHashSet};
use crate::error::MempoolError;
use ethrex_common::{
Address, H160, H256, U256,
types::{
BlobTuple, BlobsBundle, BlockHeader, ChainConfig, Fork, MempoolTransaction, Transaction,
TxType, kzg_commitment_to_versioned_hash,
},
};
use ethrex_crypto::NativeCrypto;
use ethrex_storage::error::StoreError;
use ethrex_vm::{intrinsic_gas_dimensions, intrinsic_gas_floor};
use tracing::warn;
pub const MAX_ALTERNATES_PER_HASH: usize = 8;
pub const MAX_BLOB_MEMPOOL_SIZE: usize = 512;
#[derive(Debug, Clone, Copy)]
pub struct Alternate {
pub peer_id: H256,
pub tx_type: u8,
pub tx_size: usize,
}
#[derive(Debug, Default)]
struct MempoolInner {
broadcast_pool: FxHashSet<H256>,
transaction_pool: FxHashMap<H256, MempoolTransaction>,
blobs_bundle_pool: FxHashMap<H256, BlobsBundle>,
in_flight_txs: FxHashSet<H256>,
alternates: FxHashMap<H256, (VecDeque<Alternate>, Instant)>,
blobs_bundle_by_versioned_hash: FxHashMap<H256, FxHashMap<H256, usize>>,
txs_by_sender_nonce: BTreeMap<(H160, u64), H256>,
txs_order: VecDeque<H256>,
max_mempool_size: usize,
max_blob_mempool_size: usize,
mempool_prune_threshold: usize,
}
impl MempoolInner {
fn new(max_mempool_size: usize) -> Self {
MempoolInner {
txs_order: VecDeque::with_capacity(max_mempool_size * 2),
transaction_pool: FxHashMap::with_capacity_and_hasher(
max_mempool_size,
Default::default(),
),
max_mempool_size,
max_blob_mempool_size: MAX_BLOB_MEMPOOL_SIZE,
mempool_prune_threshold: max_mempool_size + max_mempool_size / 2,
..Default::default()
}
}
fn remove_transaction_with_lock(&mut self, hash: &H256) -> Result<(), StoreError> {
let Some(tx) = self.transaction_pool.remove(hash) else {
return Ok(());
};
if matches!(tx.tx_type(), TxType::EIP4844) {
self.remove_blob_bundle(hash);
}
self.txs_by_sender_nonce.remove(&(tx.sender(), tx.nonce()));
self.broadcast_pool.remove(hash);
Ok(())
}
pub fn remove_blob_bundle(&mut self, hash: &H256) {
let Some(h) = self.blobs_bundle_pool.remove(hash) else {
return;
};
for commitment in &h.commitments {
let versioned_hash = kzg_commitment_to_versioned_hash(commitment);
if let Entry::Occupied(mut entry) =
self.blobs_bundle_by_versioned_hash.entry(versioned_hash)
{
let txn_to_bundle = entry.get_mut();
txn_to_bundle.remove(hash);
if txn_to_bundle.is_empty() {
entry.remove();
}
}
}
}
fn blob_tx_count(&self) -> usize {
self.blobs_bundle_pool.len()
}
fn regular_tx_count(&self) -> usize {
self.transaction_pool
.len()
.saturating_sub(self.blob_tx_count())
}
fn remove_oldest_regular_transaction(&mut self) -> Result<(), StoreError> {
while self.regular_tx_count() >= self.max_mempool_size {
if let Some(oldest_hash) = self.txs_order.pop_front() {
self.remove_transaction_with_lock(&oldest_hash)?;
} else {
warn!(
"Regular mempool is full but there are no transactions to remove, this should not happen and will make the mempool grow indefinitely"
);
break;
}
}
Ok(())
}
fn remove_worst_blob_transaction(&mut self) -> Result<(), StoreError> {
while self.blob_tx_count() > self.max_blob_mempool_size {
let mut min_nonce_by_sender: FxHashMap<Address, u64> = FxHashMap::default();
for tx in self
.blobs_bundle_pool
.keys()
.filter_map(|hash| self.transaction_pool.get(hash))
{
min_nonce_by_sender
.entry(tx.sender())
.and_modify(|n| *n = (*n).min(tx.nonce()))
.or_insert(tx.nonce());
}
let worst = self
.blobs_bundle_pool
.keys()
.filter_map(|hash| self.transaction_pool.get(hash).map(|tx| (*hash, tx)))
.max_by_key(|(_, tx)| {
let baseline = min_nonce_by_sender.get(&tx.sender()).copied().unwrap_or(0);
let offset = tx.nonce().saturating_sub(baseline);
(offset, Reverse(tx.max_fee_per_blob_gas()))
})
.map(|(hash, _)| hash);
match worst {
Some(hash) => self.remove_transaction_with_lock(&hash)?,
None => {
warn!(
"Blob mempool is over cap but no evictable blob transaction is present, this should not happen"
);
break;
}
}
}
Ok(())
}
}
#[derive(Debug, Default)]
pub struct Mempool {
inner: RwLock<MempoolInner>,
tx_added: tokio::sync::Notify,
tx_seq: AtomicU64,
}
impl Mempool {
pub fn new(max_mempool_size: usize) -> Self {
Mempool {
inner: RwLock::new(MempoolInner::new(max_mempool_size)),
tx_added: tokio::sync::Notify::new(),
tx_seq: AtomicU64::new(0),
}
}
pub fn with_max_blob_mempool_size(self, max_blob_mempool_size: usize) -> Self {
if let Ok(mut inner) = self.inner.write() {
inner.max_blob_mempool_size = max_blob_mempool_size;
}
self
}
pub(crate) fn tx_added(&self) -> &tokio::sync::Notify {
&self.tx_added
}
pub(crate) fn tx_seq(&self) -> u64 {
self.tx_seq.load(Ordering::Acquire)
}
fn write(&self) -> Result<std::sync::RwLockWriteGuard<'_, MempoolInner>, StoreError> {
self.inner
.write()
.map_err(|error| StoreError::MempoolWriteLock(error.to_string()))
}
fn read(&self) -> Result<std::sync::RwLockReadGuard<'_, MempoolInner>, StoreError> {
self.inner
.read()
.map_err(|error| StoreError::MempoolReadLock(error.to_string()))
}
pub fn add_transaction(
&self,
hash: H256,
sender: Address,
transaction: MempoolTransaction,
) -> Result<(), StoreError> {
let mut inner = self.write()?;
let is_blob = matches!(transaction.tx_type(), TxType::EIP4844);
if inner.txs_order.len() > inner.mempool_prune_threshold {
let txpool = core::mem::take(&mut inner.transaction_pool);
inner.txs_order.retain(|tx| txpool.contains_key(tx));
inner.transaction_pool = txpool;
}
if is_blob {
if inner.blob_tx_count() > inner.max_blob_mempool_size {
inner.remove_worst_blob_transaction()?;
}
} else {
if inner.regular_tx_count() >= inner.max_mempool_size {
inner.remove_oldest_regular_transaction()?;
}
inner.txs_order.push_back(hash);
}
inner
.txs_by_sender_nonce
.insert((sender, transaction.nonce()), hash);
inner.transaction_pool.insert(hash, transaction);
inner.broadcast_pool.insert(hash);
inner.alternates.remove(&hash);
drop(inner);
self.tx_seq.fetch_add(1, Ordering::Release);
self.tx_added.notify_waiters();
Ok(())
}
pub fn get_txs_for_broadcast(&self) -> Result<Vec<MempoolTransaction>, StoreError> {
let inner = self.read()?;
let txs = inner
.transaction_pool
.iter()
.filter_map(|(hash, tx)| {
if !inner.broadcast_pool.contains(hash) {
None
} else {
Some(tx.clone())
}
})
.collect::<Vec<_>>();
Ok(txs)
}
pub fn remove_broadcasted_txs(&self, hashes: &[H256]) -> Result<(), StoreError> {
let mut inner = self.write()?;
for hash in hashes {
inner.broadcast_pool.remove(hash);
}
Ok(())
}
pub fn blob_txs(&self) -> Result<Vec<(H256, Address, u64)>, StoreError> {
let inner = self.read()?;
Ok(inner
.blobs_bundle_pool
.keys()
.filter_map(|hash| {
inner
.transaction_pool
.get(hash)
.map(|tx| (*hash, tx.sender(), tx.nonce()))
})
.collect())
}
pub fn add_blobs_bundle(
&self,
tx_hash: H256,
blobs_bundle: BlobsBundle,
) -> Result<(), StoreError> {
let mut mempool = self.write()?;
for (i, c) in blobs_bundle.commitments.iter().enumerate() {
let versioned_hash = kzg_commitment_to_versioned_hash(c);
mempool
.blobs_bundle_by_versioned_hash
.entry(versioned_hash)
.or_default()
.insert(tx_hash, i);
}
mempool.blobs_bundle_pool.insert(tx_hash, blobs_bundle);
Ok(())
}
pub fn get_blobs_bundle(&self, tx_hash: H256) -> Result<Option<BlobsBundle>, StoreError> {
Ok(self.read()?.blobs_bundle_pool.get(&tx_hash).cloned())
}
pub fn remove_transaction(&self, hash: &H256) -> Result<(), StoreError> {
let mut inner = self.write()?;
inner.remove_transaction_with_lock(hash)?;
Ok(())
}
pub fn filter_transactions(
&self,
filter: &PendingTxFilter,
) -> Result<FxHashMap<Address, Vec<MempoolTransaction>>, StoreError> {
let filter_tx = |tx: &Transaction| -> bool {
let is_blob_tx = matches!(tx, Transaction::EIP4844Transaction(_));
if filter.only_plain_txs && is_blob_tx || filter.only_blob_txs && !is_blob_tx {
return false;
}
if let Some(min_tip) = filter.min_tip.map(U256::from) {
if tx
.effective_gas_tip(filter.base_fee)
.is_none_or(|tip| tip < min_tip)
{
return false;
}
} else if tx.effective_gas_tip(filter.base_fee).is_none() {
return false;
}
if is_blob_tx
&& let Some(blob_fee) = filter.blob_fee
&& tx
.max_fee_per_blob_gas()
.is_none_or(|fee| fee < blob_fee.into())
{
return false;
}
true
};
self.filter_transactions_with_filter_fn(&filter_tx)
}
pub fn get_all_txs_by_sender(
&self,
) -> Result<FxHashMap<Address, Vec<MempoolTransaction>>, StoreError> {
let mut txs_by_sender: FxHashMap<Address, Vec<MempoolTransaction>> =
FxHashMap::with_capacity_and_hasher(128, Default::default());
let tx_pool = &self.read()?.transaction_pool;
for (_, tx) in tx_pool.iter() {
txs_by_sender
.entry(tx.sender())
.or_insert_with(|| Vec::with_capacity(128))
.push(tx.clone())
}
txs_by_sender.iter_mut().for_each(|(_, txs)| txs.sort());
Ok(txs_by_sender)
}
pub fn filter_transactions_with_filter_fn(
&self,
filter: &dyn Fn(&Transaction) -> bool,
) -> Result<FxHashMap<Address, Vec<MempoolTransaction>>, StoreError> {
let mut txs_by_sender: FxHashMap<Address, Vec<MempoolTransaction>> =
FxHashMap::with_capacity_and_hasher(128, Default::default());
let tx_pool = &self.read()?.transaction_pool;
for (_, tx) in tx_pool.iter() {
if filter(tx) {
txs_by_sender
.entry(tx.sender())
.or_insert_with(|| Vec::with_capacity(128))
.push(tx.clone())
}
}
txs_by_sender.iter_mut().for_each(|(_, txs)| txs.sort());
Ok(txs_by_sender)
}
pub fn reserve_unknown_hashes(
&self,
hashes: &[H256],
types: &[u8],
sizes: &[usize],
announcer: H256,
) -> Result<Vec<H256>, StoreError> {
debug_assert_eq!(hashes.len(), types.len());
debug_assert_eq!(hashes.len(), sizes.len());
let mut inner = self.write()?;
let unknown: Vec<H256> = hashes
.iter()
.filter(|hash| {
!inner.in_flight_txs.contains(hash) && !inner.transaction_pool.contains_key(hash)
})
.copied()
.collect();
inner.in_flight_txs.extend(unknown.iter().copied());
if hashes.len() > unknown.len() {
let unknown_set: FxHashSet<H256> = unknown.iter().copied().collect();
let now = Instant::now();
for (i, hash) in hashes.iter().enumerate() {
if unknown_set.contains(hash) || inner.transaction_pool.contains_key(hash) {
continue;
}
let alt = Alternate {
peer_id: announcer,
tx_type: types[i],
tx_size: sizes[i],
};
let entry = inner
.alternates
.entry(*hash)
.or_insert_with(|| (VecDeque::new(), now));
entry.1 = now;
if !entry.0.iter().any(|a| a.peer_id == announcer) {
if entry.0.len() >= MAX_ALTERNATES_PER_HASH {
entry.0.pop_front();
}
entry.0.push_back(alt);
}
}
}
Ok(unknown)
}
pub fn clear_in_flight_txs(&self, hashes: &[H256]) -> Result<(), StoreError> {
let mut inner = self.write()?;
for hash in hashes {
inner.in_flight_txs.remove(hash);
}
Ok(())
}
pub fn pop_alternate(&self, hash: H256) -> Result<Option<Alternate>, StoreError> {
let mut inner = self.write()?;
let Some(entry) = inner.alternates.get_mut(&hash) else {
return Ok(None);
};
let popped = entry.0.pop_front();
if entry.0.is_empty() {
inner.alternates.remove(&hash);
}
Ok(popped)
}
pub fn prune_alternates(&self, ttl: Duration) -> Result<(), StoreError> {
let mut inner = self.write()?;
let now = Instant::now();
inner
.alternates
.retain(|_, (_, last_seen)| now.saturating_duration_since(*last_seen) < ttl);
Ok(())
}
pub fn get_transaction_by_hash(
&self,
transaction_hash: H256,
) -> Result<Option<Transaction>, StoreError> {
let tx = self
.read()?
.transaction_pool
.get(&transaction_hash)
.map(|e| e.transaction().clone());
Ok(tx)
}
pub fn get_nonce(&self, address: &Address) -> Result<Option<u64>, MempoolError> {
Ok(self
.read()?
.txs_by_sender_nonce
.range((*address, 0)..=(*address, u64::MAX))
.last()
.map(|((_address, nonce), _hash)| nonce + 1))
}
pub fn get_mempool_size(&self) -> Result<(u64, u64), MempoolError> {
let txs_size = {
let pool_lock = &self.read()?.transaction_pool;
pool_lock.len()
};
let blobs_size = {
let pool_lock = &self.read()?.blobs_bundle_pool;
pool_lock.len()
};
Ok((txs_size as u64, blobs_size as u64))
}
pub fn content(&self) -> Result<Vec<Transaction>, MempoolError> {
let pooled_transactions = &self.read()?.transaction_pool;
Ok(pooled_transactions
.values()
.map(MempoolTransaction::transaction)
.cloned()
.collect())
}
pub fn get_blobs_bundle_pool(&self) -> Result<Vec<BlobsBundle>, MempoolError> {
let blobs_bundle_pool = &self.read()?.blobs_bundle_pool;
Ok(blobs_bundle_pool.values().cloned().collect())
}
pub fn get_blobs_data_by_versioned_hashes(
&self,
versioned_hashes: &[H256],
) -> Result<Vec<Option<BlobTuple>>, MempoolError> {
let mempool = self.read()?;
let blobs_bundle_pool = &mempool.blobs_bundle_pool;
let blobs_bundle_by_versioned_hash = &mempool.blobs_bundle_by_versioned_hash;
let mut res = vec![None; versioned_hashes.len()];
for (idx, vh) in versioned_hashes.iter().enumerate() {
if let Some((found_hash, inner_pos)) = blobs_bundle_by_versioned_hash
.get(vh)
.and_then(|h| h.iter().next())
{
res[idx] = blobs_bundle_pool
.get(found_hash)
.and_then(|b| b.get_blob_tuple_by_index(*inner_pos))
}
}
Ok(res)
}
pub fn status(&self) -> Result<u64, MempoolError> {
let pool_lock = &self.read()?.transaction_pool;
Ok(pool_lock.len() as u64)
}
pub fn contains_sender_nonce(
&self,
sender: Address,
nonce: u64,
received_hash: H256,
) -> Result<Option<MempoolTransaction>, MempoolError> {
let Some(hash) = self
.read()?
.txs_by_sender_nonce
.get(&(sender, nonce))
.cloned()
else {
return Ok(None);
};
if hash == received_hash {
return Ok(None);
}
let transaction_pool = &self.read()?.transaction_pool;
let tx = transaction_pool.get(&hash).cloned();
Ok(tx)
}
pub fn contains_tx(&self, tx_hash: H256) -> Result<bool, MempoolError> {
let contains = self.read()?.transaction_pool.contains_key(&tx_hash);
Ok(contains)
}
pub fn find_tx_to_replace(
&self,
sender: Address,
nonce: u64,
tx: &Transaction,
) -> Result<Option<H256>, MempoolError> {
let Some(tx_in_pool) = self.contains_sender_nonce(sender, nonce, tx.hash(&NativeCrypto))?
else {
return Ok(None);
};
let is_a_replacement_tx = {
let old_tx_max_fee_per_gas = tx_in_pool.max_fee_per_gas().unwrap_or_default();
let old_tx_max_priority_fee_per_gas = tx_in_pool.max_priority_fee().unwrap_or_default();
let new_tx_max_fee_per_gas = tx.max_fee_per_gas().unwrap_or_default();
let new_tx_max_priority_fee_per_gas = tx.max_priority_fee().unwrap_or_default();
let old_tx_gas_price = tx_in_pool.gas_price();
let new_tx_gas_price = tx.gas_price();
let old_tx_max_fee_per_blob = tx_in_pool.max_fee_per_blob_gas();
let new_tx_max_fee_per_blob = tx.max_fee_per_blob_gas();
let eip4844_higher_fees = if let (Some(old_blob_fee), Some(new_blob_fee)) =
(old_tx_max_fee_per_blob, new_tx_max_fee_per_blob)
{
new_blob_fee > old_blob_fee
} else {
true };
let eip1559_higher_fees = new_tx_max_fee_per_gas > old_tx_max_fee_per_gas
&& new_tx_max_priority_fee_per_gas > old_tx_max_priority_fee_per_gas;
let legacy_higher_fees = new_tx_gas_price > old_tx_gas_price;
eip4844_higher_fees && (eip1559_higher_fees || legacy_higher_fees)
};
if !is_a_replacement_tx {
return Err(MempoolError::UnderpricedReplacement);
}
Ok(Some(tx_in_pool.hash(&NativeCrypto)))
}
}
#[derive(Debug, Default)]
pub struct PendingTxFilter {
pub min_tip: Option<u64>,
pub base_fee: Option<u64>,
pub blob_fee: Option<u64>,
pub only_plain_txs: bool,
pub only_blob_txs: bool,
}
pub fn transaction_intrinsic_gas(
tx: &Transaction,
sender: Address,
header: &BlockHeader,
config: &ChainConfig,
) -> Result<u64, MempoolError> {
let fork = config.fork(header.timestamp);
let (regular, state) = intrinsic_gas_dimensions(tx, sender, fork, header.gas_limit)
.map_err(|e| MempoolError::IntrinsicGasError(e.to_string()))?;
let intrinsic = regular
.checked_add(state)
.ok_or(MempoolError::TxGasOverflowError)?;
let calldata_floor = if fork >= Fork::Prague {
intrinsic_gas_floor(tx, fork).map_err(|e| MempoolError::IntrinsicGasError(e.to_string()))?
} else {
0
};
Ok(intrinsic.max(calldata_floor))
}