mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-06-20 13:44:31 +00:00
feat: vm.Hooks.PreprocessingGasCharge() (#235)
## Why this should be merged In lieu of modifications to `core.IntrinsicGas()`, required for support of Warp in SAE. If we were to introduce variadic `options.Option`s to `IntrinsicGas()`, it's impossible to guarantee that they would always be passed. The `types.Transaction` itself isn't actually passed to `IntrinsicGas()` either, so we can't rely on it to carry payloads/hooks. ## How this works `vm.Hooks` are extended to include `PreprocessingGasCharge(tx common.Hash) (uint64, error)`, which returns the amount of gas to be charged between `core.IntrinsicGas` and `vm.EVM` execution charges. The two entry points to execution, `vm.EVM.Call()` and `vm.EVM.Create()` are modified to first spend said charge before running upstream logic. The new hook is defined on a separate interface definition, embedded in `vm.Hooks`, to allow types to implement just that method and enforce it with the `var _ vm.Preprocessor = (*impl)(nil)` pattern. ## How this was tested Integration tests via both `core.ApplyTransaction()` and `core.ApplyMessage()`. The former is more comprehensive, while the latter allows for inspection of interim error values. --------- Signed-off-by: Arran Schlosberg <519948+ARR4N@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Austin Larson <78000745+alarso16@users.noreply.github.com>
This commit is contained in:
parent
865e03ca6a
commit
9021836d7d
9 changed files with 353 additions and 6 deletions
|
|
@ -187,7 +187,7 @@ func (evm *EVM) Interpreter() *EVMInterpreter {
|
|||
// parameters. It also handles any necessary value transfer required and takes
|
||||
// the necessary steps to create accounts and reverses the state in case of an
|
||||
// execution error or failed value transfer.
|
||||
func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas uint64, value *uint256.Int) (ret []byte, leftOverGas uint64, err error) {
|
||||
func (evm *EVM) call(caller ContractRef, addr common.Address, input []byte, gas uint64, value *uint256.Int) (ret []byte, leftOverGas uint64, err error) {
|
||||
// Fail if we're trying to execute above the call depth limit
|
||||
if evm.depth > int(params.CallCreateDepth) {
|
||||
return nil, gas, ErrDepth
|
||||
|
|
@ -433,8 +433,8 @@ func (c *codeAndHash) Hash() common.Hash {
|
|||
return c.hash
|
||||
}
|
||||
|
||||
// create creates a new contract using code as deployment code.
|
||||
func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, value *uint256.Int, address common.Address, typ OpCode) ([]byte, common.Address, uint64, error) {
|
||||
// createCommon creates a new contract using code as deployment code.
|
||||
func (evm *EVM) createCommon(caller ContractRef, codeAndHash *codeAndHash, gas uint64, value *uint256.Int, address common.Address, typ OpCode) ([]byte, common.Address, uint64, error) {
|
||||
// Depth check execution. Fail if we're trying to execute above the
|
||||
// limit.
|
||||
if evm.depth > int(params.CallCreateDepth) {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@
|
|||
package vm
|
||||
|
||||
import (
|
||||
"github.com/holiman/uint256"
|
||||
|
||||
"github.com/ava-labs/libevm/common"
|
||||
"github.com/ava-labs/libevm/libevm"
|
||||
"github.com/ava-labs/libevm/log"
|
||||
|
|
@ -52,6 +54,42 @@ func (evm *EVM) canCreateContract(caller ContractRef, contractToCreate common.Ad
|
|||
return gas, err
|
||||
}
|
||||
|
||||
// Call executes the contract associated with the addr with the given input as
|
||||
// parameters. It also handles any necessary value transfer required and takes
|
||||
// the necessary steps to create accounts and reverses the state in case of an
|
||||
// execution error or failed value transfer.
|
||||
func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas uint64, value *uint256.Int) (ret []byte, leftOverGas uint64, err error) {
|
||||
gas, err = evm.spendPreprocessingGas(gas)
|
||||
if err != nil {
|
||||
return nil, gas, err
|
||||
}
|
||||
return evm.call(caller, addr, input, gas, value)
|
||||
}
|
||||
|
||||
// create wraps the original geth method of the same name, now named
|
||||
// [EVM.createCommon], first spending preprocessing gas.
|
||||
func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, value *uint256.Int, address common.Address, typ OpCode) ([]byte, common.Address, uint64, error) {
|
||||
gas, err := evm.spendPreprocessingGas(gas)
|
||||
if err != nil {
|
||||
return nil, common.Address{}, gas, err
|
||||
}
|
||||
return evm.createCommon(caller, codeAndHash, gas, value, address, typ)
|
||||
}
|
||||
|
||||
func (evm *EVM) spendPreprocessingGas(gas uint64) (uint64, error) {
|
||||
if internalCall := evm.depth > 0; internalCall || !libevmHooks.Registered() {
|
||||
return gas, nil
|
||||
}
|
||||
c, err := libevmHooks.Get().PreprocessingGasCharge(evm.StateDB.TxHash())
|
||||
if err != nil {
|
||||
return gas, err
|
||||
}
|
||||
if c > gas {
|
||||
return 0, ErrOutOfGas
|
||||
}
|
||||
return gas - c, nil
|
||||
}
|
||||
|
||||
// InvalidateExecution sets the error that will be returned by
|
||||
// [EVM.ExecutionInvalidated] for the length of the current transaction; i.e.
|
||||
// until [EVM.Reset] is called. This is honoured by state-transition logic to
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/ava-labs/libevm/common"
|
||||
"github.com/ava-labs/libevm/libevm"
|
||||
"github.com/ava-labs/libevm/params"
|
||||
)
|
||||
|
|
@ -47,6 +48,10 @@ func (o *evmArgOverrider) OverrideEVMResetArgs(r params.Rules, _ *EVMResetArgs)
|
|||
}
|
||||
}
|
||||
|
||||
func (*evmArgOverrider) PreprocessingGasCharge(common.Hash) (uint64, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (o *evmArgOverrider) register(t *testing.T) {
|
||||
t.Helper()
|
||||
TestOnlyClearRegisteredHooks()
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
package vm
|
||||
|
||||
import (
|
||||
"github.com/ava-labs/libevm/common"
|
||||
"github.com/ava-labs/libevm/libevm"
|
||||
"github.com/ava-labs/libevm/libevm/register"
|
||||
"github.com/ava-labs/libevm/params"
|
||||
|
|
@ -57,6 +58,14 @@ var libevmHooks register.AtMostOnce[Hooks]
|
|||
type Hooks interface {
|
||||
OverrideNewEVMArgs(*NewEVMArgs) *NewEVMArgs
|
||||
OverrideEVMResetArgs(params.Rules, *EVMResetArgs) *EVMResetArgs
|
||||
Preprocessor
|
||||
}
|
||||
|
||||
// A Preprocessor performs computation on a transaction before the
|
||||
// [EVMInterpreter] is invoked and reports its gas charge for spending at the
|
||||
// beginning of [EVM.Call] or [EVM.Create].
|
||||
type Preprocessor interface {
|
||||
PreprocessingGasCharge(tx common.Hash) (uint64, error)
|
||||
}
|
||||
|
||||
// NewEVMArgs are the arguments received by [NewEVM], available for override
|
||||
|
|
@ -97,3 +106,23 @@ func (evm *EVM) overrideEVMResetArgs(txCtx TxContext, statedb StateDB) (TxContex
|
|||
args := libevmHooks.Get().OverrideEVMResetArgs(evm.chainRules, &EVMResetArgs{txCtx, statedb})
|
||||
return args.TxContext, args.StateDB
|
||||
}
|
||||
|
||||
// NOOPHooks implements [Hooks] such that every method is a noop.
|
||||
type NOOPHooks struct{}
|
||||
|
||||
var _ Hooks = NOOPHooks{}
|
||||
|
||||
// OverrideNewEVMArgs returns the args unchanged.
|
||||
func (NOOPHooks) OverrideNewEVMArgs(a *NewEVMArgs) *NewEVMArgs {
|
||||
return a
|
||||
}
|
||||
|
||||
// OverrideEVMResetArgs returns the args unchanged.
|
||||
func (NOOPHooks) OverrideEVMResetArgs(_ params.Rules, a *EVMResetArgs) *EVMResetArgs {
|
||||
return a
|
||||
}
|
||||
|
||||
// PreprocessingGasCharge returns (0, nil).
|
||||
func (NOOPHooks) PreprocessingGasCharge(common.Hash) (uint64, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,6 +82,8 @@ type StateDB interface {
|
|||
|
||||
AddLog(*types.Log)
|
||||
AddPreimage(common.Hash, []byte)
|
||||
|
||||
StateDBRemainder
|
||||
}
|
||||
|
||||
// CallContext provides a basic interface for the EVM calling conventions. The EVM
|
||||
|
|
|
|||
27
core/vm/interface.libevm.go
Normal file
27
core/vm/interface.libevm.go
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
// Copyright 2025 the libevm authors.
|
||||
//
|
||||
// The libevm additions to go-ethereum are free software: you can redistribute
|
||||
// them and/or modify them 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 libevm additions are distributed in the hope that they 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 vm
|
||||
|
||||
import "github.com/ava-labs/libevm/common"
|
||||
|
||||
// StateDBRemainder defines methods not included in the geth definition of
|
||||
// [StateDB] but present on the concrete type and exposed for libevm
|
||||
// functionality.
|
||||
type StateDBRemainder interface {
|
||||
TxHash() common.Hash
|
||||
TxIndex() int
|
||||
}
|
||||
189
core/vm/preprocess.libevm_test.go
Normal file
189
core/vm/preprocess.libevm_test.go
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
// Copyright 2025 the libevm authors.
|
||||
//
|
||||
// The libevm additions to go-ethereum are free software: you can redistribute
|
||||
// them and/or modify them 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 libevm additions are distributed in the hope that they 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 vm_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/holiman/uint256"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/ava-labs/libevm/common"
|
||||
"github.com/ava-labs/libevm/core"
|
||||
"github.com/ava-labs/libevm/core/types"
|
||||
"github.com/ava-labs/libevm/core/vm"
|
||||
"github.com/ava-labs/libevm/crypto"
|
||||
"github.com/ava-labs/libevm/libevm/ethtest"
|
||||
"github.com/ava-labs/libevm/params"
|
||||
)
|
||||
|
||||
type preprocessingCharger struct {
|
||||
vm.NOOPHooks
|
||||
charge map[common.Hash]uint64
|
||||
}
|
||||
|
||||
var errUnknownTx = errors.New("unknown tx")
|
||||
|
||||
func (p preprocessingCharger) PreprocessingGasCharge(tx common.Hash) (uint64, error) {
|
||||
c, ok := p.charge[tx]
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("%w: %v", errUnknownTx, tx)
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func TestChargePreprocessingGas(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
to *common.Address
|
||||
charge uint64
|
||||
skipChargeRegistration bool
|
||||
txGas uint64
|
||||
wantVMErr error
|
||||
wantGasUsed uint64
|
||||
}{
|
||||
{
|
||||
name: "standard create",
|
||||
to: nil,
|
||||
txGas: params.TxGas + params.CreateGas,
|
||||
wantGasUsed: params.TxGas + params.CreateGas,
|
||||
},
|
||||
{
|
||||
name: "create with extra charge",
|
||||
to: nil,
|
||||
charge: 1234,
|
||||
txGas: params.TxGas + params.CreateGas + 2000,
|
||||
wantGasUsed: params.TxGas + params.CreateGas + 1234,
|
||||
},
|
||||
{
|
||||
name: "standard call",
|
||||
to: &common.Address{},
|
||||
txGas: params.TxGas,
|
||||
wantGasUsed: params.TxGas,
|
||||
},
|
||||
{
|
||||
name: "out of gas",
|
||||
to: &common.Address{},
|
||||
charge: 1000,
|
||||
txGas: params.TxGas + 999,
|
||||
wantGasUsed: params.TxGas + 999,
|
||||
wantVMErr: vm.ErrOutOfGas,
|
||||
},
|
||||
{
|
||||
name: "call with extra charge",
|
||||
to: &common.Address{},
|
||||
charge: 13579,
|
||||
txGas: params.TxGas + 20000,
|
||||
wantGasUsed: params.TxGas + 13579,
|
||||
},
|
||||
{
|
||||
name: "error propagation",
|
||||
to: &common.Address{},
|
||||
skipChargeRegistration: true,
|
||||
txGas: params.TxGas,
|
||||
wantGasUsed: params.TxGas,
|
||||
wantVMErr: errUnknownTx,
|
||||
},
|
||||
}
|
||||
|
||||
config := params.AllDevChainProtocolChanges
|
||||
key, err := crypto.GenerateKey()
|
||||
require.NoError(t, err, "crypto.GenerateKey()")
|
||||
eoa := crypto.PubkeyToAddress(key.PublicKey)
|
||||
|
||||
header := &types.Header{
|
||||
Number: big.NewInt(0),
|
||||
Difficulty: big.NewInt(0),
|
||||
BaseFee: big.NewInt(0),
|
||||
}
|
||||
signer := types.MakeSigner(config, header.Number, header.Time)
|
||||
|
||||
var txs types.Transactions
|
||||
charge := make(map[common.Hash]uint64)
|
||||
for i, tt := range tests {
|
||||
tx := types.MustSignNewTx(key, signer, &types.LegacyTx{
|
||||
// Although nonces aren't strictly necessary, they guarantee a
|
||||
// different tx hash for each one.
|
||||
Nonce: uint64(i), //nolint:gosec // Known to not overflow
|
||||
To: tt.to,
|
||||
GasPrice: big.NewInt(1),
|
||||
Gas: tt.txGas,
|
||||
})
|
||||
txs = append(txs, tx)
|
||||
if !tt.skipChargeRegistration {
|
||||
charge[tx.Hash()] = tt.charge
|
||||
}
|
||||
}
|
||||
|
||||
vm.RegisterHooks(&preprocessingCharger{
|
||||
charge: charge,
|
||||
})
|
||||
t.Cleanup(vm.TestOnlyClearRegisteredHooks)
|
||||
|
||||
for i, tt := range tests {
|
||||
tx := txs[i]
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Logf("Extra gas charge: %d", tt.charge)
|
||||
|
||||
t.Run("ApplyTransaction", func(t *testing.T) {
|
||||
_, _, sdb := ethtest.NewEmptyStateDB(t)
|
||||
sdb.SetTxContext(tx.Hash(), i)
|
||||
sdb.SetBalance(eoa, new(uint256.Int).SetAllOne())
|
||||
sdb.SetNonce(eoa, tx.Nonce())
|
||||
|
||||
var gotGasUsed uint64
|
||||
gp := core.GasPool(math.MaxUint64)
|
||||
|
||||
receipt, err := core.ApplyTransaction(
|
||||
config, ethtest.DummyChainContext(), &common.Address{},
|
||||
&gp, sdb, header, tx, &gotGasUsed, vm.Config{},
|
||||
)
|
||||
require.NoError(t, err, "core.ApplyTransaction(...)")
|
||||
|
||||
wantStatus := types.ReceiptStatusSuccessful
|
||||
if tt.wantVMErr != nil {
|
||||
wantStatus = types.ReceiptStatusFailed
|
||||
}
|
||||
assert.Equalf(t, wantStatus, receipt.Status, "%T.Status", receipt)
|
||||
|
||||
assert.Equal(t, tt.wantGasUsed, gotGasUsed, "core.ApplyTransaction(..., &gotGasUsed, ...)")
|
||||
assert.Equalf(t, tt.wantGasUsed, receipt.GasUsed, "core.ApplyTransaction(...) -> %T.GasUsed", receipt)
|
||||
})
|
||||
|
||||
t.Run("VM_error", func(t *testing.T) {
|
||||
sdb, evm := ethtest.NewZeroEVM(t, ethtest.WithChainConfig(config))
|
||||
sdb.SetTxContext(tx.Hash(), i)
|
||||
sdb.SetBalance(eoa, new(uint256.Int).SetAllOne())
|
||||
sdb.SetNonce(eoa, tx.Nonce())
|
||||
|
||||
msg, err := core.TransactionToMessage(tx, signer, header.BaseFee)
|
||||
require.NoError(t, err, "core.TransactionToMessage(...)")
|
||||
|
||||
gp := core.GasPool(math.MaxUint64)
|
||||
got, err := core.ApplyMessage(evm, msg, &gp)
|
||||
require.NoError(t, err, "core.ApplyMessage(...)")
|
||||
require.ErrorIsf(t, got.Err, tt.wantVMErr, "%T.Err", got)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
44
libevm/ethtest/dummy.go
Normal file
44
libevm/ethtest/dummy.go
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
// Copyright 2025 the libevm authors.
|
||||
//
|
||||
// The libevm additions to go-ethereum are free software: you can redistribute
|
||||
// them and/or modify them 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 libevm additions are distributed in the hope that they 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 ethtest
|
||||
|
||||
import (
|
||||
"github.com/ava-labs/libevm/common"
|
||||
"github.com/ava-labs/libevm/consensus"
|
||||
"github.com/ava-labs/libevm/core"
|
||||
"github.com/ava-labs/libevm/core/types"
|
||||
)
|
||||
|
||||
// DummyChainContext returns a dummy that returns [DummyEngine] when its
|
||||
// Engine() method is called, and panics when its GetHeader() method is called.
|
||||
func DummyChainContext() core.ChainContext {
|
||||
return chainContext{}
|
||||
}
|
||||
|
||||
// DummyEngine returns a dummy that panics when its Author() method is called.
|
||||
func DummyEngine() consensus.Engine {
|
||||
return engine{}
|
||||
}
|
||||
|
||||
type (
|
||||
chainContext struct{}
|
||||
engine struct{ consensus.Engine }
|
||||
)
|
||||
|
||||
func (chainContext) Engine() consensus.Engine { return engine{} }
|
||||
func (chainContext) GetHeader(common.Hash, uint64) *types.Header { panic("unimplemented") }
|
||||
func (engine) Author(h *types.Header) (common.Address, error) { panic("unimplemented") }
|
||||
|
|
@ -23,14 +23,28 @@ import (
|
|||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/ava-labs/libevm/common"
|
||||
"github.com/ava-labs/libevm/core"
|
||||
"github.com/ava-labs/libevm/core/rawdb"
|
||||
"github.com/ava-labs/libevm/core/state"
|
||||
"github.com/ava-labs/libevm/core/types"
|
||||
"github.com/ava-labs/libevm/core/vm"
|
||||
"github.com/ava-labs/libevm/ethdb"
|
||||
"github.com/ava-labs/libevm/params"
|
||||
)
|
||||
|
||||
// NewEmptyStateDB returns a fresh database from [rawdb.NewMemoryDatabase], a
|
||||
// [state.Database] wrapping it, and a [state.StateDB] wrapping that, opened to
|
||||
// [types.EmptyRootHash].
|
||||
func NewEmptyStateDB(tb testing.TB) (ethdb.Database, state.Database, *state.StateDB) {
|
||||
tb.Helper()
|
||||
|
||||
db := rawdb.NewMemoryDatabase()
|
||||
cache := state.NewDatabase(db)
|
||||
sdb, err := state.New(types.EmptyRootHash, cache, nil)
|
||||
require.NoError(tb, err, "state.New()")
|
||||
return db, cache, sdb
|
||||
}
|
||||
|
||||
// NewZeroEVM returns a new EVM backed by a [rawdb.NewMemoryDatabase]; all other
|
||||
// arguments to [vm.NewEVM] are the zero values of their respective types,
|
||||
// except for the use of [core.CanTransfer] and [core.Transfer] instead of nil
|
||||
|
|
@ -38,8 +52,7 @@ import (
|
|||
func NewZeroEVM(tb testing.TB, opts ...EVMOption) (*state.StateDB, *vm.EVM) {
|
||||
tb.Helper()
|
||||
|
||||
sdb, err := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil)
|
||||
require.NoError(tb, err, "state.New()")
|
||||
_, _, sdb := NewEmptyStateDB(tb)
|
||||
|
||||
args := &evmConstructorArgs{
|
||||
vm.BlockContext{
|
||||
|
|
|
|||
Loading…
Reference in a new issue