Skip to content

fix(l1): align mempool intrinsic-gas and EIP-7702 auth-list validation with execution#6892

Open
ElFantasma wants to merge 4 commits into
mainfrom
fix/mempool-intrinsic-gas-alignment
Open

fix(l1): align mempool intrinsic-gas and EIP-7702 auth-list validation with execution#6892
ElFantasma wants to merge 4 commits into
mainfrom
fix/mempool-intrinsic-gas-alignment

Conversation

@ElFantasma

Copy link
Copy Markdown
Contributor

Motivation

On Prague (the active mainnet fork) the mempool's intrinsic-gas check did not match what LEVM enforces at execution, so transactions the VM will reject at inclusion were admitted to the pool — wasting payload-builder cycles and crowding out valid txs. Two gaps (EF findings eip7702-mempool-intrinsic-gas-gap, mempool-calldata-floor-gas-gap):

  • the per-EIP-7702 authorization-tuple cost (PER_EMPTY_ACCOUNT_COST) was omitted;
  • the EIP-7623 calldata floor was not applied.

Additionally, an EIP-7702 transaction with an empty authorization_list (invalid per the EIP, rejected by LEVM in validate_type_4_tx) was admitted.

None of these are consensus issues — LEVM independently rejects all three at inclusion — but they let invalid txs pollute the pool, diverging from geth/reth which reject them at admission.

Description

  • transaction_intrinsic_gas now delegates to the VM's own helpers (intrinsic_gas_dimensions + intrinsic_gas_floor) for every fork instead of a parallel hand-rolled computation. This picks up the EIP-7702 auth-list cost and the EIP-7623 calldata floor automatically, and removes a drift-prone duplicate implementation. The floor is gated to fork >= Fork::Prague, matching the VM's default-hook gating.
  • validate_transaction rejects type-4 txs with an empty authorization_list via the new MempoolError::EmptyAuthorizationList.
  • Tests: added parity/regression tests for the auth-list cost, the EIP-7623 floor, and the empty-auth-list rejection. The pre-Istanbul calldata test was removed (unreachable: get_fork() floors at Paris, so EIP-2028 is always in effect, making it a duplicate of the post-Istanbul test); the two Shanghai create tests were corrected to EIP-2028 pricing.

Resolves #6889

Checklist

  • Added regression tests (intrinsic auth-list cost, EIP-7623 floor, empty-auth rejection)
  • cargo fmt + cargo clippy clean on the touched crates
  • No Store changes (no STORE_SCHEMA_VERSION bump needed)

@ElFantasma ElFantasma requested a review from a team as a code owner June 18, 2026 18:08
@github-actions github-actions Bot added the L1 Ethereum client label Jun 18, 2026
@github-actions

Copy link
Copy Markdown

🤖 Kimi Code Review

Overall Assessment: This PR correctly fixes a critical mempool-EVM inconsistency (pool pollution bug) by delegating intrinsic gas calculations to LEVM and adds proper EIP-7702 empty authorization list validation. The approach is sound and the implementation is idiomatic.

Specific Issues:

1. Incorrect EIP reference in comment

  • File: crates/blockchain/mempool.rs
  • Lines: 783-785
  • Issue: The comment mentions "EIP-7981 access-list data bytes" which appears to be an error. EIP-7981 is "Passive Root Exits" (or unrelated depending on context), not access list pricing. Access list intrinsic gas was introduced in EIP-2930. If referring to the calldata floor, that's EIP-7623.
  • Suggestion: Correct the comment to reference the appropriate EIP (likely EIP-2930 for access lists or remove if referring to general access list handling).

2. Test expectation change verification

  • File: test/tests/blockchain/mempool_tests.rs
  • Lines: 196, 221-222
  • Issue: The tests transaction_create_intrinsic_gas_pre_shanghai and transaction_create_intrinsic_gas_post_shanghai were updated to use TX_DATA_NON_ZERO_GAS_EIP2028 (16 gas) instead of TX_DATA_NON_ZERO_GAS (68 gas). This assumes build_basic_config_and_header(true, false) activates Istanbul.
  • Verification: Ensure the first boolean parameter in build_basic_config_and_header enables Istanbul (EIP-2028). If it doesn't, these test expectations are incorrect for pre-Istanbul scenarios.

