From c1b1faed8b93ea3c46ddaff09651718c3016b96c Mon Sep 17 00:00:00 2001 From: MariusVanDerWijden Date: Tue, 28 Apr 2026 19:29:17 +0200 Subject: [PATCH] core/state: use journaling approach --- core/state/journal.go | 60 ++++++++++++++++++------------------ core/state/statedb.go | 9 +++--- core/state/statedb_hooked.go | 4 +-- core/state_transition.go | 9 +++--- core/vm/evm.go | 19 +++--------- core/vm/interface.go | 6 ++-- 6 files changed, 47 insertions(+), 60 deletions(-) diff --git a/core/state/journal.go b/core/state/journal.go index c80efa93db..b189d54427 100644 --- a/core/state/journal.go +++ b/core/state/journal.go @@ -216,33 +216,15 @@ func (j *journal) length() int { return len(j.entries) } -// stateChangedBytes computes the state bytes created by the call frame -// identified by snapshotId. Since subcalls always compute their results -// before the parent (innermost-first), this only scans journal entries -// between this snapshot and the next one — the frame's own entries. -// Subcall results are summed from the cache and subtracted. -// -// The result is cached in stateBytesCharged[snapshotId] so the parent -// frame can look it up instead of re-scanning. -func (j *journal) stateChangedBytes(snapshotId int, stateObjects map[common.Address]*stateObject) int64 { - // TODO (MariusVanDerWijden): this is a bit slop-py needs to be cleaned up - // Resolve snapshot index. - idx := sort.Search(len(j.validRevisions), func(i int) bool { - return j.validRevisions[i].id >= snapshotId - }) - if idx == len(j.validRevisions) || j.validRevisions[idx].id != snapshotId { - panic(fmt.Errorf("snapshot id %v not found for stateChangedBytes", snapshotId)) +// stateChangedBytes computes the state bytes created by the current (topmost) +// call frame, walking only entries that belong directly to this frame and +// skipping over closed child frame ranges. +func (j *journal) stateChangedBytes(stateObjects map[common.Address]*stateObject) int64 { + if len(j.validRevisions) == 0 { + return 0 } - start := j.validRevisions[idx].journalIndex + rev := j.validRevisions[len(j.validRevisions)-1] - // Our range is [start, end) where end is the next revision's start, - // or the end of the journal if we're the last revision. - end := len(j.entries) - if idx+1 < len(j.validRevisions) { - end = j.validRevisions[idx+1].journalIndex - } - - // Walk only our own entries. type slotKey struct { addr common.Address key common.Hash @@ -255,8 +237,11 @@ func (j *journal) stateChangedBytes(snapshotId int, stateObjects map[common.Addr created := make(map[common.Address]bool) codeChanged := make(map[common.Address]bool) - for i := start; i < end; i++ { - switch e := j.entries[i].(type) { + // Walk only this frame's own entries, skipping closed child ranges. + // Add cached subcall costs from closedChildren. + var subcallBytes int64 + visit := func(e journalEntry) { + switch e := e.(type) { case createContractChange: created[e.account] = true case codeChange: @@ -268,6 +253,18 @@ func (j *journal) stateChangedBytes(snapshotId int, stateObjects map[common.Addr } } } + idx := rev.journalIndex + for _, child := range rev.closedChildren { + for ; idx < child.start; idx++ { + visit(j.entries[idx]) + } + // Add the cached cost for this subcall. + subcallBytes += j.stateBytesCharged[child.start] + idx = child.end + } + for ; idx < len(j.entries); idx++ { + visit(j.entries[idx]) + } var totalBytes int64 for range created { @@ -296,7 +293,7 @@ func (j *journal) stateChangedBytes(snapshotId int, stateObjects map[common.Addr // cleared in earlier frame, re-set here — no charge. // - X → Y (non-zero to non-zero): no charge. // - zero → zero: no change. - // - !prevZero && curZero && !origZero: pre-exising slot was + // - !prevZero && curZero && !origZero: pre-existing slot was // cleared now, don't refund to not enable gas tokens. } for addr := range codeChanged { @@ -306,8 +303,11 @@ func (j *journal) stateChangedBytes(snapshotId int, stateObjects map[common.Addr } } - // Cache our result so the parent can look it up. - j.stateBytesCharged[snapshotId] = totalBytes + // Add subcall costs to get the total for this frame (own + children). + totalBytes += subcallBytes + + // Cache so the parent can look up this frame's total cost. + j.stateBytesCharged[rev.journalIndex] = totalBytes return totalBytes } diff --git a/core/state/statedb.go b/core/state/statedb.go index 875bde5d5a..5624c0c686 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -772,11 +772,10 @@ const ( CostPerSlot = 32 ) -// StateChangedBytes computes the state bytes created since the given snapshot, -// excluding bytes already charged by subcalls. See journal.stateChangedBytes -// for the detailed accounting. -func (s *StateDB) StateChangedBytes(snapshotId int) int64 { - return s.journal.stateChangedBytes(snapshotId, s.stateObjects) +// StateChangedBytes computes the state bytes created by the current (topmost) +// call frame, excluding entries from closed child frames. +func (s *StateDB) StateChangedBytes() int64 { + return s.journal.stateChangedBytes(s.stateObjects) } type removedAccountWithBalance struct { diff --git a/core/state/statedb_hooked.go b/core/state/statedb_hooked.go index 8886cacb70..b1d05c30a2 100644 --- a/core/state/statedb_hooked.go +++ b/core/state/statedb_hooked.go @@ -293,6 +293,6 @@ func (s *hookedStateDB) Finalise(deleteEmptyObjects bool) *bal.StateAccessList { return s.inner.Finalise(deleteEmptyObjects) } -func (s *hookedStateDB) StateChangedBytes(snapshotId int) int64 { - return s.inner.StateChangedBytes(snapshotId) +func (s *hookedStateDB) StateChangedBytes() int64 { + return s.inner.StateChangedBytes() } diff --git a/core/state_transition.go b/core/state_transition.go index afeb1dd453..932699acd1 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -598,10 +598,9 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { ret []byte vmerr error // vm errors do not effect consensus and are therefore not assigned to err ) - // EIP-8037: Take a snapshot for the outer call frame so we can compute - // state gas for state changes made at the transaction level (nonce, - // value transfer, authorizations, and contract creation overhead). - outerSnapshot := st.state.Snapshot() + + // Take a snapshot for gas calculation + st.state.Snapshot() var execGasUsed vm.GasUsed if contractCreation { @@ -634,7 +633,7 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { // EIP-8037: charge state gas for the outer call frame's own state changes. if rules.IsAmsterdam { if vmerr == nil { - outerBytes := st.state.StateChangedBytes(outerSnapshot) + outerBytes := st.state.StateChangedBytes() st.gasRemaining.Charge(vm.GasCosts{StateGas: outerBytes * int64(st.evm.Context.CostPerStateByte)}) } else { if execGasUsed.StateGas > 0 { diff --git a/core/vm/evm.go b/core/vm/evm.go index 1cf78814e7..cb14a3743b 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -281,11 +281,6 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g } evm.StateDB.CreateAccount(addr) } - if evm.chainRules.IsAmsterdam { - // Compute state changed bytes for account creation. - evm.StateDB.StateChangedBytes(snapshot) - } - innerSnapshot := evm.StateDB.Snapshot() // Perform the value transfer only in non-syscall mode. // Calling this is required even for zero-value transfers, // to ensure the state clearing mechanism is applied. @@ -327,7 +322,7 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g evm.StateDB.CloseSnapshot(snapshot) if evm.chainRules.IsAmsterdam { // Charge state costs - bytesCharged := evm.StateDB.StateChangedBytes(innerSnapshot) + bytesCharged := evm.StateDB.StateChangedBytes() stateGasCost := GasCosts{StateGas: bytesCharged * int64(evm.Context.CostPerStateByte)} if !gas.CanAfford(stateGasCost) { gas.Exhaust() @@ -389,7 +384,7 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt } else { evm.StateDB.CloseSnapshot(snapshot) if evm.chainRules.IsAmsterdam { - bytesCharged := evm.StateDB.StateChangedBytes(snapshot) + bytesCharged := evm.StateDB.StateChangedBytes() stateGasCost := GasCosts{StateGas: bytesCharged * int64(evm.Context.CostPerStateByte)} if !gas.CanAfford(stateGasCost) { gas.Exhaust() @@ -444,7 +439,7 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address, } else { evm.StateDB.CloseSnapshot(snapshot) if evm.chainRules.IsAmsterdam { - bytesCharged := evm.StateDB.StateChangedBytes(snapshot) + bytesCharged := evm.StateDB.StateChangedBytes() stateGasCost := GasCosts{StateGas: bytesCharged * int64(evm.Context.CostPerStateByte)} if !gas.CanAfford(stateGasCost) { gas.Exhaust() @@ -599,12 +594,6 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value } evm.Context.Transfer(evm.StateDB, caller, address, value, &evm.chainRules) - if evm.chainRules.IsAmsterdam { - // Compute the state changed for the contract init. - evm.StateDB.StateChangedBytes(snapshot) - } - initSnapshot := evm.StateDB.Snapshot() - // Initialise a new contract and set the code that is to be used by the EVM. // The contract is a scoped environment for this execution context only. contract := NewContract(caller, address, value, gas, evm.jumpDests) @@ -624,7 +613,7 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value evm.StateDB.CloseSnapshot(snapshot) if evm.chainRules.IsAmsterdam { // Charge initcode's state changes to the created contract's gas. - bytesCharged := evm.StateDB.StateChangedBytes(initSnapshot) + bytesCharged := evm.StateDB.StateChangedBytes() stateGasCost := GasCosts{StateGas: bytesCharged * int64(evm.Context.CostPerStateByte)} if !contract.Gas.CanAfford(stateGasCost) { contract.Gas.Exhaust() diff --git a/core/vm/interface.go b/core/vm/interface.go index 9c132061c6..3c5f4acd6c 100644 --- a/core/vm/interface.go +++ b/core/vm/interface.go @@ -106,7 +106,7 @@ type StateDB interface { // Finalise must be invoked at the end of a transaction Finalise(bool) *bal.StateAccessList - // StateChangedBytes returns the number of state bytes created since the - // given snapshot. Used by EIP-8037 for state gas metering. - StateChangedBytes(snapshotId int) int64 + // StateChangedBytes returns the number of state bytes created by the + // current call frame. Used by EIP-8037 for state gas metering. + StateChangedBytes() int64 }