From 442bd28b0bad76674348a67b4c9f8689170bfcdb Mon Sep 17 00:00:00 2001 From: felipe Date: Mon, 27 Apr 2026 14:35:49 +0200 Subject: [PATCH 1/8] cmd/evm/internal/t8ntool: stream t8n alloc to ease heavy memory cases (#34785) --- cmd/evm/internal/t8ntool/execution.go | 90 ++++++++++++++++++- cmd/evm/internal/t8ntool/transition.go | 119 ++++++++++++++++++++++--- 2 files changed, 193 insertions(+), 16 deletions(-) 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/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 { From 822e7c648618a43e7f8326868b9334b9976185c8 Mon Sep 17 00:00:00 2001 From: cui Date: Mon, 27 Apr 2026 22:13:42 +0800 Subject: [PATCH 2/8] accounts/scwallet: truncate before write (#34815) Co-authored-by: Guillaume Ballet <3272758+gballet@users.noreply.github.com> --- accounts/scwallet/hub.go | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) 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 { From 51c97216c5f1c512781de46a85e756c15ed2c888 Mon Sep 17 00:00:00 2001 From: Rahman Date: Tue, 28 Apr 2026 02:57:58 -0600 Subject: [PATCH 3/8] p2p/discover: fix timeout loop early exit when removing expired matchers (#34743) Save `el.Next()` before calling `plist.Remove(el)` so iteration continues correctly. Previously the loop exited after removing the first expired matcher because `Remove` invalidates the element's links. --------- Co-authored-by: Felix Lange --- p2p/discover/common.go | 15 +++++++++++++++ p2p/discover/v4_udp.go | 11 ++++------- 2 files changed, 19 insertions(+), 7 deletions(-) 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) From 4dc7d461556a2ccde3cd6bbe816bd448755a0e07 Mon Sep 17 00:00:00 2001 From: Guillaume Ballet <3272758+gballet@users.noreply.github.com> Date: Tue, 28 Apr 2026 11:10:44 +0200 Subject: [PATCH 4/8] core/vm: implement stack arena (#33960) Here, we change the EVM stack implementation to use an 'arena', i.e. a shared allocation pool for sub-call stacks. The stack is now more GC-friendly, since it is a slice of uint256 values instead of a slice of pointers. Code that pushes an item to the stack has been changed to get() the top item, then overwrite it. The PR is a rewrite/rebase of #30362. --------- Co-authored-by: Martin Holst Swende Co-authored-by: Marius van der Wijden --- core/state_prefetcher.go | 1 + core/state_processor.go | 1 + core/vm/eips.go | 28 ++--- core/vm/evm.go | 9 ++ core/vm/gas_table.go | 30 ++--- core/vm/instructions.go | 74 ++++++----- core/vm/instructions_test.go | 77 +++++++----- core/vm/interpreter.go | 6 +- core/vm/interpreter_test.go | 2 +- core/vm/memory_table.go | 46 +++---- core/vm/operations_acl.go | 10 +- core/vm/operations_verkle.go | 8 +- core/vm/runtime/runtime_test.go | 60 +++++++++ core/vm/stack.go | 209 ++++++++++++++++++++----------- eth/gasestimator/gasestimator.go | 1 + eth/state_accessor.go | 1 + eth/tracers/api.go | 9 +- internal/ethapi/api.go | 2 + internal/ethapi/simulate.go | 1 + miner/worker.go | 3 + 20 files changed, 366 insertions(+), 212 deletions(-) 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/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 59e301c0a7..26b2f73a00 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: newMapJumpDests(), + 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..efb089d456 100644 --- a/eth/gasestimator/gasestimator.go +++ b/eth/gasestimator/gasestimator.go @@ -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..158f1b26c0 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 ( From b5d9c8d1c2745256b6fb45e1108758720cc4868b Mon Sep 17 00:00:00 2001 From: rjl493456442 Date: Tue, 28 Apr 2026 19:10:15 +0800 Subject: [PATCH 5/8] core: implement BAL reader for prefetching (#33737) --- core/state/reader_eip_7928.go | 247 +++++++++++++++++++++++++++++ core/state/reader_eip_7928_test.go | 145 +++++++++++++++++ 2 files changed, 392 insertions(+) create mode 100644 core/state/reader_eip_7928.go create mode 100644 core/state/reader_eip_7928_test.go 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) + } + } +} From 0c0d299c52c84141ad1f8ee5ea56df0003739afb Mon Sep 17 00:00:00 2001 From: cui Date: Tue, 28 Apr 2026 20:33:40 +0800 Subject: [PATCH 6/8] core/state: opt stateObject.GetState (#34825) --- core/state/state_object.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 From db8d6abced13e81b9a493d5a315164d3cbc9872f Mon Sep 17 00:00:00 2001 From: Miki Noir Date: Tue, 28 Apr 2026 14:36:01 +0100 Subject: [PATCH 7/8] accounts/keystore: enable fsnotify watcher on linux/arm64 (#34834) --- accounts/keystore/watch.go | 4 ++-- accounts/keystore/watch_fallback.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) 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. From 01036bed831186d892a3e3e35b32fcf304d4cc29 Mon Sep 17 00:00:00 2001 From: Giulio rebuffo Date: Tue, 28 Apr 2026 17:25:16 +0200 Subject: [PATCH 8/8] core: skip tx gas cap after Amsterdam (#34841) EIP-7825 caps the transaction gas limit at `MaxTxGas`, but after Amsterdam/EIP-8037 the transaction gas limit can include state gas reservoir in addition to the regular gas dimension. Applying the Osaka cap to the full `tx.Gas()` rejects otherwise valid Amsterdam transactions that need more than `MaxTxGas` total gas because of state gas, while their regular gas use remains within the intended limit. This changes geth to stop applying the full transaction gas cap once Amsterdam is active: - txpool stateless validation no longer rejects `tx.Gas() > MaxTxGas` under Amsterdam - legacy pool reorg cleanup does not purge high-total-gas transactions at the Osaka transition if Amsterdam is also active - execution precheck mirrors the txpool behavior and does not reject high-total-gas messages under Amsterdam The block gas limit check remains in place, so transactions still cannot request more total gas than the current block gas limit. Validation run: ``` go test ./core/txpool ./core/txpool/legacypool go test ./core -run TestStateProcessorErrors ``` --------- Co-authored-by: Gary Rong --- cmd/evm/internal/t8ntool/transaction.go | 4 +++- core/state_transition.go | 3 ++- core/txpool/legacypool/legacypool.go | 6 ++++-- core/txpool/validation.go | 2 +- eth/gasestimator/gasestimator.go | 8 ++++---- miner/worker.go | 2 +- 6 files changed, 15 insertions(+), 10 deletions(-) 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/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/eth/gasestimator/gasestimator.go b/eth/gasestimator/gasestimator.go index efb089d456..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. diff --git a/miner/worker.go b/miner/worker.go index 158f1b26c0..42e3695025 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -563,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