Commit 58c36147 authored by protolambda's avatar protolambda

mipsevm: work in progress evm tracing/testing

parent 61066ab3
...@@ -20,6 +20,8 @@ import ( ...@@ -20,6 +20,8 @@ import (
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
) )
var StepBytes4 = crypto.Keccak256Hash([]byte("Step(bytes32,bytes,bytes)")).Bytes()[:4]
func LoadContracts() (*Contracts, error) { func LoadContracts() (*Contracts, error) {
mips, err := LoadContract("MIPS") mips, err := LoadContract("MIPS")
if err != nil { if err != nil {
...@@ -42,7 +44,7 @@ func LoadContracts() (*Contracts, error) { ...@@ -42,7 +44,7 @@ func LoadContracts() (*Contracts, error) {
func LoadContract(name string) (*Contract, error) { func LoadContract(name string) (*Contract, error) {
// TODO change to forge build output // TODO change to forge build output
dat, err := os.ReadFile(fmt.Sprintf("../artifacts/contracts/%s.sol/%s.json", name, name)) dat, err := os.ReadFile(fmt.Sprintf("../contracts/out/%s.sol/%s.json", name, name))
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to read contract JSON definition of %q: %w", name, err) return nil, fmt.Errorf("failed to read contract JSON definition of %q: %w", name, err)
} }
......
package main
import (
"bytes"
"encoding/binary"
"math/big"
"os"
"path"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/eth/tracers/logger"
"github.com/stretchr/testify/require"
uc "github.com/unicorn-engine/unicorn/bindings/go/unicorn"
)
func TestEVM(t *testing.T) {
t.Skip("work in progress!")
testFiles, err := os.ReadDir("test/bin")
require.NoError(t, err)
contracts, err := LoadContracts()
require.NoError(t, err)
addrs := &Addresses{
MIPS: common.Address{0: 0xff, 19: 1},
MIPSMemory: common.Address{0: 0xff, 19: 2},
Challenge: common.Address{0: 0xff, 19: 3},
}
sender := common.Address{0x13, 0x37}
for _, f := range testFiles {
t.Run(f.Name(), func(t *testing.T) {
if f.Name() == "oracle.bin" {
t.Skip("oracle test needs to be updated to use syscall pre-image oracle")
}
env := NewEVMEnv(contracts, addrs)
env.Config.Debug = true
env.Config.Tracer = logger.NewMarkdownLogger(&logger.Config{}, os.Stdout)
fn := path.Join("test/bin", f.Name())
programMem, err := os.ReadFile(fn)
state := &State{PC: 0, Memory: make(map[uint32]*Page)}
err = state.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] = endAddr
mu, err := NewUnicorn()
require.NoError(t, err, "load unicorn")
defer mu.Close()
require.NoError(t, mu.MemMap(baseAddrStart, ((baseAddrEnd-baseAddrStart)&^pageAddrMask)+pageSize))
require.NoError(t, mu.MemMap(endAddr&^pageAddrMask, pageSize))
al := &AccessList{}
err = LoadUnicorn(state, mu)
require.NoError(t, err, "load state into unicorn")
err = HookUnicorn(state, mu, os.Stdout, os.Stderr, al)
require.NoError(t, err, "hook unicorn to state")
// Add hook to stop unicorn once we reached the end of the test (i.e. "ate food")
_, err = mu.HookAdd(uc.HOOK_CODE, func(mu uc.Unicorn, addr uint64, size uint32) {
if state.PC == endAddr {
require.NoError(t, mu.Stop(), "stop test when returned")
}
}, 0, ^uint64(0))
require.NoError(t, err, "")
so := NewStateCache()
for i := 0; i < 1000; i++ {
insn := state.GetMemory(state.PC)
al.Reset() // reset
require.NoError(t, RunUnicorn(mu, state.PC, 1))
require.LessOrEqual(t, len(al.memReads)+len(al.memWrites), 1, "expecting at most a single mem read or write")
proofData := make([]byte, 0, 32*2)
proofData = append(proofData, uint32ToBytes32(32)...) // length in bytes
var tmp [32]byte
binary.BigEndian.PutUint32(tmp[0:4], insn) // instruction
if len(al.memReads) > 0 {
binary.BigEndian.PutUint32(tmp[4:8], state.GetMemory(al.memReads[0]))
}
if len(al.memWrites) > 0 {
binary.BigEndian.PutUint32(tmp[4:8], state.GetMemory(al.memWrites[0]))
}
proofData = append(proofData, tmp[:]...)
memRoot := state.MerkleizeMemory(so)
stateData := make([]byte, 0, 44*32)
stateData = append(stateData, memRoot[:]...)
stateData = append(stateData, make([]byte, 32)...) // TODO preimageKey
stateData = append(stateData, make([]byte, 32)...) // TODO preimageOffset
for i := 0; i < 32; i++ {
stateData = append(stateData, uint32ToBytes32(state.Registers[i])...)
}
stateData = append(stateData, uint32ToBytes32(state.PC)...)
stateData = append(stateData, uint32ToBytes32(state.NextPC)...)
stateData = append(stateData, uint32ToBytes32(state.LR)...)
stateData = append(stateData, uint32ToBytes32(state.LO)...)
stateData = append(stateData, uint32ToBytes32(state.HI)...)
stateData = append(stateData, uint32ToBytes32(state.Heap)...)
stateData = append(stateData, uint8ToBytes32(state.ExitCode)...)
stateData = append(stateData, boolToBytes32(state.Exited)...)
stateData = append(stateData, uint64ToBytes32(state.Step)...)
stateHash := crypto.Keccak256Hash(stateData)
var input []byte
input = append(input, StepBytes4...)
input = append(input, stateHash[:]...)
input = append(input, uint32ToBytes32(32*3)...) // state data offset in bytes
input = append(input, uint32ToBytes32(32*3+32+uint32(len(stateData)))...) // proof data offset in bytes
input = append(input, uint32ToBytes32(uint32(len(stateData)))...) // state data length in bytes
input = append(input, stateData[:]...)
input = append(input, uint32ToBytes32(uint32(len(proofData)))...) // proof data length in bytes
input = append(input, proofData[:]...)
startingGas := uint64(30_000_000)
ret, leftOverGas, err := env.Call(vm.AccountRef(sender), addrs.MIPS, input, startingGas, big.NewInt(0))
require.NoError(t, err, "evm should not fail")
t.Logf("step took %d gas", startingGas-leftOverGas)
t.Logf("output (state hash): %x", ret)
// TODO compare output against unicorn (need to reconstruct state and memory hash)
}
require.NoError(t, err, "must run steps without error")
// inspect test result
done, result := state.GetMemory(baseAddrEnd+4), state.GetMemory(baseAddrEnd+8)
require.Equal(t, done, uint32(1), "must be done")
require.Equal(t, result, uint32(1), "must have success result")
})
}
}
func uint64ToBytes32(v uint64) []byte {
var out [32]byte
binary.BigEndian.PutUint64(out[32-8:], v)
return out[:]
}
func uint32ToBytes32(v uint32) []byte {
var out [32]byte
binary.BigEndian.PutUint32(out[32-4:], v)
return out[:]
}
func uint8ToBytes32(v uint8) []byte {
var out [32]byte
out[31] = v
return out[:]
}
func boolToBytes32(v bool) []byte {
var out [32]byte
if v {
out[31] = 1
}
return out[:]
}
...@@ -11,12 +11,12 @@ import ( ...@@ -11,12 +11,12 @@ import (
func LoadELF(f *elf.File) (*State, error) { func LoadELF(f *elf.File) (*State, error) {
s := &State{ s := &State{
PC: uint32(f.Entry), PC: uint32(f.Entry),
Hi: 0, HI: 0,
Lo: 0, LO: 0,
Heap: 0x20000000, Heap: 0x20000000,
Registers: [32]uint32{}, Registers: [32]uint32{},
Memory: make(map[uint32]*Page), Memory: make(map[uint32]*Page),
Exit: 0, ExitCode: 0,
Exited: false, Exited: false,
Step: 0, Step: 0,
} }
......
...@@ -34,23 +34,25 @@ func (p *Page) UnmarshalText(dat []byte) error { ...@@ -34,23 +34,25 @@ func (p *Page) UnmarshalText(dat []byte) error {
} }
type State struct { type State struct {
PC uint32 `json:"pc"` Memory map[uint32]*Page `json:"memory"`
Hi uint32 `json:"hi"`
Lo uint32 `json:"lo"`
Heap uint32 `json:"heap"` // to handle mmap growth
Registers [32]uint32 `json:"registers"` Registers [32]uint32 `json:"registers"`
Memory map[uint32]*Page `json:"memory"` PC uint32 `json:"pc"`
NextPC uint32 `json:"nextPC"`
LR uint32 `json:"lr"`
HI uint32 `json:"hi"`
LO uint32 `json:"lo"`
Heap uint32 `json:"heap"` // to handle mmap growth
Exit uint8 `json:"exit"` ExitCode uint8 `json:"exit"`
Exited bool `json:"exited"` Exited bool `json:"exited"`
Step uint64 `json:"step"` Step uint64 `json:"step"`
} }
// TODO: VM state pre-image: // TODO: VM state pre-image:
// PC, Hi, Lo, Heap = 4 * 32/8 = 16 bytes // PC, HI, LO, Heap = 4 * 32/8 = 16 bytes
// Registers = 32 * 32/8 = 256 bytes // Registers = 32 * 32/8 = 256 bytes
// Memory tree root = 32 bytes // Memory tree root = 32 bytes
// Misc exit/step data = TBD // Misc exit/step data = TBD
...@@ -119,7 +121,6 @@ func (s *State) MerkleizeMemory(so StateOracle) [32]byte { ...@@ -119,7 +121,6 @@ func (s *State) MerkleizeMemory(so StateOracle) [32]byte {
} }
func (s *State) SetMemory(addr uint32, size uint32, v uint32) { func (s *State) SetMemory(addr uint32, size uint32, v uint32) {
// TODO: maybe only support 4-byte aligned memory stores?
for i := size; i > 0; i-- { for i := size; i > 0; i-- {
pageIndex := addr >> pageAddrSize pageIndex := addr >> pageAddrSize
pageAddr := addr & pageAddrMask pageAddr := addr & pageAddrMask
......
...@@ -9,6 +9,7 @@ import ( ...@@ -9,6 +9,7 @@ import (
"testing" "testing"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
uc "github.com/unicorn-engine/unicorn/bindings/go/unicorn" uc "github.com/unicorn-engine/unicorn/bindings/go/unicorn"
) )
...@@ -58,7 +59,8 @@ func TestState(t *testing.T) { ...@@ -58,7 +59,8 @@ func TestState(t *testing.T) {
err = LoadUnicorn(state, mu) err = LoadUnicorn(state, mu)
require.NoError(t, err, "load state into unicorn") require.NoError(t, err, "load state into unicorn")
err = HookUnicorn(state, mu, os.Stdout, os.Stderr)
err = HookUnicorn(state, mu, os.Stdout, os.Stderr, NoOpTracer{})
require.NoError(t, err, "hook unicorn to state") require.NoError(t, err, "hook unicorn to state")
// Add hook to stop unicorn once we reached the end of the test (i.e. "ate food") // Add hook to stop unicorn once we reached the end of the test (i.e. "ate food")
...@@ -95,14 +97,14 @@ func TestMinimal(t *testing.T) { ...@@ -95,14 +97,14 @@ func TestMinimal(t *testing.T) {
err = LoadUnicorn(state, mu) err = LoadUnicorn(state, mu)
require.NoError(t, err, "load state into unicorn") require.NoError(t, err, "load state into unicorn")
var stdOutBuf, stdErrBuf bytes.Buffer var stdOutBuf, stdErrBuf bytes.Buffer
err = HookUnicorn(state, mu, io.MultiWriter(&stdOutBuf, os.Stdout), io.MultiWriter(&stdErrBuf, os.Stderr)) err = HookUnicorn(state, mu, io.MultiWriter(&stdOutBuf, os.Stdout), io.MultiWriter(&stdErrBuf, os.Stderr), NoOpTracer{})
require.NoError(t, err, "hook unicorn to state") require.NoError(t, err, "hook unicorn to state")
err = RunUnicorn(mu, state.PC, 400_000) err = RunUnicorn(mu, state.PC, 400_000)
require.NoError(t, err, "must run steps without error") require.NoError(t, err, "must run steps without error")
require.True(t, state.Exited, "must complete program") require.True(t, state.Exited, "must complete program")
require.Equal(t, uint8(0), state.Exit, "exit with 0") require.Equal(t, uint8(0), state.ExitCode, "exit with 0")
require.Equal(t, "hello world!", stdOutBuf.String(), "stdout says hello") require.Equal(t, "hello world!", stdOutBuf.String(), "stdout says hello")
require.Equal(t, "", stdErrBuf.String(), "stderr silent") require.Equal(t, "", stdErrBuf.String(), "stderr silent")
......
package main
type AccessList struct {
memReads []uint32
memWrites []uint32
}
func (al *AccessList) Reset() {
al.memReads = al.memReads[:0]
al.memWrites = al.memWrites[:0]
}
func (al *AccessList) OnRead(addr uint32) {
// if it matches the last, it's a duplicate; this happens because of multiple callbacks for the same effective addr.
if len(al.memReads) > 0 && al.memReads[len(al.memReads)-1] == addr {
return
}
al.memReads = append(al.memReads, addr)
}
func (al *AccessList) OnWrite(addr uint32) {
// if it matches the last, it's a duplicate; this happens because of multiple callbacks for the same effective addr.
if len(al.memWrites) > 0 && al.memWrites[len(al.memWrites)-1] == addr {
return
}
al.memWrites = append(al.memWrites, addr)
}
var _ Tracer = (*AccessList)(nil)
type Tracer interface {
// OnRead remembers reads from the given addr.
// Warning: the addr is an effective-addr, i.e. always aligned.
// But unicorn will fire it multiple times, for each byte that was changed within the effective addr boundaries.
OnRead(addr uint32)
// OnWrite remembers writes to the given addr.
// Warning: the addr is an effective-addr, i.e. always aligned.
// But unicorn will fire it multiple times, for each byte that was changed within the effective addr boundaries.
OnWrite(addr uint32)
}
type NoOpTracer struct{}
func (n NoOpTracer) OnRead(addr uint32) {}
func (n NoOpTracer) OnWrite(addr uint32) {}
var _ Tracer = NoOpTracer{}
...@@ -31,15 +31,15 @@ func LoadUnicorn(st *State, mu uc.Unicorn) error { ...@@ -31,15 +31,15 @@ func LoadUnicorn(st *State, mu uc.Unicorn) error {
regValues[i] = uint64(v) regValues[i] = uint64(v)
} }
regValues[32] = uint64(st.PC) regValues[32] = uint64(st.PC)
regValues[33] = uint64(st.Lo) regValues[33] = uint64(st.LO)
regValues[34] = uint64(st.Hi) regValues[34] = uint64(st.HI)
if err := mu.RegWriteBatch(regBatchKeys(), regValues); err != nil { if err := mu.RegWriteBatch(regBatchKeys(), regValues); err != nil {
return fmt.Errorf("failed to write registers: %w", err) return fmt.Errorf("failed to write registers: %w", err)
} }
return nil return nil
} }
func HookUnicorn(st *State, mu uc.Unicorn, stdOut, stdErr io.Writer) error { func HookUnicorn(st *State, mu uc.Unicorn, stdOut, stdErr io.Writer, tr Tracer) error {
_, err := mu.HookAdd(uc.HOOK_INTR, func(mu uc.Unicorn, intno uint32) { _, err := mu.HookAdd(uc.HOOK_INTR, func(mu uc.Unicorn, intno uint32) {
if intno != 17 { if intno != 17 {
log.Fatal("invalid interrupt ", intno, " at step ", st.Step) log.Fatal("invalid interrupt ", intno, " at step ", st.Step)
...@@ -88,7 +88,7 @@ func HookUnicorn(st *State, mu uc.Unicorn, stdOut, stdErr io.Writer) error { ...@@ -88,7 +88,7 @@ func HookUnicorn(st *State, mu uc.Unicorn, stdOut, stdErr io.Writer) error {
v0 = 0x40000000 v0 = 0x40000000
case 4246: // exit_group case 4246: // exit_group
st.Exited = true st.Exited = true
st.Exit = uint8(v0) st.ExitCode = uint8(v0)
mu.Stop() mu.Stop()
return return
} }
...@@ -109,11 +109,8 @@ func HookUnicorn(st *State, mu uc.Unicorn, stdOut, stdErr io.Writer) error { ...@@ -109,11 +109,8 @@ func HookUnicorn(st *State, mu uc.Unicorn, stdOut, stdErr io.Writer) error {
} }
_, err = mu.HookAdd(uc.HOOK_MEM_READ, func(mu uc.Unicorn, access int, addr64 uint64, size int, value int64) { _, err = mu.HookAdd(uc.HOOK_MEM_READ, func(mu uc.Unicorn, access int, addr64 uint64, size int, value int64) {
//rt := value addr := uint32(addr64 & 0xFFFFFFFC) // pass effective addr to tracer
//rs := addr64 & 3 tr.OnRead(addr)
//addr := uint32(addr64 & 0xFFFFFFFC)
// TODO sanity check matches the state value
// TODO access-list entry
}, 0, ^uint64(0)) }, 0, ^uint64(0))
if err != nil { if err != nil {
return fmt.Errorf("failed to set up mem-write hook: %w", err) return fmt.Errorf("failed to set up mem-write hook: %w", err)
...@@ -127,6 +124,8 @@ func HookUnicorn(st *State, mu uc.Unicorn, stdOut, stdErr io.Writer) error { ...@@ -127,6 +124,8 @@ func HookUnicorn(st *State, mu uc.Unicorn, stdOut, stdErr io.Writer) error {
panic("invalid mem size") panic("invalid mem size")
} }
st.SetMemory(uint32(addr64), uint32(size), uint32(value)) st.SetMemory(uint32(addr64), uint32(size), uint32(value))
addr := uint32(addr64 & 0xFFFFFFFC) // pass effective addr to tracer
tr.OnWrite(addr)
}, 0, ^uint64(0)) }, 0, ^uint64(0))
if err != nil { if err != nil {
return fmt.Errorf("failed to set up mem-write hook: %w", err) return fmt.Errorf("failed to set up mem-write hook: %w", err)
...@@ -143,8 +142,8 @@ func HookUnicorn(st *State, mu uc.Unicorn, stdOut, stdErr io.Writer) error { ...@@ -143,8 +142,8 @@ func HookUnicorn(st *State, mu uc.Unicorn, stdOut, stdErr io.Writer) error {
st.Registers[i] = uint32(batch[i]) st.Registers[i] = uint32(batch[i])
} }
st.PC = uint32(batch[32]) st.PC = uint32(batch[32])
st.Lo = uint32(batch[33]) st.LO = uint32(batch[33])
st.Hi = uint32(batch[34]) st.HI = uint32(batch[34])
}, 0, ^uint64(0)) }, 0, ^uint64(0))
if err != nil { if err != nil {
return fmt.Errorf("failed to set up instruction hook: %w", err) return fmt.Errorf("failed to set up instruction hook: %w", err)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment