Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 138 additions & 0 deletions monad-eth-txpool/src/pool/tracked/limits.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// Copyright (C) 2025 Category Labs, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

use std::time::Duration;

use alloy_primitives::Address;
use indexmap::IndexMap;
use tracing::{error, info};

use crate::pool::transaction::ValidEthTransaction;

// To produce 5k tx blocks, we need the tracked tx map to hold at least 15k addresses so that, after
// pruning the txpool of up to 5k unique addresses in the last committed block update and up to 5k
// unique addresses in the pending blocktree, the tracked tx map will still have at least 5k other
// addresses with at least one tx each to use when creating the next block.
const DEFAULT_MAX_ADDRESSES: usize = 16 * 1024;

const DEFAULT_MAX_TXS: usize = 64 * 1024;

const DEFAULT_MAX_EIP2718_BYTES: usize = 4 * 1024 * 1024 * 1024;

#[derive(Clone, Debug)]
pub(crate) struct TrackedTxLimitsConfig {
max_addresses: usize,
max_txs: usize,
max_eip2718_bytes: usize,

soft_evict_addresses_watermark: usize,

soft_tx_expiry: Duration,
hard_tx_expiry: Duration,
}

impl TrackedTxLimitsConfig {
pub fn new(soft_tx_expiry: Duration, hard_tx_expiry: Duration) -> Self {
Self {
max_addresses: DEFAULT_MAX_ADDRESSES,
max_txs: DEFAULT_MAX_TXS,
max_eip2718_bytes: DEFAULT_MAX_EIP2718_BYTES,

soft_evict_addresses_watermark: DEFAULT_MAX_ADDRESSES - 512,

soft_tx_expiry,
hard_tx_expiry,
}
}
}

#[derive(Clone, Debug)]
pub(crate) struct TrackedTxLimits {
config: TrackedTxLimitsConfig,

txs: usize,
eip2718_bytes: usize,
}

impl TrackedTxLimits {
pub fn new(config: TrackedTxLimitsConfig) -> Self {
Self {
config,

txs: 0,
eip2718_bytes: 0,
}
}

pub fn build_txs_map<V>(&self) -> IndexMap<Address, V> {
IndexMap::with_capacity(self.config.max_addresses)
}

pub fn expiry_duration_during_evict(&self) -> Duration {
if self.txs < self.config.soft_evict_addresses_watermark {
self.config.hard_tx_expiry
} else {
info!(num_txs =? self.txs, "txpool limits hit soft evict addresses watermark");
self.config.soft_tx_expiry
}
}

pub fn expiry_duration_during_insert(&self) -> Duration {
self.config.hard_tx_expiry
}

pub fn can_add_address(&self, addresses: usize) -> bool {
addresses < self.config.max_addresses
}

pub fn add_tx(&mut self, tx: &ValidEthTransaction) -> bool {
let txs = self.txs + 1;
let eip2718_bytes = self.eip2718_bytes + tx.raw().eip2718_encoded_length();

if txs > self.config.max_txs {
return false;
}

if eip2718_bytes > self.config.max_eip2718_bytes {
return false;
}

self.txs = txs;
self.eip2718_bytes = eip2718_bytes;

true
}

pub fn remove_tx(&mut self, tx: &ValidEthTransaction) {
self.txs = self.txs.checked_sub(1).unwrap_or_else(|| {
error!("txpool txs limit underflowed, detected during remove_tx");
0
});

self.eip2718_bytes = self
.eip2718_bytes
.checked_sub(tx.raw().eip2718_encoded_length())
.unwrap_or_else(|| {
error!("txpool eip2718_bytes limit underflowed, detected during remove_tx");
0
});
}

pub fn remove_txs<'a>(&mut self, txs: impl Iterator<Item = &'a ValidEthTransaction>) {
for tx in txs {
self.remove_tx(tx)
}
}
}
58 changes: 38 additions & 20 deletions monad-eth-txpool/src/pool/tracked/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ use monad_eth_txpool_types::EthTxPoolDropReason;
use monad_types::Nonce;
use tracing::error;

use crate::{pool::transaction::ValidEthTransaction, EthTxPoolEventTracker};
use super::{limits::TrackedTxLimits, ValidEthTransaction};
use crate::EthTxPoolEventTracker;

