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 { ...@@ -220,13 +220,12 @@ contract MIPS {
} }
function proofOffset(uint8 proofIndex) internal returns (uint256 offset) { 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. // 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 // 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; uint256 s = 0;
assembly { s := calldatasize() } 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; return offset;
} }
...@@ -246,11 +245,9 @@ contract MIPS { ...@@ -246,11 +245,9 @@ contract MIPS {
for { let i := 0 } lt(i, 27) { i := add(i, 1) } { for { let i := 0 } lt(i, 27) { i := add(i, 1) } {
let sibling := calldataload(offset) let sibling := calldataload(offset)
offset := add(offset, 32) offset := add(offset, 32)
if and(shr(i, path), 1) { switch and(shr(i, path), 1)
node := hashPair(sibling, node) case 0 { node := hashPair(node, sibling) }
continue case 1 { node := hashPair(sibling, node) }
}
node := hashPair(node, sibling)
} }
let memRoot := mload(0x80) // load memRoot, first field of state let memRoot := mload(0x80) // load memRoot, first field of state
if iszero(eq(node, memRoot)) { // verify the root matches if iszero(eq(node, memRoot)) { // verify the root matches
...@@ -284,11 +281,9 @@ contract MIPS { ...@@ -284,11 +281,9 @@ contract MIPS {
for { let i := 0 } lt(i, 27) { i := add(i, 1) } { for { let i := 0 } lt(i, 27) { i := add(i, 1) } {
let sibling := calldataload(offset) let sibling := calldataload(offset)
offset := add(offset, 32) offset := add(offset, 32)
if and(shr(i, path), 1) { switch and(shr(i, path), 1)
node := hashPair(sibling, node) case 0 { node := hashPair(node, sibling) }
continue case 1 { node := hashPair(sibling, node) }
}
node := hashPair(node, sibling)
} }
mstore(0x80, node) // store new memRoot, first field of state mstore(0x80, node) // store new memRoot, first field of state
} }
...@@ -404,7 +399,7 @@ contract MIPS { ...@@ -404,7 +399,7 @@ contract MIPS {
} }
// ALU // 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 uint32 func = insn & 0x3f; // 6-bits
if (opcode == 0 && func >= 8 && func < 0x1c) { if (opcode == 0 && func >= 8 && func < 0x1c) {
...@@ -438,7 +433,7 @@ contract MIPS { ...@@ -438,7 +433,7 @@ contract MIPS {
// write memory // write memory
if (storeAddr != 0xFF_FF_FF_FF) { if (storeAddr != 0xFF_FF_FF_FF) {
writeMem(storeAddr, 1, mem); writeMem(storeAddr, 1, val);
} }
// write back the value to destination register // write back the value to destination register
......
...@@ -3,7 +3,6 @@ package mipsevm ...@@ -3,7 +3,6 @@ package mipsevm
import ( import (
"bytes" "bytes"
"encoding/binary" "encoding/binary"
"fmt"
"math/big" "math/big"
"os" "os"
"path" "path"
...@@ -14,13 +13,9 @@ import ( ...@@ -14,13 +13,9 @@ import (
"github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
uc "github.com/unicorn-engine/unicorn/bindings/go/unicorn"
) )
func TestEVM(t *testing.T) { func TestEVM(t *testing.T) {
t.Skip("work in progress memory proof")
testFiles, err := os.ReadDir("test/bin") testFiles, err := os.ReadDir("test/bin")
require.NoError(t, err) require.NoError(t, err)
...@@ -63,32 +58,20 @@ func TestEVM(t *testing.T) { ...@@ -63,32 +58,20 @@ func TestEVM(t *testing.T) {
require.NoError(t, mu.MemMap(baseAddrStart, ((baseAddrEnd-baseAddrStart)&^pageAddrMask)+pageSize)) require.NoError(t, mu.MemMap(baseAddrStart, ((baseAddrEnd-baseAddrStart)&^pageAddrMask)+pageSize))
require.NoError(t, mu.MemMap(endAddr&^pageAddrMask, pageSize)) require.NoError(t, mu.MemMap(endAddr&^pageAddrMask, pageSize))
al := &AccessList{mem: state.Memory}
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, al)
us, err := NewUnicornState(mu, state, os.Stdout, os.Stderr)
require.NoError(t, err, "hook unicorn to state") require.NoError(t, err, "hook unicorn to state")
var stateData []byte for i := 0; i < 1000; i++ {
var insn uint32 if us.state.PC == endAddr {
var pc uint32 break
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")
} }
} insn := state.Memory.GetMemory(state.PC)
postCode := func() { t.Logf("step: %4d pc: 0x%08x insn: 0x%08x", state.Step, state.PC, insn)
fmt.Printf("POST - pc: %08x insn: %08x\n", pc, insn)
proofData := append([]byte(nil), al.proofData...) stateData, proofData := us.Step(true)
stateHash := crypto.Keccak256Hash(stateData) stateHash := crypto.Keccak256Hash(stateData)
var input []byte var input []byte
...@@ -112,29 +95,19 @@ func TestEVM(t *testing.T) { ...@@ -112,29 +95,19 @@ func TestEVM(t *testing.T) {
postHash := common.Hash(*(*[32]byte)(ret)) postHash := common.Hash(*(*[32]byte)(ret))
logs := evmState.Logs() logs := evmState.Logs()
require.Equal(t, 1, len(logs), "expecting a log with post-state") require.Equal(t, 1, len(logs), "expecting a log with post-state")
post = logs[0].Data evmPost := logs[0].Data
require.Equal(t, crypto.Keccak256Hash(post), postHash, "logged state must be accurate") require.Equal(t, crypto.Keccak256Hash(evmPost), postHash, "logged state must be accurate")
env.StateDB.RevertToSnapshot(snap) env.StateDB.RevertToSnapshot(snap)
t.Logf("EVM step took %d gas, and returned stateHash %s", startingGas-leftOverGas, postHash) 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 // inspect test result
done, result := state.Memory.GetMemory(baseAddrEnd+4), state.Memory.GetMemory(baseAddrEnd+8) done, result := state.Memory.GetMemory(baseAddrEnd+4), state.Memory.GetMemory(baseAddrEnd+8)
require.Equal(t, done, uint32(1), "must be done") require.Equal(t, done, uint32(1), "must be done")
......
...@@ -56,6 +56,7 @@ func (p *CachedPage) MerkleRoot() [32]byte { ...@@ -56,6 +56,7 @@ func (p *CachedPage) MerkleRoot() [32]byte {
continue continue
} }
p.Cache[j] = crypto.Keccak256Hash(p.Data[i : i+64]) 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 p.Ok[j] = true
} }
......
...@@ -9,8 +9,6 @@ import ( ...@@ -9,8 +9,6 @@ import (
"testing" "testing"
"github.com/stretchr/testify/require" "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 // baseAddrStart - baseAddrEnd is used in tests to write the results to
...@@ -60,19 +58,16 @@ func TestState(t *testing.T) { ...@@ -60,19 +58,16 @@ 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, NoOpTracer{}) us, err := NewUnicornState(mu, state, os.Stdout, os.Stderr)
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") for i := 0; i < 1000; i++ {
_, err = mu.HookAdd(uc.HOOK_CODE, func(mu uc.Unicorn, addr uint64, size uint32) { if us.state.PC == endAddr {
if state.PC == endAddr { break
require.NoError(t, mu.Stop(), "stop test when returned")
} }
}, 0, ^uint64(0)) us.Step(false)
require.NoError(t, err, "hook code") }
require.Equal(t, uint32(endAddr), us.state.PC, "must reach end")
err = RunUnicorn(mu, state.PC, 1000)
require.NoError(t, err, "must run steps without error")
// inspect test result // inspect test result
done, result := state.Memory.GetMemory(baseAddrEnd+4), state.Memory.GetMemory(baseAddrEnd+8) done, result := state.Memory.GetMemory(baseAddrEnd+4), state.Memory.GetMemory(baseAddrEnd+8)
require.Equal(t, done, uint32(1), "must be done") require.Equal(t, done, uint32(1), "must be done")
...@@ -97,11 +92,15 @@ func TestMinimal(t *testing.T) { ...@@ -97,11 +92,15 @@ 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), NoOpTracer{}) us, err := NewUnicornState(mu, state, io.MultiWriter(&stdOutBuf, os.Stdout), io.MultiWriter(&stdErrBuf, os.Stderr))
require.NoError(t, err, "hook unicorn to state") require.NoError(t, err, "hook unicorn to state")
err = RunUnicorn(mu, state.PC, 400_000) for i := 0; i < 400_000; i++ {
require.NoError(t, err, "must run steps without error") if us.state.Exited {
break
}
us.Step(false)
}
require.True(t, state.Exited, "must complete program") require.True(t, state.Exited, "must complete program")
require.Equal(t, uint8(0), state.ExitCode, "exit with 0") 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 ( ...@@ -8,18 +8,55 @@ import (
uc "github.com/unicorn-engine/unicorn/bindings/go/unicorn" uc "github.com/unicorn-engine/unicorn/bindings/go/unicorn"
) )
// TestUnicorn test that unicorn works // TestUnicornDelaySlot test that unicorn works, and determine exactly how delay slots behave
func TestUnicorn(t *testing.T) { func TestUnicornDelaySlot(t *testing.T) {
mu, err := NewUnicorn() mu, err := NewUnicorn()
require.NoError(t, err) require.NoError(t, err)
defer mu.Close() defer mu.Close()
require.NoError(t, mu.MemMap(0, 4096)) 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.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) pc, err := mu.RegRead(uc.MIPS_REG_PC)
require.NoError(t, err) 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