diff --git a/cannon/Makefile b/cannon/Makefile index 0240b7df095f1..0f3836fb62fbb 100644 --- a/cannon/Makefile +++ b/cannon/Makefile @@ -29,8 +29,8 @@ test: elf contract go test -v ./... fuzz: + # Common vm tests go test $(FUZZLDFLAGS) -run NOTAREALTEST -v -fuzztime 10s -fuzz=FuzzStateSyscallBrk ./mipsevm/tests - go test $(FUZZLDFLAGS) -run NOTAREALTEST -v -fuzztime 10s -fuzz=FuzzStateSyscallClone ./mipsevm/tests go test $(FUZZLDFLAGS) -run NOTAREALTEST -v -fuzztime 10s -fuzz=FuzzStateSyscallMmap ./mipsevm/tests go test $(FUZZLDFLAGS) -run NOTAREALTEST -v -fuzztime 10s -fuzz=FuzzStateSyscallExitGroup ./mipsevm/tests go test $(FUZZLDFLAGS) -run NOTAREALTEST -v -fuzztime 10s -fuzz=FuzzStateSyscallFcntl ./mipsevm/tests @@ -38,6 +38,10 @@ fuzz: go test $(FUZZLDFLAGS) -run NOTAREALTEST -v -fuzztime 20s -fuzz=FuzzStatePreimageRead ./mipsevm/tests go test $(FUZZLDFLAGS) -run NOTAREALTEST -v -fuzztime 10s -fuzz=FuzzStateHintWrite ./mipsevm/tests go test $(FUZZLDFLAGS) -run NOTAREALTEST -v -fuzztime 20s -fuzz=FuzzStatePreimageWrite ./mipsevm/tests + # Single-threaded tests + go test $(FUZZLDFLAGS) -run NOTAREALTEST -v -fuzztime 10s -fuzz=FuzzStateSyscallCloneST ./mipsevm/tests + # Multi-threaded tests + go test $(FUZZLDFLAGS) -run NOTAREALTEST -v -fuzztime 10s -fuzz=FuzzStateSyscallCloneMT ./mipsevm/tests .PHONY: \ cannon \ diff --git a/cannon/mipsevm/iface.go b/cannon/mipsevm/iface.go index 1a3579deed937..45757d5c0eaa7 100644 --- a/cannon/mipsevm/iface.go +++ b/cannon/mipsevm/iface.go @@ -2,6 +2,7 @@ package mipsevm import ( "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum-optimism/optimism/cannon/mipsevm/memory" ) @@ -9,11 +10,23 @@ import ( type FPVMState interface { GetMemory() *memory.Memory + // GetHeap returns the current memory address at the top of the heap + GetHeap() uint32 + + // GetPreimageKey returns the most recently accessed preimage key + GetPreimageKey() common.Hash + + // GetPreimageOffset returns the current offset into the current preimage + GetPreimageOffset() uint32 + // GetPC returns the currently executing program counter GetPC() uint32 - // GetRegisters returns the currently active registers - GetRegisters() *[32]uint32 + // GetCpu returns the currently active cpu scalars, including the program counter + GetCpu() CpuScalars + + // GetRegistersRef returns a pointer to the currently active registers + GetRegistersRef() *[32]uint32 // GetStep returns the current VM step GetStep() uint64 @@ -24,6 +37,16 @@ type FPVMState interface { // GetExitCode returns the exit code GetExitCode() uint8 + // GetLastHint returns optional metadata which is not part of the VM state itself. + // It is used to remember the last pre-image hint, + // so a VM can start from any state without fetching prior pre-images, + // and instead just repeat the last hint on setup, + // to make sure pre-image requests can be served. + // The first 4 bytes are a uin32 length prefix. + // Warning: the hint MAY NOT BE COMPLETE. I.e. this is buffered, + // and should only be read when len(LastHint) > 4 && uint32(LastHint[:4]) <= len(LastHint[4:]) + GetLastHint() hexutil.Bytes + // EncodeWitness returns the witness for the current state and the state hash EncodeWitness() (witness []byte, hash common.Hash) } diff --git a/cannon/mipsevm/multithreaded/mips.go b/cannon/mipsevm/multithreaded/mips.go index a8eba1fc9bafb..7949bbe6c7716 100644 --- a/cannon/mipsevm/multithreaded/mips.go +++ b/cannon/mipsevm/multithreaded/mips.go @@ -14,9 +14,9 @@ import ( ) func (m *InstrumentedState) handleSyscall() error { - thread := m.state.getCurrentThread() + thread := m.state.GetCurrentThread() - syscallNum, a0, a1, a2, a3 := exec.GetSyscallArgs(m.state.GetRegisters()) + syscallNum, a0, a1, a2, a3 := exec.GetSyscallArgs(m.state.GetRegistersRef()) v0 := uint32(0) v1 := uint32(0) @@ -188,7 +188,7 @@ func (m *InstrumentedState) mipsStep() error { return nil } m.state.Step += 1 - thread := m.state.getCurrentThread() + thread := m.state.GetCurrentThread() // During wakeup traversal, search for the first thread blocked on the wakeup address. // Don't allow regular execution until we have found such a thread or else we have visited all threads. @@ -264,7 +264,7 @@ func (m *InstrumentedState) mipsStep() error { } // Exec the rest of the step logic - return exec.ExecMipsCoreStepLogic(m.state.getCpu(), m.state.GetRegisters(), m.state.Memory, insn, opcode, fun, m.memoryTracker, m.stackTracker) + return exec.ExecMipsCoreStepLogic(m.state.getCpuRef(), m.state.GetRegistersRef(), m.state.Memory, insn, opcode, fun, m.memoryTracker, m.stackTracker) } func (m *InstrumentedState) onWaitComplete(thread *ThreadState, isTimedOut bool) { diff --git a/cannon/mipsevm/multithreaded/stack.go b/cannon/mipsevm/multithreaded/stack.go index dae14b034f96f..ab710267fbe81 100644 --- a/cannon/mipsevm/multithreaded/stack.go +++ b/cannon/mipsevm/multithreaded/stack.go @@ -53,7 +53,7 @@ func (t *ThreadedStackTrackerImpl) Traceback() { } func (t *ThreadedStackTrackerImpl) getCurrentTracker() exec.TraceableStackTracker { - thread := t.state.getCurrentThread() + thread := t.state.GetCurrentThread() tracker, exists := t.trackersByThreadId[thread.ThreadId] if !exists { tracker = exec.NewStackTrackerUnsafe(t.state, t.meta) diff --git a/cannon/mipsevm/multithreaded/state.go b/cannon/mipsevm/multithreaded/state.go index 0604987046714..6602357747bf8 100644 --- a/cannon/mipsevm/multithreaded/state.go +++ b/cannon/mipsevm/multithreaded/state.go @@ -52,13 +52,6 @@ type State struct { NextThreadId uint32 `json:"nextThreadId"` // LastHint is optional metadata, and not part of the VM state itself. - // It is used to remember the last pre-image hint, - // so a VM can start from any state without fetching prior pre-images, - // and instead just repeat the last hint on setup, - // to make sure pre-image requests can be served. - // The first 4 bytes are a uin32 length prefix. - // Warning: the hint MAY NOT BE COMPLETE. I.e. this is buffered, - // and should only be read when len(LastHint) > 4 && uint32(LastHint[:4]) <= len(LastHint[4:]) LastHint hexutil.Bytes `json:"lastHint,omitempty"` } @@ -83,7 +76,7 @@ func CreateEmptyState() *State { func CreateInitialState(pc, heapStart uint32) *State { state := CreateEmptyState() - currentThread := state.getCurrentThread() + currentThread := state.GetCurrentThread() currentThread.Cpu.PC = pc currentThread.Cpu.NextPC = pc + 4 state.Heap = heapStart @@ -91,7 +84,7 @@ func CreateInitialState(pc, heapStart uint32) *State { return state } -func (s *State) getCurrentThread() *ThreadState { +func (s *State) GetCurrentThread() *ThreadState { activeStack := s.getActiveThreadStack() activeStackSize := len(activeStack) @@ -131,17 +124,22 @@ func (s *State) calculateThreadStackRoot(stack []*ThreadState) common.Hash { } func (s *State) GetPC() uint32 { - activeThread := s.getCurrentThread() + activeThread := s.GetCurrentThread() return activeThread.Cpu.PC } -func (s *State) GetRegisters() *[32]uint32 { - activeThread := s.getCurrentThread() - return &activeThread.Registers +func (s *State) GetCpu() mipsevm.CpuScalars { + activeThread := s.GetCurrentThread() + return activeThread.Cpu +} + +func (s *State) getCpuRef() *mipsevm.CpuScalars { + return &s.GetCurrentThread().Cpu } -func (s *State) getCpu() *mipsevm.CpuScalars { - return &s.getCurrentThread().Cpu +func (s *State) GetRegistersRef() *[32]uint32 { + activeThread := s.GetCurrentThread() + return &activeThread.Registers } func (s *State) GetExitCode() uint8 { return s.ExitCode } @@ -150,6 +148,10 @@ func (s *State) GetExited() bool { return s.Exited } func (s *State) GetStep() uint64 { return s.Step } +func (s *State) GetLastHint() hexutil.Bytes { + return s.LastHint +} + func (s *State) VMStatus() uint8 { return mipsevm.VmStatus(s.Exited, s.ExitCode) } @@ -158,6 +160,18 @@ func (s *State) GetMemory() *memory.Memory { return s.Memory } +func (s *State) GetHeap() uint32 { + return s.Heap +} + +func (s *State) GetPreimageKey() common.Hash { + return s.PreimageKey +} + +func (s *State) GetPreimageOffset() uint32 { + return s.PreimageOffset +} + func (s *State) EncodeWitness() ([]byte, common.Hash) { out := make([]byte, 0, STATE_WITNESS_SIZE) memRoot := s.Memory.MerkleRoot() @@ -214,6 +228,12 @@ func (sw StateWitness) StateHash() (common.Hash, error) { return stateHashFromWitness(sw), nil } +func GetStateHashFn() mipsevm.HashFn { + return func(sw []byte) (common.Hash, error) { + return StateWitness(sw).StateHash() + } +} + func stateHashFromWitness(sw []byte) common.Hash { if len(sw) != STATE_WITNESS_SIZE { panic("Invalid witness length") diff --git a/cannon/mipsevm/multithreaded/state_test.go b/cannon/mipsevm/multithreaded/state_test.go index e9dbef2db3462..c267416b8ae4c 100644 --- a/cannon/mipsevm/multithreaded/state_test.go +++ b/cannon/mipsevm/multithreaded/state_test.go @@ -133,7 +133,7 @@ func TestState_EmptyThreadsRoot(t *testing.T) { func TestState_EncodeThreadProof_SingleThread(t *testing.T) { state := CreateEmptyState() // Set some fields on the active thread - activeThread := state.getCurrentThread() + activeThread := state.GetCurrentThread() activeThread.Cpu.PC = 4 activeThread.Cpu.NextPC = 8 activeThread.Cpu.HI = 11 @@ -181,7 +181,7 @@ func TestState_EncodeThreadProof_MultipleThreads(t *testing.T) { expectedRoot = crypto.Keccak256Hash(hashData) } - expectedProof := append([]byte{}, state.getCurrentThread().serializeThread()[:]...) + expectedProof := append([]byte{}, state.GetCurrentThread().serializeThread()[:]...) expectedProof = append(expectedProof, expectedRoot[:]...) actualProof := state.EncodeThreadProof() diff --git a/cannon/mipsevm/program/patch.go b/cannon/mipsevm/program/patch.go index c845488dd7b9d..52a262fee585b 100644 --- a/cannon/mipsevm/program/patch.go +++ b/cannon/mipsevm/program/patch.go @@ -64,7 +64,7 @@ func PatchStack(st mipsevm.FPVMState) error { if err := st.GetMemory().SetMemoryRange(sp-4*memory.PageSize, bytes.NewReader(make([]byte, 5*memory.PageSize))); err != nil { return fmt.Errorf("failed to allocate page for stack content") } - st.GetRegisters()[29] = sp + st.GetRegistersRef()[29] = sp storeMem := func(addr uint32, v uint32) { var dat [4]byte diff --git a/cannon/mipsevm/singlethreaded/state.go b/cannon/mipsevm/singlethreaded/state.go index fc23b072b0c9b..638dcf6f3b6b3 100644 --- a/cannon/mipsevm/singlethreaded/state.go +++ b/cannon/mipsevm/singlethreaded/state.go @@ -34,13 +34,6 @@ type State struct { Registers [32]uint32 `json:"registers"` // LastHint is optional metadata, and not part of the VM state itself. - // It is used to remember the last pre-image hint, - // so a VM can start from any state without fetching prior pre-images, - // and instead just repeat the last hint on setup, - // to make sure pre-image requests can be served. - // The first 4 bytes are a uin32 length prefix. - // Warning: the hint MAY NOT BE COMPLETE. I.e. this is buffered, - // and should only be read when len(LastHint) > 4 && uint32(LastHint[:4]) <= len(LastHint[4:]) LastHint hexutil.Bytes `json:"lastHint,omitempty"` } @@ -130,7 +123,9 @@ func (s *State) UnmarshalJSON(data []byte) error { func (s *State) GetPC() uint32 { return s.Cpu.PC } -func (s *State) GetRegisters() *[32]uint32 { return &s.Registers } +func (s *State) GetCpu() mipsevm.CpuScalars { return s.Cpu } + +func (s *State) GetRegistersRef() *[32]uint32 { return &s.Registers } func (s *State) GetExitCode() uint8 { return s.ExitCode } @@ -138,6 +133,10 @@ func (s *State) GetExited() bool { return s.Exited } func (s *State) GetStep() uint64 { return s.Step } +func (s *State) GetLastHint() hexutil.Bytes { + return s.LastHint +} + func (s *State) VMStatus() uint8 { return mipsevm.VmStatus(s.Exited, s.ExitCode) } @@ -146,6 +145,18 @@ func (s *State) GetMemory() *memory.Memory { return s.Memory } +func (s *State) GetHeap() uint32 { + return s.Heap +} + +func (s *State) GetPreimageKey() common.Hash { + return s.PreimageKey +} + +func (s *State) GetPreimageOffset() uint32 { + return s.PreimageOffset +} + func (s *State) EncodeWitness() ([]byte, common.Hash) { out := make([]byte, 0, STATE_WITNESS_SIZE) memRoot := s.Memory.MerkleRoot() diff --git a/cannon/mipsevm/tests/evm_common_test.go b/cannon/mipsevm/tests/evm_common_test.go new file mode 100644 index 0000000000000..69dce9a8bf6c6 --- /dev/null +++ b/cannon/mipsevm/tests/evm_common_test.go @@ -0,0 +1,582 @@ +package tests + +import ( + "bytes" + "fmt" + "io" + "os" + "path" + "strings" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/tracing" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/stretchr/testify/require" + + "github.com/ethereum-optimism/optimism/cannon/mipsevm" + "github.com/ethereum-optimism/optimism/cannon/mipsevm/exec" + "github.com/ethereum-optimism/optimism/cannon/mipsevm/memory" + "github.com/ethereum-optimism/optimism/cannon/mipsevm/program" + "github.com/ethereum-optimism/optimism/cannon/mipsevm/testutil" +) + +func TestEVM(t *testing.T) { + testFiles, err := os.ReadDir("open_mips_tests/test/bin") + require.NoError(t, err) + + var tracer *tracing.Hooks // no-tracer by default, but test_util.MarkdownTracer + + cases := GetMipsVersionTestCases(t) + skippedTests := map[string][]string{ + "multi-threaded": []string{"clone.bin"}, + "single-threaded": []string{}, + } + + 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()) + } + } + + oracle := testutil.SelectOracleFixture(t, f.Name()) + // Short-circuit early for exit_group.bin + exitGroup := f.Name() == "exit_group.bin" + expectPanic := strings.HasSuffix(f.Name(), "panic.bin") + + evm := testutil.NewMIPSEVM(c.Contracts) + evm.SetTracer(tracer) + evm.SetLocalOracle(oracle) + testutil.LogStepFailureAtCleanup(t, evm) + + fn := path.Join("open_mips_tests/test/bin", f.Name()) + programMem, err := os.ReadFile(fn) + require.NoError(t, err) + + goVm := c.VMFactory(oracle, os.Stdout, os.Stderr, testutil.CreateLogger()) + state := goVm.GetState() + err = state.GetMemory().SetMemoryRange(0, bytes.NewReader(programMem)) + require.NoError(t, err, "load program into state") + + // set the return address ($ra) to jump into when test completes + state.GetRegistersRef()[31] = testutil.EndAddr + + // Catch panics and check if they are expected + defer func() { + if r := recover(); r != nil { + if expectPanic { + // Success + } else { + t.Errorf("unexpected panic: %v", r) + } + } + }() + + for i := 0; i < 1000; i++ { + curStep := goVm.GetState().GetStep() + if goVm.GetState().GetPC() == testutil.EndAddr { + break + } + if exitGroup && goVm.GetState().GetExited() { + break + } + insn := state.GetMemory().GetMemory(state.GetPC()) + t.Logf("step: %4d pc: 0x%08x insn: 0x%08x", state.GetStep(), state.GetPC(), insn) + + stepWitness, err := goVm.Step(true) + require.NoError(t, err) + evmPost := evm.Step(t, stepWitness, curStep, c.StateHashFn) + // verify the post-state matches. + // TODO: maybe more readable to decode the evmPost state, and do attribute-wise comparison. + goPost, _ := goVm.GetState().EncodeWitness() + require.Equalf(t, hexutil.Bytes(goPost).String(), hexutil.Bytes(evmPost).String(), + "mipsevm produced different state than EVM at step %d", state.GetStep()) + } + if exitGroup { + require.NotEqual(t, uint32(testutil.EndAddr), goVm.GetState().GetPC(), "must not reach end") + require.True(t, goVm.GetState().GetExited(), "must set exited state") + require.Equal(t, uint8(1), goVm.GetState().GetExitCode(), "must exit with 1") + } else if expectPanic { + require.NotEqual(t, uint32(testutil.EndAddr), state.GetPC(), "must not reach end") + } else { + require.Equal(t, uint32(testutil.EndAddr), state.GetPC(), "must reach end") + // inspect test result + done, result := state.GetMemory().GetMemory(testutil.BaseAddrEnd+4), state.GetMemory().GetMemory(testutil.BaseAddrEnd+8) + require.Equal(t, done, uint32(1), "must be done") + require.Equal(t, result, uint32(1), "must have success result") + } + }) + } + } +} + +func TestEVMSingleStep(t *testing.T) { + var tracer *tracing.Hooks + + versions := GetMipsVersionTestCases(t) + cases := []struct { + name string + pc uint32 + nextPC uint32 + insn uint32 + }{ + {"j MSB set target", 0, 4, 0x0A_00_00_02}, // j 0x02_00_00_02 + {"j non-zero PC region", 0x10000000, 0x10000004, 0x08_00_00_02}, // j 0x2 + {"jal MSB set target", 0, 4, 0x0E_00_00_02}, // jal 0x02_00_00_02 + {"jal non-zero PC region", 0x10000000, 0x10000004, 0x0C_00_00_02}, // jal 0x2 + } + + for _, v := range versions { + for _, tt := range cases { + testName := fmt.Sprintf("%v (%v)", tt.name, v.Name) + t.Run(testName, func(t *testing.T) { + goVm := v.VMFactory(nil, os.Stdout, os.Stderr, testutil.CreateLogger(), WithPC(tt.pc), WithNextPC(tt.nextPC)) + state := goVm.GetState() + state.GetMemory().SetMemory(tt.pc, tt.insn) + curStep := state.GetStep() + + stepWitness, err := goVm.Step(true) + require.NoError(t, err) + + evm := testutil.NewMIPSEVM(v.Contracts) + evm.SetTracer(tracer) + testutil.LogStepFailureAtCleanup(t, evm) + + evmPost := evm.Step(t, stepWitness, curStep, v.StateHashFn) + goPost, _ := goVm.GetState().EncodeWitness() + require.Equal(t, hexutil.Bytes(goPost).String(), hexutil.Bytes(evmPost).String(), + "mipsevm produced different state than EVM") + }) + } + } +} + +func TestEVM_MMap(t *testing.T) { + var tracer *tracing.Hooks + + versions := GetMipsVersionTestCases(t) + cases := []struct { + name string + heap uint32 + address uint32 + size uint32 + shouldFail bool + expectedHeap uint32 + }{ + {name: "Increment heap by max value", heap: program.HEAP_START, address: 0, size: ^uint32(0), shouldFail: true}, + {name: "Increment heap to 0", heap: program.HEAP_START, address: 0, size: ^uint32(0) - program.HEAP_START + 1, shouldFail: true}, + {name: "Increment heap to previous page", heap: program.HEAP_START, address: 0, size: ^uint32(0) - program.HEAP_START - memory.PageSize + 1, shouldFail: true}, + {name: "Increment max page size", heap: program.HEAP_START, address: 0, size: ^uint32(0) & ^uint32(memory.PageAddrMask), shouldFail: true}, + {name: "Increment max page size from 0", heap: 0, address: 0, size: ^uint32(0) & ^uint32(memory.PageAddrMask), shouldFail: true}, + {name: "Increment heap at limit", heap: program.HEAP_END, address: 0, size: 1, shouldFail: true}, + {name: "Increment heap to limit", heap: program.HEAP_END - memory.PageSize, address: 0, size: 1, shouldFail: false, expectedHeap: program.HEAP_END}, + {name: "Increment heap within limit", heap: program.HEAP_END - 2*memory.PageSize, address: 0, size: 1, shouldFail: false, expectedHeap: program.HEAP_END - memory.PageSize}, + {name: "Request specific address", heap: program.HEAP_START, address: 0x50_00_00_00, size: 0, shouldFail: false, expectedHeap: program.HEAP_START}, + } + + for _, v := range versions { + for _, c := range cases { + testName := fmt.Sprintf("%v (%v)", c.name, v.Name) + t.Run(testName, func(t *testing.T) { + goVm := v.VMFactory(nil, os.Stdout, os.Stderr, testutil.CreateLogger(), WithHeap(c.heap)) + state := goVm.GetState() + + state.GetMemory().SetMemory(state.GetPC(), syscallInsn) + *state.GetRegistersRef() = testutil.RandomRegisters(77) + state.GetRegistersRef()[2] = exec.SysMmap + state.GetRegistersRef()[4] = c.address + state.GetRegistersRef()[5] = c.size + step := state.GetStep() + + expectedRegisters := testutil.CopyRegisters(state) + expectedHeap := state.GetHeap() + expectedMemoryRoot := state.GetMemory().MerkleRoot() + if c.shouldFail { + expectedRegisters[2] = exec.SysErrorSignal + expectedRegisters[7] = exec.MipsEINVAL + } else { + expectedHeap = c.expectedHeap + if c.address == 0 { + expectedRegisters[2] = state.GetHeap() + expectedRegisters[7] = 0 + } else { + expectedRegisters[2] = c.address + expectedRegisters[7] = 0 + } + } + + stepWitness, err := goVm.Step(true) + require.NoError(t, err) + + // Check expectations + require.Equal(t, step+1, state.GetStep()) + require.Equal(t, expectedHeap, state.GetHeap()) + require.Equal(t, expectedRegisters, state.GetRegistersRef()) + require.Equal(t, expectedMemoryRoot, state.GetMemory().MerkleRoot()) + require.Equal(t, common.Hash{}, state.GetPreimageKey()) + require.Equal(t, uint32(0), state.GetPreimageOffset()) + require.Equal(t, uint32(4), state.GetCpu().PC) + require.Equal(t, uint32(8), state.GetCpu().NextPC) + require.Equal(t, uint32(0), state.GetCpu().HI) + require.Equal(t, uint32(0), state.GetCpu().LO) + require.Equal(t, false, state.GetExited()) + require.Equal(t, uint8(0), state.GetExitCode()) + require.Equal(t, hexutil.Bytes(nil), state.GetLastHint()) + + evm := testutil.NewMIPSEVM(v.Contracts) + evm.SetTracer(tracer) + testutil.LogStepFailureAtCleanup(t, evm) + + evmPost := evm.Step(t, stepWitness, step, v.StateHashFn) + goPost, _ := goVm.GetState().EncodeWitness() + require.Equal(t, hexutil.Bytes(goPost).String(), hexutil.Bytes(evmPost).String(), + "mipsevm produced different state than EVM") + }) + } + } +} + +func TestEVMSysWriteHint(t *testing.T) { + var tracer *tracing.Hooks + + versions := GetMipsVersionTestCases(t) + cases := []struct { + name string + memOffset int // Where the hint data is stored in memory + hintData []byte // Hint data stored in memory at memOffset + bytesToWrite int // How many bytes of hintData to write + lastHint []byte // The buffer that stores lastHint in the state + expectedHints [][]byte // The hints we expect to be processed + }{ + { + name: "write 1 full hint at beginning of page", + memOffset: 4096, + hintData: []byte{ + 0, 0, 0, 6, // Length prefix + 0xAA, 0xAA, 0xAA, 0xAA, 0xBB, 0xBB, // Hint data + }, + bytesToWrite: 10, + lastHint: nil, + expectedHints: [][]byte{ + {0xAA, 0xAA, 0xAA, 0xAA, 0xBB, 0xBB}, + }, + }, + { + name: "write 1 full hint across page boundary", + memOffset: 4092, + hintData: []byte{ + 0, 0, 0, 8, // Length prefix + 0xAA, 0xAA, 0xAA, 0xAA, 0xBB, 0xBB, 0xBB, 0xBB, // Hint data + }, + bytesToWrite: 12, + lastHint: nil, + expectedHints: [][]byte{ + {0xAA, 0xAA, 0xAA, 0xAA, 0xBB, 0xBB, 0xBB, 0xBB}, + }, + }, + { + name: "write 2 full hints", + memOffset: 5012, + hintData: []byte{ + 0, 0, 0, 6, // Length prefix + 0xAA, 0xAA, 0xAA, 0xAA, 0xBB, 0xBB, // Hint data + 0, 0, 0, 8, // Length prefix + 0xAA, 0xAA, 0xAA, 0xAA, 0xBB, 0xBB, 0xBB, 0xBB, // Hint data + }, + bytesToWrite: 22, + lastHint: nil, + expectedHints: [][]byte{ + {0xAA, 0xAA, 0xAA, 0xAA, 0xBB, 0xBB}, + {0xAA, 0xAA, 0xAA, 0xAA, 0xBB, 0xBB, 0xBB, 0xBB}, + }, + }, + { + name: "write a single partial hint", + memOffset: 4092, + hintData: []byte{ + 0, 0, 0, 6, // Length prefix + 0xAA, 0xAA, 0xAA, 0xAA, 0xBB, 0xBB, // Hint data + }, + bytesToWrite: 8, + lastHint: nil, + expectedHints: nil, + }, + { + name: "write 1 full, 1 partial hint", + memOffset: 5012, + hintData: []byte{ + 0, 0, 0, 6, // Length prefix + 0xAA, 0xAA, 0xAA, 0xAA, 0xBB, 0xBB, // Hint data + 0, 0, 0, 8, // Length prefix + 0xAA, 0xAA, 0xAA, 0xAA, 0xBB, 0xBB, 0xBB, 0xBB, // Hint data + }, + bytesToWrite: 16, + lastHint: nil, + expectedHints: [][]byte{ + {0xAA, 0xAA, 0xAA, 0xAA, 0xBB, 0xBB}, + }, + }, + { + name: "write a single partial hint to large capacity lastHint buffer", + memOffset: 4092, + hintData: []byte{ + 0, 0, 0, 6, // Length prefix + 0xAA, 0xAA, 0xAA, 0xAA, 0xBB, 0xBB, // Hint data + }, + bytesToWrite: 8, + lastHint: make([]byte, 0, 4096), + expectedHints: nil, + }, + { + name: "write full hint to large capacity lastHint buffer", + memOffset: 5012, + hintData: []byte{ + 0, 0, 0, 6, // Length prefix + 0xAA, 0xAA, 0xAA, 0xAA, 0xBB, 0xBB, // Hint data + }, + bytesToWrite: 10, + lastHint: make([]byte, 0, 4096), + expectedHints: [][]byte{ + {0xAA, 0xAA, 0xAA, 0xAA, 0xBB, 0xBB}, + }, + }, + { + name: "write multiple hints to large capacity lastHint buffer", + memOffset: 4092, + hintData: []byte{ + 0, 0, 0, 8, // Length prefix + 0xAA, 0xAA, 0xAA, 0xAA, 0xBB, 0xBB, 0xCC, 0xCC, // Hint data + 0, 0, 0, 8, // Length prefix + 0xAA, 0xAA, 0xAA, 0xAA, 0xBB, 0xBB, 0xBB, 0xBB, // Hint data + }, + bytesToWrite: 24, + lastHint: make([]byte, 0, 4096), + expectedHints: [][]byte{ + {0xAA, 0xAA, 0xAA, 0xAA, 0xBB, 0xBB, 0xCC, 0xCC}, + {0xAA, 0xAA, 0xAA, 0xAA, 0xBB, 0xBB, 0xBB, 0xBB}, + }, + }, + { + name: "write remaining hint data to non-empty lastHint buffer", + memOffset: 4092, + hintData: []byte{ + 0xAA, 0xAA, 0xAA, 0xAA, 0xBB, 0xBB, 0xCC, 0xCC, // Hint data + }, + bytesToWrite: 8, + lastHint: []byte{0, 0, 0, 8}, + expectedHints: [][]byte{ + {0xAA, 0xAA, 0xAA, 0xAA, 0xBB, 0xBB, 0xCC, 0xCC}, + }, + }, + { + name: "write partial hint data to non-empty lastHint buffer", + memOffset: 4092, + hintData: []byte{ + 0xAA, 0xAA, 0xAA, 0xAA, 0xBB, 0xBB, 0xCC, 0xCC, // Hint data + }, + bytesToWrite: 4, + lastHint: []byte{0, 0, 0, 8}, + expectedHints: nil, + }, + } + + const ( + insn = uint32(0x00_00_00_0C) // syscall instruction + ) + + for _, v := range versions { + for _, tt := range cases { + testName := fmt.Sprintf("%v (%v)", tt.name, v.Name) + t.Run(testName, func(t *testing.T) { + oracle := hintTrackingOracle{} + goVm := v.VMFactory(&oracle, os.Stdout, os.Stderr, testutil.CreateLogger(), WithLastHint(tt.lastHint)) + state := goVm.GetState() + state.GetRegistersRef()[2] = exec.SysWrite + state.GetRegistersRef()[4] = exec.FdHintWrite + state.GetRegistersRef()[5] = uint32(tt.memOffset) + state.GetRegistersRef()[6] = uint32(tt.bytesToWrite) + + err := state.GetMemory().SetMemoryRange(uint32(tt.memOffset), bytes.NewReader(tt.hintData)) + require.NoError(t, err) + state.GetMemory().SetMemory(0, insn) + curStep := state.GetStep() + + stepWitness, err := goVm.Step(true) + require.NoError(t, err) + require.Equal(t, tt.expectedHints, oracle.hints) + + evm := testutil.NewMIPSEVM(v.Contracts) + evm.SetTracer(tracer) + testutil.LogStepFailureAtCleanup(t, evm) + + evmPost := evm.Step(t, stepWitness, curStep, v.StateHashFn) + goPost, _ := goVm.GetState().EncodeWitness() + require.Equal(t, hexutil.Bytes(goPost).String(), hexutil.Bytes(evmPost).String(), + "mipsevm produced different state than EVM") + }) + } + } +} + +func TestEVMFault(t *testing.T) { + var tracer *tracing.Hooks // no-tracer by default, but see test_util.MarkdownTracer + sender := common.Address{0x13, 0x37} + + versions := GetMipsVersionTestCases(t) + cases := []struct { + name string + nextPC uint32 + insn uint32 + }{ + {"illegal instruction", 0, 0xFF_FF_FF_FF}, + {"branch in delay-slot", 8, 0x11_02_00_03}, + {"jump in delay-slot", 8, 0x0c_00_00_0c}, + } + + for _, v := range versions { + for _, tt := range cases { + testName := fmt.Sprintf("%v (%v)", tt.name, v.Name) + t.Run(testName, func(t *testing.T) { + env, evmState := testutil.NewEVMEnv(v.Contracts) + env.Config.Tracer = tracer + + goVm := v.VMFactory(nil, os.Stdout, os.Stderr, testutil.CreateLogger(), WithNextPC(tt.nextPC)) + state := goVm.GetState() + state.GetMemory().SetMemory(0, tt.insn) + // set the return address ($ra) to jump into when test completes + state.GetRegistersRef()[31] = testutil.EndAddr + + require.Panics(t, func() { _, _ = goVm.Step(true) }) + + insnProof := state.GetMemory().MerkleProof(0) + encodedWitness, _ := state.EncodeWitness() + stepWitness := &mipsevm.StepWitness{ + State: encodedWitness, + ProofData: insnProof[:], + } + input := testutil.EncodeStepInput(t, stepWitness, mipsevm.LocalContext{}, v.Contracts.Artifacts.MIPS) + startingGas := uint64(30_000_000) + + _, _, err := env.Call(vm.AccountRef(sender), v.Contracts.Addresses.MIPS, input, startingGas, common.U2560) + require.EqualValues(t, err, vm.ErrExecutionReverted) + logs := evmState.Logs() + require.Equal(t, 0, len(logs)) + }) + } + } +} + +func TestHelloEVM(t *testing.T) { + var tracer *tracing.Hooks // no-tracer by default, but see test_util.MarkdownTracer + versions := GetMipsVersionTestCases(t) + + for _, v := range versions { + t.Run(v.Name, func(t *testing.T) { + evm := testutil.NewMIPSEVM(v.Contracts) + evm.SetTracer(tracer) + testutil.LogStepFailureAtCleanup(t, evm) + + var stdOutBuf, stdErrBuf bytes.Buffer + elfFile := "../../testdata/example/bin/hello.elf" + goVm := v.ElfVMFactory(t, elfFile, nil, io.MultiWriter(&stdOutBuf, os.Stdout), io.MultiWriter(&stdErrBuf, os.Stderr), testutil.CreateLogger()) + state := goVm.GetState() + + start := time.Now() + for i := 0; i < 400_000; i++ { + curStep := goVm.GetState().GetStep() + if goVm.GetState().GetExited() { + break + } + insn := state.GetMemory().GetMemory(state.GetPC()) + if i%1000 == 0 { // avoid spamming test logs, we are executing many steps + t.Logf("step: %4d pc: 0x%08x insn: 0x%08x", state.GetStep(), state.GetPC(), insn) + } + + stepWitness, err := goVm.Step(true) + require.NoError(t, err) + evmPost := evm.Step(t, stepWitness, curStep, v.StateHashFn) + // verify the post-state matches. + // TODO: maybe more readable to decode the evmPost state, and do attribute-wise comparison. + goPost, _ := goVm.GetState().EncodeWitness() + require.Equal(t, hexutil.Bytes(goPost).String(), hexutil.Bytes(evmPost).String(), + "mipsevm produced different state than EVM") + } + end := time.Now() + delta := end.Sub(start) + t.Logf("test took %s, %d instructions, %s per instruction", delta, state.GetStep(), delta/time.Duration(state.GetStep())) + + require.True(t, state.GetExited(), "must complete program") + require.Equal(t, uint8(0), state.GetExitCode(), "exit with 0") + + require.Equal(t, "hello world!\n", stdOutBuf.String(), "stdout says hello") + require.Equal(t, "", stdErrBuf.String(), "stderr silent") + }) + } +} + +func TestClaimEVM(t *testing.T) { + var tracer *tracing.Hooks // no-tracer by default, but see test_util.MarkdownTracer + versions := GetMipsVersionTestCases(t) + + for _, v := range versions { + t.Run(v.Name, func(t *testing.T) { + evm := testutil.NewMIPSEVM(v.Contracts) + evm.SetTracer(tracer) + testutil.LogStepFailureAtCleanup(t, evm) + + oracle, expectedStdOut, expectedStdErr := testutil.ClaimTestOracle(t) + + var stdOutBuf, stdErrBuf bytes.Buffer + elfFile := "../../testdata/example/bin/claim.elf" + goVm := v.ElfVMFactory(t, elfFile, oracle, io.MultiWriter(&stdOutBuf, os.Stdout), io.MultiWriter(&stdErrBuf, os.Stderr), testutil.CreateLogger()) + state := goVm.GetState() + + for i := 0; i < 2000_000; i++ { + curStep := goVm.GetState().GetStep() + if goVm.GetState().GetExited() { + break + } + + insn := state.GetMemory().GetMemory(state.GetPC()) + if i%1000 == 0 { // avoid spamming test logs, we are executing many steps + t.Logf("step: %4d pc: 0x%08x insn: 0x%08x", state.GetStep(), state.GetPC(), insn) + } + + stepWitness, err := goVm.Step(true) + require.NoError(t, err) + + evmPost := evm.Step(t, stepWitness, curStep, v.StateHashFn) + + goPost, _ := goVm.GetState().EncodeWitness() + require.Equal(t, hexutil.Bytes(goPost).String(), hexutil.Bytes(evmPost).String(), + "mipsevm produced different state than EVM") + } + + require.True(t, state.GetExited(), "must complete program") + require.Equal(t, uint8(0), state.GetExitCode(), "exit with 0") + + require.Equal(t, expectedStdOut, stdOutBuf.String(), "stdout") + require.Equal(t, expectedStdErr, stdErrBuf.String(), "stderr") + }) + } +} + +type hintTrackingOracle struct { + hints [][]byte +} + +func (t *hintTrackingOracle) Hint(v []byte) { + t.hints = append(t.hints, v) +} + +func (t *hintTrackingOracle) GetPreimage(k [32]byte) []byte { + return nil +} diff --git a/cannon/mipsevm/tests/evm_multithreaded_test.go b/cannon/mipsevm/tests/evm_multithreaded_test.go new file mode 100644 index 0000000000000..32d90b74211c7 --- /dev/null +++ b/cannon/mipsevm/tests/evm_multithreaded_test.go @@ -0,0 +1,70 @@ +package tests + +import ( + "os" + "testing" + + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/tracing" + "github.com/stretchr/testify/require" + + "github.com/ethereum-optimism/optimism/cannon/mipsevm" + "github.com/ethereum-optimism/optimism/cannon/mipsevm/exec" + "github.com/ethereum-optimism/optimism/cannon/mipsevm/multithreaded" + "github.com/ethereum-optimism/optimism/cannon/mipsevm/testutil" +) + +func TestEVM_CloneFlags(t *testing.T) { + contracts := testutil.TestContractsSetup(t, testutil.MipsMultithreaded) + var tracer *tracing.Hooks + + cases := []struct { + name string + flags uint32 + valid bool + }{ + {"the supported flags bitmask", exec.ValidCloneFlags, true}, + {"no flags", 0, false}, + {"all flags", ^uint32(0), false}, + {"all unsupported flags", ^uint32(exec.ValidCloneFlags), false}, + {"a few supported flags", exec.CloneFs | exec.CloneSysvsem, false}, + {"one supported flag", exec.CloneFs, false}, + {"mixed supported and unsupported flags", exec.CloneFs | exec.CloneParentSettid, false}, + {"a single unsupported flag", exec.CloneUntraced, false}, + {"multiple unsupported flags", exec.CloneUntraced | exec.CloneParentSettid, false}, + } + + const insn = uint32(0x00_00_00_0C) // syscall instruction + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + state := multithreaded.CreateEmptyState() + state.Memory.SetMemory(state.GetPC(), insn) + state.GetRegistersRef()[2] = exec.SysClone // Set syscall number + state.GetRegistersRef()[4] = tt.flags // Set first argument + curStep := state.Step + + var err error + var stepWitness *mipsevm.StepWitness + us := multithreaded.NewInstrumentedState(state, nil, os.Stdout, os.Stderr, nil) + if !tt.valid { + // The VM should exit + stepWitness, err = us.Step(true) + require.NoError(t, err) + require.Equal(t, true, us.GetState().GetExited()) + require.Equal(t, uint8(mipsevm.VMStatusPanic), us.GetState().GetExitCode()) + } else { + stepWitness, err = us.Step(true) + require.NoError(t, err) + } + + evm := testutil.NewMIPSEVM(contracts) + evm.SetTracer(tracer) + testutil.LogStepFailureAtCleanup(t, evm) + + evmPost := evm.Step(t, stepWitness, curStep, multithreaded.GetStateHashFn()) + goPost, _ := us.GetState().EncodeWitness() + require.Equal(t, hexutil.Bytes(goPost).String(), hexutil.Bytes(evmPost).String(), + "mipsevm produced different state than EVM") + }) + } +} diff --git a/cannon/mipsevm/tests/evm_test.go b/cannon/mipsevm/tests/evm_test.go deleted file mode 100644 index 4f7faf32c348f..0000000000000 --- a/cannon/mipsevm/tests/evm_test.go +++ /dev/null @@ -1,615 +0,0 @@ -package tests - -import ( - "bytes" - "io" - "os" - "path" - "strings" - "testing" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/core/tracing" - "github.com/ethereum/go-ethereum/core/vm" - "github.com/stretchr/testify/require" - - "github.com/ethereum-optimism/optimism/cannon/mipsevm" - "github.com/ethereum-optimism/optimism/cannon/mipsevm/exec" - "github.com/ethereum-optimism/optimism/cannon/mipsevm/memory" - "github.com/ethereum-optimism/optimism/cannon/mipsevm/multithreaded" - "github.com/ethereum-optimism/optimism/cannon/mipsevm/program" - "github.com/ethereum-optimism/optimism/cannon/mipsevm/singlethreaded" - "github.com/ethereum-optimism/optimism/cannon/mipsevm/testutil" -) - -func testContractsSetup(t require.TestingT) (*testutil.Artifacts, *testutil.Addresses) { - artifacts, err := testutil.LoadArtifacts() - require.NoError(t, err) - - addrs := &testutil.Addresses{ - MIPS: common.Address{0: 0xff, 19: 1}, - Oracle: common.Address{0: 0xff, 19: 2}, - Sender: common.Address{0x13, 0x37}, - FeeRecipient: common.Address{0xaa}, - } - - return artifacts, addrs -} - -func TestEVM(t *testing.T) { - testFiles, err := os.ReadDir("open_mips_tests/test/bin") - require.NoError(t, err) - - contracts, addrs := testContractsSetup(t) - var tracer *tracing.Hooks // no-tracer by default, but test_util.MarkdownTracer - - for _, f := range testFiles { - t.Run(f.Name(), func(t *testing.T) { - oracle := testutil.SelectOracleFixture(t, f.Name()) - // Short-circuit early for exit_group.bin - exitGroup := f.Name() == "exit_group.bin" - expectPanic := strings.HasSuffix(f.Name(), "panic.bin") - - evm := testutil.NewMIPSEVM(contracts, addrs) - evm.SetTracer(tracer) - evm.SetLocalOracle(oracle) - testutil.LogStepFailureAtCleanup(t, evm) - - fn := path.Join("open_mips_tests/test/bin", f.Name()) - programMem, err := os.ReadFile(fn) - require.NoError(t, err) - state := &singlethreaded.State{Cpu: mipsevm.CpuScalars{PC: 0, NextPC: 4}, Memory: memory.NewMemory()} - err = state.Memory.SetMemoryRange(0, bytes.NewReader(programMem)) - require.NoError(t, err, "load program into state") - - // set the return address ($ra) to jump into when test completes - state.Registers[31] = testutil.EndAddr - - goState := singlethreaded.NewInstrumentedState(state, oracle, os.Stdout, os.Stderr, nil) - - // Catch panics and check if they are expected - defer func() { - if r := recover(); r != nil { - if expectPanic { - // Success - } else { - t.Errorf("unexpected panic: %v", r) - } - } - }() - - for i := 0; i < 1000; i++ { - curStep := goState.GetState().GetStep() - if goState.GetState().GetPC() == testutil.EndAddr { - break - } - if exitGroup && goState.GetState().GetExited() { - break - } - insn := state.Memory.GetMemory(state.Cpu.PC) - t.Logf("step: %4d pc: 0x%08x insn: 0x%08x", state.Step, state.Cpu.PC, insn) - - stepWitness, err := goState.Step(true) - require.NoError(t, err) - evmPost := evm.Step(t, stepWitness, curStep, singlethreaded.GetStateHashFn()) - // verify the post-state matches. - // TODO: maybe more readable to decode the evmPost state, and do attribute-wise comparison. - goPost, _ := goState.GetState().EncodeWitness() - require.Equalf(t, hexutil.Bytes(goPost).String(), hexutil.Bytes(evmPost).String(), - "mipsevm produced different state than EVM at step %d", state.Step) - } - if exitGroup { - require.NotEqual(t, uint32(testutil.EndAddr), goState.GetState().GetPC(), "must not reach end") - require.True(t, goState.GetState().GetExited(), "must set exited state") - require.Equal(t, uint8(1), goState.GetState().GetExitCode(), "must exit with 1") - } else if expectPanic { - require.NotEqual(t, uint32(testutil.EndAddr), goState.GetState().GetPC(), "must not reach end") - } else { - require.Equal(t, uint32(testutil.EndAddr), state.Cpu.PC, "must reach end") - // inspect test result - done, result := state.Memory.GetMemory(testutil.BaseAddrEnd+4), state.Memory.GetMemory(testutil.BaseAddrEnd+8) - require.Equal(t, done, uint32(1), "must be done") - require.Equal(t, result, uint32(1), "must have success result") - } - }) - } -} - -func TestEVM_CloneFlags(t *testing.T) { - //contracts, addrs := testContractsSetup(t) - //var tracer *tracing.Hooks - - cases := []struct { - name string - flags uint32 - valid bool - }{ - {"the supported flags bitmask", exec.ValidCloneFlags, true}, - {"no flags", 0, false}, - {"all flags", ^uint32(0), false}, - {"all unsupported flags", ^uint32(exec.ValidCloneFlags), false}, - {"a few supported flags", exec.CloneFs | exec.CloneSysvsem, false}, - {"one supported flag", exec.CloneFs, false}, - {"mixed supported and unsupported flags", exec.CloneFs | exec.CloneParentSettid, false}, - {"a single unsupported flag", exec.CloneUntraced, false}, - {"multiple unsupported flags", exec.CloneUntraced | exec.CloneParentSettid, false}, - } - - const insn = uint32(0x00_00_00_0C) // syscall instruction - for _, tt := range cases { - t.Run(tt.name, func(t *testing.T) { - state := multithreaded.CreateEmptyState() - state.Memory.SetMemory(state.GetPC(), insn) - state.GetRegisters()[2] = exec.SysClone // Set syscall number - state.GetRegisters()[4] = tt.flags // Set first argument - //curStep := state.Step - - us := multithreaded.NewInstrumentedState(state, nil, os.Stdout, os.Stderr, nil) - if !tt.valid { - // The VM should exit - _, err := us.Step(true) - require.NoError(t, err) - require.Equal(t, true, us.GetState().GetExited()) - require.Equal(t, uint8(mipsevm.VMStatusPanic), us.GetState().GetExitCode()) - } else { - /*stepWitness*/ _, err := us.Step(true) - require.NoError(t, err) - } - - // TODO: Validate EVM execution once onchain implementation is ready - //evm := testutil.NewMIPSEVM(contracts, addrs) - //evm.SetTracer(tracer) - //testutil.LogStepFailureAtCleanup(t, evm) - // - //evmPost := evm.Step(t, stepWitness, curStep, singlethreaded.GetStateHashFn()) - //goPost, _ := us.GetState().EncodeWitness() - //require.Equal(t, hexutil.Bytes(goPost).String(), hexutil.Bytes(evmPost).String(), - // "mipsevm produced different state than EVM") - }) - } -} - -func TestEVMSingleStep(t *testing.T) { - contracts, addrs := testContractsSetup(t) - var tracer *tracing.Hooks - - cases := []struct { - name string - pc uint32 - nextPC uint32 - insn uint32 - }{ - {"j MSB set target", 0, 4, 0x0A_00_00_02}, // j 0x02_00_00_02 - {"j non-zero PC region", 0x10000000, 0x10000004, 0x08_00_00_02}, // j 0x2 - {"jal MSB set target", 0, 4, 0x0E_00_00_02}, // jal 0x02_00_00_02 - {"jal non-zero PC region", 0x10000000, 0x10000004, 0x0C_00_00_02}, // jal 0x2 - } - - for _, tt := range cases { - t.Run(tt.name, func(t *testing.T) { - state := &singlethreaded.State{Cpu: mipsevm.CpuScalars{PC: tt.pc, NextPC: tt.nextPC}, Memory: memory.NewMemory()} - state.Memory.SetMemory(tt.pc, tt.insn) - curStep := state.Step - - us := singlethreaded.NewInstrumentedState(state, nil, os.Stdout, os.Stderr, nil) - stepWitness, err := us.Step(true) - require.NoError(t, err) - - evm := testutil.NewMIPSEVM(contracts, addrs) - evm.SetTracer(tracer) - testutil.LogStepFailureAtCleanup(t, evm) - - evmPost := evm.Step(t, stepWitness, curStep, singlethreaded.GetStateHashFn()) - goPost, _ := us.GetState().EncodeWitness() - require.Equal(t, hexutil.Bytes(goPost).String(), hexutil.Bytes(evmPost).String(), - "mipsevm produced different state than EVM") - }) - } -} - -func TestEVM_MMap(t *testing.T) { - contracts, addrs := testContractsSetup(t) - var tracer *tracing.Hooks - - cases := []struct { - name string - heap uint32 - address uint32 - size uint32 - shouldFail bool - expectedHeap uint32 - }{ - {name: "Increment heap by max value", heap: program.HEAP_START, address: 0, size: ^uint32(0), shouldFail: true}, - {name: "Increment heap to 0", heap: program.HEAP_START, address: 0, size: ^uint32(0) - program.HEAP_START + 1, shouldFail: true}, - {name: "Increment heap to previous page", heap: program.HEAP_START, address: 0, size: ^uint32(0) - program.HEAP_START - memory.PageSize + 1, shouldFail: true}, - {name: "Increment max page size", heap: program.HEAP_START, address: 0, size: ^uint32(0) & ^uint32(memory.PageAddrMask), shouldFail: true}, - {name: "Increment max page size from 0", heap: 0, address: 0, size: ^uint32(0) & ^uint32(memory.PageAddrMask), shouldFail: true}, - {name: "Increment heap at limit", heap: program.HEAP_END, address: 0, size: 1, shouldFail: true}, - {name: "Increment heap to limit", heap: program.HEAP_END - memory.PageSize, address: 0, size: 1, shouldFail: false, expectedHeap: program.HEAP_END}, - {name: "Increment heap within limit", heap: program.HEAP_END - 2*memory.PageSize, address: 0, size: 1, shouldFail: false, expectedHeap: program.HEAP_END - memory.PageSize}, - {name: "Request specific address", heap: program.HEAP_START, address: 0x50_00_00_00, size: 0, shouldFail: false, expectedHeap: program.HEAP_START}, - } - - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - state := singlethreaded.CreateEmptyState() - state.Heap = c.heap - state.Memory.SetMemory(state.GetPC(), syscallInsn) - state.Registers = testutil.RandomRegisters(77) - state.Registers[2] = exec.SysMmap - state.Registers[4] = c.address - state.Registers[5] = c.size - step := state.Step - - expectedRegisters := state.Registers - expectedHeap := state.Heap - expectedMemoryRoot := state.Memory.MerkleRoot() - if c.shouldFail { - expectedRegisters[2] = exec.SysErrorSignal - expectedRegisters[7] = exec.MipsEINVAL - } else { - expectedHeap = c.expectedHeap - if c.address == 0 { - expectedRegisters[2] = state.Heap - expectedRegisters[7] = 0 - } else { - expectedRegisters[2] = c.address - expectedRegisters[7] = 0 - } - } - - us := singlethreaded.NewInstrumentedState(state, nil, os.Stdout, os.Stderr, nil) - stepWitness, err := us.Step(true) - require.NoError(t, err) - - // Check expectations - require.Equal(t, step+1, state.Step) - require.Equal(t, expectedHeap, state.Heap) - require.Equal(t, expectedRegisters, state.Registers) - require.Equal(t, expectedMemoryRoot, state.Memory.MerkleRoot()) - require.Equal(t, common.Hash{}, state.PreimageKey) - require.Equal(t, uint32(0), state.PreimageOffset) - require.Equal(t, uint32(4), state.Cpu.PC) - require.Equal(t, uint32(8), state.Cpu.NextPC) - require.Equal(t, uint32(0), state.Cpu.HI) - require.Equal(t, uint32(0), state.Cpu.LO) - require.Equal(t, false, state.Exited) - require.Equal(t, uint8(0), state.ExitCode) - require.Equal(t, hexutil.Bytes(nil), state.LastHint) - - evm := testutil.NewMIPSEVM(contracts, addrs) - evm.SetTracer(tracer) - testutil.LogStepFailureAtCleanup(t, evm) - - evmPost := evm.Step(t, stepWitness, step, singlethreaded.GetStateHashFn()) - goPost, _ := us.GetState().EncodeWitness() - require.Equal(t, hexutil.Bytes(goPost).String(), hexutil.Bytes(evmPost).String(), - "mipsevm produced different state than EVM") - }) - } -} - -func TestEVMSysWriteHint(t *testing.T) { - contracts, addrs := testContractsSetup(t) - var tracer *tracing.Hooks - - cases := []struct { - name string - memOffset int // Where the hint data is stored in memory - hintData []byte // Hint data stored in memory at memOffset - bytesToWrite int // How many bytes of hintData to write - lastHint []byte // The buffer that stores lastHint in the state - expectedHints [][]byte // The hints we expect to be processed - }{ - { - name: "write 1 full hint at beginning of page", - memOffset: 4096, - hintData: []byte{ - 0, 0, 0, 6, // Length prefix - 0xAA, 0xAA, 0xAA, 0xAA, 0xBB, 0xBB, // Hint data - }, - bytesToWrite: 10, - lastHint: nil, - expectedHints: [][]byte{ - {0xAA, 0xAA, 0xAA, 0xAA, 0xBB, 0xBB}, - }, - }, - { - name: "write 1 full hint across page boundary", - memOffset: 4092, - hintData: []byte{ - 0, 0, 0, 8, // Length prefix - 0xAA, 0xAA, 0xAA, 0xAA, 0xBB, 0xBB, 0xBB, 0xBB, // Hint data - }, - bytesToWrite: 12, - lastHint: nil, - expectedHints: [][]byte{ - {0xAA, 0xAA, 0xAA, 0xAA, 0xBB, 0xBB, 0xBB, 0xBB}, - }, - }, - { - name: "write 2 full hints", - memOffset: 5012, - hintData: []byte{ - 0, 0, 0, 6, // Length prefix - 0xAA, 0xAA, 0xAA, 0xAA, 0xBB, 0xBB, // Hint data - 0, 0, 0, 8, // Length prefix - 0xAA, 0xAA, 0xAA, 0xAA, 0xBB, 0xBB, 0xBB, 0xBB, // Hint data - }, - bytesToWrite: 22, - lastHint: nil, - expectedHints: [][]byte{ - {0xAA, 0xAA, 0xAA, 0xAA, 0xBB, 0xBB}, - {0xAA, 0xAA, 0xAA, 0xAA, 0xBB, 0xBB, 0xBB, 0xBB}, - }, - }, - { - name: "write a single partial hint", - memOffset: 4092, - hintData: []byte{ - 0, 0, 0, 6, // Length prefix - 0xAA, 0xAA, 0xAA, 0xAA, 0xBB, 0xBB, // Hint data - }, - bytesToWrite: 8, - lastHint: nil, - expectedHints: nil, - }, - { - name: "write 1 full, 1 partial hint", - memOffset: 5012, - hintData: []byte{ - 0, 0, 0, 6, // Length prefix - 0xAA, 0xAA, 0xAA, 0xAA, 0xBB, 0xBB, // Hint data - 0, 0, 0, 8, // Length prefix - 0xAA, 0xAA, 0xAA, 0xAA, 0xBB, 0xBB, 0xBB, 0xBB, // Hint data - }, - bytesToWrite: 16, - lastHint: nil, - expectedHints: [][]byte{ - {0xAA, 0xAA, 0xAA, 0xAA, 0xBB, 0xBB}, - }, - }, - { - name: "write a single partial hint to large capacity lastHint buffer", - memOffset: 4092, - hintData: []byte{ - 0, 0, 0, 6, // Length prefix - 0xAA, 0xAA, 0xAA, 0xAA, 0xBB, 0xBB, // Hint data - }, - bytesToWrite: 8, - lastHint: make([]byte, 0, 4096), - expectedHints: nil, - }, - { - name: "write full hint to large capacity lastHint buffer", - memOffset: 5012, - hintData: []byte{ - 0, 0, 0, 6, // Length prefix - 0xAA, 0xAA, 0xAA, 0xAA, 0xBB, 0xBB, // Hint data - }, - bytesToWrite: 10, - lastHint: make([]byte, 0, 4096), - expectedHints: [][]byte{ - {0xAA, 0xAA, 0xAA, 0xAA, 0xBB, 0xBB}, - }, - }, - { - name: "write multiple hints to large capacity lastHint buffer", - memOffset: 4092, - hintData: []byte{ - 0, 0, 0, 8, // Length prefix - 0xAA, 0xAA, 0xAA, 0xAA, 0xBB, 0xBB, 0xCC, 0xCC, // Hint data - 0, 0, 0, 8, // Length prefix - 0xAA, 0xAA, 0xAA, 0xAA, 0xBB, 0xBB, 0xBB, 0xBB, // Hint data - }, - bytesToWrite: 24, - lastHint: make([]byte, 0, 4096), - expectedHints: [][]byte{ - {0xAA, 0xAA, 0xAA, 0xAA, 0xBB, 0xBB, 0xCC, 0xCC}, - {0xAA, 0xAA, 0xAA, 0xAA, 0xBB, 0xBB, 0xBB, 0xBB}, - }, - }, - { - name: "write remaining hint data to non-empty lastHint buffer", - memOffset: 4092, - hintData: []byte{ - 0xAA, 0xAA, 0xAA, 0xAA, 0xBB, 0xBB, 0xCC, 0xCC, // Hint data - }, - bytesToWrite: 8, - lastHint: []byte{0, 0, 0, 8}, - expectedHints: [][]byte{ - {0xAA, 0xAA, 0xAA, 0xAA, 0xBB, 0xBB, 0xCC, 0xCC}, - }, - }, - { - name: "write partial hint data to non-empty lastHint buffer", - memOffset: 4092, - hintData: []byte{ - 0xAA, 0xAA, 0xAA, 0xAA, 0xBB, 0xBB, 0xCC, 0xCC, // Hint data - }, - bytesToWrite: 4, - lastHint: []byte{0, 0, 0, 8}, - expectedHints: nil, - }, - } - - const ( - insn = uint32(0x00_00_00_0C) // syscall instruction - ) - - for _, tt := range cases { - t.Run(tt.name, func(t *testing.T) { - oracle := hintTrackingOracle{} - state := &singlethreaded.State{Cpu: mipsevm.CpuScalars{PC: 0, NextPC: 4}, Memory: memory.NewMemory()} - - state.LastHint = tt.lastHint - state.Registers[2] = exec.SysWrite - state.Registers[4] = exec.FdHintWrite - state.Registers[5] = uint32(tt.memOffset) - state.Registers[6] = uint32(tt.bytesToWrite) - - err := state.Memory.SetMemoryRange(uint32(tt.memOffset), bytes.NewReader(tt.hintData)) - require.NoError(t, err) - state.Memory.SetMemory(0, insn) - curStep := state.Step - - us := singlethreaded.NewInstrumentedState(state, &oracle, os.Stdout, os.Stderr, nil) - stepWitness, err := us.Step(true) - require.NoError(t, err) - require.Equal(t, tt.expectedHints, oracle.hints) - - evm := testutil.NewMIPSEVM(contracts, addrs) - evm.SetTracer(tracer) - testutil.LogStepFailureAtCleanup(t, evm) - - evmPost := evm.Step(t, stepWitness, curStep, singlethreaded.GetStateHashFn()) - goPost, _ := us.GetState().EncodeWitness() - require.Equal(t, hexutil.Bytes(goPost).String(), hexutil.Bytes(evmPost).String(), - "mipsevm produced different state than EVM") - }) - } -} - -func TestEVMFault(t *testing.T) { - contracts, addrs := testContractsSetup(t) - var tracer *tracing.Hooks // no-tracer by default, but see test_util.MarkdownTracer - sender := common.Address{0x13, 0x37} - - env, evmState := testutil.NewEVMEnv(contracts, addrs) - env.Config.Tracer = tracer - - cases := []struct { - name string - nextPC uint32 - insn uint32 - }{ - {"illegal instruction", 0, 0xFF_FF_FF_FF}, - {"branch in delay-slot", 8, 0x11_02_00_03}, - {"jump in delay-slot", 8, 0x0c_00_00_0c}, - } - - for _, tt := range cases { - t.Run(tt.name, func(t *testing.T) { - state := &singlethreaded.State{Cpu: mipsevm.CpuScalars{PC: 0, NextPC: tt.nextPC}, Memory: memory.NewMemory()} - initialState := &singlethreaded.State{Cpu: mipsevm.CpuScalars{PC: 0, NextPC: tt.nextPC}, Memory: state.Memory} - state.Memory.SetMemory(0, tt.insn) - - // set the return address ($ra) to jump into when test completes - state.Registers[31] = testutil.EndAddr - - us := singlethreaded.NewInstrumentedState(state, nil, os.Stdout, os.Stderr, nil) - require.Panics(t, func() { _, _ = us.Step(true) }) - - insnProof := initialState.Memory.MerkleProof(0) - encodedWitness, _ := initialState.EncodeWitness() - stepWitness := &mipsevm.StepWitness{ - State: encodedWitness, - ProofData: insnProof[:], - } - input := testutil.EncodeStepInput(t, stepWitness, mipsevm.LocalContext{}, contracts.MIPS) - startingGas := uint64(30_000_000) - - _, _, err := env.Call(vm.AccountRef(sender), addrs.MIPS, input, startingGas, common.U2560) - require.EqualValues(t, err, vm.ErrExecutionReverted) - logs := evmState.Logs() - require.Equal(t, 0, len(logs)) - }) - } -} - -func TestHelloEVM(t *testing.T) { - contracts, addrs := testContractsSetup(t) - var tracer *tracing.Hooks // no-tracer by default, but see test_util.MarkdownTracer - evm := testutil.NewMIPSEVM(contracts, addrs) - evm.SetTracer(tracer) - testutil.LogStepFailureAtCleanup(t, evm) - - state := testutil.LoadELFProgram(t, "../../testdata/example/bin/hello.elf", singlethreaded.CreateInitialState, true) - var stdOutBuf, stdErrBuf bytes.Buffer - goState := singlethreaded.NewInstrumentedState(state, nil, io.MultiWriter(&stdOutBuf, os.Stdout), io.MultiWriter(&stdErrBuf, os.Stderr), nil) - - start := time.Now() - for i := 0; i < 400_000; i++ { - curStep := goState.GetState().GetStep() - if goState.GetState().GetExited() { - break - } - insn := state.Memory.GetMemory(state.Cpu.PC) - if i%1000 == 0 { // avoid spamming test logs, we are executing many steps - t.Logf("step: %4d pc: 0x%08x insn: 0x%08x", state.Step, state.Cpu.PC, insn) - } - - stepWitness, err := goState.Step(true) - require.NoError(t, err) - evmPost := evm.Step(t, stepWitness, curStep, singlethreaded.GetStateHashFn()) - // verify the post-state matches. - // TODO: maybe more readable to decode the evmPost state, and do attribute-wise comparison. - goPost, _ := goState.GetState().EncodeWitness() - require.Equal(t, hexutil.Bytes(goPost).String(), hexutil.Bytes(evmPost).String(), - "mipsevm produced different state than EVM") - } - end := time.Now() - delta := end.Sub(start) - t.Logf("test took %s, %d instructions, %s per instruction", delta, state.Step, delta/time.Duration(state.Step)) - - require.True(t, state.Exited, "must complete program") - require.Equal(t, uint8(0), state.ExitCode, "exit with 0") - - require.Equal(t, "hello world!\n", stdOutBuf.String(), "stdout says hello") - require.Equal(t, "", stdErrBuf.String(), "stderr silent") -} - -func TestClaimEVM(t *testing.T) { - contracts, addrs := testContractsSetup(t) - var tracer *tracing.Hooks // no-tracer by default, but see test_util.MarkdownTracer - evm := testutil.NewMIPSEVM(contracts, addrs) - evm.SetTracer(tracer) - testutil.LogStepFailureAtCleanup(t, evm) - - state := testutil.LoadELFProgram(t, "../../testdata/example/bin/claim.elf", singlethreaded.CreateInitialState, true) - oracle, expectedStdOut, expectedStdErr := testutil.ClaimTestOracle(t) - - var stdOutBuf, stdErrBuf bytes.Buffer - goState := singlethreaded.NewInstrumentedState(state, oracle, io.MultiWriter(&stdOutBuf, os.Stdout), io.MultiWriter(&stdErrBuf, os.Stderr), nil) - - for i := 0; i < 2000_000; i++ { - curStep := goState.GetState().GetStep() - if goState.GetState().GetExited() { - break - } - - insn := state.Memory.GetMemory(state.Cpu.PC) - if i%1000 == 0 { // avoid spamming test logs, we are executing many steps - t.Logf("step: %4d pc: 0x%08x insn: 0x%08x", state.Step, state.Cpu.PC, insn) - } - - stepWitness, err := goState.Step(true) - require.NoError(t, err) - - evmPost := evm.Step(t, stepWitness, curStep, singlethreaded.GetStateHashFn()) - - goPost, _ := goState.GetState().EncodeWitness() - require.Equal(t, hexutil.Bytes(goPost).String(), hexutil.Bytes(evmPost).String(), - "mipsevm produced different state than EVM") - } - - require.True(t, state.Exited, "must complete program") - require.Equal(t, uint8(0), state.ExitCode, "exit with 0") - - require.Equal(t, expectedStdOut, stdOutBuf.String(), "stdout") - require.Equal(t, expectedStdErr, stdErrBuf.String(), "stderr") -} - -type hintTrackingOracle struct { - hints [][]byte -} - -func (t *hintTrackingOracle) Hint(v []byte) { - t.hints = append(t.hints, v) -} - -func (t *hintTrackingOracle) GetPreimage(k [32]byte) []byte { - return nil -} diff --git a/cannon/mipsevm/tests/fuzz_evm_common_test.go b/cannon/mipsevm/tests/fuzz_evm_common_test.go new file mode 100644 index 0000000000000..3a361f2d1d031 --- /dev/null +++ b/cannon/mipsevm/tests/fuzz_evm_common_test.go @@ -0,0 +1,478 @@ +package tests + +import ( + "bytes" + "math/rand" + "os" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/require" + + "github.com/ethereum-optimism/optimism/cannon/mipsevm/exec" + "github.com/ethereum-optimism/optimism/cannon/mipsevm/memory" + "github.com/ethereum-optimism/optimism/cannon/mipsevm/program" + "github.com/ethereum-optimism/optimism/cannon/mipsevm/testutil" + preimage "github.com/ethereum-optimism/optimism/op-preimage" +) + +const syscallInsn = uint32(0x00_00_00_0c) + +func FuzzStateSyscallBrk(f *testing.F) { + versions := GetMipsVersionTestCases(f) + f.Fuzz(func(t *testing.T, pc uint32, step uint64, preimageOffset uint32) { + for _, v := range versions { + t.Run(v.Name, func(t *testing.T) { + pc = pc & 0xFF_FF_FF_FC // align PC + nextPC := pc + 4 + + goVm := v.VMFactory(nil, os.Stdout, os.Stderr, testutil.CreateLogger(), + WithPC(pc), WithNextPC(nextPC), WithStep(step), WithPreimageOffset(preimageOffset)) + state := goVm.GetState() + state.GetRegistersRef()[2] = exec.SysBrk + state.GetMemory().SetMemory(pc, syscallInsn) + + preStateRoot := state.GetMemory().MerkleRoot() + expectedRegisters := testutil.CopyRegisters(state) + expectedRegisters[2] = program.PROGRAM_BREAK + + stepWitness, err := goVm.Step(true) + require.NoError(t, err) + require.False(t, stepWitness.HasPreimage()) + + require.Equal(t, pc+4, state.GetPC()) + require.Equal(t, nextPC+4, state.GetCpu().NextPC) + require.Equal(t, uint32(0), state.GetCpu().LO) + require.Equal(t, uint32(0), state.GetCpu().HI) + require.Equal(t, uint32(0), state.GetHeap()) + require.Equal(t, uint8(0), state.GetExitCode()) + require.Equal(t, false, state.GetExited()) + require.Equal(t, preStateRoot, state.GetMemory().MerkleRoot()) + require.Equal(t, expectedRegisters, state.GetRegistersRef()) + require.Equal(t, step+1, state.GetStep()) + require.Equal(t, common.Hash{}, state.GetPreimageKey()) + require.Equal(t, preimageOffset, state.GetPreimageOffset()) + + evm := testutil.NewMIPSEVM(v.Contracts) + evmPost := evm.Step(t, stepWitness, step, v.StateHashFn) + goPost, _ := goVm.GetState().EncodeWitness() + require.Equal(t, hexutil.Bytes(goPost).String(), hexutil.Bytes(evmPost).String(), + "mipsevm produced different state than EVM") + }) + } + }) +} + +func FuzzStateSyscallMmap(f *testing.F) { + // Add special cases for large memory allocation + f.Add(uint32(0), uint32(0x1000), uint32(program.HEAP_END), int64(1)) + f.Add(uint32(0), uint32(1<<31), uint32(program.HEAP_START), int64(2)) + // Check edge case - just within bounds + f.Add(uint32(0), uint32(0x1000), uint32(program.HEAP_END-4096), int64(3)) + + versions := GetMipsVersionTestCases(f) + f.Fuzz(func(t *testing.T, addr uint32, siz uint32, heap uint32, seed int64) { + for _, v := range versions { + t.Run(v.Name, func(t *testing.T) { + step := uint64(0) + goVm := v.VMFactory(nil, os.Stdout, os.Stderr, testutil.CreateLogger(), + WithStep(step), WithHeap(heap)) + state := goVm.GetState() + *state.GetRegistersRef() = testutil.RandomRegisters(seed) + state.GetRegistersRef()[2] = exec.SysMmap + state.GetRegistersRef()[4] = addr + state.GetRegistersRef()[5] = siz + state.GetMemory().SetMemory(0, syscallInsn) + + preStateRoot := state.GetMemory().MerkleRoot() + preStateRegisters := testutil.CopyRegisters(state) + + stepWitness, err := goVm.Step(true) + require.NoError(t, err) + require.False(t, stepWitness.HasPreimage()) + + var expectedHeap uint32 + expectedRegisters := preStateRegisters + if addr == 0 { + sizAlign := siz + if sizAlign&memory.PageAddrMask != 0 { // adjust size to align with page size + sizAlign = siz + memory.PageSize - (siz & memory.PageAddrMask) + } + newHeap := heap + sizAlign + if newHeap > program.HEAP_END || newHeap < heap || sizAlign < siz { + expectedHeap = heap + expectedRegisters[2] = exec.SysErrorSignal + expectedRegisters[7] = exec.MipsEINVAL + } else { + expectedRegisters[2] = heap + expectedRegisters[7] = 0 // no error + expectedHeap = heap + sizAlign + } + } else { + expectedRegisters[2] = addr + expectedRegisters[7] = 0 // no error + expectedHeap = heap + } + + require.Equal(t, uint32(4), state.GetCpu().PC) + require.Equal(t, uint32(8), state.GetCpu().NextPC) + require.Equal(t, uint32(0), state.GetCpu().LO) + require.Equal(t, uint32(0), state.GetCpu().HI) + require.Equal(t, preStateRoot, state.GetMemory().MerkleRoot()) + require.Equal(t, uint64(1), state.GetStep()) + require.Equal(t, common.Hash{}, state.GetPreimageKey()) + require.Equal(t, uint32(0), state.GetPreimageOffset()) + require.Equal(t, expectedHeap, state.GetHeap()) + require.Equal(t, uint8(0), state.GetExitCode()) + require.Equal(t, false, state.GetExited()) + require.Equal(t, expectedRegisters, state.GetRegistersRef()) + + evm := testutil.NewMIPSEVM(v.Contracts) + evmPost := evm.Step(t, stepWitness, step, v.StateHashFn) + goPost, _ := goVm.GetState().EncodeWitness() + require.Equal(t, hexutil.Bytes(goPost).String(), hexutil.Bytes(evmPost).String(), + "mipsevm produced different state than EVM") + }) + } + }) +} + +func FuzzStateSyscallExitGroup(f *testing.F) { + versions := GetMipsVersionTestCases(f) + f.Fuzz(func(t *testing.T, exitCode uint8, pc uint32, step uint64) { + for _, v := range versions { + t.Run(v.Name, func(t *testing.T) { + pc = pc & 0xFF_FF_FF_FC // align PC + nextPC := pc + 4 + goVm := v.VMFactory(nil, os.Stdout, os.Stderr, testutil.CreateLogger(), + WithPC(pc), WithNextPC(nextPC), WithStep(step)) + state := goVm.GetState() + state.GetRegistersRef()[2] = exec.SysExitGroup + state.GetRegistersRef()[4] = uint32(exitCode) + state.GetMemory().SetMemory(pc, syscallInsn) + + preStateRoot := state.GetMemory().MerkleRoot() + preStateRegisters := testutil.CopyRegisters(state) + + stepWitness, err := goVm.Step(true) + require.NoError(t, err) + require.False(t, stepWitness.HasPreimage()) + + require.Equal(t, pc, state.GetCpu().PC) + require.Equal(t, nextPC, state.GetCpu().NextPC) + require.Equal(t, uint32(0), state.GetCpu().LO) + require.Equal(t, uint32(0), state.GetCpu().HI) + require.Equal(t, uint32(0), state.GetHeap()) + require.Equal(t, uint8(exitCode), state.GetExitCode()) + require.Equal(t, true, state.GetExited()) + require.Equal(t, preStateRoot, state.GetMemory().MerkleRoot()) + require.Equal(t, preStateRegisters, state.GetRegistersRef()) + require.Equal(t, step+1, state.GetStep()) + require.Equal(t, common.Hash{}, state.GetPreimageKey()) + require.Equal(t, uint32(0), state.GetPreimageOffset()) + + evm := testutil.NewMIPSEVM(v.Contracts) + evmPost := evm.Step(t, stepWitness, step, v.StateHashFn) + goPost, _ := goVm.GetState().EncodeWitness() + require.Equal(t, hexutil.Bytes(goPost).String(), hexutil.Bytes(evmPost).String(), + "mipsevm produced different state than EVM") + }) + } + }) +} + +func FuzzStateSyscallFcntl(f *testing.F) { + versions := GetMipsVersionTestCases(f) + f.Fuzz(func(t *testing.T, fd uint32, cmd uint32) { + for _, v := range versions { + t.Run(v.Name, func(t *testing.T) { + step := uint64(0) + goVm := v.VMFactory(nil, os.Stdout, os.Stderr, testutil.CreateLogger(), + WithStep(step)) + state := goVm.GetState() + state.GetRegistersRef()[2] = exec.SysFcntl + state.GetRegistersRef()[4] = fd + state.GetRegistersRef()[5] = cmd + state.GetMemory().SetMemory(0, syscallInsn) + + preStateRoot := state.GetMemory().MerkleRoot() + preStateRegisters := testutil.CopyRegisters(state) + + stepWitness, err := goVm.Step(true) + require.NoError(t, err) + require.False(t, stepWitness.HasPreimage()) + + require.Equal(t, uint32(4), state.GetCpu().PC) + require.Equal(t, uint32(8), state.GetCpu().NextPC) + require.Equal(t, uint32(0), state.GetCpu().LO) + require.Equal(t, uint32(0), state.GetCpu().HI) + require.Equal(t, uint32(0), state.GetHeap()) + require.Equal(t, uint8(0), state.GetExitCode()) + require.Equal(t, false, state.GetExited()) + require.Equal(t, preStateRoot, state.GetMemory().MerkleRoot()) + require.Equal(t, uint64(1), state.GetStep()) + require.Equal(t, common.Hash{}, state.GetPreimageKey()) + require.Equal(t, uint32(0), state.GetPreimageOffset()) + if cmd == 3 { + expectedRegisters := preStateRegisters + switch fd { + case exec.FdStdin, exec.FdPreimageRead, exec.FdHintRead: + expectedRegisters[2] = 0 + case exec.FdStdout, exec.FdStderr, exec.FdPreimageWrite, exec.FdHintWrite: + expectedRegisters[2] = 1 + default: + expectedRegisters[2] = 0xFF_FF_FF_FF + expectedRegisters[7] = exec.MipsEBADF + } + require.Equal(t, expectedRegisters, state.GetRegistersRef()) + } else { + expectedRegisters := preStateRegisters + expectedRegisters[2] = 0xFF_FF_FF_FF + expectedRegisters[7] = exec.MipsEINVAL + require.Equal(t, expectedRegisters, state.GetRegistersRef()) + } + + evm := testutil.NewMIPSEVM(v.Contracts) + evmPost := evm.Step(t, stepWitness, step, v.StateHashFn) + goPost, _ := goVm.GetState().EncodeWitness() + require.Equal(t, hexutil.Bytes(goPost).String(), hexutil.Bytes(evmPost).String(), + "mipsevm produced different state than EVM") + }) + } + }) +} + +func FuzzStateHintRead(f *testing.F) { + versions := GetMipsVersionTestCases(f) + f.Fuzz(func(t *testing.T, addr uint32, count uint32) { + for _, v := range versions { + t.Run(v.Name, func(t *testing.T) { + step := uint64(0) + preimageData := []byte("hello world") + preimageKey := preimage.Keccak256Key(crypto.Keccak256Hash(preimageData)).PreimageKey() + oracle := testutil.StaticOracle(t, preimageData) // only used for hinting + + goVm := v.VMFactory(oracle, os.Stdout, os.Stderr, testutil.CreateLogger(), + WithStep(step), WithPreimageKey(preimageKey)) + state := goVm.GetState() + state.GetRegistersRef()[2] = exec.SysRead + state.GetRegistersRef()[4] = exec.FdHintRead + state.GetRegistersRef()[5] = addr + state.GetRegistersRef()[6] = count + state.GetMemory().SetMemory(0, syscallInsn) + + preStatePreimageKey := state.GetPreimageKey() + preStateRoot := state.GetMemory().MerkleRoot() + expectedRegisters := testutil.CopyRegisters(state) + expectedRegisters[2] = count + + stepWitness, err := goVm.Step(true) + require.NoError(t, err) + require.False(t, stepWitness.HasPreimage()) + + require.Equal(t, uint32(4), state.GetCpu().PC) + require.Equal(t, uint32(8), state.GetCpu().NextPC) + require.Equal(t, uint32(0), state.GetCpu().LO) + require.Equal(t, uint32(0), state.GetCpu().HI) + require.Equal(t, uint32(0), state.GetHeap()) + require.Equal(t, uint8(0), state.GetExitCode()) + require.Equal(t, false, state.GetExited()) + require.Equal(t, preStateRoot, state.GetMemory().MerkleRoot()) + require.Equal(t, uint64(1), state.GetStep()) + require.Equal(t, preStatePreimageKey, state.GetPreimageKey()) + require.Equal(t, expectedRegisters, state.GetRegistersRef()) + + evm := testutil.NewMIPSEVM(v.Contracts) + evmPost := evm.Step(t, stepWitness, step, v.StateHashFn) + goPost, _ := goVm.GetState().EncodeWitness() + require.Equal(t, hexutil.Bytes(goPost).String(), hexutil.Bytes(evmPost).String(), + "mipsevm produced different state than EVM") + }) + } + }) +} + +func FuzzStatePreimageRead(f *testing.F) { + versions := GetMipsVersionTestCases(f) + f.Fuzz(func(t *testing.T, addr uint32, count uint32, preimageOffset uint32) { + for _, v := range versions { + t.Run(v.Name, func(t *testing.T) { + step := uint64(0) + preimageData := []byte("hello world") + if preimageOffset >= uint32(len(preimageData)) { + t.SkipNow() + } + preimageKey := preimage.Keccak256Key(crypto.Keccak256Hash(preimageData)).PreimageKey() + oracle := testutil.StaticOracle(t, preimageData) + + goVm := v.VMFactory(oracle, os.Stdout, os.Stderr, testutil.CreateLogger(), + WithStep(step), WithPreimageKey(preimageKey), WithPreimageOffset(preimageOffset)) + state := goVm.GetState() + state.GetRegistersRef()[2] = exec.SysRead + state.GetRegistersRef()[4] = exec.FdPreimageRead + state.GetRegistersRef()[5] = addr + state.GetRegistersRef()[6] = count + state.GetMemory().SetMemory(0, syscallInsn) + + preStatePreimageKey := state.GetPreimageKey() + preStateRoot := state.GetMemory().MerkleRoot() + writeLen := count + if writeLen > 4 { + writeLen = 4 + } + if preimageOffset+writeLen > uint32(8+len(preimageData)) { + writeLen = uint32(8+len(preimageData)) - preimageOffset + } + + stepWitness, err := goVm.Step(true) + require.NoError(t, err) + require.True(t, stepWitness.HasPreimage()) + + require.Equal(t, uint32(4), state.GetCpu().PC) + require.Equal(t, uint32(8), state.GetCpu().NextPC) + require.Equal(t, uint32(0), state.GetCpu().LO) + require.Equal(t, uint32(0), state.GetCpu().HI) + require.Equal(t, uint32(0), state.GetHeap()) + require.Equal(t, uint8(0), state.GetExitCode()) + require.Equal(t, false, state.GetExited()) + if writeLen > 0 { + // Memory may be unchanged if we're writing the first zero-valued 7 bytes of the pre-image. + //require.NotEqual(t, preStateRoot, state.GetMemory().MerkleRoot()) + require.Greater(t, state.GetPreimageOffset(), preimageOffset) + } else { + require.Equal(t, preStateRoot, state.GetMemory().MerkleRoot()) + require.Equal(t, state.GetPreimageOffset(), preimageOffset) + } + require.Equal(t, uint64(1), state.GetStep()) + require.Equal(t, preStatePreimageKey, state.GetPreimageKey()) + + evm := testutil.NewMIPSEVM(v.Contracts) + evmPost := evm.Step(t, stepWitness, step, v.StateHashFn) + goPost, _ := goVm.GetState().EncodeWitness() + require.Equal(t, hexutil.Bytes(goPost).String(), hexutil.Bytes(evmPost).String(), + "mipsevm produced different state than EVM") + }) + } + }) +} + +func FuzzStateHintWrite(f *testing.F) { + versions := GetMipsVersionTestCases(f) + f.Fuzz(func(t *testing.T, addr uint32, count uint32, randSeed int64) { + for _, v := range versions { + t.Run(v.Name, func(t *testing.T) { + step := uint64(0) + preimageData := []byte("hello world") + preimageKey := preimage.Keccak256Key(crypto.Keccak256Hash(preimageData)).PreimageKey() + oracle := testutil.StaticOracle(t, preimageData) // only used for hinting + + goVm := v.VMFactory(oracle, os.Stdout, os.Stderr, testutil.CreateLogger(), + WithStep(step), WithPreimageKey(preimageKey)) + state := goVm.GetState() + state.GetRegistersRef()[2] = exec.SysWrite + state.GetRegistersRef()[4] = exec.FdHintWrite + state.GetRegistersRef()[5] = addr + state.GetRegistersRef()[6] = count + + // Set random data at the target memory range + randBytes, err := randomBytes(randSeed, count) + require.NoError(t, err) + err = state.GetMemory().SetMemoryRange(addr, bytes.NewReader(randBytes)) + require.NoError(t, err) + // Set syscall instruction + state.GetMemory().SetMemory(0, syscallInsn) + + preStatePreimageKey := state.GetPreimageKey() + preStateRoot := state.GetMemory().MerkleRoot() + expectedRegisters := testutil.CopyRegisters(state) + expectedRegisters[2] = count + + stepWitness, err := goVm.Step(true) + require.NoError(t, err) + require.False(t, stepWitness.HasPreimage()) + + require.Equal(t, uint32(4), state.GetCpu().PC) + require.Equal(t, uint32(8), state.GetCpu().NextPC) + require.Equal(t, uint32(0), state.GetCpu().LO) + require.Equal(t, uint32(0), state.GetCpu().HI) + require.Equal(t, uint32(0), state.GetHeap()) + require.Equal(t, uint8(0), state.GetExitCode()) + require.Equal(t, false, state.GetExited()) + require.Equal(t, preStateRoot, state.GetMemory().MerkleRoot()) + require.Equal(t, uint64(1), state.GetStep()) + require.Equal(t, preStatePreimageKey, state.GetPreimageKey()) + require.Equal(t, expectedRegisters, state.GetRegistersRef()) + + evm := testutil.NewMIPSEVM(v.Contracts) + evmPost := evm.Step(t, stepWitness, step, v.StateHashFn) + goPost, _ := goVm.GetState().EncodeWitness() + require.Equal(t, hexutil.Bytes(goPost).String(), hexutil.Bytes(evmPost).String(), + "mipsevm produced different state than EVM") + }) + } + }) +} + +func FuzzStatePreimageWrite(f *testing.F) { + versions := GetMipsVersionTestCases(f) + f.Fuzz(func(t *testing.T, addr uint32, count uint32) { + for _, v := range versions { + t.Run(v.Name, func(t *testing.T) { + step := uint64(0) + preimageData := []byte("hello world") + preimageKey := preimage.Keccak256Key(crypto.Keccak256Hash(preimageData)).PreimageKey() + oracle := testutil.StaticOracle(t, preimageData) + + goVm := v.VMFactory(oracle, os.Stdout, os.Stderr, testutil.CreateLogger(), + WithStep(step), WithPreimageKey(preimageKey), WithPreimageOffset(128)) + state := goVm.GetState() + state.GetRegistersRef()[2] = exec.SysWrite + state.GetRegistersRef()[4] = exec.FdPreimageWrite + state.GetRegistersRef()[5] = addr + state.GetRegistersRef()[6] = count + state.GetMemory().SetMemory(0, syscallInsn) + + preStateRoot := state.GetMemory().MerkleRoot() + expectedRegisters := testutil.CopyRegisters(state) + sz := 4 - (addr & 0x3) + if sz < count { + count = sz + } + expectedRegisters[2] = count + + stepWitness, err := goVm.Step(true) + require.NoError(t, err) + require.False(t, stepWitness.HasPreimage()) + + require.Equal(t, uint32(4), state.GetCpu().PC) + require.Equal(t, uint32(8), state.GetCpu().NextPC) + require.Equal(t, uint32(0), state.GetCpu().LO) + require.Equal(t, uint32(0), state.GetCpu().HI) + require.Equal(t, uint32(0), state.GetHeap()) + require.Equal(t, uint8(0), state.GetExitCode()) + require.Equal(t, false, state.GetExited()) + require.Equal(t, preStateRoot, state.GetMemory().MerkleRoot()) + require.Equal(t, uint64(1), state.GetStep()) + require.Equal(t, uint32(0), state.GetPreimageOffset()) + require.Equal(t, expectedRegisters, state.GetRegistersRef()) + + evm := testutil.NewMIPSEVM(v.Contracts) + evmPost := evm.Step(t, stepWitness, step, v.StateHashFn) + goPost, _ := goVm.GetState().EncodeWitness() + require.Equal(t, hexutil.Bytes(goPost).String(), hexutil.Bytes(evmPost).String(), + "mipsevm produced different state than EVM") + }) + } + }) +} + +func randomBytes(seed int64, length uint32) ([]byte, error) { + r := rand.New(rand.NewSource(seed)) + randBytes := make([]byte, length) + if _, err := r.Read(randBytes); err != nil { + return nil, err + } + return randBytes, nil +} diff --git a/cannon/mipsevm/tests/fuzz_evm_multithreaded_test.go b/cannon/mipsevm/tests/fuzz_evm_multithreaded_test.go new file mode 100644 index 0000000000000..c934b16145ae3 --- /dev/null +++ b/cannon/mipsevm/tests/fuzz_evm_multithreaded_test.go @@ -0,0 +1,58 @@ +package tests + +import ( + "os" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/stretchr/testify/require" + + "github.com/ethereum-optimism/optimism/cannon/mipsevm/exec" + "github.com/ethereum-optimism/optimism/cannon/mipsevm/testutil" +) + +// TODO +func FuzzStateSyscallCloneMT(f *testing.F) { + v := GetMultiThreadedTestCase(f) + // t.Skip is causing linting check to fail, disable for now + //nolint:staticcheck + f.Fuzz(func(t *testing.T, pc uint32, step uint64, preimageOffset uint32) { + // TODO(cp-903) Customize test for multi-threaded vm + t.Skip("TODO - customize this test for MTCannon") + pc = pc & 0xFF_FF_FF_FC // align PC + nextPC := pc + 4 + goVm := v.VMFactory(nil, os.Stdout, os.Stderr, testutil.CreateLogger(), + WithPC(pc), WithNextPC(nextPC), WithStep(step), WithPreimageOffset(preimageOffset)) + state := goVm.GetState() + state.GetRegistersRef()[2] = exec.SysClone + + state.GetMemory().SetMemory(pc, syscallInsn) + preStateRoot := state.GetMemory().MerkleRoot() + expectedRegisters := testutil.CopyRegisters(state) + expectedRegisters[2] = 0x1 + + stepWitness, err := goVm.Step(true) + require.NoError(t, err) + require.False(t, stepWitness.HasPreimage()) + + require.Equal(t, pc+4, state.GetCpu().PC) + require.Equal(t, nextPC+4, state.GetCpu().NextPC) + require.Equal(t, uint32(0), state.GetCpu().LO) + require.Equal(t, uint32(0), state.GetCpu().HI) + require.Equal(t, uint32(0), state.GetHeap()) + require.Equal(t, uint8(0), state.GetExitCode()) + require.Equal(t, false, state.GetExited()) + require.Equal(t, preStateRoot, state.GetMemory().MerkleRoot()) + require.Equal(t, expectedRegisters, state.GetRegistersRef()) + require.Equal(t, step+1, state.GetStep()) + require.Equal(t, common.Hash{}, state.GetPreimageKey()) + require.Equal(t, preimageOffset, state.GetPreimageOffset()) + + evm := testutil.NewMIPSEVM(v.Contracts) + evmPost := evm.Step(t, stepWitness, step, v.StateHashFn) + goPost, _ := goVm.GetState().EncodeWitness() + require.Equal(t, hexutil.Bytes(goPost).String(), hexutil.Bytes(evmPost).String(), + "mipsevm produced different state than EVM") + }) +} diff --git a/cannon/mipsevm/tests/fuzz_evm_singlethreaded_test.go b/cannon/mipsevm/tests/fuzz_evm_singlethreaded_test.go new file mode 100644 index 0000000000000..d3cc537d7894b --- /dev/null +++ b/cannon/mipsevm/tests/fuzz_evm_singlethreaded_test.go @@ -0,0 +1,53 @@ +package tests + +import ( + "os" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/stretchr/testify/require" + + "github.com/ethereum-optimism/optimism/cannon/mipsevm/exec" + "github.com/ethereum-optimism/optimism/cannon/mipsevm/testutil" +) + +func FuzzStateSyscallCloneST(f *testing.F) { + v := GetSingleThreadedTestCase(f) + f.Fuzz(func(t *testing.T, pc uint32, step uint64, preimageOffset uint32) { + pc = pc & 0xFF_FF_FF_FC // align PC + nextPC := pc + 4 + goVm := v.VMFactory(nil, os.Stdout, os.Stderr, testutil.CreateLogger(), + WithPC(pc), WithNextPC(nextPC), WithStep(step), WithPreimageOffset(preimageOffset)) + state := goVm.GetState() + state.GetRegistersRef()[2] = exec.SysClone + + state.GetMemory().SetMemory(pc, syscallInsn) + preStateRoot := state.GetMemory().MerkleRoot() + expectedRegisters := testutil.CopyRegisters(state) + expectedRegisters[2] = 0x1 + + stepWitness, err := goVm.Step(true) + require.NoError(t, err) + require.False(t, stepWitness.HasPreimage()) + + require.Equal(t, pc+4, state.GetCpu().PC) + require.Equal(t, nextPC+4, state.GetCpu().NextPC) + require.Equal(t, uint32(0), state.GetCpu().LO) + require.Equal(t, uint32(0), state.GetCpu().HI) + require.Equal(t, uint32(0), state.GetHeap()) + require.Equal(t, uint8(0), state.GetExitCode()) + require.Equal(t, false, state.GetExited()) + require.Equal(t, preStateRoot, state.GetMemory().MerkleRoot()) + require.Equal(t, expectedRegisters, state.GetRegistersRef()) + require.Equal(t, step+1, state.GetStep()) + require.Equal(t, common.Hash{}, state.GetPreimageKey()) + require.Equal(t, preimageOffset, state.GetPreimageOffset()) + + evm := testutil.NewMIPSEVM(v.Contracts) + evmPost := evm.Step(t, stepWitness, step, v.StateHashFn) + goPost, _ := goVm.GetState().EncodeWitness() + require.Equal(t, hexutil.Bytes(goPost).String(), hexutil.Bytes(evmPost).String(), + "mipsevm produced different state than EVM") + }) +} diff --git a/cannon/mipsevm/tests/fuzz_evm_test.go b/cannon/mipsevm/tests/fuzz_evm_test.go deleted file mode 100644 index 84f38851b2ad6..0000000000000 --- a/cannon/mipsevm/tests/fuzz_evm_test.go +++ /dev/null @@ -1,570 +0,0 @@ -package tests - -import ( - "bytes" - "math/rand" - "os" - "testing" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/crypto" - "github.com/stretchr/testify/require" - - "github.com/ethereum-optimism/optimism/cannon/mipsevm" - "github.com/ethereum-optimism/optimism/cannon/mipsevm/exec" - "github.com/ethereum-optimism/optimism/cannon/mipsevm/memory" - "github.com/ethereum-optimism/optimism/cannon/mipsevm/program" - "github.com/ethereum-optimism/optimism/cannon/mipsevm/singlethreaded" - "github.com/ethereum-optimism/optimism/cannon/mipsevm/testutil" - preimage "github.com/ethereum-optimism/optimism/op-preimage" -) - -const syscallInsn = uint32(0x00_00_00_0c) - -func FuzzStateSyscallBrk(f *testing.F) { - contracts, addrs := testContractsSetup(f) - f.Fuzz(func(t *testing.T, pc uint32, step uint64, preimageOffset uint32) { - pc = pc & 0xFF_FF_FF_FC // align PC - nextPC := pc + 4 - state := &singlethreaded.State{ - Cpu: mipsevm.CpuScalars{ - PC: pc, - NextPC: nextPC, - LO: 0, - HI: 0, - }, - Heap: 0, - ExitCode: 0, - Exited: false, - Memory: memory.NewMemory(), - Registers: [32]uint32{2: exec.SysBrk}, - Step: step, - PreimageKey: common.Hash{}, - PreimageOffset: preimageOffset, - } - state.Memory.SetMemory(pc, syscallInsn) - preStateRoot := state.Memory.MerkleRoot() - expectedRegisters := state.Registers - expectedRegisters[2] = program.PROGRAM_BREAK - - goState := singlethreaded.NewInstrumentedState(state, nil, os.Stdout, os.Stderr, nil) - stepWitness, err := goState.Step(true) - require.NoError(t, err) - require.False(t, stepWitness.HasPreimage()) - - require.Equal(t, pc+4, state.Cpu.PC) - require.Equal(t, nextPC+4, state.Cpu.NextPC) - require.Equal(t, uint32(0), state.Cpu.LO) - require.Equal(t, uint32(0), state.Cpu.HI) - require.Equal(t, uint32(0), state.Heap) - require.Equal(t, uint8(0), state.ExitCode) - require.Equal(t, false, state.Exited) - require.Equal(t, preStateRoot, state.Memory.MerkleRoot()) - require.Equal(t, expectedRegisters, state.Registers) - require.Equal(t, step+1, state.Step) - require.Equal(t, common.Hash{}, state.PreimageKey) - require.Equal(t, preimageOffset, state.PreimageOffset) - - evm := testutil.NewMIPSEVM(contracts, addrs) - evmPost := evm.Step(t, stepWitness, step, singlethreaded.GetStateHashFn()) - goPost, _ := goState.GetState().EncodeWitness() - require.Equal(t, hexutil.Bytes(goPost).String(), hexutil.Bytes(evmPost).String(), - "mipsevm produced different state than EVM") - }) -} - -func FuzzStateSyscallClone(f *testing.F) { - contracts, addrs := testContractsSetup(f) - f.Fuzz(func(t *testing.T, pc uint32, step uint64, preimageOffset uint32) { - pc = pc & 0xFF_FF_FF_FC // align PC - nextPC := pc + 4 - state := &singlethreaded.State{ - Cpu: mipsevm.CpuScalars{ - PC: pc, - NextPC: nextPC, - LO: 0, - HI: 0, - }, - Heap: 0, - ExitCode: 0, - Exited: false, - Memory: memory.NewMemory(), - Registers: [32]uint32{2: exec.SysClone}, - Step: step, - PreimageOffset: preimageOffset, - } - state.Memory.SetMemory(pc, syscallInsn) - preStateRoot := state.Memory.MerkleRoot() - expectedRegisters := state.Registers - expectedRegisters[2] = 0x1 - - goState := singlethreaded.NewInstrumentedState(state, nil, os.Stdout, os.Stderr, nil) - stepWitness, err := goState.Step(true) - require.NoError(t, err) - require.False(t, stepWitness.HasPreimage()) - - require.Equal(t, pc+4, state.Cpu.PC) - require.Equal(t, nextPC+4, state.Cpu.NextPC) - require.Equal(t, uint32(0), state.Cpu.LO) - require.Equal(t, uint32(0), state.Cpu.HI) - require.Equal(t, uint32(0), state.Heap) - require.Equal(t, uint8(0), state.ExitCode) - require.Equal(t, false, state.Exited) - require.Equal(t, preStateRoot, state.Memory.MerkleRoot()) - require.Equal(t, expectedRegisters, state.Registers) - require.Equal(t, step+1, state.Step) - require.Equal(t, common.Hash{}, state.PreimageKey) - require.Equal(t, preimageOffset, state.PreimageOffset) - - evm := testutil.NewMIPSEVM(contracts, addrs) - evmPost := evm.Step(t, stepWitness, step, singlethreaded.GetStateHashFn()) - goPost, _ := goState.GetState().EncodeWitness() - require.Equal(t, hexutil.Bytes(goPost).String(), hexutil.Bytes(evmPost).String(), - "mipsevm produced different state than EVM") - }) -} - -func FuzzStateSyscallMmap(f *testing.F) { - contracts, addrs := testContractsSetup(f) - step := uint64(0) - - // Add special cases for large memory allocation - f.Add(uint32(0), uint32(0x1000), uint32(program.HEAP_END), int64(1)) - f.Add(uint32(0), uint32(1<<31), uint32(program.HEAP_START), int64(2)) - // Check edge case - just within bounds - f.Add(uint32(0), uint32(0x1000), uint32(program.HEAP_END-4096), int64(3)) - - f.Fuzz(func(t *testing.T, addr uint32, siz uint32, heap uint32, seed int64) { - state := &singlethreaded.State{ - Cpu: mipsevm.CpuScalars{ - PC: 0, - NextPC: 4, - LO: 0, - HI: 0, - }, - Heap: heap, - ExitCode: 0, - Exited: false, - Memory: memory.NewMemory(), - Registers: testutil.RandomRegisters(seed), - Step: step, - PreimageOffset: 0, - } - state.Memory.SetMemory(0, syscallInsn) - state.Registers[2] = exec.SysMmap - state.Registers[4] = addr - state.Registers[5] = siz - preStateRoot := state.Memory.MerkleRoot() - preStateRegisters := state.Registers - - goState := singlethreaded.NewInstrumentedState(state, nil, os.Stdout, os.Stderr, nil) - stepWitness, err := goState.Step(true) - require.NoError(t, err) - require.False(t, stepWitness.HasPreimage()) - - var expectedHeap uint32 - expectedRegisters := preStateRegisters - if addr == 0 { - sizAlign := siz - if sizAlign&memory.PageAddrMask != 0 { // adjust size to align with page size - sizAlign = siz + memory.PageSize - (siz & memory.PageAddrMask) - } - newHeap := heap + sizAlign - if newHeap > program.HEAP_END || newHeap < heap || sizAlign < siz { - expectedHeap = heap - expectedRegisters[2] = exec.SysErrorSignal - expectedRegisters[7] = exec.MipsEINVAL - } else { - expectedRegisters[2] = heap - expectedRegisters[7] = 0 // no error - expectedHeap = heap + sizAlign - } - } else { - expectedRegisters[2] = addr - expectedRegisters[7] = 0 // no error - expectedHeap = heap - } - - require.Equal(t, uint32(4), state.Cpu.PC) - require.Equal(t, uint32(8), state.Cpu.NextPC) - require.Equal(t, uint32(0), state.Cpu.LO) - require.Equal(t, uint32(0), state.Cpu.HI) - require.Equal(t, preStateRoot, state.Memory.MerkleRoot()) - require.Equal(t, uint64(1), state.Step) - require.Equal(t, common.Hash{}, state.PreimageKey) - require.Equal(t, uint32(0), state.PreimageOffset) - require.Equal(t, expectedHeap, state.Heap) - require.Equal(t, uint8(0), state.ExitCode) - require.Equal(t, false, state.Exited) - require.Equal(t, expectedRegisters, state.Registers) - - evm := testutil.NewMIPSEVM(contracts, addrs) - evmPost := evm.Step(t, stepWitness, step, singlethreaded.GetStateHashFn()) - goPost, _ := goState.GetState().EncodeWitness() - require.Equal(t, hexutil.Bytes(goPost).String(), hexutil.Bytes(evmPost).String(), - "mipsevm produced different state than EVM") - }) -} - -func FuzzStateSyscallExitGroup(f *testing.F) { - contracts, addrs := testContractsSetup(f) - f.Fuzz(func(t *testing.T, exitCode uint8, pc uint32, step uint64) { - pc = pc & 0xFF_FF_FF_FC // align PC - nextPC := pc + 4 - state := &singlethreaded.State{ - Cpu: mipsevm.CpuScalars{ - PC: pc, - NextPC: nextPC, - LO: 0, - HI: 0, - }, - Heap: 0, - ExitCode: 0, - Exited: false, - Memory: memory.NewMemory(), - Registers: [32]uint32{2: exec.SysExitGroup, 4: uint32(exitCode)}, - Step: step, - PreimageOffset: 0, - } - state.Memory.SetMemory(pc, syscallInsn) - preStateRoot := state.Memory.MerkleRoot() - preStateRegisters := state.Registers - - goState := singlethreaded.NewInstrumentedState(state, nil, os.Stdout, os.Stderr, nil) - stepWitness, err := goState.Step(true) - require.NoError(t, err) - require.False(t, stepWitness.HasPreimage()) - - require.Equal(t, pc, state.Cpu.PC) - require.Equal(t, nextPC, state.Cpu.NextPC) - require.Equal(t, uint32(0), state.Cpu.LO) - require.Equal(t, uint32(0), state.Cpu.HI) - require.Equal(t, uint32(0), state.Heap) - require.Equal(t, uint8(exitCode), state.ExitCode) - require.Equal(t, true, state.Exited) - require.Equal(t, preStateRoot, state.Memory.MerkleRoot()) - require.Equal(t, preStateRegisters, state.Registers) - require.Equal(t, step+1, state.Step) - require.Equal(t, common.Hash{}, state.PreimageKey) - require.Equal(t, uint32(0), state.PreimageOffset) - - evm := testutil.NewMIPSEVM(contracts, addrs) - evmPost := evm.Step(t, stepWitness, step, singlethreaded.GetStateHashFn()) - goPost, _ := goState.GetState().EncodeWitness() - require.Equal(t, hexutil.Bytes(goPost).String(), hexutil.Bytes(evmPost).String(), - "mipsevm produced different state than EVM") - }) -} - -func FuzzStateSyscallFcntl(f *testing.F) { - contracts, addrs := testContractsSetup(f) - step := uint64(0) - f.Fuzz(func(t *testing.T, fd uint32, cmd uint32) { - state := &singlethreaded.State{ - Cpu: mipsevm.CpuScalars{ - PC: 0, - NextPC: 4, - LO: 0, - HI: 0, - }, - Heap: 0, - ExitCode: 0, - Exited: false, - Memory: memory.NewMemory(), - Registers: [32]uint32{2: exec.SysFcntl, 4: fd, 5: cmd}, - Step: step, - PreimageOffset: 0, - } - state.Memory.SetMemory(0, syscallInsn) - preStateRoot := state.Memory.MerkleRoot() - preStateRegisters := state.Registers - - goState := singlethreaded.NewInstrumentedState(state, nil, os.Stdout, os.Stderr, nil) - stepWitness, err := goState.Step(true) - require.NoError(t, err) - require.False(t, stepWitness.HasPreimage()) - - require.Equal(t, uint32(4), state.Cpu.PC) - require.Equal(t, uint32(8), state.Cpu.NextPC) - require.Equal(t, uint32(0), state.Cpu.LO) - require.Equal(t, uint32(0), state.Cpu.HI) - require.Equal(t, uint32(0), state.Heap) - require.Equal(t, uint8(0), state.ExitCode) - require.Equal(t, false, state.Exited) - require.Equal(t, preStateRoot, state.Memory.MerkleRoot()) - require.Equal(t, uint64(1), state.Step) - require.Equal(t, common.Hash{}, state.PreimageKey) - require.Equal(t, uint32(0), state.PreimageOffset) - if cmd == 3 { - expectedRegisters := preStateRegisters - switch fd { - case exec.FdStdin, exec.FdPreimageRead, exec.FdHintRead: - expectedRegisters[2] = 0 - case exec.FdStdout, exec.FdStderr, exec.FdPreimageWrite, exec.FdHintWrite: - expectedRegisters[2] = 1 - default: - expectedRegisters[2] = 0xFF_FF_FF_FF - expectedRegisters[7] = exec.MipsEBADF - } - require.Equal(t, expectedRegisters, state.Registers) - } else { - expectedRegisters := preStateRegisters - expectedRegisters[2] = 0xFF_FF_FF_FF - expectedRegisters[7] = exec.MipsEINVAL - require.Equal(t, expectedRegisters, state.Registers) - } - - evm := testutil.NewMIPSEVM(contracts, addrs) - evmPost := evm.Step(t, stepWitness, step, singlethreaded.GetStateHashFn()) - goPost, _ := goState.GetState().EncodeWitness() - require.Equal(t, hexutil.Bytes(goPost).String(), hexutil.Bytes(evmPost).String(), - "mipsevm produced different state than EVM") - }) -} - -func FuzzStateHintRead(f *testing.F) { - contracts, addrs := testContractsSetup(f) - step := uint64(0) - f.Fuzz(func(t *testing.T, addr uint32, count uint32) { - preimageData := []byte("hello world") - state := &singlethreaded.State{ - Cpu: mipsevm.CpuScalars{ - PC: 0, - NextPC: 4, - LO: 0, - HI: 0, - }, - Heap: 0, - ExitCode: 0, - Exited: false, - Memory: memory.NewMemory(), - Registers: [32]uint32{2: exec.SysRead, 4: exec.FdHintRead, 5: addr, 6: count}, - Step: step, - PreimageKey: preimage.Keccak256Key(crypto.Keccak256Hash(preimageData)).PreimageKey(), - PreimageOffset: 0, - } - state.Memory.SetMemory(0, syscallInsn) - preStatePreimageKey := state.PreimageKey - preStateRoot := state.Memory.MerkleRoot() - expectedRegisters := state.Registers - expectedRegisters[2] = count - - oracle := testutil.StaticOracle(t, preimageData) // only used for hinting - goState := singlethreaded.NewInstrumentedState(state, oracle, os.Stdout, os.Stderr, nil) - stepWitness, err := goState.Step(true) - require.NoError(t, err) - require.False(t, stepWitness.HasPreimage()) - - require.Equal(t, uint32(4), state.Cpu.PC) - require.Equal(t, uint32(8), state.Cpu.NextPC) - require.Equal(t, uint32(0), state.Cpu.LO) - require.Equal(t, uint32(0), state.Cpu.HI) - require.Equal(t, uint32(0), state.Heap) - require.Equal(t, uint8(0), state.ExitCode) - require.Equal(t, false, state.Exited) - require.Equal(t, preStateRoot, state.Memory.MerkleRoot()) - require.Equal(t, uint64(1), state.Step) - require.Equal(t, preStatePreimageKey, state.PreimageKey) - require.Equal(t, expectedRegisters, state.Registers) - - evm := testutil.NewMIPSEVM(contracts, addrs) - evmPost := evm.Step(t, stepWitness, step, singlethreaded.GetStateHashFn()) - goPost, _ := goState.GetState().EncodeWitness() - require.Equal(t, hexutil.Bytes(goPost).String(), hexutil.Bytes(evmPost).String(), - "mipsevm produced different state than EVM") - }) -} - -func FuzzStatePreimageRead(f *testing.F) { - contracts, addrs := testContractsSetup(f) - step := uint64(0) - f.Fuzz(func(t *testing.T, addr uint32, count uint32, preimageOffset uint32) { - preimageData := []byte("hello world") - if preimageOffset >= uint32(len(preimageData)) { - t.SkipNow() - } - state := &singlethreaded.State{ - Cpu: mipsevm.CpuScalars{ - PC: 0, - NextPC: 4, - LO: 0, - HI: 0, - }, - Heap: 0, - ExitCode: 0, - Exited: false, - Memory: memory.NewMemory(), - Registers: [32]uint32{2: exec.SysRead, 4: exec.FdPreimageRead, 5: addr, 6: count}, - Step: step, - PreimageKey: preimage.Keccak256Key(crypto.Keccak256Hash(preimageData)).PreimageKey(), - PreimageOffset: preimageOffset, - } - state.Memory.SetMemory(0, syscallInsn) - preStatePreimageKey := state.PreimageKey - preStateRoot := state.Memory.MerkleRoot() - writeLen := count - if writeLen > 4 { - writeLen = 4 - } - if preimageOffset+writeLen > uint32(8+len(preimageData)) { - writeLen = uint32(8+len(preimageData)) - preimageOffset - } - oracle := testutil.StaticOracle(t, preimageData) - - goState := singlethreaded.NewInstrumentedState(state, oracle, os.Stdout, os.Stderr, nil) - stepWitness, err := goState.Step(true) - require.NoError(t, err) - require.True(t, stepWitness.HasPreimage()) - - require.Equal(t, uint32(4), state.Cpu.PC) - require.Equal(t, uint32(8), state.Cpu.NextPC) - require.Equal(t, uint32(0), state.Cpu.LO) - require.Equal(t, uint32(0), state.Cpu.HI) - require.Equal(t, uint32(0), state.Heap) - require.Equal(t, uint8(0), state.ExitCode) - require.Equal(t, false, state.Exited) - if writeLen > 0 { - // Memory may be unchanged if we're writing the first zero-valued 7 bytes of the pre-image. - //require.NotEqual(t, preStateRoot, state.Memory.MerkleRoot()) - require.Greater(t, state.PreimageOffset, preimageOffset) - } else { - require.Equal(t, preStateRoot, state.Memory.MerkleRoot()) - require.Equal(t, state.PreimageOffset, preimageOffset) - } - require.Equal(t, uint64(1), state.Step) - require.Equal(t, preStatePreimageKey, state.PreimageKey) - - evm := testutil.NewMIPSEVM(contracts, addrs) - evmPost := evm.Step(t, stepWitness, step, singlethreaded.GetStateHashFn()) - goPost, _ := goState.GetState().EncodeWitness() - require.Equal(t, hexutil.Bytes(goPost).String(), hexutil.Bytes(evmPost).String(), - "mipsevm produced different state than EVM") - }) -} - -func FuzzStateHintWrite(f *testing.F) { - contracts, addrs := testContractsSetup(f) - step := uint64(0) - f.Fuzz(func(t *testing.T, addr uint32, count uint32, randSeed int64) { - preimageData := []byte("hello world") - state := &singlethreaded.State{ - Cpu: mipsevm.CpuScalars{ - PC: 0, - NextPC: 4, - LO: 0, - HI: 0, - }, - Heap: 0, - ExitCode: 0, - Exited: false, - Memory: memory.NewMemory(), - Registers: [32]uint32{2: exec.SysWrite, 4: exec.FdHintWrite, 5: addr, 6: count}, - Step: step, - PreimageKey: preimage.Keccak256Key(crypto.Keccak256Hash(preimageData)).PreimageKey(), - PreimageOffset: 0, - LastHint: nil, - } - // Set random data at the target memory range - randBytes, err := randomBytes(randSeed, count) - require.NoError(t, err) - err = state.Memory.SetMemoryRange(addr, bytes.NewReader(randBytes)) - require.NoError(t, err) - // Set syscall instruction - state.Memory.SetMemory(0, syscallInsn) - - preStatePreimageKey := state.PreimageKey - preStateRoot := state.Memory.MerkleRoot() - expectedRegisters := state.Registers - expectedRegisters[2] = count - - oracle := testutil.StaticOracle(t, preimageData) // only used for hinting - goState := singlethreaded.NewInstrumentedState(state, oracle, os.Stdout, os.Stderr, nil) - stepWitness, err := goState.Step(true) - require.NoError(t, err) - require.False(t, stepWitness.HasPreimage()) - - require.Equal(t, uint32(4), state.Cpu.PC) - require.Equal(t, uint32(8), state.Cpu.NextPC) - require.Equal(t, uint32(0), state.Cpu.LO) - require.Equal(t, uint32(0), state.Cpu.HI) - require.Equal(t, uint32(0), state.Heap) - require.Equal(t, uint8(0), state.ExitCode) - require.Equal(t, false, state.Exited) - require.Equal(t, preStateRoot, state.Memory.MerkleRoot()) - require.Equal(t, uint64(1), state.Step) - require.Equal(t, preStatePreimageKey, state.PreimageKey) - require.Equal(t, expectedRegisters, state.Registers) - - evm := testutil.NewMIPSEVM(contracts, addrs) - evmPost := evm.Step(t, stepWitness, step, singlethreaded.GetStateHashFn()) - goPost, _ := goState.GetState().EncodeWitness() - require.Equal(t, hexutil.Bytes(goPost).String(), hexutil.Bytes(evmPost).String(), - "mipsevm produced different state than EVM") - }) -} - -func FuzzStatePreimageWrite(f *testing.F) { - contracts, addrs := testContractsSetup(f) - step := uint64(0) - f.Fuzz(func(t *testing.T, addr uint32, count uint32) { - preimageData := []byte("hello world") - state := &singlethreaded.State{ - Cpu: mipsevm.CpuScalars{ - PC: 0, - NextPC: 4, - LO: 0, - HI: 0, - }, - Heap: 0, - ExitCode: 0, - Exited: false, - Memory: memory.NewMemory(), - Registers: [32]uint32{2: exec.SysWrite, 4: exec.FdPreimageWrite, 5: addr, 6: count}, - Step: 0, - PreimageKey: preimage.Keccak256Key(crypto.Keccak256Hash(preimageData)).PreimageKey(), - PreimageOffset: 128, - } - state.Memory.SetMemory(0, syscallInsn) - preStateRoot := state.Memory.MerkleRoot() - expectedRegisters := state.Registers - sz := 4 - (addr & 0x3) - if sz < count { - count = sz - } - expectedRegisters[2] = count - - oracle := testutil.StaticOracle(t, preimageData) - goState := singlethreaded.NewInstrumentedState(state, oracle, os.Stdout, os.Stderr, nil) - stepWitness, err := goState.Step(true) - require.NoError(t, err) - require.False(t, stepWitness.HasPreimage()) - - require.Equal(t, uint32(4), state.Cpu.PC) - require.Equal(t, uint32(8), state.Cpu.NextPC) - require.Equal(t, uint32(0), state.Cpu.LO) - require.Equal(t, uint32(0), state.Cpu.HI) - require.Equal(t, uint32(0), state.Heap) - require.Equal(t, uint8(0), state.ExitCode) - require.Equal(t, false, state.Exited) - require.Equal(t, preStateRoot, state.Memory.MerkleRoot()) - require.Equal(t, uint64(1), state.Step) - require.Equal(t, uint32(0), state.PreimageOffset) - require.Equal(t, expectedRegisters, state.Registers) - - evm := testutil.NewMIPSEVM(contracts, addrs) - evmPost := evm.Step(t, stepWitness, step, singlethreaded.GetStateHashFn()) - goPost, _ := goState.GetState().EncodeWitness() - require.Equal(t, hexutil.Bytes(goPost).String(), hexutil.Bytes(evmPost).String(), - "mipsevm produced different state than EVM") - }) -} - -func randomBytes(seed int64, length uint32) ([]byte, error) { - r := rand.New(rand.NewSource(seed)) - randBytes := make([]byte, length) - if _, err := r.Read(randBytes); err != nil { - return nil, err - } - return randBytes, nil -} diff --git a/cannon/mipsevm/tests/helpers.go b/cannon/mipsevm/tests/helpers.go new file mode 100644 index 0000000000000..c83aea0be3c55 --- /dev/null +++ b/cannon/mipsevm/tests/helpers.go @@ -0,0 +1,206 @@ +package tests + +import ( + "io" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/log" + "github.com/stretchr/testify/require" + + "github.com/ethereum-optimism/optimism/cannon/mipsevm" + "github.com/ethereum-optimism/optimism/cannon/mipsevm/multithreaded" + "github.com/ethereum-optimism/optimism/cannon/mipsevm/singlethreaded" + "github.com/ethereum-optimism/optimism/cannon/mipsevm/testutil" +) + +type StateMutator interface { + SetPC(pc uint32) + SetNextPC(nextPC uint32) + SetHeap(addr uint32) + SetLastHint(lastHint hexutil.Bytes) + SetPreimageKey(key common.Hash) + SetPreimageOffset(offset uint32) + SetStep(step uint64) +} + +type singlethreadedMutator struct { + state *singlethreaded.State +} + +var _ StateMutator = (*singlethreadedMutator)(nil) + +func (m *singlethreadedMutator) SetPC(pc uint32) { + m.state.Cpu.PC = pc +} + +func (m *singlethreadedMutator) SetNextPC(nextPC uint32) { + m.state.Cpu.NextPC = nextPC +} + +func (m *singlethreadedMutator) SetHeap(addr uint32) { + m.state.Heap = addr +} + +func (m *singlethreadedMutator) SetLastHint(lastHint hexutil.Bytes) { + m.state.LastHint = lastHint +} + +func (m *singlethreadedMutator) SetPreimageKey(key common.Hash) { + m.state.PreimageKey = key +} + +func (m *singlethreadedMutator) SetPreimageOffset(offset uint32) { + m.state.PreimageOffset = offset +} + +func (m *singlethreadedMutator) SetStep(step uint64) { + m.state.Step = step +} + +type multithreadedMutator struct { + state *multithreaded.State +} + +var _ StateMutator = (*multithreadedMutator)(nil) + +func (m *multithreadedMutator) SetPC(pc uint32) { + thread := m.state.GetCurrentThread() + thread.Cpu.PC = pc +} + +func (m *multithreadedMutator) SetHeap(addr uint32) { + m.state.Heap = addr +} + +func (m *multithreadedMutator) SetNextPC(nextPC uint32) { + thread := m.state.GetCurrentThread() + thread.Cpu.NextPC = nextPC +} + +func (m *multithreadedMutator) SetLastHint(lastHint hexutil.Bytes) { + m.state.LastHint = lastHint +} + +func (m *multithreadedMutator) SetPreimageKey(key common.Hash) { + m.state.PreimageKey = key +} + +func (m *multithreadedMutator) SetPreimageOffset(offset uint32) { + m.state.PreimageOffset = offset +} + +func (m *multithreadedMutator) SetStep(step uint64) { + m.state.Step = step +} + +type VMOption func(vm StateMutator) + +func WithPC(pc uint32) VMOption { + return func(state StateMutator) { + state.SetPC(pc) + } +} + +func WithNextPC(nextPC uint32) VMOption { + return func(state StateMutator) { + state.SetNextPC(nextPC) + } +} + +func WithHeap(addr uint32) VMOption { + return func(state StateMutator) { + state.SetHeap(addr) + } +} + +func WithLastHint(lastHint hexutil.Bytes) VMOption { + return func(state StateMutator) { + state.SetLastHint(lastHint) + } +} + +func WithPreimageKey(key common.Hash) VMOption { + return func(state StateMutator) { + state.SetPreimageKey(key) + } +} + +func WithPreimageOffset(offset uint32) VMOption { + return func(state StateMutator) { + state.SetPreimageOffset(offset) + } +} + +func WithStep(step uint64) VMOption { + return func(state StateMutator) { + state.SetStep(step) + } +} + +type VMFactory func(po mipsevm.PreimageOracle, stdOut, stdErr io.Writer, log log.Logger, opts ...VMOption) mipsevm.FPVM + +func singleThreadedVmFactory(po mipsevm.PreimageOracle, stdOut, stdErr io.Writer, log log.Logger, opts ...VMOption) mipsevm.FPVM { + state := singlethreaded.CreateEmptyState() + mutator := &singlethreadedMutator{state: state} + for _, opt := range opts { + opt(mutator) + } + return singlethreaded.NewInstrumentedState(state, po, stdOut, stdErr, nil) +} + +func multiThreadedVmFactory(po mipsevm.PreimageOracle, stdOut, stdErr io.Writer, log log.Logger, opts ...VMOption) mipsevm.FPVM { + state := multithreaded.CreateEmptyState() + mutator := &multithreadedMutator{state: state} + for _, opt := range opts { + opt(mutator) + } + return multithreaded.NewInstrumentedState(state, po, stdOut, stdErr, log) +} + +type ElfVMFactory func(t require.TestingT, elfFile string, po mipsevm.PreimageOracle, stdOut, stdErr io.Writer, log log.Logger) mipsevm.FPVM + +func singleThreadElfVmFactory(t require.TestingT, elfFile string, po mipsevm.PreimageOracle, stdOut, stdErr io.Writer, log log.Logger) mipsevm.FPVM { + state := testutil.LoadELFProgram(t, elfFile, singlethreaded.CreateInitialState, true) + return singlethreaded.NewInstrumentedState(state, po, stdOut, stdErr, nil) +} + +func multiThreadElfVmFactory(t require.TestingT, elfFile string, po mipsevm.PreimageOracle, stdOut, stdErr io.Writer, log log.Logger) mipsevm.FPVM { + state := testutil.LoadELFProgram(t, elfFile, multithreaded.CreateInitialState, false) + return multithreaded.NewInstrumentedState(state, po, stdOut, stdErr, log) +} + +type VersionedVMTestCase struct { + Name string + Contracts *testutil.ContractMetadata + StateHashFn mipsevm.HashFn + VMFactory VMFactory + ElfVMFactory ElfVMFactory +} + +func GetSingleThreadedTestCase(t require.TestingT) VersionedVMTestCase { + return VersionedVMTestCase{ + Name: "single-threaded", + Contracts: testutil.TestContractsSetup(t, testutil.MipsSingleThreaded), + StateHashFn: singlethreaded.GetStateHashFn(), + VMFactory: singleThreadedVmFactory, + ElfVMFactory: singleThreadElfVmFactory, + } +} + +func GetMultiThreadedTestCase(t require.TestingT) VersionedVMTestCase { + return VersionedVMTestCase{ + Name: "multi-threaded", + Contracts: testutil.TestContractsSetup(t, testutil.MipsMultithreaded), + StateHashFn: multithreaded.GetStateHashFn(), + VMFactory: multiThreadedVmFactory, + ElfVMFactory: multiThreadElfVmFactory, + } +} + +func GetMipsVersionTestCases(t require.TestingT) []VersionedVMTestCase { + return []VersionedVMTestCase{ + GetSingleThreadedTestCase(t), + GetMultiThreadedTestCase(t), + } +} diff --git a/cannon/mipsevm/testutil/constants.go b/cannon/mipsevm/testutil/constants.go index ccbd86574e8bc..1575799b4d4ad 100644 --- a/cannon/mipsevm/testutil/constants.go +++ b/cannon/mipsevm/testutil/constants.go @@ -5,3 +5,10 @@ const BaseAddrEnd = 0xbf_ff_ff_f0 // EndAddr is used as return-address for tests const EndAddr = 0xa7ef00d0 + +type MipsVersion int + +const ( + MipsSingleThreaded MipsVersion = iota + MipsMultithreaded +) diff --git a/cannon/mipsevm/testutil/elf.go b/cannon/mipsevm/testutil/elf.go index c82950f02e623..697928799d5ba 100644 --- a/cannon/mipsevm/testutil/elf.go +++ b/cannon/mipsevm/testutil/elf.go @@ -2,7 +2,6 @@ package testutil import ( "debug/elf" - "testing" "github.com/stretchr/testify/require" @@ -10,7 +9,7 @@ import ( "github.com/ethereum-optimism/optimism/cannon/mipsevm/program" ) -func LoadELFProgram[T mipsevm.FPVMState](t *testing.T, name string, initState program.CreateInitialFPVMState[T], doPatchGo bool) T { +func LoadELFProgram[T mipsevm.FPVMState](t require.TestingT, name string, initState program.CreateInitialFPVMState[T], doPatchGo bool) T { elfProgram, err := elf.Open(name) require.NoError(t, err, "open ELF file") diff --git a/cannon/mipsevm/testutil/evm.go b/cannon/mipsevm/testutil/evm.go index 6f6760968f5d9..ee833629fc04b 100644 --- a/cannon/mipsevm/testutil/evm.go +++ b/cannon/mipsevm/testutil/evm.go @@ -9,6 +9,7 @@ import ( "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/eth/tracers/logger" + "github.com/stretchr/testify/require" "github.com/ethereum-optimism/optimism/op-chain-ops/foundry" @@ -23,9 +24,50 @@ import ( "github.com/ethereum/go-ethereum/params" ) -// LoadArtifacts loads the Cannon contracts, from the contracts package. -func LoadArtifacts() (*Artifacts, error) { - mips, err := foundry.ReadArtifact("../../../packages/contracts-bedrock/forge-artifacts/MIPS.sol/MIPS.json") +type Artifacts struct { + MIPS *foundry.Artifact + Oracle *foundry.Artifact +} + +type Addresses struct { + MIPS common.Address + Oracle common.Address + Sender common.Address + FeeRecipient common.Address +} + +type ContractMetadata struct { + Artifacts *Artifacts + Addresses *Addresses +} + +func TestContractsSetup(t require.TestingT, version MipsVersion) *ContractMetadata { + artifacts, err := loadArtifacts(version) + require.NoError(t, err) + + addrs := &Addresses{ + MIPS: common.Address{0: 0xff, 19: 1}, + Oracle: common.Address{0: 0xff, 19: 2}, + Sender: common.Address{0x13, 0x37}, + FeeRecipient: common.Address{0xaa}, + } + + return &ContractMetadata{Artifacts: artifacts, Addresses: addrs} +} + +// loadArtifacts loads the Cannon contracts, from the contracts package. +func loadArtifacts(version MipsVersion) (*Artifacts, error) { + var mipsMetadata string + switch version { + case MipsSingleThreaded: + mipsMetadata = "../../../packages/contracts-bedrock/forge-artifacts/MIPS.sol/MIPS.json" + case MipsMultithreaded: + mipsMetadata = "../../../packages/contracts-bedrock/forge-artifacts/MIPS2.sol/MIPS2.json" + default: + return nil, fmt.Errorf("Unknown MipsVersion supplied: %v", version) + } + + mips, err := foundry.ReadArtifact(mipsMetadata) if err != nil { return nil, fmt.Errorf("failed to load MIPS contract: %w", err) } @@ -41,19 +83,7 @@ func LoadArtifacts() (*Artifacts, error) { }, nil } -type Artifacts struct { - MIPS *foundry.Artifact - Oracle *foundry.Artifact -} - -type Addresses struct { - MIPS common.Address - Oracle common.Address - Sender common.Address - FeeRecipient common.Address -} - -func NewEVMEnv(artifacts *Artifacts, addrs *Addresses) (*vm.EVM, *state.StateDB) { +func NewEVMEnv(contracts *ContractMetadata) (*vm.EVM, *state.StateDB) { // Temporary hack until Cancun is activated on mainnet cpy := *params.MainnetChainConfig chainCfg := &cpy // don't modify the global chain config @@ -74,20 +104,20 @@ func NewEVMEnv(artifacts *Artifacts, addrs *Addresses) (*vm.EVM, *state.StateDB) env := vm.NewEVM(blockContext, vm.TxContext{}, state, chainCfg, vmCfg) // pre-deploy the contracts - env.StateDB.SetCode(addrs.Oracle, artifacts.Oracle.DeployedBytecode.Object) + env.StateDB.SetCode(contracts.Addresses.Oracle, contracts.Artifacts.Oracle.DeployedBytecode.Object) var mipsCtorArgs [32]byte - copy(mipsCtorArgs[12:], addrs.Oracle[:]) - mipsDeploy := append(bytes.Clone(artifacts.MIPS.Bytecode.Object), mipsCtorArgs[:]...) + copy(mipsCtorArgs[12:], contracts.Addresses.Oracle[:]) + mipsDeploy := append(bytes.Clone(contracts.Artifacts.MIPS.Bytecode.Object), mipsCtorArgs[:]...) startingGas := uint64(30_000_000) - _, deployedMipsAddr, leftOverGas, err := env.Create(vm.AccountRef(addrs.Sender), mipsDeploy, startingGas, common.U2560) + _, deployedMipsAddr, leftOverGas, err := env.Create(vm.AccountRef(contracts.Addresses.Sender), mipsDeploy, startingGas, common.U2560) if err != nil { panic(fmt.Errorf("failed to deploy MIPS contract: %w. took %d gas", err, startingGas-leftOverGas)) } - addrs.MIPS = deployedMipsAddr + contracts.Addresses.MIPS = deployedMipsAddr rules := env.ChainConfig().Rules(header.Number, true, header.Time) - env.StateDB.Prepare(rules, addrs.Sender, addrs.FeeRecipient, &addrs.MIPS, vm.ActivePrecompiles(rules), nil) + env.StateDB.Prepare(rules, contracts.Addresses.Sender, contracts.Addresses.FeeRecipient, &contracts.Addresses.MIPS, vm.ActivePrecompiles(rules), nil) return env, state } diff --git a/cannon/mipsevm/testutil/mips.go b/cannon/mipsevm/testutil/mips.go index 0efb68b42e50c..edc1427b4409c 100644 --- a/cannon/mipsevm/testutil/mips.go +++ b/cannon/mipsevm/testutil/mips.go @@ -30,9 +30,9 @@ type MIPSEVM struct { lastStepInput []byte } -func NewMIPSEVM(artifacts *Artifacts, addrs *Addresses) *MIPSEVM { - env, evmState := NewEVMEnv(artifacts, addrs) - return &MIPSEVM{env, evmState, addrs, nil, artifacts, math.MaxUint64, nil} +func NewMIPSEVM(contracts *ContractMetadata) *MIPSEVM { + env, evmState := NewEVMEnv(contracts) + return &MIPSEVM{env, evmState, contracts.Addresses, nil, contracts.Artifacts, math.MaxUint64, nil} } func (m *MIPSEVM) SetTracer(tracer *tracing.Hooks) { diff --git a/cannon/mipsevm/testutil/state.go b/cannon/mipsevm/testutil/state.go index ebbde63b495c2..900cd6d4d11c6 100644 --- a/cannon/mipsevm/testutil/state.go +++ b/cannon/mipsevm/testutil/state.go @@ -1,6 +1,10 @@ package testutil -import "math/rand" +import ( + "math/rand" + + "github.com/ethereum-optimism/optimism/cannon/mipsevm" +) func RandomRegisters(seed int64) [32]uint32 { r := rand.New(rand.NewSource(seed)) @@ -10,3 +14,9 @@ func RandomRegisters(seed int64) [32]uint32 { } return registers } + +func CopyRegisters(state mipsevm.FPVMState) *[32]uint32 { + copy := new([32]uint32) + *copy = *state.GetRegistersRef() + return copy +} diff --git a/cannon/mipsevm/testutil/vmtests.go b/cannon/mipsevm/testutil/vmtests.go index 45fb9e7f9f705..75b5c3ac173bd 100644 --- a/cannon/mipsevm/testutil/vmtests.go +++ b/cannon/mipsevm/testutil/vmtests.go @@ -49,7 +49,7 @@ func RunVMTests_OpenMips[T mipsevm.FPVMState](t *testing.T, stateFactory StateFa require.NoError(t, err, "load program into state") // set the return address ($ra) to jump into when test completes - state.GetRegisters()[31] = EndAddr + state.GetRegistersRef()[31] = EndAddr us := vmFactory(state, oracle, os.Stdout, os.Stderr, CreateLogger()) diff --git a/packages/contracts-bedrock/test/cannon/MIPS2.t.sol b/packages/contracts-bedrock/test/cannon/MIPS2.t.sol index 2a6c417ab3f8f..30d6899caad68 100644 --- a/packages/contracts-bedrock/test/cannon/MIPS2.t.sol +++ b/packages/contracts-bedrock/test/cannon/MIPS2.t.sol @@ -133,6 +133,21 @@ contract MIPS2_Test is CommonTest { vm.label(address(threading), "Threading"); } + /// @notice Used to debug step() behavior given a specific input. + /// This is useful to more easily debug non-forge tests. + /// For example, in cannon/mipsevm/evm_test.go step input can be pulled here: + /// https://github.com/ethereum-optimism/optimism/blob/1f64dd6db5561f3bb76ed1d1ffdaff0cde9b7c4b/cannon/mipsevm/evm_test.go#L80-L80 + function test_mips2_step_debug_succeeds() external { + bytes memory input = + hex"e14ced3200000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a3df82bcbdf27955e04d467b84d94d0b4662c88a70264d7ea31325bc8d826681ef000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000affffffff00cbf05eda4a03d05cc6a14cff1cf2f955bfb253097c296ea96032da307da4f353ad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb500000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007c6000000000000ffffffff000000000000000000000000000000280000002c00000000000000000000000000000000000000010000000000000000000000000000000000000000fffffffd00000003000000000000000000000000000000000000000000000000bffffff00000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a7ef00d0ad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5ae020008ae11000403e0000800000000000000000000000000000000000000003c10bfff3610fff0341100013c08ffff3508fffd34090003010950212d420001ad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5b4c11951957c6f8f642c4af61cd6b24640fec6dc7fc607ee8206a99e92410d3021ddb9a356815c3fac1026b6dec5df3124afbadb485c9ba5a3e3398a04b7ba85e58769b32a1beaf1ea27375a44095a0d1fb664ce2dd358e7fcbfb78c26a193440eb01ebfc9ed27500cd4dfc979272d1f0913cc9f66540d7e8005811109e1cf2d887c22bd8750d34016ac3c66b5ff102dacdd73f6b014e710b51e8022af9a1968ffd70157e48063fc33c97a050f7f640233bf646cc98d9524c6b92bcf3ab56f839867cc5f7f196b93bae1e27e6320742445d290f2263827498b54fec539f756afcefad4e508c098b9a7e1d8feb19955fb02ba9675585078710969d3440f5054e0f9dc3e7fe016e050eff260334f18a5d4fe391d82092319f5964f2e2eb7c1c3a5f8b13a49e282f609c317a833fb8d976d11517c571d1221a265d25af778ecf8923490c6ceeb450aecdc82e28293031d10c7d73bf85e57bf041a97360aa2c5d99cc1df82d9c4b87413eae2ef048f94b4d3554cea73d92b0f7af96e0271c691e2bb5c67add7c6caf302256adedf7ab114da0acfe870d449a3a489f781d659e8beccda7bce9f4e8618b6bd2f4132ce798cdc7a60e7e1460a7299e3c6342a579626d22733e50f526ec2fa19a22b31e8ed50f23cd1fdf94c9154ed3a7609a2f1ff981fe1d3b5c807b281e4683cc6d6315cf95b9ade8641defcb32372f1c126e398ef7a5a2dce0a8a7f68bb74560f8f71837c2c2ebbcbf7fffb42ae1896f13f7c7479a0b46a28b6f55540f89444f63de0378e3d121be09e06cc9ded1c20e65876d36aa0c65e9645644786b620e2dd2ad648ddfcbf4a7e5b1a3a4ecfe7f64667a3f0b7e2f4418588ed35a2458cffeb39b93d26f18d2ab13bdce6aee58e7b99359ec2dfd95a9c16dc00d6ef18b7933a6f8dc65ccb55667138776f7dea101070dc8796e3774df84f40ae0c8229d0d6069e5c8f39a7c299677a09d367fc7b05e3bc380ee652cdc72595f74c7b1043d0e1ffbab734648c838dfb0527d971b602bc216c9619ef0abf5ac974a1ed57f4050aa510dd9c74f508277b39d7973bb2dfccc5eeb0618d4e545be579dc7118fc02cd7b19b704e4710a81bce0cb48bb7e289e403e7c969a00000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000ad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5b4c11951957c6f8f642c4af61cd6b24640fec6dc7fc607ee8206a99e92410d3021ddb9a356815c3fac1026b6dec5df3124afbadb485c9ba5a3e3398a04b7ba85e58769b32a1beaf1ea27375a44095a0d1fb664ce2dd358e7fcbfb78c26a193440eb01ebfc9ed27500cd4dfc979272d1f0913cc9f66540d7e8005811109e1cf2d887c22bd8750d34016ac3c66b5ff102dacdd73f6b014e710b51e8022af9a1968ffd70157e48063fc33c97a050f7f640233bf646cc98d9524c6b92bcf3ab56f839867cc5f7f196b93bae1e27e6320742445d290f2263827498b54fec539f756afcefad4e508c098b9a7e1d8feb19955fb02ba9675585078710969d3440f5054e0f9dc3e7fe016e050eff260334f18a5d4fe391d82092319f5964f2e2eb7c1c3a5f8b13a49e282f609c317a833fb8d976d11517c571d1221a265d25af778ecf8923490c6ceeb450aecdc82e28293031d10c7d73bf85e57bf041a97360aa2c5d99cc1df82d9c4b87413eae2ef048f94b4d3554cea73d92b0f7af96e0271c691e2bb5c67add7c6caf302256adedf7ab114da0acfe870d449a3a489f781d659e8beccda7bce9f4e8618b6bd2f4132ce798cdc7a60e7e1460a7299e3c6342a579626d22733e50f526ec2fa19a22b31e8ed50f23cd1fdf94c9154ed3a7609a2f1ff981fe1d3b5c807b281e4683cc6d6315cf95b9ade8641defcb32372f1c126e398ef7a5a2dce0a8a7f68bb74560f8f71837c2c2ebbcbf7fffb42ae1896f13f7c7479a0b46a28b6f55540f89444f63de0378e3d121be09e06cc9ded1c20e65876d36aa0c65e9645644786b620e2dd2ad648ddfcbf4a7e5b1a3a4ecfe7f64667a3f0b7e2f4418588ed35a2458cffeb39b93d26f18d2ab13bdce6aee58e7b99359ec2dfd95a9c16dc00d6ef18b7933a6f8dc65ccb55667138776f7dea101070dc8796e3774df84f40ae0c8229d0d6069e5c8f39a7c299677a09d367fc7b05e3bc380ee652cdc72595f74c7b1043d0e1ffbab734648c838dfb0527d971b602bc216c9619ef0abf5ac974a1ed57f4050aa510dd9c74f508277b39d7973bb2dfccc5eeb0618d6a3e23902bafb21ac312e717f7942f8fd8ae795f67c918083442c2ab253cc66e0000000000000000000000000000000000000000000000000000"; + (bool success, bytes memory retVal) = address(mips).call(input); + bytes memory expectedRetVal = hex"03fc952a0bd8aabc407669b857af995eab91ce55c404d8b32eaf8b941a48188c"; + + assertTrue(success); + assertEq(retVal.length, 32, "Expect a bytes32 hash of the post-state to be returned"); + assertEq(retVal, expectedRetVal); + } + function test_stepABI_succeeds() public { uint32[32] memory registers; registers[0] = 0xdeadbeef;