diff --git a/core/vm/contracts.libevm.go b/core/vm/contracts.libevm.go
index a6c9c1a860e8..8bc6ee8c7f46 100644
--- a/core/vm/contracts.libevm.go
+++ b/core/vm/contracts.libevm.go
@@ -18,14 +18,10 @@ package vm
import (
"fmt"
- "math/big"
"github.com/holiman/uint256"
"github.com/ava-labs/libevm/common"
- "github.com/ava-labs/libevm/core/types"
- "github.com/ava-labs/libevm/libevm"
- "github.com/ava-labs/libevm/params"
)
// evmCallArgs mirrors the parameters of the [EVM] methods Call(), CallCode(),
@@ -124,20 +120,9 @@ func (p statefulPrecompile) Run([]byte) ([]byte, error) {
// precompiled contract is being run; and (b) a means of calling other
// contracts.
type PrecompileEnvironment interface {
- ChainConfig() *params.ChainConfig
- Rules() params.Rules
- ReadOnly() bool
- // StateDB will be non-nil i.f.f !ReadOnly().
- StateDB() StateDB
- // ReadOnlyState will always be non-nil.
- ReadOnlyState() libevm.StateReader
- Addresses() *libevm.AddressContext
- IncomingCallType() CallType
-
- BlockHeader() (types.Header, error)
- BlockNumber() *big.Int
- BlockTime() uint64
+ Environment
+ IncomingCallType() CallType
// Call is equivalent to [EVM.Call] except that the `caller` argument is
// removed and automatically determined according to the type of call that
// invoked the precompile.
diff --git a/core/vm/contracts.libevm_test.go b/core/vm/contracts.libevm_test.go
index 59c07bbf9a7a..0282930b2a7c 100644
--- a/core/vm/contracts.libevm_test.go
+++ b/core/vm/contracts.libevm_test.go
@@ -145,7 +145,7 @@ func TestNewStatefulPrecompile(t *testing.T) {
run := func(env vm.PrecompileEnvironment, input []byte, suppliedGas uint64) ([]byte, uint64, error) {
if got, want := env.StateDB() != nil, !env.ReadOnly(); got != want {
- return nil, 0, fmt.Errorf("PrecompileEnvironment().StateDB() must be non-nil i.f.f. not read-only; got non-nil? %t; want %t", got, want)
+ return nil, 0, fmt.Errorf("Environment().StateDB() must be non-nil i.f.f. not read-only; got non-nil? %t; want %t", got, want)
}
hdr, err := env.BlockHeader()
if err != nil {
diff --git a/core/vm/environment.libevm.go b/core/vm/environment.libevm.go
index 051a5ff142ba..3100fa834968 100644
--- a/core/vm/environment.libevm.go
+++ b/core/vm/environment.libevm.go
@@ -28,6 +28,23 @@ import (
"github.com/ava-labs/libevm/params"
)
+// An Environment provides information about the context in which an instruction
+// is being executed.
+type Environment interface {
+ ChainConfig() *params.ChainConfig
+ Rules() params.Rules
+ ReadOnly() bool
+ // StateDB will be non-nil i.f.f !ReadOnly().
+ StateDB() StateDB
+ // ReadOnlyState will always be non-nil.
+ ReadOnlyState() libevm.StateReader
+ Addresses() *libevm.AddressContext
+
+ BlockHeader() (types.Header, error)
+ BlockNumber() *big.Int
+ BlockTime() uint64
+}
+
var _ PrecompileEnvironment = (*environment)(nil)
type environment struct {
diff --git a/core/vm/evm.libevm_test.go b/core/vm/evm.libevm_test.go
index d3221f0f74c2..84a5dbd9387d 100644
--- a/core/vm/evm.libevm_test.go
+++ b/core/vm/evm.libevm_test.go
@@ -38,6 +38,8 @@ func (o *evmArgOverrider) OverrideNewEVMArgs(args *NewEVMArgs) *NewEVMArgs {
return args
}
+func (evmArgOverrider) OverrideJumpTable(_ params.Rules, jt *JumpTable) *JumpTable { return jt }
+
func (o *evmArgOverrider) OverrideEVMResetArgs(r params.Rules, _ *EVMResetArgs) *EVMResetArgs {
o.gotResetChainID = r.ChainID
return &EVMResetArgs{
diff --git a/core/vm/hooks.libevm.go b/core/vm/hooks.libevm.go
index e08645044351..f4780cf1429d 100644
--- a/core/vm/hooks.libevm.go
+++ b/core/vm/hooks.libevm.go
@@ -34,6 +34,7 @@ var libevmHooks Hooks
type Hooks interface {
OverrideNewEVMArgs(*NewEVMArgs) *NewEVMArgs
OverrideEVMResetArgs(params.Rules, *EVMResetArgs) *EVMResetArgs
+ OverrideJumpTable(params.Rules, *JumpTable) *JumpTable
}
// NewEVMArgs are the arguments received by [NewEVM], available for override
@@ -74,3 +75,10 @@ func (evm *EVM) overrideEVMResetArgs(txCtx TxContext, statedb StateDB) (TxContex
args := libevmHooks.OverrideEVMResetArgs(evm.chainRules, &EVMResetArgs{txCtx, statedb})
return args.TxContext, args.StateDB
}
+
+func overrideJumpTable(r params.Rules, jt *JumpTable) *JumpTable {
+ if libevmHooks == nil {
+ return jt
+ }
+ return libevmHooks.OverrideJumpTable(r, jt)
+}
diff --git a/core/vm/interpreter.go b/core/vm/interpreter.go
index 6af763c214ce..afb1ebafd485 100644
--- a/core/vm/interpreter.go
+++ b/core/vm/interpreter.go
@@ -95,6 +95,7 @@ func NewEVMInterpreter(evm *EVM) *EVMInterpreter {
}
}
evm.Config.ExtraEips = extraEips
+ table = overrideJumpTable(evm.chainRules, table)
return &EVMInterpreter{evm: evm, table: table}
}
diff --git a/core/vm/jump_table.libevm.go b/core/vm/jump_table.libevm.go
new file mode 100644
index 000000000000..1bf04f1d3f0d
--- /dev/null
+++ b/core/vm/jump_table.libevm.go
@@ -0,0 +1,60 @@
+// Copyright 2024 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
+
+// An OperationBuilder is a factory for a new operations to include in a
+// [JumpTable].
+type OperationBuilder struct {
+ Execute OperationFunc
+ ConstantGas uint64
+ DynamicGas func(_ *EVM, _ *Contract, _ *Stack, _ *Memory, requestedMemorySize uint64) (uint64, error)
+ MinStack, MaxStack int
+ MemorySize func(s *Stack) (size uint64, overflow bool)
+}
+
+// Build constructs the operation.
+func (b OperationBuilder) Build() *operation {
+ o := &operation{
+ execute: b.Execute.internal(),
+ constantGas: b.ConstantGas,
+ dynamicGas: b.DynamicGas,
+ minStack: b.MinStack,
+ maxStack: b.MaxStack,
+ memorySize: b.MemorySize,
+ }
+ return o
+}
+
+// An OperationFunc is the execution function of a custom instruction.
+type OperationFunc func(_ Environment, pc *uint64, _ *EVMInterpreter, _ *ScopeContext) ([]byte, error)
+
+// internal converts an exported [OperationFunc] into an un-exported
+// [executionFunc] as required to build an [operation].
+func (fn OperationFunc) internal() executionFunc {
+ return func(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
+ env := &environment{
+ evm: interpreter.evm,
+ self: scope.Contract,
+ // The CallType isn't exposed by an instruction's [Environment] and,
+ // although [UnknownCallType] is the default value, it's explicitly
+ // set to avoid future accidental setting without proper
+ // justification.
+ callType: UnknownCallType,
+ }
+ return fn(env, pc, interpreter, scope)
+ }
+}
diff --git a/core/vm/jump_table.libevm_test.go b/core/vm/jump_table.libevm_test.go
new file mode 100644
index 000000000000..71de48db522a
--- /dev/null
+++ b/core/vm/jump_table.libevm_test.go
@@ -0,0 +1,110 @@
+// Copyright 2024 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 (
+ "fmt"
+ "reflect"
+ "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/vm"
+ "github.com/ava-labs/libevm/libevm/ethtest"
+ "github.com/ava-labs/libevm/params"
+)
+
+type vmHooksStub struct {
+ replacement *vm.JumpTable
+ overridden bool
+}
+
+var _ vm.Hooks = (*vmHooksStub)(nil)
+
+// OverrideJumpTable overrides all non-nil operations from s.replacement .
+func (s *vmHooksStub) OverrideJumpTable(_ params.Rules, jt *vm.JumpTable) *vm.JumpTable {
+ s.overridden = true
+ for op, instr := range s.replacement {
+ if instr != nil {
+ fmt.Println(op, instr)
+ jt[op] = instr
+ }
+ }
+ return jt
+}
+
+func (*vmHooksStub) OverrideNewEVMArgs(a *vm.NewEVMArgs) *vm.NewEVMArgs { return a }
+
+func (*vmHooksStub) OverrideEVMResetArgs(r params.Rules, a *vm.EVMResetArgs) *vm.EVMResetArgs {
+ return a
+}
+
+// An opRecorder is an instruction that records its inputs.
+type opRecorder struct {
+ stateVal common.Hash
+}
+
+func (op *opRecorder) execute(env vm.Environment, pc *uint64, interpreter *vm.EVMInterpreter, scope *vm.ScopeContext) ([]byte, error) {
+ op.stateVal = env.StateDB().GetState(scope.Contract.Address(), common.Hash{})
+ return nil, nil
+}
+
+func TestOverrideJumpTable(t *testing.T) {
+ const (
+ opcode = 1
+ gasLimit uint64 = 1e6
+ )
+ rng := ethtest.NewPseudoRand(142857)
+ gasCost := 1 + rng.Uint64n(gasLimit)
+ spy := &opRecorder{}
+
+ vmHooks := &vmHooksStub{
+ replacement: &vm.JumpTable{
+ opcode: vm.OperationBuilder{
+ Execute: spy.execute,
+ ConstantGas: gasCost,
+ MemorySize: func(s *vm.Stack) (size uint64, overflow bool) {
+ return 0, false
+ },
+ }.Build(),
+ },
+ }
+ vm.RegisterHooks(vmHooks)
+
+ state, evm := ethtest.NewZeroEVM(t)
+
+ contract := rng.Address()
+ state.CreateAccount(contract)
+ state.SetCode(contract, []byte{opcode})
+ value := rng.Hash()
+ state.SetState(contract, common.Hash{}, value)
+
+ _, gasRemaining, err := evm.Call(vm.AccountRef(rng.Address()), contract, []byte{}, gasLimit, uint256.NewInt(0))
+ require.NoError(t, err, "evm.Call([contract with overridden opcode])")
+ assert.Equal(t, gasLimit-gasCost, gasRemaining, "gas remaining")
+ assert.Equal(t, spy.stateVal, value, "StateDB propagated")
+}
+
+func TestOperationFieldCount(t *testing.T) {
+ // The libevm OperationBuilder assumes that the 6 struct fields are the only
+ // ones.
+ op := vm.OperationBuilder{}.Build()
+ require.Equalf(t, 6, reflect.TypeOf(*op).NumField(), "number of fields in %T struct", *op)
+}
diff --git a/core/vm/jump_table_export.go b/core/vm/jump_table_export.go
index fbd9817e06cf..0db3c33e057d 100644
--- a/core/vm/jump_table_export.go
+++ b/core/vm/jump_table_export.go
@@ -24,7 +24,12 @@ import (
// LookupInstructionSet returns the instruction set for the fork configured by
// the rules.
-func LookupInstructionSet(rules params.Rules) (JumpTable, error) {
+func LookupInstructionSet(rules params.Rules) (jt JumpTable, err error) {
+ defer func() {
+ if err == nil { // NOTE `err ==` NOT !=
+ jt = *overrideJumpTable(rules, &jt)
+ }
+ }()
switch {
case rules.IsVerkle:
return newCancunInstructionSet(), errors.New("verkle-fork not defined yet")