refactor(revm): drop custom SLOAD/SSTORE overrides in favour of native instructions - #194
Conversation
…e instructions `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
|
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 |
…gas (#191) * fix: close reth 2.5.2 upgrade gaps in engine validator and intrinsic gas 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. * refactor(revm): drop custom SLOAD/SSTORE overrides in favour of native 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
Deletes
sload_morph/sstore_morphfromcrates/revm/src/evm.rsand restores the nativeEthInstructionsfor SLOAD (0x54) and SSTORE (0x55).-171 / +14, one file.Closes #187 — but not with the mechanism that issue proposed. See my analysis there: the handler-level
transaction_idsync described in the issue is a no-op on revm 41 (the ids already match; the trigger is theis_coldflag), and implementing it as written reintroduced the divergence at exactly +2800 gas. The real precondition was a revm bump, which #188 delivers.Third in a stack — merge order matters, and getting it wrong is a chain split:
Why the overrides existed
Token fee deduction leaves the caller's fee-token slot in a specific state, which is the same state morph-geth has:
original_valueGetCommittedState()present_valueGetState()is_coldtrueaccessListIn go-ethereum the EIP-2200 committed value and the EIP-2929 access list are deliberately independent. revm <= 41 conflated them in
EvmStorageSlot::mark_warm_with_transaction_id:Charging the 2100 cold cost required setting
is_cold, and settingis_coldforced the re-baseline. EIP-2200 then saw a clean slot and chargedSSTORE_RESET(2900) instead of the dirty-slot 100 — 2800 gas per write against morph-geth. The overrides existed purely to undo that.Why they are no longer needed
bluealloy/revm#3746 (released in revm-state 42.0.0, still present in 43.0.0) keys the re-baseline on the transaction boundary instead of on the cold flag, while leaving the returned
is_cold— and therefore the cold gas charge — untouched. The committed value now survives an explicitmark_cold(), so native SLOAD/SSTORE produce geth-equivalent gas on their own.Verification
Controlled comparison against the golden e2e test, one variable between the rows:
morph_tx_v0_token_fee_transfer_to_fee_token_contract_gas_regressionThe middle row matters as much as the last: it proves that test is a real tripwire and not a permanently green assertion. It is therefore left untouched as the gate for this change.
Full local run on the pushed commit:
Note CI's
Build/Clippy/Run Tests/E2E Testsworkflows are gated onpull_request: branches: [main], so they will not run while this PR targets a non-mainbase. The gates above were run locally oncd881df; they need a real CI run once the stack is retargeted.Drifts this retires
The copies had already accumulated three divergences from upstream — the concrete evidence that hand-maintaining SSTORE was the wrong trade:
sstore_default_gas_accountingchargesstate_gas!and callsrefill_reservoir;sstore_morphhad neither. Dormant only because every Morph hardfork maps belowSpecId::AMSTERDAM(asserted bytest_morph_hardforks_do_not_enable_amsterdam_state_gas).cold_storage_additional_cost()(2000) tocold_storage_cost()(2100);sstore_morphstill used the old one. Unreachable — the Istanbul reentrancy sentry already requiresremaining > call_stipend(2300) before that branch.original_valuerestore ran for every cold slot, including slots of an account created within the same transaction, where revm reportsZEROwhile the DB may hold residual storage. Practically unreachable (SELFDESTRUCT is disabled in Morph), but wrong by construction.Kept
blockhash_morph(0x40) and the disabledSELFDESTRUCT/BLOBHASH/BLOBBASEFEEopcodes — genuine Morph semantics, unrelated to revm's version. A comment at the registration site now records why 0x54/0x55 are deliberately not overridden, so the workaround does not get reintroduced.