evm_test.go 12.7 KB
Newer Older
1
package mipsevm
2 3 4

import (
	"bytes"
5
	"debug/elf"
6 7
	"errors"
	"fmt"
8
	"io"
9 10 11
	"math/big"
	"os"
	"path"
12
	"strings"
13
	"testing"
14
	"time"
15

16 17
	"github.com/ethereum-optimism/optimism/op-bindings/bindings"
	preimage "github.com/ethereum-optimism/optimism/op-preimage"
18
	"github.com/ethereum/go-ethereum/common"
19
	"github.com/ethereum/go-ethereum/common/hexutil"
inphi's avatar
inphi committed
20
	"github.com/ethereum/go-ethereum/core/state"
21
	"github.com/ethereum/go-ethereum/core/vm"
22
	"github.com/ethereum/go-ethereum/eth/tracers/logger"
23 24 25
	"github.com/stretchr/testify/require"
)

inphi's avatar
inphi committed
26
func testContractsSetup(t require.TestingT) (*Contracts, *Addresses) {
27 28 29 30
	contracts, err := LoadContracts()
	require.NoError(t, err)

	addrs := &Addresses{
31 32 33 34
		MIPS:         common.Address{0: 0xff, 19: 1},
		Oracle:       common.Address{0: 0xff, 19: 2},
		Sender:       common.Address{0x13, 0x37},
		FeeRecipient: common.Address{0xaa},
35
	}
36

37 38 39 40 41
	return contracts, addrs
}

func MarkdownTracer() vm.EVMLogger {
	return logger.NewMarkdownLogger(&logger.Config{}, os.Stdout)
42 43
}

inphi's avatar
inphi committed
44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
type MIPSEVM struct {
	env      *vm.EVM
	evmState *state.StateDB
	addrs    *Addresses
}

func NewMIPSEVM(contracts *Contracts, addrs *Addresses) *MIPSEVM {
	env, evmState := NewEVMEnv(contracts, addrs)
	return &MIPSEVM{env, evmState, addrs}
}

func (m *MIPSEVM) SetTracer(tracer vm.EVMLogger) {
	m.env.Config.Tracer = tracer
}

// Step is a pure function that computes the poststate from the VM state encoded in the StepWitness.
func (m *MIPSEVM) Step(t *testing.T, stepWitness *StepWitness) []byte {
	sender := common.Address{0x13, 0x37}
	startingGas := uint64(30_000_000)

	// we take a snapshot so we can clean up the state, and isolate the logs of this instruction run.
	snap := m.env.StateDB.Snapshot()

	if stepWitness.HasPreimage() {
		t.Logf("reading preimage key %x at offset %d", stepWitness.PreimageKey, stepWitness.PreimageOffset)
Adrian Sutton's avatar
Adrian Sutton committed
69
		poInput, err := encodePreimageOracleInput(t, stepWitness, LocalContext{})
inphi's avatar
inphi committed
70
		require.NoError(t, err, "encode preimage oracle input")
clabby's avatar
clabby committed
71
		_, leftOverGas, err := m.env.Call(vm.AccountRef(sender), m.addrs.Oracle, poInput, startingGas, big.NewInt(0))
inphi's avatar
inphi committed
72 73 74
		require.NoErrorf(t, err, "evm should not fail, took %d gas", startingGas-leftOverGas)
	}

Adrian Sutton's avatar
Adrian Sutton committed
75
	input := encodeStepInput(t, stepWitness, LocalContext{})
inphi's avatar
inphi committed
76 77 78 79 80 81 82 83
	ret, leftOverGas, err := m.env.Call(vm.AccountRef(sender), m.addrs.MIPS, input, startingGas, big.NewInt(0))
	require.NoError(t, err, "evm should not fail")
	require.Len(t, ret, 32, "expecting 32-byte state hash")
	// remember state hash, to check it against state
	postHash := common.Hash(*(*[32]byte)(ret))
	logs := m.evmState.Logs()
	require.Equal(t, 1, len(logs), "expecting a log with post-state")
	evmPost := logs[0].Data
84

85 86
	stateHash, err := StateWitness(evmPost).StateHash()
	require.NoError(t, err, "state hash could not be computed")
87
	require.Equal(t, stateHash, postHash, "logged state must be accurate")
inphi's avatar
inphi committed
88 89 90 91 92 93

	m.env.StateDB.RevertToSnapshot(snap)
	t.Logf("EVM step took %d gas, and returned stateHash %s", startingGas-leftOverGas, postHash)
	return evmPost
}

