Skip to content

fix(l1): include blob gas in mempool balance check for EIP-4844 transactions#6509

Open
ilitteri wants to merge 2 commits intomainfrom
fix/mempool-blob-gas-balance-check
Open

fix(l1): include blob gas in mempool balance check for EIP-4844 transactions#6509
ilitteri wants to merge 2 commits intomainfrom
fix/mempool-blob-gas-balance-check

Conversation

@ilitteri
Copy link
Copy Markdown
Collaborator

Motivation

Transaction::cost_without_base_fee() at crates/common/types/transaction.rs:444 is consumed by the mempool balance check in crates/blockchain/blockchain.rs:2488-2493. For EIP-4844 transactions it was returning max_fee_per_gas * gas_limit + value and omitting the blob-gas component blob_gas_used * max_fee_per_blob_gas. This let the mempool admit blob transactions whose sender could not actually afford them; they propagated over the network and failed at block inclusion, wasting pool capacity and peer bandwidth.

Every peer Ethereum execution client (geth tx.Cost(), reth EthPooledTransaction::cost, nethermind BalanceTooLowFilter, erigon requiredBalance, besu upfrontCost) includes the blob-gas term in this check. ethrex did not.

Description

  • Extend Transaction::cost_without_base_fee() so the EIP-4844 branch adds blob_versioned_hashes.len() * GAS_PER_BLOB * max_fee_per_blob_gas using saturating arithmetic, matching the function's existing style.
  • Add regression test test_cost_without_base_fee_eip4844_includes_blob_gas in crates/common/types/transaction.rs. The test fails against the pre-fix code with a delta of exactly GAS_PER_BLOB * max_fee_per_blob_gas.
  • Document MIN_GAS_TIP in crates/common/types/constants.rs as an RPC gas-price estimator floor only, not a mempool admission gate (it is consumed exclusively by eth_gasPrice / eth_maxPriorityFeePerGas).
  • Document PendingTxFilter.min_tip in crates/blockchain/mempool.rs as a payload-builder query filter, not an admission gate.

Previously-admitted EIP-4844 transactions with insufficient balance for blob gas will now be rejected at ingress. This is a correctness fix, not a policy change. A dedicated admission-time minimum-tip floor is tracked separately and is out of scope here.

…actions

Transaction::cost_without_base_fee() was omitting the blob-gas component
from the upfront cost it returns, so the mempool balance check at
crates/blockchain/blockchain.rs:2488-2493 accepted EIP-4844 transactions
whose sender could not afford the blob fees. The transactions then failed
at block inclusion, wasting pool capacity and peer bandwidth.

Every peer client (geth, reth, nethermind, erigon, besu) includes
blob_gas * max_fee_per_blob_gas in the equivalent check; ethrex did not.

Changes:
- Extend cost_without_base_fee() so the EIP-4844 branch adds
  blob_versioned_hashes.len() * GAS_PER_BLOB * max_fee_per_blob_gas
  using saturating arithmetic, matching the function's existing style.
- Add regression test test_cost_without_base_fee_eip4844_includes_blob_gas
  in crates/common/types/transaction.rs.
- Document MIN_GAS_TIP in crates/common/types/constants.rs as an
  RPC gas-price estimator floor, not a mempool admission gate.
- Document PendingTxFilter.min_tip in crates/blockchain/mempool.rs as
  a payload-builder query filter, not an admission gate.
Copilot AI review requested due to automatic review settings April 20, 2026 21:14
@ilitteri ilitteri requested review from a team, ManuelBilbao and avilagaston9 as code owners April 20, 2026 21:14
@github-actions github-actions Bot added the L1 Ethereum client label Apr 20, 2026
@github-actions
Copy link
Copy Markdown

🤖 Kimi Code Review

This PR correctly fixes a balance-check bug for EIP-4844 blob transactions and improves architectural documentation. Specific feedback:

crates/common/types/transaction.rs

  1. Lines 462-471: The blob cost calculation correctly implements the balance-sufficiency check per EIP-4844. Using saturating_mul/saturating_add prevents U256 overflow, and the GAS_PER_BLOB * blob_count formula matches the consensus gas accounting.

  2. Line 465: The cast as u64 on blob_versioned_hashes.len() is safe in practice (protocol limits blobs to 6 per tx), but consider adding a defensive comment or using try_into().unwrap() to panic explicitly if the invariant is violated, since silent truncation would cause incorrect cost calculation.

crates/common/types/constants.rs

  1. Lines 12-19: The documentation clarifying that MIN_GAS_TIP is for RPC estimation only—not mempool admission—is critical for avoiding consensus splits. Consider making the TODO (line 19) more specific about the configurability mechanism (e.g., "expose via Config struct").

crates/blockchain/mempool.rs

  1. Lines 501-507: The doc comment on PendingTxFilter correctly distinguishes between admission gates and payload-building filters. This prevents future developers from mistakenly assuming min_tip rejects transactions at the mempool border.

Test Coverage

  1. Lines 3732-3765: The regression test in test_cost_without_base_fee_eip4844_includes_blob_gas correctly verifies the fix. The test structure properly isolates the calculation and verifies against the expected formula.

Security/Consensus Assessment

  • Correctness: The fix ensures that blob transactions undergo the same upfront balance validation as peer clients (geth, reth, etc.), preventing mempool DoS via underfunded blob txs.
  • No breaking changes: This only tightens validation; previously under-estimated transactions will now be correctly rejected.

Recommendation: Approved with minor suggestion—consider adding a debug_assert! or comment at line 465 noting the protocol-level blob limit (6) that makes the usizeu64 cast safe.


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

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 20, 2026

Greptile Summary

This PR fixes a correctness bug where Transaction::cost_without_base_fee() omitted the blob-gas term (blob_count × GAS_PER_BLOB × max_fee_per_blob_gas) for EIP-4844 transactions, allowing the mempool to admit blob txs whose senders lacked sufficient balance — matching behaviour now aligns with geth, reth, nethermind, erigon, and besu. The remaining two files are documentation-only clarifications for MIN_GAS_TIP and PendingTxFilter.min_tip.

Confidence Score: 5/5

Safe to merge — the fix is narrowly scoped, correct, and well-tested; only finding is a minor style note on a as u64 cast.

All remaining findings are P2 style suggestions with no impact on correctness. The core fix is arithmetically correct, uses saturating arithmetic consistently, and is validated by a regression test that would have failed against the pre-fix code.

No files require special attention.

Important Files Changed

Filename Overview
crates/common/types/transaction.rs Adds blob gas cost (GAS_PER_BLOB * blob_count * max_fee_per_blob_gas) to cost_without_base_fee() for EIP-4844 transactions; adds regression test. Core fix is correct and matches peer client behaviour.
crates/blockchain/mempool.rs Documentation-only change: clarifies that PendingTxFilter.min_tip is a payload-builder query filter, not a mempool admission gate.
crates/common/types/constants.rs Documentation-only change: clarifies scope of MIN_GAS_TIP as an RPC estimator floor only, not a mempool admission gate.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Mempool: validate_transaction] --> B[cost_without_base_fee]
    B --> C{tx_type?}
    C -->|Legacy / EIP2930| D[gas_price × gas_limit + value]
    C -->|EIP1559 / EIP7702 / FeeToken| E[max_fee_per_gas × gas_limit + value]
    C -->|EIP4844| F[max_fee_per_gas × gas_limit + value]
    F --> G["+ GAS_PER_BLOB × blob_count × max_fee_per_blob_gas NEW"]
    G --> H[total cost]
    D --> H
    E --> H
    H --> I{sender_balance >= total cost?}
    I -->|Yes| J[Admit to pool]
    I -->|No| K[Reject: InsufficientFunds]
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: crates/common/types/transaction.rs
Line: 465

Comment:
**`usize` to `u64` cast could truncate on unusual 32-bit targets**

`tx.blob_versioned_hashes.len() as u64` is safe on all 64-bit platforms, but on 32-bit targets `usize` is already 32 bits so the cast is fine there too. However, using `as u64` is a silent truncating cast — the idiomatic, panic-free alternative is relying on the fact that `U256::from` accepts `usize` directly. Given the protocol limit is tiny (≤6 blobs) this is very low risk, but worth noting for consistency with the saturating style used throughout the function.

```suggestion
                .saturating_mul(U256::from(tx.blob_versioned_hashes.len()));
```

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

Reviews (1): Last reviewed commit: "fix(l1): include blob gas in mempool bal..." | Re-trigger Greptile

// includes this in the balance-sufficiency check.
if let Transaction::EIP4844Transaction(tx) = self {
let blob_gas = U256::from(crate::constants::GAS_PER_BLOB)
.saturating_mul(U256::from(tx.blob_versioned_hashes.len() as u64));
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 usize to u64 cast could truncate on unusual 32-bit targets

tx.blob_versioned_hashes.len() as u64 is safe on all 64-bit platforms, but on 32-bit targets usize is already 32 bits so the cast is fine there too. However, using as u64 is a silent truncating cast — the idiomatic, panic-free alternative is relying on the fact that U256::from accepts usize directly. Given the protocol limit is tiny (≤6 blobs) this is very low risk, but worth noting for consistency with the saturating style used throughout the function.

Suggested change
.saturating_mul(U256::from(tx.blob_versioned_hashes.len() as u64));
.saturating_mul(U256::from(tx.blob_versioned_hashes.len()));
Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/common/types/transaction.rs
Line: 465

Comment:
**`usize` to `u64` cast could truncate on unusual 32-bit targets**

`tx.blob_versioned_hashes.len() as u64` is safe on all 64-bit platforms, but on 32-bit targets `usize` is already 32 bits so the cast is fine there too. However, using `as u64` is a silent truncating cast — the idiomatic, panic-free alternative is relying on the fact that `U256::from` accepts `usize` directly. Given the protocol limit is tiny (≤6 blobs) this is very low risk, but worth noting for consistency with the saturating style used throughout the function.

```suggestion
                .saturating_mul(U256::from(tx.blob_versioned_hashes.len()));
```

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

@github-actions
Copy link
Copy Markdown

🤖 Codex Code Review

  1. crates/common/types/transaction.rs now computes the EIP-4844 blob upfront-cost term with saturating arithmetic, which can make mempool admission disagree with execution for overflow cases. In crates/common/types/transaction.rs:455 and crates/common/types/transaction.rs:464, saturating_mul/saturating_add clamp the cost to U256::MAX; then crates/blockchain/blockchain.rs:2488 accepts the tx if the sender balance is also U256::MAX. The execution path does not behave that way: crates/vm/levm/src/hooks/default_hook.rs:594 uses checked arithmetic and rejects overflow. That leaves a consensus-adjacent validity mismatch between mempool and execution. Suggestion: switch cost_without_base_fee() to checked math for both the gas term and the new blob term, and return None on overflow so mempool admission keeps surfacing InvalidTxGasvalues.

Aside from that, the behavioral change itself is correct: including blob_gas * max_fee_per_blob_gas in the EIP-4844 upfront balance check matches the execution logic and peer clients, and the doc-only clarifications in crates/blockchain/mempool.rs:501 and crates/common/types/constants.rs:12 improve intent.

I couldn’t run the targeted Rust tests here because cargo/rustup failed trying to write under /home/runner/.rustup/tmp on a read-only filesystem.


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

@github-actions
Copy link
Copy Markdown

Lines of code report

Total lines added: 33
Total lines removed: 0
Total lines changed: 33

Detailed view
+-------------------------------------------+-------+------+
| File                                      | Lines | Diff |
+-------------------------------------------+-------+------+
| ethrex/crates/common/types/transaction.rs | 3387  | +33  |
+-------------------------------------------+-------+------+

@github-actions
Copy link
Copy Markdown

🤖 Claude Code Review

Now I have all the context I need to write a thorough review.


Review: fix(l1): include blob gas in mempool balance check for EIP-4844 transactions

Summary

This is a well-targeted correctness fix. The bug is real — EIP-4844 blob transactions were admitted to the mempool without verifying the sender can afford the blob gas cost, wasting pool capacity and peer bandwidth. The diff is narrow, the arithmetic is consistent with the function's existing style, and the behavior matches all major peer clients.


Correctness

Core formula (transaction.rs:464–467): correct per EIP-4844 §3.

upfront_cost = max_fee_per_gas * gas_limit + value + GAS_PER_BLOB * blob_count * max_fee_per_blob_gas

Using max_fee_per_blob_gas (the sender's stated maximum, not the actual blob base fee) is intentional and correct — it mirrors how max_fee_per_gas is used in the EIP-1559 component: both are worst-case upfront reserves.

Saturating arithmetic is applied consistently throughout, matching the function's existing style.


Minor Issues

len() as u64 cast (transaction.rs:465):

U256::from(tx.blob_versioned_hashes.len() as u64)

GAS_PER_BLOB is u32 and len() returns usize. The as u64 cast is safe on all platforms (both 32-bit and 64-bit usize widen without truncation into u64). Since the max blob count is 9 (Electra), there's no practical risk. Still, usize as u64 may draw questions from reviewers on 32-bit contexts. A minor alternative would be:

U256::from(tx.blob_versioned_hashes.len())
// or
u64::try_from(tx.blob_versioned_hashes.len()).unwrap_or(0)

The first form works if primitive_types::U256 has impl From<usize> available in-scope (it does via primitive-types). Not a blocker.

Test covers only blob_count = 1 (transaction.rs:3744): The single-blob test is a sufficient regression guard. Consider adding a second parametrized assertion for the maximum blob count per transaction (6 pre-Electra, 9 post-Electra from MAX_BLOB_COUNT_ELECTRA = 9 in blobs_bundle.rs:275) to ensure the linear scaling is exercised. Not required, but would strengthen the safety net at low cost.

Internal roadmap reference in a public constant's doc comment (constants.rs):

/// A dedicated admission-time min-tip floor is tracked in the umbrella
/// `mempool-hardening-roadmap` change (Tier 1, PR T1b).

This is an internal tracking artefact (tier labels, internal PR names) embedded in a public-facing doc comment. It will rot and confuse external contributors. Consider replacing with a link to a GitHub issue (there's already a TODO referencing issue #680 two lines down) or simply omitting the tier/PR-name detail.


Positive

  • The early-return structure (if let … { return Some(…) }) is clean and does not require restructuring the existing match on tx_type().
  • No changes were needed at the call site in blockchain.rs:2488–2493 because the fix is correctly placed upstream in the shared helper — good layering.
  • The test uses ..Default::default() to isolate only the relevant fields, which makes the intent easy to read.
  • Documentation additions to PendingTxFilter and MIN_GAS_TIP are accurate and address a real source of confusion (admission gate vs. query filter vs. RPC estimator floor).

Verdict

Approve with minor suggestions. The internal roadmap string in the constants.rs doc comment is the only item worth addressing before merge; the cast and single-blob-test observations are advisory.


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

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes mempool balance-sufficiency accounting for EIP-4844 (blob) transactions by including the blob-gas upfront cost in Transaction::cost_without_base_fee(), preventing underfunded blob txs from being admitted and later failing at inclusion.

Changes:

  • Update Transaction::cost_without_base_fee() to add blob_gas_used * max_fee_per_blob_gas for EIP-4844 transactions (using saturating arithmetic).
  • Add a regression test ensuring the blob-gas component is included in the computed cost.
  • Clarify documentation around MIN_GAS_TIP and PendingTxFilter::min_tip to distinguish RPC estimation / payload-building filters from mempool admission rules.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
crates/common/types/transaction.rs Add blob-gas upfront cost for EIP-4844 to cost_without_base_fee() and add a regression test.
crates/common/types/constants.rs Document the scope of MIN_GAS_TIP as RPC-estimator-only (not mempool admission).
crates/blockchain/mempool.rs Document PendingTxFilter / min_tip as query-time payload-builder filters rather than admission gates.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/common/types/constants.rs Outdated
Comment on lines +17 to +18
/// A dedicated admission-time min-tip floor is tracked in the umbrella
/// `mempool-hardening-roadmap` change (Tier 1, PR T1b).
pub struct PendingTxFilter {
/// Minimum effective priority fee for a transaction to be surfaced to
/// the payload builder. This is a block-building filter, not an
/// admission check — see `crates/common/types/constants.rs::MIN_GAS_TIP`.
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.

3 participants