3. Fork gating for EIP-7702 empty auth list

  • File: crates/blockchain/blockchain.rs
  • Lines: 2911-2917
  • Note: The empty authorization list check is not explicitly gated by Prague activation. While type-4 transactions should be rejected earlier as invalid transaction types pre-Prague, consider adding a fork check here for defense-in-depth if validate_transaction is called in contexts where the tx type hasn't been validated yet.

Positive Aspects:

  • Security: Correctly prevents mempool pollution by rejecting transactions that LEVM would reject at execution (EIP-7702 auth costs and EIP-7623 calldata floor).
  • Maintainability: Delegating to intrinsic_gas_dimensions eliminates code duplication between mempool and EVM, ensuring they stay synchronized.
  • Safety: Proper use of checked_add with overflow error mapping prevents panics on maliciously crafted gas values.
  • Testing: Good regression test coverage for the specific EF findings mentioned in the comments.

Nit:

  • File: crates/blockchain/mempool.rs
  • Line: 792
  • The variable name floor could be more descriptive (e.g., calldata_floor or eip7623_floor) to clarify it's the EIP-7623 minimum gas floor.

Automated review by Kimi (Moonshot AI) · kimi-k2.5 · custom prompt

@github-actions

github-actions Bot commented Jun 18, 2026

Copy link
Copy Markdown

Lines of code report

Total lines added: 14
Total lines removed: 45
Total lines changed: 59

Detailed view
+----------------------------------------+-------+------+
| File                                   | Lines | Diff |
+----------------------------------------+-------+------+
| ethrex/crates/blockchain/blockchain.rs | 2760  | +8   |
+----------------------------------------+-------+------+
| ethrex/crates/blockchain/error.rs      | 155   | +6   |
+----------------------------------------+-------+------+
| ethrex/crates/blockchain/mempool.rs    | 558   | -45  |
+----------------------------------------+-------+------+

@github-actions

Copy link
Copy Markdown

🤖 Claude Code Review

Now I have all the context I need. Here's the full review:


PR Review: fix(l1): align mempool intrinsic-gas and EIP-7702 auth-list validation with execution

Overview

This PR fixes two EF findings (eip7702-mempool-intrinsic-gas-gap, mempool-calldata-floor-gas-gap) by replacing a hand-rolled intrinsic gas calculation in the mempool with a delegation to LEVM's own helpers (intrinsic_gas_dimensions + intrinsic_gas_floor). It also adds a missing rejection of type-4 transactions with an empty authorization_list. The approach is sound and eliminates an entire category of future drift.


Correctness

The Prague gate on the floor is correctly derived.
The comment in mempool.rs asserts the VM gates validate_min_gas_limit the same way (fork >= Fork::Prague). This is verified: default_hook.rs:39 wraps the validate_min_gas_limit call in exactly that guard. The mempool and the VM are now in sync. ✓

intrinsic_gas_dimensions handles all forks correctly.
For Prague (non-Amsterdam), the auth-list branch takes the else path: regular_gas += PER_EMPTY_ACCOUNT_COST * n_tuples. For Amsterdam, it uses the new 2D model. The refactored mempool path inherits all of this for free. ✓

Existing Shanghai CREATE tests corrected.
build_basic_config_and_header(true, true) activates Istanbul, so EIP-2028 prices apply. The old expected values using TX_DATA_NON_ZERO_GAS (16 pre-EIP-2028) were wrong; the fix to TX_DATA_NON_ZERO_GAS_EIP2028 (16 — same value!) is... wait, these are the same constant value? Let me note this below.

Removal of pre-Istanbul test is justified.
get_fork() floors at Fork::Paris (Istanbul is always active for any practical config), making the pre-Istanbul branch unreachable. ✓