/// Stores byte-validated transactions alongside the an account_nonce to enforce at the type level
/// that all the transactions in the txs map have a nonce at least account_nonce. Similar to
Expand All @@ -42,19 +43,20 @@ impl TrackedTxList {
pub fn try_new(
this_entry: VacantEntry<'_, Address, Self>,
event_tracker: &mut EthTxPoolEventTracker<'_>,
limit_tracker: &mut TrackedTxLimits,
txs: Vec<ValidEthTransaction>,
account_nonce: u64,
on_insert: &mut impl FnMut(&ValidEthTransaction),
last_commit_base_fee: u64,
tx_expiry: Duration,
) {
let mut this = TrackedTxList {
account_nonce,
txs: BTreeMap::default(),
};

for tx in txs {
if let Some(tx) = this.try_insert_tx(event_tracker, tx, last_commit_base_fee, tx_expiry)
if let Some(tx) =
this.try_insert_tx(event_tracker, limit_tracker, tx, last_commit_base_fee)
{
on_insert(tx);
}
Expand Down Expand Up @@ -105,9 +107,9 @@ impl TrackedTxList {
pub(crate) fn try_insert_tx(
&mut self,
event_tracker: &mut EthTxPoolEventTracker<'_>,
limit_tracker: &mut TrackedTxLimits,
tx: ValidEthTransaction,
last_commit_base_fee: u64,
tx_expiry: Duration,
) -> Option<&ValidEthTransaction> {
if tx.nonce() < self.account_nonce {
event_tracker.drop(tx.hash(), EthTxPoolDropReason::NonceTooLow);
Expand All @@ -122,29 +124,37 @@ impl TrackedTxList {
btree_map::Entry::Occupied(mut entry) => {
let (existing_tx, existing_tx_insert_time) = entry.get();

if tx_expired(existing_tx_insert_time, tx_expiry, &event_tracker.now)
|| tx.has_higher_priority(existing_tx, last_commit_base_fee)
if !tx_expired(
existing_tx_insert_time,
limit_tracker.expiry_duration_during_insert(),
&event_tracker.now,
) && !tx.has_higher_priority(existing_tx, last_commit_base_fee)
{
event_tracker.replace(
tx.signer_ref(),
existing_tx.hash(),
tx.hash(),
tx.is_owned(),
);
entry.insert((tx, event_tracker.now));

Some(&entry.into_mut().0)
} else {
event_tracker.drop(tx.hash(), EthTxPoolDropReason::ExistingHigherPriority);
return None;
}

None
if !limit_tracker.add_tx(&tx) {
event_tracker.drop(tx.hash(), EthTxPoolDropReason::PoolFull);
return None;
}

event_tracker.replace(
tx.signer_ref(),
existing_tx.hash(),
tx.hash(),
tx.is_owned(),
);
entry.insert((tx, event_tracker.now));

Some(&entry.into_mut().0)
}
}
}

pub fn update_committed_nonce_usage(
event_tracker: &mut EthTxPoolEventTracker<'_>,
limit_tracker: &mut TrackedTxLimits,
mut this: indexmap::map::OccupiedEntry<'_, Address, Self>,
nonce_usage: NonceUsage,
) {
Expand All @@ -164,6 +174,7 @@ impl TrackedTxList {

let txs = this.get_mut().txs.split_off(&account_nonce);

limit_tracker.remove_txs(txs.values().map(|(tx, _)| tx));
event_tracker.tracked_commit(
txs.is_empty(),
this.get().txs.values().map(|(tx, _)| tx.hash()),
Expand All @@ -180,25 +191,30 @@ impl TrackedTxList {
// Produces true when the entry was removed and false otherwise
pub fn evict_expired_txs(
event_tracker: &mut EthTxPoolEventTracker<'_>,
limit_tracker: &mut TrackedTxLimits,
mut this: indexmap::map::IndexedEntry<'_, Address, Self>,
tx_expiry: Duration,
) -> bool {
let now = Instant::now();

let txs = &mut this.get_mut().txs;

let mut removed_hashes = Vec::default();
let mut removed_txs = Vec::default();

txs.retain(|_, (tx, tx_insert)| {
if !tx_expired(tx_insert, tx_expiry, &now) {
return true;
}

removed_hashes.push(tx.hash());
removed_txs.push(tx.clone());
false
});

event_tracker.tracked_evict_expired(txs.is_empty(), removed_hashes.into_iter());
limit_tracker.remove_txs(removed_txs.iter());
event_tracker.tracked_evict_expired(
txs.is_empty(),
removed_txs.iter().map(ValidEthTransaction::hash),
);

if txs.is_empty() {
this.swap_remove();
Expand All @@ -211,6 +227,7 @@ impl TrackedTxList {
pub fn static_validate_all_txs<CRT>(
&mut self,
event_tracker: &mut EthTxPoolEventTracker<'_>,
limit_tracker: &mut TrackedTxLimits,
chain_id: u64,
chain_revision: &CRT,
execution_revision: &MonadExecutionRevision,
Expand All @@ -227,6 +244,7 @@ impl TrackedTxList {
return true;
};

limit_tracker.remove_tx(tx);
event_tracker.drop(tx.hash(), EthTxPoolDropReason::NotWellFormed(error));

false
Expand Down
Loading