core/vm, core, miner: abort timed-out block transactions

This commit is contained in:
RekCuy63 2026-05-08 10:45:55 +08:00
parent 1abbae239d
commit 98e364dfbd
4 changed files with 137 additions and 1 deletions

View file

@ -176,6 +176,9 @@ func ApplyTransactionWithEVM(msg *Message, gp *GasPool, statedb *state.StateDB,
if err != nil { if err != nil {
return nil, err return nil, err
} }
if evm.Cancelled() {
return nil, vm.ErrExecutionInterrupted
}
// Update the state with pending changes. // Update the state with pending changes.
var root []byte var root []byte
if evm.ChainConfig().IsByzantium(blockNumber) { if evm.ChainConfig().IsByzantium(blockNumber) {

View file

@ -38,6 +38,7 @@ var (
ErrGasUintOverflow = errors.New("gas uint64 overflow") ErrGasUintOverflow = errors.New("gas uint64 overflow")
ErrInvalidCode = errors.New("invalid code: must not begin with 0xef") ErrInvalidCode = errors.New("invalid code: must not begin with 0xef")
ErrNonceUintOverflow = errors.New("nonce uint64 overflow") ErrNonceUintOverflow = errors.New("nonce uint64 overflow")
ErrExecutionInterrupted = errors.New("execution interrupted")
// errStopToken is an internal token indicating interpreter loop termination, // errStopToken is an internal token indicating interpreter loop termination,
// never returned to outside callers. // never returned to outside callers.

View file

@ -175,14 +175,23 @@ func (miner *Miner) generateWork(ctx context.Context, genParam *generateParams,
} }
} else { } else {
interrupt := new(atomic.Int32) interrupt := new(atomic.Int32)
timeout := make(chan struct{})
timer := time.AfterFunc(miner.config.Recommit, func() { timer := time.AfterFunc(miner.config.Recommit, func() {
interrupt.Store(commitInterruptTimeout) interrupt.Store(commitInterruptTimeout)
work.evm.Cancel()
close(timeout)
}) })
defer timer.Stop()
err := miner.fillTransactions(ctx, interrupt, work) err := miner.fillTransactions(ctx, interrupt, work)
if !timer.Stop() {
<-timeout
if err == nil {
err = errBlockInterruptedByTimeout
}
}
if errors.Is(err, errBlockInterruptedByTimeout) { if errors.Is(err, errBlockInterruptedByTimeout) {
log.Warn("Block building is interrupted", "allowance", common.PrettyDuration(miner.config.Recommit)) 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 }, 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) { func (miner *Miner) commitTransaction(ctx context.Context, env *environment, tx *types.Transaction) (err error) {
_, _, spanEnd := telemetry.StartSpan(ctx, "miner.commitTransaction") _, _, spanEnd := telemetry.StartSpan(ctx, "miner.commitTransaction")
defer spanEnd(&err) defer spanEnd(&err)
@ -418,6 +434,11 @@ func (miner *Miner) applyTransaction(env *environment, tx *types.Transaction) (*
gp = env.gasPool.Snapshot() gp = env.gasPool.Snapshot()
) )
receipt, err := core.ApplyTransaction(env.evm, env.gasPool, env.state, env.header, tx) 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 { if err != nil {
env.state.RevertToSnapshot(snap) env.state.RevertToSnapshot(snap)
env.gasPool.Set(gp) env.gasPool.Set(gp)
@ -522,6 +543,9 @@ func (miner *Miner) commitTransactions(ctx context.Context, env *environment, pl
err := miner.commitTransaction(ctx, env, tx) err := miner.commitTransaction(ctx, env, tx)
switch { switch {
case errors.Is(err, errBlockInterruptedByTimeout):
return err
case errors.Is(err, core.ErrNonceTooLow): case errors.Is(err, core.ErrNonceTooLow):
// New head notification data race between the transaction pool and miner, shift // 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()) log.Trace("Skipping transaction with low nonce", "hash", ltx.Hash, "sender", from, "nonce", tx.Nonce())

108
miner/worker_test.go Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
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)
}
}