Skip to content
9 changes: 4 additions & 5 deletions cannon/cmd/load_elf.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,7 @@ func LoadELF(ctx *cli.Context) error {
if err != nil {
return err
}
switch ver {
case versions.GetCurrentSingleThreaded():
if versions.IsSupportedSingleThreaded(ver) {
createInitialState = func(f *elf.File) (mipsevm.FPVMState, error) {
return program.LoadELF(f, singlethreaded.CreateInitialState)
}
Expand All @@ -72,11 +71,11 @@ func LoadELF(ctx *cli.Context) error {
}
return program.PatchStack(state)
}
case versions.GetCurrentMultiThreaded(), versions.GetCurrentMultiThreaded64():
} else if versions.IsSupportedMultiThreaded(ver) || versions.IsSupportedMultiThreaded64(ver) {
createInitialState = func(f *elf.File) (mipsevm.FPVMState, error) {
return program.LoadELF(f, multithreaded.CreateInitialState)
}
default:
} else {
return fmt.Errorf("unsupported state version: %d (%s)", ver, ver.String())
}

Expand All @@ -97,7 +96,7 @@ func LoadELF(ctx *cli.Context) error {
}

// Ensure the state is written with appropriate version information
versionedState, err := versions.NewFromState(state)
versionedState, err := versions.NewFromState(ver, state)
if err != nil {
return fmt.Errorf("failed to create versioned state: %w", err)
}
Expand Down
10 changes: 9 additions & 1 deletion cannon/mipsevm/iface.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ type FPVMState interface {
EncodeWitness() (witness []byte, hash common.Hash)

// CreateVM creates a FPVM that can operate on this state.
CreateVM(logger log.Logger, po PreimageOracle, stdOut, stdErr io.Writer, meta Metadata) FPVM
CreateVM(logger log.Logger, po PreimageOracle, stdOut, stdErr io.Writer, meta Metadata, features FeatureToggles) FPVM
}

type SymbolMatcher func(addr arch.Word) bool
Expand All @@ -68,6 +68,14 @@ type Metadata interface {
CreateSymbolMatcher(name string) SymbolMatcher
}

// FeatureToggles defines the set of features which are enabled only on some of the supported state versions.
// This allows supporting multiple state versions concurrently without needing to create completely separate
// FPVM implementations and duplicate a lot of code.
// Toggles here are temporary and should be removed once the newer state version is deployed widely. The older
// version can then be supported via multicannon pulling in a specific build and support for it dropped in latest code.
type FeatureToggles struct {
}

type FPVM interface {
// GetState returns the current state of the VM. The FPVMState is updated by successive calls to Step
GetState() FPVMState
Expand Down
4 changes: 3 additions & 1 deletion cannon/mipsevm/multithreaded/instrumented.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,12 @@ type InstrumentedState struct {

preimageOracle *exec.TrackingPreimageOracleReader
meta mipsevm.Metadata
features mipsevm.FeatureToggles
}

var _ mipsevm.FPVM = (*InstrumentedState)(nil)

func NewInstrumentedState(state *State, po mipsevm.PreimageOracle, stdOut, stdErr io.Writer, log log.Logger, meta mipsevm.Metadata) *InstrumentedState {
func NewInstrumentedState(state *State, po mipsevm.PreimageOracle, stdOut, stdErr io.Writer, log log.Logger, meta mipsevm.Metadata, features mipsevm.FeatureToggles) *InstrumentedState {
return &InstrumentedState{
state: state,
log: log,
Expand All @@ -39,6 +40,7 @@ func NewInstrumentedState(state *State, po mipsevm.PreimageOracle, stdOut, stdEr
statsTracker: NoopStatsTracker(),
preimageOracle: exec.NewTrackingPreimageOracleReader(po),
meta: meta,
features: features,
}
}

Expand Down
21 changes: 17 additions & 4 deletions cannon/mipsevm/multithreaded/instrumented_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"io"
"os"
"reflect"
"testing"

"github.com/ethereum/go-ethereum/log"
Expand All @@ -16,7 +17,7 @@ import (
)

func vmFactory(state *State, po mipsevm.PreimageOracle, stdOut, stdErr io.Writer, log log.Logger, meta *program.Metadata) mipsevm.FPVM {
return NewInstrumentedState(state, po, stdOut, stdErr, log, meta)
return NewInstrumentedState(state, po, stdOut, stdErr, log, meta, allFeaturesEnabled())
}

func TestInstrumentedState_OpenMips(t *testing.T) {
Expand Down Expand Up @@ -53,7 +54,7 @@ func TestInstrumentedState_UtilsCheck(t *testing.T) {
oracle := testutil.StaticOracle(t, []byte{})

var stdOutBuf, stdErrBuf bytes.Buffer
us := NewInstrumentedState(state, oracle, io.MultiWriter(&stdOutBuf, os.Stdout), io.MultiWriter(&stdErrBuf, os.Stderr), testutil.CreateLogger(), meta)
us := NewInstrumentedState(state, oracle, io.MultiWriter(&stdOutBuf, os.Stdout), io.MultiWriter(&stdErrBuf, os.Stderr), testutil.CreateLogger(), meta, allFeaturesEnabled())

for i := 0; i < 1_000_000; i++ {
if us.GetState().GetExited() {
Expand Down Expand Up @@ -186,7 +187,7 @@ func TestInstrumentedState_MultithreadedProgram(t *testing.T) {
oracle := testutil.StaticOracle(t, []byte{})

var stdOutBuf, stdErrBuf bytes.Buffer
us := NewInstrumentedState(state, oracle, io.MultiWriter(&stdOutBuf, os.Stdout), io.MultiWriter(&stdErrBuf, os.Stderr), testutil.CreateLogger(), meta)
us := NewInstrumentedState(state, oracle, io.MultiWriter(&stdOutBuf, os.Stdout), io.MultiWriter(&stdErrBuf, os.Stderr), testutil.CreateLogger(), meta, allFeaturesEnabled())

for i := 0; i < test.steps; i++ {
if us.GetState().GetExited() {
Expand Down Expand Up @@ -231,7 +232,7 @@ func TestInstrumentedState_Alloc(t *testing.T) {
state, meta := testutil.LoadELFProgram(t, testutil.ProgramPath("alloc"), CreateInitialState, false)
oracle := testutil.AllocOracle(t, test.numAllocs, test.allocSize)

us := NewInstrumentedState(state, oracle, os.Stdout, os.Stderr, testutil.CreateLogger(), meta)
us := NewInstrumentedState(state, oracle, os.Stdout, os.Stderr, testutil.CreateLogger(), meta, allFeaturesEnabled())
require.NoError(t, us.InitDebug())
// emulation shouldn't take more than 20 B steps
for i := 0; i < 20_000_000_000; i++ {
Expand All @@ -252,3 +253,15 @@ func TestInstrumentedState_Alloc(t *testing.T) {
})
}
}

// allFeaturesEnabled returns a FeatureToggles with all toggles enabled.
// We can't use versions.FeaturesForVersion to test the specific toggles for each version because it creates a
// dependency loop, so we just cover the everything enabled case as that should be the upcoming version.
func allFeaturesEnabled() mipsevm.FeatureToggles {
toggles := mipsevm.FeatureToggles{}
tRef := reflect.ValueOf(toggles)
for i := 0; i < tRef.NumField(); i++ {
tRef.Field(i).Set(reflect.ValueOf(true))
}
return toggles
}
4 changes: 2 additions & 2 deletions cannon/mipsevm/multithreaded/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,9 @@ func CreateInitialState(pc, heapStart Word) *State {
return state
}

func (s *State) CreateVM(logger log.Logger, po mipsevm.PreimageOracle, stdOut, stdErr io.Writer, meta mipsevm.Metadata) mipsevm.FPVM {
func (s *State) CreateVM(logger log.Logger, po mipsevm.PreimageOracle, stdOut, stdErr io.Writer, meta mipsevm.Metadata, features mipsevm.FeatureToggles) mipsevm.FPVM {
logger.Info("Using cannon multithreaded VM", "is32", arch.IsMips32)
return NewInstrumentedState(s, po, stdOut, stdErr, logger, meta)
return NewInstrumentedState(s, po, stdOut, stdErr, logger, meta, features)
}

func (s *State) GetCurrentThread() *ThreadState {
Expand Down
2 changes: 1 addition & 1 deletion cannon/mipsevm/program/testutil/mocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,6 @@ func (m MockFPVMState) EncodeWitness() (witness []byte, hash common.Hash) {
panic("not implemented")
}

func (m MockFPVMState) CreateVM(logger log.Logger, po mipsevm.PreimageOracle, stdOut, stdErr io.Writer, meta mipsevm.Metadata) mipsevm.FPVM {
func (m MockFPVMState) CreateVM(_ log.Logger, _ mipsevm.PreimageOracle, _, _ io.Writer, _ mipsevm.Metadata, _ mipsevm.FeatureToggles) mipsevm.FPVM {
panic("not implemented")
}
2 changes: 1 addition & 1 deletion cannon/mipsevm/singlethreaded/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ func CreateInitialState(pc, heapStart Word) *State {
return state
}

func (s *State) CreateVM(logger log.Logger, po mipsevm.PreimageOracle, stdOut, stdErr io.Writer, meta mipsevm.Metadata) mipsevm.FPVM {
func (s *State) CreateVM(logger log.Logger, po mipsevm.PreimageOracle, stdOut, stdErr io.Writer, meta mipsevm.Metadata, _ mipsevm.FeatureToggles) mipsevm.FPVM {
return NewInstrumentedState(s, po, stdOut, stdErr, meta)
}

Expand Down
60 changes: 31 additions & 29 deletions cannon/mipsevm/tests/evm_common64_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,36 +144,38 @@ func TestEVM_SingleStep_Shift64(t *testing.T) {
{name: "dsra32", funct: 0x3f, rd: Word(0xAA_BB_CC_DD_A1_B1_C1_D1), rt: Word(0x7F_FF_FF_FF_FF_FF_FF_FF), sa: 31, expectRes: Word(0x0)}, // dsra32 t8, s2, 1
}

v := GetMultiThreadedTestCase(t)
for i, tt := range cases {
testName := fmt.Sprintf("%v %v", v.Name, tt.name)
t.Run(testName, func(t *testing.T) {
pc := Word(0x0)
goVm := v.VMFactory(nil, os.Stdout, os.Stderr, testutil.CreateLogger(), testutil.WithRandomization(int64(i)), testutil.WithPCAndNextPC(pc))
state := goVm.GetState()
var insn uint32
var rtReg uint32
var rdReg uint32
rtReg = 18
rdReg = 8
insn = rtReg<<16 | rdReg<<11 | tt.sa<<6 | tt.funct
state.GetRegistersRef()[rdReg] = tt.rd
state.GetRegistersRef()[rtReg] = tt.rt
testutil.StoreInstruction(state.GetMemory(), pc, insn)
step := state.GetStep()

// Setup expectations
expected := testutil.NewExpectedState(state)
expected.ExpectStep()
expected.Registers[rdReg] = tt.expectRes

stepWitness, err := goVm.Step(true)
require.NoError(t, err)

// Check expectations
expected.Validate(t, state)
testutil.ValidateEVM(t, stepWitness, step, goVm, v.StateHashFn, v.Contracts)
})
for _, v := range GetMultiThreadedTestCases(t) {
v := v
testName := fmt.Sprintf("%v %v", v.Name, tt.name)
t.Run(testName, func(t *testing.T) {
pc := Word(0x0)
goVm := v.VMFactory(nil, os.Stdout, os.Stderr, testutil.CreateLogger(), testutil.WithRandomization(int64(i)), testutil.WithPCAndNextPC(pc))
state := goVm.GetState()
var insn uint32
var rtReg uint32
var rdReg uint32
rtReg = 18
rdReg = 8
insn = rtReg<<16 | rdReg<<11 | tt.sa<<6 | tt.funct
state.GetRegistersRef()[rdReg] = tt.rd
state.GetRegistersRef()[rtReg] = tt.rt
testutil.StoreInstruction(state.GetMemory(), pc, insn)
step := state.GetStep()

// Setup expectations
expected := testutil.NewExpectedState(state)
expected.ExpectStep()
expected.Registers[rdReg] = tt.expectRes

stepWitness, err := goVm.Step(true)
require.NoError(t, err)

// Check expectations
expected.Validate(t, state)
testutil.ValidateEVM(t, stepWitness, step, goVm, v.StateHashFn, v.Contracts)
})
}
}
}

Expand Down
13 changes: 3 additions & 10 deletions cannon/mipsevm/tests/evm_common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"testing"
"time"

"github.com/ethereum-optimism/optimism/cannon/mipsevm/versions"
"github.com/ethereum/go-ethereum/core/tracing"
"github.com/stretchr/testify/require"

Expand All @@ -27,21 +28,13 @@ func TestEVM_OpenMIPS(t *testing.T) {
require.NoError(t, err)

cases := GetMipsVersionTestCases(t)
skippedTests := map[string][]string{
"multi-threaded": {"clone.bin"},
"single-threaded": {},
}

for _, c := range cases {
skipped, exists := skippedTests[c.Name]
require.True(t, exists)
for _, f := range testFiles {
testName := fmt.Sprintf("%v (%v)", f.Name(), c.Name)
t.Run(testName, func(t *testing.T) {
for _, skipped := range skipped {
if f.Name() == skipped {
t.Skipf("Skipping explicitly excluded open_mips testcase: %v", f.Name())
}
if !versions.IsSupportedSingleThreaded(c.Version) && f.Name() == "clone.bin" {
t.Skipf("%v only supported for singlethreaded VMs", f.Name())
}

oracle := testutil.SelectOracleFixture(t, f.Name())
Expand Down
35 changes: 22 additions & 13 deletions cannon/mipsevm/tests/evm_multithreaded64_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,11 @@ var NoopSyscalls64 = map[string]uint32{
}

func TestEVM_NoopSyscall64(t *testing.T) {
testNoopSyscall(t, NoopSyscalls64)
t.Parallel()
for _, version := range GetMultiThreadedTestCases(t) {
noOpCalls := maps.Clone(NoopSyscalls64)
testNoopSyscall(t, version, noOpCalls)
}
}

func TestEVM_UnsupportedSyscall64(t *testing.T) {
Expand All @@ -480,7 +484,10 @@ func TestEVM_UnsupportedSyscall64(t *testing.T) {
unsupportedSyscalls = append(unsupportedSyscalls, candidate)
}

testUnsupportedSyscall(t, unsupportedSyscalls)
for _, version := range GetMultiThreadedTestCases(t) {
unsupported := unsupportedSyscalls
testUnsupportedSyscall(t, version, unsupported)
}
}

// Asserts the undefined syscall handling on cannon64 triggers a VM panic
Expand All @@ -492,16 +499,18 @@ func TestEVM_UndefinedSyscall(t *testing.T) {
"SysLlseek": arch.SysLlseek,
}
for name, val := range undefinedSyscalls {
t.Run(name, func(t *testing.T) {
t.Parallel()
goVm, state, contracts := setup(t, int(val), nil)
testutil.StoreInstruction(state.Memory, state.GetPC(), syscallInsn)
state.GetRegistersRef()[2] = Word(val) // Set syscall number
proofData := multiThreadedProofGenerator(t, state)

require.Panics(t, func() { _, _ = goVm.Step(true) })
errorMessage := "unimplemented syscall"
testutil.AssertEVMReverts(t, state, contracts, nil, proofData, testutil.CreateErrorStringMatcher(errorMessage))
})
for _, version := range GetMultiThreadedTestCases(t) {
t.Run(fmt.Sprintf("%v-%v", version.Name, name), func(t *testing.T) {
t.Parallel()
goVm, state, contracts := setupWithTestCase(t, version, int(val), nil)
testutil.StoreInstruction(state.Memory, state.GetPC(), syscallInsn)
state.GetRegistersRef()[2] = Word(val) // Set syscall number
proofData := multiThreadedProofGenerator(t, state)

require.Panics(t, func() { _, _ = goVm.Step(true) })
errorMessage := "unimplemented syscall"
testutil.AssertEVMReverts(t, state, contracts, nil, proofData, testutil.CreateErrorStringMatcher(errorMessage))
})
}
}
}
Loading