Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
N
nebula
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
exchain
nebula
Commits
58c36147
Unverified
Commit
58c36147
authored
Apr 21, 2023
by
protolambda
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
mipsevm: work in progress evm tracing/testing
parent
61066ab3
Changes
7
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
247 additions
and
27 deletions
+247
-27
evm.go
mipsevm/evm.go
+3
-1
evm_test.go
mipsevm/evm_test.go
+168
-0
patch.go
mipsevm/patch.go
+3
-3
state.go
mipsevm/state.go
+10
-9
state_test.go
mipsevm/state_test.go
+5
-3
tracer.go
mipsevm/tracer.go
+48
-0
unicorn.go
mipsevm/unicorn.go
+10
-11
No files found.
mipsevm/evm.go
View file @
58c36147
...
...
@@ -20,6 +20,8 @@ import (
"github.com/ethereum/go-ethereum/params"
)
var
StepBytes4
=
crypto
.
Keccak256Hash
([]
byte
(
"Step(bytes32,bytes,bytes)"
))
.
Bytes
()[
:
4
]
func
LoadContracts
()
(
*
Contracts
,
error
)
{
mips
,
err
:=
LoadContract
(
"MIPS"
)
if
err
!=
nil
{
...
...
@@ -42,7 +44,7 @@ func LoadContracts() (*Contracts, error) {
func
LoadContract
(
name
string
)
(
*
Contract
,
error
)
{
// 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
{
return
nil
,
fmt
.
Errorf
(
"failed to read contract JSON definition of %q: %w"
,
name
,
err
)
}
...
...
mipsevm/evm_test.go
0 → 100644
View file @
58c36147
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
[
:
]
}
mipsevm/patch.go
View file @
58c36147
...
...
@@ -11,12 +11,12 @@ import (
func
LoadELF
(
f
*
elf
.
File
)
(
*
State
,
error
)
{
s
:=
&
State
{
PC
:
uint32
(
f
.
Entry
),
H
i
:
0
,
L
o
:
0
,
H
I
:
0
,
L
O
:
0
,
Heap
:
0x20000000
,
Registers
:
[
32
]
uint32
{},
Memory
:
make
(
map
[
uint32
]
*
Page
),
Exit
:
0
,
Exit
Code
:
0
,
Exited
:
false
,
Step
:
0
,
}
...
...
mipsevm/state.go
View file @
58c36147
...
...
@@ -34,23 +34,25 @@ func (p *Page) UnmarshalText(dat []byte) error {
}
type
State
struct
{
PC
uint32
`json:"pc"`
Hi
uint32
`json:"hi"`
Lo
uint32
`json:"lo"`
Heap
uint32
`json:"heap"`
// to handle mmap growth
Memory
map
[
uint32
]
*
Page
`json:"memory"`
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"`
Exited
bool
`json:"exited"`
Exit
Code
uint8
`json:"exit"`
Exited
bool
`json:"exited"`
Step
uint64
`json:"step"`
}
// TODO: VM state pre-image:
// PC, H
i, Lo
, Heap = 4 * 32/8 = 16 bytes
// PC, H
I, LO
, Heap = 4 * 32/8 = 16 bytes
// Registers = 32 * 32/8 = 256 bytes
// Memory tree root = 32 bytes
// Misc exit/step data = TBD
...
...
@@ -119,7 +121,6 @@ func (s *State) MerkleizeMemory(so StateOracle) [32]byte {
}
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
--
{
pageIndex
:=
addr
>>
pageAddrSize
pageAddr
:=
addr
&
pageAddrMask
...
...
mipsevm/state_test.go
View file @
58c36147
...
...
@@ -9,6 +9,7 @@ import (
"testing"
"github.com/stretchr/testify/require"
uc
"github.com/unicorn-engine/unicorn/bindings/go/unicorn"
)
...
...
@@ -58,7 +59,8 @@ 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
)
err
=
HookUnicorn
(
state
,
mu
,
os
.
Stdout
,
os
.
Stderr
,
NoOpTracer
{})
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")
...
...
@@ -95,14 +97,14 @@ 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
))
err
=
HookUnicorn
(
state
,
mu
,
io
.
MultiWriter
(
&
stdOutBuf
,
os
.
Stdout
),
io
.
MultiWriter
(
&
stdErrBuf
,
os
.
Stderr
)
,
NoOpTracer
{}
)
require
.
NoError
(
t
,
err
,
"hook unicorn to state"
)
err
=
RunUnicorn
(
mu
,
state
.
PC
,
400
_000
)
require
.
NoError
(
t
,
err
,
"must run steps without error"
)
require
.
True
(
t
,
state
.
Exited
,
"must complete program"
)
require
.
Equal
(
t
,
uint8
(
0
),
state
.
Exit
,
"exit with 0"
)
require
.
Equal
(
t
,
uint8
(
0
),
state
.
Exit
Code
,
"exit with 0"
)
require
.
Equal
(
t
,
"hello world!"
,
stdOutBuf
.
String
(),
"stdout says hello"
)
require
.
Equal
(
t
,
""
,
stdErrBuf
.
String
(),
"stderr silent"
)
...
...
mipsevm/tracer.go
0 → 100644
View file @
58c36147
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
{}
mipsevm/unicorn.go
View file @
58c36147
...
...
@@ -31,15 +31,15 @@ func LoadUnicorn(st *State, mu uc.Unicorn) error {
regValues
[
i
]
=
uint64
(
v
)
}
regValues
[
32
]
=
uint64
(
st
.
PC
)
regValues
[
33
]
=
uint64
(
st
.
L
o
)
regValues
[
34
]
=
uint64
(
st
.
H
i
)
regValues
[
33
]
=
uint64
(
st
.
L
O
)
regValues
[
34
]
=
uint64
(
st
.
H
I
)
if
err
:=
mu
.
RegWriteBatch
(
regBatchKeys
(),
regValues
);
err
!=
nil
{
return
fmt
.
Errorf
(
"failed to write registers: %w"
,
err
)
}
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
)
{
if
intno
!=
17
{
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 {
v0
=
0x40000000
case
4246
:
// exit_group
st
.
Exited
=
true
st
.
Exit
=
uint8
(
v0
)
st
.
Exit
Code
=
uint8
(
v0
)
mu
.
Stop
()
return
}
...
...
@@ -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
)
{
//rt := value
//rs := addr64 & 3
//addr := uint32(addr64 & 0xFFFFFFFC)
// TODO sanity check matches the state value
// TODO access-list entry
addr
:=
uint32
(
addr64
&
0xFFFFFFFC
)
// pass effective addr to tracer
tr
.
OnRead
(
addr
)
},
0
,
^
uint64
(
0
))
if
err
!=
nil
{
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 {
panic
(
"invalid mem size"
)
}
st
.
SetMemory
(
uint32
(
addr64
),
uint32
(
size
),
uint32
(
value
))
addr
:=
uint32
(
addr64
&
0xFFFFFFFC
)
// pass effective addr to tracer
tr
.
OnWrite
(
addr
)
},
0
,
^
uint64
(
0
))
if
err
!=
nil
{
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 {
st
.
Registers
[
i
]
=
uint32
(
batch
[
i
])
}
st
.
PC
=
uint32
(
batch
[
32
])
st
.
L
o
=
uint32
(
batch
[
33
])
st
.
H
i
=
uint32
(
batch
[
34
])
st
.
L
O
=
uint32
(
batch
[
33
])
st
.
H
I
=
uint32
(
batch
[
34
])
},
0
,
^
uint64
(
0
))
if
err
!=
nil
{
return
fmt
.
Errorf
(
"failed to set up instruction hook: %w"
,
err
)
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment