From 2ca74d2ef9314c18a70e396858ddc6dc7de2350f Mon Sep 17 00:00:00 2001 From: Sina Mahmoodi Date: Wed, 22 Apr 2026 17:55:43 +0000 Subject: [PATCH 01/14] eth/fetcher: lazy-allocate hashes slice in scheduleFetches scheduleFetches.func1 is the single biggest allocator in the Pyroscope profile of a busy node (~13.5 GB/hr, 8% of total alloc_space). Each peer-iteration pre-allocated 'make([]common.Hash, 0, maxTxRetrievals)' = 8 KB, even for peers that end up collecting no new hashes (all their announces were already being fetched by someone else). Defer the slice allocation to the first append. Peers that collect zero hashes now pay zero allocation, which is the common case on the timeoutTrigger path where all peers with any announces are iterated. New benchmarks BenchmarkScheduleFetches_{100peers_10new, 100peers_allFetching, 500peers_3new} (benchstat, 6 samples): scenario ns/op B/op allocs/op 100p/10new unchanged unchanged unchanged (fast path) 100p/allFetching -62% -92% -20% 500p/3new -22% -44% -7% geomean -33% -65% -9% --- eth/fetcher/tx_fetcher.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/eth/fetcher/tx_fetcher.go b/eth/fetcher/tx_fetcher.go index 5817dfbcf5..2165b7f103 100644 --- a/eth/fetcher/tx_fetcher.go +++ b/eth/fetcher/tx_fetcher.go @@ -991,8 +991,10 @@ func (f *TxFetcher) scheduleFetches(timer *mclock.Timer, timeout chan struct{}, if len(f.announces[peer]) == 0 { return // continue in the for-each } + // hashes is allocated lazily: peers that collect no new hashes + // (all announces already being fetched) skip the 8KB allocation. var ( - hashes = make([]common.Hash, 0, maxTxRetrievals) + hashes []common.Hash bytes uint64 ) f.forEachAnnounce(f.announces[peer], func(hash common.Hash, meta txMetadata) bool { @@ -1009,6 +1011,9 @@ func (f *TxFetcher) scheduleFetches(timer *mclock.Timer, timeout chan struct{}, f.alternates[hash] = f.announced[hash] delete(f.announced, hash) + if hashes == nil { + hashes = make([]common.Hash, 0, maxTxRetrievals) + } // Accumulate the hash and stop if the limit was reached hashes = append(hashes, hash) if len(hashes) >= maxTxRetrievals { From c876755839b2d6ebffa1c43538c42713e769347d Mon Sep 17 00:00:00 2001 From: Sina M <1591639+s1na@users.noreply.github.com> Date: Fri, 24 Apr 2026 12:12:26 +0200 Subject: [PATCH 02/14] Update eth/fetcher/tx_fetcher.go Co-authored-by: jwasinger --- eth/fetcher/tx_fetcher.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/eth/fetcher/tx_fetcher.go b/eth/fetcher/tx_fetcher.go index 2165b7f103..20621c531d 100644 --- a/eth/fetcher/tx_fetcher.go +++ b/eth/fetcher/tx_fetcher.go @@ -991,8 +991,6 @@ func (f *TxFetcher) scheduleFetches(timer *mclock.Timer, timeout chan struct{}, if len(f.announces[peer]) == 0 { return // continue in the for-each } - // hashes is allocated lazily: peers that collect no new hashes - // (all announces already being fetched) skip the 8KB allocation. var ( hashes []common.Hash bytes uint64 From 8091994e7b5954827ad68ccca463647b220b1621 Mon Sep 17 00:00:00 2001 From: rayoo Date: Fri, 24 Apr 2026 19:37:34 +0800 Subject: [PATCH 03/14] eth/protocols/snap: fix data race on testPeer counters (#34802) The testPeer request counters (nAccountRequests, nStorageRequests, nBytecodeRequests, nTrienodeRequests) were plain int fields incremented with ++. These increments happen in Request* methods that are invoked concurrently by the Syncer from multiple goroutines (assignBytecodeTasks, assignStorageTasks, etc.), causing a data race reliably detected by go test -race. Change the counters to atomic.Int64 so increments and reads are synchronized without introducing a mutex. Fixes races detected in TestMultiSyncManyUseless, TestMultiSyncManyUselessWithLowTimeout, TestMultiSyncManyUnresponsive, TestSyncWithStorageAndOneCappedPeer, TestSyncWithStorageAndCorruptPeer, and TestSyncWithStorageAndNonProvingPeer. --- eth/protocols/snap/sync_test.go | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/eth/protocols/snap/sync_test.go b/eth/protocols/snap/sync_test.go index b11ad4e78a..c506488e91 100644 --- a/eth/protocols/snap/sync_test.go +++ b/eth/protocols/snap/sync_test.go @@ -25,6 +25,7 @@ import ( mrand "math/rand" "slices" "sync" + "sync/atomic" "testing" "time" @@ -142,10 +143,10 @@ type testPeer struct { term func() // counters - nAccountRequests int - nStorageRequests int - nBytecodeRequests int - nTrienodeRequests int + nAccountRequests atomic.Int64 + nStorageRequests atomic.Int64 + nBytecodeRequests atomic.Int64 + nTrienodeRequests atomic.Int64 } func newTestPeer(id string, t *testing.T, term func()) *testPeer { @@ -179,25 +180,25 @@ func (t *testPeer) Stats() string { Storage requests: %d Bytecode requests: %d Trienode requests: %d -`, t.nAccountRequests, t.nStorageRequests, t.nBytecodeRequests, t.nTrienodeRequests) +`, t.nAccountRequests.Load(), t.nStorageRequests.Load(), t.nBytecodeRequests.Load(), t.nTrienodeRequests.Load()) } func (t *testPeer) RequestAccountRange(id uint64, root, origin, limit common.Hash, bytes int) error { t.logger.Trace("Fetching range of accounts", "reqid", id, "root", root, "origin", origin, "limit", limit, "bytes", common.StorageSize(bytes)) - t.nAccountRequests++ + t.nAccountRequests.Add(1) go t.accountRequestHandler(t, id, root, origin, limit, bytes) return nil } func (t *testPeer) RequestTrieNodes(id uint64, root common.Hash, count int, paths []TrieNodePathSet, bytes int) error { t.logger.Trace("Fetching set of trie nodes", "reqid", id, "root", root, "pathsets", len(paths), "bytes", common.StorageSize(bytes)) - t.nTrienodeRequests++ + t.nTrienodeRequests.Add(1) go t.trieRequestHandler(t, id, root, paths, bytes) return nil } func (t *testPeer) RequestStorageRanges(id uint64, root common.Hash, accounts []common.Hash, origin, limit []byte, bytes int) error { - t.nStorageRequests++ + t.nStorageRequests.Add(1) if len(accounts) == 1 && origin != nil { t.logger.Trace("Fetching range of large storage slots", "reqid", id, "root", root, "account", accounts[0], "origin", common.BytesToHash(origin), "limit", common.BytesToHash(limit), "bytes", common.StorageSize(bytes)) } else { @@ -208,7 +209,7 @@ func (t *testPeer) RequestStorageRanges(id uint64, root common.Hash, accounts [] } func (t *testPeer) RequestByteCodes(id uint64, hashes []common.Hash, bytes int) error { - t.nBytecodeRequests++ + t.nBytecodeRequests.Add(1) t.logger.Trace("Fetching set of byte codes", "reqid", id, "hashes", len(hashes), "bytes", common.StorageSize(bytes)) go t.codeRequestHandler(t, id, hashes, bytes) return nil @@ -1901,7 +1902,7 @@ func testSyncAccountPerformance(t *testing.T, scheme string) { // sync cycle starts. When popping the queue, we do not look it up again. // Doing so would bring this number down to zero in this artificial testcase, // but only add extra IO for no reason in practice. - if have, want := src.nTrienodeRequests, 1; have != want { + if have, want := src.nTrienodeRequests.Load(), int64(1); have != want { fmt.Print(src.Stats()) t.Errorf("trie node heal requests wrong, want %d, have %d", want, have) } From b70d9a4b8e409640d5f77af860864e1369dc87fd Mon Sep 17 00:00:00 2001 From: rayoo Date: Fri, 24 Apr 2026 23:30:03 +0800 Subject: [PATCH 04/14] core/state,core/types/bal: copy stateReadList in StateDB.Copy The stateReadList field introduced by #34776 to track the state access footprint for EIP-7928 was not propagated by StateDB.Copy. Every other per-transaction field that lives alongside it (accessList, transientStorage, journal, witness, accessEvents) is copied explicitly, so this field was simply missed. After Copy the copy's stateReadList is nil while the original keeps its entries, so the nil-safe guards on StateAccessList.AddAccount / AddState silently drop every access recorded on the copy. For any post-Amsterdam code path that copies a prepared state and keeps reading from the copy, the BAL footprint becomes incomplete. Add a Copy method on bal.StateAccessList and invoke it from StateDB.Copy, matching the pattern used for accessList and accessEvents. --------- Co-authored-by: jwasinger --- core/state/statedb.go | 3 +++ core/types/bal/access_list.go | 14 ++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/core/state/statedb.go b/core/state/statedb.go index 5d94d4806d..1858f4758d 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -716,6 +716,9 @@ func (s *StateDB) Copy() *StateDB { if s.accessEvents != nil { state.accessEvents = s.accessEvents.Copy() } + if s.stateReadList != nil { + state.stateReadList = s.stateReadList.Copy() + } // Deep copy cached state objects. for addr, obj := range s.stateObjects { state.stateObjects[addr] = obj.deepCopy(state) diff --git a/core/types/bal/access_list.go b/core/types/bal/access_list.go index 91da5ebcb7..e563fa22e2 100644 --- a/core/types/bal/access_list.go +++ b/core/types/bal/access_list.go @@ -78,3 +78,17 @@ func (s *StateAccessList) Merge(other *StateAccessList) { maps.Copy(slots, otherSlots) } } + +// Copy returns a deep copy of the StateAccessList. +func (s *StateAccessList) Copy() *StateAccessList { + if s == nil { + return nil + } + cpy := &StateAccessList{ + list: make(map[common.Address]StorageAccessList, len(s.list)), + } + for addr, slots := range s.list { + cpy.list[addr] = maps.Clone(slots) + } + return cpy +} From b26391773d7cec44dcc52326b5fbfae9cc63f573 Mon Sep 17 00:00:00 2001 From: cui Date: Sun, 26 Apr 2026 17:54:07 +0800 Subject: [PATCH 05/14] core/state: and instead of or (#34819) --- core/state/snapshot/iterator_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 { From 2d5da603717417ffe63d238a92a8cd9dc4e02745 Mon Sep 17 00:00:00 2001 From: rjl493456442 Date: Sun, 26 Apr 2026 23:32:39 +0800 Subject: [PATCH 06/14] core/types/bal: update the BAL definition to the latest spec (#34799) This PR updates the BAL structure definition to the latest the spec, - Balance has been changed from [16]byte to uint256 - Storage key and value has been changed from [32]byte to uint256 - BlockAccessList has been changed from a struct to a slice of AccountChanges - TxIndex has been changed from uint16 to uint32 --- core/types/bal/bal.go | 34 +- core/types/bal/bal_encoding.go | 210 ++++++---- core/types/bal/bal_encoding_rlp_generated.go | 400 +++++++++---------- core/types/bal/bal_test.go | 97 ++--- eth/protocols/snap/handler_test.go | 12 +- 5 files changed, 399 insertions(+), 354 deletions(-) diff --git a/core/types/bal/bal.go b/core/types/bal/bal.go index 86dc8e5426..99ead8d6f0 100644 --- a/core/types/bal/bal.go +++ b/core/types/bal/bal.go @@ -25,13 +25,13 @@ import ( ) // ConstructionAccountAccess contains post-block account state for mutations as well as -// all storage keys that were read during execution. It is used when building block +// all storage keys that were read during execution. It is used when building block // access list during execution. type ConstructionAccountAccess struct { // StorageWrites is the post-state values of an account's storage slots // that were modified in a block, keyed by the slot key and the tx index // where the modification occurred. - StorageWrites map[common.Hash]map[uint16]common.Hash `json:"storageWrites,omitempty"` + StorageWrites map[common.Hash]map[uint32]common.Hash `json:"storageWrites,omitempty"` // StorageReads is the set of slot keys that were accessed during block // execution. @@ -42,25 +42,25 @@ type ConstructionAccountAccess struct { // BalanceChanges contains the post-transaction balances of an account, // keyed by transaction indices where it was changed. - BalanceChanges map[uint16]*uint256.Int `json:"balanceChanges,omitempty"` + BalanceChanges map[uint32]*uint256.Int `json:"balanceChanges,omitempty"` // NonceChanges contains the post-state nonce values of an account keyed // by tx index. - NonceChanges map[uint16]uint64 `json:"nonceChanges,omitempty"` + NonceChanges map[uint32]uint64 `json:"nonceChanges,omitempty"` // CodeChange contains the post-state contract code of an account keyed // by tx index. - CodeChange map[uint16][]byte `json:"codeChange,omitempty"` + CodeChange map[uint32][]byte `json:"codeChange,omitempty"` } // NewConstructionAccountAccess initializes the account access object. func NewConstructionAccountAccess() *ConstructionAccountAccess { return &ConstructionAccountAccess{ - StorageWrites: make(map[common.Hash]map[uint16]common.Hash), + StorageWrites: make(map[common.Hash]map[uint32]common.Hash), StorageReads: make(map[common.Hash]struct{}), - BalanceChanges: make(map[uint16]*uint256.Int), - NonceChanges: make(map[uint16]uint64), - CodeChange: make(map[uint16][]byte), + BalanceChanges: make(map[uint32]*uint256.Int), + NonceChanges: make(map[uint32]uint64), + CodeChange: make(map[uint32][]byte), } } @@ -97,12 +97,12 @@ func (b *ConstructionBlockAccessList) StorageRead(address common.Address, key co // StorageWrite records the post-transaction value of a mutated storage slot. // The storage slot is removed from the list of read slots. -func (b *ConstructionBlockAccessList) StorageWrite(txIdx uint16, address common.Address, key, value common.Hash) { +func (b *ConstructionBlockAccessList) StorageWrite(txIdx uint32, address common.Address, key, value common.Hash) { if _, ok := b.Accounts[address]; !ok { b.Accounts[address] = NewConstructionAccountAccess() } if _, ok := b.Accounts[address].StorageWrites[key]; !ok { - b.Accounts[address].StorageWrites[key] = make(map[uint16]common.Hash) + b.Accounts[address].StorageWrites[key] = make(map[uint32]common.Hash) } b.Accounts[address].StorageWrites[key][txIdx] = value @@ -110,7 +110,7 @@ func (b *ConstructionBlockAccessList) StorageWrite(txIdx uint16, address common. } // CodeChange records the code of a newly-created contract. -func (b *ConstructionBlockAccessList) CodeChange(address common.Address, txIndex uint16, code []byte) { +func (b *ConstructionBlockAccessList) CodeChange(address common.Address, txIndex uint32, code []byte) { if _, ok := b.Accounts[address]; !ok { b.Accounts[address] = NewConstructionAccountAccess() } @@ -120,7 +120,7 @@ func (b *ConstructionBlockAccessList) CodeChange(address common.Address, txIndex // NonceChange records tx post-state nonce of any contract-like accounts whose // nonce was incremented. -func (b *ConstructionBlockAccessList) NonceChange(address common.Address, txIdx uint16, postNonce uint64) { +func (b *ConstructionBlockAccessList) NonceChange(address common.Address, txIdx uint32, postNonce uint64) { if _, ok := b.Accounts[address]; !ok { b.Accounts[address] = NewConstructionAccountAccess() } @@ -129,7 +129,7 @@ func (b *ConstructionBlockAccessList) NonceChange(address common.Address, txIdx // BalanceChange records the post-transaction balance of an account whose // balance changed. -func (b *ConstructionBlockAccessList) BalanceChange(txIdx uint16, address common.Address, balance *uint256.Int) { +func (b *ConstructionBlockAccessList) BalanceChange(txIdx uint32, address common.Address, balance *uint256.Int) { if _, ok := b.Accounts[address]; !ok { b.Accounts[address] = NewConstructionAccountAccess() } @@ -148,21 +148,21 @@ func (b *ConstructionBlockAccessList) Copy() *ConstructionBlockAccessList { for addr, aa := range b.Accounts { var aaCopy ConstructionAccountAccess - slotWrites := make(map[common.Hash]map[uint16]common.Hash, len(aa.StorageWrites)) + slotWrites := make(map[common.Hash]map[uint32]common.Hash, len(aa.StorageWrites)) for key, m := range aa.StorageWrites { slotWrites[key] = maps.Clone(m) } aaCopy.StorageWrites = slotWrites aaCopy.StorageReads = maps.Clone(aa.StorageReads) - balances := make(map[uint16]*uint256.Int, len(aa.BalanceChanges)) + balances := make(map[uint32]*uint256.Int, len(aa.BalanceChanges)) for index, balance := range aa.BalanceChanges { balances[index] = balance.Clone() } aaCopy.BalanceChanges = balances aaCopy.NonceChanges = maps.Clone(aa.NonceChanges) - codes := make(map[uint16][]byte, len(aa.CodeChange)) + codes := make(map[uint32][]byte, len(aa.CodeChange)) for index, code := range aa.CodeChange { codes[index] = bytes.Clone(code) } diff --git a/core/types/bal/bal_encoding.go b/core/types/bal/bal_encoding.go index 6d52c17c83..03f97f3809 100644 --- a/core/types/bal/bal_encoding.go +++ b/core/types/bal/bal_encoding.go @@ -33,27 +33,59 @@ import ( "github.com/holiman/uint256" ) -//go:generate go run github.com/ethereum/go-ethereum/rlp/rlpgen -out bal_encoding_rlp_generated.go -type BlockAccessList -decoder +//go:generate go run github.com/ethereum/go-ethereum/rlp/rlpgen -out bal_encoding_rlp_generated.go -type AccountAccess -decoder // These are objects used as input for the access list encoding. They mirror // the spec format. // BlockAccessList is the encoding format of ConstructionBlockAccessList. -type BlockAccessList struct { - Accesses []AccountAccess `ssz-max:"300000"` +type BlockAccessList []AccountAccess + +// EncodeRLP implements rlp.Encoder. It encodes the access list as a single +// RLP list of AccountAccess entries. +func (e BlockAccessList) EncodeRLP(w io.Writer) error { + buf := rlp.NewEncoderBuffer(w) + l := buf.List() + for i := range e { + if err := e[i].EncodeRLP(buf); err != nil { + return err + } + } + buf.ListEnd(l) + return buf.Flush() +} + +// DecodeRLP implements rlp.Decoder. +func (e *BlockAccessList) DecodeRLP(s *rlp.Stream) error { + if _, err := s.List(); err != nil { + return err + } + var list BlockAccessList + for s.MoreDataInList() { + var a AccountAccess + if err := a.DecodeRLP(s); err != nil { + return err + } + list = append(list, a) + } + if err := s.ListEnd(); err != nil { + return err + } + *e = list + return nil } // Validate returns an error if the contents of the access list are not ordered // according to the spec or any code changes are contained which exceed protocol // max code size. -func (e *BlockAccessList) Validate() error { - if !slices.IsSortedFunc(e.Accesses, func(a, b AccountAccess) int { +func (e *BlockAccessList) Validate(rules params.Rules) error { + if !slices.IsSortedFunc(*e, func(a, b AccountAccess) int { return bytes.Compare(a.Address[:], b.Address[:]) }) { return errors.New("block access list accounts not in lexicographic order") } - for _, entry := range e.Accesses { - if err := entry.validate(); err != nil { + for _, entry := range *e { + if err := entry.validate(rules); err != nil { return err } } @@ -63,56 +95,44 @@ func (e *BlockAccessList) Validate() 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{} } return crypto.Keccak256Hash(enc.Bytes()) } -// encodeBalance encodes the provided balance into 16-bytes. -func encodeBalance(val *uint256.Int) [16]byte { - valBytes := val.Bytes() - if len(valBytes) > 16 { - panic("can't encode value that is greater than 16 bytes in size") - } - var enc [16]byte - copy(enc[16-len(valBytes):], valBytes[:]) - return enc -} - // encodingBalanceChange is the encoding format of BalanceChange. type encodingBalanceChange struct { - TxIdx uint16 `ssz-size:"2"` - Balance [16]byte `ssz-size:"16"` + TxIdx uint32 + Balance *uint256.Int } // encodingAccountNonce is the encoding format of NonceChange. type encodingAccountNonce struct { - TxIdx uint16 `ssz-size:"2"` - Nonce uint64 `ssz-size:"8"` + TxIdx uint32 + Nonce uint64 } // encodingStorageWrite is the encoding format of StorageWrites. type encodingStorageWrite struct { - TxIdx uint16 - ValueAfter [32]byte `ssz-size:"32"` + TxIdx uint32 + ValueAfter *uint256.Int } // encodingStorageWrite is the encoding format of SlotWrites. type encodingSlotWrites struct { - Slot [32]byte `ssz-size:"32"` - Accesses []encodingStorageWrite `ssz-max:"300000"` + Slot *uint256.Int + Accesses []encodingStorageWrite } // validate returns an instance of the encoding-representation slot writes in // working representation. func (e *encodingSlotWrites) validate() error { if slices.IsSortedFunc(e.Accesses, func(a, b encodingStorageWrite) int { - return cmp.Compare[uint16](a.TxIdx, b.TxIdx) + return cmp.Compare[uint32](a.TxIdx, b.TxIdx) }) { return nil } @@ -122,27 +142,27 @@ func (e *encodingSlotWrites) validate() error { // encodingCodeChange contains the runtime bytecode deployed at an address // and the transaction index where the deployment took place. type encodingCodeChange struct { - TxIndex uint16 `ssz-size:"2"` - Code []byte `ssz-max:"300000"` // TODO(rjl493456442) shall we put the limit here? The limit will be increased gradually + TxIndex uint32 + Code []byte } // AccountAccess is the encoding format of ConstructionAccountAccess. type AccountAccess struct { - Address [20]byte `ssz-size:"20"` // 20-byte Ethereum address - StorageWrites []encodingSlotWrites `ssz-max:"300000"` // Storage changes (slot -> [tx_index -> new_value]) - StorageReads [][32]byte `ssz-max:"300000"` // Read-only storage keys - BalanceChanges []encodingBalanceChange `ssz-max:"300000"` // Balance changes ([tx_index -> post_balance]) - NonceChanges []encodingAccountNonce `ssz-max:"300000"` // Nonce changes ([tx_index -> new_nonce]) - CodeChanges []encodingCodeChange `ssz-max:"300000"` // Code changes ([tx_index -> new_code]) + Address [20]byte // 20-byte Ethereum address + StorageWrites []encodingSlotWrites // Storage changes (slot -> [tx_index -> new_value]) + StorageReads []*uint256.Int // Read-only storage keys + BalanceChanges []encodingBalanceChange // Balance changes ([tx_index -> post_balance]) + NonceChanges []encodingAccountNonce // Nonce changes ([tx_index -> new_nonce]) + CodeChanges []encodingCodeChange // Code changes ([tx_index -> new_code]) } // validate converts the account accesses out of encoding format. // If any of the keys in the encoding object are not ordered according to the // spec, an error is returned. -func (e *AccountAccess) validate() error { +func (e *AccountAccess) validate(rules params.Rules) error { // Check the storage write slots are sorted in order if !slices.IsSortedFunc(e.StorageWrites, func(a, b encodingSlotWrites) int { - return bytes.Compare(a.Slot[:], b.Slot[:]) + return a.Slot.Cmp(b.Slot) }) { return errors.New("storage writes slots not in lexicographic order") } @@ -153,36 +173,41 @@ func (e *AccountAccess) validate() error { } // Check the storage read slots are sorted in order - if !slices.IsSortedFunc(e.StorageReads, func(a, b [32]byte) int { - return bytes.Compare(a[:], b[:]) + if !slices.IsSortedFunc(e.StorageReads, func(a, b *uint256.Int) int { + return a.Cmp(b) }) { return errors.New("storage read slots not in lexicographic order") } // Check the balance changes are sorted in order if !slices.IsSortedFunc(e.BalanceChanges, func(a, b encodingBalanceChange) int { - return cmp.Compare[uint16](a.TxIdx, b.TxIdx) + return cmp.Compare[uint32](a.TxIdx, b.TxIdx) }) { return errors.New("balance changes not in ascending order by tx index") } // Check the nonce changes are sorted in order if !slices.IsSortedFunc(e.NonceChanges, func(a, b encodingAccountNonce) int { - return cmp.Compare[uint16](a.TxIdx, b.TxIdx) + return cmp.Compare[uint32](a.TxIdx, b.TxIdx) }) { return errors.New("nonce changes not in ascending order by tx index") } // Check the code changes are sorted in order if !slices.IsSortedFunc(e.CodeChanges, func(a, b encodingCodeChange) int { - return cmp.Compare[uint16](a.TxIndex, b.TxIndex) + return cmp.Compare[uint32](a.TxIndex, b.TxIndex) }) { return errors.New("code changes not in ascending order by tx index") } for _, change := range e.CodeChanges { - // TODO(rjl493456442): This check should be fork-aware, since the limit may - // differ across forks. - if len(change.Code) > params.MaxCodeSize { + var sizeLimit int + switch { + case rules.IsAmsterdam: + sizeLimit = params.MaxCodeSizeAmsterdam + default: + sizeLimit = params.MaxCodeSize + } + if len(change.Code) > sizeLimit { return errors.New("code change contained oversized code") } } @@ -193,16 +218,32 @@ func (e *AccountAccess) validate() error { func (e *AccountAccess) Copy() AccountAccess { res := AccountAccess{ Address: e.Address, - StorageReads: slices.Clone(e.StorageReads), - BalanceChanges: slices.Clone(e.BalanceChanges), + StorageReads: make([]*uint256.Int, 0, len(e.StorageReads)), + BalanceChanges: make([]encodingBalanceChange, 0, len(e.BalanceChanges)), NonceChanges: slices.Clone(e.NonceChanges), StorageWrites: make([]encodingSlotWrites, 0, len(e.StorageWrites)), CodeChanges: make([]encodingCodeChange, 0, len(e.CodeChanges)), } + for _, slot := range e.StorageReads { + res.StorageReads = append(res.StorageReads, slot.Clone()) + } + for _, change := range e.BalanceChanges { + res.BalanceChanges = append(res.BalanceChanges, encodingBalanceChange{ + TxIdx: change.TxIdx, + Balance: change.Balance.Clone(), + }) + } for _, storageWrite := range e.StorageWrites { + accesses := make([]encodingStorageWrite, 0, len(storageWrite.Accesses)) + for _, w := range storageWrite.Accesses { + accesses = append(accesses, encodingStorageWrite{ + TxIdx: w.TxIdx, + ValueAfter: w.ValueAfter.Clone(), + }) + } res.StorageWrites = append(res.StorageWrites, encodingSlotWrites{ - Slot: storageWrite.Slot, - Accesses: slices.Clone(storageWrite.Accesses), + Slot: storageWrite.Slot.Clone(), + Accesses: accesses, }) } for _, codeChange := range e.CodeChanges { @@ -221,13 +262,13 @@ func (b *ConstructionBlockAccessList) EncodeRLP(wr io.Writer) error { var _ rlp.Encoder = &ConstructionBlockAccessList{} -// toEncodingObj creates an instance of the ConstructionAccountAccess of the type that is -// used as input for the encoding. +// toEncodingObj creates an instance of the ConstructionAccountAccess of the type +// that is used as input for the encoding. func (a *ConstructionAccountAccess) toEncodingObj(addr common.Address) AccountAccess { res := AccountAccess{ Address: addr, StorageWrites: make([]encodingSlotWrites, 0, len(a.StorageWrites)), - StorageReads: make([][32]byte, 0, len(a.StorageReads)), + StorageReads: make([]*uint256.Int, 0, len(a.StorageReads)), BalanceChanges: make([]encodingBalanceChange, 0, len(a.BalanceChanges)), NonceChanges: make([]encodingAccountNonce, 0, len(a.NonceChanges)), CodeChanges: make([]encodingCodeChange, 0, len(a.CodeChange)), @@ -237,18 +278,19 @@ func (a *ConstructionAccountAccess) toEncodingObj(addr common.Address) AccountAc writeSlots := slices.Collect(maps.Keys(a.StorageWrites)) slices.SortFunc(writeSlots, common.Hash.Cmp) for _, slot := range writeSlots { - var obj encodingSlotWrites - obj.Slot = slot - + obj := encodingSlotWrites{ + Slot: new(uint256.Int).SetBytes(slot[:]), + } slotWrites := a.StorageWrites[slot] obj.Accesses = make([]encodingStorageWrite, 0, len(slotWrites)) indices := slices.Collect(maps.Keys(slotWrites)) - slices.SortFunc(indices, cmp.Compare[uint16]) + slices.SortFunc(indices, cmp.Compare[uint32]) for _, index := range indices { + val := slotWrites[index] obj.Accesses = append(obj.Accesses, encodingStorageWrite{ TxIdx: index, - ValueAfter: slotWrites[index], + ValueAfter: new(uint256.Int).SetBytes(val[:]), }) } res.StorageWrites = append(res.StorageWrites, obj) @@ -258,22 +300,22 @@ func (a *ConstructionAccountAccess) toEncodingObj(addr common.Address) AccountAc readSlots := slices.Collect(maps.Keys(a.StorageReads)) slices.SortFunc(readSlots, common.Hash.Cmp) for _, slot := range readSlots { - res.StorageReads = append(res.StorageReads, slot) + res.StorageReads = append(res.StorageReads, new(uint256.Int).SetBytes(slot[:])) } // Convert balance changes balanceIndices := slices.Collect(maps.Keys(a.BalanceChanges)) - slices.SortFunc(balanceIndices, cmp.Compare[uint16]) + slices.SortFunc(balanceIndices, cmp.Compare[uint32]) for _, idx := range balanceIndices { res.BalanceChanges = append(res.BalanceChanges, encodingBalanceChange{ TxIdx: idx, - Balance: encodeBalance(a.BalanceChanges[idx]), + Balance: a.BalanceChanges[idx].Clone(), }) } // Convert nonce changes nonceIndices := slices.Collect(maps.Keys(a.NonceChanges)) - slices.SortFunc(nonceIndices, cmp.Compare[uint16]) + slices.SortFunc(nonceIndices, cmp.Compare[uint32]) for _, idx := range nonceIndices { res.NonceChanges = append(res.NonceChanges, encodingAccountNonce{ TxIdx: idx, @@ -283,11 +325,16 @@ func (a *ConstructionAccountAccess) toEncodingObj(addr common.Address) AccountAc // Convert code change codeIndices := slices.Collect(maps.Keys(a.CodeChange)) - slices.SortFunc(codeIndices, cmp.Compare[uint16]) + slices.SortFunc(codeIndices, cmp.Compare[uint32]) for _, idx := range codeIndices { res.CodeChanges = append(res.CodeChanges, encodingCodeChange{ TxIndex: idx, - Code: a.CodeChange[idx], + + // TODO(rjl493456442) the contract code is not deep-copied. + // In theory the deep-copy is unnecessary, the semantics of + // the function should be probably changed that the returned + // AccessList is unsafe for modification. + Code: a.CodeChange[idx], }) } return res @@ -302,9 +349,9 @@ func (b *ConstructionBlockAccessList) toEncodingObj() *BlockAccessList { } slices.SortFunc(addresses, common.Address.Cmp) - var res BlockAccessList + res := make(BlockAccessList, 0, len(addresses)) for _, addr := range addresses { - res.Accesses = append(res.Accesses, b.Accounts[addr].toEncodingObj(addr)) + res = append(res, b.Accounts[addr].toEncodingObj(addr)) } return &res } @@ -314,26 +361,25 @@ func (e *BlockAccessList) PrettyPrint() string { printWithIndent := func(indent int, text string) { fmt.Fprintf(&res, "%s%s\n", strings.Repeat(" ", indent), text) } - for _, accountDiff := range e.Accesses { + for _, accountDiff := range *e { printWithIndent(0, fmt.Sprintf("%x:", accountDiff.Address)) printWithIndent(1, "storage writes:") for _, sWrite := range accountDiff.StorageWrites { - printWithIndent(2, fmt.Sprintf("%x:", sWrite.Slot)) + printWithIndent(2, fmt.Sprintf("%s:", sWrite.Slot.Hex())) for _, access := range sWrite.Accesses { - printWithIndent(3, fmt.Sprintf("%d: %x", access.TxIdx, access.ValueAfter)) + printWithIndent(3, fmt.Sprintf("%d: %s", access.TxIdx, access.ValueAfter.Hex())) } } printWithIndent(1, "storage reads:") for _, slot := range accountDiff.StorageReads { - printWithIndent(2, fmt.Sprintf("%x", slot)) + printWithIndent(2, slot.Hex()) } printWithIndent(1, "balance changes:") for _, change := range accountDiff.BalanceChanges { - balance := new(uint256.Int).SetBytes(change.Balance[:]).String() - printWithIndent(2, fmt.Sprintf("%d: %s", change.TxIdx, balance)) + printWithIndent(2, fmt.Sprintf("%d: %s", change.TxIdx, change.Balance)) } printWithIndent(1, "nonce changes:") @@ -351,11 +397,9 @@ func (e *BlockAccessList) PrettyPrint() string { // Copy returns a deep copy of the access list func (e *BlockAccessList) Copy() *BlockAccessList { - cpy := &BlockAccessList{ - Accesses: make([]AccountAccess, 0, len(e.Accesses)), + cpy := make(BlockAccessList, 0, len(*e)) + for _, accountAccess := range *e { + cpy = append(cpy, accountAccess.Copy()) } - for _, accountAccess := range e.Accesses { - cpy.Accesses = append(cpy.Accesses, accountAccess.Copy()) - } - return cpy + return &cpy } diff --git a/core/types/bal/bal_encoding_rlp_generated.go b/core/types/bal/bal_encoding_rlp_generated.go index 640035e30e..540987c076 100644 --- a/core/types/bal/bal_encoding_rlp_generated.go +++ b/core/types/bal/bal_encoding_rlp_generated.go @@ -3,274 +3,264 @@ package bal import "github.com/ethereum/go-ethereum/rlp" +import "github.com/holiman/uint256" import "io" -func (obj *BlockAccessList) EncodeRLP(_w io.Writer) error { +func (obj *AccountAccess) EncodeRLP(_w io.Writer) error { w := rlp.NewEncoderBuffer(_w) _tmp0 := w.List() + w.WriteBytes(obj.Address[:]) _tmp1 := w.List() - for _, _tmp2 := range obj.Accesses { + for _, _tmp2 := range obj.StorageWrites { _tmp3 := w.List() - w.WriteBytes(_tmp2.Address[:]) + if _tmp2.Slot == nil { + w.Write(rlp.EmptyString) + } else { + w.WriteUint256(_tmp2.Slot) + } _tmp4 := w.List() - for _, _tmp5 := range _tmp2.StorageWrites { + for _, _tmp5 := range _tmp2.Accesses { _tmp6 := w.List() - w.WriteBytes(_tmp5.Slot[:]) - _tmp7 := w.List() - for _, _tmp8 := range _tmp5.Accesses { - _tmp9 := w.List() - w.WriteUint64(uint64(_tmp8.TxIdx)) - w.WriteBytes(_tmp8.ValueAfter[:]) - w.ListEnd(_tmp9) + w.WriteUint64(uint64(_tmp5.TxIdx)) + if _tmp5.ValueAfter == nil { + w.Write(rlp.EmptyString) + } else { + w.WriteUint256(_tmp5.ValueAfter) } - w.ListEnd(_tmp7) w.ListEnd(_tmp6) } w.ListEnd(_tmp4) - _tmp10 := w.List() - for _, _tmp11 := range _tmp2.StorageReads { - w.WriteBytes(_tmp11[:]) - } - w.ListEnd(_tmp10) - _tmp12 := w.List() - for _, _tmp13 := range _tmp2.BalanceChanges { - _tmp14 := w.List() - w.WriteUint64(uint64(_tmp13.TxIdx)) - w.WriteBytes(_tmp13.Balance[:]) - w.ListEnd(_tmp14) - } - w.ListEnd(_tmp12) - _tmp15 := w.List() - for _, _tmp16 := range _tmp2.NonceChanges { - _tmp17 := w.List() - w.WriteUint64(uint64(_tmp16.TxIdx)) - w.WriteUint64(_tmp16.Nonce) - w.ListEnd(_tmp17) - } - w.ListEnd(_tmp15) - _tmp18 := w.List() - for _, _tmp19 := range _tmp2.CodeChanges { - _tmp20 := w.List() - w.WriteUint64(uint64(_tmp19.TxIndex)) - w.WriteBytes(_tmp19.Code) - w.ListEnd(_tmp20) - } - w.ListEnd(_tmp18) w.ListEnd(_tmp3) } w.ListEnd(_tmp1) + _tmp7 := w.List() + for _, _tmp8 := range obj.StorageReads { + if _tmp8 == nil { + w.Write(rlp.EmptyString) + } else { + w.WriteUint256(_tmp8) + } + } + w.ListEnd(_tmp7) + _tmp9 := w.List() + for _, _tmp10 := range obj.BalanceChanges { + _tmp11 := w.List() + w.WriteUint64(uint64(_tmp10.TxIdx)) + if _tmp10.Balance == nil { + w.Write(rlp.EmptyString) + } else { + w.WriteUint256(_tmp10.Balance) + } + w.ListEnd(_tmp11) + } + w.ListEnd(_tmp9) + _tmp12 := w.List() + for _, _tmp13 := range obj.NonceChanges { + _tmp14 := w.List() + w.WriteUint64(uint64(_tmp13.TxIdx)) + w.WriteUint64(_tmp13.Nonce) + w.ListEnd(_tmp14) + } + w.ListEnd(_tmp12) + _tmp15 := w.List() + for _, _tmp16 := range obj.CodeChanges { + _tmp17 := w.List() + w.WriteUint64(uint64(_tmp16.TxIndex)) + w.WriteBytes(_tmp16.Code) + w.ListEnd(_tmp17) + } + w.ListEnd(_tmp15) w.ListEnd(_tmp0) return w.Flush() } -func (obj *BlockAccessList) DecodeRLP(dec *rlp.Stream) error { - var _tmp0 BlockAccessList +func (obj *AccountAccess) DecodeRLP(dec *rlp.Stream) error { + var _tmp0 AccountAccess { if _, err := dec.List(); err != nil { return err } - // Accesses: - var _tmp1 []AccountAccess + // Address: + var _tmp1 [20]byte + if err := dec.ReadBytes(_tmp1[:]); err != nil { + return err + } + _tmp0.Address = _tmp1 + // StorageWrites: + var _tmp2 []encodingSlotWrites if _, err := dec.List(); err != nil { return err } for dec.MoreDataInList() { - var _tmp2 AccountAccess + var _tmp3 encodingSlotWrites { if _, err := dec.List(); err != nil { return err } - // Address: - var _tmp3 [20]byte - if err := dec.ReadBytes(_tmp3[:]); err != nil { + // Slot: + var _tmp4 uint256.Int + if err := dec.ReadUint256(&_tmp4); err != nil { return err } - _tmp2.Address = _tmp3 - // StorageWrites: - var _tmp4 []encodingSlotWrites + _tmp3.Slot = &_tmp4 + // Accesses: + var _tmp5 []encodingStorageWrite if _, err := dec.List(); err != nil { return err } for dec.MoreDataInList() { - var _tmp5 encodingSlotWrites - { - if _, err := dec.List(); err != nil { - return err - } - // Slot: - var _tmp6 [32]byte - if err := dec.ReadBytes(_tmp6[:]); err != nil { - return err - } - _tmp5.Slot = _tmp6 - // Accesses: - var _tmp7 []encodingStorageWrite - if _, err := dec.List(); err != nil { - return err - } - for dec.MoreDataInList() { - var _tmp8 encodingStorageWrite - { - if _, err := dec.List(); err != nil { - return err - } - // TxIdx: - _tmp9, err := dec.Uint16() - if err != nil { - return err - } - _tmp8.TxIdx = _tmp9 - // ValueAfter: - var _tmp10 [32]byte - if err := dec.ReadBytes(_tmp10[:]); err != nil { - return err - } - _tmp8.ValueAfter = _tmp10 - if err := dec.ListEnd(); err != nil { - return err - } - } - _tmp7 = append(_tmp7, _tmp8) - } - if err := dec.ListEnd(); err != nil { - return err - } - _tmp5.Accesses = _tmp7 - if err := dec.ListEnd(); err != nil { - return err - } - } - _tmp4 = append(_tmp4, _tmp5) - } - if err := dec.ListEnd(); err != nil { - return err - } - _tmp2.StorageWrites = _tmp4 - // StorageReads: - var _tmp11 [][32]byte - if _, err := dec.List(); err != nil { - return err - } - for dec.MoreDataInList() { - var _tmp12 [32]byte - if err := dec.ReadBytes(_tmp12[:]); err != nil { - return err - } - _tmp11 = append(_tmp11, _tmp12) - } - if err := dec.ListEnd(); err != nil { - return err - } - _tmp2.StorageReads = _tmp11 - // BalanceChanges: - var _tmp13 []encodingBalanceChange - if _, err := dec.List(); err != nil { - return err - } - for dec.MoreDataInList() { - var _tmp14 encodingBalanceChange + var _tmp6 encodingStorageWrite { if _, err := dec.List(); err != nil { return err } // TxIdx: - _tmp15, err := dec.Uint16() + _tmp7, err := dec.Uint32() if err != nil { return err } - _tmp14.TxIdx = _tmp15 - // Balance: - var _tmp16 [16]byte - if err := dec.ReadBytes(_tmp16[:]); err != nil { + _tmp6.TxIdx = _tmp7 + // ValueAfter: + var _tmp8 uint256.Int + if err := dec.ReadUint256(&_tmp8); err != nil { return err } - _tmp14.Balance = _tmp16 + _tmp6.ValueAfter = &_tmp8 if err := dec.ListEnd(); err != nil { return err } } - _tmp13 = append(_tmp13, _tmp14) + _tmp5 = append(_tmp5, _tmp6) } if err := dec.ListEnd(); err != nil { return err } - _tmp2.BalanceChanges = _tmp13 - // NonceChanges: - var _tmp17 []encodingAccountNonce - if _, err := dec.List(); err != nil { - return err - } - for dec.MoreDataInList() { - var _tmp18 encodingAccountNonce - { - if _, err := dec.List(); err != nil { - return err - } - // TxIdx: - _tmp19, err := dec.Uint16() - if err != nil { - return err - } - _tmp18.TxIdx = _tmp19 - // Nonce: - _tmp20, err := dec.Uint64() - if err != nil { - return err - } - _tmp18.Nonce = _tmp20 - if err := dec.ListEnd(); err != nil { - return err - } - } - _tmp17 = append(_tmp17, _tmp18) - } - if err := dec.ListEnd(); err != nil { - return err - } - _tmp2.NonceChanges = _tmp17 - // CodeChanges: - var _tmp21 []encodingCodeChange - if _, err := dec.List(); err != nil { - return err - } - for dec.MoreDataInList() { - var _tmp22 encodingCodeChange - { - if _, err := dec.List(); err != nil { - return err - } - // TxIndex: - _tmp23, err := dec.Uint16() - if err != nil { - return err - } - _tmp22.TxIndex = _tmp23 - // Code: - _tmp24, err := dec.Bytes() - if err != nil { - return err - } - _tmp22.Code = _tmp24 - if err := dec.ListEnd(); err != nil { - return err - } - } - _tmp21 = append(_tmp21, _tmp22) - } - if err := dec.ListEnd(); err != nil { - return err - } - _tmp2.CodeChanges = _tmp21 + _tmp3.Accesses = _tmp5 if err := dec.ListEnd(); err != nil { return err } } - _tmp1 = append(_tmp1, _tmp2) + _tmp2 = append(_tmp2, _tmp3) } if err := dec.ListEnd(); err != nil { return err } - _tmp0.Accesses = _tmp1 + _tmp0.StorageWrites = _tmp2 + // StorageReads: + var _tmp9 []*uint256.Int + if _, err := dec.List(); err != nil { + return err + } + for dec.MoreDataInList() { + var _tmp10 uint256.Int + if err := dec.ReadUint256(&_tmp10); err != nil { + return err + } + _tmp9 = append(_tmp9, &_tmp10) + } + if err := dec.ListEnd(); err != nil { + return err + } + _tmp0.StorageReads = _tmp9 + // BalanceChanges: + var _tmp11 []encodingBalanceChange + if _, err := dec.List(); err != nil { + return err + } + for dec.MoreDataInList() { + var _tmp12 encodingBalanceChange + { + if _, err := dec.List(); err != nil { + return err + } + // TxIdx: + _tmp13, err := dec.Uint32() + if err != nil { + return err + } + _tmp12.TxIdx = _tmp13 + // Balance: + var _tmp14 uint256.Int + if err := dec.ReadUint256(&_tmp14); err != nil { + return err + } + _tmp12.Balance = &_tmp14 + if err := dec.ListEnd(); err != nil { + return err + } + } + _tmp11 = append(_tmp11, _tmp12) + } + if err := dec.ListEnd(); err != nil { + return err + } + _tmp0.BalanceChanges = _tmp11 + // NonceChanges: + var _tmp15 []encodingAccountNonce + if _, err := dec.List(); err != nil { + return err + } + for dec.MoreDataInList() { + var _tmp16 encodingAccountNonce + { + if _, err := dec.List(); err != nil { + return err + } + // TxIdx: + _tmp17, err := dec.Uint32() + if err != nil { + return err + } + _tmp16.TxIdx = _tmp17 + // Nonce: + _tmp18, err := dec.Uint64() + if err != nil { + return err + } + _tmp16.Nonce = _tmp18 + if err := dec.ListEnd(); err != nil { + return err + } + } + _tmp15 = append(_tmp15, _tmp16) + } + if err := dec.ListEnd(); err != nil { + return err + } + _tmp0.NonceChanges = _tmp15 + // CodeChanges: + var _tmp19 []encodingCodeChange + if _, err := dec.List(); err != nil { + return err + } + for dec.MoreDataInList() { + var _tmp20 encodingCodeChange + { + if _, err := dec.List(); err != nil { + return err + } + // TxIndex: + _tmp21, err := dec.Uint32() + if err != nil { + return err + } + _tmp20.TxIndex = _tmp21 + // Code: + _tmp22, err := dec.Bytes() + if err != nil { + return err + } + _tmp20.Code = _tmp22 + if err := dec.ListEnd(); err != nil { + return err + } + } + _tmp19 = append(_tmp19, _tmp20) + } + if err := dec.ListEnd(); err != nil { + return err + } + _tmp0.CodeChanges = _tmp19 if err := dec.ListEnd(); err != nil { return err } diff --git a/core/types/bal/bal_test.go b/core/types/bal/bal_test.go index 58ba639ff0..32a0292f2e 100644 --- a/core/types/bal/bal_test.go +++ b/core/types/bal/bal_test.go @@ -25,22 +25,16 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/internal/testrand" + "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rlp" "github.com/holiman/uint256" ) -func equalBALs(a *BlockAccessList, b *BlockAccessList) bool { - if !reflect.DeepEqual(a, b) { - return false - } - return true -} - func makeTestConstructionBAL() *ConstructionBlockAccessList { return &ConstructionBlockAccessList{ map[common.Address]*ConstructionAccountAccess{ common.BytesToAddress([]byte{0xff, 0xff}): { - StorageWrites: map[common.Hash]map[uint16]common.Hash{ + StorageWrites: map[common.Hash]map[uint32]common.Hash{ common.BytesToHash([]byte{0x01}): { 1: common.BytesToHash([]byte{1, 2, 3, 4}), 2: common.BytesToHash([]byte{1, 2, 3, 4, 5, 6}), @@ -52,20 +46,20 @@ func makeTestConstructionBAL() *ConstructionBlockAccessList { StorageReads: map[common.Hash]struct{}{ common.BytesToHash([]byte{1, 2, 3, 4, 5, 6, 7}): {}, }, - BalanceChanges: map[uint16]*uint256.Int{ + BalanceChanges: map[uint32]*uint256.Int{ 1: uint256.NewInt(100), 2: uint256.NewInt(500), }, - NonceChanges: map[uint16]uint64{ + NonceChanges: map[uint32]uint64{ 1: 2, 2: 6, }, - CodeChange: map[uint16][]byte{ + CodeChange: map[uint32][]byte{ 0: common.Hex2Bytes("deadbeef"), }, }, common.BytesToAddress([]byte{0xff, 0xff, 0xff}): { - StorageWrites: map[common.Hash]map[uint16]common.Hash{ + StorageWrites: map[common.Hash]map[uint32]common.Hash{ common.BytesToHash([]byte{0x01}): { 2: common.BytesToHash([]byte{1, 2, 3, 4, 5, 6}), 3: common.BytesToHash([]byte{1, 2, 3, 4, 5, 6, 7, 8}), @@ -77,14 +71,14 @@ func makeTestConstructionBAL() *ConstructionBlockAccessList { StorageReads: map[common.Hash]struct{}{ common.BytesToHash([]byte{1, 2, 3, 4, 5, 6, 7, 8}): {}, }, - BalanceChanges: map[uint16]*uint256.Int{ + BalanceChanges: map[uint32]*uint256.Int{ 2: uint256.NewInt(100), 3: uint256.NewInt(500), }, - NonceChanges: map[uint16]uint64{ + NonceChanges: map[uint32]uint64{ 1: 2, }, - CodeChange: map[uint16][]byte{ + CodeChange: map[uint32][]byte{ 0: common.Hex2Bytes("deadbeef"), }, }, @@ -101,13 +95,13 @@ 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() { t.Fatalf("encoded block hash doesn't match decoded") } - if !equalBALs(bal.toEncodingObj(), &dec) { + if !reflect.DeepEqual(bal.toEncodingObj(), &dec) { t.Fatal("decoded BAL doesn't match") } } @@ -115,63 +109,79 @@ func TestBALEncoding(t *testing.T) { func makeTestAccountAccess(sort bool) AccountAccess { var ( storageWrites []encodingSlotWrites - storageReads [][32]byte + storageReads []*uint256.Int 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: testrand.Hash(), + Slot: randSlot(), } for j := 0; j < 3; j++ { slot.Accesses = append(slot.Accesses, encodingStorageWrite{ - TxIdx: uint16(2 * j), - ValueAfter: testrand.Hash(), + TxIdx: uint32(2 * j), + ValueAfter: randSlot(), }) } if sort { slices.SortFunc(slot.Accesses, func(a, b encodingStorageWrite) int { - return cmp.Compare[uint16](a.TxIdx, b.TxIdx) + return cmp.Compare[uint32](a.TxIdx, b.TxIdx) }) } storageWrites = append(storageWrites, slot) } if sort { slices.SortFunc(storageWrites, func(a, b encodingSlotWrites) int { - return bytes.Compare(a.Slot[:], b.Slot[:]) + return a.Slot.Cmp(b.Slot) }) } for i := 0; i < 5; i++ { - storageReads = append(storageReads, testrand.Hash()) + storageReads = append(storageReads, randSlot()) } if sort { - slices.SortFunc(storageReads, func(a, b [32]byte) int { - return bytes.Compare(a[:], b[:]) + slices.SortFunc(storageReads, func(a, b *uint256.Int) int { + return a.Cmp(b) }) } for i := 0; i < 5; i++ { balances = append(balances, encodingBalanceChange{ - TxIdx: uint16(2 * i), - Balance: [16]byte(testrand.Bytes(16)), + TxIdx: uint32(2 * i), + Balance: new(uint256.Int).SetBytes(testrand.Bytes(16)), }) } if sort { slices.SortFunc(balances, func(a, b encodingBalanceChange) int { - return cmp.Compare[uint16](a.TxIdx, b.TxIdx) + return cmp.Compare[uint32](a.TxIdx, b.TxIdx) }) } for i := 0; i < 5; i++ { nonces = append(nonces, encodingAccountNonce{ - TxIdx: uint16(2 * i), + TxIdx: uint32(2 * i), Nonce: uint64(i + 100), }) } if sort { slices.SortFunc(nonces, func(a, b encodingAccountNonce) int { - return cmp.Compare[uint16](a.TxIdx, b.TxIdx) + return cmp.Compare[uint32](a.TxIdx, b.TxIdx) + }) + } + + for i := 0; i < 5; i++ { + codes = append(codes, encodingCodeChange{ + TxIndex: uint32(2 * i), + Code: testrand.Bytes(256), + }) + } + if sort { + slices.SortFunc(codes, func(a, b encodingCodeChange) int { + return cmp.Compare[uint32](a.TxIndex, b.TxIndex) }) } @@ -181,26 +191,21 @@ func makeTestAccountAccess(sort bool) AccountAccess { StorageReads: storageReads, BalanceChanges: balances, NonceChanges: nonces, - CodeChanges: []encodingCodeChange{ - { - TxIndex: 100, - Code: testrand.Bytes(256), - }, - }, + CodeChanges: codes, } } func makeTestBAL(sort bool) *BlockAccessList { - list := &BlockAccessList{} + list := make(BlockAccessList, 0, 5) for i := 0; i < 5; i++ { - list.Accesses = append(list.Accesses, makeTestAccountAccess(sort)) + list = append(list, makeTestAccountAccess(sort)) } if sort { - slices.SortFunc(list.Accesses, func(a, b AccountAccess) int { + slices.SortFunc(list, func(a, b AccountAccess) int { return bytes.Compare(a.Address[:], b.Address[:]) }) } - return list + return &list } func TestBlockAccessListCopy(t *testing.T) { @@ -216,9 +221,9 @@ func TestBlockAccessListCopy(t *testing.T) { } // Make sure the mutations on copy won't affect the origin - for _, aa := range cpyCpy.Accesses { + for _, aa := range *cpyCpy { for i := 0; i < len(aa.StorageReads); i++ { - aa.StorageReads[i] = [32]byte(testrand.Bytes(32)) + aa.StorageReads[i] = new(uint256.Int).SetBytes(testrand.Bytes(32)) } } if !reflect.DeepEqual(list, cpy) { @@ -229,7 +234,7 @@ func TestBlockAccessListCopy(t *testing.T) { func TestBlockAccessListValidation(t *testing.T) { // Validate the block access list after RLP decoding enc := makeTestBAL(true) - if err := enc.Validate(); err != nil { + if err := enc.Validate(params.Rules{}); err != nil { t.Fatalf("Unexpected validation error: %v", err) } var buf bytes.Buffer @@ -241,14 +246,14 @@ func TestBlockAccessListValidation(t *testing.T) { if err := dec.DecodeRLP(rlp.NewStream(bytes.NewReader(buf.Bytes()), 0)); err != nil { t.Fatalf("Unexpected RLP-decode error: %v", err) } - if err := dec.Validate(); err != nil { + if err := dec.Validate(params.Rules{}); err != nil { t.Fatalf("Unexpected validation error: %v", err) } // Validate the derived block access list cBAL := makeTestConstructionBAL() listB := cBAL.toEncodingObj() - if err := listB.Validate(); err != nil { + if err := listB.Validate(params.Rules{}); err != nil { t.Fatalf("Unexpected validation error: %v", err) } } diff --git a/eth/protocols/snap/handler_test.go b/eth/protocols/snap/handler_test.go index 3f6a43a059..b0522c20bb 100644 --- a/eth/protocols/snap/handler_test.go +++ b/eth/protocols/snap/handler_test.go @@ -31,18 +31,24 @@ import ( "github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rlp" + "github.com/holiman/uint256" ) func makeTestBAL(minSize int) *bal.BlockAccessList { n := minSize/33 + 1 // 33 bytes per storage read slot in RLP access := bal.AccountAccess{ Address: common.HexToAddress("0x01"), - StorageReads: make([][32]byte, n), + StorageReads: make([]*uint256.Int, 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 { - binary.BigEndian.PutUint64(access.StorageReads[i][24:], uint64(i)) + var b [32]byte + b[0] = 0xff + binary.BigEndian.PutUint64(b[24:], uint64(i)) + access.StorageReads[i] = new(uint256.Int).SetBytes(b[:]) } - return &bal.BlockAccessList{Accesses: []bal.AccountAccess{access}} + return &bal.BlockAccessList{access} } // getChainWithBALs creates a minimal test chain with BALs stored for each block. From a06558042299eba2e19ca656d3f46edb85df1420 Mon Sep 17 00:00:00 2001 From: Rahman Date: Mon, 27 Apr 2026 01:25:57 -0600 Subject: [PATCH 07/14] triedb/pathdb: compute size in StateSetWithOrigin.decode (#34828) `StateSetWithOrigin.decode()` was missing size computation after deserializing origin data, causing `size` to remain zero after journal reload. Added the same calculation logic used in `NewStateSetWithOrigin()`. --- triedb/pathdb/states.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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 } From b1daa4432c9dbe2eff03aa8f639df552f5ee6f83 Mon Sep 17 00:00:00 2001 From: MariusVanDerWijden Date: Mon, 27 Apr 2026 16:46:00 +0200 Subject: [PATCH 08/14] core: implement EIP-8037: state creation gas cost increase (new spec) --- cmd/evm/internal/t8ntool/transaction.go | 5 +- core/bench_test.go | 2 +- core/bintrie_witness_test.go | 4 +- core/evm.go | 54 +++- core/gaspool.go | 45 +++- core/state/journal.go | 114 ++++++++- core/state/statedb.go | 12 + core/state/statedb_hooked.go | 4 + core/state_processor.go | 6 +- core/state_transition.go | 311 +++++++++++++++++------- core/txpool/validation.go | 14 +- core/vm/contracts_fuzz_test.go | 2 +- core/vm/contracts_test.go | 8 +- core/vm/evm.go | 59 ++++- core/vm/gas_table_test.go | 4 +- core/vm/gascosts.go | 76 ++++-- core/vm/instructions.go | 12 +- core/vm/interface.go | 4 + core/vm/interpreter_test.go | 4 +- core/vm/runtime/runtime.go | 6 +- eth/catalyst/api_test.go | 113 +++++++++ eth/tracers/js/tracer_test.go | 2 +- eth/tracers/logger/logger_test.go | 2 +- internal/ethapi/api.go | 1 + params/protocol_params.go | 9 + tests/state_test.go | 2 +- tests/transaction_test_util.go | 4 +- 27 files changed, 711 insertions(+), 168 deletions(-) diff --git a/cmd/evm/internal/t8ntool/transaction.go b/cmd/evm/internal/t8ntool/transaction.go index ad89876601..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.IsHomestead, rules.IsIstanbul, rules.IsShanghai) + 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) @@ -147,7 +148,7 @@ func Transaction(ctx *cli.Context) error { } // For Prague txs, validate the floor data gas. if rules.IsPrague { - floorDataGas, err := core.FloorDataGas(rules, tx.Data()) + floorDataGas, err := core.FloorDataGas(rules, tx.Data(), tx.AccessList()) if err != nil { r.Error = err results = append(results, r) diff --git a/core/bench_test.go b/core/bench_test.go index 20d1a7794b..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, false, false, false) + 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 1b033151d3..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, true, true, true) + 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, true, true, true) + 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 818b23bee5..fe0847dfce 100644 --- a/core/evm.go +++ b/core/evm.go @@ -18,6 +18,7 @@ package core import ( "math/big" + "math/bits" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus" @@ -68,21 +69,50 @@ 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()), } } +// CostPerStateByte computes the cost per one byte of state creation +// after EIP-8037. +func CostPerStateByte(header *types.Header, config *params.ChainConfig) uint64 { + if !config.IsAmsterdam(header.Number, header.Time) { + return 0 + } + const ( + blocksPerYear uint64 = 2_628_000 // 7200 * 365 + offset uint64 = 9578 + significantBts uint64 = 5 + ) + numerator := header.GasLimit * blocksPerYear + denominator := uint64(2) * params.TargetStateGrowthPerYear + raw := (numerator + denominator - 1) / denominator + shifted := raw + offset + // bit length of shifted + bitLen := uint64(64 - bits.LeadingZeros64(shifted)) + var shift uint64 + if bitLen > significantBts { + shift = bitLen - significantBts + } + quantized := (shifted >> shift) << shift + if quantized > offset { + return quantized - offset + } + return 1 +} + // NewEVMTxContext creates a new transaction context for a single transaction. func NewEVMTxContext(msg *Message) vm.TxContext { ctx := vm.TxContext{ 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..6a7f54ebc8 100644 --- a/core/state/journal.go +++ b/core/state/journal.go @@ -55,12 +55,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 +78,7 @@ func (j *journal) reset() { j.entries = j.entries[:0] j.validRevisions = j.validRevisions[:0] clear(j.dirties) + clear(j.stateBytesCharged) j.nextRevisionId = 0 } @@ -135,6 +143,101 @@ func (j *journal) length() int { return len(j.entries) } +// stateChangedBytes computes the state bytes created by the call frame +// identified by snapshotId. Since subcalls always compute their results +// before the parent (innermost-first), this only scans journal entries +// between this snapshot and the next one — the frame's own entries. +// Subcall results are summed from the cache and subtracted. +// +// The result is cached in stateBytesCharged[snapshotId] so the parent +// frame can look it up instead of re-scanning. +func (j *journal) stateChangedBytes(snapshotId int, stateObjects map[common.Address]*stateObject) int64 { + // TODO (MariusVanDerWijden): this is a bit slop-py needs to be cleaned up + // Resolve snapshot index. + idx := sort.Search(len(j.validRevisions), func(i int) bool { + return j.validRevisions[i].id >= snapshotId + }) + if idx == len(j.validRevisions) || j.validRevisions[idx].id != snapshotId { + panic(fmt.Errorf("snapshot id %v not found for stateChangedBytes", snapshotId)) + } + start := j.validRevisions[idx].journalIndex + + // Our range is [start, end) where end is the next revision's start, + // or the end of the journal if we're the last revision. + end := len(j.entries) + if idx+1 < len(j.validRevisions) { + end = j.validRevisions[idx+1].journalIndex + } + + // Walk only our own entries. + type slotKey struct { + addr common.Address + key common.Hash + } + 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) + + for i := start; i < end; i++ { + switch e := j.entries[i].(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} + } + } + } + + 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-exising 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)) + } + } + + // Cache our result so the parent can look it up. + j.stateBytesCharged[snapshotId] = totalBytes + return totalBytes +} + // copy returns a deep-copied journal. func (j *journal) copy() *journal { entries := make([]journalEntry, 0, j.length()) @@ -142,10 +245,11 @@ func (j *journal) copy() *journal { entries = append(entries, j.entries[i].copy()) } 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: slices.Clone(j.validRevisions), + nextRevisionId: j.nextRevisionId, + stateBytesCharged: maps.Clone(j.stateBytesCharged), } } diff --git a/core/state/statedb.go b/core/state/statedb.go index 1858f4758d..ee28605c03 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -758,6 +758,18 @@ func (s *StateDB) GetRefund() uint64 { return s.refund } +const ( + CostPerAccount = 112 + CostPerSlot = 32 +) + +// StateChangedBytes computes the state bytes created since the given snapshot, +// excluding bytes already charged by subcalls. See journal.stateChangedBytes +// for the detailed accounting. +func (s *StateDB) StateChangedBytes(snapshotId int) int64 { + return s.journal.stateChangedBytes(snapshotId, s.stateObjects) +} + type removedAccountWithBalance struct { address common.Address balance *uint256.Int diff --git a/core/state/statedb_hooked.go b/core/state/statedb_hooked.go index c5faa7c98e..a9ea4d5397 100644 --- a/core/state/statedb_hooked.go +++ b/core/state/statedb_hooked.go @@ -288,3 +288,7 @@ func (s *hookedStateDB) Finalise(deleteEmptyObjects bool) *bal.StateAccessList { } return s.inner.Finalise(deleteEmptyObjects) } + +func (s *hookedStateDB) StateChangedBytes(snapshotId int) int64 { + return s.inner.StateChangedBytes(snapshotId) +} diff --git a/core/state_processor.go b/core/state_processor.go index fda3bf8fe7..ac233cc8d3 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -261,7 +261,7 @@ func ProcessBeaconBlockRoot(beaconRoot common.Hash, evm *vm.EVM) { } 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, vm.NewGasBudgetReg(30_000_000), common.U2560) if evm.StateDB.AccessEvents() != nil { evm.StateDB.AccessEvents().Merge(evm.AccessEvents) } @@ -288,7 +288,7 @@ func ProcessParentBlockHash(prevHash common.Hash, evm *vm.EVM) { } 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, vm.NewGasBudgetReg(30_000_000), common.U2560) if err != nil { panic(err) } @@ -327,7 +327,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, vm.NewGasBudgetReg(30_000_000), 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 c3ebffd060..afeb1dd453 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, isHomestead, isEIP2028, isEIP3860 bool) (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 - if isContractCreation && isHomestead { - gas = params.TxGasContractCreation + var gas vm.GasCosts + if isContractCreation && rules.IsHomestead { + 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 @@ -85,39 +101,60 @@ func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.Set // Make sure we don't exceed uint64 for all data combinations nonZeroGas := params.TxDataNonZeroGasFrontier - if isEIP2028 { + 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 && isEIP3860 { + 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 { - gas += uint64(len(accessList)) * params.TxAccessListAddressGas - gas += uint64(accessList.StorageKeys()) * params.TxAccessListStorageKeyGas + addresses := uint64(len(accessList)) + storageKeys := uint64(accessList.StorageKeys()) + if (math.MaxUint64-gas.RegularGas)/params.TxAccessListAddressGas < addresses { + return vm.GasCosts{}, ErrGasUintOverflow + } + gas.RegularGas += addresses * params.TxAccessListAddressGas + if (math.MaxUint64-gas.RegularGas)/params.TxAccessListStorageKeyGas < storageKeys { + return vm.GasCosts{}, ErrGasUintOverflow + } + gas.RegularGas += storageKeys * params.TxAccessListStorageKeyGas + + // EIP-7981: access list data is charged in addition to the base charge. + if rules.IsAmsterdam { + const ( + addressCost = common.AddressLength * params.TxCostFloorPerToken7976 * params.TxTokenPerNonZeroByte + storageKeyCost = common.HashLength * params.TxCostFloorPerToken7976 * params.TxTokenPerNonZeroByte + ) + if (math.MaxUint64-gas.RegularGas)/addressCost < addresses { + return vm.GasCosts{}, ErrGasUintOverflow + } + gas.RegularGas += addresses * addressCost + if (math.MaxUint64-gas.RegularGas)/storageKeyCost < storageKeys { + return vm.GasCosts{}, ErrGasUintOverflow + } + gas.RegularGas += storageKeys * storageKeyCost + } } - if authList != nil { - gas += uint64(len(authList)) * params.CallNewAccountGas - } - 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). -func FloorDataGas(rules params.Rules, data []byte) (uint64, error) { +func FloorDataGas(rules params.Rules, data []byte, accessList types.AccessList) (uint64, error) { var ( tokens uint64 tokenCost uint64 @@ -127,6 +164,9 @@ func FloorDataGas(rules params.Rules, data []byte) (uint64, error) { // From 10/40 to 64/64 for zero/non-zero bytes. tokens = uint64(len(data)) * params.TxTokenPerNonZeroByte tokenCost = params.TxCostFloorPerToken7976 + // EIP-7981 adds additional tokens for every entry in the accesslist + tokens += uint64(len(accessList)) * common.AddressLength * params.TxTokenPerNonZeroByte + tokens += uint64(accessList.StorageKeys()) * common.HashLength * params.TxTokenPerNonZeroByte } else { var ( z = uint64(bytes.Count(data, []byte{0})) @@ -282,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) @@ -306,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) @@ -362,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) } } @@ -387,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) } } } @@ -410,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) } } @@ -419,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() @@ -450,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 } @@ -460,26 +504,64 @@ 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.IsHomestead, rules.IsIstanbul, rules.IsShanghai) + cost, err := IntrinsicGas(msg.Data, msg.AccessList, msg.SetCodeAuthorizations, contractCreation, rules, st.evm.Context.CostPerStateByte) if err != nil { return nil, err } + + // 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 + } + } + + // 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 { + return nil, err + } + if msg.GasLimit < floorDataGas { + return nil, fmt.Errorf("%w: have %d, want %d", ErrFloorDataGas, msg.GasLimit, floorDataGas) + } + } + + 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 { - t.OnGasChange(prior, st.gasRemaining.RegularGas, tracing.GasChangeTxIntrinsicGas) - } - // Gas limit suffices for the floor data cost (EIP-7623) - if rules.IsPrague { - floorDataGas, err = FloorDataGas(rules, msg.Data) - if err != nil { - return nil, err - } - if msg.GasLimit < floorDataGas { - return nil, fmt.Errorf("%w: have %d, want %d", ErrFloorDataGas, msg.GasLimit, floorDataGas) + if rules.IsAmsterdam { + t.OnGasChange(msg.GasLimit, st.gasRemaining.RegularGas+st.gasRemaining.StateGas, tracing.GasChangeTxIntrinsicGas) + } else { + t.OnGasChange(prior, st.gasRemaining.RegularGas, tracing.GasChangeTxIntrinsicGas) } } @@ -516,6 +598,12 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { ret []byte vmerr error // vm errors do not effect consensus and are therefore not assigned to err ) + // EIP-8037: Take a snapshot for the outer call frame so we can compute + // state gas for state changes made at the transaction level (nonce, + // value transfer, authorizations, and contract creation overhead). + outerSnapshot := st.state.Snapshot() + + var execGasUsed vm.GasUsed if contractCreation { ret, _, st.gasRemaining, vmerr = st.evm.Create(msg.From, msg.Data, st.gasRemaining, value) } else { @@ -526,7 +614,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) } } @@ -543,6 +631,19 @@ 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) + 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() @@ -553,28 +654,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 { @@ -587,8 +701,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 @@ -601,8 +721,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, @@ -641,30 +762,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. + var refund uint64 if st.state.Exist(authority) { - st.state.AddRefund(params.CallNewAccountGas - params.TxAuthTupleGas) + 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. @@ -683,22 +817,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 85bf65ac40..284fc062fc 100644 --- a/core/txpool/validation.go +++ b/core/txpool/validation.go @@ -125,16 +125,26 @@ 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, true, rules.IsIstanbul, rules.IsShanghai) + 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()) + floorDataGas, err := core.FloorDataGas(rules, tx.Data(), tx.AccessList()) if err != nil { return err } 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/evm.go b/core/vm/evm.go index 59e301c0a7..27323cfb4f 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. @@ -316,6 +317,15 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g // TODO: consider clearing up unused snapshots: //} else { // evm.StateDB.DiscardSnapshot(snapshot) + } else if evm.chainRules.IsAmsterdam { + // Charge state costs + bytesCharged := evm.StateDB.StateChangedBytes(snapshot) + stateGasCost := GasCosts{StateGas: bytesCharged * int64(evm.Context.CostPerStateByte)} + if !gas.CanAfford(stateGasCost) { + gas.Exhaust() + return ret, gas, ErrOutOfGas + } + gas.Charge(stateGasCost) } return ret, gas, err } @@ -367,6 +377,14 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt } gas.Exhaust() } + } else if evm.chainRules.IsAmsterdam { + bytesCharged := evm.StateDB.StateChangedBytes(snapshot) + stateGasCost := GasCosts{StateGas: bytesCharged * int64(evm.Context.CostPerStateByte)} + if !gas.CanAfford(stateGasCost) { + gas.Exhaust() + return ret, gas, ErrOutOfGas + } + gas.Charge(stateGasCost) } return ret, gas, err } @@ -411,6 +429,14 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address, } gas.Exhaust() } + } else if evm.chainRules.IsAmsterdam { + bytesCharged := evm.StateDB.StateChangedBytes(snapshot) + stateGasCost := GasCosts{StateGas: bytesCharged * int64(evm.Context.CostPerStateByte)} + if !gas.CanAfford(stateGasCost) { + gas.Exhaust() + return ret, gas, ErrOutOfGas + } + gas.Charge(stateGasCost) } return ret, gas, err } @@ -555,6 +581,12 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value } evm.Context.Transfer(evm.StateDB, caller, address, value, &evm.chainRules) + if evm.chainRules.IsAmsterdam { + // Compute the state changed for the contract init. + evm.StateDB.StateChangedBytes(snapshot) + } + initSnapshot := evm.StateDB.Snapshot() + // Initialise a new contract and set the code that is to be used by the EVM. // The contract is a scoped environment for this execution context only. contract := NewContract(caller, address, value, gas, evm.jumpDests) @@ -570,6 +602,15 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value 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(initSnapshot) + stateGasCost := GasCosts{StateGas: bytesCharged * int64(evm.Context.CostPerStateByte)} + if !contract.Gas.CanAfford(stateGasCost) { + contract.Gas.Exhaust() + return ret, address, contract.Gas, ErrOutOfGas + } + contract.Gas.Charge(stateGasCost) } return ret, address, contract.Gas, err } 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 487d8002f9..a58fddac22 100644 --- a/core/vm/interface.go +++ b/core/vm/interface.go @@ -99,4 +99,8 @@ type StateDB interface { // Finalise must be invoked at the end of a transaction Finalise(bool) *bal.StateAccessList + + // StateChangedBytes returns the number of state bytes created since the + // given snapshot. Used by EIP-8037 for state gas metering. + StateChangedBytes(snapshotId int) int64 } 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/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/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 149e12c5b8..58414b3a87 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -1341,6 +1341,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 9da275c486..e631776a51 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. @@ -186,6 +188,13 @@ const ( HistoryServeWindow = 8191 // Number of blocks to serve historical block hashes for, EIP-2935. MaxBlockSize = 8_388_608 // maximum size of an RLP-encoded block + + TargetStateGrowthPerYear = 100 * 1024 * 1024 * 1024 // 100GB + AccountCreationSize = 112 + StorageCreationSize = 32 + AuthorizationCreationSize = 23 + + GasBlockAccessListItem = 2000 // EIP-7928: gas cost per BAL item for gas limit check ) // Bls12381G1MultiExpDiscountTable is the gas discount table for BLS12-381 G1 multi exponentiation operation 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 8b8d0357bf..e38bcea410 100644 --- a/tests/transaction_test_util.go +++ b/tests/transaction_test_util.go @@ -81,7 +81,7 @@ func (tt *TransactionTest) Run() error { return } // Intrinsic gas - cost, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules.IsHomestead, rules.IsIstanbul, rules.IsShanghai) + cost, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules, 1) if err != nil { return } @@ -92,7 +92,7 @@ func (tt *TransactionTest) Run() error { if rules.IsPrague { var floorDataGas uint64 - floorDataGas, err = core.FloorDataGas(rules, tx.Data()) + floorDataGas, err = core.FloorDataGas(rules, tx.Data(), tx.AccessList()) if err != nil { return } From c3d51efe2d47489c8ba5733a79c170b520c3886e Mon Sep 17 00:00:00 2001 From: MariusVanDerWijden Date: Tue, 28 Apr 2026 16:49:02 +0200 Subject: [PATCH 09/14] core: more fixes --- core/vm/evm.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/core/vm/evm.go b/core/vm/evm.go index 27323cfb4f..daa751b95c 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -280,6 +280,11 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g } evm.StateDB.CreateAccount(addr) } + if evm.chainRules.IsAmsterdam { + // Compute state changed bytes for account creation. + evm.StateDB.StateChangedBytes(snapshot) + } + innerSnapshot := evm.StateDB.Snapshot() // Perform the value transfer only in non-syscall mode. // Calling this is required even for zero-value transfers, // to ensure the state clearing mechanism is applied. @@ -319,7 +324,7 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g // evm.StateDB.DiscardSnapshot(snapshot) } else if evm.chainRules.IsAmsterdam { // Charge state costs - bytesCharged := evm.StateDB.StateChangedBytes(snapshot) + bytesCharged := evm.StateDB.StateChangedBytes(innerSnapshot) stateGasCost := GasCosts{StateGas: bytesCharged * int64(evm.Context.CostPerStateByte)} if !gas.CanAfford(stateGasCost) { gas.Exhaust() From 46be549b0024352f712ba83a393ea30c869c733a Mon Sep 17 00:00:00 2001 From: Gary Rong Date: Tue, 28 Apr 2026 23:31:28 +0800 Subject: [PATCH 10/14] core: extend the journal and introduce the ability for traversal --- core/state/journal.go | 85 +++++++++++++- core/state/journal_test.go | 213 +++++++++++++++++++++++++++++++++++ core/state/statedb.go | 9 ++ core/state/statedb_hooked.go | 4 + core/vm/evm.go | 76 ++++++++----- core/vm/interface.go | 6 + 6 files changed, 361 insertions(+), 32 deletions(-) create mode 100644 core/state/journal_test.go diff --git a/core/state/journal.go b/core/state/journal.go index 6a7f54ebc8..c80efa93db 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 @@ -86,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 } @@ -106,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) @@ -244,10 +317,18 @@ func (j *journal) copy() *journal { 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), + 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/statedb.go b/core/state/statedb.go index ee28605c03..875bde5d5a 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -753,6 +753,15 @@ 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 diff --git a/core/state/statedb_hooked.go b/core/state/statedb_hooked.go index a9ea4d5397..8886cacb70 100644 --- a/core/state/statedb_hooked.go +++ b/core/state/statedb_hooked.go @@ -147,6 +147,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() } diff --git a/core/vm/evm.go b/core/vm/evm.go index daa751b95c..1cf78814e7 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -276,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(snapshot) return nil, gas, nil } evm.StateDB.CreateAccount(addr) @@ -322,15 +323,18 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g // TODO: consider clearing up unused snapshots: //} else { // evm.StateDB.DiscardSnapshot(snapshot) - } else if evm.chainRules.IsAmsterdam { - // Charge state costs - bytesCharged := evm.StateDB.StateChangedBytes(innerSnapshot) - stateGasCost := GasCosts{StateGas: bytesCharged * int64(evm.Context.CostPerStateByte)} - if !gas.CanAfford(stateGasCost) { - gas.Exhaust() - return ret, gas, ErrOutOfGas + } else { + evm.StateDB.CloseSnapshot(snapshot) + if evm.chainRules.IsAmsterdam { + // Charge state costs + bytesCharged := evm.StateDB.StateChangedBytes(innerSnapshot) + stateGasCost := GasCosts{StateGas: bytesCharged * int64(evm.Context.CostPerStateByte)} + if !gas.CanAfford(stateGasCost) { + gas.Exhaust() + return ret, gas, ErrOutOfGas + } + gas.Charge(stateGasCost) } - gas.Charge(stateGasCost) } return ret, gas, err } @@ -382,14 +386,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) - stateGasCost := GasCosts{StateGas: bytesCharged * int64(evm.Context.CostPerStateByte)} - if !gas.CanAfford(stateGasCost) { - gas.Exhaust() - return ret, gas, ErrOutOfGas + } else { + evm.StateDB.CloseSnapshot(snapshot) + if evm.chainRules.IsAmsterdam { + bytesCharged := evm.StateDB.StateChangedBytes(snapshot) + stateGasCost := GasCosts{StateGas: bytesCharged * int64(evm.Context.CostPerStateByte)} + if !gas.CanAfford(stateGasCost) { + gas.Exhaust() + return ret, gas, ErrOutOfGas + } + gas.Charge(stateGasCost) } - gas.Charge(stateGasCost) } return ret, gas, err } @@ -434,15 +441,19 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address, } gas.Exhaust() } - } else if evm.chainRules.IsAmsterdam { - bytesCharged := evm.StateDB.StateChangedBytes(snapshot) - stateGasCost := GasCosts{StateGas: bytesCharged * int64(evm.Context.CostPerStateByte)} - if !gas.CanAfford(stateGasCost) { - gas.Exhaust() - return ret, gas, ErrOutOfGas + } else { + evm.StateDB.CloseSnapshot(snapshot) + if evm.chainRules.IsAmsterdam { + bytesCharged := evm.StateDB.StateChangedBytes(snapshot) + stateGasCost := GasCosts{StateGas: bytesCharged * int64(evm.Context.CostPerStateByte)} + if !gas.CanAfford(stateGasCost) { + gas.Exhaust() + return ret, gas, ErrOutOfGas + } + gas.Charge(stateGasCost) } - gas.Charge(stateGasCost) } + return ret, gas, err } @@ -497,6 +508,8 @@ func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []b } gas.Exhaust() } + } else { + evm.StateDB.CloseSnapshot(snapshot) } return ret, gas, err } @@ -607,15 +620,18 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value 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(initSnapshot) - stateGasCost := GasCosts{StateGas: bytesCharged * int64(evm.Context.CostPerStateByte)} - if !contract.Gas.CanAfford(stateGasCost) { - contract.Gas.Exhaust() - return ret, address, contract.Gas, ErrOutOfGas + } else { + evm.StateDB.CloseSnapshot(snapshot) + if evm.chainRules.IsAmsterdam { + // Charge initcode's state changes to the created contract's gas. + bytesCharged := evm.StateDB.StateChangedBytes(initSnapshot) + stateGasCost := GasCosts{StateGas: bytesCharged * int64(evm.Context.CostPerStateByte)} + if !contract.Gas.CanAfford(stateGasCost) { + contract.Gas.Exhaust() + return ret, address, contract.Gas, ErrOutOfGas + } + contract.Gas.Charge(stateGasCost) } - contract.Gas.Charge(stateGasCost) } return ret, address, contract.Gas, err } diff --git a/core/vm/interface.go b/core/vm/interface.go index a58fddac22..9c132061c6 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) From c1b1faed8b93ea3c46ddaff09651718c3016b96c Mon Sep 17 00:00:00 2001 From: MariusVanDerWijden Date: Tue, 28 Apr 2026 19:29:17 +0200 Subject: [PATCH 11/14] core/state: use journaling approach --- core/state/journal.go | 60 ++++++++++++++++++------------------ core/state/statedb.go | 9 +++--- core/state/statedb_hooked.go | 4 +-- core/state_transition.go | 9 +++--- core/vm/evm.go | 19 +++--------- core/vm/interface.go | 6 ++-- 6 files changed, 47 insertions(+), 60 deletions(-) diff --git a/core/state/journal.go b/core/state/journal.go index c80efa93db..b189d54427 100644 --- a/core/state/journal.go +++ b/core/state/journal.go @@ -216,33 +216,15 @@ func (j *journal) length() int { return len(j.entries) } -// stateChangedBytes computes the state bytes created by the call frame -// identified by snapshotId. Since subcalls always compute their results -// before the parent (innermost-first), this only scans journal entries -// between this snapshot and the next one — the frame's own entries. -// Subcall results are summed from the cache and subtracted. -// -// The result is cached in stateBytesCharged[snapshotId] so the parent -// frame can look it up instead of re-scanning. -func (j *journal) stateChangedBytes(snapshotId int, stateObjects map[common.Address]*stateObject) int64 { - // TODO (MariusVanDerWijden): this is a bit slop-py needs to be cleaned up - // Resolve snapshot index. - idx := sort.Search(len(j.validRevisions), func(i int) bool { - return j.validRevisions[i].id >= snapshotId - }) - if idx == len(j.validRevisions) || j.validRevisions[idx].id != snapshotId { - panic(fmt.Errorf("snapshot id %v not found for stateChangedBytes", snapshotId)) +// stateChangedBytes computes the state bytes created by the current (topmost) +// call frame, walking only entries that belong directly to this frame and +// skipping over closed child frame ranges. +func (j *journal) stateChangedBytes(stateObjects map[common.Address]*stateObject) int64 { + if len(j.validRevisions) == 0 { + return 0 } - start := j.validRevisions[idx].journalIndex + rev := j.validRevisions[len(j.validRevisions)-1] - // Our range is [start, end) where end is the next revision's start, - // or the end of the journal if we're the last revision. - end := len(j.entries) - if idx+1 < len(j.validRevisions) { - end = j.validRevisions[idx+1].journalIndex - } - - // Walk only our own entries. type slotKey struct { addr common.Address key common.Hash @@ -255,8 +237,11 @@ func (j *journal) stateChangedBytes(snapshotId int, stateObjects map[common.Addr created := make(map[common.Address]bool) codeChanged := make(map[common.Address]bool) - for i := start; i < end; i++ { - switch e := j.entries[i].(type) { + // Walk only this frame's own entries, skipping closed child ranges. + // Add cached subcall costs from closedChildren. + var subcallBytes int64 + visit := func(e journalEntry) { + switch e := e.(type) { case createContractChange: created[e.account] = true case codeChange: @@ -268,6 +253,18 @@ func (j *journal) stateChangedBytes(snapshotId int, stateObjects map[common.Addr } } } + idx := rev.journalIndex + for _, child := range rev.closedChildren { + for ; idx < child.start; idx++ { + visit(j.entries[idx]) + } + // Add the cached cost for this subcall. + subcallBytes += j.stateBytesCharged[child.start] + idx = child.end + } + for ; idx < len(j.entries); idx++ { + visit(j.entries[idx]) + } var totalBytes int64 for range created { @@ -296,7 +293,7 @@ func (j *journal) stateChangedBytes(snapshotId int, stateObjects map[common.Addr // cleared in earlier frame, re-set here — no charge. // - X → Y (non-zero to non-zero): no charge. // - zero → zero: no change. - // - !prevZero && curZero && !origZero: pre-exising slot was + // - !prevZero && curZero && !origZero: pre-existing slot was // cleared now, don't refund to not enable gas tokens. } for addr := range codeChanged { @@ -306,8 +303,11 @@ func (j *journal) stateChangedBytes(snapshotId int, stateObjects map[common.Addr } } - // Cache our result so the parent can look it up. - j.stateBytesCharged[snapshotId] = totalBytes + // Add subcall costs to get the total for this frame (own + children). + totalBytes += subcallBytes + + // Cache so the parent can look up this frame's total cost. + j.stateBytesCharged[rev.journalIndex] = totalBytes return totalBytes } diff --git a/core/state/statedb.go b/core/state/statedb.go index 875bde5d5a..5624c0c686 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -772,11 +772,10 @@ const ( CostPerSlot = 32 ) -// StateChangedBytes computes the state bytes created since the given snapshot, -// excluding bytes already charged by subcalls. See journal.stateChangedBytes -// for the detailed accounting. -func (s *StateDB) StateChangedBytes(snapshotId int) int64 { - return s.journal.stateChangedBytes(snapshotId, s.stateObjects) +// StateChangedBytes computes the state bytes created by the current (topmost) +// call frame, excluding entries from closed child frames. +func (s *StateDB) StateChangedBytes() int64 { + return s.journal.stateChangedBytes(s.stateObjects) } type removedAccountWithBalance struct { diff --git a/core/state/statedb_hooked.go b/core/state/statedb_hooked.go index 8886cacb70..b1d05c30a2 100644 --- a/core/state/statedb_hooked.go +++ b/core/state/statedb_hooked.go @@ -293,6 +293,6 @@ func (s *hookedStateDB) Finalise(deleteEmptyObjects bool) *bal.StateAccessList { return s.inner.Finalise(deleteEmptyObjects) } -func (s *hookedStateDB) StateChangedBytes(snapshotId int) int64 { - return s.inner.StateChangedBytes(snapshotId) +func (s *hookedStateDB) StateChangedBytes() int64 { + return s.inner.StateChangedBytes() } diff --git a/core/state_transition.go b/core/state_transition.go index afeb1dd453..932699acd1 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -598,10 +598,9 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { ret []byte vmerr error // vm errors do not effect consensus and are therefore not assigned to err ) - // EIP-8037: Take a snapshot for the outer call frame so we can compute - // state gas for state changes made at the transaction level (nonce, - // value transfer, authorizations, and contract creation overhead). - outerSnapshot := st.state.Snapshot() + + // Take a snapshot for gas calculation + st.state.Snapshot() var execGasUsed vm.GasUsed if contractCreation { @@ -634,7 +633,7 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { // EIP-8037: charge state gas for the outer call frame's own state changes. if rules.IsAmsterdam { if vmerr == nil { - outerBytes := st.state.StateChangedBytes(outerSnapshot) + outerBytes := st.state.StateChangedBytes() st.gasRemaining.Charge(vm.GasCosts{StateGas: outerBytes * int64(st.evm.Context.CostPerStateByte)}) } else { if execGasUsed.StateGas > 0 { diff --git a/core/vm/evm.go b/core/vm/evm.go index 1cf78814e7..cb14a3743b 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -281,11 +281,6 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g } evm.StateDB.CreateAccount(addr) } - if evm.chainRules.IsAmsterdam { - // Compute state changed bytes for account creation. - evm.StateDB.StateChangedBytes(snapshot) - } - innerSnapshot := evm.StateDB.Snapshot() // Perform the value transfer only in non-syscall mode. // Calling this is required even for zero-value transfers, // to ensure the state clearing mechanism is applied. @@ -327,7 +322,7 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g evm.StateDB.CloseSnapshot(snapshot) if evm.chainRules.IsAmsterdam { // Charge state costs - bytesCharged := evm.StateDB.StateChangedBytes(innerSnapshot) + bytesCharged := evm.StateDB.StateChangedBytes() stateGasCost := GasCosts{StateGas: bytesCharged * int64(evm.Context.CostPerStateByte)} if !gas.CanAfford(stateGasCost) { gas.Exhaust() @@ -389,7 +384,7 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt } else { evm.StateDB.CloseSnapshot(snapshot) if evm.chainRules.IsAmsterdam { - bytesCharged := evm.StateDB.StateChangedBytes(snapshot) + bytesCharged := evm.StateDB.StateChangedBytes() stateGasCost := GasCosts{StateGas: bytesCharged * int64(evm.Context.CostPerStateByte)} if !gas.CanAfford(stateGasCost) { gas.Exhaust() @@ -444,7 +439,7 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address, } else { evm.StateDB.CloseSnapshot(snapshot) if evm.chainRules.IsAmsterdam { - bytesCharged := evm.StateDB.StateChangedBytes(snapshot) + bytesCharged := evm.StateDB.StateChangedBytes() stateGasCost := GasCosts{StateGas: bytesCharged * int64(evm.Context.CostPerStateByte)} if !gas.CanAfford(stateGasCost) { gas.Exhaust() @@ -599,12 +594,6 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value } evm.Context.Transfer(evm.StateDB, caller, address, value, &evm.chainRules) - if evm.chainRules.IsAmsterdam { - // Compute the state changed for the contract init. - evm.StateDB.StateChangedBytes(snapshot) - } - initSnapshot := evm.StateDB.Snapshot() - // Initialise a new contract and set the code that is to be used by the EVM. // The contract is a scoped environment for this execution context only. contract := NewContract(caller, address, value, gas, evm.jumpDests) @@ -624,7 +613,7 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value evm.StateDB.CloseSnapshot(snapshot) if evm.chainRules.IsAmsterdam { // Charge initcode's state changes to the created contract's gas. - bytesCharged := evm.StateDB.StateChangedBytes(initSnapshot) + bytesCharged := evm.StateDB.StateChangedBytes() stateGasCost := GasCosts{StateGas: bytesCharged * int64(evm.Context.CostPerStateByte)} if !contract.Gas.CanAfford(stateGasCost) { contract.Gas.Exhaust() diff --git a/core/vm/interface.go b/core/vm/interface.go index 9c132061c6..3c5f4acd6c 100644 --- a/core/vm/interface.go +++ b/core/vm/interface.go @@ -106,7 +106,7 @@ type StateDB interface { // Finalise must be invoked at the end of a transaction Finalise(bool) *bal.StateAccessList - // StateChangedBytes returns the number of state bytes created since the - // given snapshot. Used by EIP-8037 for state gas metering. - StateChangedBytes(snapshotId int) int64 + // StateChangedBytes returns the number of state bytes created by the + // current call frame. Used by EIP-8037 for state gas metering. + StateChangedBytes() int64 } From 87641c7265284ecf69c75e4ddd4400b855df8e74 Mon Sep 17 00:00:00 2001 From: MariusVanDerWijden Date: Tue, 28 Apr 2026 21:06:05 +0200 Subject: [PATCH 12/14] core/state: charge account creation to parent --- core/state/journal.go | 41 +++++++++++++++++----------- core/state/statedb.go | 9 ++++--- core/state/statedb_hooked.go | 4 +-- core/state_transition.go | 4 +-- core/vm/evm.go | 52 +++++++++++++++++++++++------------- core/vm/interface.go | 6 +++-- 6 files changed, 73 insertions(+), 43 deletions(-) diff --git a/core/state/journal.go b/core/state/journal.go index b189d54427..903deacc79 100644 --- a/core/state/journal.go +++ b/core/state/journal.go @@ -216,14 +216,23 @@ func (j *journal) length() int { return len(j.entries) } -// stateChangedBytes computes the state bytes created by the current (topmost) -// call frame, walking only entries that belong directly to this frame and -// skipping over closed child frame ranges. -func (j *journal) stateChangedBytes(stateObjects map[common.Address]*stateObject) int64 { - if len(j.validRevisions) == 0 { - return 0 +// 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[len(j.validRevisions)-1] + rev := j.validRevisions[idx] type slotKey struct { addr common.Address @@ -253,17 +262,19 @@ func (j *journal) stateChangedBytes(stateObjects map[common.Address]*stateObject } } } - idx := rev.journalIndex + pos := rev.journalIndex for _, child := range rev.closedChildren { - for ; idx < child.start; idx++ { - visit(j.entries[idx]) + for ; pos < child.start; pos++ { + visit(j.entries[pos]) } - // Add the cached cost for this subcall. - subcallBytes += j.stateBytesCharged[child.start] - idx = child.end + if !excludeSubcalls { + // Add the cached cost for this subcall. + subcallBytes += j.stateBytesCharged[child.start] + } + pos = child.end } - for ; idx < len(j.entries); idx++ { - visit(j.entries[idx]) + for ; pos < len(j.entries); pos++ { + visit(j.entries[pos]) } var totalBytes int64 diff --git a/core/state/statedb.go b/core/state/statedb.go index 5624c0c686..56b083e17c 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -772,10 +772,11 @@ const ( CostPerSlot = 32 ) -// StateChangedBytes computes the state bytes created by the current (topmost) -// call frame, excluding entries from closed child frames. -func (s *StateDB) StateChangedBytes() int64 { - return s.journal.stateChangedBytes(s.stateObjects) +// 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) } type removedAccountWithBalance struct { diff --git a/core/state/statedb_hooked.go b/core/state/statedb_hooked.go index b1d05c30a2..db3d692aee 100644 --- a/core/state/statedb_hooked.go +++ b/core/state/statedb_hooked.go @@ -293,6 +293,6 @@ func (s *hookedStateDB) Finalise(deleteEmptyObjects bool) *bal.StateAccessList { return s.inner.Finalise(deleteEmptyObjects) } -func (s *hookedStateDB) StateChangedBytes() int64 { - return s.inner.StateChangedBytes() +func (s *hookedStateDB) StateChangedBytes(revid int, excludeSubcalls bool) int64 { + return s.inner.StateChangedBytes(revid, excludeSubcalls) } diff --git a/core/state_transition.go b/core/state_transition.go index 932699acd1..fa2332106f 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -600,7 +600,7 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { ) // Take a snapshot for gas calculation - st.state.Snapshot() + outerSnapshot := st.state.Snapshot() var execGasUsed vm.GasUsed if contractCreation { @@ -633,7 +633,7 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { // EIP-8037: charge state gas for the outer call frame's own state changes. if rules.IsAmsterdam { if vmerr == nil { - outerBytes := st.state.StateChangedBytes() + outerBytes := st.state.StateChangedBytes(outerSnapshot, false) st.gasRemaining.Charge(vm.GasCosts{StateGas: outerBytes * int64(st.evm.Context.CostPerStateByte)}) } else { if execGasUsed.StateGas > 0 { diff --git a/core/vm/evm.go b/core/vm/evm.go index cb14a3743b..8a01205ba6 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -255,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) { @@ -268,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 } @@ -276,7 +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(snapshot) + evm.StateDB.CloseSnapshot(snapshot1) return nil, gas, nil } evm.StateDB.CreateAccount(addr) @@ -288,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 { @@ -308,28 +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 { - evm.StateDB.CloseSnapshot(snapshot) if evm.chainRules.IsAmsterdam { - // Charge state costs - bytesCharged := evm.StateDB.StateChangedBytes() + // 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 } @@ -382,9 +388,8 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt gas.Exhaust() } } else { - evm.StateDB.CloseSnapshot(snapshot) if evm.chainRules.IsAmsterdam { - bytesCharged := evm.StateDB.StateChangedBytes() + bytesCharged := evm.StateDB.StateChangedBytes(snapshot, false) stateGasCost := GasCosts{StateGas: bytesCharged * int64(evm.Context.CostPerStateByte)} if !gas.CanAfford(stateGasCost) { gas.Exhaust() @@ -392,6 +397,7 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt } gas.Charge(stateGasCost) } + evm.StateDB.CloseSnapshot(snapshot) } return ret, gas, err } @@ -437,9 +443,8 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address, gas.Exhaust() } } else { - evm.StateDB.CloseSnapshot(snapshot) if evm.chainRules.IsAmsterdam { - bytesCharged := evm.StateDB.StateChangedBytes() + bytesCharged := evm.StateDB.StateChangedBytes(snapshot, false) stateGasCost := GasCosts{StateGas: bytesCharged * int64(evm.Context.CostPerStateByte)} if !gas.CanAfford(stateGasCost) { gas.Exhaust() @@ -447,6 +452,7 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address, } gas.Charge(stateGasCost) } + evm.StateDB.CloseSnapshot(snapshot) } return ret, gas, err @@ -567,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) } @@ -594,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) @@ -605,22 +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 { - evm.StateDB.CloseSnapshot(snapshot) if evm.chainRules.IsAmsterdam { // Charge initcode's state changes to the created contract's gas. - bytesCharged := evm.StateDB.StateChangedBytes() + 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 } diff --git a/core/vm/interface.go b/core/vm/interface.go index 3c5f4acd6c..3e2f6d115c 100644 --- a/core/vm/interface.go +++ b/core/vm/interface.go @@ -107,6 +107,8 @@ type StateDB interface { Finalise(bool) *bal.StateAccessList // StateChangedBytes returns the number of state bytes created by the - // current call frame. Used by EIP-8037 for state gas metering. - StateChangedBytes() int64 + // 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 } From 48d7bf08d723d921b716d4aecdf35161ec4355a1 Mon Sep 17 00:00:00 2001 From: MariusVanDerWijden Date: Tue, 28 Apr 2026 22:26:39 +0200 Subject: [PATCH 13/14] core: do selfdestructs --- core/evm.go | 22 +-------------- core/state/statedb.go | 55 ++++++++++++++++++++++++++++++++++++ core/state/statedb_hooked.go | 4 +++ core/state_transition.go | 2 ++ core/vm/interface.go | 5 ++++ 5 files changed, 67 insertions(+), 21 deletions(-) diff --git a/core/evm.go b/core/evm.go index fe0847dfce..019a8f29ed 100644 --- a/core/evm.go +++ b/core/evm.go @@ -18,7 +18,6 @@ package core import ( "math/big" - "math/bits" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus" @@ -91,26 +90,7 @@ func CostPerStateByte(header *types.Header, config *params.ChainConfig) uint64 { if !config.IsAmsterdam(header.Number, header.Time) { return 0 } - const ( - blocksPerYear uint64 = 2_628_000 // 7200 * 365 - offset uint64 = 9578 - significantBts uint64 = 5 - ) - numerator := header.GasLimit * blocksPerYear - denominator := uint64(2) * params.TargetStateGrowthPerYear - raw := (numerator + denominator - 1) / denominator - shifted := raw + offset - // bit length of shifted - bitLen := uint64(64 - bits.LeadingZeros64(shifted)) - var shift uint64 - if bitLen > significantBts { - shift = bitLen - significantBts - } - quantized := (shifted >> shift) << shift - if quantized > offset { - return quantized - offset - } - return 1 + return 1174 } // NewEVMTxContext creates a new transaction context for a single transaction. diff --git a/core/state/statedb.go b/core/state/statedb.go index 56b083e17c..5920277728 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -779,6 +779,61 @@ 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 db3d692aee..652dd7d9b8 100644 --- a/core/state/statedb_hooked.go +++ b/core/state/statedb_hooked.go @@ -296,3 +296,7 @@ func (s *hookedStateDB) Finalise(deleteEmptyObjects bool) *bal.StateAccessList { 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_transition.go b/core/state_transition.go index fa2332106f..0662e8c16b 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -634,6 +634,8 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { 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 { diff --git a/core/vm/interface.go b/core/vm/interface.go index 3e2f6d115c..ccd5210d6c 100644 --- a/core/vm/interface.go +++ b/core/vm/interface.go @@ -111,4 +111,9 @@ type StateDB interface { // 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 } From f71f4da77a01e44fbbca60010f7a24fe58a44434 Mon Sep 17 00:00:00 2001 From: MariusVanDerWijden Date: Tue, 28 Apr 2026 22:33:01 +0200 Subject: [PATCH 14/14] core: last fixes --- core/state_processor.go | 19 +++++++++-- core/state_transition.go | 6 ++-- core/vm/eips.go | 8 +++++ core/vm/evm.go | 10 +++++- core/vm/jump_table.go | 1 + core/vm/operations_acl.go | 72 +++++++++++++++++++++++++++++++++++++++ params/protocol_params.go | 1 + 7 files changed, 110 insertions(+), 7 deletions(-) diff --git a/core/state_processor.go b/core/state_processor.go index ac233cc8d3..a00989d3fc 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -241,6 +241,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) { @@ -261,7 +274,7 @@ func ProcessBeaconBlockRoot(beaconRoot common.Hash, evm *vm.EVM) { } evm.SetTxContext(NewEVMTxContext(msg)) evm.StateDB.AddAddressToAccessList(params.BeaconRootsAddress) - _, _, _ = evm.Call(msg.From, *msg.To, msg.Data, vm.NewGasBudgetReg(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) } @@ -288,7 +301,7 @@ func ProcessParentBlockHash(prevHash common.Hash, evm *vm.EVM) { } evm.SetTxContext(NewEVMTxContext(msg)) evm.StateDB.AddAddressToAccessList(params.HistoryStorageAddress) - _, _, err := evm.Call(msg.From, *msg.To, msg.Data, vm.NewGasBudgetReg(30_000_000), common.U2560) + _, _, err := evm.Call(msg.From, *msg.To, msg.Data, systemCallGasBudget(evm), common.U2560) if err != nil { panic(err) } @@ -327,7 +340,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.NewGasBudgetReg(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 0662e8c16b..e0fd882dcf 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -769,10 +769,10 @@ func (st *stateTransition) applyAuthorization(rules params.Rules, auth *types.Se return 0, err } - // If the account already exists in state, refund the new account cost - // charged in the intrinsic calculation. + // 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.Exist(authority) { + 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 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 8a01205ba6..a0250bfdc0 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -660,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/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/params/protocol_params.go b/params/protocol_params.go index e631776a51..d76ed62b15 100644 --- a/params/protocol_params.go +++ b/params/protocol_params.go @@ -193,6 +193,7 @@ const ( AccountCreationSize = 112 StorageCreationSize = 32 AuthorizationCreationSize = 23 + SystemMaxSstoresPerCall = 16 // EIP-8037: upper bound on new SSTOREs per system call GasBlockAccessListItem = 2000 // EIP-7928: gas cost per BAL item for gas limit check )