Issues

1. TX_DATA_NON_ZERO_GAS vs TX_DATA_NON_ZERO_GAS_EIP2028 — same value, wrong diagnosis?

In mempool_tests.rs lines 196 and 221, the test expectations were changed from TX_DATA_NON_ZERO_GAS to TX_DATA_NON_ZERO_GAS_EIP2028. The PR describes these as "corrected to EIP-2028 pricing." However, if both constants are numerically identical (both 16 gas per non-zero byte), the tests would have passed before and after with the same result. The change is semantically correct (using the right-named constant) but worth verifying that TX_DATA_NON_ZERO_GAS != TX_DATA_NON_ZERO_GAS_EIP2028 — if they are the same value, the previous tests weren't actually wrong and the description is misleading.

# please verify:
grep -n "TX_DATA_NON_ZERO_GAS\b\|TX_DATA_NON_ZERO_GAS_EIP2028" crates/blockchain/src/constants.rs

2. Empty auth-list check ordering vs. LEVM ordering

In blockchain.rs:2914, EmptyAuthorizationList is checked after TxIntrinsicGasCostAboveLimitError (line 2907). LEVM's validate_type_4_tx runs Type4TxAuthorizationListIsEmpty before gas validation. For a type-4 tx with an empty list and gas_limit below the intrinsic, the mempool returns TxIntrinsicGasCostAboveLimitError while the VM returns Type4TxAuthorizationListIsEmpty. This divergence in error precedence is minor but inconsistent. Swapping the two checks (empty-auth before intrinsic gas) would align with the VM's ordering and give the more specific error.

3. No fork gate on the empty auth-list check

The new EmptyAuthorizationList check has no fork >= Fork::Prague guard. EIP-7702 (type-4) transactions only exist from Prague onward; a pre-Prague network that somehow receives a type-4 tx would hit this check rather than a Type4TxPreFork-equivalent error. This is a pre-existing gap in the mempool (no pre-Prague type-4 rejection exists anywhere here), but the new check could mask it. Low severity in practice — mainnet is on Prague — but worth a comment or an explicit guard for correctness.

4. Slightly misleading test comment

In prague_config_and_header():

/// Prague config where Amsterdam is NOT active, so `transaction_intrinsic_gas`
/// exercises the non-Amsterdam path.

After this PR, transaction_intrinsic_gas has no separate "Amsterdam path" — both cases call intrinsic_gas_dimensions and differ only within that function. The comment should read something like "...so intrinsic_gas_dimensions exercises its pre-Amsterdam sub-path (flat 2D cost)."

5. Error-loss in error mapping

In mempool.rs:

intrinsic_gas_dimensions(tx, fork, header.gas_limit)
    .map_err(|_| MempoolError::TxGasOverflowError)?;

intrinsic_gas_dimensions returns VMError, which can include TypeConversion or Custom errors, all silently mapped to TxGasOverflowError. This matches the previous Amsterdam-gated code's behavior and isn't a regression, but it would be marginally better to log or at least distinguish the internal error type. Low priority.


Tests

The three regression tests are well-structured:

  • prague_eip7702_intrinsic_includes_auth_list_cost: correct expected value 21000 + 2 × 25000 = 71000
  • prague_intrinsic_applies_eip7623_calldata_floor: 1000 zero-bytes → 1000 weighted tokens × 10 + base = 31000. Math checks out ✓
  • validate_transaction_rejects_empty_auth_list: genesis setup is thorough; the max_fee_per_gas: 0 and 100 ETH balance ensure balance/tip checks don't fire first, so the empty-auth rejection is exercised cleanly ✓

The full genesis config in the async test is verbose. A helper similar to prague_config_and_header plus setup_storage (already in the file) would reduce boilerplate, but this is a style point.


Summary

The core fix is correct and well-reasoned. The delegation to intrinsic_gas_dimensions + intrinsic_gas_floor is the right design: it eliminates an entire class of drift bugs. Point 1 (same constant value) should be verified before merge. Points 2 and 3 are worth a follow-up issue or small fixup; the rest are cosmetic.


