diff --git a/cmd/evm/internal/t8ntool/transaction.go b/cmd/evm/internal/t8ntool/transaction.go index 138db990e5..d8eb86523a 100644 --- a/cmd/evm/internal/t8ntool/transaction.go +++ b/cmd/evm/internal/t8ntool/transaction.go @@ -133,7 +133,8 @@ func Transaction(ctx *cli.Context) error { } // Check intrinsic gas rules := chainConfig.Rules(common.Big0, true, 0) - cost, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules) + gasCostPerStateByte := core.CostPerStateByte(&types.Header{}, chainConfig) + cost, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules, gasCostPerStateByte) if err != nil { r.Error = err results = append(results, r) diff --git a/core/bench_test.go b/core/bench_test.go index 2edec9104d..b0cbc0ec4e 100644 --- a/core/bench_test.go +++ b/core/bench_test.go @@ -89,7 +89,7 @@ func genValueTx(nbytes int) func(int, *BlockGen) { data := make([]byte, nbytes) return func(i int, gen *BlockGen) { toaddr := common.Address{} - cost, _ := IntrinsicGas(data, nil, nil, false, params.Rules{}) + cost, _ := IntrinsicGas(data, nil, nil, false, params.Rules{}, 1) signer := gen.Signer() gasPrice := big.NewInt(0) if gen.header.BaseFee != nil { diff --git a/core/bintrie_witness_test.go b/core/bintrie_witness_test.go index da0f7cfb27..89074236f9 100644 --- a/core/bintrie_witness_test.go +++ b/core/bintrie_witness_test.go @@ -63,12 +63,12 @@ var ( func TestProcessUBT(t *testing.T) { var ( code = common.FromHex(`6060604052600a8060106000396000f360606040526008565b00`) - intrinsicContractCreationGas, _ = IntrinsicGas(code, nil, nil, true, params.TestRules) + intrinsicContractCreationGas, _ = IntrinsicGas(code, nil, nil, true, params.TestRules, 1) // A contract creation that calls EXTCODECOPY in the constructor. Used to ensure that the witness // will not contain that copied data. // Source: https://gist.github.com/gballet/a23db1e1cb4ed105616b5920feb75985 codeWithExtCodeCopy = common.FromHex(`0x60806040526040516100109061017b565b604051809103906000f08015801561002c573d6000803e3d6000fd5b506000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555034801561007857600080fd5b5060008067ffffffffffffffff8111156100955761009461024a565b5b6040519080825280601f01601f1916602001820160405280156100c75781602001600182028036833780820191505090505b50905060008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1690506020600083833c81610101906101e3565b60405161010d90610187565b61011791906101a3565b604051809103906000f080158015610133573d6000803e3d6000fd5b50600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550505061029b565b60d58061046783390190565b6102068061053c83390190565b61019d816101d9565b82525050565b60006020820190506101b86000830184610194565b92915050565b6000819050602082019050919050565b600081519050919050565b6000819050919050565b60006101ee826101ce565b826101f8846101be565b905061020381610279565b925060208210156102435761023e7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8360200360080261028e565b831692505b5050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b600061028582516101d9565b80915050919050565b600082821b905092915050565b6101bd806102aa6000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c8063f566852414610030575b600080fd5b61003861004e565b6040516100459190610146565b60405180910390f35b6000600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff166381ca91d36040518163ffffffff1660e01b815260040160206040518083038186803b1580156100b857600080fd5b505afa1580156100cc573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100f0919061010a565b905090565b60008151905061010481610170565b92915050565b6000602082840312156101205761011f61016b565b5b600061012e848285016100f5565b91505092915050565b61014081610161565b82525050565b600060208201905061015b6000830184610137565b92915050565b6000819050919050565b600080fd5b61017981610161565b811461018457600080fd5b5056fea2646970667358221220a6a0e11af79f176f9c421b7b12f441356b25f6489b83d38cc828a701720b41f164736f6c63430008070033608060405234801561001057600080fd5b5060b68061001f6000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c8063ab5ed15014602d575b600080fd5b60336047565b604051603e9190605d565b60405180910390f35b60006001905090565b6057816076565b82525050565b6000602082019050607060008301846050565b92915050565b600081905091905056fea26469706673582212203a14eb0d5cd07c277d3e24912f110ddda3e553245a99afc4eeefb2fbae5327aa64736f6c63430008070033608060405234801561001057600080fd5b5060405161020638038061020683398181016040528101906100329190610063565b60018160001c6100429190610090565b60008190555050610145565b60008151905061005d8161012e565b92915050565b60006020828403121561007957610078610129565b5b60006100878482850161004e565b91505092915050565b600061009b826100f0565b91506100a6836100f0565b9250827fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff038211156100db576100da6100fa565b5b828201905092915050565b6000819050919050565b6000819050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b600080fd5b610137816100e6565b811461014257600080fd5b50565b60b3806101536000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c806381ca91d314602d575b600080fd5b60336047565b604051603e9190605a565b60405180910390f35b60005481565b6054816073565b82525050565b6000602082019050606d6000830184604d565b92915050565b600081905091905056fea26469706673582212209bff7098a2f526de1ad499866f27d6d0d6f17b74a413036d6063ca6a0998ca4264736f6c63430008070033`) - intrinsicCodeWithExtCodeCopyGas, _ = IntrinsicGas(codeWithExtCodeCopy, nil, nil, true, params.TestRules) + intrinsicCodeWithExtCodeCopyGas, _ = IntrinsicGas(codeWithExtCodeCopy, nil, nil, true, params.TestRules, 1) signer = types.LatestSigner(testUBTChainConfig) testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") bcdb = rawdb.NewMemoryDatabase() // Database for the blockchain diff --git a/core/evm.go b/core/evm.go index ce5cf801dc..917ce296b7 100644 --- a/core/evm.go +++ b/core/evm.go @@ -68,18 +68,19 @@ func NewEVMBlockContext(header *types.Header, chain ChainContext, author *common } return vm.BlockContext{ - CanTransfer: CanTransfer, - Transfer: Transfer, - GetHash: GetHashFn(header, chain), - Coinbase: beneficiary, - BlockNumber: new(big.Int).Set(header.Number), - Time: header.Time, - Difficulty: new(big.Int).Set(header.Difficulty), - BaseFee: baseFee, - BlobBaseFee: blobBaseFee, - GasLimit: header.GasLimit, - Random: random, - SlotNum: slotNum, + CanTransfer: CanTransfer, + Transfer: Transfer, + GetHash: GetHashFn(header, chain), + Coinbase: beneficiary, + BlockNumber: new(big.Int).Set(header.Number), + Time: header.Time, + Difficulty: new(big.Int).Set(header.Difficulty), + BaseFee: baseFee, + BlobBaseFee: blobBaseFee, + GasLimit: header.GasLimit, + Random: random, + SlotNum: slotNum, + CostPerStateByte: CostPerStateByte(header, chain.Config()), } } diff --git a/core/gaspool.go b/core/gaspool.go index 14f5abd93c..7cab36307e 100644 --- a/core/gaspool.go +++ b/core/gaspool.go @@ -27,6 +27,11 @@ type GasPool struct { remaining uint64 initial uint64 cumulativeUsed uint64 + + // EIP-8037: per-dimension cumulative sums for Amsterdam. + // Block gas used = max(cumulativeRegular, cumulativeState). + cumulativeRegular uint64 + cumulativeState uint64 } // NewGasPool initializes the gasPool with the given amount. @@ -68,20 +73,44 @@ func (gp *GasPool) ReturnGas(returned uint64, gasUsed uint64) error { return nil } +// ReturnGasAmsterdam calculates the new remaining gas in the pool after the +// execution of a message. +func (gp *GasPool) ReturnGasAmsterdam(txRegular, txState, receiptGasUsed uint64) error { + gp.cumulativeRegular += txRegular + gp.cumulativeState += txState + gp.cumulativeUsed += receiptGasUsed + + blockUsed := max(gp.cumulativeRegular, gp.cumulativeState) + if gp.initial < blockUsed { + return fmt.Errorf("%w: block gas overflow: initial %d, used %d (regular: %d, state: %d)", + ErrGasLimitReached, gp.initial, blockUsed, gp.cumulativeRegular, gp.cumulativeState) + } + // TX inclusion: only the regular dimension is checked when deciding + // whether the next transaction fits. + gp.remaining = gp.initial - gp.cumulativeRegular + return nil +} + // Gas returns the amount of gas remaining in the pool. func (gp *GasPool) Gas() uint64 { return gp.remaining } -// CumulativeUsed returns the amount of cumulative consumed gas (refunded included). +// CumulativeUsed returns the cumulative gas consumed for receipt tracking. +// For Amsterdam blocks, this is the sum of per-tx tx_gas_used_after_refund +// (what users pay), not the 2D block-level metric. func (gp *GasPool) CumulativeUsed() uint64 { return gp.cumulativeUsed } -// Used returns the amount of consumed gas. +// Used returns the amount of consumed gas. For Amsterdam blocks with +// 2D gas accounting (EIP-8037), returns max(sum_regular, sum_state). func (gp *GasPool) Used() uint64 { + if gp.cumulativeRegular > 0 || gp.cumulativeState > 0 { + return max(gp.cumulativeRegular, gp.cumulativeState) + } if gp.initial < gp.remaining { - panic("gas used underflow") + panic(fmt.Sprintf("gas used underflow: %v %v", gp.initial, gp.remaining)) } return gp.initial - gp.remaining } @@ -89,9 +118,11 @@ func (gp *GasPool) Used() uint64 { // Snapshot returns the deep-copied object as the snapshot. func (gp *GasPool) Snapshot() *GasPool { return &GasPool{ - initial: gp.initial, - remaining: gp.remaining, - cumulativeUsed: gp.cumulativeUsed, + initial: gp.initial, + remaining: gp.remaining, + cumulativeUsed: gp.cumulativeUsed, + cumulativeRegular: gp.cumulativeRegular, + cumulativeState: gp.cumulativeState, } } @@ -100,6 +131,8 @@ func (gp *GasPool) Set(other *GasPool) { gp.initial = other.initial gp.remaining = other.remaining gp.cumulativeUsed = other.cumulativeUsed + gp.cumulativeRegular = other.cumulativeRegular + gp.cumulativeState = other.cumulativeState } func (gp *GasPool) String() string { diff --git a/core/state/journal.go b/core/state/journal.go index a79bd7331a..903deacc79 100644 --- a/core/state/journal.go +++ b/core/state/journal.go @@ -27,9 +27,24 @@ import ( "github.com/holiman/uint256" ) +// frameRange is a half-open interval [start, end) of journal entry indices, +// used to record the slice of entries occupied by a closed child call frame. +type frameRange struct { + start, end int +} + type revision struct { id int journalIndex int + // closedChildren holds the [start, end) ranges of child call frames that + // have been closed under this revision via closeSnapshot. Together with + // journalIndex (this frame's own start) and the current journal length + // (this frame's tentative end) they describe the slice of entries that + // belong directly to this frame, with descendant frames' entries excluded. + // + // Invariant: ranges are appended in increasing order, are non-overlapping, + // and lie entirely within [journalIndex, len(entries)). + closedChildren []frameRange } // journalEntry is a modification entry in the state change journal that can be @@ -55,12 +70,19 @@ type journal struct { validRevisions []revision nextRevisionId int + + // stateBytesCharged caches the state bytes result per snapshot ID. + // When a call boundary computes its state bytes and charges gas, + // the result is stored here. The parent frame subtracts the sum + // of its subcalls' cached results to avoid double-counting. + stateBytesCharged map[int]int64 } // newJournal creates a new initialized journal. func newJournal() *journal { return &journal{ - dirties: make(map[common.Address]int), + dirties: make(map[common.Address]int), + stateBytesCharged: make(map[int]int64), } } @@ -71,6 +93,7 @@ func (j *journal) reset() { j.entries = j.entries[:0] j.validRevisions = j.validRevisions[:0] clear(j.dirties) + clear(j.stateBytesCharged) j.nextRevisionId = 0 } @@ -78,7 +101,7 @@ func (j *journal) reset() { func (j *journal) snapshot() int { id := j.nextRevisionId j.nextRevisionId++ - j.validRevisions = append(j.validRevisions, revision{id, j.length()}) + j.validRevisions = append(j.validRevisions, revision{id: id, journalIndex: j.length()}) return id } @@ -98,6 +121,64 @@ func (j *journal) revertToSnapshot(revid int, s *StateDB) { j.validRevisions = j.validRevisions[:idx] } +// closeSnapshot marks the end of the call frame identified by revid without +// reverting any state. The frame's entry range [snapshot_index, current_length) +// is recorded on its parent revision so callers can later iterate the parent's +// own entries while skipping over closed children (and, transitively, their +// descendants — descendant ranges are absorbed into the closing child's range +// when the descendant itself was closed earlier under that child). +// +// closeSnapshot must be invoked in LIFO order: revid must identify the topmost +// snapshot. It panics otherwise. The corresponding revision is popped, so a +// subsequent revertToSnapshot on the same id is no longer valid. +func (j *journal) closeSnapshot(revid int) { + if len(j.validRevisions) == 0 { + panic(fmt.Errorf("revision id %v cannot be closed: no open snapshot", revid)) + } + top := len(j.validRevisions) - 1 + if j.validRevisions[top].id != revid { + panic(fmt.Errorf("revision id %v cannot be closed: top is %v", + revid, j.validRevisions[top].id)) + } + closed := frameRange{ + start: j.validRevisions[top].journalIndex, + end: len(j.entries), + } + // Only propagate non-empty ranges, and only if there is a parent frame to + // receive them. The outermost frame has nothing to bubble up to. + if closed.start < closed.end && top > 0 { + parent := &j.validRevisions[top-1] + parent.closedChildren = append(parent.closedChildren, closed) + } + // Drop this revision's bookkeeping. The slice is reused by the parent so + // avoid pinning it via the popped tail. + j.validRevisions[top].closedChildren = nil + j.validRevisions = j.validRevisions[:top] +} + +// frameEntries invokes visit for each entry that belongs directly to the +// current (topmost) call frame, skipping entries that lie within any closed +// child frame's range. Entries are visited in append order. If no frame is +// open, frameEntries is a no-op. +// +// nolint:unused +func (j *journal) frameEntries(visit func(entry journalEntry)) { + if len(j.validRevisions) == 0 { + return + } + rev := j.validRevisions[len(j.validRevisions)-1] + idx := rev.journalIndex + for _, child := range rev.closedChildren { + for ; idx < child.start; idx++ { + visit(j.entries[idx]) + } + idx = child.end + } + for ; idx < len(j.entries); idx++ { + visit(j.entries[idx]) + } +} + // append inserts a new modification entry to the end of the change journal. func (j *journal) append(entry journalEntry) { j.entries = append(j.entries, entry) @@ -135,17 +216,132 @@ func (j *journal) length() int { return len(j.entries) } +// stateChangedBytes computes the state bytes created by the call frame +// identified by revid, walking only entries that belong directly to this +// frame and skipping over closed child frame ranges. The result is cached +// in stateBytesCharged so that parent frames can look it up. +// +// When excludeSubcalls is true, cached subcall costs are not added to the +// total. This is useful when subcalls have already been charged to their +// own gas budgets and shouldn't bubble up to the ancestor frames. +func (j *journal) stateChangedBytes(revid int, stateObjects map[common.Address]*stateObject, excludeSubcalls bool) int64 { + // Find the revision by ID. + idx := sort.Search(len(j.validRevisions), func(i int) bool { + return j.validRevisions[i].id >= revid + }) + if idx == len(j.validRevisions) || j.validRevisions[idx].id != revid { + panic(fmt.Errorf("revision id %v not found for stateChangedBytes", revid)) + } + rev := j.validRevisions[idx] + + type slotKey struct { + addr common.Address + key common.Hash + } + type slotInfo struct { + prev common.Hash // value before first write in this frame + orig common.Hash // committed/original value from trie + } + slots := make(map[slotKey]*slotInfo) + created := make(map[common.Address]bool) + codeChanged := make(map[common.Address]bool) + + // 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: + codeChanged[e.account] = true + case storageChange: + sk := slotKey{e.account, e.key} + if _, seen := slots[sk]; !seen { + slots[sk] = &slotInfo{prev: e.prevvalue, orig: e.origvalue} + } + } + } + pos := rev.journalIndex + for _, child := range rev.closedChildren { + for ; pos < child.start; pos++ { + visit(j.entries[pos]) + } + if !excludeSubcalls { + // Add the cached cost for this subcall. + subcallBytes += j.stateBytesCharged[child.start] + } + pos = child.end + } + for ; pos < len(j.entries); pos++ { + visit(j.entries[pos]) + } + + var totalBytes int64 + for range created { + totalBytes += CostPerAccount + } + for sk, si := range slots { + obj := stateObjects[sk.addr] + if obj == nil { + continue + } + cur := obj.dirtyStorage[sk.key] + prevZero := si.prev == (common.Hash{}) + curZero := cur == (common.Hash{}) + origZero := si.orig == (common.Hash{}) + + if prevZero && !curZero && origZero { + // Frame-entry zero, frame-exit non-zero, tx-entry zero: + // this frame created a new slot, charge. + totalBytes += CostPerSlot + } else if !prevZero && curZero && origZero { + // Only refund slots created and freed in this transaction + totalBytes -= CostPerSlot + } + // All other transitions are free: + // - prevZero && !curZero && !origZero: pre-existing slot was + // 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-existing slot was + // cleared now, don't refund to not enable gas tokens. + } + for addr := range codeChanged { + obj := stateObjects[addr] + if obj != nil { + totalBytes += int64(len(obj.code)) + } + } + + // 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 +} + // copy returns a deep-copied journal. func (j *journal) copy() *journal { entries := make([]journalEntry, 0, j.length()) for i := 0; i < j.length(); i++ { entries = append(entries, j.entries[i].copy()) } + revisions := make([]revision, len(j.validRevisions)) + for i, r := range j.validRevisions { + revisions[i] = revision{ + id: r.id, + journalIndex: r.journalIndex, + closedChildren: slices.Clone(r.closedChildren), + } + } return &journal{ - entries: entries, - dirties: maps.Clone(j.dirties), - validRevisions: slices.Clone(j.validRevisions), - nextRevisionId: j.nextRevisionId, + entries: entries, + dirties: maps.Clone(j.dirties), + validRevisions: revisions, + nextRevisionId: j.nextRevisionId, + stateBytesCharged: maps.Clone(j.stateBytesCharged), } } diff --git a/core/state/journal_test.go b/core/state/journal_test.go new file mode 100644 index 0000000000..0e0e2b55b1 --- /dev/null +++ b/core/state/journal_test.go @@ -0,0 +1,213 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package state + +import ( + "slices" + "testing" + + "github.com/ethereum/go-ethereum/common" +) + +// tagEntry is a minimal journalEntry used by journal tests. It carries an +// integer tag so frameEntries iteration order can be verified, and is a no-op +// on revert so the surrounding StateDB can be a zero value. +type tagEntry struct { + tag int +} + +func (t tagEntry) revert(*StateDB) {} +func (t tagEntry) dirtied() (common.Address, bool) { return common.Address{}, false } +func (t tagEntry) copy() journalEntry { return t } + +// frameTags drives frameEntries and returns the visited tags in order. +func frameTags(j *journal) []int { + var got []int + j.frameEntries(func(e journalEntry) { + got = append(got, e.(tagEntry).tag) + }) + return got +} + +// didPanic reports whether fn panicked. +func didPanic(fn func()) (panicked bool) { + defer func() { + if r := recover(); r != nil { + panicked = true + } + }() + fn() + return false +} + +// TestJournalFrameTracking covers the happy paths of closeSnapshot and +// frameEntries together: basic single-child filtering, empty-range elision, +// multiple siblings, transitive descendant absorption, and the no-open-frame +// edge case for frameEntries. Building one composite scenario and asserting +// at each step keeps the expected behaviour as a connected story rather than +// scattering it across many tiny tests. +func TestJournalFrameTracking(t *testing.T) { + j := newJournal() + + // frameEntries on an empty journal is a no-op. + if got := frameTags(j); len(got) != 0 { + t.Fatalf("empty journal frameEntries: have %v, want []", got) + } + + j.snapshot() + j.append(tagEntry{1}) // outer + + // Closing an empty child frame must not record a degenerate range. + empty := j.snapshot() + j.closeSnapshot(empty) + if got := j.validRevisions[0].closedChildren; len(got) != 0 { + t.Fatalf("empty child should not propagate, have %+v", got) + } + + // First sibling child: two entries, then close. Range goes onto outer. + c1 := j.snapshot() + c1Start := len(j.entries) + j.append(tagEntry{10}) + j.append(tagEntry{11}) + c1End := len(j.entries) + j.closeSnapshot(c1) + + j.append(tagEntry{2}) // outer between siblings + + // Second sibling, with a grandchild closed inside it. After the + // grandchild closes, more entries appear in the child before it itself + // closes. The outer must end up with a single range that covers the + // child (which transitively covers the grandchild). + c2 := j.snapshot() + c2Start := len(j.entries) + j.append(tagEntry{20}) + + gc := j.snapshot() + j.append(tagEntry{300}) + j.closeSnapshot(gc) + + j.append(tagEntry{21}) + c2End := len(j.entries) + j.closeSnapshot(c2) + + j.append(tagEntry{3}) // outer after both siblings + + got := j.validRevisions[0].closedChildren + want := []frameRange{{c1Start, c1End}, {c2Start, c2End}} + if !slices.Equal(got, want) { + t.Fatalf("closedChildren: have %+v, want %+v", got, want) + } + if tags := frameTags(j); !slices.Equal(tags, []int{1, 2, 3}) { + t.Fatalf("frameEntries: have %v, want [1 2 3]", tags) + } + + // Closing the outermost (no-parent) frame is allowed: there is nothing + // to populate, but the revision is still popped and its range silently + // dropped. The journal ends up with no open frames. + outer := j.validRevisions[0].id + j.closeSnapshot(outer) + if len(j.validRevisions) != 0 { + t.Fatalf("after closing outermost, have %d open revisions, want 0", len(j.validRevisions)) + } +} + +// TestJournalCloseSnapshotPanics asserts the LIFO precondition: closing when +// no snapshot is open, or closing a revision while a more recent snapshot is +// still open above it, must panic rather than silently mutate state. Closing +// the outermost (no-parent) frame *is* permitted and is covered in +// TestJournalFrameTracking. +func TestJournalCloseSnapshotPanics(t *testing.T) { + j := newJournal() + if !didPanic(func() { j.closeSnapshot(0) }) { + t.Fatal("closing with no open snapshot should panic") + } + bottom := j.snapshot() + j.snapshot() // a more recent snapshot is now on top + if !didPanic(func() { j.closeSnapshot(bottom) }) { + t.Fatal("closing a snapshot that is not the most recent should panic") + } +} + +// TestJournalRevertInteractions verifies the two cross-cuts between revert +// and close: reverting a parent that has absorbed closed children also +// throws away the children's entries, and reverting a child (rather than +// closing it) leaves no closed-child range on the parent. +func TestJournalRevertInteractions(t *testing.T) { + t.Run("revertParentWithClosedChild", func(t *testing.T) { + j := newJournal() + outer := j.snapshot() + j.append(tagEntry{1}) + + c := j.snapshot() + j.append(tagEntry{10}) + j.append(tagEntry{11}) + j.closeSnapshot(c) + + j.append(tagEntry{2}) + j.revertToSnapshot(outer, &StateDB{}) + + if len(j.entries) != 0 || len(j.validRevisions) != 0 { + t.Fatalf("after revert have entries=%d revisions=%d, want both 0", + len(j.entries), len(j.validRevisions)) + } + }) + t.Run("revertedChildLeavesNoRange", func(t *testing.T) { + j := newJournal() + j.snapshot() + j.append(tagEntry{1}) + + c := j.snapshot() + j.append(tagEntry{10}) + j.revertToSnapshot(c, &StateDB{}) + j.append(tagEntry{2}) + + if got := j.validRevisions[0].closedChildren; len(got) != 0 { + t.Fatalf("reverted child should not appear in closedChildren, have %+v", got) + } + if tags := frameTags(j); !slices.Equal(tags, []int{1, 2}) { + t.Fatalf("frameEntries: have %v, want [1 2]", tags) + } + }) +} + +// TestJournalCopyAndReset checks that the bookkeeping for closed-child ranges +// participates in journal.copy (deep-copied, not aliased) and journal.reset +// (cleared along with everything else). +func TestJournalCopyAndReset(t *testing.T) { + j := newJournal() + j.snapshot() + j.append(tagEntry{1}) + c := j.snapshot() + j.append(tagEntry{10}) + j.closeSnapshot(c) + + cp := j.copy() + if !slices.Equal(cp.validRevisions[0].closedChildren, j.validRevisions[0].closedChildren) { + t.Fatalf("copy lost closedChildren: orig=%+v copy=%+v", + j.validRevisions[0].closedChildren, cp.validRevisions[0].closedChildren) + } + cp.validRevisions[0].closedChildren = append(cp.validRevisions[0].closedChildren, frameRange{99, 100}) + if len(j.validRevisions[0].closedChildren) != 1 { + t.Fatal("original aliased copy's closedChildren slice") + } + + j.reset() + if len(j.entries) != 0 || len(j.validRevisions) != 0 { + t.Fatalf("after reset have entries=%d revisions=%d, want both 0", + len(j.entries), len(j.validRevisions)) + } +} diff --git a/core/state/snapshot/iterator_test.go b/core/state/snapshot/iterator_test.go index dd6c4cf968..a95bd66dde 100644 --- a/core/state/snapshot/iterator_test.go +++ b/core/state/snapshot/iterator_test.go @@ -342,7 +342,7 @@ func TestAccountIteratorTraversalValues(t *testing.T) { if i%8 == 0 { e[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 4, i) } - if i > 50 || i < 85 { + if i > 50 && i < 85 { f[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 5, i) } if i%64 == 0 { diff --git a/core/state/statedb.go b/core/state/statedb.go index d17f947a12..cf2b918a9d 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -762,11 +762,87 @@ func (s *StateDB) RevertToSnapshot(revid int) { s.journal.revertToSnapshot(revid, s) } +// CloseSnapshot marks the call frame identified by revid as completed without +// reverting any state. Its journal entry range is recorded on the parent +// frame so the parent can later iterate its own entries while skipping over +// closed children. revid must identify the topmost open snapshot (i.e. frames +// must be closed in LIFO order). It panics otherwise. +func (s *StateDB) CloseSnapshot(revid int) { + s.journal.closeSnapshot(revid) +} + // GetRefund returns the current value of the refund counter. func (s *StateDB) GetRefund() uint64 { return s.refund } +const ( + CostPerAccount = 112 + CostPerSlot = 32 +) + +// StateChangedBytes computes the state bytes created by the call frame +// identified by revid, excluding entries from closed child frames. When +// excludeSubcalls is true, cached subcall costs are not added to the total. +func (s *StateDB) StateChangedBytes(revid int, excludeSubcalls bool) int64 { + return s.journal.stateChangedBytes(revid, s.stateObjects, excludeSubcalls) +} + +// SelfDestructRefundBytes computes the total state bytes to refund at tx-end +// for accounts that were both created and selfdestructed during this +// transaction. +func (s *StateDB) SelfDestructRefundBytes() int64 { + // Collect addresses created and selfdestructed in this tx. + targets := make(map[common.Address]*stateObject) + for addr, obj := range s.stateObjects { + if s.IsNewContract(addr) && s.HasSelfDestructed(addr) { + targets[addr] = obj + } + } + if len(targets) == 0 { + return 0 + } + // Account creation + code deposit refunds. + var bytes int64 + for _, obj := range targets { + bytes += CostPerAccount + int64(len(obj.code)) + } + // For storage slots: walk journal storage entries to find the tx-entry + // value of each slot. Count slots where tx-entry was zero and the final + // dirty value is non-zero (i.e. the slot was charged as new and not + // subsequently cleared). + type slotKey struct { + addr common.Address + key common.Hash + } + originAtTxEntry := make(map[slotKey]common.Hash) + for _, e := range s.journal.entries { + sc, ok := e.(storageChange) + if !ok { + continue + } + if _, ok := targets[sc.account]; !ok { + continue + } + sk := slotKey{sc.account, sc.key} + if _, seen := originAtTxEntry[sk]; !seen { + originAtTxEntry[sk] = sc.origvalue + } + } + for sk, orig := range originAtTxEntry { + if orig != (common.Hash{}) { + continue + } + obj := targets[sk.addr] + cur, dirty := obj.dirtyStorage[sk.key] + if !dirty || cur == (common.Hash{}) { + continue + } + bytes += CostPerSlot + } + return bytes +} + type removedAccountWithBalance struct { address common.Address balance *uint256.Int diff --git a/core/state/statedb_hooked.go b/core/state/statedb_hooked.go index ae6d795cd3..6295e2a4ae 100644 --- a/core/state/statedb_hooked.go +++ b/core/state/statedb_hooked.go @@ -148,6 +148,10 @@ func (s *hookedStateDB) RevertToSnapshot(i int) { s.inner.RevertToSnapshot(i) } +func (s *hookedStateDB) CloseSnapshot(i int) { + s.inner.CloseSnapshot(i) +} + func (s *hookedStateDB) Snapshot() int { return s.inner.Snapshot() } @@ -289,3 +293,11 @@ func (s *hookedStateDB) Finalise(deleteEmptyObjects bool) (*bal.StateAccessList, } return s.inner.Finalise(deleteEmptyObjects) } + +func (s *hookedStateDB) StateChangedBytes(revid int, excludeSubcalls bool) int64 { + return s.inner.StateChangedBytes(revid, excludeSubcalls) +} + +func (s *hookedStateDB) SelfDestructRefundBytes() int64 { + return s.inner.SelfDestructRefundBytes() +} diff --git a/core/state_processor.go b/core/state_processor.go index 53fb2acfe9..4f129038d0 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -274,6 +274,19 @@ func ApplyTransaction(evm *vm.EVM, gp *GasPool, statedb *state.StateDB, header * return ApplyTransactionWithEVM(msg, gp, statedb, header.Number, header.Hash(), header.Time, tx, evm) } +// systemCallGasBudget returns the gas budget for system calls. Pre-Amsterdam +// the budget is 30M regular gas. Post-Amsterdam (EIP-8037), an additional +// state-gas reservoir of `STATE_BYTES_PER_STORAGE_SET × CPSB × SYSTEM_MAX_SSTORES_PER_CALL` +// is provided to cover the expected new SSTOREs in system contracts. +func systemCallGasBudget(evm *vm.EVM) vm.GasBudget { + const regular = 30_000_000 + if evm.ChainConfig().IsAmsterdam(evm.Context.BlockNumber, evm.Context.Time) { + stateGas := params.StorageCreationSize * evm.Context.CostPerStateByte * params.SystemMaxSstoresPerCall + return vm.NewGasBudget(regular, stateGas) + } + return vm.NewGasBudgetReg(regular) +} + // ProcessBeaconBlockRoot applies the EIP-4788 system call to the beacon block root // contract. This method is exported to be used in tests. func ProcessBeaconBlockRoot(beaconRoot common.Hash, evm *vm.EVM) (*bal.StateAccessList, *bal.StateMutations) { @@ -294,7 +307,7 @@ func ProcessBeaconBlockRoot(beaconRoot common.Hash, evm *vm.EVM) (*bal.StateAcce } evm.SetTxContext(NewEVMTxContext(msg)) evm.StateDB.AddAddressToAccessList(params.BeaconRootsAddress) - _, _, _ = evm.Call(msg.From, *msg.To, msg.Data, vm.NewGasBudget(30_000_000), common.U2560) + _, _, _ = evm.Call(msg.From, *msg.To, msg.Data, systemCallGasBudget(evm), common.U2560) if evm.StateDB.AccessEvents() != nil { evm.StateDB.AccessEvents().Merge(evm.AccessEvents) } @@ -321,7 +334,7 @@ func ProcessParentBlockHash(prevHash common.Hash, evm *vm.EVM) (*bal.StateAccess } evm.SetTxContext(NewEVMTxContext(msg)) evm.StateDB.AddAddressToAccessList(params.HistoryStorageAddress) - _, _, err := evm.Call(msg.From, *msg.To, msg.Data, vm.NewGasBudget(30_000_000), common.U2560) + _, _, err := evm.Call(msg.From, *msg.To, msg.Data, systemCallGasBudget(evm), common.U2560) if err != nil { panic(err) } @@ -360,7 +373,7 @@ func processRequestsSystemCall(requests *[][]byte, evm *vm.EVM, requestType byte } evm.SetTxContext(NewEVMTxContext(msg)) evm.StateDB.AddAddressToAccessList(addr) - ret, _, err := evm.Call(msg.From, *msg.To, msg.Data, vm.NewGasBudget(30_000_000), common.U2560) + ret, _, err := evm.Call(msg.From, *msg.To, msg.Data, systemCallGasBudget(evm), common.U2560) if evm.StateDB.AccessEvents() != nil { evm.StateDB.AccessEvents().Merge(evm.AccessEvents) } diff --git a/core/state_transition.go b/core/state_transition.go index b50f071669..e0fd882dcf 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -68,13 +68,29 @@ func (result *ExecutionResult) Revert() []byte { } // IntrinsicGas computes the 'intrinsic gas' for a message with the given data. -func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.SetCodeAuthorization, isContractCreation bool, rules params.Rules) (vm.GasCosts, error) { +// costPerStateByte needs to be set post-Amsterdam. +func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.SetCodeAuthorization, isContractCreation bool, rules params.Rules, costPerStateByte uint64) (vm.GasCosts, error) { // Set the starting gas for the raw transaction - var gas uint64 + var gas vm.GasCosts if isContractCreation && rules.IsHomestead { - gas = params.TxGasContractCreation + if rules.IsAmsterdam { + // EIP-8037: account creation is state gas; base tx + CREATE overhead is regular gas. + gas.RegularGas = params.TxGas + params.CreateGasAmsterdam + gas.StateGas = int64(params.AccountCreationSize * costPerStateByte) + } else { + gas.RegularGas = params.TxGasContractCreation + } } else { - gas = params.TxGas + gas.RegularGas = params.TxGas + } + // EIP-8037: authorization tuples contribute both regular and state gas. + if authList != nil { + if rules.IsAmsterdam { + gas.RegularGas += uint64(len(authList)) * params.TxAuthTupleRegularGas + gas.StateGas += int64(len(authList)) * (params.AuthorizationCreationSize + params.AccountCreationSize) * int64(costPerStateByte) + } else { + gas.RegularGas += uint64(len(authList)) * params.CallNewAccountGas + } } dataLen := uint64(len(data)) // Bump the required gas by the amount of transactional data @@ -88,35 +104,35 @@ func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.Set if rules.IsIstanbul { nonZeroGas = params.TxDataNonZeroGasEIP2028 } - if (math.MaxUint64-gas)/nonZeroGas < nz { + if (math.MaxUint64-gas.RegularGas)/nonZeroGas < nz { return vm.GasCosts{}, ErrGasUintOverflow } - gas += nz * nonZeroGas + gas.RegularGas += nz * nonZeroGas - if (math.MaxUint64-gas)/params.TxDataZeroGas < z { + if (math.MaxUint64-gas.RegularGas)/params.TxDataZeroGas < z { return vm.GasCosts{}, ErrGasUintOverflow } - gas += z * params.TxDataZeroGas + gas.RegularGas += z * params.TxDataZeroGas if isContractCreation && rules.IsShanghai { lenWords := toWordSize(dataLen) - if (math.MaxUint64-gas)/params.InitCodeWordGas < lenWords { + if (math.MaxUint64-gas.RegularGas)/params.InitCodeWordGas < lenWords { return vm.GasCosts{}, ErrGasUintOverflow } - gas += lenWords * params.InitCodeWordGas + gas.RegularGas += lenWords * params.InitCodeWordGas } } if accessList != nil { addresses := uint64(len(accessList)) storageKeys := uint64(accessList.StorageKeys()) - if (math.MaxUint64-gas)/params.TxAccessListAddressGas < addresses { + if (math.MaxUint64-gas.RegularGas)/params.TxAccessListAddressGas < addresses { return vm.GasCosts{}, ErrGasUintOverflow } - gas += addresses * params.TxAccessListAddressGas - if (math.MaxUint64-gas)/params.TxAccessListStorageKeyGas < storageKeys { + gas.RegularGas += addresses * params.TxAccessListAddressGas + if (math.MaxUint64-gas.RegularGas)/params.TxAccessListStorageKeyGas < storageKeys { return vm.GasCosts{}, ErrGasUintOverflow } - gas += storageKeys * params.TxAccessListStorageKeyGas + gas.RegularGas += storageKeys * params.TxAccessListStorageKeyGas // EIP-7981: access list data is charged in addition to the base charge. if rules.IsAmsterdam { @@ -124,17 +140,17 @@ func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.Set addressCost = common.AddressLength * params.TxCostFloorPerToken7976 * params.TxTokenPerNonZeroByte storageKeyCost = common.HashLength * params.TxCostFloorPerToken7976 * params.TxTokenPerNonZeroByte ) - if (math.MaxUint64-gas)/addressCost < addresses { + if (math.MaxUint64-gas.RegularGas)/addressCost < addresses { return vm.GasCosts{}, ErrGasUintOverflow } - gas += addresses * addressCost - if (math.MaxUint64-gas)/storageKeyCost < storageKeys { + gas.RegularGas += addresses * addressCost + if (math.MaxUint64-gas.RegularGas)/storageKeyCost < storageKeys { return vm.GasCosts{}, ErrGasUintOverflow } - gas += storageKeys * storageKeyCost + gas.RegularGas += storageKeys * storageKeyCost } } - return vm.GasCosts{RegularGas: gas}, nil + return gas, nil } // FloorDataGas computes the minimum gas required for a transaction based on its data tokens (EIP-7623). @@ -306,7 +322,7 @@ func (st *stateTransition) to() common.Address { return *st.msg.To } -func (st *stateTransition) buyGas() error { +func (st *stateTransition) buyGas() (uint64, error) { mgval := new(big.Int).SetUint64(st.msg.GasLimit) mgval.Mul(mgval, st.msg.GasPrice) balanceCheck := new(big.Int).Set(mgval) @@ -330,54 +346,57 @@ func (st *stateTransition) buyGas() error { } balanceCheckU256, overflow := uint256.FromBig(balanceCheck) if overflow { - return fmt.Errorf("%w: address %v required balance exceeds 256 bits", ErrInsufficientFunds, st.msg.From.Hex()) + return 0, fmt.Errorf("%w: address %v required balance exceeds 256 bits", ErrInsufficientFunds, st.msg.From.Hex()) } if have, want := st.state.GetBalance(st.msg.From), balanceCheckU256; have.Cmp(want) < 0 { - return fmt.Errorf("%w: address %v have %v want %v", ErrInsufficientFunds, st.msg.From.Hex(), have, want) - } - if err := st.gp.SubGas(st.msg.GasLimit); err != nil { - return err + return 0, fmt.Errorf("%w: address %v have %v want %v", ErrInsufficientFunds, st.msg.From.Hex(), have, want) } if st.evm.Config.Tracer != nil && st.evm.Config.Tracer.OnGasChange != nil { st.evm.Config.Tracer.OnGasChange(0, st.msg.GasLimit, tracing.GasChangeTxInitialBalance) } - st.gasRemaining = vm.NewGasBudget(st.msg.GasLimit) - st.initialBudget = st.gasRemaining.Copy() + // After Amsterdam we limit the regular gas to 16k, the data gas to the transaction limit + limit := st.msg.GasLimit + if st.evm.ChainConfig().IsAmsterdam(st.evm.Context.BlockNumber, st.evm.Context.Time) { + limit = min(st.msg.GasLimit, params.MaxTxGas) + } + st.initialBudget = vm.NewGasBudget(limit, st.msg.GasLimit-limit) + st.gasRemaining = st.initialBudget.Copy() mgvalU256, _ := uint256.FromBig(mgval) st.state.SubBalance(st.msg.From, mgvalU256, tracing.BalanceDecreaseGasBuy) - return nil + return st.msg.GasLimit, nil } -func (st *stateTransition) preCheck() error { +func (st *stateTransition) preCheck() (uint64, error) { // Only check transactions that are not fake msg := st.msg if !msg.SkipNonceChecks { // Make sure this transaction's nonce is correct. stNonce := st.state.GetNonce(msg.From) if msgNonce := msg.Nonce; stNonce < msgNonce { - return fmt.Errorf("%w: address %v, tx: %d state: %d", ErrNonceTooHigh, + return 0, fmt.Errorf("%w: address %v, tx: %d state: %d", ErrNonceTooHigh, msg.From.Hex(), msgNonce, stNonce) } else if stNonce > msgNonce { - return fmt.Errorf("%w: address %v, tx: %d state: %d", ErrNonceTooLow, + return 0, fmt.Errorf("%w: address %v, tx: %d state: %d", ErrNonceTooLow, msg.From.Hex(), msgNonce, stNonce) } else if stNonce+1 < stNonce { - return fmt.Errorf("%w: address %v, nonce: %d", ErrNonceMax, + return 0, fmt.Errorf("%w: address %v, nonce: %d", ErrNonceMax, msg.From.Hex(), stNonce) } } isOsaka := st.evm.ChainConfig().IsOsaka(st.evm.Context.BlockNumber, st.evm.Context.Time) + isAmsterdam := st.evm.ChainConfig().IsAmsterdam(st.evm.Context.BlockNumber, st.evm.Context.Time) if !msg.SkipTransactionChecks { // Verify tx gas limit does not exceed EIP-7825 cap. - if isOsaka && msg.GasLimit > params.MaxTxGas { - return fmt.Errorf("%w (cap: %d, tx: %d)", ErrGasLimitTooHigh, params.MaxTxGas, msg.GasLimit) + if !isAmsterdam && isOsaka && msg.GasLimit > params.MaxTxGas { + return 0, fmt.Errorf("%w (cap: %d, tx: %d)", ErrGasLimitTooHigh, params.MaxTxGas, msg.GasLimit) } // Make sure the sender is an EOA code := st.state.GetCode(msg.From) _, delegated := types.ParseDelegation(code) if len(code) > 0 && !delegated { - return fmt.Errorf("%w: address %v, len(code): %d", ErrSenderNoEOA, msg.From.Hex(), len(code)) + return 0, fmt.Errorf("%w: address %v, len(code): %d", ErrSenderNoEOA, msg.From.Hex(), len(code)) } } // Make sure that transaction gasFeeCap is greater than the baseFee (post london) @@ -386,21 +405,21 @@ func (st *stateTransition) preCheck() error { skipCheck := st.evm.Config.NoBaseFee && msg.GasFeeCap.BitLen() == 0 && msg.GasTipCap.BitLen() == 0 if !skipCheck { if l := msg.GasFeeCap.BitLen(); l > 256 { - return fmt.Errorf("%w: address %v, maxFeePerGas bit length: %d", ErrFeeCapVeryHigh, + return 0, fmt.Errorf("%w: address %v, maxFeePerGas bit length: %d", ErrFeeCapVeryHigh, msg.From.Hex(), l) } if l := msg.GasTipCap.BitLen(); l > 256 { - return fmt.Errorf("%w: address %v, maxPriorityFeePerGas bit length: %d", ErrTipVeryHigh, + return 0, fmt.Errorf("%w: address %v, maxPriorityFeePerGas bit length: %d", ErrTipVeryHigh, msg.From.Hex(), l) } if msg.GasFeeCap.Cmp(msg.GasTipCap) < 0 { - return fmt.Errorf("%w: address %v, maxPriorityFeePerGas: %s, maxFeePerGas: %s", ErrTipAboveFeeCap, + return 0, fmt.Errorf("%w: address %v, maxPriorityFeePerGas: %s, maxFeePerGas: %s", ErrTipAboveFeeCap, msg.From.Hex(), msg.GasTipCap, msg.GasFeeCap) } // This will panic if baseFee is nil, but basefee presence is verified // as part of header validation. if msg.GasFeeCap.Cmp(st.evm.Context.BaseFee) < 0 { - return fmt.Errorf("%w: address %v, maxFeePerGas: %s, baseFee: %s", ErrFeeCapTooLow, + return 0, fmt.Errorf("%w: address %v, maxFeePerGas: %s, baseFee: %s", ErrFeeCapTooLow, msg.From.Hex(), msg.GasFeeCap, st.evm.Context.BaseFee) } } @@ -411,17 +430,17 @@ func (st *stateTransition) preCheck() error { // has it as a non-nillable value, so any msg derived from blob transaction has it non-nil. // However, messages created through RPC (eth_call) don't have this restriction. if msg.To == nil { - return ErrBlobTxCreate + return 0, ErrBlobTxCreate } if len(msg.BlobHashes) == 0 { - return ErrMissingBlobHashes + return 0, ErrMissingBlobHashes } if isOsaka && len(msg.BlobHashes) > params.BlobTxMaxBlobs { - return ErrTooManyBlobs + return 0, ErrTooManyBlobs } for i, hash := range msg.BlobHashes { if !kzg4844.IsValidVersionedHash(hash[:]) { - return fmt.Errorf("blob %d has invalid hash version", i) + return 0, fmt.Errorf("blob %d has invalid hash version", i) } } } @@ -434,7 +453,7 @@ func (st *stateTransition) preCheck() error { // This will panic if blobBaseFee is nil, but blobBaseFee presence // is verified as part of header validation. if msg.BlobGasFeeCap.Cmp(st.evm.Context.BlobBaseFee) < 0 { - return fmt.Errorf("%w: address %v blobGasFeeCap: %v, blobBaseFee: %v", ErrBlobFeeCapTooLow, + return 0, fmt.Errorf("%w: address %v blobGasFeeCap: %v, blobBaseFee: %v", ErrBlobFeeCapTooLow, msg.From.Hex(), msg.BlobGasFeeCap, st.evm.Context.BlobBaseFee) } } @@ -443,10 +462,10 @@ func (st *stateTransition) preCheck() error { // Check that EIP-7702 authorization list signatures are well formed. if msg.SetCodeAuthorizations != nil { if msg.To == nil { - return fmt.Errorf("%w (sender %v)", ErrSetCodeTxCreate, msg.From) + return 0, fmt.Errorf("%w (sender %v)", ErrSetCodeTxCreate, msg.From) } if len(msg.SetCodeAuthorizations) == 0 { - return fmt.Errorf("%w (sender %v)", ErrEmptyAuthList, msg.From) + return 0, fmt.Errorf("%w (sender %v)", ErrEmptyAuthList, msg.From) } } return st.buyGas() @@ -474,7 +493,8 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { // 6. caller has enough balance to cover asset transfer for **topmost** call // Check clauses 1-3, buy gas if everything is correct - if err := st.preCheck(); err != nil { + gas, err := st.preCheck() + if err != nil { return nil, err } @@ -484,19 +504,34 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { contractCreation = msg.To == nil floorDataGas uint64 ) + + if !rules.IsAmsterdam { + if err := st.gp.SubGas(gas); err != nil { + return nil, err + } + } + // Check clauses 4-5, subtract intrinsic gas if everything is correct - cost, err := IntrinsicGas(msg.Data, msg.AccessList, msg.SetCodeAuthorizations, contractCreation, rules) + cost, err := IntrinsicGas(msg.Data, msg.AccessList, msg.SetCodeAuthorizations, contractCreation, rules, st.evm.Context.CostPerStateByte) if err != nil { return nil, err } - prior, sufficient := st.gasRemaining.Charge(cost) - if !sufficient { - return nil, fmt.Errorf("%w: have %d, want %d", ErrIntrinsicGas, st.gasRemaining.RegularGas, cost.RegularGas) + + // Regular gas check for block inclusion post-amsterdam includes state gas. + if rules.IsAmsterdam { + subGasAmount := msg.GasLimit + if subGasAmount > uint64(cost.StateGas) { + subGasAmount -= uint64(cost.StateGas) + } else { + subGasAmount = 0 + } + subGasAmount = min(subGasAmount, params.MaxTxGas) + if err := st.gp.SubGas(subGasAmount); err != nil { + return nil, err + } } - if t := st.evm.Config.Tracer; t != nil && t.OnGasChange != nil { - t.OnGasChange(prior, st.gasRemaining.RegularGas, tracing.GasChangeTxIntrinsicGas) - } - // Gas limit suffices for the floor data cost (EIP-7623) + + // Compute the floor data cost (EIP-7623), needed for both Prague and Amsterdam validation. if rules.IsPrague { floorDataGas, err = FloorDataGas(rules, msg.Data, msg.AccessList) if err != nil { @@ -507,6 +542,29 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { } } + if rules.IsAmsterdam { + // EIP-8037: total intrinsic must fit within the transaction gas limit. + if cost.Sum() > msg.GasLimit { + return nil, fmt.Errorf("%w: have %d, want %d", ErrIntrinsicGas, msg.GasLimit, cost.Sum()) + } + // EIP-8037: the regular gas consumption (intrinsic or floor) must fit within MaxTxGas. + maxRegularGas := max(cost.RegularGas, floorDataGas) + if maxRegularGas > params.MaxTxGas { + return nil, fmt.Errorf("%w: max regular gas %d exceeds limit %d", ErrIntrinsicGas, maxRegularGas, params.MaxTxGas) + } + } + prior, sufficient := st.gasRemaining.Charge(cost) + if !sufficient { + return nil, fmt.Errorf("%w: have %d, want %d", ErrIntrinsicGas, st.gasRemaining.RegularGas, cost.RegularGas) + } + if t := st.evm.Config.Tracer; t != nil && t.OnGasChange != nil { + if rules.IsAmsterdam { + t.OnGasChange(msg.GasLimit, st.gasRemaining.RegularGas+st.gasRemaining.StateGas, tracing.GasChangeTxIntrinsicGas) + } else { + t.OnGasChange(prior, st.gasRemaining.RegularGas, tracing.GasChangeTxIntrinsicGas) + } + } + if rules.IsEIP4762 { st.evm.AccessEvents.AddTxOrigin(msg.From) @@ -540,6 +598,11 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { ret []byte vmerr error // vm errors do not effect consensus and are therefore not assigned to err ) + + // Take a snapshot for gas calculation + outerSnapshot := st.state.Snapshot() + + var execGasUsed vm.GasUsed if contractCreation { ret, _, st.gasRemaining, vmerr = st.evm.Create(msg.From, msg.Data, st.gasRemaining, value) } else { @@ -550,7 +613,7 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { if msg.SetCodeAuthorizations != nil { for _, auth := range msg.SetCodeAuthorizations { // Note errors are ignored, we simply skip invalid authorizations here. - st.applyAuthorization(&auth) + st.applyAuthorization(rules, &auth) } } @@ -567,6 +630,21 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { ret, st.gasRemaining, vmerr = st.evm.Call(msg.From, st.to(), msg.Data, st.gasRemaining, value) } + // 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, false) + // Refund state gas for selfdestructed accounts. + outerBytes -= st.state.SelfDestructRefundBytes() + st.gasRemaining.Charge(vm.GasCosts{StateGas: outerBytes * int64(st.evm.Context.CostPerStateByte)}) + } else { + if execGasUsed.StateGas > 0 { + st.gasRemaining.StateGas += uint64(execGasUsed.StateGas) + } + execGasUsed.StateGas = 0 + } + } + // Record the gas used excluding gas refunds. This value represents the actual // gas allowance required to complete execution. peakGasUsed := st.gasUsed() @@ -577,28 +655,41 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { if rules.IsPrague { // After EIP-7623: Data-heavy transactions pay the floor gas. if used := st.gasUsed(); used < floorDataGas { - prior, _ := st.gasRemaining.Charge(vm.GasCosts{RegularGas: floorDataGas - used}) + prev := st.gasRemaining.RegularGas + // When the calldata floor exceeds actual gas used, any + // remaining state gas must also be consumed. + targetRemaining := (st.initialBudget.RegularGas + st.initialBudget.StateGas) - floorDataGas + st.gasRemaining.StateGas = 0 + st.gasRemaining.RegularGas = targetRemaining if t := st.evm.Config.Tracer; t != nil && t.OnGasChange != nil { - t.OnGasChange(prior, st.gasRemaining.RegularGas, tracing.GasChangeTxDataFloor) + t.OnGasChange(prev, st.gasRemaining.RegularGas, tracing.GasChangeTxDataFloor) } } if peakGasUsed < floorDataGas { peakGasUsed = floorDataGas } } - // Return gas to the user - st.returnGas() - // Return gas to the gas pool + returned := st.returnGas() if rules.IsAmsterdam { - // Refund is excluded for returning - err = st.gp.ReturnGas(st.initialBudget.RegularGas-peakGasUsed, st.gasUsed()) + // EIP-8037: 2D gas accounting for Amsterdam. + // tx_regular = intrinsic_regular + exec_regular_gas_used + // tx_state = intrinsic_state (adjusted) + exec_state_gas_used + // execGasUsed.StateGas may be negative when an SSTORE 0→x→0 refund + // exceeded the intrinsic-charged state gas + txState := uint64(cost.StateGas) + if execGasUsed.StateGas > 0 { + txState += uint64(execGasUsed.StateGas) + } + txRegular := cost.RegularGas + execGasUsed.RegularGas + txRegular = max(txRegular, floorDataGas) + if err := st.gp.ReturnGasAmsterdam(txRegular, txState, st.gasUsed()); err != nil { + return nil, err + } } else { - // Refund is included for returning - err = st.gp.ReturnGas(st.gasRemaining.RegularGas, st.gasUsed()) - } - if err != nil { - return nil, err + if err = st.gp.ReturnGas(returned, st.gasUsed()); err != nil { + return nil, err + } } effectiveTip := msg.GasPrice if rules.IsLondon { @@ -611,8 +702,14 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { // are 0. This avoids a negative effectiveTip being applied to // the coinbase when simulating calls. } else { - fee := new(uint256.Int).SetUint64(st.gasUsed()) + // For Amsterdam, the fee is based on what the user pays (receipt gas used). + feeGas := st.gasUsed() + fee := new(uint256.Int).SetUint64(feeGas) fee.Mul(fee, effectiveTipU256) + + // always read the coinbase account to include it in the BAL (TODO check this is actually part of the spec) + st.state.GetBalance(st.evm.Context.Coinbase) + st.state.AddBalance(st.evm.Context.Coinbase, fee, tracing.BalanceIncreaseRewardTransactionFee) // add the coinbase to the witness iff the fee is greater than 0 @@ -625,8 +722,9 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { st.evm.StateDB.AddLog(log) } } + usedGas := st.gasUsed() return &ExecutionResult{ - UsedGas: st.gasUsed(), + UsedGas: usedGas, MaxUsedGas: peakGasUsed, Err: vmerr, ReturnData: ret, @@ -665,30 +763,43 @@ func (st *stateTransition) validateAuthorization(auth *types.SetCodeAuthorizatio } // applyAuthorization applies an EIP-7702 code delegation to the state. -func (st *stateTransition) applyAuthorization(auth *types.SetCodeAuthorization) error { +func (st *stateTransition) applyAuthorization(rules params.Rules, auth *types.SetCodeAuthorization) (uint64, error) { authority, err := st.validateAuthorization(auth) if err != nil { - return err + return 0, err } - // If the account already exists in state, refund the new account cost - // charged in the intrinsic calculation. - if st.state.Exist(authority) { - st.state.AddRefund(params.CallNewAccountGas - params.TxAuthTupleGas) + // If the account is not empty (per EIP-161: non-zero nonce, balance, or + // code) refund the new account cost charged in the intrinsic calculation. + var refund uint64 + if !st.state.Empty(authority) { + if rules.IsAmsterdam { + // EIP-8037: refund account creation state gas to the reservoir + refund = params.AccountCreationSize * st.evm.Context.CostPerStateByte + st.gasRemaining.StateGas += refund + } else { + st.state.AddRefund(params.CallNewAccountGas - params.TxAuthTupleGas) + } } + prevDelegation, isDelegated := types.ParseDelegation(st.state.GetCode(authority)) + // Update nonce and account code. st.state.SetNonce(authority, auth.Nonce+1, tracing.NonceChangeAuthorization) if auth.Address == (common.Address{}) { // Delegation to zero address means clear. - st.state.SetCode(authority, nil, tracing.CodeChangeAuthorizationClear) - return nil + if isDelegated { + st.state.SetCode(authority, nil, tracing.CodeChangeAuthorizationClear) + } + return refund, nil } - // Otherwise install delegation to auth.Address. - st.state.SetCode(authority, types.AddressToDelegation(auth.Address), tracing.CodeChangeAuthorization) + // install delegation to auth.Address if the delegation changed + if !isDelegated || auth.Address != prevDelegation { + st.state.SetCode(authority, types.AddressToDelegation(auth.Address), tracing.CodeChangeAuthorization) + } - return nil + return refund, nil } // calcRefund computes refund counter, capped to a refund quotient. @@ -707,22 +818,25 @@ func (st *stateTransition) calcRefund() vm.GasBudget { if st.evm.Config.Tracer != nil && st.evm.Config.Tracer.OnGasChange != nil && refund > 0 { st.evm.Config.Tracer.OnGasChange(st.gasRemaining.RegularGas, st.gasRemaining.RegularGas+refund, tracing.GasChangeTxRefunds) } - return vm.NewGasBudget(refund) + return vm.NewGasBudgetReg(refund) } // returnGas returns ETH for remaining gas, // exchanged at the original rate. -func (st *stateTransition) returnGas() { - remaining := uint256.NewInt(st.gasRemaining.RegularGas) +func (st *stateTransition) returnGas() uint64 { + gas := st.gasRemaining.RegularGas + st.gasRemaining.StateGas + remaining := uint256.NewInt(gas) remaining.Mul(remaining, uint256.MustFromBig(st.msg.GasPrice)) st.state.AddBalance(st.msg.From, remaining, tracing.BalanceIncreaseGasReturn) - if st.evm.Config.Tracer != nil && st.evm.Config.Tracer.OnGasChange != nil && st.gasRemaining.RegularGas > 0 { - st.evm.Config.Tracer.OnGasChange(st.gasRemaining.RegularGas, 0, tracing.GasChangeTxLeftOverReturned) + if st.evm.Config.Tracer != nil && st.evm.Config.Tracer.OnGasChange != nil && gas > 0 { + st.evm.Config.Tracer.OnGasChange(gas, 0, tracing.GasChangeTxLeftOverReturned) } + return gas } // gasUsed returns the amount of gas used up by the state transition. +// For Amsterdam (2D gas), this includes both regular and state gas consumed. func (st *stateTransition) gasUsed() uint64 { return st.gasRemaining.Used(st.initialBudget) } diff --git a/core/txpool/validation.go b/core/txpool/validation.go index d1709b9347..284fc062fc 100644 --- a/core/txpool/validation.go +++ b/core/txpool/validation.go @@ -125,13 +125,23 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types } // Ensure the transaction has more gas than the bare minimum needed to cover // the transaction metadata - intrGas, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules) + gasCostPerStateByte := core.CostPerStateByte(head, opts.Config) + intrGas, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules, gasCostPerStateByte) if err != nil { return err } + if gasCostPerStateByte != 0 { + // We require transactions to pay for 110% of intrinsic gas in order to + // prevent situations where a change in gas limit invalidates a lot + // of transactions in the txpool + if minGas := (intrGas.RegularGas * 10) / 9; tx.Gas() < minGas { + return fmt.Errorf("%w: gas %v, minimum needed %v", core.ErrIntrinsicGas, tx.Gas(), minGas) + } + } if tx.Gas() < intrGas.RegularGas { return fmt.Errorf("%w: gas %v, minimum needed %v", core.ErrIntrinsicGas, tx.Gas(), intrGas.RegularGas) } + // Ensure the transaction can cover floor data gas. if rules.IsPrague { floorDataGas, err := core.FloorDataGas(rules, tx.Data(), tx.AccessList()) diff --git a/core/types/bal/bal_encoding.go b/core/types/bal/bal_encoding.go index 899d60c501..e23b157fd2 100644 --- a/core/types/bal/bal_encoding.go +++ b/core/types/bal/bal_encoding.go @@ -162,12 +162,11 @@ func (e BlockAccessList) ValidateGasLimit(blockGasLimit uint64) error { // Hash computes the keccak256 hash of the access list func (e *BlockAccessList) Hash() common.Hash { var enc bytes.Buffer - err := e.EncodeRLP(&enc) - if err != nil { - // errors here are related to BAL values exceeding maximum size defined - // by the spec. Hard-fail because these cases are not expected to be hit - // under reasonable conditions. - panic(err) + if err := e.EncodeRLP(&enc); err != nil { + // Errors here are related to BAL values exceeding maximum size defined + // by the spec. Return empty hash because these cases are not expected + // to be hit under reasonable conditions. + return common.Hash{} } /* bal, err := json.MarshalIndent(e.StringableRepresentation(), "", " ") @@ -431,7 +430,7 @@ func (e *AccountAccess) Copy() AccountAccess { res := AccountAccess{ Address: e.Address, StorageReads: slices.Clone(e.StorageReads), - BalanceChanges: slices.Clone(e.BalanceChanges), + BalanceChanges: make([]encodingBalanceChange, 0, len(e.BalanceChanges)), NonceChanges: slices.Clone(e.NonceChanges), } for _, storageWrite := range e.StorageChanges { @@ -477,6 +476,7 @@ func (a *ConstructionAccountAccesses) toEncodingObj(addr common.Address) Account indices := slices.Collect(maps.Keys(slotWrites)) slices.SortFunc(indices, cmp.Compare[uint32]) for _, index := range indices { + val := slotWrites[index] obj.Accesses = append(obj.Accesses, encodingStorageWrite{ TxIdx: index, ValueAfter: NewEncodedStorageFromHash(slotWrites[index]), @@ -536,7 +536,7 @@ func (c *ConstructionBlockAccessList) ToEncodingObj() *BlockAccessList { } slices.SortFunc(addresses, common.Address.Cmp) - var res BlockAccessList + res := make(BlockAccessList, 0, len(addresses)) for _, addr := range addresses { res = append(res, c.list[addr].toEncodingObj(addr)) } diff --git a/core/types/bal/bal_encoding_rlp_generated.go b/core/types/bal/bal_encoding_rlp_generated.go index f65a688929..6db8ba0bcf 100644 --- a/core/types/bal/bal_encoding_rlp_generated.go +++ b/core/types/bal/bal_encoding_rlp_generated.go @@ -2,10 +2,13 @@ package bal -import "github.com/ethereum/go-ethereum/common" -import "github.com/ethereum/go-ethereum/rlp" -import "github.com/holiman/uint256" -import "io" +import ( + "io" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/rlp" + "github.com/holiman/uint256" +) func (obj *AccountAccess) EncodeRLP(_w io.Writer) error { w := rlp.NewEncoderBuffer(_w) diff --git a/core/types/bal/bal_test.go b/core/types/bal/bal_test.go index ebf3abf4a2..2bc29fe091 100644 --- a/core/types/bal/bal_test.go +++ b/core/types/bal/bal_test.go @@ -95,7 +95,7 @@ func TestBALEncoding(t *testing.T) { t.Fatalf("encoding failed: %v\n", err) } var dec BlockAccessList - if err := dec.DecodeRLP(rlp.NewStream(bytes.NewReader(buf.Bytes()), 10000000)); err != nil { + if err := dec.DecodeRLP(rlp.NewStream(bytes.NewReader(buf.Bytes()), 0)); err != nil { t.Fatalf("decoding failed: %v\n", err) } if dec.Hash() != bal.ToEncodingObj().Hash() { @@ -114,7 +114,11 @@ func makeTestAccountAccess(sort bool) AccountAccess { storageReads []common.Hash balances []encodingBalanceChange nonces []encodingAccountNonce + codes []encodingCodeChange ) + randSlot := func() *uint256.Int { + return new(uint256.Int).SetBytes(testrand.Bytes(32)) + } for i := 0; i < 5; i++ { slot := encodingSlotWrites{ Slot: NewEncodedStorageFromHash(testrand.Hash()), @@ -139,7 +143,7 @@ func makeTestAccountAccess(sort bool) AccountAccess { } for i := 0; i < 5; i++ { - storageReads = append(storageReads, testrand.Hash()) + storageReads = append(storageReads, randSlot()) } if sort { slices.SortFunc(storageReads, func(a, b common.Hash) int { @@ -200,7 +204,7 @@ func makeTestBAL(sort bool) BlockAccessList { return bytes.Compare(a.Address[:], b.Address[:]) }) } - return list + return &list } func TestBlockAccessListCopy(t *testing.T) { diff --git a/core/vm/contracts_fuzz_test.go b/core/vm/contracts_fuzz_test.go index 988cdb91f2..4d28df6a6a 100644 --- a/core/vm/contracts_fuzz_test.go +++ b/core/vm/contracts_fuzz_test.go @@ -37,7 +37,7 @@ func FuzzPrecompiledContracts(f *testing.F) { return } inWant := string(input) - RunPrecompiledContract(nil, p, a, input, NewGasBudget(gas), nil, params.Rules{}) + RunPrecompiledContract(nil, p, a, input, NewGasBudget(gas, 0), nil, params.Rules{}) if inHave := string(input); inWant != inHave { t.Errorf("Precompiled %v modified input data", a) } diff --git a/core/vm/contracts_test.go b/core/vm/contracts_test.go index e7841c8552..c6975bd0a6 100644 --- a/core/vm/contracts_test.go +++ b/core/vm/contracts_test.go @@ -100,7 +100,7 @@ func testPrecompiled(addr string, test precompiledTest, t *testing.T) { in := common.Hex2Bytes(test.Input) gas := p.RequiredGas(in) t.Run(fmt.Sprintf("%s-Gas=%d", test.Name, gas), func(t *testing.T) { - if res, _, err := RunPrecompiledContract(nil, p, common.HexToAddress(addr), in, NewGasBudget(gas), nil, params.Rules{}); err != nil { + if res, _, err := RunPrecompiledContract(nil, p, common.HexToAddress(addr), in, NewGasBudget(gas, 0), nil, params.Rules{}); err != nil { t.Error(err) } else if common.Bytes2Hex(res) != test.Expected { t.Errorf("Expected %v, got %v", test.Expected, common.Bytes2Hex(res)) @@ -122,7 +122,7 @@ func testPrecompiledOOG(addr string, test precompiledTest, t *testing.T) { gas := test.Gas - 1 t.Run(fmt.Sprintf("%s-Gas=%d", test.Name, gas), func(t *testing.T) { - _, _, err := RunPrecompiledContract(nil, p, common.HexToAddress(addr), in, NewGasBudget(gas), nil, params.Rules{}) + _, _, err := RunPrecompiledContract(nil, p, common.HexToAddress(addr), in, NewGasBudget(gas, 0), nil, params.Rules{}) if err.Error() != "out of gas" { t.Errorf("Expected error [out of gas], got [%v]", err) } @@ -139,7 +139,7 @@ func testPrecompiledFailure(addr string, test precompiledFailureTest, t *testing in := common.Hex2Bytes(test.Input) gas := p.RequiredGas(in) t.Run(test.Name, func(t *testing.T) { - _, _, err := RunPrecompiledContract(nil, p, common.HexToAddress(addr), in, NewGasBudget(gas), nil, params.Rules{}) + _, _, err := RunPrecompiledContract(nil, p, common.HexToAddress(addr), in, NewGasBudget(gas, 0), nil, params.Rules{}) if err.Error() != test.ExpectedError { t.Errorf("Expected error [%v], got [%v]", test.ExpectedError, err) } @@ -170,7 +170,7 @@ func benchmarkPrecompiled(addr string, test precompiledTest, bench *testing.B) { start := time.Now() for bench.Loop() { copy(data, in) - res, _, err = RunPrecompiledContract(nil, p, common.HexToAddress(addr), data, NewGasBudget(reqGas), nil, params.Rules{}) + res, _, err = RunPrecompiledContract(nil, p, common.HexToAddress(addr), data, NewGasBudget(reqGas, 0), nil, params.Rules{}) } elapsed := uint64(time.Since(start)) if elapsed < 1 { diff --git a/core/vm/eips.go b/core/vm/eips.go index 54e5cb0c60..aa03453cd8 100644 --- a/core/vm/eips.go +++ b/core/vm/eips.go @@ -44,6 +44,7 @@ var activators = map[int]func(*JumpTable){ 7939: enable7939, 8024: enable8024, 7843: enable7843, + 8037: enable8037, } // EnableEIP enables the given EIP on the config. @@ -169,6 +170,13 @@ func enable3529(jt *JumpTable) { jt[SELFDESTRUCT].dynamicGas = gasSelfdestructEIP3529 } +// enable8037 enables EIP-8037 SSTORE repricing: the regular-gas portion of +// new slot creation and same-tx 0→X→0 reset is reduced; the state-gas +// portion is charged/refunded at frame-end via the journal. +func enable8037(jt *JumpTable) { + jt[SSTORE].dynamicGas = gasSStoreEIP8037 +} + // enable3198 applies EIP-3198 (BASEFEE Opcode) // - Adds an opcode that returns the current block's base fee. func enable3198(jt *JumpTable) { diff --git a/core/vm/evm.go b/core/vm/evm.go index 59e301c0a7..a0250bfdc0 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -58,15 +58,16 @@ type BlockContext struct { GetHash GetHashFunc // Block information - Coinbase common.Address // Provides information for COINBASE - GasLimit uint64 // Provides information for GASLIMIT - BlockNumber *big.Int // Provides information for NUMBER - Time uint64 // Provides information for TIME - Difficulty *big.Int // Provides information for DIFFICULTY - BaseFee *big.Int // Provides information for BASEFEE (0 if vm runs with NoBaseFee flag and 0 gas price) - BlobBaseFee *big.Int // Provides information for BLOBBASEFEE (0 if vm runs with NoBaseFee flag and 0 blob gas price) - Random *common.Hash // Provides information for PREVRANDAO - SlotNum uint64 // Provides information for SLOTNUM + Coinbase common.Address // Provides information for COINBASE + GasLimit uint64 // Provides information for GASLIMIT + BlockNumber *big.Int // Provides information for NUMBER + Time uint64 // Provides information for TIME + Difficulty *big.Int // Provides information for DIFFICULTY + BaseFee *big.Int // Provides information for BASEFEE (0 if vm runs with NoBaseFee flag and 0 gas price) + BlobBaseFee *big.Int // Provides information for BLOBBASEFEE (0 if vm runs with NoBaseFee flag and 0 blob gas price) + Random *common.Hash // Provides information for PREVRANDAO + SlotNum uint64 // Provides information for SLOTNUM + CostPerStateByte uint64 // EIP-8037: per-byte state creation cost } // TxContext provides the EVM with information about a transaction. @@ -254,7 +255,7 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g if !syscall && !value.IsZero() && !evm.Context.CanTransfer(evm.StateDB, caller, value) { return nil, gas, ErrInsufficientBalance } - snapshot := evm.StateDB.Snapshot() + snapshot1 := evm.StateDB.Snapshot() p, isPrecompile := evm.precompile(addr) if !evm.StateDB.Exist(addr) { if !isPrecompile && evm.chainRules.IsEIP4762 && !isSystemCall(caller) { @@ -267,7 +268,7 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g // Thus, only pay for the creation of the code hash leaf here. wgas := evm.AccessEvents.CodeHashGas(addr, true, gas.RegularGas, false) if _, ok := gas.Charge(GasCosts{RegularGas: wgas}); !ok { - evm.StateDB.RevertToSnapshot(snapshot) + evm.StateDB.RevertToSnapshot(snapshot1) gas.Exhaust() return nil, gas, ErrOutOfGas } @@ -275,6 +276,7 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g if !isPrecompile && evm.chainRules.IsEIP158 && value.IsZero() { // Calling a non-existing account, don't do anything. + evm.StateDB.CloseSnapshot(snapshot1) return nil, gas, nil } evm.StateDB.CreateAccount(addr) @@ -286,6 +288,9 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g evm.Context.Transfer(evm.StateDB, caller, addr, value, &evm.chainRules) } + // Second snapshot: callee execution frame. + snapshot2 := evm.StateDB.Snapshot() + if isPrecompile { ret, gas, err = RunPrecompiledContract(evm.StateDB, p, addr, input, gas, evm.Config.Tracer, evm.chainRules) } else { @@ -306,16 +311,31 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g // above we revert to the snapshot and consume any gas remaining. Additionally, // when we're in homestead this also counts for code storage gas errors. if err != nil { - evm.StateDB.RevertToSnapshot(snapshot) + evm.StateDB.RevertToSnapshot(snapshot1) if err != ErrExecutionReverted { if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil { evm.Config.Tracer.OnGasChange(gas.RegularGas, 0, tracing.GasChangeCallFailedExecution) } gas.Exhaust() } - // TODO: consider clearing up unused snapshots: - //} else { - // evm.StateDB.DiscardSnapshot(snapshot) + } else { + if evm.chainRules.IsAmsterdam { + // Charge callee's state changes to the callee's gas. + bytesCharged := evm.StateDB.StateChangedBytes(snapshot2, false) + stateGasCost := GasCosts{StateGas: bytesCharged * int64(evm.Context.CostPerStateByte)} + if !gas.CanAfford(stateGasCost) { + evm.StateDB.RevertToSnapshot(snapshot1) + gas.Exhaust() + return ret, gas, ErrOutOfGas + } + gas.Charge(stateGasCost) + } + evm.StateDB.CloseSnapshot(snapshot2) + if evm.chainRules.IsAmsterdam { + // Cache parents costs (excluding subcalls) + evm.StateDB.StateChangedBytes(snapshot1, true) + } + evm.StateDB.CloseSnapshot(snapshot1) } return ret, gas, err } @@ -367,6 +387,17 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt } gas.Exhaust() } + } else { + if evm.chainRules.IsAmsterdam { + bytesCharged := evm.StateDB.StateChangedBytes(snapshot, false) + stateGasCost := GasCosts{StateGas: bytesCharged * int64(evm.Context.CostPerStateByte)} + if !gas.CanAfford(stateGasCost) { + gas.Exhaust() + return ret, gas, ErrOutOfGas + } + gas.Charge(stateGasCost) + } + evm.StateDB.CloseSnapshot(snapshot) } return ret, gas, err } @@ -411,7 +442,19 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address, } gas.Exhaust() } + } else { + if evm.chainRules.IsAmsterdam { + bytesCharged := evm.StateDB.StateChangedBytes(snapshot, false) + stateGasCost := GasCosts{StateGas: bytesCharged * int64(evm.Context.CostPerStateByte)} + if !gas.CanAfford(stateGasCost) { + gas.Exhaust() + return ret, gas, ErrOutOfGas + } + gas.Charge(stateGasCost) + } + evm.StateDB.CloseSnapshot(snapshot) } + return ret, gas, err } @@ -466,6 +509,8 @@ func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []b } gas.Exhaust() } + } else { + evm.StateDB.CloseSnapshot(snapshot) } return ret, gas, err } @@ -528,7 +573,7 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value // Create a new account on the state only if the object was not present. // It might be possible the contract code is deployed to a pre-existent // account with non-zero balance. - snapshot := evm.StateDB.Snapshot() + snapshot1 := evm.StateDB.Snapshot() if !evm.StateDB.Exist(address) { evm.StateDB.CreateAccount(address) } @@ -555,6 +600,9 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value } evm.Context.Transfer(evm.StateDB, caller, address, value, &evm.chainRules) + // Second snapshot: initcode execution frame. + snapshot2 := 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) @@ -566,10 +614,29 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value ret, err = evm.initNewContract(contract, address) if err != nil && (evm.chainRules.IsHomestead || err != ErrCodeStoreOutOfGas) { - evm.StateDB.RevertToSnapshot(snapshot) + // Revert to snapshot1 to undo both account creation and initcode changes. + evm.StateDB.RevertToSnapshot(snapshot1) if err != ErrExecutionReverted { contract.UseGas(GasCosts{RegularGas: contract.Gas.RegularGas}, evm.Config.Tracer, tracing.GasChangeCallFailedExecution) } + } else { + if evm.chainRules.IsAmsterdam { + // Charge initcode's state changes to the created contract's gas. + bytesCharged := evm.StateDB.StateChangedBytes(snapshot2, false) + stateGasCost := GasCosts{StateGas: bytesCharged * int64(evm.Context.CostPerStateByte)} + if !contract.Gas.CanAfford(stateGasCost) { + evm.StateDB.RevertToSnapshot(snapshot1) + contract.Gas.Exhaust() + return ret, address, contract.Gas, ErrOutOfGas + } + contract.Gas.Charge(stateGasCost) + } + evm.StateDB.CloseSnapshot(snapshot2) + if evm.chainRules.IsAmsterdam { + // Cache snapshot1's state bytes (exclude subcalls) + evm.StateDB.StateChangedBytes(snapshot1, true) + } + evm.StateDB.CloseSnapshot(snapshot1) } return ret, address, contract.Gas, err } @@ -593,7 +660,15 @@ func (evm *EVM) initNewContract(contract *Contract, address common.Address) ([]b } if !evm.chainRules.IsEIP4762 { - createDataGas := uint64(len(ret)) * params.CreateDataGas + var createDataGas uint64 + if evm.chainRules.IsAmsterdam { + // EIP-8037: regular gas portion is the keccak hashing cost + // (6 × ⌈L/32⌉). The state-gas portion (L × CPSB) is charged + // at frame end via the journal's codeChange walker. + createDataGas = ((uint64(len(ret)) + 31) / 32) * params.Keccak256WordGas + } else { + createDataGas = uint64(len(ret)) * params.CreateDataGas + } if !contract.UseGas(GasCosts{RegularGas: createDataGas}, evm.Config.Tracer, tracing.GasChangeCallCodeStorage) { return ret, ErrCodeStoreOutOfGas } diff --git a/core/vm/gas_table_test.go b/core/vm/gas_table_test.go index 16ce651a7d..7fe8dbea57 100644 --- a/core/vm/gas_table_test.go +++ b/core/vm/gas_table_test.go @@ -97,7 +97,7 @@ func TestEIP2200(t *testing.T) { Transfer: func(StateDB, common.Address, common.Address, *uint256.Int, *params.Rules) {}, } evm := NewEVM(vmctx, statedb, params.AllEthashProtocolChanges, Config{ExtraEips: []int{2200}}) - initialGas := NewGasBudget(tt.gaspool) + initialGas := NewGasBudget(tt.gaspool, 0) _, leftOver, err := evm.Call(common.Address{}, address, nil, initialGas.Copy(), new(uint256.Int)) if !errors.Is(err, tt.failure) { t.Errorf("test %d: failure mismatch: have %v, want %v", i, err, tt.failure) @@ -157,7 +157,7 @@ func TestCreateGas(t *testing.T) { } evm := NewEVM(vmctx, statedb, chainConfig, config) - initialGas := NewGasBudget(uint64(testGas)) + initialGas := NewGasBudget(uint64(testGas), 0) ret, leftOver, err := evm.Call(common.Address{}, address, nil, initialGas.Copy(), new(uint256.Int)) if err != nil { return false diff --git a/core/vm/gascosts.go b/core/vm/gascosts.go index cc90c54798..4f8f60f63b 100644 --- a/core/vm/gascosts.go +++ b/core/vm/gascosts.go @@ -18,17 +18,29 @@ package vm import "fmt" +// GasUsed is the per-frame accumulator for gas consumption. +// StateGas is signed, because it can be negative in a 0 -> x -> 0 scenario. +type GasUsed struct { + RegularGas uint64 + StateGas int64 +} + +func (g *GasUsed) Add(costs GasCosts) { + g.RegularGas += costs.RegularGas + g.StateGas += costs.StateGas +} + // GasCosts denotes a vector of gas costs in the // multidimensional metering paradigm. It represents the cost // charged by an individual operation. type GasCosts struct { RegularGas uint64 - StateGas uint64 + StateGas int64 } // Sum returns the total gas (regular + state). func (g GasCosts) Sum() uint64 { - return g.RegularGas + g.StateGas + return g.RegularGas + uint64(g.StateGas) } // String returns a visual representation of the gas vector. @@ -43,23 +55,30 @@ func (g GasCosts) String() string { type GasBudget struct { RegularGas uint64 // The leftover gas for execution and state gas usage StateGas uint64 // The state gas reservoir + + // Tracks the gas refunds in this call frame. Needed so we can + // revert the refunds if the call frame reverts. + StateGasRefund uint64 } -// NewGasBudget creates a GasBudget with the given initial regular gas allowance. -func NewGasBudget(gas uint64) GasBudget { +// NewGasBudgetReg creates a GasBudget with the given initial regular gas allowance. +func NewGasBudgetReg(gas uint64) GasBudget { return GasBudget{RegularGas: gas} } -// Used returns the amount of regular gas consumed so far. -func (g GasBudget) Used(initial GasBudget) uint64 { - return initial.RegularGas - g.RegularGas +// NewGasBudget creates a GasBudget with the given regular and state gas allowances. +func NewGasBudget(regular, state uint64) GasBudget { + return GasBudget{RegularGas: regular, StateGas: state} } -// Exhaust sets all remaining gas to zero, preserving the initial amount -// for usage tracking. +// Used returns the total amount of gas consumed so far (regular + state). +func (g GasBudget) Used(initial GasBudget) uint64 { + return (initial.RegularGas + initial.StateGas) - (g.RegularGas + g.StateGas) +} + +// Exhaust burns the remaining regular gas on exceptional halt. func (g *GasBudget) Exhaust() { g.RegularGas = 0 - g.StateGas = 0 } func (g *GasBudget) Copy() GasBudget { @@ -72,26 +91,51 @@ func (g GasBudget) String() string { } // CanAfford reports whether the budget has sufficient gas to cover the cost. +// When state gas exceeds the reservoir, the excess spills to regular gas. func (g GasBudget) CanAfford(cost GasCosts) bool { - return g.RegularGas >= cost.RegularGas + if g.RegularGas < cost.RegularGas { + return false + } + if cost.StateGas < 0 { + return true + } + if uint64(cost.StateGas) > g.StateGas { + spillover := uint64(cost.StateGas) - g.StateGas + if spillover > g.RegularGas-cost.RegularGas { + return false + } + } + return true } // Charge deducts the given gas cost from the budget. It returns the -// pre-charge gas value and false if the budget does not have sufficient -// gas to cover the cost. +// pre-charge regular gas value and false if the budget does not have +// sufficient gas to cover the cost. func (g *GasBudget) Charge(cost GasCosts) (uint64, bool) { prior := g.RegularGas - if prior < cost.RegularGas { + if !g.CanAfford(cost) { return prior, false } g.RegularGas -= cost.RegularGas + if cost.StateGas < 0 { + g.StateGas -= uint64(cost.StateGas) + return prior, true + } + if uint64(cost.StateGas) > g.StateGas { + spillover := uint64(cost.StateGas) - g.StateGas + g.StateGas = 0 + g.RegularGas -= spillover + } else { + g.StateGas -= uint64(cost.StateGas) + } return prior, true } -// Refund adds the given gas budget back. It returns the pre-refund gas +// Refund adds the given gas budget back. It returns the pre-refund regular gas // value and whether the budget was actually changed. func (g *GasBudget) Refund(other GasBudget) (uint64, bool) { prior := g.RegularGas g.RegularGas += other.RegularGas - return prior, g.RegularGas != prior + g.StateGas += other.StateGas + return prior, other.RegularGas != 0 || other.StateGas != 0 } diff --git a/core/vm/instructions.go b/core/vm/instructions.go index 3311af0d22..d4d342281d 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -669,7 +669,7 @@ func opCreate(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { scope.Contract.UseGas(GasCosts{RegularGas: gas}, evm.Config.Tracer, tracing.GasChangeCallContractCreation) - res, addr, returnGas, suberr := evm.Create(scope.Contract.Address(), input, NewGasBudget(gas), &value) + res, addr, returnGas, suberr := evm.Create(scope.Contract.Address(), input, NewGasBudgetReg(gas), &value) // Push item on the stack based on the returned error. If the ruleset is // homestead we must check for CodeStoreOutOfGasError (homestead only // rule) and treat as an error, if the ruleset is frontier we must @@ -710,7 +710,7 @@ func opCreate2(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { scope.Contract.UseGas(GasCosts{RegularGas: gas}, evm.Config.Tracer, tracing.GasChangeCallContractCreation2) // reuse size int for stackvalue stackvalue := size - res, addr, returnGas, suberr := evm.Create2(scope.Contract.Address(), input, NewGasBudget(gas), + res, addr, returnGas, suberr := evm.Create2(scope.Contract.Address(), input, NewGasBudgetReg(gas), &endowment, &salt) // Push item on the stack based on the returned error. if suberr != nil { @@ -747,7 +747,7 @@ func opCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { if !value.IsZero() { gas += params.CallStipend } - ret, returnGas, err := evm.Call(scope.Contract.Address(), toAddr, args, NewGasBudget(gas), &value) + ret, returnGas, err := evm.Call(scope.Contract.Address(), toAddr, args, NewGasBudgetReg(gas), &value) if err != nil { temp.Clear() @@ -781,7 +781,7 @@ func opCallCode(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { gas += params.CallStipend } - ret, returnGas, err := evm.CallCode(scope.Contract.Address(), toAddr, args, NewGasBudget(gas), &value) + ret, returnGas, err := evm.CallCode(scope.Contract.Address(), toAddr, args, NewGasBudgetReg(gas), &value) if err != nil { temp.Clear() } else { @@ -810,7 +810,7 @@ func opDelegateCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { // Get arguments from the memory. args := scope.Memory.GetPtr(inOffset.Uint64(), inSize.Uint64()) - ret, returnGas, err := evm.DelegateCall(scope.Contract.Caller(), scope.Contract.Address(), toAddr, args, NewGasBudget(gas), scope.Contract.value) + ret, returnGas, err := evm.DelegateCall(scope.Contract.Caller(), scope.Contract.Address(), toAddr, args, NewGasBudgetReg(gas), scope.Contract.value) if err != nil { temp.Clear() } else { @@ -839,7 +839,7 @@ func opStaticCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { // Get arguments from the memory. args := scope.Memory.GetPtr(inOffset.Uint64(), inSize.Uint64()) - ret, returnGas, err := evm.StaticCall(scope.Contract.Address(), toAddr, args, NewGasBudget(gas)) + ret, returnGas, err := evm.StaticCall(scope.Contract.Address(), toAddr, args, NewGasBudgetReg(gas)) if err != nil { temp.Clear() } else { diff --git a/core/vm/interface.go b/core/vm/interface.go index 4d75dd46d0..cab36822ed 100644 --- a/core/vm/interface.go +++ b/core/vm/interface.go @@ -87,6 +87,12 @@ type StateDB interface { Prepare(rules params.Rules, sender, coinbase common.Address, dest *common.Address, precompiles []common.Address, txAccesses types.AccessList) RevertToSnapshot(int) + + // CloseSnapshot marks the given snapshot's call frame as completed without + // reverting any state. The call frame's entry range is recorded on the + // parent frame so the parent can later iterate its own entries while + // skipping over closed children. Snapshots must be closed in LIFO order. + CloseSnapshot(int) Snapshot() int AddLog(*types.Log) @@ -99,4 +105,15 @@ type StateDB interface { // Finalise must be invoked at the end of a transaction Finalise(bool) (*bal.StateAccessList, *bal.StateMutations) + + // StateChangedBytes returns the number of state bytes created by the + // call frame identified by the given snapshot ID. When excludeSubcalls + // is true, cached subcall costs are not added to the total. Used by + // EIP-8037 for state gas metering. + StateChangedBytes(revid int, excludeSubcalls bool) int64 + + // SelfDestructRefundBytes returns the total state bytes to refund at + // tx-end for accounts that were both created and selfdestructed during + // this transaction (per EIP-6780). + SelfDestructRefundBytes() int64 } diff --git a/core/vm/interpreter_test.go b/core/vm/interpreter_test.go index 69c2316907..a788da8265 100644 --- a/core/vm/interpreter_test.go +++ b/core/vm/interpreter_test.go @@ -55,7 +55,7 @@ func TestLoopInterrupt(t *testing.T) { timeout := make(chan bool) go func(evm *EVM) { - _, _, err := evm.Call(common.Address{}, address, nil, NewGasBudget(math.MaxUint64), new(uint256.Int)) + _, _, err := evm.Call(common.Address{}, address, nil, NewGasBudget(math.MaxUint64, 0), new(uint256.Int)) errChannel <- err }(evm) @@ -85,7 +85,7 @@ func BenchmarkInterpreter(b *testing.B) { value = uint256.NewInt(0) stack = newstack() mem = NewMemory() - contract = NewContract(common.Address{}, common.Address{}, value, NewGasBudget(startGas), nil) + contract = NewContract(common.Address{}, common.Address{}, value, NewGasBudget(startGas, 0), nil) ) stack.push(uint256.NewInt(123)) stack.push(uint256.NewInt(123)) diff --git a/core/vm/jump_table.go b/core/vm/jump_table.go index 82fc43ec13..62ee31e3a1 100644 --- a/core/vm/jump_table.go +++ b/core/vm/jump_table.go @@ -97,6 +97,7 @@ func newAmsterdamInstructionSet() JumpTable { instructionSet := newOsakaInstructionSet() enable7843(&instructionSet) // EIP-7843 (SLOTNUM opcode) enable8024(&instructionSet) // EIP-8024 (Backward compatible SWAPN, DUPN, EXCHANGE) + enable8037(&instructionSet) // EIP-8037 SSTORE repricing return validate(instructionSet) } diff --git a/core/vm/operations_acl.go b/core/vm/operations_acl.go index 313d03819e..87941a5ae1 100644 --- a/core/vm/operations_acl.go +++ b/core/vm/operations_acl.go @@ -222,8 +222,80 @@ var ( // gasSStoreEIP3529 implements gas cost for SSTORE according to EIP-3529 // Replace `SSTORE_CLEARS_SCHEDULE` with `SSTORE_RESET_GAS + ACCESS_LIST_STORAGE_KEY_COST` (4,800) gasSStoreEIP3529 = makeGasSStoreFunc(params.SstoreClearsScheduleRefundEIP3529) + + // gasSStoreEIP8037 implements gas cost for SSTORE under EIP-8037. + // New slot creation (orig=0, current=0, value!=0) is repriced from + // SstoreSetGas (20,000) to SstoreUpdateGas - ColdSloadCost (2,900); the + // state-gas portion (32 × CPSB) is charged at frame-end via the journal. + // Likewise the same-tx 0→X→0 reset refund is reduced from 19,900 to + // SstoreUpdateGas - ColdSloadCost - WarmStorageReadCost (2,800); the + // state-gas refund is also handled at frame-end. + gasSStoreEIP8037 = makeGasSStoreFuncAmsterdam(params.SstoreClearsScheduleRefundEIP3529) ) +// makeGasSStoreFuncAmsterdam returns the EIP-8037 SSTORE gas function. It is +// identical to makeGasSStoreFunc except that the regular-gas portion of new +// slot creation and same-tx 0→X→0 reset is reduced (the state-gas portion is +// charged/refunded at frame-end via the journal). +func makeGasSStoreFuncAmsterdam(clearingRefund uint64) gasFunc { + return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { + if evm.readOnly { + return GasCosts{}, ErrWriteProtection + } + if contract.Gas.RegularGas <= params.SstoreSentryGasEIP2200 { + return GasCosts{}, errors.New("not enough gas for reentrancy sentry") + } + var ( + y, x = stack.Back(1), stack.peek() + slot = common.Hash(x.Bytes32()) + current, original = evm.StateDB.GetStateAndCommittedState(contract.Address(), slot) + cost = uint64(0) + ) + if _, slotPresent := evm.StateDB.SlotInAccessList(contract.Address(), slot); !slotPresent { + cost = params.ColdSloadCostEIP2929 + evm.StateDB.AddSlotToAccessList(contract.Address(), slot) + } + value := common.Hash(y.Bytes32()) + + // EIP-8037: regular-gas portion of new slot creation is the storage + // update cost minus cold sload (2,900). State-gas portion is at + // frame-end. + sstoreNewSlotRegularGas := params.SstoreResetGasEIP2200 - params.ColdSloadCostEIP2929 + + if current == value { // noop + return GasCosts{RegularGas: cost + params.WarmStorageReadCostEIP2929}, nil + } + if original == current { + if original == (common.Hash{}) { // create slot (2.1.1) + return GasCosts{RegularGas: cost + sstoreNewSlotRegularGas}, nil + } + if value == (common.Hash{}) { // delete pre-existing slot + evm.StateDB.AddRefund(clearingRefund) + } + return GasCosts{RegularGas: cost + (params.SstoreResetGasEIP2200 - params.ColdSloadCostEIP2929)}, nil + } + if original != (common.Hash{}) { + if current == (common.Hash{}) { // recreate slot (2.2.1.1) + evm.StateDB.SubRefund(clearingRefund) + } else if value == (common.Hash{}) { // delete dirty (2.2.1.2) + evm.StateDB.AddRefund(clearingRefund) + } + } + if original == value { + if original == (common.Hash{}) { // 0→X→0: reset to original-zero + // EIP-8037: regular-gas refund is reduced because the + // original SET cost was already reduced to + // sstoreNewSlotRegularGas. State-gas refund (32 × CPSB) + // is applied at frame-end. + evm.StateDB.AddRefund(sstoreNewSlotRegularGas - params.WarmStorageReadCostEIP2929) + } else { // reset to original existing slot + evm.StateDB.AddRefund((params.SstoreResetGasEIP2200 - params.ColdSloadCostEIP2929) - params.WarmStorageReadCostEIP2929) + } + } + return GasCosts{RegularGas: cost + params.WarmStorageReadCostEIP2929}, nil + } +} + // makeSelfdestructGasFn can create the selfdestruct dynamic gas function for EIP-2929 and EIP-3529 func makeSelfdestructGasFn(refundsEnabled bool) gasFunc { gasFunc := func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { diff --git a/core/vm/runtime/runtime.go b/core/vm/runtime/runtime.go index 4fafdf3a50..eefa9ea5f7 100644 --- a/core/vm/runtime/runtime.go +++ b/core/vm/runtime/runtime.go @@ -148,7 +148,7 @@ func Execute(code, input []byte, cfg *Config) ([]byte, *state.StateDB, error) { cfg.Origin, common.BytesToAddress([]byte("contract")), input, - vm.NewGasBudget(cfg.GasLimit), + vm.NewGasBudgetReg(cfg.GasLimit), uint256.MustFromBig(cfg.Value), ) if cfg.EVMConfig.Tracer != nil && cfg.EVMConfig.Tracer.OnTxEnd != nil { @@ -182,7 +182,7 @@ func Create(input []byte, cfg *Config) ([]byte, common.Address, uint64, error) { code, address, leftOverGas, err := vmenv.Create( cfg.Origin, input, - vm.NewGasBudget(cfg.GasLimit), + vm.NewGasBudgetReg(cfg.GasLimit), uint256.MustFromBig(cfg.Value), ) if cfg.EVMConfig.Tracer != nil && cfg.EVMConfig.Tracer.OnTxEnd != nil { @@ -217,7 +217,7 @@ func Call(address common.Address, input []byte, cfg *Config) ([]byte, uint64, er cfg.Origin, address, input, - vm.NewGasBudget(cfg.GasLimit), + vm.NewGasBudgetReg(cfg.GasLimit), uint256.MustFromBig(cfg.Value), ) if cfg.EVMConfig.Tracer != nil && cfg.EVMConfig.Tracer.OnTxEnd != nil { diff --git a/eth/catalyst/api_test.go b/eth/catalyst/api_test.go index 1f38c4dd8a..6abe217062 100644 --- a/eth/catalyst/api_test.go +++ b/eth/catalyst/api_test.go @@ -2111,3 +2111,116 @@ func runGetBlobs(t testing.TB, getBlobs getBlobsFn, start, limit int, fillRandom t.Fatalf("Unexpected result for case %s", name) } } + +// TestFailedContractCreationGas tests that a failed contract creation under +// Amsterdam/EIP-8037 computes the correct gasUsed in the receipt. This +// reproduces a cross-client mismatch where Nethermind and geth disagree on +// the receipt root due to different gasUsed for a failing CREATE tx. +func TestFailedContractCreationGas(t *testing.T) { + config := *params.MergedTestChainConfig + // The exact init code from the devnet tx that fails with RETURNDATACOPY OOB + initCode := common.Hex2Bytes("7fd13216074149a6e9713267dccbbc37024275d51076bec33169b0a400a8975d657f000000000000000000000000000000000000000000000000000000000000001e5b6202ffff168161ffff1691502061bfc41446717a6cd1bc37217702e2aac1e77e6e8a75f67b6202ffff16816202ffff1691508261ffff1692503e435b61093d62ae347360835960d24a865b7700b64f1bcf1ada04bb8d96db5221a5fa497115d4b936fdd76202ffff168161ffff169150205b8364a804f9fd32877f00000000000000000000000000000000000000000000000000000000000000016000527f00000000000000000000000000000000000000000000000000000000000000026020527f00000000000000000000000000000000000000000000000000000000000001456040527f1050130679e8fbd2a4b3ebdcb21747c5f6efbaecad13f9b45361510394c098c06060526040608060806000600060065af160805160a0515b6202ffff168161ffff169150fd965b606d9a6202ffff1653831c7c5f8bb81ca4686beebb952b5f20db9445c190332b394f1669b230b104fd75f86956a39ab4b498be875bb61ff1412424a8b675c0a1316202ffff165c496b68a43600766e14ae19e7cbcc93060304845b19446202ffff165d02426202ffff168161ffff169150a100") + + gspec := &core.Genesis{ + Config: &config, + Alloc: types.GenesisAlloc{ + testAddr: {Balance: new(big.Int).Mul(big.NewInt(1e6), big.NewInt(params.Ether))}, + params.BeaconRootsAddress: {Balance: common.Big0, Code: params.BeaconRootsCode}, + params.HistoryStorageAddress: {Balance: common.Big0, Code: params.HistoryStorageCode}, + params.WithdrawalQueueAddress: {Balance: common.Big0, Code: params.WithdrawalQueueCode}, + params.ConsolidationQueueAddress: {Balance: common.Big0, Code: params.ConsolidationQueueCode}, + config.DepositContractAddress: {Balance: common.Big0}, + }, + Difficulty: common.Big0, + BaseFee: big.NewInt(params.InitialBaseFee), + GasLimit: 60_000_000, + } + + n, ethservice := startEthService(t, gspec, nil) + defer n.Close() + + api := newConsensusAPIWithoutHeartbeat(ethservice) + parent := ethservice.BlockChain().CurrentBlock() + signer := types.LatestSigner(ethservice.BlockChain().Config()) + + // Create the failing contract creation tx (gas=1000000, value=51417) + tx, _ := types.SignTx(types.NewContractCreation( + 0, + big.NewInt(51417), + 1000000, + big.NewInt(2*params.InitialBaseFee), + initCode, + ), signer, testKey) + ethservice.TxPool().Add([]*types.Transaction{tx}, false) + + slotNumber := uint64(1) + beaconRoot := common.Hash{0x01} + args := &miner.BuildPayloadArgs{ + Parent: parent.Hash(), + Timestamp: parent.Time + 12, + FeeRecipient: common.Address{0xfe}, + Random: common.Hash{0xaa}, + Withdrawals: []*types.Withdrawal{}, + BeaconRoot: &beaconRoot, + SlotNum: &slotNumber, + } + payload, err := api.eth.Miner().BuildPayload(context.Background(), args, false) + if err != nil { + t.Fatalf("BuildPayload failed: %v", err) + } + envelope := payload.ResolveFull() + execPayload := envelope.ExecutionPayload + + // Validate via newPayload + resp, err := api.newPayload(context.Background(), *execPayload, []common.Hash{}, &beaconRoot, envelope.Requests, false) + if err != nil { + t.Fatalf("newPayload error: %v", err) + } + if resp.Status != engine.VALID { + t.Fatalf("newPayload returned %s: %v", resp.Status, resp.ValidationError) + } + + // Set head + fcState := engine.ForkchoiceStateV1{ + HeadBlockHash: execPayload.BlockHash, + SafeBlockHash: execPayload.BlockHash, + FinalizedBlockHash: execPayload.BlockHash, + } + if _, err := api.ForkchoiceUpdatedV1(context.Background(), fcState, nil); err != nil { + t.Fatalf("FCU error: %v", err) + } + + // Get the receipt and check gasUsed + block := ethservice.BlockChain().GetBlockByHash(execPayload.BlockHash) + if block == nil { + t.Fatal("block not found after import") + } + + receipts := ethservice.BlockChain().GetReceiptsByHash(block.Hash()) + if len(receipts) == 0 { + t.Fatal("no receipts found") + } + + // Find the contract creation receipt + for i, r := range receipts { + if r.ContractAddress != (common.Address{}) || r.Status == types.ReceiptStatusFailed { + t.Logf("Receipt %d: status=%d gasUsed=%d cumulative=%d contract=%s", + i, r.Status, r.GasUsed, r.CumulativeGasUsed, r.ContractAddress.Hex()) + + // The tx should fail (RETURNDATACOPY OOB) + if r.Status != types.ReceiptStatusFailed { + t.Errorf("expected failed receipt, got status %d", r.Status) + } + + // Check gasUsed makes sense under EIP-8037 + // For a failed tx: all regular gas is burned, but state gas + // should NOT be charged (since no state was created). + // gasUsed should be < tx.gas if there's any state gas component. + t.Logf("Tx gas limit: %d", tx.Gas()) + if r.GasUsed > tx.Gas() { + t.Errorf("gasUsed %d exceeds tx gas limit %d", r.GasUsed, tx.Gas()) + } + t.Logf("Gas difference (limit - used): %d", tx.Gas()-r.GasUsed) + } + } +} diff --git a/eth/protocols/snap/handler_test.go b/eth/protocols/snap/handler_test.go index 61b5e6abde..eae7805c3b 100644 --- a/eth/protocols/snap/handler_test.go +++ b/eth/protocols/snap/handler_test.go @@ -39,6 +39,8 @@ func makeTestBAL(minSize int) *bal.BlockAccessList { Address: common.HexToAddress("0x01"), StorageReads: make([]*bal.EncodedStorage, n), } + // Use a full-width 32-byte value (top byte 0xff) so each slot still + // encodes to 33 RLP bytes regardless of the index. for i := range access.StorageReads { var slot common.Hash binary.BigEndian.PutUint64(slot[24:], uint64(i)) diff --git a/eth/tracers/js/tracer_test.go b/eth/tracers/js/tracer_test.go index 6570d73575..2fefa46492 100644 --- a/eth/tracers/js/tracer_test.go +++ b/eth/tracers/js/tracer_test.go @@ -55,7 +55,7 @@ func runTrace(tracer *tracers.Tracer, vmctx *vmContext, chaincfg *params.ChainCo gasLimit uint64 = 31000 startGas uint64 = 10000 value = uint256.NewInt(0) - contract = vm.NewContract(common.Address{}, common.Address{}, value, vm.NewGasBudget(startGas), nil) + contract = vm.NewContract(common.Address{}, common.Address{}, value, vm.NewGasBudget(startGas, 0), nil) ) evm.SetTxContext(vmctx.txCtx) contract.Code = []byte{byte(vm.PUSH1), 0x1, byte(vm.PUSH1), 0x1, 0x0} diff --git a/eth/tracers/logger/logger_test.go b/eth/tracers/logger/logger_test.go index decdf588e1..73868d22e0 100644 --- a/eth/tracers/logger/logger_test.go +++ b/eth/tracers/logger/logger_test.go @@ -47,7 +47,7 @@ func TestStoreCapture(t *testing.T) { var ( logger = NewStructLogger(nil) evm = vm.NewEVM(vm.BlockContext{}, &dummyStatedb{}, params.TestChainConfig, vm.Config{Tracer: logger.Hooks()}) - contract = vm.NewContract(common.Address{}, common.Address{}, new(uint256.Int), vm.NewGasBudget(100000), nil) + contract = vm.NewContract(common.Address{}, common.Address{}, new(uint256.Int), vm.NewGasBudget(100000, 0), nil) ) contract.Code = []byte{byte(vm.PUSH1), 0x1, byte(vm.PUSH1), 0x0, byte(vm.SSTORE)} var index common.Hash diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 01c3f99d72..d882256c49 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -1347,6 +1347,7 @@ func AccessList(ctx context.Context, b Backend, blockNrOrHash rpc.BlockNumberOrH } // Prevent redundant operations if args contain more authorizations than EVM may handle + // TODO change with EIP-8037 maxAuthorizations := uint64(*args.Gas) / params.CallNewAccountGas if uint64(len(args.AuthorizationList)) > maxAuthorizations { return nil, 0, nil, errors.New("insufficient gas to process all authorizations") diff --git a/params/protocol_params.go b/params/protocol_params.go index c9a9dbe0f6..2043bd32dc 100644 --- a/params/protocol_params.go +++ b/params/protocol_params.go @@ -88,6 +88,7 @@ const ( LogTopicGas uint64 = 375 // Multiplied by the * of the LOG*, per LOG transaction. e.g. LOG0 incurs 0 * c_txLogTopicGas, LOG4 incurs 4 * c_txLogTopicGas. CreateGas uint64 = 32000 // Once per CREATE operation & contract-creation transaction. Create2Gas uint64 = 32000 // Once per CREATE2 operation + CreateGasAmsterdam uint64 = 9000 // Regular gas portion of CREATE in Amsterdam (EIP-8037); state gas is charged separately. CreateNGasEip4762 uint64 = 1000 // Once per CREATEn operations post-verkle SelfdestructRefundGas uint64 = 24000 // Refunded following a selfdestruct operation. MemoryGas uint64 = 3 // Times the address of the (highest referenced byte in memory + 1). NOTE: referencing happens on read, write and in instructions such as RETURN and CALL. @@ -100,6 +101,7 @@ const ( TxAccessListAddressGas uint64 = 2400 // Per address specified in EIP 2930 access list TxAccessListStorageKeyGas uint64 = 1900 // Per storage key specified in EIP 2930 access list TxAuthTupleGas uint64 = 12500 // Per auth tuple code specified in EIP-7702 + TxAuthTupleRegularGas uint64 = 7500 // Per auth tuple regular gas specified in EIP-8037 // These have been changed during the course of the chain CallGasFrontier uint64 = 40 // Once per CALL operation & message call transaction. diff --git a/tests/state_test.go b/tests/state_test.go index 8444d211cf..b6633e79b4 100644 --- a/tests/state_test.go +++ b/tests/state_test.go @@ -326,7 +326,7 @@ func runBenchmark(b *testing.B, t *StateTest) { b.StartTimer() start := time.Now() - initialGas := vm.NewGasBudget(msg.GasLimit) + initialGas := vm.NewGasBudget(msg.GasLimit, 0) // Execute the message. _, leftOverGas, err := evm.Call(sender.Address(), *msg.To, msg.Data, initialGas.Copy(), uint256.MustFromBig(msg.Value)) diff --git a/tests/transaction_test_util.go b/tests/transaction_test_util.go index 05e924436a..59e3c7804b 100644 --- a/tests/transaction_test_util.go +++ b/tests/transaction_test_util.go @@ -81,7 +81,11 @@ func (tt *TransactionTest) Run() error { return } // Intrinsic gas +<<<<<<< HEAD cost, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules) +======= + cost, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules, 1) +>>>>>>> eip-8037-rewrite if err != nil { return } diff --git a/triedb/pathdb/states.go b/triedb/pathdb/states.go index c54d8b1136..27a6c1d422 100644 --- a/triedb/pathdb/states.go +++ b/triedb/pathdb/states.go @@ -583,6 +583,18 @@ func (s *StateSetWithOrigin) decode(r *rlp.Stream) error { } } s.storageOrigin = storageSet + + // Compute the size of origin data, keeping consistent with NewStateSetWithOrigin + var size int + for _, data := range s.accountOrigin { + size += common.HashLength + len(data) + } + for _, slots := range s.storageOrigin { + for _, data := range slots { + size += 2*common.HashLength + len(data) + } + } + s.size = s.stateSet.size + uint64(size) return nil }