EIP-8141: cap pending frame transactions per non-canonical paymaster - #12774
EIP-8141: cap pending frame transactions per non-canonical paymaster#12774Marchhill wants to merge 29 commits into
Conversation
The public-mempool rules bound how many pending transactions a single non-canonical paymaster may sponsor (MAX_PENDING_TXS_USING_NON_CANONICAL_PAYMASTER = 1), so one sponsor's balance or code change cannot invalidate an unbounded set of pending transactions. Nothing enforced that: a flood of frame txs all paying through one contract sponsor was admitted and gossiped. FrameTxPaymasterFilter rejects a frame tx whose recognized validation prefix ends in a pay frame targeting a code-carrying account that already sponsors the maximum pending transactions. The count is keyed on the pay-frame target, which FrameTxValidation.GetPrefixPaymaster derives from the frame layout alone, so the pool's insert and remove hooks stay symmetric even when the paymaster's code changes while a transaction is pending. A default-code sponsor is not a paymaster and stays governed by the per-payer exposure rule. No canonical paymaster runtime is pinned in production yet, so every code- carrying pay target is treated as non-canonical; exempting canonical instances additionally requires their balance reservation, which remains outstanding.
|
Claude finished @Marchhill's task in 5m 46s —— View job Review: EIP-8141 non-canonical paymaster pending capReviewed
What holds upThe core design is sound and I could not break the accounting:
Findings
Style, DI, and comment discipline follow the repo rules; the shared prefix grammar is reused rather than duplicated, and the constant carries its EIP reference. Verdict: not blocking-critical, but I'd want #1 fixed (or explicitly deferred with rationale in the PR body) and #3 added before merge, since the wiring is the only part of this change with no test behind it. Note: I could not run |
…nature work The pending cap counted the transaction a fee bump would displace, so with MAX_PENDING_TXS_USING_NON_CANONICAL_PAYMASTER = 1 a sponsored frame tx could never be re-priced or cancelled by replacement: the tx it replaces still held the paymaster's only slot when the filter ran, and the pool only swaps them later, inside TryInsert. The spec decrements on replacement, so the pending set does not grow; discount a same-sender, same-nonce pending tx naming the same paymaster before comparing against the cap. Requiring the same paymaster matters, since replacing a tx sponsored elsewhere frees that sponsor's slot while still taking one here. Also move the filter ahead of FrameTxSignatureFilter. It needs only the frame layout and one account read, so leaving it behind the uncapped elliptic-curve work defeated the point of a cheap flood gate; the trade-off is that a flood tx is no longer also reported as malformed. Cover the TxPool wiring end to end, which the hand-driven counter in the filter tests did not: two senders sharing one code-carrying pay target, admitted then capped then admitted again after removal.
|
Two summary-only findings (no inline threads):
This branch has also been merged forward onto the current devnet7 tip so it sits on the same base as the rest of the chain. |
|
@claude review |
|
Claude finished @Marchhill's task in 5m 37s —— View job Review: EIP-8141 non-canonical paymaster pending cap (re-review)Re-reviewed
Prior findings — all addressed
What I verified on the new code
Findings
Style, DI and comment discipline follow the repo rules; the prefix grammar is reused rather than duplicated, the constant carries its EIP reference, tests are parameterized via Verdict: finding #1 is the only one I'd hold merge on — it's the same fee-bump regression that Note: |
Also discount a replacement from the blob pool: a blob-carrying frame tx is counted against the cap from there, so its fee bump was rejected for want of the discount.
# Conflicts: # src/Nethermind/Nethermind.TxPool/Metrics.cs
|
Two findings from the summary table that were not filed inline: #2 (deferred list). The ordering bypass is now stated rather than framed as passive staleness: a code-less pay target that later gains code is attacker-ordered, and the count is not re-evaluated. It is recorded in the #3 (trace message off by one). Valid — after the discount, |
|
@claude review |
|
Claude finished @Marchhill's task in 5m 55s —— View job Review: EIP-8141 non-canonical paymaster pending cap (round 3)Re-reviewed
Prior findings — all addressed
What I re-verified on this tip
Findings
Style, DI and comment discipline follow the repo rules: Verdict: finding #1 is the one I'd resolve before merge — it's a two-line predicate change plus a test case, and as it stands the documented contract and the behaviour disagree in a way that silently throttles smart-account senders to a single pending frame transaction. #2–#4 are cosmetic or documentation. Note: |
…not self-payment Pin a code-carrying sender naming itself in the pay frame as capped, drive the blob-path replacement test through the real blob pool type, and record the reorg re-admission and broadcaster-retained count gaps.
# Conflicts: # src/Nethermind/Nethermind.TxPool.Test/TxPoolTests.cs # src/Nethermind/Nethermind.TxPool/Filters/NotSupportedTxFilter.cs
# Conflicts: # src/Nethermind/Nethermind.TxPool/Filters/NotSupportedTxFilter.cs # src/Nethermind/Nethermind.TxPool/TxPoolErrorMessages.cs
Renumber the paymaster-cap result: AcceptTxResult compares by id, so it collided with the simulation-failed result the merge brought down.
# Conflicts: # src/Nethermind/Nethermind.TxPool/TxPool.cs
|
@claude review |
|
Claude finished @Marchhill's task in 6m 45s —— View job Review: EIP-8141 non-canonical paymaster pending cap (round 4)Re-reviewed
Prior findings
What holds up
Findings
Style, DI and comment discipline follow the repo rules: Verdict: #1 is the one I'd hold merge on — it is the same fee-bump regression and a bypass of the cap itself, and the fix is already written two files away in Note: |
NonCanonicalPaymasterLimitReached shared id 25 with FrameSimulationFailed, and AcceptTxResult compares by id alone, so the two were equal; 26 is taken by FrameSimulationDeferred on the simulation-guards branch, so this takes 27. The replacement discount now matches on the pool's competing key, since an EIP-8250 transaction sharing a nonce in another key domain joins the pending set rather than displacing it and must still count against the sponsor. Restores the payer-exposure gap item, which is still open on this branch.
Changes
Enforces the EIP-8141 public-mempool cap on how many pending frame transactions may pay through one non-canonical paymaster (
MAX_PENDING_TXS_USING_NON_CANONICAL_PAYMASTER = 1). Nothing enforced it before: a flood of frame transactions all naming one contract sponsor was admitted and gossiped, so a single balance or code change on that sponsor could invalidate an unbounded set of pending transactions — exactly the dependency the mempool rules exist to bound.FrameTxValidation.GetPrefixPaymaster— thepayframe target ending a recognized validation prefix, derived from the frame layout alone (no state read). Reuses the existing shared prefix grammar, so a leadingexpiry_verify/deployframe is skipped just as it is for pricing and payer resolution. A self-relay prefix, an unrecognized layout, or a null pay target (which resolves to the sender, not a sponsor) all yieldnull.PendingPaymasterCache— pending-transaction count per paymaster, incremented on pool insert and decremented on pool removal (covering eviction, replacement, inclusion and reorg removal, which all funnel through the existingRemovedevent). Because the key is state-free, the count a transaction contributes on insert is exactly the one it releases on removal, even if the paymaster's code changes while it is pending. Over-release clamps at zero so the cap can never be disabled.FrameTxPaymasterFilter— rejects with a newAcceptTxResult.NonCanonicalPaymasterLimitReachedwhen the pay target already sponsors the maximum. Registered ahead of payer resolution and validation-prefix simulation so a flood naming one sponsor is dropped before that work is spent.PendingTransactionsFrameTxPaymasterLimitReached.Only a
paytarget that carries code is a paymaster: per the spec, a target with the empty code hash is a default-code sponsor, governed by the per-payer exposure rule alone (FrameTxPayerExposureFilter, #12617).The check reads the pending count rather than taking a reservation, so it holds no state a later rejecting filter would have to release — the same trade-off
DelegatedAccountFilteralready makes for pending delegations, where concurrent submissions naming one address may briefly exceed the bound.Base branch
Stacked on
eip8141-mempool-phase2(#12624), the tip of the EIP-8141 mempool chain (#12610 → #12617 → #12624), for two reasons: the shared validation-prefix grammar this builds on lands in that chain, and the paymaster cap is the sibling of the per-payer exposure rule in #12617 — the spec states them in the same paragraph. It is otherwise independent of the simulation layer and could be rebased onto an earlier link if the chain is reordered.Deferred
paytarget is treated as non-canonical. That is the conservative direction — declining is always mempool-legal — but it also caps a canonical instance that should instead be admitted under the balance-reservation rule. Exempting one requires both the pinned runtime code hash andreserved_pending_cost/pending_withdrawal_amountaccounting, which stays on theEIP8141-GAPlist inNotSupportedTxFilter.Types of changes
What types of changes does your code introduce?
Testing
Requires testing
If yes, did you write tests?
Notes on testing
FrameTxPaymasterFilterTestscovers the cap matrix (deployed paymaster rejected; the same prefix behind leadingexpiry_verify+deployframes still keyed on the pay target; default-code sponsor accepted; self-relay, null pay target, unrecognized prefix and non-frame transactions all pass through), the first-admitted / second-rejected sequence, re-admission once the pending transaction leaves the pool, and the cache's count-up / clamp-at-zero behaviour.Full
Nethermind.TxPool.Testsuite green (729 passed, 1 skipped);Nethermind.Core.Testframe suites green (88); full Runner build clean,dotnet format whitespaceclean.Documentation
Requires documentation update
Requires explanation in Release Notes