Automated review by Claude (Anthropic) · sonnet · custom prompt

@github-actions

Copy link
Copy Markdown

🤖 Codex Code Review

Findings

  1. crates/blockchain/mempool.rs: transaction_intrinsic_gas() now delegates to intrinsic_gas_dimensions() on every fork, but that helper prices calldata through gas_cost::tx_calldata(), which is hard-coded to 16 gas per non-zero byte (crates/vm/levm/src/gas_cost.rs). Pre-Istanbul mempool admission should still charge 68 per non-zero byte. The removed test and the updated expectations in test/tests/blockchain/mempool_tests.rs are masking that regression rather than fixing it. On legacy configs, this can admit under-gassed transactions that should be rejected.

  2. crates/blockchain/blockchain.rs: the new admission check only rejects type-4 transactions when authorization_list is empty. LEVM also rejects all EIP-7702 transactions before Prague (crates/vm/levm/src/hooks/default_hook.rs). Without mirroring that fork gate here, a pre-Prague node can still accept a non-empty type-4 tx into the pool even though execution will later reject it.

I could not run the targeted Rust tests in this environment because cargo/rustup tried to write under /home/runner/.rustup, which is read-only here.


Automated review by OpenAI Codex · gpt-5.4 · custom prompt

@greptile-apps

greptile-apps Bot commented Jun 18, 2026

Copy link
Copy Markdown

Greptile Summary

This PR closes two EF-reported mempool/LEVM divergence gaps on Prague: the per-EIP-7702 authorization-tuple intrinsic cost (PER_EMPTY_ACCOUNT_COST × n_tuples) was silently omitted, and the EIP-7623 calldata floor was never applied, so transactions the VM would reject at inclusion were passing admission. It also adds an explicit rejection for type-4 txs with an empty authorization_list, which LEVM rejects during execution but the mempool previously admitted.

  • transaction_intrinsic_gas now delegates to intrinsic_gas_dimensions + intrinsic_gas_floor for all forks, removing the parallel hand-rolled computation and keeping the mempool permanently in lockstep with LEVM. The EIP-7623 floor is gated to fork >= Fork::Prague to match the VM's own gating.
  • A new MempoolError::EmptyAuthorizationList variant is returned when a type-4 tx carries an empty authorization_list.
  • Test suite adds parity regressions for the auth-list cost and calldata floor; corrects two Shanghai create tests that used the pre-EIP-2028 gas constant (68) despite having Istanbul activated.

Confidence Score: 4/5

Safe to merge. The change is a targeted fix to two well-scoped mempool admission gaps; LEVM itself is untouched and serves as the authoritative gate at inclusion time, so no consensus risk is introduced.

The refactoring is well-motivated and the new implementation delegates directly to the VM helpers it was previously duplicating, which is strictly better. The regression tests cover all three newly-enforced invariants. The only item worth a second look is the ordering of the empty-auth-list guard relative to the intrinsic-gas check: for the degenerate case of a type-4 tx with both an empty auth list and a gas_limit below 21 000, the returned error misidentifies the root cause — harmless in practice since the tx is rejected either way, but a diagnostics inconsistency.

crates/blockchain/blockchain.rs — review the placement of the EmptyAuthorizationList guard relative to the intrinsic-gas check.

Important Files Changed

