diff --git a/core/vm/evm.go b/core/vm/evm.go index b9fd682b9a7..b9eca8cfb7f 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -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) { diff --git a/core/vm/evm.libevm.go b/core/vm/evm.libevm.go index c2c807c1378..b0d415bbacd 100644 --- a/core/vm/evm.libevm.go +++ b/core/vm/evm.libevm.go @@ -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 diff --git a/core/vm/evm.libevm_test.go b/core/vm/evm.libevm_test.go index c0a33718e3d..deb7a0c6770 100644 --- a/core/vm/evm.libevm_test.go +++ b/core/vm/evm.libevm_test.go @@ -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/params" ) @@ -46,6 +47,10 @@ func (o *evmArgOverrider) OverrideEVMResetArgs(r params.Rules, _ *EVMResetArgs) } } +func (o *evmArgOverrider) PreprocessingGasCharge(common.Hash) (uint64, error) { + return 0, nil +} + func (o *evmArgOverrider) register(t *testing.T) { t.Helper() TestOnlyClearRegisteredHooks() diff --git a/core/vm/hooks.libevm.go b/core/vm/hooks.libevm.go index 1e5acd49db9..a0ef69ba811 100644 --- a/core/vm/hooks.libevm.go +++ b/core/vm/hooks.libevm.go @@ -17,6 +17,7 @@ package vm import ( + "github.com/ava-labs/libevm/common" "github.com/ava-labs/libevm/libevm/register" "github.com/ava-labs/libevm/params" ) @@ -40,6 +41,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 @@ -80,3 +89,19 @@ 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 } diff --git a/core/vm/interface.go b/core/vm/interface.go index 4a9e15a6d3c..25ef393e863 100644 --- a/core/vm/interface.go +++ b/core/vm/interface.go @@ -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 diff --git a/core/vm/interface.libevm.go b/core/vm/interface.libevm.go new file mode 100644 index 00000000000..ee999fcc8c5 --- /dev/null +++ b/core/vm/interface.libevm.go @@ -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 +// . + +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 +} diff --git a/core/vm/preprocess.libevm_test.go b/core/vm/preprocess.libevm_test.go new file mode 100644 index 00000000000..509682668c0 --- /dev/null +++ b/core/vm/preprocess.libevm_test.go @@ -0,0 +1,193 @@ +// 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 +// . + +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), + 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) + + if got, want := gotGasUsed, tt.wantGasUsed; got != want { + t.Errorf("core.ApplyTransaction(..., &gotGasUsed, ...) got %d; want %d", got, want) + } + if got, want := receipt.GasUsed, tt.wantGasUsed; got != want { + t.Errorf("core.ApplyTransaction(...) -> %T.GasUsed = %d; want %d", receipt, got, want) + } + }) + + 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) + }) + }) + } +} diff --git a/libevm/ethtest/dummy.go b/libevm/ethtest/dummy.go new file mode 100644 index 00000000000..e800a513d27 --- /dev/null +++ b/libevm/ethtest/dummy.go @@ -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 +// . + +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") } diff --git a/libevm/ethtest/evm.go b/libevm/ethtest/evm.go index 4e16c4e90bb..7a7b463295e 100644 --- a/libevm/ethtest/evm.go +++ b/libevm/ethtest/evm.go @@ -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{