94 95 96 97
func encodeStepInput(t *testing.T, wit *StepWitness, localContext LocalContext) []byte {
	mipsAbi, err := bindings.MIPSMetaData.GetAbi()
	require.NoError(t, err)

Adrian Sutton's avatar
Adrian Sutton committed
98
	input, err := mipsAbi.Pack("step", wit.State, wit.MemProof, localContext)
99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
	require.NoError(t, err)
	return input
}

func encodePreimageOracleInput(t *testing.T, wit *StepWitness, localContext LocalContext) ([]byte, error) {
	if wit.PreimageKey == ([32]byte{}) {
		return nil, errors.New("cannot encode pre-image oracle input, witness has no pre-image to proof")
	}

	preimageAbi, err := bindings.PreimageOracleMetaData.GetAbi()
	require.NoError(t, err, "failed to load pre-image oracle ABI")

	switch preimage.KeyType(wit.PreimageKey[0]) {
	case preimage.LocalKeyType:
		if len(wit.PreimageValue) > 32+8 {
			return nil, fmt.Errorf("local pre-image exceeds maximum size of 32 bytes with key 0x%x", wit.PreimageKey)
		}
		preimagePart := wit.PreimageValue[8:]
		var tmp [32]byte
		copy(tmp[:], preimagePart)
		input, err := preimageAbi.Pack("loadLocalData",
			new(big.Int).SetBytes(wit.PreimageKey[1:]),
Adrian Sutton's avatar
Adrian Sutton committed
121
			localContext,
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
			tmp,
			new(big.Int).SetUint64(uint64(len(preimagePart))),
			new(big.Int).SetUint64(uint64(wit.PreimageOffset)),
		)
		require.NoError(t, err)
		return input, nil
	case preimage.Keccak256KeyType:
		input, err := preimageAbi.Pack(
			"loadKeccak256PreimagePart",
			new(big.Int).SetUint64(uint64(wit.PreimageOffset)),
			wit.PreimageValue[8:])
		require.NoError(t, err)
		return input, nil
	default:
		return nil, fmt.Errorf("unsupported pre-image type %d, cannot prepare preimage with key %x offset %d for oracle",
			wit.PreimageKey[0], wit.PreimageKey, wit.PreimageOffset)
	}
}