Filename Overview
crates/blockchain/blockchain.rs Adds empty-auth-list rejection for EIP-7702 type-4 txs; the guard is placed after the intrinsic-gas check, which can surface a misleading error for the extreme edge case of gas_limit < 21000.
crates/blockchain/mempool.rs transaction_intrinsic_gas now delegates unconditionally to VM helpers (intrinsic_gas_dimensions + intrinsic_gas_floor), eliminating the drift-prone hand-rolled computation; EIP-7623 floor correctly gated at Fork::Prague.
crates/blockchain/error.rs Adds MempoolError::EmptyAuthorizationList variant with a clear error message.
test/tests/blockchain/mempool_tests.rs Adds regression tests for EIP-7623 floor, EIP-7702 auth-list cost, and empty-auth rejection; fixes pre-existing wrong gas constants (TX_DATA_NON_ZERO_GAS → TX_DATA_NON_ZERO_GAS_EIP2028) in the two Shanghai create tests; removes the unreachable pre-Istanbul calldata test.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[validate_transaction] --> B{tx.gas_limit < intrinsic_gas?}
    B -->|yes| C[Err: TxIntrinsicGasCostAboveLimitError]
    B -->|no| D{EIP-7702 tx with\nempty auth_list?}
    D -->|yes| E[Err: EmptyAuthorizationList]
    D -->|no| F[continue validation...]

    subgraph transaction_intrinsic_gas
        G[intrinsic_gas_dimensions tx, fork, gas_limit]
        G --> H["(regular_gas, state_gas)"]
        H --> I[intrinsic = regular + state]
        I --> J{fork >= Prague?}
        J -->|yes| K[floor = intrinsic_gas_floor tx, fork]
        J -->|no| L[floor = 0]
        K --> M[return max intrinsic, floor]
        L --> M
    end

    B -. calls .-> G
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[validate_transaction] --> B{tx.gas_limit < intrinsic_gas?}
    B -->|yes| C[Err: TxIntrinsicGasCostAboveLimitError]
    B -->|no| D{EIP-7702 tx with\nempty auth_list?}
    D -->|yes| E[Err: EmptyAuthorizationList]
    D -->|no| F[continue validation...]

    subgraph transaction_intrinsic_gas
        G[intrinsic_gas_dimensions tx, fork, gas_limit]
        G --> H["(regular_gas, state_gas)"]
        H --> I[intrinsic = regular + state]
        I --> J{fork >= Prague?}
        J -->|yes| K[floor = intrinsic_gas_floor tx, fork]
        J -->|no| L[floor = 0]
        K --> M[return max intrinsic, floor]
        L --> M
    end

    B -. calls .-> G
Loading
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
crates/blockchain/blockchain.rs:2911-2918
**Empty-auth check ordering — wrong error for gas_limit < 21000**

The `EmptyAuthorizationList` guard runs after the intrinsic-gas check. For an EIP-7702 transaction that has both an empty `authorization_list` and `gas_limit < TX_GAS_COST` (21 000), the mempool returns `TxIntrinsicGasCostAboveLimitError` rather than `EmptyAuthorizationList`. The primary invalidity — the empty auth list — is structurally obvious; the intrinsic-gas error is a secondary consequence. Consider moving this check to just before (or just after the tx-type check earlier in the function) so the returned error accurately diagnoses the root cause. The transaction is rejected either way, so this only affects callers that inspect the error variant for diagnostics.

Reviews (1): Last reviewed commit: "fix(l1): align mempool intrinsic-gas and..." | Re-trigger Greptile

