From c6f6dbc50c3f7fa814dbce3576f1704c9201e231 Mon Sep 17 00:00:00 2001 From: Gary Rong Date: Fri, 27 Feb 2026 11:36:27 +0800 Subject: [PATCH] core, eth, internal, miner: rework the gaspool --- cmd/evm/internal/t8ntool/execution.go | 13 ++- core/chain_makers.go | 18 ++--- core/error.go | 4 + core/gaspool.go | 80 +++++++++++++++---- core/state_prefetcher.go | 2 +- core/state_processor.go | 44 +++++----- core/state_transition.go | 21 +++-- eth/gasestimator/gasestimator.go | 3 +- eth/state_accessor.go | 2 +- eth/tracers/api.go | 12 +-- eth/tracers/api_test.go | 2 +- .../internal/tracetest/calltrace_test.go | 6 +- .../internal/tracetest/erc7562_tracer_test.go | 2 +- .../internal/tracetest/flat_calltrace_test.go | 2 +- .../internal/tracetest/prestate_test.go | 2 +- .../tracetest/selfdestruct_state_test.go | 2 +- eth/tracers/tracers_test.go | 2 +- internal/ethapi/api.go | 20 +++-- internal/ethapi/api_test.go | 4 +- internal/ethapi/simulate.go | 76 ++++++++++++------ internal/ethapi/simulate_test.go | 6 +- miner/worker.go | 33 +++----- tests/state_test_util.go | 4 +- 23 files changed, 215 insertions(+), 145 deletions(-) diff --git a/cmd/evm/internal/t8ntool/execution.go b/cmd/evm/internal/t8ntool/execution.go index 803747b358..b3fb79bc4a 100644 --- a/cmd/evm/internal/t8ntool/execution.go +++ b/cmd/evm/internal/t8ntool/execution.go @@ -149,15 +149,13 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig, isEIP4762 = chainConfig.IsVerkle(big.NewInt(int64(pre.Env.Number)), pre.Env.Timestamp) statedb = MakePreState(rawdb.NewMemoryDatabase(), pre.Pre, isEIP4762) signer = types.MakeSigner(chainConfig, new(big.Int).SetUint64(pre.Env.Number), pre.Env.Timestamp) - gaspool = new(core.GasPool) + gaspool = core.NewGasPool(pre.Env.GasLimit) blockHash = common.Hash{0x13, 0x37} rejectedTxs []*rejectedTx includedTxs types.Transactions - gasUsed = uint64(0) blobGasUsed = uint64(0) receipts = make(types.Receipts, 0) ) - gaspool.AddGas(pre.Env.GasLimit) vmContext := vm.BlockContext{ CanTransfer: core.CanTransfer, Transfer: core.Transfer, @@ -258,20 +256,19 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig, statedb.SetTxContext(tx.Hash(), len(receipts)) var ( snapshot = statedb.Snapshot() - prevGas = gaspool.Gas() + gp = gaspool.Snapshot() ) - receipt, _, err := core.ApplyTransactionWithEVM(msg, gaspool, statedb, vmContext.BlockNumber, blockHash, pre.Env.Timestamp, tx, gasUsed, evm) + receipt, err := core.ApplyTransactionWithEVM(msg, gaspool, statedb, vmContext.BlockNumber, blockHash, pre.Env.Timestamp, tx, evm) if err != nil { statedb.RevertToSnapshot(snapshot) log.Info("rejected tx", "index", i, "hash", tx.Hash(), "from", msg.From, "error", err) rejectedTxs = append(rejectedTxs, &rejectedTx{i, err.Error()}) - gaspool.SetGas(prevGas) + gaspool.Set(gp) continue } if receipt.Logs == nil { receipt.Logs = []*types.Log{} } - gasUsed += receipt.GasUsed includedTxs = append(includedTxs, tx) if hashError != nil { return nil, nil, nil, NewError(ErrorMissingBlockhash, hashError) @@ -353,7 +350,7 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig, Receipts: receipts, Rejected: rejectedTxs, Difficulty: (*math.HexOrDecimal256)(vmContext.Difficulty), - GasUsed: (math.HexOrDecimal64)(gasUsed), + GasUsed: (math.HexOrDecimal64)(gaspool.Used()), BaseFee: (*math.HexOrDecimal256)(vmContext.BaseFee), } if pre.Env.Withdrawals != nil { diff --git a/core/chain_makers.go b/core/chain_makers.go index 7fe5a2627d..8ada2abc82 100644 --- a/core/chain_makers.go +++ b/core/chain_makers.go @@ -38,12 +38,11 @@ import ( // BlockGen creates blocks for testing. // See GenerateChain for a detailed explanation. type BlockGen struct { - i int - cumulativeGas uint64 - cm *chainMaker - parent *types.Block - header *types.Header - statedb *state.StateDB + i int + cm *chainMaker + parent *types.Block + header *types.Header + statedb *state.StateDB gasPool *GasPool txs []*types.Transaction @@ -64,7 +63,7 @@ func (b *BlockGen) SetCoinbase(addr common.Address) { panic("coinbase can only be set once") } b.header.Coinbase = addr - b.gasPool = new(GasPool).AddGas(b.header.GasLimit) + b.gasPool = NewGasPool(b.header.GasLimit) } // SetExtra sets the extra data field of the generated block. @@ -120,11 +119,12 @@ func (b *BlockGen) addTx(bc *BlockChain, vmConfig vm.Config, tx *types.Transacti err error ) b.statedb.SetTxContext(tx.Hash(), len(b.txs)) - receipt, b.cumulativeGas, err = ApplyTransaction(evm, b.gasPool, b.statedb, b.header, tx, b.cumulativeGas) + receipt, err = ApplyTransaction(evm, b.gasPool, b.statedb, b.header, tx) if err != nil { panic(err) } - b.header.GasUsed += receipt.GasUsed + b.header.GasUsed = b.gasPool.Used() + // Merge the tx-local access event into the "block-local" one, in order to collect // all values, so that the witness can be built. if b.statedb.Database().TrieDB().IsVerkle() { diff --git a/core/error.go b/core/error.go index 635d802863..4610842cee 100644 --- a/core/error.go +++ b/core/error.go @@ -58,6 +58,10 @@ var ( // by a transaction is higher than what's left in the block. ErrGasLimitReached = errors.New("gas limit reached") + // ErrGasLimitOverflow is returned by the gas pool if the remaining gas + // exceeds the maximum value of uint64. + ErrGasLimitOverflow = errors.New("gas limit overflow") + // ErrInsufficientFundsForTransfer is returned if the transaction sender doesn't // have enough funds for transfer(topmost call only). ErrInsufficientFundsForTransfer = errors.New("insufficient funds for transfer") diff --git a/core/gaspool.go b/core/gaspool.go index 767222674f..14f5abd93c 100644 --- a/core/gaspool.go +++ b/core/gaspool.go @@ -21,39 +21,87 @@ import ( "math" ) -// GasPool tracks the amount of gas available during execution of the transactions -// in a block. The zero value is a pool with zero gas available. -type GasPool uint64 +// GasPool tracks the amount of gas available for transaction execution +// within a block, along with the cumulative gas consumed. +type GasPool struct { + remaining uint64 + initial uint64 + cumulativeUsed uint64 +} -// AddGas makes gas available for execution. -func (gp *GasPool) AddGas(amount uint64) *GasPool { - if uint64(*gp) > math.MaxUint64-amount { - panic("gas pool pushed above uint64") +// NewGasPool initializes the gasPool with the given amount. +func NewGasPool(amount uint64) *GasPool { + return &GasPool{ + remaining: amount, + initial: amount, } - *(*uint64)(gp) += amount - return gp } // SubGas deducts the given amount from the pool if enough gas is // available and returns an error otherwise. func (gp *GasPool) SubGas(amount uint64) error { - if uint64(*gp) < amount { + if gp.remaining < amount { return ErrGasLimitReached } - *(*uint64)(gp) -= amount + gp.remaining -= amount + return nil +} + +// ReturnGas adds the refunded gas back to the pool and updates +// the cumulative gas usage accordingly. +func (gp *GasPool) ReturnGas(returned uint64, gasUsed uint64) error { + if gp.remaining > math.MaxUint64-returned { + return fmt.Errorf("%w: remaining: %d, returned: %d", ErrGasLimitOverflow, gp.remaining, returned) + } + // The returned gas calculation differs across forks. + // + // - Pre-Amsterdam: + // returned = purchased - remaining (refund included) + // + // - Post-Amsterdam: + // returned = purchased - gasUsed (refund excluded) + gp.remaining += returned + + // gasUsed = max(txGasUsed - gasRefund, calldataFloorGasCost) + // regardless of Amsterdam is activated or not. + gp.cumulativeUsed += gasUsed return nil } // Gas returns the amount of gas remaining in the pool. func (gp *GasPool) Gas() uint64 { - return uint64(*gp) + return gp.remaining } -// SetGas sets the amount of gas with the provided number. -func (gp *GasPool) SetGas(gas uint64) { - *(*uint64)(gp) = gas +// CumulativeUsed returns the amount of cumulative consumed gas (refunded included). +func (gp *GasPool) CumulativeUsed() uint64 { + return gp.cumulativeUsed +} + +// Used returns the amount of consumed gas. +func (gp *GasPool) Used() uint64 { + if gp.initial < gp.remaining { + panic("gas used underflow") + } + return gp.initial - gp.remaining +} + +// Snapshot returns the deep-copied object as the snapshot. +func (gp *GasPool) Snapshot() *GasPool { + return &GasPool{ + initial: gp.initial, + remaining: gp.remaining, + cumulativeUsed: gp.cumulativeUsed, + } +} + +// Set sets the content of gasPool with the provided one. +func (gp *GasPool) Set(other *GasPool) { + gp.initial = other.initial + gp.remaining = other.remaining + gp.cumulativeUsed = other.cumulativeUsed } func (gp *GasPool) String() string { - return fmt.Sprintf("%d", *gp) + return fmt.Sprintf("initial: %d, remaining: %d, cumulative used: %d", gp.initial, gp.remaining, gp.cumulativeUsed) } diff --git a/core/state_prefetcher.go b/core/state_prefetcher.go index 1c738c1e38..c91d40d94f 100644 --- a/core/state_prefetcher.go +++ b/core/state_prefetcher.go @@ -107,7 +107,7 @@ func (p *statePrefetcher) Prefetch(block *types.Block, statedb *state.StateDB, c // We attempt to apply a transaction. The goal is not to execute // the transaction successfully, rather to warm up touched data slots. - if _, err := ApplyMessage(evm, msg, new(GasPool).AddGas(block.GasLimit())); err != nil { + if _, err := ApplyMessage(evm, msg, nil); err != nil { fails.Add(1) return nil // Ugh, something went horribly wrong, bail out } diff --git a/core/state_processor.go b/core/state_processor.go index e862223d39..9fc6e92752 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -67,15 +67,8 @@ func (p *StateProcessor) Process(ctx context.Context, block *types.Block, stated blockHash = block.Hash() blockNumber = block.Number() allLogs []*types.Log - gp = new(GasPool).AddGas(block.GasLimit()) - // We are maintaining two counters here: - // one counts all the cumulativeGas which includes refunds - // while the other counts only the usedGas which excludes refunds after Amsterdam - // We need the cumulativeGas for receipts and the usedGas for the block gas limit - cumulativeGas = uint64(0) - usedGas = uint64(0) + gp = NewGasPool(block.GasLimit()) ) - var tracingStateDB = vm.StateDB(statedb) if hooks := cfg.Tracer; hooks != nil { tracingStateDB = state.NewHookedState(statedb, hooks) @@ -114,11 +107,10 @@ func (p *StateProcessor) Process(ctx context.Context, block *types.Block, stated ) var receipt *types.Receipt - receipt, cumulativeGas, err = ApplyTransactionWithEVM(msg, gp, statedb, blockNumber, blockHash, context.Time, tx, cumulativeGas, evm) + receipt, err = ApplyTransactionWithEVM(msg, gp, statedb, blockNumber, blockHash, context.Time, tx, evm) if err != nil { return nil, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err) } - usedGas += receipt.GasUsed receipts = append(receipts, receipt) allLogs = append(allLogs, receipt.Logs...) @@ -136,7 +128,7 @@ func (p *StateProcessor) Process(ctx context.Context, block *types.Block, stated Receipts: receipts, Requests: requests, Logs: allLogs, - GasUsed: usedGas, + GasUsed: gp.Used(), }, nil } @@ -168,7 +160,7 @@ func postExecution(ctx context.Context, config *params.ChainConfig, block *types // ApplyTransactionWithEVM attempts to apply a transaction to the given state database // and uses the input parameters for its environment similar to ApplyTransaction. However, // this method takes an already created EVM instance as input. -func ApplyTransactionWithEVM(msg *Message, gp *GasPool, statedb *state.StateDB, blockNumber *big.Int, blockHash common.Hash, blockTime uint64, tx *types.Transaction, cumulativeGas uint64, evm *vm.EVM) (receipt *types.Receipt, cGas uint64, err error) { +func ApplyTransactionWithEVM(msg *Message, gp *GasPool, statedb *state.StateDB, blockNumber *big.Int, blockHash common.Hash, blockTime uint64, tx *types.Transaction, evm *vm.EVM) (receipt *types.Receipt, err error) { if hooks := evm.Config.Tracer; hooks != nil { if hooks.OnTxStart != nil { hooks.OnTxStart(evm.GetVMContext(), tx, msg.From) @@ -180,7 +172,7 @@ func ApplyTransactionWithEVM(msg *Message, gp *GasPool, statedb *state.StateDB, // Apply the transaction to the current state (included in the env). result, err := ApplyMessage(evm, msg, gp) if err != nil { - return nil, cumulativeGas, err + return nil, err } // Update the state with pending changes. var root []byte @@ -189,20 +181,21 @@ func ApplyTransactionWithEVM(msg *Message, gp *GasPool, statedb *state.StateDB, } else { root = statedb.IntermediateRoot(evm.ChainConfig().IsEIP158(blockNumber)).Bytes() } - cGas = cumulativeGas + result.UsedGas - // Merge the tx-local access event into the "block-local" one, in order to collect // all values, so that the witness can be built. if statedb.Database().TrieDB().IsVerkle() { statedb.AccessEvents().Merge(evm.AccessEvents) } - return MakeReceipt(evm, result, statedb, blockNumber, blockHash, blockTime, tx, cGas, root), cGas, nil + return MakeReceipt(evm, result, statedb, blockNumber, blockHash, blockTime, tx, gp.CumulativeUsed(), root), nil } // MakeReceipt generates the receipt object for a transaction given its execution result. func MakeReceipt(evm *vm.EVM, result *ExecutionResult, statedb *state.StateDB, blockNumber *big.Int, blockHash common.Hash, blockTime uint64, tx *types.Transaction, cumulativeGas uint64, root []byte) *types.Receipt { - // Create a new receipt for the transaction, storing the intermediate root and gas used - // by the tx. + // Create a new receipt for the transaction, storing the intermediate root + // and gas used by the tx. + // + // The cumulative gas used equals the sum of gasUsed across all preceding + // txs with refunded gas deducted. receipt := &types.Receipt{Type: tx.Type(), PostState: root, CumulativeGasUsed: cumulativeGas} if result.Failed() { receipt.Status = types.ReceiptStatusFailed @@ -210,11 +203,10 @@ func MakeReceipt(evm *vm.EVM, result *ExecutionResult, statedb *state.StateDB, b receipt.Status = types.ReceiptStatusSuccessful } receipt.TxHash = tx.Hash() - if evm.ChainConfig().IsAmsterdam(blockNumber, blockTime) { - receipt.GasUsed = result.MaxUsedGas - } else { - receipt.GasUsed = result.UsedGas - } + + // GasUsed = max(tx_gas_used - gas_refund, calldata_floor_gas_cost), unchanged + // in the Amsterdam fork. + receipt.GasUsed = result.UsedGas if tx.Type() == types.BlobTxType { receipt.BlobGasUsed = uint64(len(tx.BlobHashes()) * params.BlobTxBlobGasPerBlob) @@ -239,13 +231,13 @@ func MakeReceipt(evm *vm.EVM, result *ExecutionResult, statedb *state.StateDB, b // and uses the input parameters for its environment. It returns the receipt // for the transaction and an error if the transaction failed, // indicating the block was invalid. -func ApplyTransaction(evm *vm.EVM, gp *GasPool, statedb *state.StateDB, header *types.Header, tx *types.Transaction, cumulativeGas uint64) (*types.Receipt, uint64, error) { +func ApplyTransaction(evm *vm.EVM, gp *GasPool, statedb *state.StateDB, header *types.Header, tx *types.Transaction) (*types.Receipt, error) { msg, err := TransactionToMessage(tx, types.MakeSigner(evm.ChainConfig(), header.Number, header.Time), header.BaseFee) if err != nil { - return nil, cumulativeGas, err + return nil, err } // Create a new context to be used in the EVM environment - return ApplyTransactionWithEVM(msg, gp, statedb, header.Number, header.Hash(), header.Time, tx, cumulativeGas, evm) + return ApplyTransactionWithEVM(msg, gp, statedb, header.Number, header.Hash(), header.Time, tx, evm) } // ProcessBeaconBlockRoot applies the EIP-4788 system call to the beacon block root diff --git a/core/state_transition.go b/core/state_transition.go index 095bf17b41..76a5147363 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -34,7 +34,7 @@ import ( // ExecutionResult includes all output after executing given evm // message no matter the execution itself is successful or not. type ExecutionResult struct { - UsedGas uint64 // Total used gas, not including the refunded gas + UsedGas uint64 // Total used gas, refunded gas is deducted MaxUsedGas uint64 // Maximum gas consumed during execution, excluding gas refunds. Err error // Any error encountered during the execution(listed in core/vm/errors.go) ReturnData []byte // Returned data from evm(function result or data supplied with revert opcode) @@ -210,6 +210,11 @@ func TransactionToMessage(tx *types.Transaction, s types.Signer, baseFee *big.In // indicates a core error meaning that the message would always fail for that particular // state and would never be accepted within a block. func ApplyMessage(evm *vm.EVM, msg *Message, gp *GasPool) (*ExecutionResult, error) { + // Do not panic if the gas pool is nil. This is allowed when executing + // a single message via RPC invocation. + if gp == nil { + gp = NewGasPool(msg.GasLimit) + } evm.SetTxContext(NewEVMTxContext(msg)) return newStateTransition(evm, msg, gp).execute() } @@ -300,8 +305,8 @@ func (st *stateTransition) buyGas() error { st.evm.Config.Tracer.OnGasChange(0, st.msg.GasLimit, tracing.GasChangeTxInitialBalance) } st.gasRemaining = st.msg.GasLimit - st.initialGas = st.msg.GasLimit + mgvalU256, _ := uint256.FromBig(mgval) st.state.SubBalance(st.msg.From, mgvalU256, tracing.BalanceDecreaseGasBuy) return nil @@ -544,13 +549,18 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { } // Return gas to the user st.returnGas() + // Return gas to the gas pool if rules.IsAmsterdam { - st.gp.AddGas(st.initialGas - peakGasUsed) + // Refund is excluded for returning + err = st.gp.ReturnGas(st.initialGas-peakGasUsed, st.gasUsed()) } else { - st.gp.AddGas(st.gasRemaining) + // Refund is included for returning + err = st.gp.ReturnGas(st.gasRemaining, st.gasUsed()) + } + if err != nil { + return nil, err } - effectiveTip := msg.GasPrice if rules.IsLondon { effectiveTip = new(big.Int).Sub(msg.GasPrice, st.evm.Context.BaseFee) @@ -571,7 +581,6 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { st.evm.AccessEvents.AddAccount(st.evm.Context.Coinbase, true, math.MaxUint64) } } - return &ExecutionResult{ UsedGas: st.gasUsed(), MaxUsedGas: peakGasUsed, diff --git a/eth/gasestimator/gasestimator.go b/eth/gasestimator/gasestimator.go index 6e79fbd62b..80aeb3d3b2 100644 --- a/eth/gasestimator/gasestimator.go +++ b/eth/gasestimator/gasestimator.go @@ -20,7 +20,6 @@ import ( "context" "errors" "fmt" - "math" "math/big" "github.com/ethereum/go-ethereum/common" @@ -268,7 +267,7 @@ func run(ctx context.Context, call *core.Message, opts *Options) (*core.Executio evm.Cancel() }() // Execute the call, returning a wrapped error or the result - result, err := core.ApplyMessage(evm, call, new(core.GasPool).AddGas(math.MaxUint64)) + result, err := core.ApplyMessage(evm, call, nil) if vmerr := dirtyState.Error(); vmerr != nil { return nil, vmerr } diff --git a/eth/state_accessor.go b/eth/state_accessor.go index 1261320b58..871f2c9269 100644 --- a/eth/state_accessor.go +++ b/eth/state_accessor.go @@ -265,7 +265,7 @@ func (eth *Ethereum) stateAtTransaction(ctx context.Context, block *types.Block, // Not yet the searched for transaction, execute on top of the current state statedb.SetTxContext(tx.Hash(), idx) - if _, err := core.ApplyMessage(evm, msg, new(core.GasPool).AddGas(tx.Gas())); err != nil { + if _, err := core.ApplyMessage(evm, msg, nil); err != nil { return nil, vm.BlockContext{}, nil, nil, fmt.Errorf("transaction %#x failed: %v", tx.Hash(), err) } // Ensure any modifications are committed to the state diff --git a/eth/tracers/api.go b/eth/tracers/api.go index d8e2128bc2..eed404622e 100644 --- a/eth/tracers/api.go +++ b/eth/tracers/api.go @@ -551,7 +551,7 @@ func (api *API) IntermediateRoots(ctx context.Context, hash common.Hash, config } msg, _ := core.TransactionToMessage(tx, signer, block.BaseFee()) statedb.SetTxContext(tx.Hash(), i) - if _, err := core.ApplyMessage(evm, msg, new(core.GasPool).AddGas(msg.GasLimit)); err != nil { + if _, err := core.ApplyMessage(evm, msg, nil); err != nil { log.Warn("Tracing intermediate roots did not complete", "txindex", i, "txhash", tx.Hash(), "err", err) // We intentionally don't return the error here: if we do, then the RPC server will not // return the roots. Most likely, the caller already knows that a certain transaction fails to @@ -707,7 +707,7 @@ txloop: // Generate the next state snapshot fast without tracing msg, _ := core.TransactionToMessage(tx, signer, block.BaseFee()) statedb.SetTxContext(tx.Hash(), i) - if _, err := core.ApplyMessage(evm, msg, new(core.GasPool).AddGas(msg.GasLimit)); err != nil { + if _, err := core.ApplyMessage(evm, msg, nil); err != nil { failed = err break txloop } @@ -792,7 +792,7 @@ func (api *API) standardTraceBlockToFile(ctx context.Context, block *types.Block msg, _ := core.TransactionToMessage(tx, signer, block.BaseFee()) if txHash != (common.Hash{}) && tx.Hash() != txHash { // Process the tx to update state, but don't trace it. - _, err := core.ApplyMessage(evm, msg, new(core.GasPool).AddGas(msg.GasLimit)) + _, err := core.ApplyMessage(evm, msg, nil) if err != nil { return dumps, err } @@ -827,7 +827,7 @@ func (api *API) standardTraceBlockToFile(ctx context.Context, block *types.Block if tracer.OnTxStart != nil { tracer.OnTxStart(evm.GetVMContext(), tx, msg.From) } - _, err = core.ApplyMessage(evm, msg, new(core.GasPool).AddGas(msg.GasLimit)) + _, err = core.ApplyMessage(evm, msg, nil) if writer != nil { writer.Flush() } @@ -1011,7 +1011,6 @@ func (api *API) traceTx(ctx context.Context, tx *types.Transaction, message *cor tracer *Tracer err error timeout = defaultTraceTimeout - usedGas uint64 ) if config == nil { config = &TraceConfig{} @@ -1055,7 +1054,8 @@ func (api *API) traceTx(ctx context.Context, tx *types.Transaction, message *cor // Call Prepare to clear out the statedb access list statedb.SetTxContext(txctx.TxHash, txctx.TxIndex) - _, _, err = core.ApplyTransactionWithEVM(message, new(core.GasPool).AddGas(message.GasLimit), statedb, vmctx.BlockNumber, txctx.BlockHash, vmctx.Time, tx, usedGas, evm) + + _, err = core.ApplyTransactionWithEVM(message, core.NewGasPool(message.GasLimit), statedb, vmctx.BlockNumber, txctx.BlockHash, vmctx.Time, tx, evm) if err != nil { return nil, fmt.Errorf("tracing failed: %w", err) } diff --git a/eth/tracers/api_test.go b/eth/tracers/api_test.go index f76c35a1d5..1d5024ad08 100644 --- a/eth/tracers/api_test.go +++ b/eth/tracers/api_test.go @@ -188,7 +188,7 @@ func (b *testBackend) StateAtTransaction(ctx context.Context, block *types.Block return tx, context, statedb, release, nil } msg, _ := core.TransactionToMessage(tx, signer, block.BaseFee()) - if _, err := core.ApplyMessage(evm, msg, new(core.GasPool).AddGas(tx.Gas())); err != nil { + if _, err := core.ApplyMessage(evm, msg, nil); err != nil { return nil, vm.BlockContext{}, nil, nil, fmt.Errorf("transaction %#x failed: %v", tx.Hash(), err) } statedb.Finalise(evm.ChainConfig().IsEIP158(block.Number())) diff --git a/eth/tracers/internal/tracetest/calltrace_test.go b/eth/tracers/internal/tracetest/calltrace_test.go index 08bdafd91f..85eaef32ce 100644 --- a/eth/tracers/internal/tracetest/calltrace_test.go +++ b/eth/tracers/internal/tracetest/calltrace_test.go @@ -132,7 +132,7 @@ func testCallTracer(tracerName string, dirPath string, t *testing.T) { } evm := vm.NewEVM(context, logState, test.Genesis.Config, vm.Config{Tracer: tracer.Hooks}) tracer.OnTxStart(evm.GetVMContext(), tx, msg.From) - vmRet, err := core.ApplyMessage(evm, msg, new(core.GasPool).AddGas(tx.Gas())) + vmRet, err := core.ApplyMessage(evm, msg, nil) if err != nil { t.Fatalf("failed to execute transaction: %v", err) } @@ -224,7 +224,7 @@ func benchTracer(tracerName string, test *callTracerTest, b *testing.B) { if tracer.OnTxStart != nil { tracer.OnTxStart(evm.GetVMContext(), tx, msg.From) } - _, err = core.ApplyMessage(evm, msg, new(core.GasPool).AddGas(tx.Gas())) + _, err = core.ApplyMessage(evm, msg, nil) if err != nil { b.Fatalf("failed to execute transaction: %v", err) } @@ -374,7 +374,7 @@ func TestInternals(t *testing.T) { t.Fatalf("test %v: failed to create message: %v", tc.name, err) } tc.tracer.OnTxStart(evm.GetVMContext(), tx, msg.From) - vmRet, err := core.ApplyMessage(evm, msg, new(core.GasPool).AddGas(tx.Gas())) + vmRet, err := core.ApplyMessage(evm, msg, nil) if err != nil { t.Fatalf("test %v: failed to execute transaction: %v", tc.name, err) } diff --git a/eth/tracers/internal/tracetest/erc7562_tracer_test.go b/eth/tracers/internal/tracetest/erc7562_tracer_test.go index f6e81f5886..02377b8dcb 100644 --- a/eth/tracers/internal/tracetest/erc7562_tracer_test.go +++ b/eth/tracers/internal/tracetest/erc7562_tracer_test.go @@ -124,7 +124,7 @@ func TestErc7562Tracer(t *testing.T) { } evm := vm.NewEVM(context, logState, test.Genesis.Config, vm.Config{Tracer: tracer.Hooks}) tracer.OnTxStart(evm.GetVMContext(), tx, msg.From) - vmRet, err := core.ApplyMessage(evm, msg, new(core.GasPool).AddGas(tx.Gas())) + vmRet, err := core.ApplyMessage(evm, msg, nil) if err != nil { t.Fatalf("failed to execute transaction: %v", err) } diff --git a/eth/tracers/internal/tracetest/flat_calltrace_test.go b/eth/tracers/internal/tracetest/flat_calltrace_test.go index 1882ef315e..37a05966ee 100644 --- a/eth/tracers/internal/tracetest/flat_calltrace_test.go +++ b/eth/tracers/internal/tracetest/flat_calltrace_test.go @@ -113,7 +113,7 @@ func flatCallTracerTestRunner(tracerName string, filename string, dirPath string } evm := vm.NewEVM(context, state.StateDB, test.Genesis.Config, vm.Config{Tracer: tracer.Hooks}) tracer.OnTxStart(evm.GetVMContext(), tx, msg.From) - vmRet, err := core.ApplyMessage(evm, msg, new(core.GasPool).AddGas(tx.Gas())) + vmRet, err := core.ApplyMessage(evm, msg, nil) if err != nil { return fmt.Errorf("failed to execute transaction: %v", err) } diff --git a/eth/tracers/internal/tracetest/prestate_test.go b/eth/tracers/internal/tracetest/prestate_test.go index 456d962c69..23216fa78c 100644 --- a/eth/tracers/internal/tracetest/prestate_test.go +++ b/eth/tracers/internal/tracetest/prestate_test.go @@ -105,7 +105,7 @@ func testPrestateTracer(tracerName string, dirPath string, t *testing.T) { } evm := vm.NewEVM(context, state.StateDB, test.Genesis.Config, vm.Config{Tracer: tracer.Hooks}) tracer.OnTxStart(evm.GetVMContext(), tx, msg.From) - vmRet, err := core.ApplyMessage(evm, msg, new(core.GasPool).AddGas(tx.Gas())) + vmRet, err := core.ApplyMessage(evm, msg, nil) if err != nil { t.Fatalf("failed to execute transaction: %v", err) } diff --git a/eth/tracers/internal/tracetest/selfdestruct_state_test.go b/eth/tracers/internal/tracetest/selfdestruct_state_test.go index 53ea8ab63f..bb1a3d9f18 100644 --- a/eth/tracers/internal/tracetest/selfdestruct_state_test.go +++ b/eth/tracers/internal/tracetest/selfdestruct_state_test.go @@ -620,7 +620,7 @@ func TestSelfdestructStateTracer(t *testing.T) { } context := core.NewEVMBlockContext(block.Header(), blockchain, nil) evm := vm.NewEVM(context, hookedState, tt.genesis.Config, vm.Config{Tracer: tracer.Hooks()}) - _, _, err = core.ApplyTransactionWithEVM(msg, new(core.GasPool).AddGas(tx.Gas()), statedb, block.Number(), block.Hash(), block.Time(), tx, uint64(0), evm) + _, err = core.ApplyTransactionWithEVM(msg, core.NewGasPool(msg.GasLimit), statedb, block.Number(), block.Hash(), block.Time(), tx, evm) if err != nil { t.Fatalf("failed to execute transaction: %v", err) } diff --git a/eth/tracers/tracers_test.go b/eth/tracers/tracers_test.go index 06edeaf698..24f9b3701e 100644 --- a/eth/tracers/tracers_test.go +++ b/eth/tracers/tracers_test.go @@ -91,7 +91,7 @@ func BenchmarkTransactionTraceV2(b *testing.B) { evm.Config.Tracer = tracer snap := state.StateDB.Snapshot() - _, err := core.ApplyMessage(evm, msg, new(core.GasPool).AddGas(tx.Gas())) + _, err := core.ApplyMessage(evm, msg, nil) if err != nil { b.Fatal(err) } diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 4f3071cb03..ff6797f67b 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -741,11 +741,10 @@ func doCall(ctx context.Context, b Backend, args TransactionArgs, state *state.S // Make sure the context is cancelled when the call has completed // this makes sure resources are cleaned up. defer cancel() - gp := new(core.GasPool) + + gp := core.NewGasPool(globalGasCap) if globalGasCap == 0 { - gp.AddGas(gomath.MaxUint64) - } else { - gp.AddGas(globalGasCap) + gp = core.NewGasPool(gomath.MaxUint64) } return applyMessage(ctx, b, args, state, header, timeout, gp, &blockCtx, &vm.Config{NoBaseFee: true}, precompiles) } @@ -855,12 +854,11 @@ func (api *BlockChainAPI) SimulateV1(ctx context.Context, opts simOpts, blockNrO gasCap = gomath.MaxUint64 } sim := &simulator{ - b: api.b, - state: state, - base: base, - chainConfig: api.b.ChainConfig(), - // Each tx and all the series of txes shouldn't consume more gas than cap - gp: new(core.GasPool).AddGas(gasCap), + b: api.b, + state: state, + base: base, + chainConfig: api.b.ChainConfig(), + gasRemaining: gasCap, traceTransfers: opts.TraceTransfers, validate: opts.Validation, fullTx: opts.ReturnFullTransactions, @@ -1369,7 +1367,7 @@ func AccessList(ctx context.Context, b Backend, blockNrOrHash rpc.BlockNumberOrH if msg.BlobGasFeeCap != nil && msg.BlobGasFeeCap.BitLen() == 0 { evm.Context.BlobBaseFee = new(big.Int) } - res, err := core.ApplyMessage(evm, msg, new(core.GasPool).AddGas(msg.GasLimit)) + res, err := core.ApplyMessage(evm, msg, nil) 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/api_test.go b/internal/ethapi/api_test.go index 2f0c07694d..a82df440e6 100644 --- a/internal/ethapi/api_test.go +++ b/internal/ethapi/api_test.go @@ -2507,7 +2507,7 @@ func TestSimulateV1ChainLinkage(t *testing.T) { state: stateDB, base: baseHeader, chainConfig: backend.ChainConfig(), - gp: new(core.GasPool).AddGas(math.MaxUint64), + gasRemaining: math.MaxUint64, traceTransfers: false, validate: false, fullTx: false, @@ -2592,7 +2592,7 @@ func TestSimulateV1TxSender(t *testing.T) { state: stateDB, base: baseHeader, chainConfig: backend.ChainConfig(), - gp: new(core.GasPool).AddGas(math.MaxUint64), + gasRemaining: math.MaxUint64, traceTransfers: false, validate: false, fullTx: true, diff --git a/internal/ethapi/simulate.go b/internal/ethapi/simulate.go index c9396cd327..325ee6d5bb 100644 --- a/internal/ethapi/simulate.go +++ b/internal/ethapi/simulate.go @@ -156,7 +156,7 @@ type simulator struct { state *state.StateDB base *types.Header chainConfig *params.ChainConfig - gp *core.GasPool + gasRemaining uint64 traceTransfers bool validate bool fullTx bool @@ -200,7 +200,13 @@ func (sim *simulator) execute(ctx context.Context, blocks []simBlock) ([]*simBlo return nil, err } headers[bi] = result.Header() - results[bi] = &simBlockResult{fullTx: sim.fullTx, chainConfig: sim.chainConfig, Block: result, Calls: callResults, senders: senders} + results[bi] = &simBlockResult{ + fullTx: sim.fullTx, + chainConfig: sim.chainConfig, + Block: result, + Calls: callResults, + senders: senders, + } parent = result.Header() } return results, nil @@ -234,15 +240,19 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header, blockContext.BlobBaseFee = block.BlockOverrides.BlobBaseFee.ToInt() } precompiles := sim.activePrecompiles(header) + // State overrides are applied prior to execution of a block if err := block.StateOverrides.Apply(sim.state, precompiles); err != nil { return nil, nil, nil, err } var ( - gasUsed, blobGasUsed uint64 - txes = make([]*types.Transaction, len(block.Calls)) - callResults = make([]simCallResult, len(block.Calls)) - receipts = make([]*types.Receipt, len(block.Calls)) + gp = core.NewGasPool(blockContext.GasLimit) + blobGasUsed uint64 + + txes = make([]*types.Transaction, len(block.Calls)) + callResults = make([]simCallResult, len(block.Calls)) + receipts = make([]*types.Receipt, len(block.Calls)) + // Block hash will be repaired after execution. tracer = newTracer(sim.traceTransfers, blockContext.BlockNumber.Uint64(), blockContext.Time, common.Hash{}, common.Hash{}, 0) vmConfig = &vm.Config{ @@ -272,10 +282,11 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header, } var allLogs []*types.Log for i, call := range block.Calls { + // Terminate if the context is cancelled if err := ctx.Err(); err != nil { return nil, nil, nil, err } - if err := sim.sanitizeCall(&call, sim.state, header, blockContext, &gasUsed); err != nil { + if err := sim.sanitizeCall(&call, sim.state, header, gp); err != nil { return nil, nil, nil, err } var ( @@ -285,10 +296,11 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header, txes[i] = tx senders[txHash] = call.from() tracer.reset(txHash, uint(i)) - sim.state.SetTxContext(txHash, i) + // EoA check is always skipped, even in validation mode. + sim.state.SetTxContext(txHash, i) msg := call.ToMessage(header.BaseFee, !sim.validate) - result, err := applyMessageWithEVM(ctx, evm, msg, timeout, sim.gp) + result, err := applyMessageWithEVM(ctx, evm, msg, timeout, gp) if err != nil { txErr := txValidationError(err) return nil, nil, nil, txErr @@ -300,9 +312,16 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header, } else { root = sim.state.IntermediateRoot(sim.chainConfig.IsEIP158(blockContext.BlockNumber)).Bytes() } - gasUsed += result.UsedGas - receipts[i] = core.MakeReceipt(evm, result, sim.state, blockContext.BlockNumber, common.Hash{}, blockContext.Time, tx, gasUsed, root) + receipts[i] = core.MakeReceipt(evm, result, sim.state, blockContext.BlockNumber, common.Hash{}, blockContext.Time, tx, gp.CumulativeUsed(), root) blobGasUsed += receipts[i].BlobGasUsed + + // Make sure the gas cap is still enforced. It's only for + // internally protection. + if sim.gasRemaining < result.UsedGas { + return nil, nil, nil, fmt.Errorf("gas cap reached, required: %d, remaining: %d", result.UsedGas, sim.gasRemaining) + } + sim.gasRemaining -= result.UsedGas + logs := tracer.Logs() callRes := simCallResult{ReturnValue: result.Return(), Logs: logs, GasUsed: hexutil.Uint64(result.UsedGas)} if result.Failed() { @@ -320,12 +339,14 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header, } callResults[i] = callRes } - header.GasUsed = gasUsed + // Assign total consumed gas to the header + header.GasUsed = gp.Used() if sim.chainConfig.IsCancun(header.Number, header.Time) { header.BlobGasUsed = &blobGasUsed } - var requests [][]byte + // Process EIP-7685 requests + var requests [][]byte if sim.chainConfig.IsPrague(header.Number, header.Time) { requests = [][]byte{} // EIP-6110 @@ -345,7 +366,11 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header, reqHash := types.CalcRequestsHash(requests) header.RequestsHash = &reqHash } - blockBody := &types.Body{Transactions: txes, Withdrawals: *block.BlockOverrides.Withdrawals} + + blockBody := &types.Body{ + Transactions: txes, + Withdrawals: *block.BlockOverrides.Withdrawals, + } chainHeadReader := &simChainHeadReader{ctx, sim.b} b, err := sim.b.Engine().FinalizeAndAssemble(chainHeadReader, header, sim.state, blockBody, receipts) if err != nil { @@ -366,23 +391,20 @@ func repairLogs(calls []simCallResult, hash common.Hash) { } } -func (sim *simulator) sanitizeCall(call *TransactionArgs, state vm.StateDB, header *types.Header, blockContext vm.BlockContext, gasUsed *uint64) error { +func (sim *simulator) sanitizeCall(call *TransactionArgs, state vm.StateDB, header *types.Header, gp *core.GasPool) error { if call.Nonce == nil { nonce := state.GetNonce(call.from()) call.Nonce = (*hexutil.Uint64)(&nonce) } // Let the call run wild unless explicitly specified. + remaining := gp.Gas() if call.Gas == nil { - remaining := blockContext.GasLimit - *gasUsed call.Gas = (*hexutil.Uint64)(&remaining) } - if *gasUsed+uint64(*call.Gas) > blockContext.GasLimit { - return &blockGasLimitReachedError{fmt.Sprintf("block gas limit reached: %d >= %d", *gasUsed, blockContext.GasLimit)} + if remaining < uint64(*call.Gas) { + return &blockGasLimitReachedError{fmt.Sprintf("block gas limit reached: remaining: %d, required: %d", remaining, *call.Gas)} } - if err := call.CallDefaults(sim.gp.Gas(), header.BaseFee, sim.chainConfig.ChainID); err != nil { - return err - } - return nil + return call.CallDefaults(0, header.BaseFee, sim.chainConfig.ChainID) } func (sim *simulator) activePrecompiles(base *types.Header) vm.PrecompiledContracts { @@ -473,12 +495,14 @@ func (sim *simulator) makeHeaders(blocks []simBlock) ([]*types.Header, error) { } overrides := block.BlockOverrides - var withdrawalsHash *common.Hash number := overrides.Number.ToInt() timestamp := (uint64)(*overrides.Time) + + var withdrawalsHash *common.Hash if sim.chainConfig.IsShanghai(number, timestamp) { withdrawalsHash = &types.EmptyWithdrawalsHash } + var parentBeaconRoot *common.Hash if sim.chainConfig.IsCancun(number, timestamp) { parentBeaconRoot = &common.Hash{} @@ -508,7 +532,11 @@ func (sim *simulator) makeHeaders(blocks []simBlock) ([]*types.Header, error) { } func (sim *simulator) newSimulatedChainContext(ctx context.Context, headers []*types.Header) *ChainContext { - return NewChainContext(ctx, &simBackend{base: sim.base, b: sim.b, headers: headers}) + return NewChainContext(ctx, &simBackend{ + base: sim.base, + b: sim.b, + headers: headers, + }) } type simBackend struct { diff --git a/internal/ethapi/simulate_test.go b/internal/ethapi/simulate_test.go index c747b76477..b6037a8f35 100644 --- a/internal/ethapi/simulate_test.go +++ b/internal/ethapi/simulate_test.go @@ -17,6 +17,7 @@ package ethapi import ( + "math" "math/big" "testing" @@ -80,7 +81,10 @@ func TestSimulateSanitizeBlockOrder(t *testing.T) { err: "block timestamps must be in order: 72 <= 72", }, } { - sim := &simulator{base: &types.Header{Number: big.NewInt(int64(tc.baseNumber)), Time: tc.baseTimestamp}} + sim := &simulator{ + base: &types.Header{Number: big.NewInt(int64(tc.baseNumber)), Time: tc.baseTimestamp}, + gasRemaining: math.MaxUint64, + } res, err := sim.sanitizeChain(tc.blocks) if err != nil { if err.Error() == tc.err { diff --git a/miner/worker.go b/miner/worker.go index 93b1a081c3..3e7df2b6b0 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -56,14 +56,13 @@ func (miner *Miner) maxBlobsPerBlock(time uint64) int { // environment is the worker's current environment and holds all // information of the sealing block generation. type environment struct { - signer types.Signer - state *state.StateDB // apply state changes here - tcount int // tx count in cycle - size uint64 // size of the block we are building - gasPool *core.GasPool // available gas used to pack transactions - coinbase common.Address - evm *vm.EVM - cumulativeGas uint64 + signer types.Signer + state *state.StateDB // apply state changes here + tcount int // tx count in cycle + size uint64 // size of the block we are building + gasPool *core.GasPool // available gas used to pack transactions + coinbase common.Address + evm *vm.EVM header *types.Header txs []*types.Transaction @@ -320,6 +319,7 @@ func (miner *Miner) makeEnv(parent *types.Header, header *types.Header, coinbase state: state, size: uint64(header.Size()), coinbase: coinbase, + gasPool: core.NewGasPool(header.GasLimit), header: header, witness: state.Witness(), evm: vm.NewEVM(core.NewEVMBlockContext(header, miner.chain, &coinbase), state, miner.chainConfig, vm.Config{}), @@ -373,27 +373,20 @@ func (miner *Miner) commitBlobTransaction(env *environment, tx *types.Transactio func (miner *Miner) applyTransaction(env *environment, tx *types.Transaction) (*types.Receipt, error) { var ( snap = env.state.Snapshot() - gp = env.gasPool.Gas() + gp = env.gasPool.Snapshot() ) - receipt, cumulativeGas, err := core.ApplyTransaction(env.evm, env.gasPool, env.state, env.header, tx, env.cumulativeGas) + receipt, err := core.ApplyTransaction(env.evm, env.gasPool, env.state, env.header, tx) if err != nil { env.state.RevertToSnapshot(snap) - env.gasPool.SetGas(gp) + env.gasPool.Set(gp) return nil, err } - env.cumulativeGas = cumulativeGas - env.header.GasUsed += receipt.GasUsed + env.header.GasUsed = env.gasPool.Used() return receipt, nil } func (miner *Miner) commitTransactions(env *environment, plainTxs, blobTxs *transactionsByPriceAndNonce, interrupt *atomic.Int32) error { - var ( - isCancun = miner.chainConfig.IsCancun(env.header.Number, env.header.Time) - gasLimit = env.header.GasLimit - ) - if env.gasPool == nil { - env.gasPool = new(core.GasPool).AddGas(gasLimit) - } + isCancun := miner.chainConfig.IsCancun(env.header.Number, env.header.Time) for { // Check interruption signal and abort building if it's fired. if interrupt != nil { diff --git a/tests/state_test_util.go b/tests/state_test_util.go index 7525081f84..3c7ee1c31d 100644 --- a/tests/state_test_util.go +++ b/tests/state_test_util.go @@ -342,9 +342,7 @@ func (t *StateTest) RunNoVerify(subtest StateSubtest, vmconfig vm.Config, snapsh } // Execute the message. snapshot := st.StateDB.Snapshot() - gaspool := new(core.GasPool) - gaspool.AddGas(block.GasLimit()) - vmRet, err := core.ApplyMessage(evm, msg, gaspool) + vmRet, err := core.ApplyMessage(evm, msg, core.NewGasPool(block.GasLimit())) if err != nil { st.StateDB.RevertToSnapshot(snapshot) if tracer := evm.Config.Tracer; tracer != nil && tracer.OnTxEnd != nil {