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
3 changes: 2 additions & 1 deletion crates/node/rpc/src/txpool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,8 @@ mod tests {
hash[20..28].copy_from_slice(&nonce.to_be_bytes());
hash[28..].copy_from_slice(&(gas_price as u32).to_be_bytes());
let hash = B256::from(hash);
OrderedTransaction::new(hash, sender, nonce, gas_price, 0, envelope)
// base_fee=0 for test helper; effective_gas_price = gas_price
OrderedTransaction::new(hash, sender, nonce, gas_price, 0, 0, envelope)
}

#[tokio::test]
Expand Down
63 changes: 50 additions & 13 deletions crates/node/txpool/src/ordering.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ pub struct OrderedTransaction {
pub sender: Address,
/// Transaction nonce.
pub nonce: u64,
/// Effective gas price for ordering.
/// Effective gas price (EIP-1559: `min(max_fee_per_gas, base_fee + max_priority_fee_per_gas)`).
pub effective_gas_price: u128,
/// The base fee used to compute `effective_gas_price`.
pub base_fee: u128,
/// Timestamp when transaction was received.
pub timestamp: u64,
/// The decoded transaction envelope.
Expand All @@ -29,16 +31,16 @@ impl OrderedTransaction {
sender: Address,
nonce: u64,
effective_gas_price: u128,
base_fee: u128,
timestamp: u64,
envelope: TxEnvelope,
) -> Self {
Self { hash, sender, nonce, effective_gas_price, timestamp, envelope }
Self { hash, sender, nonce, effective_gas_price, base_fee, timestamp, envelope }
}

/// Calculates the effective tip given a base fee.
pub fn effective_tip(&self, base_fee: Option<u128>) -> u128 {
base_fee
.map_or(self.effective_gas_price, |base| self.effective_gas_price.saturating_sub(base))
/// Calculates the effective tip (priority fee) the transaction pays above the base fee.
pub const fn effective_tip(&self) -> u128 {
self.effective_gas_price.saturating_sub(self.base_fee)
}
}

Expand All @@ -59,8 +61,8 @@ impl PartialOrd for OrderedTransaction {
impl Ord for OrderedTransaction {
fn cmp(&self, other: &Self) -> Ordering {
other
.effective_gas_price
.cmp(&self.effective_gas_price)
.effective_tip()
.cmp(&self.effective_tip())
.then_with(|| self.timestamp.cmp(&other.timestamp))
.then_with(|| self.hash.cmp(&other.hash))
}
Expand Down Expand Up @@ -101,7 +103,7 @@ impl SenderQueue {
match self.queued.binary_search_by(|q| q.nonce.cmp(&tx.nonce)) {
Ok(pos) => {
let existing = &self.queued[pos];
if tx.effective_gas_price > existing.effective_gas_price {
if tx.effective_tip() > existing.effective_tip() {
let old = std::mem::replace(&mut self.queued[pos], tx);
Some(old)
} else {
Expand All @@ -117,7 +119,7 @@ impl SenderQueue {
let idx = (tx.nonce - self.next_nonce) as usize;
if idx < self.pending.len() {
let existing = &self.pending[idx];
if tx.effective_gas_price > existing.effective_gas_price {
if tx.effective_tip() > existing.effective_tip() {
let old = std::mem::replace(&mut self.pending[idx], tx);
return Some(old);
}
Expand Down Expand Up @@ -211,12 +213,21 @@ mod tests {
}

fn make_tx(nonce: u64, gas_price: u128) -> OrderedTransaction {
make_tx_with_base_fee(nonce, gas_price, gas_price, 0)
}

fn make_tx_with_base_fee(
nonce: u64,
max_fee: u128,
max_priority_fee: u128,
base_fee: u128,
) -> OrderedTransaction {
let inner = TxEip1559 {
chain_id: 1,
nonce,
gas_limit: 21000,
max_fee_per_gas: gas_price,
max_priority_fee_per_gas: gas_price,
max_fee_per_gas: max_fee,
max_priority_fee_per_gas: max_priority_fee,
to: TxKind::Call(Address::ZERO),
value: U256::ZERO,
access_list: Default::default(),
Expand All @@ -229,7 +240,16 @@ mod tests {
);
let signed = inner.into_signed(sig);
let envelope = TxEnvelope::from(signed);
OrderedTransaction::new(random_b256(), random_address(), nonce, gas_price, 0, envelope)
let effective_gas_price = std::cmp::min(max_fee, base_fee + max_priority_fee);
OrderedTransaction::new(
random_b256(),
random_address(),
nonce,
effective_gas_price,
base_fee,
0,
envelope,
)
}

#[test]
Expand Down Expand Up @@ -316,4 +336,21 @@ mod tests {

assert!(tx2 < tx1);
}

/// EIP-1559: a transaction with higher effective tip should be ordered first,
/// even when its max_fee_per_gas is lower.
#[test]
fn eip1559_ordering_by_effective_tip() {
let base_fee: u128 = 10;

// tx_high_max: max_fee=100, priority_fee=1 => effective_gas_price=11, tip=1
let tx_high_max = make_tx_with_base_fee(0, 100, 1, base_fee);
// tx_high_tip: max_fee=50, priority_fee=50 => effective_gas_price=50, tip=40
let tx_high_tip = make_tx_with_base_fee(0, 50, 50, base_fee);

// tx_high_tip has a higher effective tip (40 vs 1), so it should be ordered first (< in Ord)
assert!(tx_high_tip < tx_high_max);
assert_eq!(tx_high_tip.effective_tip(), 40);
assert_eq!(tx_high_max.effective_tip(), 1);
}
}
62 changes: 46 additions & 16 deletions crates/node/txpool/src/pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ fn insertion_target(queue: Option<&SenderQueue>, tx: &OrderedTransaction) -> Ins
pub struct TransactionPool {
inner: Arc<RwLock<PoolInner>>,
config: PoolConfig,
/// Current base fee used for EIP-1559 effective gas price computation.
base_fee: Arc<RwLock<u128>>,
events: Option<broadcast::Sender<MempoolEvent>>,
metrics: Arc<RwLock<Option<AppMetrics>>>,
}
Expand All @@ -143,6 +145,7 @@ impl TransactionPool {
Self {
inner: Arc::new(RwLock::new(PoolInner::new())),
config,
base_fee: Arc::new(RwLock::new(0)),
events: None,
metrics: Arc::new(RwLock::new(None)),
}
Expand All @@ -154,11 +157,25 @@ impl TransactionPool {
Self {
inner: Arc::new(RwLock::new(PoolInner::new())),
config,
base_fee: Arc::new(RwLock::new(0)),
events: Some(events),
metrics: Arc::new(RwLock::new(None)),
}
}

/// Updates the current base fee used for EIP-1559 ordering.
///
/// Should be called whenever a new block is produced or received so that
/// transaction ordering and eviction reflect the latest fee market state.
pub fn set_base_fee(&self, base_fee: u128) {
*self.base_fee.write() = base_fee;
}

/// Returns the current base fee.
pub fn base_fee(&self) -> u128 {
*self.base_fee.read()
}

/// Attach application-level metrics to this pool.
///
/// Because the metrics handle is shared across all clones of this pool,
Expand Down Expand Up @@ -321,8 +338,8 @@ impl TransactionPool {
return Err(TxPoolError::PoolFull);
}
if inner.pending_count >= self.config.max_pending_txs
&& let Some(min_price) = Self::min_pending_price(inner)
&& tx.effective_gas_price <= min_price
&& let Some(min_tip) = Self::min_pending_tip(inner)
&& tx.effective_tip() <= min_tip
{
return Err(TxPoolError::PoolFull);
}
Expand All @@ -332,8 +349,8 @@ impl TransactionPool {
return Err(TxPoolError::PoolFull);
}
if inner.queued_count >= self.config.max_queued_txs
&& let Some(min_price) = Self::min_queued_price(inner)
&& tx.effective_gas_price <= min_price
&& let Some(min_tip) = Self::min_queued_tip(inner)
&& tx.effective_tip() <= min_tip
{
return Err(TxPoolError::PoolFull);
}
Expand All @@ -344,19 +361,19 @@ impl TransactionPool {
Ok(())
}

fn min_pending_price(inner: &PoolInner) -> Option<u128> {
fn min_pending_tip(inner: &PoolInner) -> Option<u128> {
inner
.by_sender
.values()
.flat_map(|queue| queue.pending.iter().map(|tx| tx.effective_gas_price))
.flat_map(|queue| queue.pending.iter().map(|tx| tx.effective_tip()))
.min()
}

fn min_queued_price(inner: &PoolInner) -> Option<u128> {
fn min_queued_tip(inner: &PoolInner) -> Option<u128> {
inner
.by_sender
.values()
.flat_map(|queue| queue.queued.iter().map(|tx| tx.effective_gas_price))
.flat_map(|queue| queue.queued.iter().map(|tx| tx.effective_tip()))
.min()
}

Expand All @@ -365,7 +382,7 @@ impl TransactionPool {
.by_sender
.values()
.flat_map(|queue| queue.pending.iter())
.min_by_key(|tx| (tx.effective_gas_price, std::cmp::Reverse(tx.timestamp), tx.hash))
.min_by_key(|tx| (tx.effective_tip(), std::cmp::Reverse(tx.timestamp), tx.hash))
.map(|tx| tx.hash)?;
let removed = inner.remove_by_hash(&hash);
inner.update_counts();
Expand All @@ -377,7 +394,7 @@ impl TransactionPool {
.by_sender
.values()
.flat_map(|queue| queue.queued.iter())
.min_by_key(|tx| (tx.effective_gas_price, std::cmp::Reverse(tx.timestamp), tx.hash))
.min_by_key(|tx| (tx.effective_tip(), std::cmp::Reverse(tx.timestamp), tx.hash))
.map(|tx| tx.hash)?;
let removed = inner.remove_by_hash(&hash);
inner.update_counts();
Expand Down Expand Up @@ -580,6 +597,7 @@ impl Clone for TransactionPool {
Self {
inner: self.inner.clone(),
config: self.config.clone(),
base_fee: self.base_fee.clone(),
events: self.events.clone(),
metrics: self.metrics.clone(), // Arc clone: all clones share the same metrics handle
}
Expand Down Expand Up @@ -633,32 +651,43 @@ fn rejection_reason(err: &TxPoolError) -> String {
}
}

fn tx_to_ordered(tx: &Tx) -> Option<OrderedTransaction> {
fn tx_to_ordered(tx: &Tx, base_fee: u128) -> Option<OrderedTransaction> {
let envelope = TxEnvelope::decode_2718(&mut tx.bytes.as_ref()).ok()?;
let sender = recover_sender_from_envelope(&envelope).ok()?;
let hash = alloy_primitives::keccak256(&tx.bytes);
let nonce = envelope.nonce();
let effective_gas_price = match &envelope {
TxEnvelope::Legacy(tx) => tx.tx().gas_price,
TxEnvelope::Eip2930(tx) => tx.tx().gas_price,
TxEnvelope::Eip1559(tx) => tx.tx().max_fee_per_gas,
TxEnvelope::Eip4844(tx) => tx.tx().tx().max_fee_per_gas,
TxEnvelope::Eip7702(tx) => tx.tx().max_fee_per_gas,
TxEnvelope::Eip1559(tx) => {
let tx = tx.tx();
std::cmp::min(tx.max_fee_per_gas, base_fee + tx.max_priority_fee_per_gas)
}
TxEnvelope::Eip4844(tx) => {
let tx = tx.tx().tx();
std::cmp::min(tx.max_fee_per_gas, base_fee + tx.max_priority_fee_per_gas)
}
TxEnvelope::Eip7702(tx) => {
let tx = tx.tx();
std::cmp::min(tx.max_fee_per_gas, base_fee + tx.max_priority_fee_per_gas)
}
};

Some(OrderedTransaction::new(
hash,
sender,
nonce,
effective_gas_price,
base_fee,
current_timestamp(),
envelope,
))
}

impl Mempool for TransactionPool {
fn insert(&self, tx: Tx) -> bool {
let Some(ordered) = tx_to_ordered(&tx) else {
let base_fee = self.base_fee();
let Some(ordered) = tx_to_ordered(&tx, base_fee) else {
trace!("failed to decode transaction for mempool insert");
self.record_rejection("decode_error");
return false;
Expand Down Expand Up @@ -804,7 +833,8 @@ mod tests {
let sig = Signature::from_scalars_and_parity(B256::ZERO, B256::ZERO, false);
let signed = inner.into_signed(sig);
let envelope = TxEnvelope::from(signed);
OrderedTransaction::new(random_b256(), sender, nonce, gas_price, 0, envelope)
// base_fee=0 so effective_gas_price = min(gas_price, 0 + gas_price) = gas_price
OrderedTransaction::new(random_b256(), sender, nonce, gas_price, 0, 0, envelope)
}

fn tx_nonce(tx: &Tx) -> u64 {
Expand Down
Loading
Loading