Skip to content

feat(revm): upgrade to upstream v107 (revm 38.0.0) + op-geth parity fixes for Mantle Elysium - #26

Merged
asdv23 merged 234 commits into
mainfrom
mantle-elysium
Aug 4, 2026
Merged

asdv23 merged 234 commits into
mainfrom
mantle-elysium

Conversation

@asdv23

@asdv23 asdv23 commented May 11, 2026

Copy link
Copy Markdown

Merge upstream bluealloy/revm from v98 (revm 31.0.0) to v107 (revm 38.0.0) — spanning 7 major versions — and land the follow-up op-geth parity fixes for Mantle Elysium. Upgrading the bottom-most dependency in preparation for op-reth v2.2.0 integration.

Scope: 230 commits, 357 files, +22468 / −6698 against main (56aee9a6).


Part 1 — Upstream merge (3e3ea167)

Key upstream changes

  • EIP-8037: Reservoir gas model — GasTracker refactored with reservoir(), record_state_cost(), record_regular_cost()
  • EIP-7825: Tx gas limit cap (16M) — disabled for Mantle, aligned with op-geth !IsOptimism() behavior
  • EIP-7954: Increased max contract size (24→32 KiB)
  • EIP-7928 (BAL): New crates/state/src/bal/ module (~850 LOC) plus examples/bal_example
  • Handler, Context, Database, Interpreter API refactors across all crates

Mantle conflict resolutions (8 files, manually merged)

File Resolution
op-revm/handler.rs (13 conflicts) Preserved Arsia fee model with is_enabled_in(OpSpecId::ARSIA) guard for historical block compatibility; adopted upstream effective_used with reservoir deduction; preserved BVM_ETH catch_error audit fix (Issue #5) adapted to the new Halt { gas: ResultGas, logs } structure
op-revm/l1block.rs (4 conflicts) Preserved GAS_ORACLE_CONTRACT + TOKEN_RATIO_SLOT loading; added reservoir gas to operator_fee_charge; adopted upstream get_tokens_in_calldata_istanbul rename
context/cfg.rs Preserved tx_gas_limit_cap = u64::MAX (EIP-7825 disabled for Mantle)
interpreter/gas.rs Added Gas::set_limit() wrapper delegating to GasTracker::set_limit()
database/alloydb.rs Migrated Audit fix #25 (InvalidBytecode, Internal variants) to upstream AlloyDBError enum
block_traces/main.rs Preserved Mantle L2 replay tooling (build_op())
35 files (CHANGELOG/Cargo) Accepted upstream (zero Mantle changes)

Historical block compatibility

All spec-gated conditions (is_enabled_in(OpSpecId::ARSIA)) are preserved to ensure:

  • Pre-Arsia block replay produces correct state roots
  • eth_call / eth_estimateGas at historical blocks returns correct results
  • No accidental hard fork for existing Mantle nodes

Part 2 — Mantle fixes on top of the merge

Landed after the upstream merge, mostly op-geth behavioural parity for BVM_ETH deposits. Net effect on crates/op-revm: 17 files, +1971 / −688.

Commit Change
fcae1dab Address review findings from the v107 merge (gas.rs, handler.rs, l1block.rs)
232bb55b cargo fmt + cargo clippy --fix
e637f61e Guard token_ratio against zero; migrate deprecated gas APIs; regenerate BLS12-381 goldens
2b42441d Remove redundant L1BlockInfo refetch in deduct-caller (+379 LOC of tests)
5d5ac498 crates/state: re-baseline SSTORE original_value only across tx boundaries. mark_warm_with_transaction_id used to reset original_value = present whenever the slot was cold — but "cold" also covers a slot flagged cold within the same transaction (access-list reset / reverted StorageWarmed), which wrongly re-baselined the EIP-2200 original. Now restricted to genuine cross-transaction access (transaction_id mismatch).
e85c5821 (#28) Reset BVM_ETH to cold after deposit mint to match op-geth; move the logic out of handler.rs into transaction/bvm_eth.rs
e8f14818 (#32) Persist BVM_ETH Mint/Transfer logs on failed deposits to match op-geth; includes two real mainnet replay fixtures (blocks 286432, 286456)
1ac02c34 (#33) Treat zero eth_value / eth_tx_value as None in deposit (op-geth parity)
11b77ca1 (#36) op-geth parity for deposit pre-call gates and the exact Isthmus precompile set

Test plan

  • cargo test -p op-revm125 passed, 0 failed (includes the BVM_ETH deposit replay fixtures and the Isthmus precompile set)
  • cargo test --workspace --exclude revm-ee-tests398 passed, 0 failed across 51 test binaries

rakita and others added 30 commits November 4, 2025 14:10
* fix: use access list to decide if slot is cold

* add test
…uealloy#3154)

* fix(op): Ensure L1Block account is always loaded

* dont panic on State::storage fn

* add test for pre regolit tx
* feat: process precompile logs to inspector

* ci nits

* no_std

* std import

* add log and log_full

* no_std

* doc

* revert &Log to Log

* nits rm clones
bluealloy#3144)

* feat(precompiles): add performant PrecompileError::OtherStr variant

* Apply suggestion from @rakita

* use cow

* rm Other(String) and use Other(Cow<'static,str>)

* fn other_static

* revert api change
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* bump: v99 revm v32.0.0

* typo:
…luealloy#3159)

The feature exists in the context crate but was not exposed in revm's Cargo.toml, making it inaccessible to users.
* feat(context): add mark_cold method to JournaledAccount

Add a public `mark_cold()` method to `JournaledAccount` that delegates
to the underlying `Account::mark_cold()`. This is analogous and provides
a way to mark accounts as cold that was available in previous revm
versions.

This is used by celo-revm to provide backwards compatibility for the
transfer precompile, which does not warm addresses in its initial
implementation.

* Declare mark_cold to be unsafe

and add explanation.

Co-authored-by: rakita <rakita@users.noreply.github.com>

---------

Co-authored-by: rakita <rakita@users.noreply.github.com>
* fix(op-revm): return error when enveloped_tx is missing

* fmt

* Remove unnecessary clone
* fix: correctly handle selfdestruct cold load

* add constantinople tests in script
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* chore(test): rn tar after download

* try with latest

* print size,and rm unused files

* rm du prints
… precompile modules (bluealloy#3191)

Co-authored-by: megakabi <jakevin@megatechnology.com>
github-actions Bot and others added 13 commits April 10, 2026 02:58
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* fix: reject transactions with nonce at u64::MAX to prevent overflow

Incrementing a nonce of u64::MAX would overflow, so validate this
upfront in transaction validation.

* Apply suggestion from @rakita
* docs: add v106 migration guide entry

* chore: v106 release prep — version bumps and changelogs

* add revm version
…luealloy#3573)

When building the BAL for a selfdestructed account, storage slots were
only recorded as reads via `update_reads`. This meant subsequent
transactions consuming the BAL would not see the storage zeroing and
would fall through to the database, getting stale pre-selfdestruct
values. Record proper writes to zero instead.
* refactor: pass reservoir into first_frame_input

* fix

* fix
Adds the new `&mut InitialAndFloorGas` parameter introduced in bluealloy#3577
to the OpHandler trait impl, and passes `&mut Default::default()` at
all test call sites.
…sium upgrade

  Merge upstream revm v98 → v107, spanning 7 major versions (revm 31→38).

  Key upstream changes incorporated:
  - EIP-8037 reservoir gas model (GasTracker refactor)
  - EIP-7825 tx gas limit cap (disabled for Mantle, aligned with op-geth)
  - EIP-7954 increased max contract size
  - alloy-primitives, precompile, and handler API refactors

  Mantle-specific conflict resolutions:
  - op-revm/handler.rs: preserved Arsia fee model (operator_fee, fee recipients)
    with ARSIA spec guard for historical block compatibility
  - op-revm/l1block.rs: preserved GAS_ORACLE_CONTRACT + TOKEN_RATIO_SLOT loading
    and token_ratio calculation, added reservoir gas handling
  - catch_error: preserved BVM_ETH deposit logic with audit fix (Issue #5),
    adapted to upstream's new Halt structure (ResultGas + logs)
  - context/cfg.rs: preserved tx_gas_limit_cap = u64::MAX (EIP-7825 disabled)
  - interpreter/gas.rs: added Gas::set_limit wrapper for token_ratio scaling
  - database/alloydb.rs: migrated Audit fix (#25) to upstream AlloyDBError enum
  - block_traces example: preserved Mantle L2 replay tooling

  Base: mantle-xyz/revm main (tag v2.2.2, upstream v98)
  Target: bluealloy/revm v107 (revm 38.0.0)
  - Remove commented-out `drop(acc)` dead code in catch_error
  - Add `[MANTLE] spec_id not used` doc on operator_fee_charge_inner
  - Ensure L1BlockInfo is reloaded before ARSIA fee deduction in
    validate_against_state_and_deduct_caller
  - Normalize `[MANTLE]` annotation format in gas.rs and l1block.rs
…as APIs

  - Add .max(1) to token_ratio in validate_initial_tx_gas and
    last_frame_result, consistent with the existing guard in execution().
    A zero token_ratio would zero out initial_total_gas, giving the
    execution frame the full tx gas_limit instead of the correct amount.
  - Replace deprecated Gas::spent() with total_gas_spent() and
    ExecutionResult::gas_used() with tx_gas_used() per upstream v107.
  - Fix Bedrock revert test assertions to match upstream behavior.
  - Regenerate ee-tests snapshots for upstream ResultGas schema change
    (removed intrinsic_gas/limit fields, added state_gas_spent).
asdv23 and others added 8 commits June 4, 2026 12:41
The try_fetch(journal.db_mut()) guard in
validate_against_state_and_deduct_caller bypassed the journal and read
stale underlying-DB storage. Running after reset_l2_block() within a
gas-oracle tx, it re-pinned l2_block to the current block with the
pre-SSTORE token_ratio, so the next tx never refetched and the
L1FeeVault was over-credited at the stale ratio.

validate_initial_tx_gas always runs first (validate() before
pre_execution()) and already handles L1BlockInfo refresh via try_fetch
and reset_l2_block, so the guard was redundant. Add regression tests
covering intra-block token_ratio updates.
EvmStorageSlot::mark_warm_with_transaction_id reset original_value=present
whenever the slot was cold, but 'cold' also covers a slot flagged cold within
the SAME transaction (access-list reset / reverted StorageWarmed). That wrongly
re-baselined the EIP-2200 original for same-tx cold resets. Restrict the
re-baseline to genuine cross-transaction access (transaction_id mismatch).
Deposit minting writes BVM_ETH via the journal (load_account/sload/sstore), which warms the BVM_ETH account + storage slots for the subsequent EVM execution. op-geth mints via StateDB.SetState(), which never touches the EVM access list, so it stays cold. This warm/cold divergence was patched over with a static BVM_ETH_MINT_GAS_COMPENSATION (4500) gated on `!tx.input().is_empty()`.

That heuristic mis-fired for non-empty-calldata deposits to EOAs (hoodi-qa2 block 59294: reth 25628 vs geth 21320 -> receiptsRoot/blockHash fork), and over/under-compensated other access paths since the real cold-access cost is dynamic.

Root-cause fix: after process_eth_deposit, reset BVM_ETH (account + touched storage slots) back to EIP-2929 cold via a new JournalColdExt, so the EVM observes it exactly like op-geth (cold then falls back to the tx access list / to / precompiles). The static compensation constant and the refund-stage hack are removed entirely.

Scope: op-revm only, no core revm changes. The JournalColdExt bound is carried by OpContextTr so no per-call-site threading is needed.

Tests: process_eth_deposit_leaves_bvm_eth_cold (mechanism); deposit_to_eoa_with_calldata_no_compensation_matches_geth (block 59294 replay asserts 21320, not 25628); existing mainnet fixture 89718944 (L2StandardBridge) still passes. cargo test -p op-revm: all green.
fix: BVM_ETH deposit gas parity on revm-19 (cold reset + cross-tx original re-baseline)
…o match op-geth (#32)

* fix(op-revm): persist BVM_ETH Mint/Transfer logs on failed deposits to match op-geth

A failed Mantle deposit (status=0) must persist its BVM_ETH `Mint` log (and
the `Transfer` log when the transfer executed) into the receipt, matching
op-geth. Dropping them forks the receipts/state root.

op-geth model (core/state_transition.go):
- Case A: innerExecute returns a Go error (transfer ErrEthTxValueTooLarge,
  intrinsic/validation) -> RevertToSnapshot -> only the pre-snapshot Mint
  persists, nonce++, gas = GasLimit.
- Case B: the EVM ran then REVERT/OOG (vmerr, err == nil; "vm errors do not
  effect consensus") -> no rewind -> Mint + Transfer persist, gas = actual.

Changes:
- catch_error (Case A): capture journal logs before commit_tx so the mint-only
  Mint log survives into the FailedDeposit halt.
- execution_result (Case B): for a post-Regolith deposit whose frame
  Reverts/Halts, keep the surviving logs + actual ResultGas and re-label as
  FailedDeposit (no checkpoint_revert, no re-mint -- the pre-frame state,
  including the CREATE recipient and caller nonce, is already correct).

Verified against op-geth on the three known fork blocks: Sepolia 286432
(Case A, Mint only) and 286456 (Case B, Mint+Transfer) via real-block replay
fixtures, and mainnet 96442768 (byte-exact Mint log); also covers CREATE
deposit recipient parity and OOG.

Regenerates the test_halted_deposit_tx golden (benign: non-consensus status
flag + original_info.code only; material state byte-identical) and clears
clippy -D warnings.

* refactor(op-revm): dedupe execution_result cleanup into a single tail

Compute the failed-deposit re-label result first, then run the
commit_tx/clear_tx_l1_cost/local.clear/frame_stack.clear quartet once for
both the deposit and normal paths instead of duplicating it across an early
return. No behavior change (op-revm 121 tests pass, clippy -D warnings clean).
…posit

`process_eth_deposit` gated the BVM_ETH mint/transfer on `is_some()`, so a
`Some(0)` eth_value/eth_tx_value would mint/transfer zero — which is not a
no-op: it touches the BVM_ETH balance/totalSupply storage slots and emits a
zero-value Mint/Transfer log. op-geth gates these on `!= nil && != 0`
(`core/state_transition.go`), so a stray `Some(0)` would diverge on both state
root (touched slots) and receipts root (extra log).

Filter zero to `None` at the top of `process_eth_deposit`, mirroring op-geth and
keeping the guard at the execution layer (self-sufficient regardless of how the
deposit was constructed: engine API, direct tx-env, or the conversion layer).

Defense-in-depth, not a live bug on the current pipeline (op-node nils zero
EthValue/EthTxValue; alloy-op-evm normalizes eth_value==0 -> None) — but that
normalization was asymmetric (eth_tx_value passed through) and relied on the
input pipeline rather than the execution layer.

Test: process_eth_deposit_zero_eth_value_treated_as_none.
…-guard

fix(op-revm): treat zero BVM_ETH eth_value/eth_tx_value as None in deposit (op-geth parity)
…us precompile set (#36)

* fix(op-revm): gate CanTransfer and EIP-3860 on deposits to match op-geth

Port the pre-call deposit gates from the arsia line to elysium (v107).

op-geth applies all pre-call checks inside innerExecute() and, on a deposit,
routes any failure to the mint-only path (RevertToSnapshot + gasUsed=GasLimit),
keeping only the BVM_ETH Mint log. op-revm skips env validation for deposits, so
two pre-call conditions leaked into the EVM frame and took the wrong route,
diverging from op-geth:

  - native (MNT) value transfer the caller cannot cover after the mint
    (CanTransfer, state_transition.go L781)
  - CREATE deposit whose init code exceeds EIP-3860 MAX_INITCODE_SIZE
    (state_transition.go L786)

Add both gates to validate_against_state_and_deduct_caller's deposit branch so
they return a tx-level error -> catch_error -> mint-only, matching op-geth.

Regression tests:
  - test_failed_deposit_native_value_too_large_keeps_eth_mint_only
  - test_failed_create_deposit_initcode_too_large_keeps_eth_mint_only

* fix(op-revm): drop OP input-size limits from Isthmus precompiles to match Mantle op-geth

Mantle maps its entire pre-Limb era (Skadi and genesis) to OpSpecId::ISTHMUS
(see mantle-v2 alloy-op-evm `spec_by_timestamp_after_bedrock` and kona
`mantle_spec_id`: anything below Limb -> ISTHMUS). op-geth's pre-Limb set
`PrecompiledContractsMantleSkadi` is the standard Prague precompile set plus
secp256r1 p256verify, using the standard Istanbul bn254 pairing and the
standard EIP-2537 BLS12-381 precompiles -- none of which carry OP Stack
input-size limits.

op-revm's `isthmus()`, however, inherited `granite()`'s bn254 pairing limit
(GRANITE_MAX_INPUT_SIZE = 112687) and the upstream OP Isthmus BLS12-381
input-size limits (G1 MSM / G2 MSM / pairing). This made op-revm reject
oversized-but-valid bn254/BLS inputs that Mantle op-geth accepts, a
consensus divergence for the pre-Limb era.

Build the Isthmus set from `fjord()` (Cancun + p256verify, unrestricted
Istanbul bn254 pairing) plus the unrestricted Prague BLS12-381 precompiles,
matching `PrecompiledContractsMantleSkadi` exactly. `jovian()` is unaffected:
it removes the variable-input precompiles by address and re-adds its own
Jovian-limited versions. OSAKA/ARSIA (Limb and after) were already correct.

Add a regression test asserting the Isthmus set imposes no OP input-size
limit on bn254 pairing or BLS12-381 G1 MSM / G2 MSM / pairing.

* test(ee-tests): add e2e regression for unrestricted Isthmus bn254 pairing

Mirrors the existing bn254 fjord/granite tx-level tests with an Isthmus case.
Feeds the bn254 pairing precompile an input that is a valid multiple of the
pair-element length but larger than the Granite input cap, so the only thing
that could reject it on length is the OP Stack cap. Asserts the call is NOT
halted with the Granite-style "bn254 invalid pair length" error, proving the
Isthmus set (which Mantle maps its pre-Limb era to) carries no OP input-size
limit, consistent with op-geth PrecompiledContractsMantleSkadi.

Verified RED on the pre-fix granite-based isthmus() (halts with "bn254 invalid
pair length") and GREEN after the fix (runs past the length gate, out of
precompile gas).

---------

Co-authored-by: ivan <314130948@qq.com>
@byteflyfunny byteflyfunny changed the title feat(revm): upgrade to upstream v107 (revm 38.0.0) for Mantle Elysium feat(revm): upgrade to upstream v107 (revm 38.0.0) + op-geth parity fixes for Mantle Elysium Aug 4, 2026
Resolves the divergence between the v98-based BVM_ETH fixes on `main`
(PRs #27, #29, #31) and their v107-based rework on this branch
(PRs #28, #32, #33, #36). Both lines implement the same op-geth parity
semantics against different revm baselines, so `op-revm/handler.rs` and
`transaction/bvm_eth.rs` conflicted broadly.

Resolution principle: keep the v107 code shape (this branch), but ensure
every semantic fix and every test present on `main` survives the merge.

Conflict resolution (38 hunks across 7 files)
- context/interface/result.rs: keep `gas: ResultGas` + logs on all three
  variants (upstream bluealloy#3413/bluealloy#3424). Retain main's `serde(default)` on
  `Halt.logs` so pre-existing serialized halts still decode, and its
  rationale doc. `logs()`/`into_logs()` now also surface Revert logs,
  which v107 carries and main's enum could not.
- handler/post_execution.rs: keep the upstream path that forwards
  `take_logs()` into Halt. This is what lets a failed deposit's
  pre-frame BVM_ETH mint/transfer logs survive without main's
  checkpoint_revert-and-rebuild.
- op-revm/handler.rs: take this branch. Its `execution_result` relabels
  the failed deposit in place instead of reverting and re-applying
  mint/transfer/nonce, avoiding the divergence risk of rebuilding state.
  main's test set here is a strict subset of this branch's.
- op-revm/transaction/bvm_eth.rs: impl region verified byte-identical to
  this branch's pre-merge version. Ported main's 13 JournalColdExt
  cooling tests (PR #27), which had no counterpart here — cold/warm
  state drives gas and therefore the state root.
- examples/block_traces: took main's per-receipt reconciliation
  (status/cumGas/logs/bloom vs on-chain) and range summary, ported to
  the v107 `tx_gas_used()`/`into_logs()` API, and folded the previous
  per-tx gas check back in as an extra `gas` field.
- examples/custom_precompile_journal, ee-tests: v107 API shape only.

The ported cooling tests pass against this branch's v107 implementation
with no assertion changes — only `..Default::default()` added for the
new `AccountInfo::account_id` field — which independently confirms the
two cooling implementations are semantically equivalent.

Verification
- cargo test -p op-revm: 138 passed, 0 failed (125 + 13 ported)
- cargo test --workspace --exclude revm-ee-tests: 411 passed, 0 failed
- cargo clippy --workspace --all-targets: no warnings
- cargo fmt --all --check: clean
Review of the origin/main merge found one semantic loss: the conflict
resolution took this branch's side wholesale for bvm_eth.rs tests, which
reverted `deposit_to_eoa_with_calldata_no_compensation_matches_geth` from
ARSIA back to ISTHMUS. This branch never picked up main's 244f2cd.

Block 59294 — and every deployed Mantle deposit — executes under ARSIA
(eth_spec OSAKA). ISTHMUS is a different gas regime and leaves the
is_arsia branches uncovered, so the replay was validating against a spec
the chain does not run.

The 21_320 EIP-7623 floor assertion still holds under ARSIA on v107.

cargo test -p op-revm: 138 passed, 0 failed
Three CI jobs were red on this PR. Only the first is introduced by this
branch; the other two also reproduce on main and are fixed here because
they block the merge.

- database/alloydb.rs: `[MANTLE]` in a doc comment is parsed by rustdoc
  as an intra-doc link and fails to resolve, breaking `cargo doc` for
  revm-database. Wrapped in backticks. This one is new in this PR —
  origin/main's alloydb.rs has no such comment (the Audit-fix #25
  migration to AlloyDBError brought it in). The many `// [MANTLE]` line
  comments elsewhere are unaffected: rustdoc only parses `///`.
- database/states/bundle_account.rs: manual `Option::filter` via
  and_then. Present verbatim on main too; only newer clippy in CI flags
  it (local clippy 0.1.95 does not).
- op-revm/handler.rs: "REVERTs" trips typos v1.49 (tokenizes to REVER).
  Present on main as well. Lowercased to "reverts".

cargo doc --workspace --all-features: clean
cargo clippy --workspace --all-targets --all-features: clean
cargo test -p op-revm: 138 passed; --workspace: 411 passed, 0 failed
… link

The previous CI fix only escaped the two `[MANTLE]` markers in
database/alloydb.rs, so `cargo doc` still failed on revm-interpreter.
The local run passed at the time only because cargo's incremental
fingerprint did not re-document the untouched crates — a false negative.

Escaped every remaining Mantle marker in a doc comment:
- interpreter/src/gas.rs:219
- op-revm/src/l1block.rs:185, 319
- op-revm/src/precompiles.rs:83

Also qualified `[`calculate_tx_l1_cost_arsia`]` as `Self::` — it is a
private method, so the bare link resolved to nothing (warning only, but
it would turn into an error under a stricter rustdoc lint config).

Verified with a forced full rebuild (cargo clean --doc + touch every
.rs) rather than an incremental run:
- cargo doc --workspace --all-features --no-deps --document-private-items:
  0 errors, 0 unresolved links
- cargo test -p op-revm: 138 passed; --workspace: 411 passed, 0 failed
- cargo clippy --all-targets --all-features: clean; cargo fmt: clean
@asdv23
asdv23 merged commit 1ed03aa into main Aug 4, 2026
25 of 30 checks passed
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.