Comment thread crates/blockchain/blockchain.rs Outdated
Comment on lines +2911 to +2918
// EIP-7702: an empty `authorization_list` makes a type-4 tx invalid
// (LEVM rejects it in `validate_type_4_tx`). Reject at admission so it
// never enters the pool.
if let Transaction::EIP7702Transaction(eip7702) = tx
&& eip7702.authorization_list.is_empty()
{
return Err(MempoolError::EmptyAuthorizationList);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Empty-auth check ordering — wrong error for gas_limit < 21000

The EmptyAuthorizationList guard runs after the intrinsic-gas check. For an EIP-7702 transaction that has both an empty authorization_list and gas_limit < TX_GAS_COST (21 000), the mempool returns TxIntrinsicGasCostAboveLimitError rather than EmptyAuthorizationList. The primary invalidity — the empty auth list — is structurally obvious; the intrinsic-gas error is a secondary consequence. Consider moving this check to just before (or just after the tx-type check earlier in the function) so the returned error accurately diagnoses the root cause. The transaction is rejected either way, so this only affects callers that inspect the error variant for diagnostics.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/blockchain/blockchain.rs
Line: 2911-2918

Comment:
**Empty-auth check ordering — wrong error for gas_limit < 21000**

The `EmptyAuthorizationList` guard runs after the intrinsic-gas check. For an EIP-7702 transaction that has both an empty `authorization_list` and `gas_limit < TX_GAS_COST` (21 000), the mempool returns `TxIntrinsicGasCostAboveLimitError` rather than `EmptyAuthorizationList`. The primary invalidity — the empty auth list — is structurally obvious; the intrinsic-gas error is a secondary consequence. Consider moving this check to just before (or just after the tx-type check earlier in the function) so the returned error accurately diagnoses the root cause. The transaction is rejected either way, so this only affects callers that inspect the error variant for diagnostics.

How can I resolve this? If you propose a fix, please make it concise.

@ElFantasma

Copy link
Copy Markdown
Contributor Author

Thanks for the reviews. Follow-up commits 8db215ad4 and 579f453be address the actionable points; summary below.

Addressed

Empty-auth check ordering (Claude, Greptile P2) — moved the EIP-7702 type-4 structural validation ahead of the intrinsic-gas check, so an empty-auth tx now reports EmptyAuthorizationList rather than the downstream TxIntrinsicGasCostAboveLimitError, matching LEVM's validate_type_4_tx ordering. Regression test added.

Pre-Prague type-4 gate (Codex #2, Claude #3, Kimi #3) — real gap, good catch. ethrex supports pre-Prague forks (Paris/Shanghai/Cancun), and the mempool was admitting type-4 txs that LEVM rejects at inclusion with Type4TxPreFork. Added a MempoolError::Eip7702TxPreFork rejection gated on is_prague_activated, with a test.

Lossy intrinsic-gas error mapping (Claude #5) — the helper failures now map to a new IntrinsicGasError(String) that preserves the underlying VMError cause instead of collapsing to TxGasOverflowError. While there, I also corrected a pre-existing swap in two MempoolError Display messages: TxGasOverflowError and TxTipAboveFeeCapError carried each other's text, so RPC users hit the wrong message on those two rejections.

Cosmeticsfloorcalldata_floor (Kimi); updated the now-stale "non-Amsterdam path" doc comment on prague_config_and_header (Claude #4).

False positive

Pre-Istanbul calldata pricing "regression" (Codex #1, Claude #1, Kimi #2) — not a regression. ChainConfig::get_fork() floors at Fork::Paris (there are no sub-Paris fork variants, and the block-import runner rejects pre-Merge forks), so EIP-2028 (16 gas/non-zero byte) is always in effect on any reachable ethrex config — the old 68-gas/byte branch was dead code. The two constants are genuinely distinct (TX_DATA_NON_ZERO_GAS = 68, TX_DATA_NON_ZERO_GAS_EIP2028 = 16), so the updated test expectations reflect a real value change (e.g. 74556 → 58072), not a same-value rename — they align the tests with what LEVM actually charges rather than masking anything. The removed pre_istanbul test was an exact duplicate of post_istanbul once both resolve to Paris.

Not changed

EIP-7981 reference in the new comment (Kimi #1) — kept intentionally. It mirrors the terminology LEVM already uses in gas_cost.rs / utils.rs for the Amsterdam access-list-bytes-into-floor rule (floor_tokens_in_access_list). If that EIP number is inaccurate it's inaccurate at the source too and should be fixed there in a dedicated change rather than diverging this comment.

Genesis-setup verbosity in the async test (Claude) — left as-is; it follows the existing pattern in l1_tx_type_tests.rs. A shared helper would be a reasonable cleanup but is out of scope here.

All mempool tests green (32), clippy + fmt clean.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

L1 Ethereum client

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

mempool: align intrinsic-gas validation with execution (EIP-7702 auth-list + EIP-7623 floor)

2 participants