Commit 1771860d authored by protolambda's avatar protolambda

mipsevm,contracts: fix onchain read/write memory proof, improve unicorn...

mipsevm,contracts: fix onchain read/write memory proof, improve unicorn tooling mips steps and update tests
parent c868c051
......@@ -220,13 +220,12 @@ contract MIPS {
}
function proofOffset(uint8 proofIndex) internal returns (uint256 offset) {
require(proofIndex & 3 == 0, "addr must be aligned to 4 bytes");
// A proof of 32 bit memory, with 32-byte leaf values, is (32-5)=27 bytes32 entries.
// And the leaf value itself needs to be encoded as well. And proof.offset == 390
offset = 390 + proofIndex + (28*32);
offset = 390 + (uint256(proofIndex) * (28*32));
uint256 s = 0;
assembly { s := calldatasize() }
require(s > (offset + 28*32), "check that there is enough calldata");
require(s >= (offset + 28*32), "check that there is enough calldata");
return offset;
}
......@@ -246,11 +245,9 @@ contract MIPS {
for { let i := 0 } lt(i, 27) { i := add(i, 1) } {
let sibling := calldataload(offset)
offset := add(offset, 32)
if and(shr(i, path), 1) {
node := hashPair(sibling, node)
continue
}
node := hashPair(node, sibling)
switch and(shr(i, path), 1)
case 0 { node := hashPair(node, sibling) }
case 1 { node := hashPair(sibling, node) }
}
let memRoot := mload(0x80) // load memRoot, first field of state
if iszero(eq(node, memRoot)) { // verify the root matches
......@@ -284,11 +281,9 @@ contract MIPS {
for { let i := 0 } lt(i, 27) { i := add(i, 1) } {
let sibling := calldataload(offset)
offset := add(offset, 32)
if and(shr(i, path), 1) {
node := hashPair(sibling, node)
continue
}
node := hashPair(node, sibling)
switch and(shr(i, path), 1)
case 0 { node := hashPair(node, sibling) }
case 1 { node := hashPair(sibling, node) }
}
mstore(0x80, node) // store new memRoot, first field of state
}
......@@ -404,7 +399,7 @@ contract MIPS {
}
// ALU
uint32 val = execute(insn, rs, rt, mem);
uint32 val = execute(insn, rs, rt, mem) & 0xffFFffFF; // swr outputs more than 4 bytes without the mask
uint32 func = insn & 0x3f; // 6-bits
if (opcode == 0 && func >= 8 && func < 0x1c) {
......@@ -438,7 +433,7 @@ contract MIPS {
// write memory
if (storeAddr != 0xFF_FF_FF_FF) {
writeMem(storeAddr, 1, mem);
writeMem(storeAddr, 1, val);
}
// write back the value to destination register
......
......@@ -3,7 +3,6 @@ package mipsevm
import (
"bytes"
"encoding/binary"
"fmt"
"math/big"
"os"
"path"
......@@ -14,13 +13,9 @@ import (
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/crypto"
"github.com/stretchr/testify/require"
uc "github.com/unicorn-engine/unicorn/bindings/go/unicorn"
)
func TestEVM(t *testing.T) {
t.Skip("work in progress memory proof")
testFiles, err := os.ReadDir("test/bin")
require.NoError(t, err)
......@@ -63,32 +58,20 @@ func TestEVM(t *testing.T) {
require.NoError(t, mu.MemMap(baseAddrStart, ((baseAddrEnd-baseAddrStart)&^pageAddrMask)+pageSize))
require.NoError(t, mu.MemMap(endAddr&^pageAddrMask, pageSize))
al := &AccessList{mem: state.Memory}
err = LoadUnicorn(state, mu)
require.NoError(t, err, "load state into unicorn")
err = HookUnicorn(state, mu, os.Stdout, os.Stderr, al)
us, err := NewUnicornState(mu, state, os.Stdout, os.Stderr)
require.NoError(t, err, "hook unicorn to state")
var stateData []byte
var insn uint32
var pc uint32
var post []byte
preCode := func() {
insn = state.Memory.GetMemory(state.PC)
pc = state.PC
fmt.Printf("PRE - pc: %08x insn: %08x\n", pc, insn)
// remember the pre-state, to repeat it in the EVM during the post processing step
stateData = state.EncodeWitness()
if post != nil {
require.Equal(t, hexutil.Bytes(stateData).String(), hexutil.Bytes(post).String(),
"unicorn produced different state than EVM")
for i := 0; i < 1000; i++ {
if us.state.PC == endAddr {
break
}
}
postCode := func() {
fmt.Printf("POST - pc: %08x insn: %08x\n", pc, insn)
insn := state.Memory.GetMemory(state.PC)
t.Logf("step: %4d pc: 0x%08x insn: 0x%08x", state.Step, state.PC, insn)
proofData := append([]byte(nil), al.proofData...)
stateData, proofData := us.Step(true)
stateHash := crypto.Keccak256Hash(stateData)
var input []byte
......@@ -112,29 +95,19 @@ func TestEVM(t *testing.T) {
postHash := common.Hash(*(*[32]byte)(ret))
logs := evmState.Logs()
require.Equal(t, 1, len(logs), "expecting a log with post-state")
post = logs[0].Data
require.Equal(t, crypto.Keccak256Hash(post), postHash, "logged state must be accurate")
evmPost := logs[0].Data
require.Equal(t, crypto.Keccak256Hash(evmPost), postHash, "logged state must be accurate")
env.StateDB.RevertToSnapshot(snap)
t.Logf("EVM step took %d gas, and returned stateHash %s", startingGas-leftOverGas, postHash)
}
firstStep := true
_, 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")
}
if !firstStep {
postCode()
}
preCode()
firstStep = false
}, 0, ^uint64(0))
require.NoError(t, err, "hook code")
err = RunUnicorn(mu, state.PC, 1000)
require.NoError(t, err, "must run steps without error")
// verify the post-state matches.
// TODO: maybe more readable to decode the evmPost state, and do attribute-wise comparison.
uniPost := us.state.EncodeWitness()
require.Equal(t, hexutil.Bytes(uniPost).String(), hexutil.Bytes(evmPost).String(),
"unicorn produced different state than EVM")
}
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")
......
......@@ -56,6 +56,7 @@ func (p *CachedPage) MerkleRoot() [32]byte {
continue
}
p.Cache[j] = crypto.Keccak256Hash(p.Data[i : i+64])
//fmt.Printf("0x%x 0x%x -> 0x%x\n", p.Data[i:i+32], p.Data[i+32:i+64], p.Cache[j])
p.Ok[j] = true
}
......
......@@ -9,8 +9,6 @@ import (
"testing"
"github.com/stretchr/testify/require"
uc "github.com/unicorn-engine/unicorn/bindings/go/unicorn"
)
// baseAddrStart - baseAddrEnd is used in tests to write the results to
......@@ -60,19 +58,16 @@ func TestState(t *testing.T) {
err = LoadUnicorn(state, mu)
require.NoError(t, err, "load state into unicorn")
err = HookUnicorn(state, mu, os.Stdout, os.Stderr, NoOpTracer{})
us, err := NewUnicornState(mu, state, os.Stdout, os.Stderr)
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")
for i := 0; i < 1000; i++ {
if us.state.PC == endAddr {
break
}
}, 0, ^uint64(0))
require.NoError(t, err, "hook code")
err = RunUnicorn(mu, state.PC, 1000)
require.NoError(t, err, "must run steps without error")
us.Step(false)
}
require.Equal(t, uint32(endAddr), us.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")
......@@ -97,11 +92,15 @@ func TestMinimal(t *testing.T) {
err = LoadUnicorn(state, mu)
require.NoError(t, err, "load state into unicorn")
var stdOutBuf, stdErrBuf bytes.Buffer
err = HookUnicorn(state, mu, io.MultiWriter(&stdOutBuf, os.Stdout), io.MultiWriter(&stdErrBuf, os.Stderr), NoOpTracer{})
us, err := NewUnicornState(mu, state, io.MultiWriter(&stdOutBuf, os.Stdout), io.MultiWriter(&stdErrBuf, os.Stderr))
require.NoError(t, err, "hook unicorn to state")
err = RunUnicorn(mu, state.PC, 400_000)
require.NoError(t, err, "must run steps without error")
for i := 0; i < 400_000; i++ {
if us.state.Exited {
break
}
us.Step(false)
}
require.True(t, state.Exited, "must complete program")
require.Equal(t, uint8(0), state.ExitCode, "exit with 0")
......
package mipsevm
import "fmt"
type MemEntry struct {
EffAddr uint32
PreValue uint32
}
type AccessList struct {
mem *Memory
memAccessAddr uint32
proofData []byte
}
func (al *AccessList) Reset() {
al.memAccessAddr = ^uint32(0)
al.proofData = nil
}
func (al *AccessList) OnRead(effAddr uint32) {
if al.memAccessAddr == effAddr {
return
}
if al.memAccessAddr != ^uint32(0) {
panic(fmt.Errorf("bad read of %08x, already have %08x", effAddr, al.memAccessAddr))
}
al.memAccessAddr = effAddr
proof := al.mem.MerkleProof(effAddr)
al.proofData = append(al.proofData, proof[:]...)
}
func (al *AccessList) OnWrite(effAddr uint32) {
if al.memAccessAddr == effAddr {
return
}
if al.memAccessAddr != ^uint32(0) {
panic(fmt.Errorf("bad write of %08x, already have %08x", effAddr, al.memAccessAddr))
}
proof := al.mem.MerkleProof(effAddr)
al.proofData = append(al.proofData, proof[:]...)
}
func (al *AccessList) PreInstruction(pc uint32) {
proof := al.mem.MerkleProof(pc)
al.proofData = append(al.proofData, proof[:]...)
}
var _ Tracer = (*AccessList)(nil)
type Tracer interface {
// OnRead remembers reads from the given effAddr.
// Warning: the addr is an effective-addr, i.e. always aligned.
// But unicorn may fire it multiple times, for each byte that was changed within the effective addr boundaries.
OnRead(effAddr uint32)
// OnWrite remembers writes to the given effAddr.
// Warning: the addr is an effective-addr, i.e. always aligned.
// But unicorn may fire it multiple times, for each byte that was changed within the effective addr boundaries.
OnWrite(effAddr uint32)
PreInstruction(pc uint32)
}
type NoOpTracer struct{}
func (n NoOpTracer) OnRead(effAddr uint32) {}
func (n NoOpTracer) OnWrite(effAddr uint32) {}
func (n NoOpTracer) PreInstruction(pc uint32) {}
var _ Tracer = NoOpTracer{}
This diff is collapsed.
......@@ -8,18 +8,55 @@ import (
uc "github.com/unicorn-engine/unicorn/bindings/go/unicorn"
)
// TestUnicorn test that unicorn works
func TestUnicorn(t *testing.T) {
// TestUnicornDelaySlot test that unicorn works, and determine exactly how delay slots behave
func TestUnicornDelaySlot(t *testing.T) {
mu, err := NewUnicorn()
require.NoError(t, err)
defer mu.Close()
require.NoError(t, mu.MemMap(0, 4096))
require.NoError(t, mu.RegWrite(uc.MIPS_REG_RA, 420), "set RA to addr that is multiple of 4")
require.NoError(t, mu.MemWrite(0, []byte{0x03, 0xe0, 0x00, 0x08}), "jmp $ra")
require.NoError(t, mu.MemWrite(0, []byte{0x03, 0xe0, 0x00, 0x08}), "jr $ra")
require.NoError(t, mu.MemWrite(4, []byte{0x20, 0x09, 0x0a, 0xFF}), "addi $t1 $r0 0x0aff")
require.NoError(t, mu.MemWrite(32, []byte{0x20, 0x09, 0x0b, 0xFF}), "addi $t1 $r0 0x0bff")
mu.HookAdd(uc.HOOK_CODE, func(mu uc.Unicorn, addr uint64, size uint32) {
t.Logf("addr: %08x", addr)
}, uint64(0), ^uint64(0))
// stop at instruction in addr=4, the delay slot
require.NoError(t, mu.StartWithOptions(uint64(0), uint64(4), &uc.UcOptions{
Timeout: 0, // 0 to disable, value is in ms.
Count: 2,
}))
t1, err := mu.RegRead(uc.MIPS_REG_T1)
require.NoError(t, err)
require.NotEqual(t, uint64(0x0aff), t1, "delay slot should not execute")
require.NoError(t, RunUnicorn(mu, 0, 1))
pc, err := mu.RegRead(uc.MIPS_REG_PC)
require.NoError(t, err)
require.Equal(t, uint64(420), pc, "jumped")
// unicorn is weird here: when entering a delay slot, it does not update the PC register by itself.
require.Equal(t, uint64(0), pc, "delay slot, no jump yet")
// now restart, but run two instructions, to include the delay slot
require.NoError(t, mu.StartWithOptions(uint64(0), ^uint64(0), &uc.UcOptions{
Timeout: 0, // 0 to disable, value is in ms.
Count: 2,
}))
pc, err = mu.RegRead(uc.MIPS_REG_PC)
require.NoError(t, err)
require.Equal(t, uint64(420), pc, "jumped after NOP delay slot")
t1, err = mu.RegRead(uc.MIPS_REG_T1)
require.NoError(t, err)
require.Equal(t, uint64(0x0aff), t1, "delay slot should execute")
require.NoError(t, mu.StartWithOptions(uint64(32), uint64(32+4), &uc.UcOptions{
Timeout: 0, // 0 to disable, value is in ms.
Count: 1,
}))
t1, err = mu.RegRead(uc.MIPS_REG_T1)
require.NoError(t, err)
require.Equal(t, uint64(0x0bff), t1, "regular instruction should work fine")
}
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