ethereum-optimism / ethereum-optimism/optimism
bug(alloy-op-evm): eth_createAccessList reports gasUsed of the run without the discovered access list
- 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
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