From 98e364dfbd55889b30a3dfc77ea5a886ab2318c3 Mon Sep 17 00:00:00 2001 From: RekCuy63 Date: Fri, 8 May 2026 10:45:55 +0800 Subject: [PATCH] core/vm, core, miner: abort timed-out block transactions --- core/state_processor.go | 3 ++ core/vm/errors.go | 1 + miner/worker.go | 26 +++++++++- miner/worker_test.go | 108 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 miner/worker_test.go diff --git a/core/state_processor.go b/core/state_processor.go index 54ebbd047b..465f247e11 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -176,6 +176,9 @@ func ApplyTransactionWithEVM(msg *Message, gp *GasPool, statedb *state.StateDB, if err != nil { return nil, err } + if evm.Cancelled() { + return nil, vm.ErrExecutionInterrupted + } // Update the state with pending changes. var root []byte if evm.ChainConfig().IsByzantium(blockNumber) { diff --git a/core/vm/errors.go b/core/vm/errors.go index b6235d44a6..ad7bba0626 100644 --- a/core/vm/errors.go +++ b/core/vm/errors.go @@ -38,6 +38,7 @@ var ( ErrGasUintOverflow = errors.New("gas uint64 overflow") ErrInvalidCode = errors.New("invalid code: must not begin with 0xef") ErrNonceUintOverflow = errors.New("nonce uint64 overflow") + ErrExecutionInterrupted = errors.New("execution interrupted") // errStopToken is an internal token indicating interpreter loop termination, // never returned to outside callers. diff --git a/miner/worker.go b/miner/worker.go index 42e3695025..415451a0c7 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -175,14 +175,23 @@ func (miner *Miner) generateWork(ctx context.Context, genParam *generateParams, } } else { interrupt := new(atomic.Int32) + timeout := make(chan struct{}) timer := time.AfterFunc(miner.config.Recommit, func() { interrupt.Store(commitInterruptTimeout) + work.evm.Cancel() + close(timeout) }) - defer timer.Stop() err := miner.fillTransactions(ctx, interrupt, work) + if !timer.Stop() { + <-timeout + if err == nil { + err = errBlockInterruptedByTimeout + } + } if errors.Is(err, errBlockInterruptedByTimeout) { log.Warn("Block building is interrupted", "allowance", common.PrettyDuration(miner.config.Recommit)) + miner.resetWorkEVM(work) } } } @@ -366,6 +375,13 @@ func (miner *Miner) makeEnv(parent *types.Header, header *types.Header, coinbase }, nil } +func (miner *Miner) resetWorkEVM(env *environment) { + if env.evm != nil { + env.evm.Release() + } + env.evm = vm.NewEVM(core.NewEVMBlockContext(env.header, miner.chain, &env.coinbase), env.state, miner.chainConfig, vm.Config{}) +} + func (miner *Miner) commitTransaction(ctx context.Context, env *environment, tx *types.Transaction) (err error) { _, _, spanEnd := telemetry.StartSpan(ctx, "miner.commitTransaction") defer spanEnd(&err) @@ -418,6 +434,11 @@ func (miner *Miner) applyTransaction(env *environment, tx *types.Transaction) (* gp = env.gasPool.Snapshot() ) receipt, err := core.ApplyTransaction(env.evm, env.gasPool, env.state, env.header, tx) + if env.evm.Cancelled() { + env.state.RevertToSnapshot(snap) + env.gasPool.Set(gp) + return nil, errBlockInterruptedByTimeout + } if err != nil { env.state.RevertToSnapshot(snap) env.gasPool.Set(gp) @@ -522,6 +543,9 @@ func (miner *Miner) commitTransactions(ctx context.Context, env *environment, pl err := miner.commitTransaction(ctx, env, tx) switch { + case errors.Is(err, errBlockInterruptedByTimeout): + return err + case errors.Is(err, core.ErrNonceTooLow): // New head notification data race between the transaction pool and miner, shift log.Trace("Skipping transaction with low nonce", "hash", ltx.Hash, "sender", from, "nonce", tx.Nonce()) diff --git a/miner/worker_test.go b/miner/worker_test.go new file mode 100644 index 0000000000..9c6172269f --- /dev/null +++ b/miner/worker_test.go @@ -0,0 +1,108 @@ +// 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 miner + +import ( + "context" + "errors" + "math/big" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/tracing" + "github.com/ethereum/go-ethereum/core/txpool" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/params" + "github.com/holiman/uint256" +) + +func TestCommitTransactionsReturnsTimeoutOnCancelledEVM(t *testing.T) { + var ( + key, _ = crypto.GenerateKey() + from = crypto.PubkeyToAddress(key.PublicKey) + contract = common.HexToAddress("0x1000000000000000000000000000000000000000") + header = &types.Header{ + Number: big.NewInt(1), + GasLimit: 1_000_000, + GasUsed: 0, + BaseFee: big.NewInt(params.InitialBaseFee), + Difficulty: big.NewInt(0), + Coinbase: common.HexToAddress("0x2000000000000000000000000000000000000000"), + } + ) + statedb, _ := state.New(types.EmptyRootHash, state.NewDatabaseForTesting()) + statedb.CreateAccount(from) + statedb.AddBalance(from, uint256.MustFromBig(new(big.Int).Lsh(big.NewInt(1), 100)), tracing.BalanceIncreaseGenesisBalance) + statedb.CreateAccount(contract) + // Store a value before entering a cancelled jump loop. Without the miner-side + // Cancelled check, the interpreter clears its stop token and this write survives. + statedb.SetCode(contract, common.Hex2Bytes("600160005560075b600756"), tracing.CodeChangeUnspecified) + statedb.Finalise(true) + + evm := vm.NewEVM(core.NewEVMBlockContext(header, nil, &header.Coinbase), statedb, params.TestChainConfig, vm.Config{}) + defer evm.Release() + evm.Cancel() + + signer := types.MakeSigner(params.TestChainConfig, header.Number, header.Time) + tx := types.MustSignNewTx(key, signer, &types.LegacyTx{ + Nonce: 0, + To: &contract, + Gas: 100_000, + GasPrice: big.NewInt(params.InitialBaseFee), + }) + plainTxs := newTransactionsByPriceAndNonce(signer, map[common.Address][]*txpool.LazyTransaction{ + from: {{ + Tx: tx, + Hash: tx.Hash(), + Time: time.Now(), + GasFeeCap: uint256.MustFromBig(tx.GasFeeCap()), + GasTipCap: uint256.MustFromBig(tx.GasTipCap()), + Gas: tx.Gas(), + }}, + }, header.BaseFee) + blobTxs := newTransactionsByPriceAndNonce(signer, map[common.Address][]*txpool.LazyTransaction{}, header.BaseFee) + env := &environment{ + signer: signer, + state: statedb, + gasPool: core.NewGasPool(header.GasLimit), + header: header, + evm: evm, + } + miner := &Miner{chainConfig: params.TestChainConfig} + + err := miner.commitTransactions(context.Background(), env, plainTxs, blobTxs, nil) + if !errors.Is(err, errBlockInterruptedByTimeout) { + t.Fatalf("unexpected error: got %v, want %v", err, errBlockInterruptedByTimeout) + } + if len(env.txs) != 0 { + t.Fatalf("interrupted transaction included: %d txs", len(env.txs)) + } + if got := statedb.GetState(contract, common.Hash{}); got != (common.Hash{}) { + t.Fatalf("interrupted transaction state was not reverted: %x", got) + } + if got := env.gasPool.Used(); got != 0 { + t.Fatalf("interrupted transaction gas was not restored: %d", got) + } + if header.GasUsed != 0 { + t.Fatalf("interrupted transaction updated header gas: %d", header.GasUsed) + } +}