Summary
Ethereum's Amsterdam fork introduces multi-dimensional gas (EIP-8037) alongside block gas accounting changes (EIP-7778) and block access lists (EIP-7928). Our current implementation on the eip-8037 branch threads a new MdGas{Regular, State} type through 46 files, duplicating critical save/restore and spill-reversal logic at 6 call/create sites and 2 call/create lifecycle functions. This duplication is the root cause of contradicting edge case fixes — the commit history shows a recurring cycle of "fix X breaks Y" as the duplicated blocks diverge.
14 Amsterdam test fixtures are currently skipped, several showing large gas discrepancies (e.g., 56K gas mismatch), indicating structural accounting errors rather than off-by-one issues.
The risk analysis identifies extracting shared helpers (Phase 1) as a high-value, near-zero-risk first step that can be done immediately. It eliminates the most dangerous code duplication — the 6× save/restore pattern in instruction handlers and the 2× spill-reversal block in call()/create() — without introducing new abstractions or changing any APIs. This alone would reduce the surface area where contradicting fixes can occur and may resolve some of the skipped tests by unifying logic that has silently diverged between the two copies.
Further phases (centralized GasLedger, typed gas functions, formalized two-phase gas for BAL) offer increasing structural improvement but carry higher merge conflict risk with the actively-developed eip-8037 branch and should be sequenced after the current batch of gas fixes stabilizes.
Background
This document was produced to inform architectural decisions around multi-dimensional gas in Erigon. It combines:
- A detailed analysis of how three gas dimensions (regular, state, blob) flow through the EVM, including EIP-7778 refund accounting and EIP-7928's stateful/stateless gas split for block access lists
- An outline design for a centralized "GasLedger" that addresses the structural problems identified in the analysis
- A risk and sizing assessment for migrating to that design
The outline design is presented for analysis purposes — it illustrates one approach to centralizing gas lifecycle management and provides a concrete basis for sizing and risk evaluation. Alternative approaches are possible, including lighter-weight refactors that address the most critical duplication without introducing the full ledger abstraction. The phased migration strategy is specifically designed to allow stopping at any phase boundary if the cost/benefit shifts.
Part I: Architecture Analysis
1. The Three Gas Dimensions
| Dimension |
EIP |
Introduced |
Scope |
Pricing |
Pool Limit |
| Regular |
(original) |
Genesis |
EVM execution, memory, computation |
EIP-1559 base fee + tip |
block.gasLimit |
| Blob |
EIP-4844 |
Cancun |
Data availability (transaction-level) |
Independent exponential blob base fee |
maxBlobGasPerBlock |
| State |
EIP-8037 |
Amsterdam |
Storage/account creation (EVM-level) |
Dynamic CostPerStateByte per block |
Shares block.gasLimit via max(regular, state) |
Additionally, EIP-7778 (Amsterdam) splits regular gas into two accounting views — receipt gas (with refunds) and block gas (without refunds) — applied retrospectively at the end of transaction execution.
2. EIP Specifications
2.1 EIP-7778: Block Gas Accounting Without Refunds
Problem: Gas refunds reduce both user costs and block gas accounting, allowing blocks to exceed their intended computational limits. Block 20,878,522 showed 28.5 MGas net usage but 32.51 MGas gross, exceeding the block limit.
Solution: Split gas accounting into two views of the same execution:
- User gas (ReceiptGasUsed): refunds still apply —
gas_spent = max(tx_gas_used - gas_refund, calldata_floor_gas_cost)
- Block gas (BlockGasUsed): refunds excluded —
block.gas_used += max(tx_gas_used, calldata_floor_gas_cost)
Key property: This is a retrospective divergence — during execution there's only one gas counter. The split happens purely at the accounting stage after execution completes. This is why 7778 was relatively clean to implement.
Implementation in state_transition.go:639-667:
gasUsed := st.gasUsed() // raw gas consumed
st.blockGasUsed = gasUsed // start with raw
stateRefund := st.state.GetRefund()
effectiveRefund = min(gasUsed/refundQuotient, stateRefund)
gasUsed = gasUsed - effectiveRefund // user gets refund
if rules.IsAmsterdam {
// EIP-7778: block accounting ignores refunds
st.blockGasUsed = max(intrinsicGasResult.FloorGasCost, st.blockGasUsed)
} else {
st.blockGasUsed = gasUsed // pre-Amsterdam: block = user view
}
st.gp.AddGas(st.initialGas - st.blockGasUsed) // return unused to pool
Fee calculations use ReceiptGasUsed (with refunds) — so the validator gets paid based on the refunded amount, while the block limit is enforced on the non-refunded amount.
2.2 EIP-7928: Block-Level Access Lists (BAL)
Core change: Two-phase gas validation:
- Pre-state validation — gas costs calculable without state access (memory expansion, base cost, warm/cold designation)
- Post-state validation — costs requiring state access (account existence, EIP-7702 delegation resolution)
Critical rule: "Pre-state validation MUST pass before any state access occurs. If pre-state validation fails, the target resource is never accessed" and therefore excluded from the BAL.
BAL recording: Only addresses and storage slots actually touched are recorded. EIP-2930 access list entries are NOT automatically included.
Block constraint: bal_items ≤ block_gas_limit ÷ 2000
2.3 EIP-8037: State Creation Gas Cost Increase (Multi-Dimensional Gas)
Problem: State creation costs are too low relative to their long-term impact. As block limits increase, state growth accelerates unsustainably.
Solution: Introduce a second gas dimension tracked during EVM execution:
- Regular gas: computation, memory, parallelizable operations
- State gas: persistent state creation (SSTORE new slot, CREATE, code deposit)
Dynamic pricing via cost_per_state_byte calculated per block from gas limit, targeting 100 GiB/year state growth at 50% utilization. Currently hardcoded to 1174 for bal-devnet-3.
Key properties:
- State gas can spill into regular gas when the state reservoir is exhausted
- Block validity:
max(regular_gas_used, state_gas_used) ≤ gas_limit
- Transaction gas limit can exceed
MaxTxnGasLimit — the excess becomes the state gas reservoir
3. Current Implementation Architecture
3.1 Blob Gas (Clean, Independent)
Blob gas is the cleanest model — genuinely independent of the EVM:
BlobTx.BlobVersionedHashes → GetBlobGas() = count × 131,072
↓
preCheck(): validate maxFeePerBlobGas ≥ blobBaseFee
↓
buyGas(): GasPool.SubBlobGas() — independent pool
↓
Receipt: BlobGasUsed per-tx
Header: BlobGasUsed total, ExcessBlobGas for next block pricing
Why it works well:
- Never enters the EVM interpreter loop — the EVM is unaware of blob gas
- Has its own independent pool (
GasPool.blobGas)
- Has its own pricing mechanism (exponential
FakeExponential)
- Has its own balance check (
maxFeePerBlobGas × blobGas)
- Block validity: simple
blobGasUsed ≤ maxBlobGasPerBlock
3.2 EIP-7778 Implementation (Retrospective Split)
The ExecutionResult struct stores both accounting views:
type ExecutionResult struct {
ReceiptGasUsed uint64 // with refunds (user pays)
BlockGasUsed uint64 // without refunds (block limit)
}
On the eip-8037 branch, BlockGasUsed further splits into BlockRegularGasUsed and BlockStateGasUsed, with the block-level value being max(regular, state).
3.3 State Gas Implementation (Entangled with Regular)
On the eip-8037 branch, state gas is threaded through the entire EVM:
New type — execution/vm/evmtypes/mdgas/md_gas.go:
type MdGas struct {
Regular uint64
State uint64
}
With spill logic: MinusStateGas(sg) — if state gas insufficient, overflow charged from regular gas.
Key changes:
CallContext gains stateGas uint64 alongside existing gas uint64
Run() signature changes from gas uint64 to gas MdGas
- All
Call/Create signatures take and return MdGas
- Three new EVM tracking fields:
stateGasConsumed, regularGasConsumed, revertedSpillGas
- Every
gasFunc returns MdGas instead of uint64
- Refund counter changes from
uint64 to MdGas
4. The Stateful vs Stateless Gas Split
4.1 Current Two-Phase Pattern
The codebase already separates gas calculation into two phases for CALL variants, introduced for EIP-7702 delegation resolution:
| Phase |
Function |
Computes |
State Access? |
| Stateless |
statelessGasCall() |
Memory expansion, value transfer, base cost |
No |
| Stateful |
statefulGasCall() |
Account creation gas (Empty() check) |
Yes |
| Call gas |
calcCallGas() |
63/64 rule allocation |
No |
Composed in makeCallVariantGasCallEIP7702() (operations_acl.go:290):
1. Cold/warm access check (access list lookup — no state read)
2. statelessCalculator() — memory, value transfer (no state read)
3. Pre-state OOG check — ABORT HERE if can't afford stateless cost
4. statefulCalculator() — Empty() check (STATE READ)
5. Delegation resolution — GetDelegatedDesignation() (STATE READ)
6. calcCallGas() — 63/64 rule (no state read)
4.2 Opcode Classification by Gas State Dependency
Fully stateless gas (never needs state to compute cost):
- Arithmetic, stack, memory, flow control
- LOG0-4, KECCAK256, RETURNDATACOPY, CALLDATACOPY, CODECOPY
- MCOPY, PUSH, DUP, SWAP
- SLOAD — gas depends only on warm/cold (access list), not state values
- BALANCE, EXTCODESIZE, EXTCODEHASH, EXTCODECOPY — same, warm/cold only
Two-phase gas (can split stateless/stateful):
- CALL: stateless = memory + value transfer; stateful =
Empty() for account creation
- CALLCODE, DELEGATECALL, STATICCALL: stateless only (no account creation cost)
- SELFDESTRUCT: stateless = base cost; stateful =
Empty() + GetBalance() for creation gas
- CREATE/CREATE2: stateless = memory + initcode words; stateful = collision check (pass/fail, not cost-varying)
Inherently stateful gas (cost depends on state values):
- SSTORE: gas depends on current value, original value, new value — cannot split
SSTORE is the only opcode whose gas cost is truly state-dependent. Everything else can determine gas from the access list (warm/cold) plus stack arguments.
4.3 BAL Recording Implications
Even if gas is stateless, BAL recording depends on whether the operation actually proceeds:
- CALL to cold address → add to access list (gas) →
Empty() check (state) → if OOG on cold gas, don't record in BAL
- CALL to warm address → no gas for access → proceed to
Empty() → do record in BAL
- SSTORE with insufficient gas for sentry check → abort → don't record in BAL
- SSTORE with sufficient gas → read state for cost determination → do record in BAL
This is why MarkReadsInternal() and the revertable flag exist — to distinguish "I read state for gas calculation purposes" from "I actually performed the operation."
4.4 EIP-8037 Complication for BAL
With state gas, the CALL statefulGasCall now returns MdGas{State: 112 × cpsb} when creating a new account. The spill mechanism means that even though the cost is classified as "state gas," the deduction may come from regular gas. If the spill causes OOG on regular gas, the operation fails — but the state read (Empty()) already happened.
This is correct for BAL purposes (the access is recorded) and satisfies EIP-7928's pre-state validation requirement (the stateless phase passed before the state read). But it creates an asymmetry where the payment fails due to cross-dimension spill after the state access.
5. Structural Problems
5.1 State Gas Lives in Two Places with Different Semantics
CallContext has gas uint64 and stateGas uint64 as separate fields, but MdGas is the struct passed across call boundaries. Every call/create entry point must manually save parentStateGas, zero scope.stateGas, then conditionally restore on error. This save/restore pattern is duplicated 6 times (opCall, opCallCode, opDelegateCall, opStaticCall, opCreate, opCreate2) with identical logic:
parentStateGas := scope.stateGas
scope.stateGas = 0
// ... call ...
if err != nil {
scope.stateGas = parentStateGas
} else {
scope.stateGas = returnGas.State
}
5.2 Spill Logic is Duplicated and Error-Prone
The "state gas overflows into regular gas" spill happens in useMdGas() (the generic helper), but the reversal of spill on revert is duplicated in both evm.call() and evm.create() with nearly identical ~20-line blocks:
childStateConsumed := evm.stateGasConsumed - savedStateGasConsumed
if depth > 0 {
evm.stateGasConsumed = savedStateGasConsumed
}
reservoirUsed := initialChildState - gas.State
if childStateConsumed > reservoirUsed {
spill := childStateConsumed - reservoirUsed
gas.Regular += spill
if depth == 0 && err == ErrExecutionReverted {
evm.revertedSpillGas += spill
}
}
if err != ErrExecutionReverted {
gas.State = initialChildState
if depth == 0 {
gas.Regular = 0
}
}
Any bug in one copy but not the other creates edge cases. The commit history tells the story: "fix restorations on revert/halts", "fix spill and pass on state gas cost in call", "adjust regularGasConsumed tracking", "fix exceptional halt".
5.3 Block-Level Accounting is Scattered
regularGasConsumed is updated in 5+ places:
- Constant gas in the interpreter loop
- Dynamic regular gas in the interpreter loop
- Precompile calls in
evm.call()
- Exceptional halt (remaining gas counted as consumed)
- Stipend adjustments in opCall/opCallCode (
regularGasConsumed -= CallStipend)
This makes it very hard to verify the invariant: regularGasConsumed + leftoverRegular == initialRegularGas.
5.4 The gasFunc Signature Change is All-or-Nothing
Every gasFunc now returns MdGas even for pre-Amsterdam operations that only ever use Regular. This means 30+ gas functions wrap their return in mdgas.MdGas{Regular: gas} boilerplate. The type signature doesn't distinguish between "this function can return state gas" and "this function never does."
5.5 Revert Semantics Differ by Depth
At depth 0, state gas is kept consumed (for block accounting) but spill is restored. At depth > 0, stateGasConsumed is rolled back. This depth-dependent behavior is the primary source of contradicting edge case fixes.
5.6 The Three-Way Refund Interaction
EIP-7778 says block gas ignores refunds. EIP-8037 has state refunds. The refund total is min(gasUsed/5, regularRefund + stateRefund) applied as a single scalar. But gasUsed now includes spilled state gas. The revertedSpillGas field patches this:
txnGasUsedB4Refunds = mdGasUsed.Total() + evm.RevertedSpillGas()
6. The Full Gas Flow
┌─────────────────────────────────────────────────┐
│ TRANSACTION ENTRY │
│ │
│ Tx.Gas → IntrinsicGas (regular + state) │
│ → SplitIntoMdGas(regular, state) │
│ → buyGas(GasPool.SubGas) │
│ │
│ Tx.BlobGas → GasPool.SubBlobGas (independent) │
│ │
│ Tx balance must cover: │
│ gasLimit × maxFeePerGas │
│ + blobGas × maxFeePerBlobGas │
│ + value │
└──────────┬──────────────────────────────────────┘
│
┌──────────▼──────────────────────────────────────┐
│ EVM EXECUTION │
│ │
│ Per-opcode: │
│ constantGas ──────────► regular only │
│ dynamicGas: │
│ Phase 1 (stateless) ► regular gas │
│ Phase 2 (stateful) ► regular + state gas │
│ └─ state can SPILL to regular │
│ └─ BAL records state accesses │
│ │
│ SSTORE: inherently stateful (no phase split) │
│ └─ state gas for new slot creation (8037) │
│ └─ state refund for slot deletion (8037) │
│ │
│ CALL/CREATE: two-phase split │
│ └─ state gas for account creation (8037) │
│ └─ 63/64 rule applies to regular only │
│ └─ state gas passed whole to child │
│ └─ on revert: undo spill, depth-dependent │
│ │
│ Blob gas: NOT in EVM (tx-level only) │
└──────────┬──────────────────────────────────────┘
│
┌──────────▼──────────────────────────────────────┐
│ END OF TRANSACTION ACCOUNTING │
│ │
│ EIP-7778: Retrospective refund split │
│ ReceiptGasUsed = gasUsed - effectiveRefund │
│ BlockGasUsed = max(floor, gasUsed) ◄ no refund │
│ │
│ EIP-8037: Dimension split │
│ BlockRegularGasUsed = regularGasConsumed │
│ BlockStateGasUsed = stateGasConsumed │
│ BlockGasUsed = max(regular, state) │
│ │
│ EIP-4844: Independent │
│ BlobGasUsed = count × GasPerBlob │
│ │
│ Fee distribution (uses ReceiptGasUsed): │
│ tip = receiptGasUsed × effectiveTip → coinbase│
│ burn = receiptGasUsed × baseFee → burn addr │
│ blobFee = blobGas × blobBaseFee → burn addr │
│ refund = gasRemaining × gasPrice → sender │
│ │
│ Block validity: │
│ max(regularGasUsed, stateGasUsed) ≤ gasLimit │
│ blobGasUsed ≤ maxBlobGasPerBlock │
└─────────────────────────────────────────────────┘
7. Where Edge Cases Breed
The edge cases that are proving "time consuming" and "contradictory" cluster at three interaction points:
7.1 Spill + Revert: State gas borrows from regular, then execution reverts. Must undo the borrow. Different rules at depth 0 vs depth N:
- Depth > 0:
stateGasConsumed restored to parent's snapshot. Spill returned to gas.Regular.
- Depth 0 (top-level REVERT):
stateGasConsumed kept for block accounting. Spill tracked in revertedSpillGas.
- Depth 0 (exceptional halt): regular gas zeroed. State gas reservoir preserved.
7.2 Refund + State Gas + Block Accounting: EIP-7778 says block gas ignores refunds. EIP-8037 has state refunds. The combined formula requires revertedSpillGas to be correct or mdGasUsed.Total() is short.
7.3 Pre-State Validation + State Gas OOG: The stateless phase passes, the stateful phase determines a state gas cost, but payment fails due to spill exhaustion after the state read has already occurred.
7.4 Composition: A reverted CALL that spilled state gas to regular gas, inside a transaction that gets a state refund, checked against a block gas limit using max(regular, state). Getting all the counters right across every combination is what causes the fix-contradiction cycle.
Part II: Outline Design
Note: This design is presented for analysis purposes — to provide a concrete basis for sizing the migration and evaluating risks. Alternative approaches are possible, including lighter-weight refactors that address critical duplication without the full ledger abstraction.
8. Gas Ledger Pattern
Instead of threading MdGas through every function and manually tracking counters on the EVM, introduce a centralized GasLedger:
type GasDimension uint8
const (
Regular GasDimension = iota
State
Blob
NumDimensions
)
type GasVector [NumDimensions]uint64
type SpillRule struct {
From GasDimension // when this runs out...
To GasDimension // ...borrow from this
}
type GasLedger struct {
frames []GasFrame
// Block-level accumulators (never rolled back by inner frames)
blockUsed GasVector
// Spill rules (currently only State→Regular)
spills []SpillRule
// Per-dimension refund counters
refunds GasVector
}
type GasFrame struct {
available GasVector
initial GasVector // snapshot at frame entry (for spill reversal)
depth int
spillLog []spillEntry // records each spill as it happens
}
type spillEntry struct {
from, to GasDimension
amount uint64
}
Key operations:
// PushFrame — called at CALL/CREATE boundaries
func (l *GasLedger) PushFrame(gas MdGas, depth int)
// PopFrame — handles revert/halt/success in ONE place
// Replaces the duplicated logic in call() and create()
func (l *GasLedger) PopFrame(err error) MdGas
// Use — charge gas in a specific dimension with spill
func (l *GasLedger) Use(dim GasDimension, amount uint64) bool
// Refund — add to dimension-specific refund counter
func (l *GasLedger) Refund(dim GasDimension, amount uint64)
Benefits:
-
Revert/halt/success logic lives in ONE place (PopFrame) instead of being duplicated in call(), create(), and every opCall* instruction. The depth-0 vs depth-N distinction is handled once.
-
Spill log enables clean reversal — instead of computing childStateConsumed - reservoirUsed to figure out how much spilled, the frame records each spill as it happens. On revert, replay in reverse.
-
Block accounting is centralized — blockUsed is updated inside Use(), not scattered across 5+ locations in the interpreter loop.
-
New dimensions are configuration — if a future EIP adds compute gas or bandwidth gas, it's a new entry in GasDimension and possibly a new SpillRule, not threading a new uint64 through 46 files.
9. Typed Gas Returns from gasFunc
Split gasFunc into two types:
type gasFunc func(evm *EVM, ctx *CallContext, avail MdGas, memSize uint64) (MdGas, error)
type regularGasFunc func(evm *EVM, ctx *CallContext, avail uint64, memSize uint64) (uint64, error)
Only 5 functions ever return MdGas.State > 0:
| Function |
Where |
State Gas Source |
gasCreateEip3860 |
gas_table.go |
112 × cpsb for contract creation |
gasCreate2Eip3860 |
gas_table.go |
112 × cpsb for contract creation |
statefulGasCall |
gas_table.go |
112 × cpsb for new account (empty + value) |
makeGasSStoreFunc |
operations_acl.go |
32 × cpsb for new storage slot |
makeSelfdestructGasFn |
operations_acl.go |
112 × cpsb for selfdestruct to empty |
Everything else (17+ gas functions) is regular-only. The type split eliminates 44 MdGas{Regular: x} wrappers and 108 mdgas.MdGas{} zero-value returns.
10. Formalize Two-Phase Gas for BAL
Instead of ad-hoc function pairs (statelessGasCall/statefulGasCall), make the two phases a type-level contract:
type GasPhase1Result struct {
RegularGas uint64 // known without state
CanProceed bool // false = abort before state access
}
type GasPhase2Result struct {
RegularGas uint64 // additional regular gas from state check
StateGas uint64 // state creation gas (EIP-8037)
BALAccess []Address // addresses to record in BAL
}
type TwoPhaseGasFunc struct {
Stateless func(evm *EVM, ctx *CallContext, avail MdGas, memSize uint64) (GasPhase1Result, error)
Stateful func(evm *EVM, ctx *CallContext, avail MdGas, p1 GasPhase1Result) (GasPhase2Result, error)
}
This makes the EIP-7928 invariant explicit: Phase 1 determines affordability without state. Phase 2 is only called if Phase 1 succeeds. BAL recording is a return value, not a side effect.
11. Save/Restore Helper
The repeated pattern in instructions.go (6 copies) becomes a single helper:
func (evm *EVM) executeSubCall(scope *CallContext, stipend uint64,
fn func(gas MdGas) ([]byte, MdGas, error)) ([]byte, error) {
gas := MdGas{Regular: evm.CallGasTemp() + stipend, State: scope.stateGas}
scope.stateGas = 0
// Stipend doesn't count in block accounting
evm.ledger.AdjustBlockRegular(-int64(stipend))
ret, returnGas, err := fn(gas)
if err != nil {
scope.stateGas = /* restored by ledger.PopFrame */
} else {
scope.stateGas = returnGas.State
}
scope.refundGas(returnGas.Regular, ...)
return ret, err
}
12. Blob Gas Stays at Transaction Level
Blob gas never enters a GasFrame because it never enters the EVM. The GasLedger handles it at PushTransaction/PopTransaction only, preserving its clean independence.
Part III: Size Impact and Risk Assessment
13. Scope of Current eip-8037 Implementation
| Metric |
Count |
| Files changed (eip-8037 vs main) |
46 |
| Lines added/removed |
+1,162 / -898 |
MdGas{Regular: x} wrappers (no state gas, boilerplate) |
44 |
mdgas.MdGas{} zero-value returns (error paths) |
108 |
parentStateGas save/restore duplications |
12 references across 6 call/create ops |
childStateConsumed spill-reversal blocks |
6 (3 in call(), 3 in create()) |
regularGasConsumed += update sites |
13 |
stateGasConsumed += update sites |
14 |
| Skipped Amsterdam test fixtures |
14 (gas mismatches, receipt hash mismatches) |
The 14 skipped tests are the clearest signal — several show large gas discrepancies (e.g., gas used: 9897862, in header: 9841510), indicating structural accounting errors, not off-by-one issues.
14. Blast Radius Analysis
66 non-test files across the codebase import execution/vm:
| Category |
Files |
Impact from Refactor |
EVM core (execution/vm/) |
~15 |
Heavy — all gas functions, interpreter, evm.go |
State transition (execution/protocol/) |
6 |
Heavy — state_transition.go, block_exec.go |
Executors (execution/stagedsync/) |
4 |
Medium — exec3_serial.go, exec3_parallel.go |
Tracers (execution/tracing/) |
12 |
Low — use uint64 projection, not MdGas directly |
RPC (rpc/jsonrpc/) |
16 |
Low — consume ExecutionResult, not internal gas |
CLI/tools (cmd/) |
4 |
Low — t8ntool, integration, state runner |
| Builder |
2 |
Low — uses gas pool, not internal gas |
| Other (polygon, otterscan, etc.) |
7 |
Minimal — use vm.Config or error types |
Tracer compatibility: Tracers currently see uint64 only — state gas is projected away via startGas.Regular. All 12 tracer files, 16 RPC files, and external tracer consumers are unaffected by internal gas restructuring as long as this projection is preserved.
15. Migration Phases
Phase 1: Extract Helpers (Low Risk)
Scope: execution/vm/instructions.go, execution/vm/evm.go
What: Factor duplicated patterns into shared functions without changing any types or APIs.
1a — Save/Restore Helper: Replace 6 copies of the parentStateGas pattern with 1 helper called 6 times.
| Metric |
Value |
| Files touched |
1 (instructions.go) |
| Lines removed |
~60 (duplicated blocks) |
| Lines added |
~25 (helper + 6 one-line calls) |
| Risk |
Very low — pure refactor, no logic change |
| Test impact |
Existing tests validate; no new tests needed |
1b — Spill Reversal Helper: Replace 2 copies of the ~20-line spill reversal block in call() and create() with a shared function.
| Metric |
Value |
| Files touched |
1 (evm.go) |
| Lines removed |
~40 (duplicated blocks) |
| Lines added |
~25 (shared function) |
| Risk |
Low — same logic, one location. Any test failure reveals pre-existing divergence between the two copies. |
Phase 1 Total: 2 files, ~-50 net lines, low risk, no dependencies.
Phase 2: Introduce GasLedger (Medium Risk)
Scope: New gas_ledger.go, modifications to evm.go and interpreter.go
What: Create the GasLedger as an internal EVM component wrapping existing fields. Replace 27 scattered counter updates with ledger.Use() calls, and the frame save/restore with PushFrame/PopFrame.
| Metric |
Value |
| Files touched |
4 |
| Net lines |
~+60 |
| Risk |
Medium — the frame lifecycle is where all edge cases live |
| Merge conflict potential |
High with concurrent gas work on eip-8037 |
Phase 3: Typed Gas Functions (Low-Medium Risk)
Scope: gas_table.go, operations_acl.go, jump_table.go
What: Split gasFunc into regularGasFunc (returns uint64) and gasFunc (returns MdGas). Eliminate boilerplate wrappers.
| Metric |
Value |
| Files touched |
4 |
| Net lines |
~-80 |
| Risk |
Low — mechanical transformation, no behavioral change |
| Dependencies |
Can be done independently of Phase 2 |
Phase 4: Two-Phase Gas Formalization (Medium Risk)
Scope: operations_acl.go, gas_table.go
What: Replace ad-hoc stateless/stateful function pairs with formal TwoPhaseGasFunc type.
| Metric |
Value |
| Files touched |
3-4 |
| Lines changed |
~200 |
| Risk |
Medium — changes gas calculation structure |
| Dependencies |
Phase 3; should only be pursued if BAL recording bugs persist |
16. Size Summary
| Phase |
Files |
Lines Added |
Lines Removed |
Net |
| 1: Extract helpers |
2 |
50 |
100 |
-50 |
| 2: GasLedger |
4 |
197 |
137 |
+60 |
| 3: Typed gas functions |
4 |
40 |
120 |
-80 |
| 4: Two-phase formalization |
4 |
200 |
150 |
+50 |
| Total |
~8 |
~487 |
~477 |
~+10 |
The refactor is roughly code-neutral in size. With the GasLedger, the eip-8037 diff would shrink by ~280 lines (wrappers, zero returns, duplicated logic), from +1,162 to approximately +880 additions.
17. Risk Register
| ID |
Risk |
Severity |
Likelihood |
Mitigation |
| R1 |
Consensus divergence — different gas values than geth |
Critical |
Medium |
EEST fixtures must all pass; devnet comparison |
| R2 |
Tracer regression — incorrect gas in debug_trace* |
High |
Low |
Preserve uint64 projection; run tracer tests |
| R3 |
Merge conflicts — concurrent gas fixes on eip-8037 |
High |
High |
Land Phase 1 quickly; Phase 2 needs stable window |
| R4 |
Performance regression — method call overhead |
Medium |
Very low |
Pre-allocate frames; ~50µs/s overhead (negligible) |
| R5 |
Incomplete migration — two mental models |
Medium |
Medium |
Phase 1 is standalone valuable; Phase 2 is all-or-nothing |
| R6 |
Spill log correctness — wrong reversal order |
High |
Low |
Test all spill×revert×depth combinations; debug invariant |
| R7 |
Frame stack overflow — allocation pressure at depth 1024 |
Low |
Very low |
Pre-allocate to max call depth |
18. Decision Matrix
| Factor |
Phase 1 |
Phase 2 |
Phase 3 |
Phase 4 |
| Value |
High — eliminates most dangerous duplication |
High — centralizes edge case logic |
Medium — code cleanliness |
Medium — BAL correctness |
| Risk |
Very low |
Medium |
Low |
Medium |
| Urgency |
Now — while edge cases are being fixed |
Before Amsterdam hardfork |
Any time |
When BAL bugs surface |
| Dependencies |
None |
Phase 1 |
None |
Phase 3 |
| Merge conflict risk |
Low |
High |
Medium |
Medium |
| Revertable? |
Yes — expand inline |
Yes — but painful |
Yes — add wrappers back |
Yes — revert to ad-hoc |
19. Recommended Sequence
-
Phase 1 NOW — Extract helpers. Pure improvement with near-zero risk. Immediately reduces the surface area for contradicting edge case fixes. Can be reviewed and landed in a single PR.
-
Phase 3 NEXT — Typed gas functions. Independent of Phase 2, low risk, reduces boilerplate. Good warmup for reviewing gas structure changes.
-
Phase 2 AFTER STABILIZATION — GasLedger. The big win but needs a stable period on eip-8037. Ideally done when the current batch of skipped tests is resolved.
-
Phase 4 IF NEEDED — Two-phase formalization. Only pursue if BAL recording side-effects continue to cause bugs after Phases 1-3.
20. What Could Go Wrong
Worst case — Consensus fork: A gas accounting bug causes erigon to compute different header.GasUsed than other clients. Detected on devnet immediately. Recovery: revert the PR.
Likely case — Merge conflict hell: Phase 2 lands, then a concurrent gas fix on eip-8037 conflicts. The fix author must understand the GasLedger. Recovery: document invariants, add debug assertions.
Best case — Skipped tests start passing: If the duplicated spill reversal in call() and create() has diverged (which the skip list suggests), centralizing it may fix multiple tests at once. The skip list entries with large gas mismatches (e.g., 56K gas difference) are likely candidates.
Appendix: Key Files Reference
| Component |
File |
Purpose |
| MdGas type |
execution/vm/evmtypes/mdgas/md_gas.go |
Multi-dimensional gas struct |
| Interpreter loop |
execution/vm/interpreter.go |
Opcode dispatch, gas deduction |
| EVM call/create |
execution/vm/evm.go |
Call frame lifecycle, revert handling |
| Gas functions |
execution/vm/gas_table.go |
Per-opcode dynamic gas calculation |
| ACL gas functions |
execution/vm/operations_acl.go |
EIP-2929/7702/7928 gas with access lists |
| Instructions |
execution/vm/instructions.go |
Opcode implementations (CALL, CREATE, etc.) |
| Jump table |
execution/vm/jump_table.go |
gasFunc type definition, opcode→gas mapping |
| State transition |
execution/protocol/state_transition.go |
Transaction gas lifecycle, refund, 7778 |
| Gas pool |
execution/protocol/gaspool.go |
Block-level gas + blob gas pools |
| Block execution |
execution/protocol/block_exec.go |
Block validation, gas limit checks |
| State processor |
execution/protocol/state_processor.go |
GasUsed{Receipt,Block,Blob} accumulation |
| Intrinsic gas |
execution/protocol/fixedgas/intrinsic_gas.go |
Transaction intrinsic gas (regular + state) |
| CostPerStateByte |
execution/protocol/misc/eip8037.go |
Dynamic state gas pricing |
| Blob gas pricing |
execution/protocol/misc/eip4844.go |
Blob base fee calculation |
| Serial executor |
execution/stagedsync/exec3_serial.go |
Serial block execution gas tracking |
| Parallel executor |
execution/stagedsync/exec3_parallel.go |
Parallel block execution gas tracking |
| Access list |
execution/state/access_list.go |
EIP-2929 warm/cold tracking |
| Intra-block state |
execution/state/intra_block_state.go |
Refund counter, BAL access marking |
| Versioned IO |
execution/state/versionedio.go |
BAL construction from versioned reads/writes |
| BAL types |
execution/types/block_access_list.go |
Block access list data structures |
| Execution result |
execution/vm/evmtypes/evmtypes.go |
ExecutionResult with dual gas fields |
| Transaction pool |
txnprovider/txpool/pool.go |
Tx validation with MdGas |
| Gas constants |
execution/protocol/params/protocol.go |
All gas cost parameters |
| Chain config |
execution/chain/chain_config.go |
Blob config, fork rules |
Summary
Ethereum's Amsterdam fork introduces multi-dimensional gas (EIP-8037) alongside block gas accounting changes (EIP-7778) and block access lists (EIP-7928). Our current implementation on the
eip-8037branch threads a newMdGas{Regular, State}type through 46 files, duplicating critical save/restore and spill-reversal logic at 6 call/create sites and 2 call/create lifecycle functions. This duplication is the root cause of contradicting edge case fixes — the commit history shows a recurring cycle of "fix X breaks Y" as the duplicated blocks diverge.14 Amsterdam test fixtures are currently skipped, several showing large gas discrepancies (e.g., 56K gas mismatch), indicating structural accounting errors rather than off-by-one issues.
The risk analysis identifies extracting shared helpers (Phase 1) as a high-value, near-zero-risk first step that can be done immediately. It eliminates the most dangerous code duplication — the 6× save/restore pattern in instruction handlers and the 2× spill-reversal block in
call()/create()— without introducing new abstractions or changing any APIs. This alone would reduce the surface area where contradicting fixes can occur and may resolve some of the skipped tests by unifying logic that has silently diverged between the two copies.Further phases (centralized GasLedger, typed gas functions, formalized two-phase gas for BAL) offer increasing structural improvement but carry higher merge conflict risk with the actively-developed eip-8037 branch and should be sequenced after the current batch of gas fixes stabilizes.
Background
This document was produced to inform architectural decisions around multi-dimensional gas in Erigon. It combines:
The outline design is presented for analysis purposes — it illustrates one approach to centralizing gas lifecycle management and provides a concrete basis for sizing and risk evaluation. Alternative approaches are possible, including lighter-weight refactors that address the most critical duplication without introducing the full ledger abstraction. The phased migration strategy is specifically designed to allow stopping at any phase boundary if the cost/benefit shifts.
Part I: Architecture Analysis
1. The Three Gas Dimensions
block.gasLimitmaxBlobGasPerBlockCostPerStateByteper blockblock.gasLimitviamax(regular, state)Additionally, EIP-7778 (Amsterdam) splits regular gas into two accounting views — receipt gas (with refunds) and block gas (without refunds) — applied retrospectively at the end of transaction execution.
2. EIP Specifications
2.1 EIP-7778: Block Gas Accounting Without Refunds
Problem: Gas refunds reduce both user costs and block gas accounting, allowing blocks to exceed their intended computational limits. Block 20,878,522 showed 28.5 MGas net usage but 32.51 MGas gross, exceeding the block limit.
Solution: Split gas accounting into two views of the same execution:
gas_spent = max(tx_gas_used - gas_refund, calldata_floor_gas_cost)block.gas_used += max(tx_gas_used, calldata_floor_gas_cost)Key property: This is a retrospective divergence — during execution there's only one gas counter. The split happens purely at the accounting stage after execution completes. This is why 7778 was relatively clean to implement.
Implementation in
state_transition.go:639-667:Fee calculations use ReceiptGasUsed (with refunds) — so the validator gets paid based on the refunded amount, while the block limit is enforced on the non-refunded amount.
2.2 EIP-7928: Block-Level Access Lists (BAL)
Core change: Two-phase gas validation:
Critical rule: "Pre-state validation MUST pass before any state access occurs. If pre-state validation fails, the target resource is never accessed" and therefore excluded from the BAL.
BAL recording: Only addresses and storage slots actually touched are recorded. EIP-2930 access list entries are NOT automatically included.
Block constraint:
bal_items ≤ block_gas_limit ÷ 20002.3 EIP-8037: State Creation Gas Cost Increase (Multi-Dimensional Gas)
Problem: State creation costs are too low relative to their long-term impact. As block limits increase, state growth accelerates unsustainably.
Solution: Introduce a second gas dimension tracked during EVM execution:
Dynamic pricing via
cost_per_state_bytecalculated per block from gas limit, targeting 100 GiB/year state growth at 50% utilization. Currently hardcoded to 1174 for bal-devnet-3.Key properties:
max(regular_gas_used, state_gas_used) ≤ gas_limitMaxTxnGasLimit— the excess becomes the state gas reservoir3. Current Implementation Architecture
3.1 Blob Gas (Clean, Independent)
Blob gas is the cleanest model — genuinely independent of the EVM:
Why it works well:
GasPool.blobGas)FakeExponential)maxFeePerBlobGas × blobGas)blobGasUsed ≤ maxBlobGasPerBlock3.2 EIP-7778 Implementation (Retrospective Split)
The
ExecutionResultstruct stores both accounting views:On the eip-8037 branch,
BlockGasUsedfurther splits intoBlockRegularGasUsedandBlockStateGasUsed, with the block-level value beingmax(regular, state).3.3 State Gas Implementation (Entangled with Regular)
On the
eip-8037branch, state gas is threaded through the entire EVM:New type —
execution/vm/evmtypes/mdgas/md_gas.go:With spill logic:
MinusStateGas(sg)— if state gas insufficient, overflow charged from regular gas.Key changes:
CallContextgainsstateGas uint64alongside existinggas uint64Run()signature changes fromgas uint64togas MdGasCall/Createsignatures take and returnMdGasstateGasConsumed,regularGasConsumed,revertedSpillGasgasFuncreturnsMdGasinstead ofuint64uint64toMdGas4. The Stateful vs Stateless Gas Split
4.1 Current Two-Phase Pattern
The codebase already separates gas calculation into two phases for CALL variants, introduced for EIP-7702 delegation resolution:
statelessGasCall()statefulGasCall()Empty()check)calcCallGas()Composed in
makeCallVariantGasCallEIP7702()(operations_acl.go:290):4.2 Opcode Classification by Gas State Dependency
Fully stateless gas (never needs state to compute cost):
Two-phase gas (can split stateless/stateful):
Empty()for account creationEmpty()+GetBalance()for creation gasInherently stateful gas (cost depends on state values):
SSTORE is the only opcode whose gas cost is truly state-dependent. Everything else can determine gas from the access list (warm/cold) plus stack arguments.
4.3 BAL Recording Implications
Even if gas is stateless, BAL recording depends on whether the operation actually proceeds:
Empty()check (state) → if OOG on cold gas, don't record in BALEmpty()→ do record in BALThis is why
MarkReadsInternal()and therevertableflag exist — to distinguish "I read state for gas calculation purposes" from "I actually performed the operation."4.4 EIP-8037 Complication for BAL
With state gas, the CALL
statefulGasCallnow returnsMdGas{State: 112 × cpsb}when creating a new account. The spill mechanism means that even though the cost is classified as "state gas," the deduction may come from regular gas. If the spill causes OOG on regular gas, the operation fails — but the state read (Empty()) already happened.This is correct for BAL purposes (the access is recorded) and satisfies EIP-7928's pre-state validation requirement (the stateless phase passed before the state read). But it creates an asymmetry where the payment fails due to cross-dimension spill after the state access.
5. Structural Problems
5.1 State Gas Lives in Two Places with Different Semantics
CallContexthasgas uint64andstateGas uint64as separate fields, butMdGasis the struct passed across call boundaries. Every call/create entry point must manually saveparentStateGas, zeroscope.stateGas, then conditionally restore on error. This save/restore pattern is duplicated 6 times (opCall, opCallCode, opDelegateCall, opStaticCall, opCreate, opCreate2) with identical logic:5.2 Spill Logic is Duplicated and Error-Prone
The "state gas overflows into regular gas" spill happens in
useMdGas()(the generic helper), but the reversal of spill on revert is duplicated in bothevm.call()andevm.create()with nearly identical ~20-line blocks:Any bug in one copy but not the other creates edge cases. The commit history tells the story: "fix restorations on revert/halts", "fix spill and pass on state gas cost in call", "adjust regularGasConsumed tracking", "fix exceptional halt".
5.3 Block-Level Accounting is Scattered
regularGasConsumedis updated in 5+ places:evm.call()regularGasConsumed -= CallStipend)This makes it very hard to verify the invariant:
regularGasConsumed + leftoverRegular == initialRegularGas.5.4 The gasFunc Signature Change is All-or-Nothing
Every
gasFuncnow returnsMdGaseven for pre-Amsterdam operations that only ever useRegular. This means 30+ gas functions wrap their return inmdgas.MdGas{Regular: gas}boilerplate. The type signature doesn't distinguish between "this function can return state gas" and "this function never does."5.5 Revert Semantics Differ by Depth
At depth 0, state gas is kept consumed (for block accounting) but spill is restored. At depth > 0,
stateGasConsumedis rolled back. This depth-dependent behavior is the primary source of contradicting edge case fixes.5.6 The Three-Way Refund Interaction
EIP-7778 says block gas ignores refunds. EIP-8037 has state refunds. The refund total is
min(gasUsed/5, regularRefund + stateRefund)applied as a single scalar. ButgasUsednow includes spilled state gas. TherevertedSpillGasfield patches this:6. The Full Gas Flow
7. Where Edge Cases Breed
The edge cases that are proving "time consuming" and "contradictory" cluster at three interaction points:
7.1 Spill + Revert: State gas borrows from regular, then execution reverts. Must undo the borrow. Different rules at depth 0 vs depth N:
stateGasConsumedrestored to parent's snapshot. Spill returned togas.Regular.stateGasConsumedkept for block accounting. Spill tracked inrevertedSpillGas.7.2 Refund + State Gas + Block Accounting: EIP-7778 says block gas ignores refunds. EIP-8037 has state refunds. The combined formula requires
revertedSpillGasto be correct ormdGasUsed.Total()is short.7.3 Pre-State Validation + State Gas OOG: The stateless phase passes, the stateful phase determines a state gas cost, but payment fails due to spill exhaustion after the state read has already occurred.
7.4 Composition: A reverted CALL that spilled state gas to regular gas, inside a transaction that gets a state refund, checked against a block gas limit using
max(regular, state). Getting all the counters right across every combination is what causes the fix-contradiction cycle.Part II: Outline Design
8. Gas Ledger Pattern
Instead of threading
MdGasthrough every function and manually tracking counters on the EVM, introduce a centralized GasLedger:Key operations:
Benefits:
Revert/halt/success logic lives in ONE place (
PopFrame) instead of being duplicated incall(),create(), and everyopCall*instruction. The depth-0 vs depth-N distinction is handled once.Spill log enables clean reversal — instead of computing
childStateConsumed - reservoirUsedto figure out how much spilled, the frame records each spill as it happens. On revert, replay in reverse.Block accounting is centralized —
blockUsedis updated insideUse(), not scattered across 5+ locations in the interpreter loop.New dimensions are configuration — if a future EIP adds compute gas or bandwidth gas, it's a new entry in
GasDimensionand possibly a newSpillRule, not threading a newuint64through 46 files.9. Typed Gas Returns from gasFunc
Split
gasFuncinto two types:Only 5 functions ever return
MdGas.State > 0:gasCreateEip3860112 × cpsbfor contract creationgasCreate2Eip3860112 × cpsbfor contract creationstatefulGasCall112 × cpsbfor new account (empty + value)makeGasSStoreFunc32 × cpsbfor new storage slotmakeSelfdestructGasFn112 × cpsbfor selfdestruct to emptyEverything else (17+ gas functions) is regular-only. The type split eliminates 44
MdGas{Regular: x}wrappers and 108mdgas.MdGas{}zero-value returns.10. Formalize Two-Phase Gas for BAL
Instead of ad-hoc function pairs (
statelessGasCall/statefulGasCall), make the two phases a type-level contract:This makes the EIP-7928 invariant explicit: Phase 1 determines affordability without state. Phase 2 is only called if Phase 1 succeeds. BAL recording is a return value, not a side effect.
11. Save/Restore Helper
The repeated pattern in instructions.go (6 copies) becomes a single helper:
12. Blob Gas Stays at Transaction Level
Blob gas never enters a
GasFramebecause it never enters the EVM. TheGasLedgerhandles it atPushTransaction/PopTransactiononly, preserving its clean independence.Part III: Size Impact and Risk Assessment
13. Scope of Current eip-8037 Implementation
MdGas{Regular: x}wrappers (no state gas, boilerplate)mdgas.MdGas{}zero-value returns (error paths)parentStateGassave/restore duplicationschildStateConsumedspill-reversal blockscall(), 3 increate())regularGasConsumed +=update sitesstateGasConsumed +=update sitesThe 14 skipped tests are the clearest signal — several show large gas discrepancies (e.g.,
gas used: 9897862, in header: 9841510), indicating structural accounting errors, not off-by-one issues.14. Blast Radius Analysis
66 non-test files across the codebase import
execution/vm:execution/vm/)execution/protocol/)execution/stagedsync/)execution/tracing/)uint64projection, not MdGas directlyrpc/jsonrpc/)cmd/)Tracer compatibility: Tracers currently see
uint64only — state gas is projected away viastartGas.Regular. All 12 tracer files, 16 RPC files, and external tracer consumers are unaffected by internal gas restructuring as long as this projection is preserved.15. Migration Phases
Phase 1: Extract Helpers (Low Risk)
Scope:
execution/vm/instructions.go,execution/vm/evm.goWhat: Factor duplicated patterns into shared functions without changing any types or APIs.
1a — Save/Restore Helper: Replace 6 copies of the parentStateGas pattern with 1 helper called 6 times.
instructions.go)1b — Spill Reversal Helper: Replace 2 copies of the ~20-line spill reversal block in
call()andcreate()with a shared function.evm.go)Phase 1 Total: 2 files, ~-50 net lines, low risk, no dependencies.
Phase 2: Introduce GasLedger (Medium Risk)
Scope: New
gas_ledger.go, modifications toevm.goandinterpreter.goWhat: Create the GasLedger as an internal EVM component wrapping existing fields. Replace 27 scattered counter updates with
ledger.Use()calls, and the frame save/restore withPushFrame/PopFrame.Phase 3: Typed Gas Functions (Low-Medium Risk)
Scope:
gas_table.go,operations_acl.go,jump_table.goWhat: Split
gasFuncintoregularGasFunc(returnsuint64) andgasFunc(returnsMdGas). Eliminate boilerplate wrappers.Phase 4: Two-Phase Gas Formalization (Medium Risk)
Scope:
operations_acl.go,gas_table.goWhat: Replace ad-hoc stateless/stateful function pairs with formal
TwoPhaseGasFunctype.16. Size Summary
The refactor is roughly code-neutral in size. With the GasLedger, the eip-8037 diff would shrink by ~280 lines (wrappers, zero returns, duplicated logic), from +1,162 to approximately +880 additions.
17. Risk Register
uint64projection; run tracer tests18. Decision Matrix
19. Recommended Sequence
Phase 1 NOW — Extract helpers. Pure improvement with near-zero risk. Immediately reduces the surface area for contradicting edge case fixes. Can be reviewed and landed in a single PR.
Phase 3 NEXT — Typed gas functions. Independent of Phase 2, low risk, reduces boilerplate. Good warmup for reviewing gas structure changes.
Phase 2 AFTER STABILIZATION — GasLedger. The big win but needs a stable period on eip-8037. Ideally done when the current batch of skipped tests is resolved.
Phase 4 IF NEEDED — Two-phase formalization. Only pursue if BAL recording side-effects continue to cause bugs after Phases 1-3.
20. What Could Go Wrong
Worst case — Consensus fork: A gas accounting bug causes erigon to compute different
header.GasUsedthan other clients. Detected on devnet immediately. Recovery: revert the PR.Likely case — Merge conflict hell: Phase 2 lands, then a concurrent gas fix on eip-8037 conflicts. The fix author must understand the GasLedger. Recovery: document invariants, add debug assertions.
Best case — Skipped tests start passing: If the duplicated spill reversal in
call()andcreate()has diverged (which the skip list suggests), centralizing it may fix multiple tests at once. The skip list entries with large gas mismatches (e.g., 56K gas difference) are likely candidates.Appendix: Key Files Reference
execution/vm/evmtypes/mdgas/md_gas.goexecution/vm/interpreter.goexecution/vm/evm.goexecution/vm/gas_table.goexecution/vm/operations_acl.goexecution/vm/instructions.goexecution/vm/jump_table.goexecution/protocol/state_transition.goexecution/protocol/gaspool.goexecution/protocol/block_exec.goexecution/protocol/state_processor.goexecution/protocol/fixedgas/intrinsic_gas.goexecution/protocol/misc/eip8037.goexecution/protocol/misc/eip4844.goexecution/stagedsync/exec3_serial.goexecution/stagedsync/exec3_parallel.goexecution/state/access_list.goexecution/state/intra_block_state.goexecution/state/versionedio.goexecution/types/block_access_list.goexecution/vm/evmtypes/evmtypes.gotxnprovider/txpool/pool.goexecution/protocol/params/protocol.goexecution/chain/chain_config.go