Skip to content

merge basefee promotion with account updates #18401

@mattsse

Description

@mattsse

Describe the feature

we currently trigger poolupdates in two places:

// update block info
let block_hash = block_info.last_seen_block_hash;
self.set_block_info(block_info);
// Remove all transaction that were included in the block
let mut removed_txs_count = 0;
for tx_hash in &mined_transactions {
if self.prune_transaction_by_hash(tx_hash).is_some() {
removed_txs_count += 1;
}
}
// Update removed transactions metric
self.metrics.removed_transactions.increment(removed_txs_count);
let UpdateOutcome { promoted, discarded } = self.update_accounts(changed_senders);

Place 1: Basefee/Blobfee-triggered updates (lines 580-580)

        // update block info
        let block_hash = block_info.last_seen_block_hash;
        self.set_block_info(block_info);

This calls set_block_info() which internally calls:

  • update_basefee() → can trigger promotions from BaseFee pool to Pending when basefee decreases
  • update_blob_fee() → can trigger promotions/demotions for blob transactions

Place 2: Account/sender-triggered updates (lines 593-593)

        // Update removed transactions metric
        self.metrics.removed_transactions.increment(removed_txs_count);

        let UpdateOutcome { promoted, discarded } = self.update_accounts(changed_senders);

This calls update_accounts() which:

  • Processes sender balance/nonce changes
  • Triggers sub-pool updates based on account state changes

The Problem
Both places can update the same transactions independently:

  1. Basefee decrease → promotes transactions from BaseFee → Pending via enforce_basefee_with()
  2. Sender changes → same transactions might get updated again via update_accounts()

This creates overlapping/duplicate tracking in the promoted and discarded outcome sets, leading to inaccurate metrics and potential inconsistent pool state.

where a lower basefee can trigger a promotion:

Ordering::Less => {
// Base fee decreased: recheck BaseFee and promote.
// Invariants:
// - BaseFee contains only non-blob txs (blob txs live in Blob) and they already
// have ENOUGH_BLOB_FEE_CAP_BLOCK.
// - PENDING_POOL_BITS = BASE_FEE_POOL_BITS | ENOUGH_FEE_CAP_BLOCK |
// ENOUGH_BLOB_FEE_CAP_BLOCK.
// With the lower base fee they gain ENOUGH_FEE_CAP_BLOCK, so we can set the bit and
// insert directly into Pending (skip generic routing).
self.basefee_pool.enforce_basefee_with(
self.all_transactions.pending_fees.base_fee,
|tx| {
// Update transaction state — guaranteed Pending by the invariants above
let meta =
self.all_transactions.txs.get_mut(tx.id()).expect("tx exists in set");
meta.state.insert(TxState::ENOUGH_FEE_CAP_BLOCK);

which we currently dont track properly and only via the changed accounts:

let UpdateOutcome { promoted, discarded } = self.update_accounts(changed_senders);

the problem here is that these can overlap

e.g. promoted basefee txs can also appear in either promoted or discarded

a potential fix here is
before updating the basefee internally, use the new basefee to iterate over the changed senders

// Apply the state changes to the total set of transactions which triggers sub-pool updates.
let updates = self.all_transactions.update(&changed_senders);

then we get and perform the updates for all senders correctly with the new fee
and after we append the updates from updating the fee pool

Additional context

No response

Metadata

Metadata

Assignees

Labels

A-tx-poolRelated to the transaction mempoolC-bugAn unexpected or incorrect behaviorC-enhancementNew feature or request

Type

No type

Projects

Status

Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions