ethereum-optimism / ethereum-optimism/optimism

bug(alloy-op-evm): eth_createAccessList reports gasUsed of the run without the discovered access list

Open
#21,682 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Go
Stars
6.5k
Forks
4k
Avg merge
2d 15h
Merged PRs (30d)
145

Description

## Summary

On op-reth, `eth_createAccessList` returns a `gasUsed` that corresponds to executing the request **without** the access list it just discovered, instead of the intended "exact gas used" of a re-run **with** the list attached. Ethereum reth returns the with-list gas for the identical request shape, and geth/op-geth's `AccessList` implementation also returns the with-list gas — op-reth is the odd one out.

The root cause is a 3-line wrapper impl in `alloy-op-evm` that assigns the access-list field directly instead of delegating to `TxEnv::set_access_list`, silently dropping the Legacy→EIP-2930 tx-type upgrade that makes the list effective in revm.

## Reproduction (any public op-reth OP-mainnet endpoint)

```bash
URL=https://optimism-rpc.publicnode.com # reth/v2.3.0 as of writing
REQ='{"to":"0x94b008aA00579c1307B0EF2c499aD98a8ce58e58","data":"0x70a0823100000000000000000000000000000000000000000000000000000000deadbeef"}'

# 1) createAccessList discovers a 1-address/1-key list and reports gasUsed = 23759 (0x5ccf)
curl -s $URL -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_createAccessList","params":['$REQ',"latest"]}'

# 2) eth_estimateGas WITHOUT the list ≈ 24097; WITH the returned accessList attached ≈ 26401.
# Executing with the list genuinely costs ~2300 more (2400/addr + 1900/key EIP-2930 intrinsic
# minus a 2000 warm-SLOAD saving) — yet createAccessList reports the without-list number.

# 3) Smoking gun: add a semantically-empty "accessList":[] to the SAME request. The only effect
# is that the request-derived tx type becomes EIP-2930 instead of Legacy — and the reported
# gasUsed flips from 23759 to 26059 (the correct with-list value):
REQ2='{"to":"0x94b008aA00579c1307B0EF2c499aD98a8ce58e58","data":"0x70a0823100000000000000000000000000000000000000000000000000000000deadbeef","accessList":[]}'
curl -s $URL -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_createAccessList","params":['$REQ2',"latest"]}'
```

On nodes with `debug` enabled, `debug_traceCall` of the same request with/without the returned `accessList` shows the two numbers directly: the reported `gasUsed` always equals the **without-list** execution. (Values above move slightly with state; the structure — "reported = without-list, flips when the request carries any accessList field" — is stable and reproduces on every op-reth v2.3.x endpoint we tried. Ethereum-mainnet reth of the same version reports the with-list value.)

## Root cause

`eth_createAccessList` goes through reth's generic `create_access_list_with`, which after the inspection pass does:

```rust
tx_env.set_access_list(access_list.clone());
...
// transact again to get the exact gas used
let result = this.transact(&mut db, evm_env, tx_env)?;
```

For a list-less request the first-pass tx env derives `TxType::Legacy` (`minimal_tx_type()`). The base implementation `TransactionEnvMut for TxEnv` (alloy-evm 0.36.0, `src/env.rs`) handles this by upgrading the type:

```rust
fn set_access_list(&mut self, access_list: AccessList) {
self.access_list = access_list;
if self.tx_type == TransactionType::Legacy as u8 {
self.tx_type = TransactionType::Eip2930 as u8;
}
}
```

but the OP wrapper impl bypasses it ([`rust/alloy-op-evm/src/tx.rs#L271-L273`](https://github.com/ethereum-optimism/optimism/blob/b40d2ce097b928fe1a4ec79a8a1925eb231c675e/rust/alloy-op-evm/src/tx.rs#L271-L273)):

```rust
fn set_access_list(&mut self, access_list: alloy_eips::eip2930::AccessList) {
self.0.base.access_list = access_list;
}
```

revm treats an access list on a Legacy-typed transaction as fully inert — no EIP-2930 intrinsic pricing (revm-context-interface 19.0.3, `gas_params.rs`, `initial_tx_gas_for_tx`: "Legacy is the only tx type that does not have an access list") and no prewarming (revm-handler 20.0.3, `pre_execution.rs`, same gate). So the "transact again" re-run is a byte-for-byte repeat of the first run, and the reported `gasUsed` is the without-list gas.

The exact-gas re-run semantics were introduced deliberately in paradigmxyz/reth#10416 / paradigmxyz/reth#10422. Before the `TransactionEnvMut` migration, reth carried a correct **delegating** impl for this exact wrapper shape (`impl TransactionEnv for op_revm::OpTransaction` delegated to `self.base.set_access_list(...)`; removed in paradigmxyz/reth#23218) — so this is a regression relative to the previously-established pattern, not a design choice. The in-repo example `rust/op-reth/examples/custom-node/src/evm/env.rs` also delegates for its wrapper type.

## Expected behavior

`eth_createAccessList.gasUsed` should be "the gas the transaction consumes if submitted with the returned access list" — matching geth/op-geth (`internal/ethapi/api.go`, `AccessList` attaches the list to the message each iteration and returns that execution's `UsedGas`) and matching reth's behavior on Ethereum mainnet.

## Proposed fix

One line — delegate to the base impl so the type upgrade applies (deposit transactions are unaffected; the upgrade only fires for Legacy):

```rust
fn set_access_list(&mut self, access_list: alloy_eips::eip2930::AccessList) {
self.0.base.set_access_list(access_list);
}
```

PR with the fix + regression tests: https://github.com/ethereum-optimism/optimism/pull/21683

Contributor guide

Open the contributing guide

Research direction

Start at rust/alloy-op-evm/src/tx.rs#L271-L273 and compare the wrapper behavior with rust/op-reth/examples/custom-node/src/evm/env.rs. Reproduce eth_createAccessList with and without an accessList, then review the regression tests in linked PR #21683. Done means gasUsed matches execution with the discovered list, including for list-less requests.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
blockchain
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Stale
Clarity
Clearly specified
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.