diff --git a/accounts/keystore/watch.go b/accounts/keystore/watch.go index 1bef321cd1..e2fcd1871a 100644 --- a/accounts/keystore/watch.go +++ b/accounts/keystore/watch.go @@ -14,8 +14,8 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -//go:build (darwin && !ios && cgo) || freebsd || (linux && !arm64) || netbsd || solaris -// +build darwin,!ios,cgo freebsd linux,!arm64 netbsd solaris +//go:build (darwin && !ios && cgo) || freebsd || linux || netbsd || solaris +// +build darwin,!ios,cgo freebsd linux netbsd solaris package keystore diff --git a/accounts/keystore/watch_fallback.go b/accounts/keystore/watch_fallback.go index e3c133b3f6..ee1b989e63 100644 --- a/accounts/keystore/watch_fallback.go +++ b/accounts/keystore/watch_fallback.go @@ -14,8 +14,8 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -//go:build (darwin && !cgo) || ios || (linux && arm64) || windows || (!darwin && !freebsd && !linux && !netbsd && !solaris) -// +build darwin,!cgo ios linux,arm64 windows !darwin,!freebsd,!linux,!netbsd,!solaris +//go:build (darwin && !cgo) || ios || windows || (!darwin && !freebsd && !linux && !netbsd && !solaris) +// +build darwin,!cgo ios windows !darwin,!freebsd,!linux,!netbsd,!solaris // This is the fallback implementation of directory watching. // It is used on unsupported platforms. diff --git a/accounts/scwallet/hub.go b/accounts/scwallet/hub.go index 1b1899dc8e..185815365e 100644 --- a/accounts/scwallet/hub.go +++ b/accounts/scwallet/hub.go @@ -113,7 +113,7 @@ func (hub *Hub) readPairings() error { } func (hub *Hub) writePairings() error { - pairingFile, err := os.OpenFile(filepath.Join(hub.datadir, "smartcards.json"), os.O_RDWR|os.O_CREATE, 0755) + pairingFile, err := os.OpenFile(filepath.Join(hub.datadir, "smartcards.json"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) if err != nil { return err } @@ -129,11 +129,8 @@ func (hub *Hub) writePairings() error { return err } - if _, err := pairingFile.Write(pairingData); err != nil { - return err - } - - return nil + _, err = pairingFile.Write(pairingData) + return err } func (hub *Hub) pairing(wallet *Wallet) *smartcardPairing { diff --git a/cmd/evm/internal/t8ntool/execution.go b/cmd/evm/internal/t8ntool/execution.go index f17829ec53..253ebe1111 100644 --- a/cmd/evm/internal/t8ntool/execution.go +++ b/cmd/evm/internal/t8ntool/execution.go @@ -17,9 +17,11 @@ package t8ntool import ( + "encoding/json" "fmt" stdmath "math" "math/big" + "os" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" @@ -47,6 +49,9 @@ type Prestate struct { Env stEnv `json:"env"` Pre types.GenesisAlloc `json:"pre"` TreeLeaves map[common.Hash]hexutil.Bytes `json:"vkt,omitempty"` + // AllocPath, when non-empty, causes Apply to stream the alloc from disk + // instead of reading Pre, so the full map never materializes in memory. + AllocPath string `json:"-"` } //go:generate go run github.com/fjl/gencodec -type ExecutionResult -field-override executionResultMarshaling -out gen_execresult.go @@ -146,8 +151,19 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig, return h } var ( - isEIP4762 = chainConfig.IsUBT(big.NewInt(int64(pre.Env.Number)), pre.Env.Timestamp) - statedb = MakePreState(rawdb.NewMemoryDatabase(), pre.Pre, isEIP4762) + isEIP4762 = chainConfig.IsUBT(big.NewInt(int64(pre.Env.Number)), pre.Env.Timestamp) + statedb *state.StateDB + ) + if pre.AllocPath != "" { + var err error + statedb, err = MakePreStateStreaming(rawdb.NewMemoryDatabase(), pre.AllocPath, isEIP4762) + if err != nil { + return nil, nil, nil, err + } + } else { + statedb = MakePreState(rawdb.NewMemoryDatabase(), pre.Pre, isEIP4762) + } + var ( signer = types.MakeSigner(chainConfig, new(big.Int).SetUint64(pre.Env.Number), pre.Env.Timestamp) gaspool = core.NewGasPool(pre.Env.GasLimit) blockHash = common.Hash{0x13, 0x37} @@ -414,6 +430,76 @@ func MakePreState(db ethdb.Database, accounts types.GenesisAlloc, isBintrie bool return statedb } +// MakePreStateStreaming is like MakePreState, but decodes the alloc from disk +// one account at a time so the full map is never held in memory. +func MakePreStateStreaming(db ethdb.Database, allocPath string, isBintrie bool) (*state.StateDB, error) { + tdb := triedb.NewDatabase(db, &triedb.Config{Preimages: true, IsUBT: isBintrie}) + sdb := state.NewDatabase(tdb, nil) + + root := types.EmptyRootHash + if isBintrie { + root = types.EmptyBinaryHash + } + statedb, err := state.New(root, sdb) + if err != nil { + return nil, NewError(ErrorEVM, fmt.Errorf("failed to create initial statedb: %v", err)) + } + + f, err := os.Open(allocPath) + if err != nil { + return nil, NewError(ErrorIO, fmt.Errorf("failed reading alloc file: %v", err)) + } + defer f.Close() + + dec := json.NewDecoder(f) + tok, err := dec.Token() + if err != nil { + return nil, NewError(ErrorJson, fmt.Errorf("failed reading alloc opening token: %v", err)) + } + if d, ok := tok.(json.Delim); !ok || d != '{' { + return nil, NewError(ErrorJson, fmt.Errorf("expected alloc object, got %v", tok)) + } + for dec.More() { + keyTok, err := dec.Token() + if err != nil { + return nil, NewError(ErrorJson, fmt.Errorf("failed reading alloc key: %v", err)) + } + keyStr, ok := keyTok.(string) + if !ok { + return nil, NewError(ErrorJson, fmt.Errorf("alloc key not a string: %v", keyTok)) + } + addr := common.HexToAddress(keyStr) + var acct types.Account + if err := dec.Decode(&acct); err != nil { + return nil, NewError(ErrorJson, fmt.Errorf("failed decoding account %s: %v", keyStr, err)) + } + statedb.SetCode(addr, acct.Code, tracing.CodeChangeUnspecified) + statedb.SetNonce(addr, acct.Nonce, tracing.NonceChangeGenesis) + if acct.Balance != nil { + statedb.SetBalance(addr, uint256.MustFromBig(acct.Balance), tracing.BalanceIncreaseGenesisBalance) + } + for k, v := range acct.Storage { + statedb.SetState(addr, k, v) + } + } + if _, err := dec.Token(); err != nil { + return nil, NewError(ErrorJson, fmt.Errorf("failed reading alloc closing token: %v", err)) + } + + root, err = statedb.Commit(0, false, false) + if err != nil { + return nil, NewError(ErrorEVM, fmt.Errorf("failed to commit initial state: %v", err)) + } + if isBintrie { + return statedb, nil + } + statedb, err = state.New(root, sdb) + if err != nil { + return nil, NewError(ErrorEVM, fmt.Errorf("failed to reopen state after commit: %v", err)) + } + return statedb, nil +} + func rlpHash(x any) (h common.Hash) { hw := keccak.NewLegacyKeccak256() rlp.Encode(hw, x) diff --git a/cmd/evm/internal/t8ntool/transaction.go b/cmd/evm/internal/t8ntool/transaction.go index ad89876601..7e068c06af 100644 --- a/cmd/evm/internal/t8ntool/transaction.go +++ b/cmd/evm/internal/t8ntool/transaction.go @@ -185,7 +185,9 @@ func Transaction(ctx *cli.Context) error { } } - if chainConfig.IsOsaka(new(big.Int), 0) && tx.Gas() > params.MaxTxGas { + isOsaka := chainConfig.IsOsaka(new(big.Int), 0) + isAmsterdam := chainConfig.IsAmsterdam(new(big.Int), 0) + if isOsaka && !isAmsterdam && tx.Gas() > params.MaxTxGas { r.Error = errors.New("gas limit exceeds maximum") } results = append(results, r) diff --git a/cmd/evm/internal/t8ntool/transition.go b/cmd/evm/internal/t8ntool/transition.go index 6a23e9dc70..e0bb3a449d 100644 --- a/cmd/evm/internal/t8ntool/transition.go +++ b/cmd/evm/internal/t8ntool/transition.go @@ -17,6 +17,7 @@ package t8ntool import ( + "bufio" "encoding/json" "errors" "fmt" @@ -115,11 +116,10 @@ func Transition(ctx *cli.Context) error { } } if allocStr != stdinSelector { - if err := readFile(allocStr, "alloc", &inputData.Alloc); err != nil { - return err - } + prestate.AllocPath = allocStr + } else { + prestate.Pre = inputData.Alloc } - prestate.Pre = inputData.Alloc if btStr != stdinSelector && btStr != "" { if err := readFile(btStr, "BT", &inputData.BT); err != nil { @@ -223,22 +223,57 @@ func Transition(ctx *cli.Context) error { return err } } - // Dump the execution result + // Dump the execution result. var ( - collector = make(Alloc) + collector Alloc btleaves map[common.Hash]hexutil.Bytes ) isBinary := chainConfig.IsUBT(big.NewInt(int64(prestate.Env.Number)), prestate.Env.Timestamp) - if !isBinary { + allocOutput := ctx.String(OutputAllocFlag.Name) + switch { + case !isBinary && allocOutput != "" && allocOutput != "stdout" && allocOutput != "stderr": + // Stream directly to the output file to avoid materializing the + // whole post-state in memory. dispatchOutput is told to skip alloc + // by clearing the output name. + if err := writeStreamedAlloc(filepath.Join(baseDir, allocOutput), s); err != nil { + return err + } + allocOutput = "" + case !isBinary: + collector = make(Alloc) s.DumpToCollector(collector, nil) - } else { + default: btleaves = make(map[common.Hash]hexutil.Bytes) if err := s.DumpBinTrieLeaves(btleaves); err != nil { return err } } + return dispatchOutput(ctx, baseDir, result, collector, allocOutput, body, btleaves) +} - return dispatchOutput(ctx, baseDir, result, collector, body, btleaves) +// writeStreamedAlloc writes the post-state alloc to path one account at a +// time, producing the same JSON shape as saveFile on an Alloc map. +func writeStreamedAlloc(path string, s *state.StateDB) error { + f, err := os.Create(path) + if err != nil { + return NewError(ErrorIO, fmt.Errorf("failed creating alloc output file: %v", err)) + } + bw := bufio.NewWriter(f) + sa := newStreamingAlloc(bw) + s.DumpToCollector(sa, nil) + if err := sa.Close(); err != nil { + f.Close() + return NewError(ErrorIO, fmt.Errorf("failed writing alloc output: %v", err)) + } + if err := bw.Flush(); err != nil { + f.Close() + return NewError(ErrorIO, fmt.Errorf("failed flushing alloc output: %v", err)) + } + if err := f.Close(); err != nil { + return NewError(ErrorIO, fmt.Errorf("failed closing alloc output file: %v", err)) + } + log.Info("Wrote file", "file", path) + return nil } func applyLondonChecks(env *stEnv, chainConfig *params.ChainConfig) error { @@ -327,6 +362,10 @@ func (g Alloc) OnAccount(addr *common.Address, dumpAccount state.DumpAccount) { if addr == nil { return } + g[*addr] = dumpAccountToTypesAccount(dumpAccount) +} + +func dumpAccountToTypesAccount(dumpAccount state.DumpAccount) types.Account { balance, _ := new(big.Int).SetString(dumpAccount.Balance, 0) var storage map[common.Hash]common.Hash if dumpAccount.Storage != nil { @@ -335,13 +374,64 @@ func (g Alloc) OnAccount(addr *common.Address, dumpAccount state.DumpAccount) { storage[k] = common.HexToHash(v) } } - genesisAccount := types.Account{ + return types.Account{ Code: dumpAccount.Code, Storage: storage, Balance: balance, Nonce: dumpAccount.Nonce, } - g[*addr] = genesisAccount +} + +// streamingAlloc is a DumpCollector that writes each account to w as it is +// visited, emitting a single JSON object keyed by address. Close must be +// called to emit the closing brace. +type streamingAlloc struct { + w io.Writer + wroteOne bool + err error +} + +func newStreamingAlloc(w io.Writer) *streamingAlloc { + return &streamingAlloc{w: w} +} + +func (s *streamingAlloc) write(b []byte) { + if s.err != nil { + return + } + _, s.err = s.w.Write(b) +} + +func (s *streamingAlloc) OnRoot(common.Hash) { + s.write([]byte{'{'}) +} + +func (s *streamingAlloc) OnAccount(addr *common.Address, dumpAccount state.DumpAccount) { + if s.err != nil || addr == nil { + return + } + keyJSON, err := json.Marshal(*addr) + if err != nil { + s.err = err + return + } + valueJSON, err := json.Marshal(dumpAccountToTypesAccount(dumpAccount)) + if err != nil { + s.err = err + return + } + if s.wroteOne { + s.write([]byte{','}) + } + s.write(keyJSON) + s.write([]byte{':'}) + s.write(valueJSON) + s.wroteOne = true +} + +func (s *streamingAlloc) Close() error { + s.write([]byte{'}'}) + return s.err } // saveFile marshals the object to the given file @@ -359,8 +449,9 @@ func saveFile(baseDir, filename string, data interface{}) error { } // dispatchOutput writes the output data to either stderr or stdout, or to the specified -// files -func dispatchOutput(ctx *cli.Context, baseDir string, result *ExecutionResult, alloc Alloc, body hexutil.Bytes, bt map[common.Hash]hexutil.Bytes) error { +// files. An empty allocOutput skips the alloc dispatch, which is used when the +// alloc has already been streamed to disk by the caller. +func dispatchOutput(ctx *cli.Context, baseDir string, result *ExecutionResult, alloc Alloc, allocOutput string, body hexutil.Bytes, bt map[common.Hash]hexutil.Bytes) error { stdOutObject := make(map[string]interface{}) stdErrObject := make(map[string]interface{}) dispatch := func(baseDir, fName, name string, obj interface{}) error { @@ -378,7 +469,7 @@ func dispatchOutput(ctx *cli.Context, baseDir string, result *ExecutionResult, a } return nil } - if err := dispatch(baseDir, ctx.String(OutputAllocFlag.Name), "alloc", alloc); err != nil { + if err := dispatch(baseDir, allocOutput, "alloc", alloc); err != nil { return err } if err := dispatch(baseDir, ctx.String(OutputResultFlag.Name), "result", result); err != nil { diff --git a/core/state/reader_eip_7928.go b/core/state/reader_eip_7928.go new file mode 100644 index 0000000000..ff315ac5eb --- /dev/null +++ b/core/state/reader_eip_7928.go @@ -0,0 +1,247 @@ +// 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 ( + "sync" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" +) + +// The EIP27928 reader utilizes a hierarchical architecture to optimize state +// access during block execution: +// +// - Base layer: The reader is initialized with the pre-transition state root, +// providing the access of the state. +// +// - Prefetching Layer: This base reader is wrapped by newPrefetchStateReader. +// Using an Access List hint, it asynchronously fetches required state data +// in the background, minimizing I/O blocking during transaction processing. +// +// - Execution Layer: To support parallel transaction execution within the EIP +// 7928 context, readers are wrapped in ReaderWithBlockLevelAccessList. +// This layer provides a "unified view" by merging the pre-transition state +// with mutated states from preceding transactions in the block. +// +// - Tracking Layer: Finally, the readerTracker wraps the execution reader to +// capture all state reads made during a specific transaction. These individual +// reads are subsequently merged to construct a comprehensive access list +// for the entire block. +// +// The architecture can be illustrated by the diagram below: +// +// ┌──────────────┴──────────────┐ ┌──────────────┴──────────────┐ +// │ ReaderWithBlockLevelAL │ │ ReaderWithBlockLevelAL │ +// │ (Pre-state + Mutations) │ │ (Pre-state + Mutations) │ +// └──────────────┬──────────────┘ └──────────────┬──────────────┘ +// │ │ +// └────────────────┬─────────────────┘ +// │ +// ┌──────────────┴──────────────┐ +// │ newPrefetchStateReader │ (Async I/O) +// │ (Access List Hint driven) │ +// └──────────────┬──────────────┘ +// │ +// ┌──────────────┴──────────────┐ +// │ Base Reader │ (State Root) +// │ (State & Contract Code) │ +// └─────────────────────────────┘ + +// Note: The block producer, which is responsible for generating the block +// along with the block-level access list, does not maintain the internal +// hierarchy (e.g., PrefetchStateReader or ReaderWithBlockLevelAL). +// Instead, it directly utilizes the readerTracker, wrapped around the +// base reader, to construct the access list. + +type fetchTask struct { + addr common.Address + slots []common.Hash +} + +func (t *fetchTask) weight() int { return 1 + len(t.slots) } + +type prefetchStateReader struct { + StateReader + + tasks []*fetchTask + nThreads int + done chan struct{} + term chan struct{} + closeOnce sync.Once +} + +// nolint:unused +func newPrefetchStateReader(reader StateReader, accessList map[common.Address][]common.Hash, nThreads int) *prefetchStateReader { + tasks := make([]*fetchTask, 0, len(accessList)) + for addr, slots := range accessList { + tasks = append(tasks, &fetchTask{ + addr: addr, + slots: slots, + }) + } + return newPrefetchStateReaderInternal(reader, tasks, nThreads) +} + +func newPrefetchStateReaderInternal(reader StateReader, tasks []*fetchTask, nThreads int) *prefetchStateReader { + r := &prefetchStateReader{ + StateReader: reader, + tasks: tasks, + nThreads: nThreads, + done: make(chan struct{}), + term: make(chan struct{}), + } + go r.prefetch() + return r +} + +func (r *prefetchStateReader) Close() { + r.closeOnce.Do(func() { + close(r.term) + <-r.done + }) +} + +func (r *prefetchStateReader) Wait() error { + select { + case <-r.term: + return nil + case <-r.done: + return nil + } +} + +func (r *prefetchStateReader) prefetch() { + defer close(r.done) + + if len(r.tasks) == 0 { + return + } + var total int + for _, t := range r.tasks { + total += t.weight() + } + var ( + wg sync.WaitGroup + unit = (total + r.nThreads - 1) / r.nThreads // round-up the per worker unit + ) + for i := 0; i < r.nThreads; i++ { + start := i * unit + if start >= total { + break + } + limit := (i + 1) * unit + if i == r.nThreads-1 { + limit = total + } + // Schedule the worker for prefetching, the items on the range [start, limit) + // is exclusively assigned for this worker. + wg.Add(1) + go func(workerID, startW, endW int) { + r.process(startW, endW) + wg.Done() + }(i, start, limit) + } + wg.Wait() +} + +func (r *prefetchStateReader) process(start, limit int) { + var total = 0 + for _, t := range r.tasks { + tw := t.weight() + if total+tw > start { + s := 0 + if start > total { + s = start - total + } + l := tw + if limit < total+tw { + l = limit - total + } + for j := s; j < l; j++ { + select { + case <-r.term: + return + default: + if j == 0 { + r.StateReader.Account(t.addr) + } else { + r.StateReader.Storage(t.addr, t.slots[j-1]) + } + } + } + } + total += tw + if total >= limit { + return + } + } +} + +// ReaderWithBlockLevelAccessList provides state access that reflects the +// pre-transition state combined with the mutations made by transactions +// prior to TxIndex. +type ReaderWithBlockLevelAccessList struct { + Reader + AccessList *bal.ConstructionBlockAccessList + TxIndex int +} + +// NewReaderWithBlockLevelAccessList constructs a reader for accessing states +// with the mutations made by transactions prior to txIndex. +// +// The txIndex refers to the call frame as such: +// - 0 for pre‑execution system contract calls. +// - 1 … n for transactions (in block order). +// - n + 1 for post‑execution system contract calls. +func NewReaderWithBlockLevelAccessList(base Reader, accessList *bal.ConstructionBlockAccessList, txIndex int) *ReaderWithBlockLevelAccessList { + return &ReaderWithBlockLevelAccessList{ + Reader: base, + AccessList: accessList, + TxIndex: txIndex, + } +} + +// Account implements Reader, returning the account with the specific address. +func (r *ReaderWithBlockLevelAccessList) Account(addr common.Address) (*types.StateAccount, error) { + panic("implement me") +} + +// Storage implements Reader, returning the storage slot with the specific +// address and slot key. +func (r *ReaderWithBlockLevelAccessList) Storage(addr common.Address, slot common.Hash) (common.Hash, error) { + panic("implement me") +} + +// Has implements Reader, returning the flag indicating whether the contract +// code with specified address and hash exists or not. +func (r *ReaderWithBlockLevelAccessList) Has(addr common.Address, codeHash common.Hash) bool { + panic("implement me") +} + +// Code implements Reader, returning the contract code with specified address +// and hash. +func (r *ReaderWithBlockLevelAccessList) Code(addr common.Address, codeHash common.Hash) ([]byte, error) { + panic("implement me") +} + +// CodeSize implements Reader, returning the contract code size with specified +// address and hash. +func (r *ReaderWithBlockLevelAccessList) CodeSize(addr common.Address, codeHash common.Hash) (int, error) { + panic("implement me") +} diff --git a/core/state/reader_eip_7928_test.go b/core/state/reader_eip_7928_test.go new file mode 100644 index 0000000000..b2d432258c --- /dev/null +++ b/core/state/reader_eip_7928_test.go @@ -0,0 +1,145 @@ +// 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 ( + "fmt" + "math/rand" + "sync" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/internal/testrand" +) + +type countingStateReader struct { + accounts map[common.Address]int + storages map[common.Address]map[common.Hash]int + lock sync.Mutex +} + +func newRefStateReader() *countingStateReader { + return &countingStateReader{ + accounts: make(map[common.Address]int), + storages: make(map[common.Address]map[common.Hash]int), + } +} + +func (r *countingStateReader) validate(total int) error { + var sum int + for addr, n := range r.accounts { + if n != 1 { + return fmt.Errorf("duplicated account access: %x-%d", addr, n) + } + sum += 1 + + slots, exists := r.storages[addr] + if !exists { + continue + } + for key, n := range slots { + if n != 1 { + return fmt.Errorf("duplicated storage access: %x-%x-%d", addr, key, n) + } + sum += 1 + } + } + for addr := range r.storages { + _, exists := r.accounts[addr] + if !exists { + return fmt.Errorf("dangling storage access: %x", addr) + } + } + if sum != total { + return fmt.Errorf("unexpected number of access, want: %d, got: %d", total, sum) + } + return nil +} + +func (r *countingStateReader) Account(addr common.Address) (*types.StateAccount, error) { + r.lock.Lock() + defer r.lock.Unlock() + + r.accounts[addr] += 1 + return nil, nil +} +func (r *countingStateReader) Storage(addr common.Address, slot common.Hash) (common.Hash, error) { + r.lock.Lock() + defer r.lock.Unlock() + + slots, exists := r.storages[addr] + if !exists { + slots = make(map[common.Hash]int) + r.storages[addr] = slots + } + slots[slot] += 1 + return common.Hash{}, nil +} + +func makeFetchTasks(n int) ([]*fetchTask, int) { + var ( + total int + tasks []*fetchTask + ) + for i := 0; i < n; i++ { + var slots []common.Hash + if rand.Intn(3) != 0 { + for j := 0; j < rand.Intn(100); j++ { + slots = append(slots, testrand.Hash()) + } + } + tasks = append(tasks, &fetchTask{ + addr: testrand.Address(), + slots: slots, + }) + total += len(slots) + 1 + } + return tasks, total +} + +func TestPrefetchReader(t *testing.T) { + type suite struct { + tasks []*fetchTask + threads int + total int + } + var suites []suite + for i := 0; i < 100; i++ { + tasks, total := makeFetchTasks(100) + suites = append(suites, suite{ + tasks: tasks, + threads: rand.Intn(30) + 1, + total: total, + }) + } + // num(tasks) < num(threads) + tasks, total := makeFetchTasks(1) + suites = append(suites, suite{ + tasks: tasks, + threads: 100, + total: total, + }) + for _, s := range suites { + r := newRefStateReader() + pr := newPrefetchStateReaderInternal(r, s.tasks, s.threads) + pr.Wait() + if err := r.validate(s.total); err != nil { + t.Fatal(err) + } + } +} diff --git a/core/state/state_object.go b/core/state/state_object.go index 264dfd920d..8e72486825 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -162,8 +162,11 @@ func (s *stateObject) getPrefetchedTrie() Trie { // GetState retrieves a value associated with the given storage key. func (s *stateObject) GetState(key common.Hash) common.Hash { - value, _ := s.getState(key) - return value + value, dirty := s.dirtyStorage[key] + if dirty { + return value + } + return s.GetCommittedState(key) } // getState retrieves a value associated with the given storage key, along with diff --git a/core/state_prefetcher.go b/core/state_prefetcher.go index c91d40d94f..ed292d0beb 100644 --- a/core/state_prefetcher.go +++ b/core/state_prefetcher.go @@ -93,6 +93,7 @@ func (p *statePrefetcher) Prefetch(block *types.Block, statedb *state.StateDB, c } // Execute the message to preload the implicit touched states evm := vm.NewEVM(NewEVMBlockContext(header, p.chain, nil), stateCpy, p.config, cfg) + defer evm.Release() // Convert the transaction into an executable message and pre-cache its sender msg, err := TransactionToMessage(tx, signer, header.BaseFee) diff --git a/core/state_processor.go b/core/state_processor.go index fda3bf8fe7..54ebbd047b 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -88,6 +88,7 @@ func (p *StateProcessor) Process(ctx context.Context, block *types.Block, stated // Apply pre-execution system calls. context = NewEVMBlockContext(header, p.chain, nil) evm := vm.NewEVM(context, tracingStateDB, config, cfg) + defer evm.Release() if beaconRoot := block.BeaconRoot(); beaconRoot != nil { ProcessBeaconBlockRoot(*beaconRoot, evm) diff --git a/core/state_transition.go b/core/state_transition.go index c3ebffd060..c7b0593857 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -344,9 +344,10 @@ func (st *stateTransition) preCheck() error { } } 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 { + if isOsaka && !isAmsterdam && msg.GasLimit > params.MaxTxGas { return fmt.Errorf("%w (cap: %d, tx: %d)", ErrGasLimitTooHigh, params.MaxTxGas, msg.GasLimit) } // Make sure the sender is an EOA diff --git a/core/txpool/legacypool/legacypool.go b/core/txpool/legacypool/legacypool.go index 78a0161c41..00630de04c 100644 --- a/core/txpool/legacypool/legacypool.go +++ b/core/txpool/legacypool/legacypool.go @@ -1222,8 +1222,10 @@ func (pool *LegacyPool) runReorg(done chan struct{}, reset *txpoolResetRequest, pool.mu.Lock() if reset != nil { if reset.newHead != nil && reset.oldHead != nil { - // Discard the transactions with the gas limit higher than the cap. - if pool.chainconfig.IsOsaka(reset.newHead.Number, reset.newHead.Time) && !pool.chainconfig.IsOsaka(reset.oldHead.Number, reset.oldHead.Time) { + // Discard the transactions with the gas limit higher than the cap at the + // Osaka fork boundary. + if pool.chainconfig.IsOsaka(reset.newHead.Number, reset.newHead.Time) && + !pool.chainconfig.IsOsaka(reset.oldHead.Number, reset.oldHead.Time) { var hashes []common.Hash pool.all.Range(func(hash common.Hash, tx *types.Transaction) bool { if tx.Gas() > params.MaxTxGas { diff --git a/core/txpool/validation.go b/core/txpool/validation.go index 85bf65ac40..6891dc94d2 100644 --- a/core/txpool/validation.go +++ b/core/txpool/validation.go @@ -92,7 +92,7 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types return err } } - if rules.IsOsaka && tx.Gas() > params.MaxTxGas { + if rules.IsOsaka && !rules.IsAmsterdam && tx.Gas() > params.MaxTxGas { return fmt.Errorf("%w (cap: %d, tx: %d)", core.ErrGasLimitTooHigh, params.MaxTxGas, tx.Gas()) } // Transactions can't be negative. This may never happen using RLP decoded diff --git a/core/vm/eips.go b/core/vm/eips.go index 54e5cb0c60..33af8fd4fd 100644 --- a/core/vm/eips.go +++ b/core/vm/eips.go @@ -24,7 +24,6 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/params" - "github.com/holiman/uint256" ) var activators = map[int]func(*JumpTable){ @@ -92,8 +91,7 @@ func enable1884(jt *JumpTable) { } func opSelfBalance(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { - balance := evm.StateDB.GetBalance(scope.Contract.Address()) - scope.Stack.push(balance) + scope.Stack.get().Set(evm.StateDB.GetBalance(scope.Contract.Address())) return nil, nil } @@ -111,8 +109,7 @@ func enable1344(jt *JumpTable) { // opChainID implements CHAINID opcode func opChainID(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { - chainId, _ := uint256.FromBig(evm.chainConfig.ChainID) - scope.Stack.push(chainId) + scope.Stack.get().SetFromBig(evm.chainConfig.ChainID) return nil, nil } @@ -222,8 +219,7 @@ func opTstore(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { // opBaseFee implements BASEFEE opcode func opBaseFee(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { - baseFee, _ := uint256.FromBig(evm.Context.BaseFee) - scope.Stack.push(baseFee) + scope.Stack.get().SetFromBig(evm.Context.BaseFee) return nil, nil } @@ -240,7 +236,7 @@ func enable3855(jt *JumpTable) { // opPush0 implements the PUSH0 opcode func opPush0(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { - scope.Stack.push(new(uint256.Int)) + scope.Stack.get().Clear() return nil, nil } @@ -291,8 +287,7 @@ func opBlobHash(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { // opBlobBaseFee implements BLOBBASEFEE opcode func opBlobBaseFee(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { - blobBaseFee, _ := uint256.FromBig(evm.Context.BlobBaseFee) - scope.Stack.push(blobBaseFee) + scope.Stack.get().SetFromBig(evm.Context.BlobBaseFee) return nil, nil } @@ -397,11 +392,11 @@ func opExtCodeCopyEIP4762(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, er func opPush1EIP4762(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { var ( codeLen = uint64(len(scope.Contract.Code)) - integer = new(uint256.Int) + elem = scope.Stack.get() ) *pc += 1 if *pc < codeLen { - scope.Stack.push(integer.SetUint64(uint64(scope.Contract.Code[*pc]))) + elem.SetUint64(uint64(scope.Contract.Code[*pc])) if !scope.Contract.IsDeployment && !scope.Contract.IsSystemCall && *pc%31 == 0 { // touch next chunk if PUSH1 is at the boundary. if so, *pc has @@ -414,7 +409,7 @@ func opPush1EIP4762(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { } } } else { - scope.Stack.push(integer.Clear()) + elem.Clear() } return nil, nil } @@ -426,12 +421,11 @@ func makePushEIP4762(size uint64, pushByteSize int) executionFunc { start = min(codeLen, int(*pc+1)) end = min(codeLen, start+pushByteSize) ) - scope.Stack.push(new(uint256.Int).SetBytes( + scope.Stack.get().SetBytes( common.RightPadBytes( scope.Contract.Code[start:end], pushByteSize, - )), - ) + )) if !scope.Contract.IsDeployment && !scope.Contract.IsSystemCall { contractAddr := scope.Contract.Address() @@ -583,7 +577,7 @@ func enable7702(jt *JumpTable) { // opSlotNum enables the SLOTNUM opcode func opSlotNum(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { - scope.Stack.push(uint256.NewInt(evm.Context.SlotNum)) + scope.Stack.get().SetUint64(evm.Context.SlotNum) return nil, nil } diff --git a/core/vm/evm.go b/core/vm/evm.go index 579c81ed0a..c0a649cf1d 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -127,6 +127,8 @@ type EVM struct { readOnly bool // Whether to throw on stateful modifications returnData []byte // Last CALL's return data for subsequent reuse + + arena *stackArena } // NewEVM constructs an EVM instance with the supplied block context, state @@ -141,6 +143,7 @@ func NewEVM(blockCtx BlockContext, statedb StateDB, chainConfig *params.ChainCon chainConfig: chainConfig, chainRules: chainConfig.Rules(blockCtx.BlockNumber, blockCtx.Random != nil, blockCtx.Time), jumpDests: newGlobalJumpDests(), + arena: newArena(), } evm.precompiles = activePrecompiledContracts(evm.chainRules) @@ -223,6 +226,12 @@ func (evm *EVM) Cancel() { evm.abort.Store(true) } +// Release returns some memory allocated by the EVM, should be called after the EVM was used +// for the last time. Not necessary, but an improvement. +func (evm *EVM) Release() { + returnStack(evm.arena) +} + // Cancelled returns true if Cancel has been called func (evm *EVM) Cancelled() bool { return evm.abort.Load() diff --git a/core/vm/gas_table.go b/core/vm/gas_table.go index b3259b2ec7..046311f9cc 100644 --- a/core/vm/gas_table.go +++ b/core/vm/gas_table.go @@ -71,7 +71,7 @@ func memoryCopierGas(stackpos int) gasFunc { return GasCosts{}, err } // And gas for copying data, charged per word at param.CopyGas - words, overflow := stack.Back(stackpos).Uint64WithOverflow() + words, overflow := stack.back(stackpos).Uint64WithOverflow() if overflow { return GasCosts{}, ErrGasUintOverflow } @@ -100,7 +100,7 @@ func gasSStore(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySi return GasCosts{}, ErrWriteProtection } var ( - y, x = stack.Back(1), stack.Back(0) + y, x = stack.back(1), stack.back(0) current, original = evm.StateDB.GetStateAndCommittedState(contract.Address(), x.Bytes32()) ) // The legacy gas metering only takes into consideration the current state @@ -192,7 +192,7 @@ func gasSStoreEIP2200(evm *EVM, contract *Contract, stack *Stack, mem *Memory, m } // Gas sentry honoured, do the actual gas calculation based on the stored value var ( - y, x = stack.Back(1), stack.Back(0) + y, x = stack.back(1), stack.back(0) current, original = evm.StateDB.GetStateAndCommittedState(contract.Address(), x.Bytes32()) ) value := common.Hash(y.Bytes32()) @@ -228,7 +228,7 @@ func gasSStoreEIP2200(evm *EVM, contract *Contract, stack *Stack, mem *Memory, m func makeGasLog(n uint64) gasFunc { return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { - requestedSize, overflow := stack.Back(1).Uint64WithOverflow() + requestedSize, overflow := stack.back(1).Uint64WithOverflow() if overflow { return GasCosts{}, ErrGasUintOverflow } @@ -261,7 +261,7 @@ func gasKeccak256(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memor if err != nil { return GasCosts{}, err } - wordGas, overflow := stack.Back(1).Uint64WithOverflow() + wordGas, overflow := stack.back(1).Uint64WithOverflow() if overflow { return GasCosts{}, ErrGasUintOverflow } @@ -299,7 +299,7 @@ func gasCreate2(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memoryS if err != nil { return GasCosts{}, err } - wordGas, overflow := stack.Back(2).Uint64WithOverflow() + wordGas, overflow := stack.back(2).Uint64WithOverflow() if overflow { return GasCosts{}, ErrGasUintOverflow } @@ -317,7 +317,7 @@ func gasCreateEip3860(evm *EVM, contract *Contract, stack *Stack, mem *Memory, m if err != nil { return GasCosts{}, err } - size, overflow := stack.Back(2).Uint64WithOverflow() + size, overflow := stack.back(2).Uint64WithOverflow() if overflow { return GasCosts{}, ErrGasUintOverflow } @@ -336,7 +336,7 @@ func gasCreate2Eip3860(evm *EVM, contract *Contract, stack *Stack, mem *Memory, if err != nil { return GasCosts{}, err } - size, overflow := stack.Back(2).Uint64WithOverflow() + size, overflow := stack.back(2).Uint64WithOverflow() if overflow { return GasCosts{}, ErrGasUintOverflow } @@ -352,7 +352,7 @@ func gasCreate2Eip3860(evm *EVM, contract *Contract, stack *Stack, mem *Memory, } func gasExpFrontier(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { - expByteLen := uint64((stack.data[stack.len()-2].BitLen() + 7) / 8) + expByteLen := uint64((stack.back(1).BitLen() + 7) / 8) var ( gas = expByteLen * params.ExpByteFrontier // no overflow check required. Max is 256 * ExpByte gas @@ -365,7 +365,7 @@ func gasExpFrontier(evm *EVM, contract *Contract, stack *Stack, mem *Memory, mem } func gasExpEIP158(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { - expByteLen := uint64((stack.data[stack.len()-2].BitLen() + 7) / 8) + expByteLen := uint64((stack.back(1).BitLen() + 7) / 8) var ( gas = expByteLen * params.ExpByteEIP158 // no overflow check required. Max is 256 * ExpByte gas @@ -390,7 +390,7 @@ func makeCallVariantGasCost(intrinsicFunc gasFunc) gasFunc { if err != nil { return GasCosts{}, err } - evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas.RegularGas, intrinsic.RegularGas, stack.Back(0)) + evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas.RegularGas, intrinsic.RegularGas, stack.back(0)) if err != nil { return GasCosts{}, err } @@ -405,8 +405,8 @@ func makeCallVariantGasCost(intrinsicFunc gasFunc) gasFunc { func gasCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { var ( gas uint64 - transfersValue = !stack.Back(2).IsZero() - address = common.Address(stack.Back(1).Bytes20()) + transfersValue = !stack.back(2).IsZero() + address = common.Address(stack.back(1).Bytes20()) ) if evm.readOnly && transfersValue { return GasCosts{}, ErrWriteProtection @@ -453,7 +453,7 @@ func gasCallCodeIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memor gas uint64 overflow bool ) - if stack.Back(2).Sign() != 0 && !evm.chainRules.IsEIP4762 { + if stack.back(2).Sign() != 0 && !evm.chainRules.IsEIP4762 { gas += params.CallValueTransferGas } if gas, overflow = math.SafeAdd(gas, memoryGas); overflow { @@ -487,7 +487,7 @@ func gasSelfdestruct(evm *EVM, contract *Contract, stack *Stack, mem *Memory, me // EIP150 homestead gas reprice fork: if evm.chainRules.IsEIP150 { gas = params.SelfdestructGasEIP150 - var address = common.Address(stack.Back(0).Bytes20()) + var address = common.Address(stack.back(0).Bytes20()) if evm.chainRules.IsEIP158 { // if empty and transfers value diff --git a/core/vm/instructions.go b/core/vm/instructions.go index 3311af0d22..4b05092cc7 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -24,7 +24,6 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/params" - "github.com/holiman/uint256" ) func opAdd(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { @@ -244,7 +243,7 @@ func opKeccak256(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { } func opAddress(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { - scope.Stack.push(new(uint256.Int).SetBytes(scope.Contract.Address().Bytes())) + scope.Stack.get().SetBytes(scope.Contract.Address().Bytes()) return nil, nil } @@ -256,17 +255,17 @@ func opBalance(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { } func opOrigin(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { - scope.Stack.push(new(uint256.Int).SetBytes(evm.Origin.Bytes())) + scope.Stack.get().SetBytes(evm.Origin.Bytes()) return nil, nil } func opCaller(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { - scope.Stack.push(new(uint256.Int).SetBytes(scope.Contract.Caller().Bytes())) + scope.Stack.get().SetBytes(scope.Contract.Caller().Bytes()) return nil, nil } func opCallValue(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { - scope.Stack.push(scope.Contract.value) + scope.Stack.get().Set(scope.Contract.value) return nil, nil } @@ -282,7 +281,7 @@ func opCallDataLoad(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { } func opCallDataSize(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { - scope.Stack.push(new(uint256.Int).SetUint64(uint64(len(scope.Contract.Input)))) + scope.Stack.get().SetUint64(uint64(len(scope.Contract.Input))) return nil, nil } @@ -305,7 +304,7 @@ func opCallDataCopy(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { } func opReturnDataSize(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { - scope.Stack.push(new(uint256.Int).SetUint64(uint64(len(evm.returnData)))) + scope.Stack.get().SetUint64(uint64(len(evm.returnData))) return nil, nil } @@ -338,7 +337,7 @@ func opExtCodeSize(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { } func opCodeSize(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { - scope.Stack.push(new(uint256.Int).SetUint64(uint64(len(scope.Contract.Code)))) + scope.Stack.get().SetUint64(uint64(len(scope.Contract.Code))) return nil, nil } @@ -416,7 +415,7 @@ func opExtCodeHash(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { } func opGasprice(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { - scope.Stack.push(evm.GasPrice.Clone()) + scope.Stack.get().Set(evm.GasPrice) return nil, nil } @@ -451,35 +450,32 @@ func opBlockhash(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { } func opCoinbase(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { - scope.Stack.push(new(uint256.Int).SetBytes(evm.Context.Coinbase.Bytes())) + scope.Stack.get().SetBytes(evm.Context.Coinbase.Bytes()) return nil, nil } func opTimestamp(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { - scope.Stack.push(new(uint256.Int).SetUint64(evm.Context.Time)) + scope.Stack.get().SetUint64(evm.Context.Time) return nil, nil } func opNumber(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { - v, _ := uint256.FromBig(evm.Context.BlockNumber) - scope.Stack.push(v) + scope.Stack.get().SetFromBig(evm.Context.BlockNumber) return nil, nil } func opDifficulty(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { - v, _ := uint256.FromBig(evm.Context.Difficulty) - scope.Stack.push(v) + scope.Stack.get().SetFromBig(evm.Context.Difficulty) return nil, nil } func opRandom(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { - v := new(uint256.Int).SetBytes(evm.Context.Random.Bytes()) - scope.Stack.push(v) + scope.Stack.get().SetBytes(evm.Context.Random.Bytes()) return nil, nil } func opGasLimit(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { - scope.Stack.push(new(uint256.Int).SetUint64(evm.Context.GasLimit)) + scope.Stack.get().SetUint64(evm.Context.GasLimit) return nil, nil } @@ -556,17 +552,17 @@ func opJumpdest(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { } func opPc(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { - scope.Stack.push(new(uint256.Int).SetUint64(*pc)) + scope.Stack.get().SetUint64(*pc) return nil, nil } func opMsize(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { - scope.Stack.push(new(uint256.Int).SetUint64(uint64(scope.Memory.Len()))) + scope.Stack.get().SetUint64(uint64(scope.Memory.Len())) return nil, nil } func opGas(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { - scope.Stack.push(new(uint256.Int).SetUint64(scope.Contract.Gas.RegularGas)) + scope.Stack.get().SetUint64(scope.Contract.Gas.RegularGas) return nil, nil } @@ -1007,7 +1003,7 @@ func opDupN(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { } //The n‘th stack item is duplicated at the top of the stack. - scope.Stack.push(scope.Stack.Back(n - 1)) + scope.Stack.push(scope.Stack.back(n - 1)) *pc += 1 return nil, nil } @@ -1034,10 +1030,10 @@ func opSwapN(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { return nil, &ErrStackUnderflow{stackLen: scope.Stack.len(), required: n + 1} } - // The (n+1)‘th stack item is swapped with the top of the stack. - indexTop := scope.Stack.len() - 1 - indexN := scope.Stack.len() - 1 - n - scope.Stack.data[indexTop], scope.Stack.data[indexN] = scope.Stack.data[indexN], scope.Stack.data[indexTop] + // The (n+1)’th stack item is swapped with the top of the stack. + top := scope.Stack.peek() + nth := scope.Stack.back(n) + *top, *nth = *nth, *top *pc += 1 return nil, nil } @@ -1067,10 +1063,10 @@ func opExchange(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { return nil, &ErrStackUnderflow{stackLen: scope.Stack.len(), required: need} } - // The (n+1)‘th stack item is swapped with the (m+1)‘th stack item. - indexN := scope.Stack.len() - 1 - n - indexM := scope.Stack.len() - 1 - m - scope.Stack.data[indexN], scope.Stack.data[indexM] = scope.Stack.data[indexM], scope.Stack.data[indexN] + // The (n+1)’th stack item is swapped with the (m+1)’th stack item. + nth := scope.Stack.back(n) + mth := scope.Stack.back(m) + *nth, *mth = *mth, *nth *pc += 1 return nil, nil } @@ -1106,13 +1102,13 @@ func makeLog(size int) executionFunc { func opPush1(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { var ( codeLen = uint64(len(scope.Contract.Code)) - integer = new(uint256.Int) + elem = scope.Stack.get() ) *pc += 1 if *pc < codeLen { - scope.Stack.push(integer.SetUint64(uint64(scope.Contract.Code[*pc]))) + elem.SetUint64(uint64(scope.Contract.Code[*pc])) } else { - scope.Stack.push(integer.Clear()) + elem.Clear() } return nil, nil } @@ -1121,14 +1117,14 @@ func opPush1(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { func opPush2(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { var ( codeLen = uint64(len(scope.Contract.Code)) - integer = new(uint256.Int) + elem = scope.Stack.get() ) if *pc+2 < codeLen { - scope.Stack.push(integer.SetBytes2(scope.Contract.Code[*pc+1 : *pc+3])) + elem.SetBytes2(scope.Contract.Code[*pc+1 : *pc+3]) } else if *pc+1 < codeLen { - scope.Stack.push(integer.SetUint64(uint64(scope.Contract.Code[*pc+1]) << 8)) + elem.SetUint64(uint64(scope.Contract.Code[*pc+1]) << 8) } else { - scope.Stack.push(integer.Clear()) + elem.Clear() } *pc += 2 return nil, nil @@ -1142,13 +1138,13 @@ func makePush(size uint64, pushByteSize int) executionFunc { start = min(codeLen, int(*pc+1)) end = min(codeLen, start+pushByteSize) ) - a := new(uint256.Int).SetBytes(scope.Contract.Code[start:end]) + a := scope.Stack.get() + a.SetBytes(scope.Contract.Code[start:end]) // Missing bytes: pushByteSize - len(pushData) if missing := pushByteSize - (end - start); missing > 0 { a.Lsh(a, uint(8*missing)) } - scope.Stack.push(a) *pc += size return nil, nil } diff --git a/core/vm/instructions_test.go b/core/vm/instructions_test.go index dbe055f2ac..354d2ce5ab 100644 --- a/core/vm/instructions_test.go +++ b/core/vm/instructions_test.go @@ -25,6 +25,7 @@ import ( "os" "strings" "testing" + "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/math" @@ -98,7 +99,7 @@ func init() { func testTwoOperandOp(t *testing.T, tests []TwoOperandTestcase, opFn executionFunc, name string) { var ( evm = NewEVM(BlockContext{}, nil, params.TestChainConfig, Config{}) - stack = newstack() + stack = newStackForTesting() pc = uint64(0) ) @@ -109,8 +110,8 @@ func testTwoOperandOp(t *testing.T, tests []TwoOperandTestcase, opFn executionFu stack.push(x) stack.push(y) opFn(&pc, evm, &ScopeContext{nil, stack, nil}) - if len(stack.data) != 1 { - t.Errorf("Expected one item on stack after %v, got %d: ", name, len(stack.data)) + if stack.len() != 1 { + t.Errorf("Expected one item on stack after %v, got %d: ", name, stack.len()) } actual := stack.pop() @@ -196,7 +197,7 @@ func TestSAR(t *testing.T) { func TestAddMod(t *testing.T) { var ( evm = NewEVM(BlockContext{}, nil, params.TestChainConfig, Config{}) - stack = newstack() + stack = newStackForTesting() pc = uint64(0) ) tests := []struct { @@ -239,7 +240,7 @@ func TestWriteExpectedValues(t *testing.T) { getResult := func(args []*twoOperandParams, opFn executionFunc) []TwoOperandTestcase { var ( evm = NewEVM(BlockContext{}, nil, params.TestChainConfig, Config{}) - stack = newstack() + stack = newStackForTesting() pc = uint64(0) ) result := make([]TwoOperandTestcase, len(args)) @@ -282,23 +283,40 @@ func TestJsonTestcases(t *testing.T) { func opBenchmark(bench *testing.B, op executionFunc, args ...string) { var ( - evm = NewEVM(BlockContext{}, nil, params.TestChainConfig, Config{}) - stack = newstack() - scope = &ScopeContext{nil, stack, nil} + evm = NewEVM(BlockContext{}, nil, params.TestChainConfig, Config{}) + stack = newStackForTesting() + code = []byte{} + opPush32 = makePush(32, 32) ) // convert args intArgs := make([]*uint256.Int, len(args)) for i, arg := range args { + code = append(code, common.LeftPadBytes(common.Hex2Bytes(arg), 32)...) intArgs[i] = new(uint256.Int).SetBytes(common.Hex2Bytes(arg)) } pc := uint64(0) - for bench.Loop() { - for _, arg := range intArgs { - stack.push(arg) + scope := &ScopeContext{nil, stack, &Contract{Code: code}} + start := time.Now() + bench.ResetTimer() + for i := 0; i < bench.N; i++ { + for range len(args) { + opPush32(&pc, evm, scope) + pc += 32 } op(&pc, evm, scope) - stack.pop() + opPop(&pc, evm, scope) } + bench.StopTimer() + elapsed := uint64(time.Since(start)) + if elapsed < 1 { + elapsed = 1 + } + reqGas := uint64(len(args))*GasFastestStep + GasFastestStep + GasQuickStep + gasUsed := reqGas * uint64(bench.N) + bench.ReportMetric(float64(reqGas), "gas/op") + // Keep it as uint64, multiply 100 to get two digit float later + mgasps := (100 * 1000 * gasUsed) / elapsed + bench.ReportMetric(float64(mgasps)/100, "mgas/s") for i, arg := range args { want := new(uint256.Int).SetBytes(common.Hex2Bytes(arg)) @@ -519,7 +537,7 @@ func BenchmarkOpIsZero(b *testing.B) { func TestOpMstore(t *testing.T) { var ( evm = NewEVM(BlockContext{}, nil, params.TestChainConfig, Config{}) - stack = newstack() + stack = newStackForTesting() mem = NewMemory() ) mem.Resize(64) @@ -542,7 +560,7 @@ func TestOpMstore(t *testing.T) { func BenchmarkOpMstore(bench *testing.B) { var ( evm = NewEVM(BlockContext{}, nil, params.TestChainConfig, Config{}) - stack = newstack() + stack = newStackForTesting() mem = NewMemory() ) mem.Resize(64) @@ -561,7 +579,7 @@ func TestOpTstore(t *testing.T) { var ( statedb, _ = state.New(types.EmptyRootHash, state.NewDatabaseForTesting()) evm = NewEVM(BlockContext{}, statedb, params.TestChainConfig, Config{}) - stack = newstack() + stack = newStackForTesting() mem = NewMemory() caller = common.Address{} to = common.Address{1} @@ -600,7 +618,7 @@ func TestOpTstore(t *testing.T) { func BenchmarkOpKeccak256(bench *testing.B) { var ( evm = NewEVM(BlockContext{}, nil, params.TestChainConfig, Config{}) - stack = newstack() + stack = newStackForTesting() mem = NewMemory() ) mem.Resize(32) @@ -672,7 +690,7 @@ func TestCreate2Addresses(t *testing.T) { codeHash := crypto.Keccak256(code) address := crypto.CreateAddress2(origin, salt, codeHash) /* - stack := newstack() + stack := newStackForTesting() // salt, but we don't need that for this test stack.push(big.NewInt(int64(len(code)))) //size stack.push(big.NewInt(0)) // memstart @@ -701,12 +719,12 @@ func TestRandom(t *testing.T) { } { var ( evm = NewEVM(BlockContext{Random: &tt.random}, nil, params.TestChainConfig, Config{}) - stack = newstack() + stack = newStackForTesting() pc = uint64(0) ) opRandom(&pc, evm, &ScopeContext{nil, stack, nil}) - if len(stack.data) != 1 { - t.Errorf("Expected one item on stack after %v, got %d: ", tt.name, len(stack.data)) + if have, want := stack.len(), 1; have != want { + t.Errorf("test '%v': want %d item(s) on stack, have %d: ", tt.name, have, want) } actual := stack.pop() expected, overflow := uint256.FromBig(new(big.Int).SetBytes(tt.random.Bytes())) @@ -741,14 +759,14 @@ func TestBlobHash(t *testing.T) { } { var ( evm = NewEVM(BlockContext{}, nil, params.TestChainConfig, Config{}) - stack = newstack() + stack = newStackForTesting() pc = uint64(0) ) evm.SetTxContext(TxContext{BlobHashes: tt.hashes}) stack.push(uint256.NewInt(tt.idx)) opBlobHash(&pc, evm, &ScopeContext{nil, stack, nil}) - if len(stack.data) != 1 { - t.Errorf("Expected one item on stack after %v, got %d: ", tt.name, len(stack.data)) + if have, want := stack.len(), 1; have != want { + t.Errorf("test '%v': want %d item(s) on stack, have %d: ", tt.name, have, want) } actual := stack.pop() expected, overflow := uint256.FromBig(new(big.Int).SetBytes(tt.expect.Bytes())) @@ -844,7 +862,7 @@ func TestOpMCopy(t *testing.T) { } { var ( evm = NewEVM(BlockContext{}, nil, params.TestChainConfig, Config{}) - stack = newstack() + stack = newStackForTesting() pc = uint64(0) ) data := common.FromHex(strings.ReplaceAll(tc.pre, " ", "")) @@ -907,7 +925,7 @@ func TestPush(t *testing.T) { scope := &ScopeContext{ Memory: nil, - Stack: newstack(), + Stack: newStackForTesting(), Contract: &Contract{ Code: code, }, @@ -988,7 +1006,7 @@ func TestOpCLZ(t *testing.T) { } for _, tc := range tests { // prepare a fresh stack and PC - stack := newstack() + stack := newStackForTesting() pc := uint64(0) // parse input @@ -1111,7 +1129,7 @@ func TestEIP8024_Execution(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { code := common.FromHex(tc.codeHex) - stack := newstack() + stack := newStackForTesting() pc := uint64(0) scope := &ScopeContext{Stack: stack, Contract: &Contract{Code: code}} var err error @@ -1189,8 +1207,9 @@ func TestEIP8024_Execution(t *testing.T) { t.Fatalf("unexpected error: %v", err) } got := make([]uint64, 0, stack.len()) - for i := stack.len() - 1; i >= 0; i-- { - got = append(got, stack.data[i].Uint64()) + data := stack.Data() + for i := len(data) - 1; i >= 0; i-- { + got = append(got, data[i].Uint64()) } if len(got) != len(tc.wantVals) { t.Fatalf("stack len=%d; want %d", len(got), len(tc.wantVals)) diff --git a/core/vm/interpreter.go b/core/vm/interpreter.go index 9dc0a0b787..4c278fc857 100644 --- a/core/vm/interpreter.go +++ b/core/vm/interpreter.go @@ -116,8 +116,8 @@ func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte var ( op OpCode // current opcode jumpTable *JumpTable = evm.table - mem = NewMemory() // bound memory - stack = newstack() // local stack + mem = NewMemory() // bound memory + stack = evm.arena.stack() // local stack callContext = &ScopeContext{ Memory: mem, Stack: stack, @@ -140,7 +140,7 @@ func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte // so that it gets executed _after_: the OnOpcode needs the stacks before // they are returned to the pools defer func() { - returnStack(stack) + stack.release() mem.Free() }() contract.Input = input diff --git a/core/vm/interpreter_test.go b/core/vm/interpreter_test.go index 69c2316907..868cb12d04 100644 --- a/core/vm/interpreter_test.go +++ b/core/vm/interpreter_test.go @@ -83,7 +83,7 @@ func BenchmarkInterpreter(b *testing.B) { evm = NewEVM(BlockContext{BlockNumber: big.NewInt(1), Time: 1, Random: &common.Hash{}}, statedb, params.MergedTestChainConfig, Config{}) startGas uint64 = 100_000_000 value = uint256.NewInt(0) - stack = newstack() + stack = newStackForTesting() mem = NewMemory() contract = NewContract(common.Address{}, common.Address{}, value, NewGasBudget(startGas), nil) ) diff --git a/core/vm/memory_table.go b/core/vm/memory_table.go index 63ad967850..8f30cbeee6 100644 --- a/core/vm/memory_table.go +++ b/core/vm/memory_table.go @@ -17,59 +17,59 @@ package vm func memoryKeccak256(stack *Stack) (uint64, bool) { - return calcMemSize64(stack.Back(0), stack.Back(1)) + return calcMemSize64(stack.back(0), stack.back(1)) } func memoryCallDataCopy(stack *Stack) (uint64, bool) { - return calcMemSize64(stack.Back(0), stack.Back(2)) + return calcMemSize64(stack.back(0), stack.back(2)) } func memoryReturnDataCopy(stack *Stack) (uint64, bool) { - return calcMemSize64(stack.Back(0), stack.Back(2)) + return calcMemSize64(stack.back(0), stack.back(2)) } func memoryCodeCopy(stack *Stack) (uint64, bool) { - return calcMemSize64(stack.Back(0), stack.Back(2)) + return calcMemSize64(stack.back(0), stack.back(2)) } func memoryExtCodeCopy(stack *Stack) (uint64, bool) { - return calcMemSize64(stack.Back(1), stack.Back(3)) + return calcMemSize64(stack.back(1), stack.back(3)) } func memoryMLoad(stack *Stack) (uint64, bool) { - return calcMemSize64WithUint(stack.Back(0), 32) + return calcMemSize64WithUint(stack.back(0), 32) } func memoryMStore8(stack *Stack) (uint64, bool) { - return calcMemSize64WithUint(stack.Back(0), 1) + return calcMemSize64WithUint(stack.back(0), 1) } func memoryMStore(stack *Stack) (uint64, bool) { - return calcMemSize64WithUint(stack.Back(0), 32) + return calcMemSize64WithUint(stack.back(0), 32) } func memoryMcopy(stack *Stack) (uint64, bool) { - mStart := stack.Back(0) // stack[0]: dest - if stack.Back(1).Gt(mStart) { - mStart = stack.Back(1) // stack[1]: source + mStart := stack.back(0) // stack[0]: dest + if stack.back(1).Gt(mStart) { + mStart = stack.back(1) // stack[1]: source } - return calcMemSize64(mStart, stack.Back(2)) // stack[2]: length + return calcMemSize64(mStart, stack.back(2)) // stack[2]: length } func memoryCreate(stack *Stack) (uint64, bool) { - return calcMemSize64(stack.Back(1), stack.Back(2)) + return calcMemSize64(stack.back(1), stack.back(2)) } func memoryCreate2(stack *Stack) (uint64, bool) { - return calcMemSize64(stack.Back(1), stack.Back(2)) + return calcMemSize64(stack.back(1), stack.back(2)) } func memoryCall(stack *Stack) (uint64, bool) { - x, overflow := calcMemSize64(stack.Back(5), stack.Back(6)) + x, overflow := calcMemSize64(stack.back(5), stack.back(6)) if overflow { return 0, true } - y, overflow := calcMemSize64(stack.Back(3), stack.Back(4)) + y, overflow := calcMemSize64(stack.back(3), stack.back(4)) if overflow { return 0, true } @@ -80,11 +80,11 @@ func memoryCall(stack *Stack) (uint64, bool) { } func memoryDelegateCall(stack *Stack) (uint64, bool) { - x, overflow := calcMemSize64(stack.Back(4), stack.Back(5)) + x, overflow := calcMemSize64(stack.back(4), stack.back(5)) if overflow { return 0, true } - y, overflow := calcMemSize64(stack.Back(2), stack.Back(3)) + y, overflow := calcMemSize64(stack.back(2), stack.back(3)) if overflow { return 0, true } @@ -95,11 +95,11 @@ func memoryDelegateCall(stack *Stack) (uint64, bool) { } func memoryStaticCall(stack *Stack) (uint64, bool) { - x, overflow := calcMemSize64(stack.Back(4), stack.Back(5)) + x, overflow := calcMemSize64(stack.back(4), stack.back(5)) if overflow { return 0, true } - y, overflow := calcMemSize64(stack.Back(2), stack.Back(3)) + y, overflow := calcMemSize64(stack.back(2), stack.back(3)) if overflow { return 0, true } @@ -110,13 +110,13 @@ func memoryStaticCall(stack *Stack) (uint64, bool) { } func memoryReturn(stack *Stack) (uint64, bool) { - return calcMemSize64(stack.Back(0), stack.Back(1)) + return calcMemSize64(stack.back(0), stack.back(1)) } func memoryRevert(stack *Stack) (uint64, bool) { - return calcMemSize64(stack.Back(0), stack.Back(1)) + return calcMemSize64(stack.back(0), stack.back(1)) } func memoryLog(stack *Stack) (uint64, bool) { - return calcMemSize64(stack.Back(0), stack.Back(1)) + return calcMemSize64(stack.back(0), stack.back(1)) } diff --git a/core/vm/operations_acl.go b/core/vm/operations_acl.go index 313d03819e..86ac262a93 100644 --- a/core/vm/operations_acl.go +++ b/core/vm/operations_acl.go @@ -37,7 +37,7 @@ func makeGasSStoreFunc(clearingRefund uint64) gasFunc { } // Gas sentry honoured, do the actual gas calculation based on the stored value var ( - y, x = stack.Back(1), stack.peek() + y, x = stack.back(1), stack.peek() slot = common.Hash(x.Bytes32()) current, original = evm.StateDB.GetStateAndCommittedState(contract.Address(), slot) cost = uint64(0) @@ -158,7 +158,7 @@ func gasEip2929AccountCheck(evm *EVM, contract *Contract, stack *Stack, mem *Mem func makeCallVariantGasCallEIP2929(oldCalculator gasFunc, addressPosition int) gasFunc { return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { - addr := common.Address(stack.Back(addressPosition).Bytes20()) + addr := common.Address(stack.back(addressPosition).Bytes20()) // Check slot presence in the access list warmAccess := evm.StateDB.AddressInAccessList(addr) // The WarmStorageReadCostEIP2929 (100) is already deducted in the form of a constant cost, so @@ -269,7 +269,7 @@ func gasCallEIP7702(evm *EVM, contract *Contract, stack *Stack, mem *Memory, mem // Although it's checked in `gasCall`, EIP-7702 loads the target's code before // to determine if it is resolving a delegation. This could incorrectly record // the target in the block access list (BAL) if the call later fails. - transfersValue := !stack.Back(2).IsZero() + transfersValue := !stack.back(2).IsZero() if evm.readOnly && transfersValue { return GasCosts{}, ErrWriteProtection } @@ -281,7 +281,7 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc gasFunc) gasFunc { var ( eip2929Cost uint64 eip7702Cost uint64 - addr = common.Address(stack.Back(1).Bytes20()) + addr = common.Address(stack.back(1).Bytes20()) ) // Perform EIP-2929 checks (stateless), checking address presence // in the accessList and charge the cold access accordingly. @@ -330,7 +330,7 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc gasFunc) gasFunc { } // Calculate the gas budget for the nested call. The costs defined by // EIP-2929 and EIP-7702 have already been applied. - evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas.RegularGas, intrinsicCost.RegularGas, stack.Back(0)) + evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas.RegularGas, intrinsicCost.RegularGas, stack.back(0)) if err != nil { return GasCosts{}, err } diff --git a/core/vm/operations_verkle.go b/core/vm/operations_verkle.go index d57f2c4dcf..4d3960a174 100644 --- a/core/vm/operations_verkle.go +++ b/core/vm/operations_verkle.go @@ -56,7 +56,7 @@ func gasExtCodeHash4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, func makeCallVariantGasEIP4762(oldCalculator gasFunc, withTransferCosts bool) gasFunc { return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { var ( - target = common.Address(stack.Back(1).Bytes20()) + target = common.Address(stack.back(1).Bytes20()) witnessGas uint64 _, isPrecompile = evm.precompile(target) isSystemContract = target == params.HistoryStorageAddress @@ -64,7 +64,7 @@ func makeCallVariantGasEIP4762(oldCalculator gasFunc, withTransferCosts bool) ga // If value is transferred, it is charged before 1/64th // is subtracted from the available gas pool. - if withTransferCosts && !stack.Back(2).IsZero() { + if withTransferCosts && !stack.back(2).IsZero() { wantedValueTransferWitnessGas := evm.AccessEvents.ValueTransferGas(contract.Address(), target, contract.Gas.RegularGas) if wantedValueTransferWitnessGas > contract.Gas.RegularGas { return GasCosts{RegularGas: wantedValueTransferWitnessGas}, nil @@ -168,8 +168,8 @@ func gasCodeCopyEip4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, gas := gasCost.RegularGas if !contract.IsDeployment && !contract.IsSystemCall { var ( - codeOffset = stack.Back(1) - length = stack.Back(2) + codeOffset = stack.back(1) + length = stack.back(2) ) uint64CodeOffset, overflow := codeOffset.Uint64WithOverflow() if overflow { diff --git a/core/vm/runtime/runtime_test.go b/core/vm/runtime/runtime_test.go index 40fb770454..bc2ffd622d 100644 --- a/core/vm/runtime/runtime_test.go +++ b/core/vm/runtime/runtime_test.go @@ -959,3 +959,63 @@ func TestDelegatedAccountAccessCost(t *testing.T) { } } } + +func TestManyLargeStacks(t *testing.T) { + // This piece of code will push 512 items to the stack, and then call itself + // recursively. + code := make([]byte, 10) + for i := range code { + code[i] = byte(vm.PUSH0) + } + code = append(code, []byte{ + byte(vm.ADDRESS), // address to call + byte(vm.GAS), + byte(vm.CALL), + }...) + + main := common.HexToAddress("0xbb") + statedb, _ := state.New(types.EmptyRootHash, state.NewDatabaseForTesting()) + statedb.SetCode(main, code, tracing.CodeChangeUnspecified) + + //tracer := logger.NewJSONLogger(nil, os.Stdout) + var tracer *tracing.Hooks + _, _, err := Call(main, nil, &Config{ + GasLimit: 10_000_000, + State: statedb, + EVMConfig: vm.Config{ + Tracer: tracer, + }}) + if err != nil { + t.Fatal("didn't expect error", err) + } +} + +func BenchmarkLargeDeepStacks(b *testing.B) { + // This piece of code will push 512 items to the stack, and then call itself + // recursively. + code := make([]byte, 512) + for i := range code { + code[i] = byte(vm.PUSH0) + } + code = append(code, []byte{ + byte(vm.ADDRESS), // address to call + byte(vm.GAS), + byte(vm.CALL), + }...) + benchmarkNonModifyingCode(10_000_000, code, "deep-large-stacks-10M", "", b) +} + +func BenchmarkShortDeepStacks(b *testing.B) { + // This piece of code will push a few items to the stack, and then call itself + // recursively. + code := make([]byte, 8) + for i := range code { + code[i] = byte(vm.PUSH0) + } + code = append(code, []byte{ + byte(vm.ADDRESS), // address to call + byte(vm.GAS), + byte(vm.CALL), + }...) + benchmarkNonModifyingCode(10_000_000, code, "deep-short-stacks-10M", "", b) +} diff --git a/core/vm/stack.go b/core/vm/stack.go index 879dc9aa6d..d8000bc86d 100644 --- a/core/vm/stack.go +++ b/core/vm/stack.go @@ -17,111 +17,170 @@ package vm import ( + "slices" "sync" "github.com/holiman/uint256" ) +// stackArena is an arena which actual evm stacks use for data storage +type stackArena struct { + data []uint256.Int + top int // first free slot +} + +func newArena() *stackArena { + return stackPool.Get().(*stackArena) +} + +// 1025, because in stack() there is a condition check +// for the stack size that would fail if it was set to +// 1024. +const initialStackSize = 1025 + var stackPool = sync.Pool{ - New: func() interface{} { - return &Stack{data: make([]uint256.Int, 0, 16)} + New: func() any { + return &stackArena{ + data: make([]uint256.Int, initialStackSize), + } }, } +func returnStack(arena *stackArena) { + arena.top = 0 // defensive, not strictly needed as s.inner.top = s.bottom in release() + stackPool.Put(arena) +} + +// stack returns an instance of a stack which uses the underlying arena. The instance +// must be released by invoking the (*Stack).release() method +func (sa *stackArena) stack() *Stack { + // make sure every substack has at least 1024 elements + if len(sa.data) <= sa.top+1024 { + // we need to grow the arena + sa.data = slices.Grow(sa.data, 1024) + sa.data = sa.data[:cap(sa.data)] + } + return &Stack{ + bottom: sa.top, + size: 0, + inner: sa, + } +} + +// newStackForTesting is meant to be used solely for testing. It creates a stack +// backed by a newly allocated arena. +func newStackForTesting() *Stack { + arena := &stackArena{ + data: make([]uint256.Int, 1025), + } + return arena.stack() +} + // Stack is an object for basic stack operations. Items popped to the stack are // expected to be changed and modified. stack does not take care of adding newly // initialized objects. type Stack struct { - data []uint256.Int + bottom int // bottom is the index of the first element of this stack + size int // size is the number of elements in this stack + inner *stackArena } -func newstack() *Stack { - return stackPool.Get().(*Stack) -} - -func returnStack(s *Stack) { - s.data = s.data[:0] - stackPool.Put(s) +// release un-claims the area of the arena which was claimed by the stack. +func (s *Stack) release() { + // When the stack is returned, need to notify the arena that the new 'top' is + // the returned stack's bottom. + s.inner.top = s.bottom } // Data returns the underlying uint256.Int array. -func (st *Stack) Data() []uint256.Int { - return st.data +func (s *Stack) Data() []uint256.Int { + return s.inner.data[s.bottom : s.bottom+s.size] } -func (st *Stack) push(d *uint256.Int) { - // NOTE push limit (1024) is checked in baseCheck - st.data = append(st.data, *d) +func (s *Stack) push(d *uint256.Int) { + elem := s.get() + *elem = *d } -func (st *Stack) pop() (ret uint256.Int) { - ret = st.data[len(st.data)-1] - st.data = st.data[:len(st.data)-1] - return +// get returns a pointer to a newly created element +// on top of the stack +func (s *Stack) get() *uint256.Int { + elem := &s.inner.data[s.inner.top] + s.inner.top++ + s.size++ + return elem } -func (st *Stack) len() int { - return len(st.data) +func (s *Stack) pop() uint256.Int { + s.inner.top-- + s.size-- + return s.inner.data[s.inner.top] } -func (st *Stack) swap1() { - st.data[st.len()-2], st.data[st.len()-1] = st.data[st.len()-1], st.data[st.len()-2] -} -func (st *Stack) swap2() { - st.data[st.len()-3], st.data[st.len()-1] = st.data[st.len()-1], st.data[st.len()-3] -} -func (st *Stack) swap3() { - st.data[st.len()-4], st.data[st.len()-1] = st.data[st.len()-1], st.data[st.len()-4] -} -func (st *Stack) swap4() { - st.data[st.len()-5], st.data[st.len()-1] = st.data[st.len()-1], st.data[st.len()-5] -} -func (st *Stack) swap5() { - st.data[st.len()-6], st.data[st.len()-1] = st.data[st.len()-1], st.data[st.len()-6] -} -func (st *Stack) swap6() { - st.data[st.len()-7], st.data[st.len()-1] = st.data[st.len()-1], st.data[st.len()-7] -} -func (st *Stack) swap7() { - st.data[st.len()-8], st.data[st.len()-1] = st.data[st.len()-1], st.data[st.len()-8] -} -func (st *Stack) swap8() { - st.data[st.len()-9], st.data[st.len()-1] = st.data[st.len()-1], st.data[st.len()-9] -} -func (st *Stack) swap9() { - st.data[st.len()-10], st.data[st.len()-1] = st.data[st.len()-1], st.data[st.len()-10] -} -func (st *Stack) swap10() { - st.data[st.len()-11], st.data[st.len()-1] = st.data[st.len()-1], st.data[st.len()-11] -} -func (st *Stack) swap11() { - st.data[st.len()-12], st.data[st.len()-1] = st.data[st.len()-1], st.data[st.len()-12] -} -func (st *Stack) swap12() { - st.data[st.len()-13], st.data[st.len()-1] = st.data[st.len()-1], st.data[st.len()-13] -} -func (st *Stack) swap13() { - st.data[st.len()-14], st.data[st.len()-1] = st.data[st.len()-1], st.data[st.len()-14] -} -func (st *Stack) swap14() { - st.data[st.len()-15], st.data[st.len()-1] = st.data[st.len()-1], st.data[st.len()-15] -} -func (st *Stack) swap15() { - st.data[st.len()-16], st.data[st.len()-1] = st.data[st.len()-1], st.data[st.len()-16] -} -func (st *Stack) swap16() { - st.data[st.len()-17], st.data[st.len()-1] = st.data[st.len()-1], st.data[st.len()-17] +func (s *Stack) len() int { + return s.size } -func (st *Stack) dup(n int) { - st.push(&st.data[st.len()-n]) +func (s *Stack) swap1() { + s.inner.data[s.bottom+s.size-2], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-2] +} +func (s *Stack) swap2() { + s.inner.data[s.bottom+s.size-3], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-3] +} +func (s *Stack) swap3() { + s.inner.data[s.bottom+s.size-4], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-4] +} +func (s *Stack) swap4() { + s.inner.data[s.bottom+s.size-5], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-5] +} +func (s *Stack) swap5() { + s.inner.data[s.bottom+s.size-6], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-6] +} +func (s *Stack) swap6() { + s.inner.data[s.bottom+s.size-7], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-7] +} +func (s *Stack) swap7() { + s.inner.data[s.bottom+s.size-8], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-8] +} +func (s *Stack) swap8() { + s.inner.data[s.bottom+s.size-9], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-9] +} +func (s *Stack) swap9() { + s.inner.data[s.bottom+s.size-10], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-10] +} +func (s *Stack) swap10() { + s.inner.data[s.bottom+s.size-11], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-11] +} +func (s *Stack) swap11() { + s.inner.data[s.bottom+s.size-12], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-12] +} +func (s *Stack) swap12() { + s.inner.data[s.bottom+s.size-13], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-13] +} +func (s *Stack) swap13() { + s.inner.data[s.bottom+s.size-14], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-14] +} +func (s *Stack) swap14() { + s.inner.data[s.bottom+s.size-15], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-15] +} +func (s *Stack) swap15() { + s.inner.data[s.bottom+s.size-16], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-16] +} +func (s *Stack) swap16() { + s.inner.data[s.bottom+s.size-17], s.inner.data[s.bottom+s.size-1] = s.inner.data[s.bottom+s.size-1], s.inner.data[s.bottom+s.size-17] } -func (st *Stack) peek() *uint256.Int { - return &st.data[st.len()-1] +func (s *Stack) dup(n int) { + s.inner.data[s.bottom+s.size] = s.inner.data[s.bottom+s.size-n] + s.size++ + s.inner.top++ } -// Back returns the n'th item in stack -func (st *Stack) Back(n int) *uint256.Int { - return &st.data[st.len()-n-1] +func (s *Stack) peek() *uint256.Int { + return &s.inner.data[s.bottom+s.size-1] +} + +// back returns the n'th item in stack +func (s *Stack) back(n int) *uint256.Int { + return &s.inner.data[s.bottom+s.size-n-1] } diff --git a/eth/gasestimator/gasestimator.go b/eth/gasestimator/gasestimator.go index ad6491fd93..ace0752037 100644 --- a/eth/gasestimator/gasestimator.go +++ b/eth/gasestimator/gasestimator.go @@ -63,10 +63,10 @@ func Estimate(ctx context.Context, call *core.Message, opts *Options, gasCap uin } // Cap the maximum gas allowance according to EIP-7825 if the estimation targets Osaka - if hi > params.MaxTxGas { - if opts.Config.IsOsaka(opts.Header.Number, opts.Header.Time) { - hi = params.MaxTxGas - } + isOsaka := opts.Config.IsOsaka(opts.Header.Number, opts.Header.Time) + isAmsterdam := opts.Config.IsAmsterdam(opts.Header.Number, opts.Header.Time) + if hi > params.MaxTxGas && isOsaka && !isAmsterdam { + hi = params.MaxTxGas } // Normalize the max fee per gas the call is willing to spend. @@ -244,6 +244,7 @@ func run(ctx context.Context, call *core.Message, opts *Options) (*core.Executio evmContext.BlobBaseFee = new(big.Int) } evm := vm.NewEVM(evmContext, dirtyState, opts.Config, vm.Config{NoBaseFee: true}) + defer evm.Release() // Monitor the outer context and interrupt the EVM upon cancellation. To avoid // a dangling goroutine until the outer estimation finishes, create an internal diff --git a/eth/state_accessor.go b/eth/state_accessor.go index 7467e1e590..a806a4fc56 100644 --- a/eth/state_accessor.go +++ b/eth/state_accessor.go @@ -247,6 +247,7 @@ func (eth *Ethereum) stateAtTransaction(ctx context.Context, block *types.Block, // Insert parent beacon block root in the state as per EIP-4788. context := core.NewEVMBlockContext(block.Header(), eth.blockchain, nil) evm := vm.NewEVM(context, statedb, eth.blockchain.Config(), vm.Config{}) + defer evm.Release() if beaconRoot := block.BeaconRoot(); beaconRoot != nil { core.ProcessBeaconBlockRoot(*beaconRoot, evm) } diff --git a/eth/tracers/api.go b/eth/tracers/api.go index b5ddc78a10..dae11b81de 100644 --- a/eth/tracers/api.go +++ b/eth/tracers/api.go @@ -379,6 +379,7 @@ func (api *API) traceChain(start, end *types.Block, config *TraceConfig, closed if api.backend.ChainConfig().IsPrague(next.Number(), next.Time()) { core.ProcessParentBlockHash(next.ParentHash(), evm) } + evm.Release() // Clean out any pending release functions of trace state. Note this // step must be done after constructing tracing state, because the // tracing state of block next depends on the parent state and construction @@ -524,6 +525,7 @@ func (api *API) IntermediateRoots(ctx context.Context, hash common.Hash, config deleteEmptyObjects = chainConfig.IsEIP158(block.Number()) ) evm := vm.NewEVM(vmctx, statedb, chainConfig, vm.Config{}) + defer evm.Release() if beaconRoot := block.BeaconRoot(); beaconRoot != nil { core.ProcessBeaconBlockRoot(*beaconRoot, evm) } @@ -584,6 +586,7 @@ func (api *API) traceBlock(ctx context.Context, block *types.Block, config *Trac blockCtx := core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), nil) evm := vm.NewEVM(blockCtx, statedb, api.backend.ChainConfig(), vm.Config{}) + defer evm.Release() if beaconRoot := block.BeaconRoot(); beaconRoot != nil { core.ProcessBeaconBlockRoot(*beaconRoot, evm) } @@ -673,6 +676,7 @@ func (api *API) traceBlockParallel(ctx context.Context, block *types.Block, stat var failed error blockCtx := core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), nil) evm := vm.NewEVM(blockCtx, statedb, api.backend.ChainConfig(), vm.Config{}) + defer evm.Release() txloop: for i, tx := range txs { @@ -758,6 +762,7 @@ func (api *API) standardTraceBlockToFile(ctx context.Context, block *types.Block } evm := vm.NewEVM(vmctx, statedb, chainConfig, vm.Config{}) + defer evm.Release() if beaconRoot := block.BeaconRoot(); beaconRoot != nil { core.ProcessBeaconBlockRoot(*beaconRoot, evm) } @@ -805,6 +810,7 @@ func (api *API) standardTraceBlockToFile(ctx context.Context, block *types.Block tracer.OnTxStart(evm.GetVMContext(), tx, msg.From) } _, err = core.ApplyMessage(evm, msg, nil) + evm.Release() if writer != nil { writer.Flush() } @@ -817,7 +823,7 @@ func (api *API) standardTraceBlockToFile(ctx context.Context, block *types.Block } // Finalize the state so any modifications are written to the trie // Only delete empty objects if EIP158/161 (a.k.a Spurious Dragon) is in effect - statedb.Finalise(evm.ChainConfig().IsEIP158(block.Number())) + statedb.Finalise(chainConfig.IsEIP158(block.Number())) // If we've traced the transaction we were looking for, abort if tx.Hash() == txHash { @@ -999,6 +1005,7 @@ func (api *API) traceTx(ctx context.Context, tx *types.Transaction, message *cor } tracingStateDB := state.NewHookedState(statedb, tracer.Hooks) evm := vm.NewEVM(vmctx, tracingStateDB, api.backend.ChainConfig(), vm.Config{Tracer: tracer.Hooks, NoBaseFee: true}) + defer evm.Release() if precompiles != nil { evm.SetPrecompiles(precompiles) } diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 149e12c5b8..e8669b86c6 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -775,6 +775,7 @@ func applyMessage(ctx context.Context, b Backend, args TransactionArgs, state *s blockContext.BlobBaseFee = new(big.Int) } evm := b.GetEVM(ctx, state, header, vmConfig, blockContext) + defer evm.Release() if precompiles != nil { evm.SetPrecompiles(precompiles) } @@ -1390,6 +1391,7 @@ func AccessList(ctx context.Context, b Backend, blockNrOrHash rpc.BlockNumberOrH evm.Context.BlobBaseFee = new(big.Int) } res, err := core.ApplyMessage(evm, msg, nil) + evm.Release() if err != nil { return nil, 0, nil, fmt.Errorf("failed to apply transaction: %v err: %v", args.ToTransaction(types.LegacyTxType).Hash(), err) } diff --git a/internal/ethapi/simulate.go b/internal/ethapi/simulate.go index d18561760e..e3a14bf5d6 100644 --- a/internal/ethapi/simulate.go +++ b/internal/ethapi/simulate.go @@ -312,6 +312,7 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header, tracingStateDB = state.NewHookedState(sim.state, hooks) } evm := vm.NewEVM(blockContext, tracingStateDB, sim.chainConfig, *vmConfig) + defer evm.Release() // It is possible to override precompiles with EVM bytecode, or // move them to another address. if precompiles != nil { diff --git a/miner/worker.go b/miner/worker.go index ae5d6c306f..42e3695025 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -83,6 +83,9 @@ func (env *environment) txFitsSize(tx *types.Transaction) bool { // discard terminates the background threads before discarding it. func (env *environment) discard() { env.state.StopPrefetcher() + if env.evm != nil { + env.evm.Release() + } } const ( @@ -560,7 +563,7 @@ func (miner *Miner) fillTransactions(ctx context.Context, interrupt *atomic.Int3 if env.header.ExcessBlobGas != nil { filter.BlobFee = uint256.MustFromBig(eip4844.CalcBlobFee(miner.chainConfig, env.header)) } - if miner.chainConfig.IsOsaka(env.header.Number, env.header.Time) { + if miner.chainConfig.IsOsaka(env.header.Number, env.header.Time) && !miner.chainConfig.IsAmsterdam(env.header.Number, env.header.Time) { filter.GasLimitCap = params.MaxTxGas } filter.BlobTxs = false diff --git a/p2p/discover/common.go b/p2p/discover/common.go index 767cc23b92..5513afd54d 100644 --- a/p2p/discover/common.go +++ b/p2p/discover/common.go @@ -17,9 +17,11 @@ package discover import ( + "container/list" "crypto/ecdsa" crand "crypto/rand" "encoding/binary" + "iter" "math/rand" "net" "net/netip" @@ -143,3 +145,16 @@ func (r *reseedingRandom) Shuffle(n int, swap func(i, j int)) { defer r.mu.Unlock() r.cur.Shuffle(n, swap) } + +// iterList iterates over the elements of the given list. +func iterList[T any](l *list.List) iter.Seq2[T, *list.Element] { + return func(yield func(T, *list.Element) bool) { + for el := l.Front(); el != nil; { + next := el.Next() + if !yield(el.Value.(T), el) { + return + } + el = next + } + } +} diff --git a/p2p/discover/v4_udp.go b/p2p/discover/v4_udp.go index dd3f363f7e..9e824dae18 100644 --- a/p2p/discover/v4_udp.go +++ b/p2p/discover/v4_udp.go @@ -446,9 +446,8 @@ func (t *UDPv4) loop() { } // Start the timer so it fires when the next pending reply has expired. now := time.Now() - for el := plist.Front(); el != nil; el = el.Next() { - nextTimeout = el.Value.(*replyMatcher) - if dist := nextTimeout.deadline.Sub(now); dist < 2*respTimeout { + for p, el := range iterList[*replyMatcher](plist) { + if dist := p.deadline.Sub(now); dist < 2*respTimeout { timeout.Reset(dist) return } @@ -478,8 +477,7 @@ func (t *UDPv4) loop() { case r := <-t.gotreply: var matched bool // whether any replyMatcher considered the reply acceptable. - for el := plist.Front(); el != nil; el = el.Next() { - p := el.Value.(*replyMatcher) + for p, el := range iterList[*replyMatcher](plist) { if p.from == r.from && p.ptype == r.data.Kind() && p.ip == r.ip { ok, requestDone := p.callback(r.data) matched = matched || ok @@ -499,8 +497,7 @@ func (t *UDPv4) loop() { nextTimeout = nil // Notify and remove callbacks whose deadline is in the past. - for el := plist.Front(); el != nil; el = el.Next() { - p := el.Value.(*replyMatcher) + for p, el := range iterList[*replyMatcher](plist) { if now.After(p.deadline) || now.Equal(p.deadline) { p.errc <- errTimeout plist.Remove(el)