fix: close reth 2.5.2 upgrade gaps in engine validator and intrinsic gas - #191
Conversation
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.
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Team Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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. Comment |
…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
Summary
Follow-ups to #188 (
v2.4.0→v2.5.2), found while auditing that diff against the upstream reth/revm sources. Stacked onchore/upgrade-reth-2.5.2— please merge #188 first, or retarget this tomainafter 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
EngineValidatorwith an empty default body:MorphTreeEngineValidatoris a wrapper that must delegate every method toinner: 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
Nonevalidate_initial_tx_gasin revm 42 takes a neweip2780: Option<Eip2780TxInfo>. #188 passesNoneat both call sites; upstream derives it:That flag is spec-derived:
CfgEnv::with_spec_and_mainnet_gas_params(whichMorphEvmConfiguses for bothevm_envandnext_evm_env) setsenable_amsterdam_eip2780 = self.enable_amsterdam_eip2780 || is_amsterdam. So a hardcodedNonewould 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_gasnow also asserts the EIP-2780 flag is off for every Morph hardfork, built through the samewith_spec_and_mainnet_gas_paramspath production uses. A positive control atSpecId::AMSTERDAMkeeps the guard from silently becoming vacuous if the flag ever stops being spec-derived.test_eip7702_refund_stays_regular_for_morph_specsregains its intent. revm 42 removedtx_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 assertingtx_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 hardcoded12500. 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-dblost its last use when theChangesetCacheparameter was replaced byOverlayManager; removed from the workspace and frommorph-node.5. Comments only
Recorded why
evm_callhands a fresh journal checkpoint toHandler::execution(executioncommits it, or unwinds to it when the runtime gas phase OOGs) and why theNonearm 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
Not in this PR
sload_morph/sstore_morphincrates/revm/src/evm.rswere not re-synced against revm 42 either: upstream changed the SSTORE cold-load skip threshold fromcold_storage_additional_cost()(2000) tocold_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 requiresremaining > 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_valuerepair those overrides perform can never fire, and nativeEthInstructionscan be restored. That belongs in its own PR against #187, gated by the same48_128golden test.