Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions core/vm/evm.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
38 changes: 38 additions & 0 deletions core/vm/evm.libevm.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions core/vm/evm.libevm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -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()
Expand Down
25 changes: 25 additions & 0 deletions core/vm/hooks.libevm.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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
Expand Down Expand Up @@ -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 }
2 changes: 2 additions & 0 deletions core/vm/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 27 additions & 0 deletions core/vm/interface.libevm.go
Original file line number Diff line number Diff line change
@@ -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
}
193 changes: 193 additions & 0 deletions core/vm/preprocess.libevm_test.go
Original file line number Diff line number Diff line change
@@ -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
// <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),
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)
})
})
}
}
Loading
Loading