141
func TestEVM(t *testing.T) {
142
	testFiles, err := os.ReadDir("open_mips_tests/test/bin")
143 144
	require.NoError(t, err)

145
	contracts, addrs := testContractsSetup(t)
146
	var tracer vm.EVMLogger // no-tracer by default, but MarkdownTracer
147 148 149

	for _, f := range testFiles {
		t.Run(f.Name(), func(t *testing.T) {
150
			var oracle PreimageOracle
151
			if strings.HasPrefix(f.Name(), "oracle") {
152
				oracle = staticOracle(t, []byte("hello world"))
153
			}
154 155
			// Short-circuit early for exit_group.bin
			exitGroup := f.Name() == "exit_group.bin"
156

inphi's avatar
inphi committed
157 158
			evm := NewMIPSEVM(contracts, addrs)
			evm.SetTracer(tracer)
159

160
			fn := path.Join("open_mips_tests/test/bin", f.Name())
161
			programMem, err := os.ReadFile(fn)
162
			require.NoError(t, err)
163 164
			state := &State{PC: 0, NextPC: 4, Memory: NewMemory()}
			err = state.Memory.SetMemoryRange(0, bytes.NewReader(programMem))
165 166 167 168 169
			require.NoError(t, err, "load program into state")

			// set the return address ($ra) to jump into when test completes
			state.Registers[31] = endAddr

170
			goState := NewInstrumentedState(state, oracle, os.Stdout, os.Stderr)
171

172
			for i := 0; i < 1000; i++ {
173
				if goState.state.PC == endAddr {
174
					break
175
				}
176
				if exitGroup && goState.state.Exited {
177 178
					break
				}
179 180
				insn := state.Memory.GetMemory(state.PC)
				t.Logf("step: %4d pc: 0x%08x insn: 0x%08x", state.Step, state.PC, insn)
181

182
				stepWitness, err := goState.Step(true)
183
				require.NoError(t, err)
inphi's avatar
inphi committed
184
				evmPost := evm.Step(t, stepWitness)
185 186
				// verify the post-state matches.
				// TODO: maybe more readable to decode the evmPost state, and do attribute-wise comparison.
187 188
				goPost := goState.state.EncodeWitness()
				require.Equal(t, hexutil.Bytes(goPost).String(), hexutil.Bytes(evmPost).String(),
189
					"mipsevm produced different state than EVM")
190
			}
191
			if exitGroup {
192 193 194
				require.NotEqual(t, uint32(endAddr), goState.state.PC, "must not reach end")
				require.True(t, goState.state.Exited, "must set exited state")
				require.Equal(t, uint8(1), goState.state.ExitCode, "must exit with 1")
195 196 197 198 199 200 201
			} else {
				require.Equal(t, uint32(endAddr), state.PC, "must reach end")
				// inspect test result
				done, result := state.Memory.GetMemory(baseAddrEnd+4), state.Memory.GetMemory(baseAddrEnd+8)
				require.Equal(t, done, uint32(1), "must be done")
				require.Equal(t, result, uint32(1), "must have success result")
			}
202 203 204 205
		})
	}
}

206 207 208 209
func TestEVMSingleStep(t *testing.T) {
	contracts, addrs := testContractsSetup(t)
	var tracer vm.EVMLogger

210
	cases := []struct {
211 212 213 214
		name   string
		pc     uint32
		nextPC uint32
		insn   uint32
215
	}{
216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240
		{"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 := &State{PC: tt.pc, NextPC: tt.nextPC, Memory: NewMemory()}
			state.Memory.SetMemory(tt.pc, tt.insn)

			us := NewInstrumentedState(state, nil, os.Stdout, os.Stderr)
			stepWitness, err := us.Step(true)
			require.NoError(t, err)

			evm := NewMIPSEVM(contracts, addrs)
			evm.SetTracer(tracer)
			evmPost := evm.Step(t, stepWitness)
			goPost := us.state.EncodeWitness()
			require.Equal(t, hexutil.Bytes(goPost).String(), hexutil.Bytes(evmPost).String(),
				"mipsevm produced different state than EVM")
		})
	}
}

241 242
func TestEVMFault(t *testing.T) {
	contracts, addrs := testContractsSetup(t)
243
	var tracer vm.EVMLogger // no-tracer by default, but see MarkdownTracer
244 245 246 247 248
	sender := common.Address{0x13, 0x37}

	env, evmState := NewEVMEnv(contracts, addrs)
	env.Config.Tracer = tracer

249
	cases := []struct {
250 251 252
		name   string
		nextPC uint32
		insn   uint32
253
	}{
254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275
		{"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 := &State{PC: 0, NextPC: tt.nextPC, Memory: NewMemory()}
			initialState := &State{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] = endAddr

			us := NewInstrumentedState(state, nil, os.Stdout, os.Stderr)
			require.Panics(t, func() { _, _ = us.Step(true) })

			insnProof := initialState.Memory.MerkleProof(0)
			stepWitness := &StepWitness{
				State:    initialState.EncodeWitness(),
				MemProof: insnProof[:],
			}
Adrian Sutton's avatar
Adrian Sutton committed
276
			input := encodeStepInput(t, stepWitness, LocalContext{})
277
			startingGas := uint64(30_000_000)
278

279 280 281 282 283 284
			_, _, err := env.Call(vm.AccountRef(sender), addrs.MIPS, input, startingGas, big.NewInt(0))
			require.EqualValues(t, err, vm.ErrExecutionReverted)
			logs := evmState.Logs()
			require.Equal(t, 0, len(logs))
		})
	}
285 286
}

287
func TestHelloEVM(t *testing.T) {
288
	contracts, addrs := testContractsSetup(t)
289
	var tracer vm.EVMLogger // no-tracer by default, but see MarkdownTracer
290

291
	elfProgram, err := elf.Open("../example/bin/hello.elf")
292 293 294 295 296
	require.NoError(t, err, "open ELF file")

	state, err := LoadELF(elfProgram)
	require.NoError(t, err, "load ELF into state")

protolambda's avatar
protolambda committed
297
	err = PatchGo(elfProgram, state)
298
	require.NoError(t, err, "apply Go runtime patches")
protolambda's avatar
protolambda committed
299
	require.NoError(t, PatchStack(state), "add initial stack")
300 301

	var stdOutBuf, stdErrBuf bytes.Buffer
302
	goState := NewInstrumentedState(state, nil, io.MultiWriter(&stdOutBuf, os.Stdout), io.MultiWriter(&stdErrBuf, os.Stderr))
303

304
	start := time.Now()
305
	for i := 0; i < 400_000; i++ {
306
		if goState.state.Exited {
307 308 309 310 311 312 313
			break
		}
		insn := state.Memory.GetMemory(state.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.PC, insn)
		}

inphi's avatar
inphi committed
314 315 316
		evm := NewMIPSEVM(contracts, addrs)
		evm.SetTracer(tracer)

317
		stepWitness, err := goState.Step(true)
318
		require.NoError(t, err)
inphi's avatar
inphi committed
319
		evmPost := evm.Step(t, stepWitness)
320 321
		// verify the post-state matches.
		// TODO: maybe more readable to decode the evmPost state, and do attribute-wise comparison.
322 323
		goPost := goState.state.EncodeWitness()
		require.Equal(t, hexutil.Bytes(goPost).String(), hexutil.Bytes(evmPost).String(),
324
			"mipsevm produced different state than EVM")
325
	}
326 327 328
	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))
329 330 331 332

	require.True(t, state.Exited, "must complete program")
	require.Equal(t, uint8(0), state.ExitCode, "exit with 0")

333
	require.Equal(t, "hello world!\n", stdOutBuf.String(), "stdout says hello")
334 335
	require.Equal(t, "", stdErrBuf.String(), "stderr silent")
}
336 337

func TestClaimEVM(t *testing.T) {
338
	contracts, addrs := testContractsSetup(t)
339
	var tracer vm.EVMLogger // no-tracer by default, but see MarkdownTracer
340 341 342 343 344 345 346

	elfProgram, err := elf.Open("../example/bin/claim.elf")
	require.NoError(t, err, "open ELF file")

	state, err := LoadELF(elfProgram)
	require.NoError(t, err, "load ELF into state")

protolambda's avatar
protolambda committed
347
	err = PatchGo(elfProgram, state)
348
	require.NoError(t, err, "apply Go runtime patches")
protolambda's avatar
protolambda committed
349
	require.NoError(t, PatchStack(state), "add initial stack")
350 351 352 353

	oracle, expectedStdOut, expectedStdErr := claimTestOracle(t)

	var stdOutBuf, stdErrBuf bytes.Buffer
354
	goState := NewInstrumentedState(state, oracle, io.MultiWriter(&stdOutBuf, os.Stdout), io.MultiWriter(&stdErrBuf, os.Stderr))
355 356

	for i := 0; i < 2000_000; i++ {
357
		if goState.state.Exited {
358 359 360 361 362 363 364 365
			break
		}

		insn := state.Memory.GetMemory(state.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.PC, insn)
		}

366
		stepWitness, err := goState.Step(true)
367
		require.NoError(t, err)
368

inphi's avatar
inphi committed
369 370 371 372
		evm := NewMIPSEVM(contracts, addrs)
		evm.SetTracer(tracer)
		evmPost := evm.Step(t, stepWitness)

373 374
		goPost := goState.state.EncodeWitness()
		require.Equal(t, hexutil.Bytes(goPost).String(), hexutil.Bytes(evmPost).String(),
inphi's avatar
inphi committed
375
			"mipsevm produced different state than EVM")
376 377 378 379 380 381 382 383
	}

	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")
}