Skip to content

fix: close reth 2.5.2 upgrade gaps in engine validator and intrinsic gas - #191

Merged
panos-xyz merged 2 commits into
chore/upgrade-reth-2.5.2from
fix/reth-2.5.2-upgrade-followups
Sep 7, 2026
Merged

fix: close reth 2.5.2 upgrade gaps in engine validator and intrinsic gas#191
panos-xyz merged 2 commits into
chore/upgrade-reth-2.5.2from
fix/reth-2.5.2-upgrade-followups

Conversation

@panos-xyz

Copy link
Copy Markdown
Contributor

Summary

Follow-ups to #188 (v2.4.0v2.5.2), found while auditing that diff against the upstream reth/revm sources. Stacked on chore/upgrade-reth-2.5.2 — please merge #188 first, or retarget this to main after it lands.

No consensus behaviour changes: every fix here is either currently unreachable for Morph (and guarded so it stays that way) or affects only test assertions and dependency hygiene.

1. Forward EngineValidator::on_canonical_head_changed (the actual bug)

reth 2.5.0 added this method to EngineValidator with an empty default body:

// reth v2.5.2 crates/engine/tree/src/tree/payload_validator.rs:1760
fn on_canonical_head_changed(&self, _hash: B256, _state: &EngineApiTreeState<N>) {}

MorphTreeEngineValidator is a wrapper that must delegate every method to inner: BasicEngineValidator. Because this one has a default body, the missing forward compiled cleanly and silently replaced upstream's implementation — txpool prewarming, opt-in via --engine.txpool-prewarming — with the no-op default. Anyone passing that flag would have got nothing.

Fixed by forwarding it, and by documenting the forwarding contract on the type: a trait method with a default body is precisely the case the compiler cannot catch, so it needs a review step on every reth upgrade.

2. Derive the EIP-2780 intrinsic-gas info instead of hardcoding None

validate_initial_tx_gas in revm 42 takes a new eip2780: Option<Eip2780TxInfo>. #188 passes None at both call sites; upstream derives it:

// revm-handler-42.0.1/src/handler.rs:394
let is_amsterdam_eip2780_enabled = ctx.cfg().is_amsterdam_eip2780_enabled();
let eip2780 = is_amsterdam_eip2780_enabled.then(|| { ... });

That flag is spec-derived: CfgEnv::with_spec_and_mainnet_gas_params (which MorphEvmConfig uses for both evm_env and next_evm_env) sets enable_amsterdam_eip2780 = self.enable_amsterdam_eip2780 || is_amsterdam. So a hardcoded None would silently diverge from upstream intrinsic gas the moment a Morph hardfork mapped to AMSTERDAM. Behaviour is unchanged today — every Morph hardfork maps below AMSTERDAM.

3. Test guards

  • test_morph_hardforks_do_not_enable_amsterdam_state_gas now also asserts the EIP-2780 flag is off for every Morph hardfork, built through the same with_spec_and_mainnet_gas_params path production uses. A positive control at SpecId::AMSTERDAM keeps the guard from silently becoming vacuous if the flag ever stops being spec-derived.
  • test_eip7702_refund_stays_regular_for_morph_specs regains its intent. revm 42 removed tx_eip7702_state_refund / tx_eip7702_auth_refund, so the rewrite in chore: upgrade reth dependencies to v2.5.2 #188 was forced — but it left the test asserting tx_eip7702_state_gas_bytecode() == 0, an exact duplicate of the assertion in the test above it, while still carrying a message about refunds, plus a hardcoded 12500. It now asserts the surviving equivalent invariant (the whole per-auth refund stays in the regular refund) against revm's own constants: PER_EMPTY_ACCOUNT_COST - PER_AUTH_BASE_COST.

4. Dependency hygiene

reth-trie-db lost its last use when the ChangesetCache parameter was replaced by OverlayManager; removed from the workspace and from morph-node.

5. Comments only

Recorded why evm_call hands a fresh journal checkpoint to Handler::execution (execution commits it, or unwinds to it when the runtime gas phase OOGs) and why the None arm is unreachable for Morph (EIP-2780 / AMSTERDAM only) — otherwise a reader cannot tell whether an OOG inside the token-fee transfer is handled correctly.

Testing

cargo nextest run --workspace                            644 passed, 0 failed
cargo nextest run -p morph-node -E 'binary(it)'          101 passed, 0 failed
  └─ morph_tx_v0_token_fee_transfer_to_fee_token_contract_gas_regression  PASS (48_128 golden)
cargo clippy --all --all-targets -- -D warnings          clean
cargo fmt --all -- --check                               clean

Not in this PR

sload_morph / sstore_morph in crates/revm/src/evm.rs were not re-synced against revm 42 either: upstream changed the SSTORE cold-load skip threshold from cold_storage_additional_cost() (2000) to cold_storage_cost() (2100), and morph's hand-written copy still uses the old one. It is unreachable — for Istanbul and later the reentrancy sentry already requires remaining > call_stipend (2300) before that branch — but it is the second measured instance of drift in that copied code.

Rather than patch the threshold, #188 makes the whole workaround deletable: revm 42 contains bluealloy/revm#3746, so the original_value repair those overrides perform can never fire, and native EthInstructions can be restored. That belongs in its own PR against #187, gated by the same 48_128 golden test.

Follow-ups to the v2.4.0 -> v2.5.2 upgrade, found while auditing that diff
against the upstream sources.

Forward `EngineValidator::on_canonical_head_changed`. reth 2.5.0 added this
method with an empty default body, so `MorphTreeEngineValidator` — a wrapper
that must delegate every method to `inner` — silently replaced
`BasicEngineValidator`'s implementation (txpool prewarming, opt-in via
`--engine.txpool-prewarming`) with the no-op default instead of failing to
compile. Document that forwarding contract on the type so the next upgrade
re-checks it: a method with a default body is exactly the case the compiler
cannot catch.

Derive the EIP-2780 intrinsic-gas info in `validate_initial_tx_gas` from
`Cfg::is_amsterdam_eip2780_enabled()` the way revm's own handler does, instead
of hardcoding the new `None` argument. `CfgEnv::with_spec_and_mainnet_gas_params`
(used by `MorphEvmConfig`) derives that flag from the spec, so a hardcoded
`None` would silently diverge from upstream intrinsic gas if a Morph hardfork
ever mapped to AMSTERDAM. Behaviour is unchanged today: every Morph hardfork
maps below AMSTERDAM.

Extend `test_morph_hardforks_do_not_enable_amsterdam_state_gas` to assert the
EIP-2780 flag stays off for every Morph hardfork, with a positive control at
AMSTERDAM so the guard cannot become vacuous if the flag stops being
spec-derived.

Restore the intent of `test_eip7702_refund_stays_regular_for_morph_specs`.
revm 42 removed `tx_eip7702_state_refund` / `tx_eip7702_auth_refund`, so the
test had been reduced to a duplicate of the state-gas assertion above it while
keeping a message about refunds, plus a hardcoded 12500. It now asserts the
surviving equivalent — the full per-auth refund equals
`PER_EMPTY_ACCOUNT_COST - PER_AUTH_BASE_COST` — against revm's constants.

Drop `reth-trie-db`: it lost its last use when the `ChangesetCache` parameter
was replaced by `OverlayManager`.

Also record why `evm_call` hands a fresh journal checkpoint to
`Handler::execution` and why its `None` arm is unreachable for Morph.
@coderabbitai

coderabbitai Bot commented Sep 7, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: bb92ed4a-e49e-448d-aad8-ca16381f4ebc

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Warning

Billing warning: we have not been able to collect payment for this subscription for more than 72 hours. Please update the payment method or pay any pending invoices in Billing to avoid service interruption.


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.

…e instructions (#194)

`sload_morph` and `sstore_morph` existed to undo an `original_value` clobber in
revm, not to express any Morph rule. `sstore_morph` in particular re-implemented
the whole upstream SSTORE lifecycle — static-call check, reentrancy sentry,
Berlin cold loading, dynamic gas and refund accounting — to insert one repair in
the middle of it.

Why they were needed: token fee deduction leaves the caller's fee-token slot
with the DB-committed `original_value`, the fee-deducted `present_value` and
`is_cold = true`. That is the same triple morph-geth has, where the EIP-2200
committed value (`GetCommittedState`) and the EIP-2929 access list are
independent. revm <= 41 conflated them in
`EvmStorageSlot::mark_warm_with_transaction_id`, re-baselining
`original_value = present_value` whenever a slot was cold — including a slot
marked cold within the same transaction. EIP-2200 then saw a clean slot and
charged `SSTORE_RESET` (2900) instead of the dirty-slot 100, diverging from
morph-geth by 2800 gas per write.

Why they are no longer needed: bluealloy/revm#3746 (released in revm-state
42.0.0, still present in 43) keys that re-baseline on the transaction boundary
instead of on the cold flag, so the committed value survives an explicit
`mark_cold()`. The native instructions now produce geth-equivalent gas on their
own. Verified as a controlled comparison against the golden e2e test:

  revm 41 + overrides   48_128  (previous state)
  revm 41 - overrides   50_928  (+2800: the divergence these overrides hid)
  revm 42 - overrides   48_128  (this commit)

The middle row also confirms
`morph_tx_v0_token_fee_transfer_to_fee_token_contract_gas_regression` is a real
tripwire rather than a permanently green assertion, so it is left untouched as
the gate for this change.

Removing the copies also retires three drifts that had already accumulated in
them:

- the EIP-8037 branch of upstream `sstore_default_gas_accounting` (`state_gas!`
  plus `refill_reservoir`) was never mirrored; dormant only because every Morph
  hardfork maps below AMSTERDAM.
- the SSTORE cold-load skip threshold stayed at revm 41's
  `cold_storage_additional_cost()` (2000) after upstream 42 moved it to
  `cold_storage_cost()` (2100); unreachable because the Istanbul reentrancy
  sentry already requires `remaining > call_stipend` (2300).
- the `original_value` repair ran for every cold slot, including slots of an
  account created within the same transaction, where revm reports `ZERO` while
  the DB may still hold residual storage.

`blockhash_morph` (0x40) and the disabled SELFDESTRUCT / BLOBHASH / BLOBBASEFEE
opcodes stay: those are genuine Morph semantics, unrelated to revm's version.

Closes #187
@panos-xyz
panos-xyz merged commit af7f036 into chore/upgrade-reth-2.5.2 Sep 7, 2026
2 of 3 checks passed
@panos-xyz
panos-xyz deleted the fix/reth-2.5.2-upgrade-followups branch September 7, 2026 07:33
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