Skip to content

Commit

Permalink
integrate eels -- ethereum-spec-evm into fuzzing vms (#131)
Browse files Browse the repository at this point in the history
This pull request adds support for [EELS](https://github.com/ethereum/execution-specs)' `ethereum-spec-evm`.

Requires an _extremely_ new checkout (at least ethereum/execution-specs@c4f5b8a).
  • Loading branch information
SamWilsn authored Mar 21, 2024
1 parent 3caa9dc commit f31084b
Show file tree
Hide file tree
Showing 41 changed files with 2,034 additions and 10 deletions.
18 changes: 18 additions & 0 deletions common/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ var (
Name: "gethbatch",
Usage: "Location of go-ethereum 'evm' binary",
}
EelsFlag = &cli.StringSliceFlag{
Name: "eels",
Usage: "Location of 'ethereum-spec-evm' binary",
}
EelsBatchFlag = &cli.StringSliceFlag{
Name: "eelsbatch",
Usage: "Location of 'ethereum-spec-evm' binary",
}
NethermindFlag = &cli.StringSliceFlag{
Name: "nethermind",
Usage: "Location of nethermind 'nethtest' binary",
Expand Down Expand Up @@ -132,6 +140,8 @@ var (
VmFlags = []cli.Flag{
GethFlag,
GethBatchFlag,
EelsFlag,
EelsBatchFlag,
NethermindFlag,
NethBatchFlag,
BesuFlag,
Expand All @@ -149,6 +159,8 @@ func initVMs(c *cli.Context) []evms.Evm {
var (
gethBins = c.StringSlice(GethFlag.Name)
gethBatchBins = c.StringSlice(GethBatchFlag.Name)
eelsBins = c.StringSlice(EelsFlag.Name)
eelsBatchBins = c.StringSlice(EelsBatchFlag.Name)
nethBins = c.StringSlice(NethermindFlag.Name)
nethBatchBins = c.StringSlice(NethBatchFlag.Name)
besuBins = c.StringSlice(BesuFlag.Name)
Expand All @@ -167,6 +179,12 @@ func initVMs(c *cli.Context) []evms.Evm {
for i, bin := range gethBatchBins {
vms = append(vms, evms.NewGethBatchVM(bin, fmt.Sprintf("gethbatch-%d", i)))
}
for i, bin := range eelsBins {
vms = append(vms, evms.NewEelsEVM(bin, fmt.Sprintf("eels-%d", i)))
}
for i, bin := range eelsBatchBins {
vms = append(vms, evms.NewEelsBatchVM(bin, fmt.Sprintf("eelsbatch-%d", i)))
}
for i, bin := range nethBins {
vms = append(vms, evms.NewNethermindVM(bin, fmt.Sprintf("nethermind-%d", i)))
}
Expand Down
6 changes: 6 additions & 0 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,12 @@ RUN apt-get install -qy --no-install-recommends libssl-dev
# besu requires openjdk-17-jre
RUN apt-get install -qy --no-install-recommends openjdk-17-jre

# Install execution-specs (EELS)
RUN apt-get install -qy --no-install-recommends pipx git && \
git clone https://github.com/ethereum/execution-specs.git --branch statetests --depth 1 && \
PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/ pipx install './execution-specs/[test]'
ENV EELS_BIN=/ethereum-spec-evm

# Go-evmlab targets
COPY --from=golang-builder /go/goevmlab/generic-fuzzer /
COPY --from=golang-builder /go/goevmlab/checkslow /
Expand Down
74 changes: 74 additions & 0 deletions docker/Dockerfile.lab.geth.eels
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#
# The Mega Dockerfile
#
# This dockerfile is an attempt to bundle the following components into
# one big dockerfile:
#
# - [x] Goevmlab binaries
# - [x] Go-ethereum binary 'evm'
# - [ ] Erigon binary 'evm'
# - [ ] EvmOne vm binary 'evmone'
# - [ ] Reth VM binary 'revme'
# - [ ] Besu
# - [ ] Nethermind
# - [ ] Nimbus-eth1
# - [x] EELS
#

#---------------------------------------------------------------
# golang-builder (debian-based)
#---------------------------------------------------------------
FROM golang:latest as golang-builder

#
# Go-evmlab
#

RUN git clone https://github.com/SamWilsn/goevmlab --depth 1 --branch eels-evm
RUN cd goevmlab && \
go build ./cmd/generic-fuzzer && \
go build ./cmd/checkslow && \
go build ./cmd/minimizer && \
go build ./cmd/repro && \
go build ./cmd/runtest && \
go build ./cmd/tracediff && \
go build ./cmd/traceview

#
# GETH
#

RUN git clone https://github.com/ethereum/go-ethereum --depth 1
RUN cd go-ethereum && go run build/ci.go install -static ./cmd/evm

#
# Main non-builder
#

FROM debian:testing

RUN apt-get update -q

# Install execution-specs (EELS)
RUN apt-get install -qy --no-install-recommends pipx git && \
git clone https://github.com/ethereum/execution-specs.git --branch statetests --depth 1
RUN PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/ pipx install './execution-specs/[test]'
ENV EELS_BIN=/ethereum-spec-evm

# Go-evmlab targets
COPY --from=golang-builder /go/goevmlab/generic-fuzzer /
COPY --from=golang-builder /go/goevmlab/checkslow /
COPY --from=golang-builder /go/goevmlab/minimizer /
COPY --from=golang-builder /go/goevmlab/repro /
COPY --from=golang-builder /go/goevmlab/runtest /
COPY --from=golang-builder /go/goevmlab/tracediff /
COPY --from=golang-builder /go/goevmlab/traceview /
COPY --from=golang-builder /go/goevmlab/evms/testdata/ /testdata/

COPY --from=golang-builder /go/go-ethereum/build/bin/evm /gethvm
ENV GETH_BIN=/gethvm


COPY readme_docker.md /README.md
COPY entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/bin/bash"]
23 changes: 21 additions & 2 deletions docker/readme_docker.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,48 @@
This is a dockerfile containing all VMs, plus go-evmlab itself.
The evm binaries are available as ENV vars:

- `$GETH_BIN`=/gethvm
- `$ERIG_BIN`=/erigon_vm
- `$NIMB_BIN`=/nimbvm
- `$EVMO_BIN`=/evmone
- `$RETH_BIN`=/revme
- `$NETH_BIN`=/neth/nethtest
- `$BESU_BIN`=/evmtool/bin/evm
- `$EELS_BIN`=/ethereum-spec-evm


## Fuzzing

If you want to do fuzzing, you should ensure that the directory where tests are
saved is mounted outside the docker container

```
docker run -it -v /home/user/fuzzing:/fuzztmp --entrypoint /generic-fuzzer --outdir=/fuzztmp --nethbatch=/nethtest --nimbus=/nimbvm --revme=/revme --erigonbatch=/erigon_vm --besubatch=/besu-vm --evmone=/evmone --fork=Cancun
docker run -it -v /home/user/fuzzing:/fuzztmp
$ /generic-fuzzer --outdir=/fuzztmp \
--gethbatch=$GETH_BIN \
--nethbatch=$NETH_BIN \
--nimbus=$NIMB_BIN \
--revme=$RETH_BIN \
--erigonbatch=$ERIG_BIN \
--besubatch=$BESU_BIN \
--evmone=$EVMO_BIN \
--eelsbatch=$EELS_BIN \
--fork=Cancun
```

## Generating reference output

Mount the reference tests, and execute the `run.sh` to create them:
```
docker run -it -v /home/user/workspace/goevmlab/evms/testdata/:/testdata \
--entrypoint /bin/bash
$ bash run.sh
```
## Checkslow

```
docker run -it -v /home/user/workspace/goevmlab/trophies/2024-02-20_slow_tests/fuzztmp:/fuzztmp --entrypoint bash holiman/omnifuzz
$ /checkslow --nethbatch=$NETH_BIN --evmone=$EVMO_BIN --verbosity -4 /fuzztmp/
Expand Down
187 changes: 187 additions & 0 deletions evms/eels.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
// Copyright 2019 Martin Holst Swende
// Copyright 2024 Sam Wilson
// This file is part of the goevmlab library.
//
// The library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the goevmlab library. If not, see <http://www.gnu.org/licenses/>.

package evms

import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io"
"os"
"os/exec"
"time"

"github.com/ethereum/go-ethereum/eth/tracers/logger"
"github.com/ethereum/go-ethereum/log"
)

// EelsEVM is s Evm-interface wrapper around the `evm` binary, based on go-ethereum.
type EelsEVM struct {
path string
name string // in case multiple instances are used

// Some metrics
stats *VmStat
}

func NewEelsEVM(path string, name string) *EelsEVM {
return &EelsEVM{
path: path,
name: name,
stats: &VmStat{},
}
}

func (evm *EelsEVM) Instance(int) Evm {
return evm
}

func (evm *EelsEVM) Name() string {
return evm.name
}

// GetStateRoot runs the test and returns the stateroot
// This currently only works for non-filled statetests. TODO: make it work even if the
// test is filled. Either by getting the whole trace, or adding stateroot to exec std output
// even in success-case
func (evm *EelsEVM) GetStateRoot(path string) (root, command string, err error) {
// In this mode, we can run it without tracing
cmd := exec.Command(evm.path, "statetest", path)
data, err := cmd.Output()
if err != nil {
return "", cmd.String(), err
}
root, err = evm.ParseStateRoot(data)
if err != nil {
log.Error("Failed to find stateroot", "vm", evm.Name(), "cmd", cmd.String())
return "", cmd.String(), err
}
return root, cmd.String(), err
}

// ParseStateRoot reads geth's stateroot from the combined output.
func (evm *EelsEVM) ParseStateRoot(data []byte) (string, error) {
start := bytes.Index(data, []byte(`"stateRoot": "`))
end := start + 14 + 66
if start == -1 || end >= len(data) {
return "", fmt.Errorf("%v: no stateroot found", evm.Name())
}
return string(data[start+14 : end]), nil
}

// RunStateTest implements the Evm interface
func (evm *EelsEVM) RunStateTest(path string, out io.Writer, speedTest bool) (*tracingResult, error) {
var (
t0 = time.Now()
stderr io.ReadCloser
err error
cmd *exec.Cmd
)
if speedTest {
cmd = exec.Command(evm.path, "statetest", "--nomemory", "--noreturndata", "--nostack", path)
} else {
cmd = exec.Command(evm.path, "statetest", "--json", "--noreturndata", "--nomemory", path)
}
if stderr, err = cmd.StderrPipe(); err != nil {
return &tracingResult{Cmd: cmd.String()}, err
}
if err = cmd.Start(); err != nil {
return &tracingResult{Cmd: cmd.String()}, err
}
// copy everything to the given writer
evm.Copy(out, stderr)
err = cmd.Wait()
// release resources
duration, slow := evm.stats.TraceDone(t0)

return &tracingResult{
Slow: slow,
ExecTime: duration,
Cmd: cmd.String(),
}, err
}

func (vm *EelsEVM) Close() {
}

// Copy reads from the reader, does some geth-specific filtering and
// outputs items onto the channel
func (evm *EelsEVM) Copy(out io.Writer, input io.Reader) {
evm.copyUntilEnd(out, input)
}

// copyUntilEnd reads from the reader, does some vm-specific filtering and
// outputs items onto the channel
func (evm *EelsEVM) copyUntilEnd(out io.Writer, input io.Reader) stateRoot {
buf := bufferPool.Get().([]byte)
//lint:ignore SA6002: argument should be pointer-like to avoid allocations.
defer bufferPool.Put(buf)
var stateRoot stateRoot
scanner := bufio.NewScanner(input)
scanner.Buffer(buf, 32*1024*1024)
for scanner.Scan() {
data := scanner.Bytes()
if len(data) > 0 && data[0] == '#' {
// Output preceded by # is ignored, but can be used for debugging, e.g.
// to check that the generated tests cover the intended surface.
fmt.Printf("%v: %v\n", evm.Name(), string(data))
continue
}
var elem logger.StructLog
if err := json.Unmarshal(data, &elem); err != nil {
fmt.Printf("eels err: %v, line\n\t%v\n", err, string(data))
continue
}
// If the output cannot be marshalled, all fields will be blanks.
// We can detect that through 'depth', which should never be less than 1
// for any actual opcode
if elem.Depth == 0 {
/* It might be the stateroot
{"output":"","gasUsed":"0x2d1cc4","error":"gas uint64 overflow"}
{"stateRoot": "0xa2b3391f7a85bf1ad08dc541a1b99da3c591c156351391f26ec88c557ff12134"}
*/
if stateRoot.StateRoot == "" {
_ = json.Unmarshal(data, &stateRoot)
}
// If we have a stateroot, we're done
if len(stateRoot.StateRoot) > 0 {
break
}
continue
}
// When geth encounters end of code, it continues anyway, on a 'virtual' STOP.
// In order to handle that, we need to drop all STOP opcodes.
if elem.Op == 0x0 {
continue
}
outp := FastMarshal(&elem)
if _, err := out.Write(append(outp, '\n')); err != nil {
fmt.Fprintf(os.Stderr, "Error writing to out: %v\n", err)
}
}
root, _ := json.Marshal(stateRoot)
if _, err := out.Write(append(root, '\n')); err != nil {
fmt.Fprintf(os.Stderr, "Error writing to out: %v\n", err)
}
return stateRoot
}

func (evm *EelsEVM) Stats() []any {
return evm.stats.Stats()
}
Loading

0 comments on commit f31084b

Please sign in to comment.