Is your feature request related to a problem? Please describe.
eth_call simulations spend ~8.4% of total CPU time on multi-dimensional gas accounting (addConstantMultiGas + MultiGas.SaturatingAddInto) that is never consumed. The UsedMultiGas struct tracks gas across 7 Arbitrum resource kinds (Computation, StorageAccess, HistoryGrowth, etc.), but eth_call never produces receipts — the only consumer of this data (state_processor.go → receipt.MultiGasUsed).
This was identified via perf profiling under 100 RPS eth_call load on Arbitrum mainnet:
| % CPU |
Function |
Category |
| 5.9% |
addConstantMultiGas |
Per-opcode constant multi-gas |
| 2.5% |
MultiGas.SaturatingAddInto |
Per-opcode dynamic multi-gas |
| 8.4% |
Total |
Write-only bookkeeping, discarded for eth_call |
Describe the solution you'd like
Add a SkipMultiGas bool field to vm.Config and gate the two hot-path multi-gas accumulation calls in the interpreter loop:
// core/vm/interpreter.go — in the Config struct
SkipMultiGas bool // Skip multi-dimensional gas accounting (RPC-only optimization)
// In Run(), before the main loop:
skipMultiGas = evm.Config.SkipMultiGas
// In the main loop — gate the two hot lines:
if !skipMultiGas {
addConstantMultiGas(&contract.UsedMultiGas, cost, op)
}
// ... and for dynamic gas:
if !skipMultiGas {
contract.UsedMultiGas.SaturatingAddInto(multigasDynamicCost)
}
Then set SkipMultiGas: true in the eth_call RPC path (internal/ethapi/api.go → doCall):
return applyMessage(ctx, b, args, state, header, timeout, gp, &blockCtx,
&vm.Config{NoBaseFee: true, SkipMultiGas: true}, precompiles, runCtx)
Safety:
- Single-dimensional gas (
contract.Gas) is completely untouched — gas limits, OOG errors, and gasUsed in responses work identically
UsedMultiGas is write-only during EVM interpretation; no execution path reads it to make branching decisions
- Block production, consensus, Stylus WASM execution,
debug_traceCall, and all other RPCs are unaffected (they don't set SkipMultiGas)
eth_estimateGas could also benefit if it uses the same code path
Describe alternatives you've considered
- Make
addConstantMultiGas a no-op function pointer — introduces indirect call overhead in the hot loop, worse than a branch
- Compile-time flag — less flexible, prevents using the same binary for both block production and RPC
- Skip
dynamicGas MultiGas construction entirely — would require changing all dynamicGas function signatures to return uint64 when in skip mode; larger refactor for diminishing returns
Additional context
Benchmarked on Arbitrum mainnet, EPYC 4584PX @ 4.8 GHz, 500-request mix (small to xlarge payloads, from 10KB to 500KB-1MB requests), 100 RPS, 3-minute runs:
| Metric |
Before |
After (SkipMultiGas) |
Change |
| P50 |
87ms |
78ms |
-10.3% |
| P90 |
131ms |
122ms |
-6.9% |
| P99 |
178ms |
173ms |
-2.8% |
| Errors |
0 |
0 |
— |
The change is 3 lines of logic + 1 config field. The skipMultiGas local variable ensures the branch predictor handles the constant check with zero overhead after the first iteration.
Tested on Nitro v3.9.7 (75e084e).
Is your feature request related to a problem? Please describe.
eth_callsimulations spend ~8.4% of total CPU time on multi-dimensional gas accounting (addConstantMultiGas+MultiGas.SaturatingAddInto) that is never consumed. TheUsedMultiGasstruct tracks gas across 7 Arbitrum resource kinds (Computation, StorageAccess, HistoryGrowth, etc.), buteth_callnever produces receipts — the only consumer of this data (state_processor.go→receipt.MultiGasUsed).This was identified via
perfprofiling under 100 RPSeth_callload on Arbitrum mainnet:addConstantMultiGasMultiGas.SaturatingAddIntoDescribe the solution you'd like
Add a
SkipMultiGas boolfield tovm.Configand gate the two hot-path multi-gas accumulation calls in the interpreter loop:Then set
SkipMultiGas: truein theeth_callRPC path (internal/ethapi/api.go→doCall):Safety:
contract.Gas) is completely untouched — gas limits, OOG errors, andgasUsedin responses work identicallyUsedMultiGasis write-only during EVM interpretation; no execution path reads it to make branching decisionsdebug_traceCall, and all other RPCs are unaffected (they don't setSkipMultiGas)eth_estimateGascould also benefit if it uses the same code pathDescribe alternatives you've considered
addConstantMultiGasa no-op function pointer — introduces indirect call overhead in the hot loop, worse than a branchdynamicGasMultiGas construction entirely — would require changing alldynamicGasfunction signatures to returnuint64when in skip mode; larger refactor for diminishing returnsAdditional context
Benchmarked on Arbitrum mainnet, EPYC 4584PX @ 4.8 GHz, 500-request mix (small to xlarge payloads, from 10KB to 500KB-1MB requests), 100 RPS, 3-minute runs:
The change is 3 lines of logic + 1 config field. The
skipMultiGaslocal variable ensures the branch predictor handles the constant check with zero overhead after the first iteration.Tested on Nitro v3.9.7 (
75e084e).