Skip to content

fix(txpool): evict AA2dPool transactions by priority instead of address to prevent DoS#2216

Merged
fgimenez merged 7 commits intomainfrom
fgimenez/chain-442
Jan 29, 2026
Merged

fix(txpool): evict AA2dPool transactions by priority instead of address to prevent DoS#2216
fgimenez merged 7 commits intomainfrom
fgimenez/chain-442

Conversation

@fgimenez
Copy link
Copy Markdown
Member

@fgimenez fgimenez commented Jan 22, 2026

Closes CHAIN-449

Fixes a DoS vulnerability in AA2dPool::discard() where transactions were evicted lexicographically by address instead of by priority. Attackers with vanity addresses (many leading zeroes like 0x0000...) could persist in the pool indefinitely while legitimate transactions from normal addresses were evicted first. This allowed low-cost DoS attacks on the transaction pool.

Changed discard() to sort transactions by CoinbaseTipOrdering priority and evict lowest-priority transactions first, regardless of sender address.

Copy link
Copy Markdown
Contributor

@0xrusowsky 0xrusowsky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the approach LGTM, but i left a cmnt for a small optimization (untested)

Copy link
Copy Markdown
Contributor

@mattsse mattsse left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this isnt a trivial problem to solve, hence the todo (hehe)

I think a more efficient and fair way to achieve this would be going by submission id which we can also use to clear older txs that we failed to evict based on some dynamic behaviour.

ideally we also track queued and pending separately so that we clear queued txs first.

so we should try to
a) evict queued txs first
b) only then start evicting pending ones

since we have const basefee the priority is also const, so we maybe order them by struct prio,submissionid in a separate map

Comment thread crates/transaction-pool/src/tt_2d_pool.rs Outdated
Comment thread crates/transaction-pool/src/tt_2d_pool.rs Outdated
@klkvr
Copy link
Copy Markdown
Member

klkvr commented Jan 23, 2026

yeah i think it should be cheap enough to maintain an extra index for transactions that can be evicted and that would also be properly sorted

@fgimenez
Copy link
Copy Markdown
Member Author

this isnt a trivial problem to solve, hence the todo (hehe)

I think a more efficient and fair way to achieve this would be going by submission id which we can also use to clear older txs that we failed to evict based on some dynamic behaviour.

ideally we also track queued and pending separately so that we clear queued txs first.

so we should try to a) evict queued txs first b) only then start evicting pending ones

since we have const basefee the priority is also const, so we maybe order them by struct prio,submissionid in a separate map

@mattsse indeed, makes total sense. i've pushed an impl for this, using BTreeSet instead of map, ptal

Copy link
Copy Markdown
Contributor

@mattsse mattsse left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find this a bit hard to review, because this now requires that we keep track if pending vs queued separately for eviction but we dont do this for the total set

maybe we should take a step back here and reconsider the current design instead of patching a bunch of stuff that makes is quite hard to reason about

Comment thread crates/transaction-pool/src/tt_2d_pool.rs Outdated
Comment thread crates/transaction-pool/src/tt_2d_pool.rs Outdated
@fgimenez
Copy link
Copy Markdown
Member Author

@mattsse all review comments should be addressed, ptal

Copy link
Copy Markdown
Contributor

@mattsse mattsse left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the atomic is technically not needed so we could optimize this away but relaxed also seems fine

Comment on lines +1173 to +1176
/// Uses `Arc<AtomicBool>` so we can mutate this flag without removing/reinserting
/// the transaction from the eviction set. This allows a single eviction set for
/// all transactions, with pending/queued filtering done at eviction time.
is_pending: Arc<AtomicBool>,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this doesnt technically need to be an atomic and can be rc refcell because this only ever accessed by the pool via mut anyway

Comment on lines +1188 to +1198
struct EvictionKey {
/// The transaction's priority (from `CoinbaseTipOrdering`).
priority: Priority<u128>,
/// The submission ID when the transaction was added to the pool.
/// This is unique per transaction, so it's sufficient for ordering.
submission_id: u64,
/// The transaction's unique identifier (for lookup during eviction).
tx_id: AA2dTransactionId,
/// Shared pending state for filtering without lookup.
is_pending: Arc<AtomicBool>,
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could technically just wrap arc AA2dInternalTransaction for this but this doesnt really matter here

@fgimenez fgimenez force-pushed the fgimenez/chain-442 branch 2 times, most recently from b624bf4 to e521651 Compare January 29, 2026 15:25
.values()
.filter(move |tx| tx.is_pending && tx.inner.transaction.origin == origin)
.filter(move |tx| {
tx.is_pending.load(Ordering::Relaxed) && tx.inner.transaction.origin == origin
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

last suggestion, would be nicer if this were a fn is_pending() then we can change this more easily later

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good call, done

}
std::cmp::Ordering::Equal => {
existing_tx.is_pending = true;
*existing_tx.is_pending.borrow_mut() = true;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would be nicer if we had fn is_pending and set_pending, then all of the borrow stuff is hidden and we can change this more easily

@fgimenez fgimenez added this pull request to the merge queue Jan 29, 2026
Merged via the queue into main with commit fce6b95 Jan 29, 2026
14 checks passed
@fgimenez fgimenez deleted the fgimenez/chain-442 branch January 29, 2026 16:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-txpool C-Bug An unexpected or incorrect behavior

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants