Skip to content

fix(txpool): stop applying the EIP-7623 calldata floor on admission - #199

Closed
panos-xyz wants to merge 2 commits into
mainfrom
fix/txpool-eip7623-floor
Closed

panos-xyz wants to merge 2 commits into
mainfrom
fix/txpool-eip7623-floor

Conversation

@panos-xyz

@panos-xyz panos-xyz commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Problem

Morph disables EIP-7623 during execution — CfgEnv::disable_eip7623 is set at every construction site in morph_evm::MorphEvmConfig (crates/evm/src/config.rs:39,90) — matching production morph-geth 5744b8f66, whose IntrinsicGas has no floor term at all (core/state_transition.go:152-200).

The transaction pool did not follow. reth's ensure_intrinsic_gas derives its SpecId from the Prague fork flag (transaction-pool/src/validate/eth.rs:1499-1543), and MorphChainSpec activates Prague at Viridian time so EIP-7702 works (crates/chainspec/src/spec.rs:121-126). So from Viridian onward the pool rejected any transaction whose gas limit fell below 21_000 + 10 * tokens.

Both clients execute those transactions fine and morph-geth's pool accepts them, so a reth node taking user traffic rejects transactions the network considers valid:

4 KiB non-zero calldata gas
actually needed to execute 86_536
demanded by the EIP-7623 floor 184_840

Anything in that window is refused with intrinsic gas too low. It covers data-availability style transactions and any wallet sizing gas from eth_estimateGas, which also has no floor.

Block import is unaffected — the engine path does not run pool validation — so this is an admission and propagation problem on any reth node exposing eth_sendRawTransaction, not a consensus one.

Found by the pre-migration cross-client audit (POOL-01), reproduced with a MockEthProvider probe before and after the Viridian timestamp.

Fix

Route the inner pipeline through MorphTransactionValidator::validate_inner_with_state, which mirrors reth's validate_one_with_provider — stateless checks, fetch and cache a state provider, stateful checks — and re-adjudicates exactly one verdict: a stateless IntrinsicGasTooLow is re-checked by intrinsic_gas_is_sufficient, which omits the floor term.

initial_total_gas is recomputed with the same revm helper and the same SpecId selection reth uses, so the part Morph does enforce cannot drift from upstream, and a genuinely underfunded transaction is still rejected with the same error.

This is an interim measure — see #201

morph-geth has EIP-7623 implemented on its unmerged eip7623 branch (morph-l2/go-ethereum, tip c685e19d3). That branch does not flip a flag on an existing fork: it introduces a new hardfork NextFork (nextForkTime, ordered after Jade) and enables the floor at IsNextFork in both execution and its own pool. NextForkTime is not scheduled on any network yet.

So the two clients gate the same rule at different points, and this PR only closes the interval between them:

floor active from
morph-reth pool (before this PR) Viridian (Prague-mapped, already live)
morph-geth eip7623 branch NextFork (after Jade, unscheduled)

When that fork lands, do not revert this — the pool would go back to applying the floor one hardfork early. Convert intrinsic_gas_is_sufficient to a fork gate instead, alongside the matching disable_eip7623 change in morph_evm and geth's L1-message exemption. #201 carries the checklist and is linked from the code.

The copied validate_one_with_provider skeleton is the cost of upstream having no knob here: ensure_intrinsic_gas takes only the ForkTracker, even though max_initcode_size and tx_gas_limit_cap on that same tracker already come from the EVM config. If upstream gains an equivalent switch this method collapses to one builder call; until then the doc comment tells the next reth upgrade to re-check both upstream functions.

Tests

Two regression tests on a validator whose fork tracker reports Prague:

  • viridian_pool_accepts_calldata_below_eip7623_floor — 4096 non-zero calldata bytes at exactly 86_536 gas is admitted.
  • viridian_pool_still_rejects_underfunded_intrinsic_gas — the same transaction at 86_535 gas is still rejected as IntrinsicGasTooLow.

cargo test --all, cargo fmt --all -- --check and cargo clippy --all --all-targets -- -D warnings pass.

Morph disables EIP-7623 during execution — `CfgEnv::disable_eip7623` is set at
every construction site in `morph_evm::MorphEvmConfig` — matching morph-geth,
whose `IntrinsicGas` has no floor term at all (core/state_transition.go:152-200).
The transaction pool did not follow: reth's `ensure_intrinsic_gas` derives its
`SpecId` from the Prague fork flag, and `MorphChainSpec` activates Prague at
Viridian time so EIP-7702 works, so from Viridian onward the pool rejected
transactions whose gas limit fell below the EIP-7623 floor.

Both clients execute those transactions fine, and morph-geth's pool accepts
them, so any reth node taking user transactions rejected traffic the network
considers valid. A transaction with 4 KiB of non-zero calldata needs 86_536 gas
to execute but 184_840 to clear the floor, which covers data-availability style
transactions and any wallet sizing gas from `eth_estimateGas`.

Run the inner pipeline through Morph's own `validate_inner_with_state`, which
mirrors reth's `validate_one_with_provider` and re-adjudicates exactly one
verdict: a stateless `IntrinsicGasTooLow` is re-checked without the floor term.
`initial_total_gas` is recomputed with the same revm helper and the same
`SpecId` selection reth uses, so genuinely underfunded transactions are still
rejected with the same error.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude Code Review

Claude Code Review is paused for this repository. To reconnect it, an admin of this repository's GitHub organization (or the account owner, for personal repositories) who can also manage your Claude organization's Code Review settings needs to re-link GitHub in Code Review settings. This is a one-time step.

Tip: disable this comment in your organization's Code Review settings.

@coderabbitai

coderabbitai Bot commented Sep 10, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 1fd91a43-6eb6-4d4c-88c5-0afdd37e255c

📥 Commits

Reviewing files that changed from the base of the PR and between dfae5d4 and b4a2526.

📒 Files selected for processing (1)
  • crates/txpool/src/validator.rs

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The transaction pool now re-adjudicates IntrinsicGasTooLow errors without the EIP-7623 calldata floor. It preserves fork-specific intrinsic gas checks, state provider loading, stateful validation, and adds Viridian coverage.

Changes

Intrinsic gas validation

Layer / File(s) Summary
Intrinsic gas policy
crates/txpool/src/validator.rs
The validator recomputes intrinsic gas with fork-specific SpecId rules and excludes the EIP-7623 calldata floor.
Stateful validation pipeline
crates/txpool/src/validator.rs
validate_one_with_state uses validate_inner_with_state, which performs stateless checks, loads the latest state provider, and performs stateful checks.
Intrinsic gas validation tests
crates/txpool/src/validator.rs
Prague-mode tests accept a transaction at the intrinsic gas threshold and reject one gas unit below it.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant TransactionPool
  participant MorphValidator
  participant RethValidator
  participant StateProvider
  TransactionPool->>MorphValidator: Validate transaction
  MorphValidator->>RethValidator: Run stateless checks
  RethValidator-->>MorphValidator: Return IntrinsicGasTooLow
  MorphValidator->>MorphValidator: Recompute intrinsic gas without EIP-7623 floor
  MorphValidator->>StateProvider: Fetch latest state
  StateProvider-->>MorphValidator: Return state
  MorphValidator->>RethValidator: Run stateful checks
  RethValidator-->>TransactionPool: Return validation result
Loading

Merge Risk: ⚪ Minimal · up to b4a25

The transaction pool now admits valid Morph transactions that were incorrectly rejected by the EIP-7623 calldata floor while continuing to reject genuinely underfunded transactions. The change is ready to merge.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 10 functions across 1 files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: preventing EIP-7623 calldata-floor enforcement during transaction-pool admission.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/txpool-eip7623-floor

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…k gate

morph-geth implements EIP-7623 on its unmerged `eip7623` branch by introducing
a new hardfork ordered after Jade, so the floor becomes correct behaviour once
that fork activates. Record that this carve-out must then be converted to a fork
gate rather than reverted, alongside the matching `disable_eip7623` change in
morph-evm and geth's L1-message exemption. Tracked in #201.
panos-xyz added a commit that referenced this pull request Sep 11, 2026
…tch to it

Block number and timestamp, base fee, L1 fee parameters and the EVM environment
were written and read as separate fields. A canonical update racing a
validation batch could pair the new block's state with the previous block's
environment, and the state provider was opened by block *number*, which a
same-height reorg cannot disambiguate.

`MorphValidationHead` bundles hash, number, timestamp, base fee, L1 info and the
`EvmEnv`, published atomically behind one `RwLock<Option<Arc<_>>>`. A batch pins
one head and opens its provider by hash. The inner validator's stateful checks
now run against that pinned provider too, so the account read, the token read
and the environment agree on a block — previously the inner validator used
`latest()` while the token read used the cached head number. Until a head has
been published, validation returns `Error` (retry) rather than validating
against a zero head.

Note for #199: its `validate_inner_with_state` covers the same
stateless/stateful split; the EIP-7623 carve-out slots in at the
`validate_stateless` call here when that branch is rebased.

Found by the second external review of #200.

Claude-Session: https://claude.ai/code/session_01PiUjd47Da71WG2BFkDQz9q
@panos-xyz panos-xyz closed this Sep 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant