Skip to content

Commit

Permalink
feat!: use full EVM execution stack and simplify interception of byte…
Browse files Browse the repository at this point in the history
…code, config, `StateDB`, etc. (#35)

* feat!: use `core.ApplyMessage()` and full `state.StateDB` for `Run()` (messy!)

* doc: comment all `runopts.Captured` functionality (and move it to a separate file)

* refactor: default `Code.Run()` to error on revert

"Make it hard to misuse an API". The majority of tests were using `runopts.ErrorOnRevert()` and one even forgot it, so the option is inverted to `NoErrorOnRevert()`.

* doc: comment all `runopts.Option`s

* chore: nolint errcheck for reading from Keccak state

* doc: default `Contract.Address` and guarantees/reqs about `vm.StateDB`

* doc: README updates

* fix: add contract address to `StateDB` access list

* test: `ExampleCaptured` also demonstrates testing `SSTORE`
  • Loading branch information
ARR4N authored Aug 17, 2024
1 parent e405591 commit a211290
Show file tree
Hide file tree
Showing 18 changed files with 709 additions and 66 deletions.
5 changes: 5 additions & 0 deletions BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,15 @@ go_library(
visibility = ["//visibility:public"],
deps = [
"//evmdebug",
"//revert",
"//runopts",
"//stack",
"//types",
"@com_github_ethereum_go_ethereum//common",
"@com_github_ethereum_go_ethereum//core",
"@com_github_ethereum_go_ethereum//core/rawdb",
"@com_github_ethereum_go_ethereum//core/state",
"@com_github_ethereum_go_ethereum//core/tracing",
"@com_github_ethereum_go_ethereum//core/vm",
"@com_github_ethereum_go_ethereum//crypto",
"@com_github_ethereum_go_ethereum//params",
Expand Down
58 changes: 56 additions & 2 deletions MODULE.bazel.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

**`specops` is a low-level, domain-specific language and compiler for crafting [Ethereum VM](https://ethereum.org/en/developers/docs/evm) bytecode. The project also includes a CLI with code execution and terminal-based debugger.**

This is a _very_ early release, a weekend project gone rogue. Feedback and contributions appreciated.

## _special_ opcodes

Writing bytecode is hard. Tracking stack items is difficult enough, made worse by refactoring that renders every `DUP` and `SWAP` off-by-X.
Expand Down Expand Up @@ -47,9 +45,10 @@ New features will be prioritised based on demand. If there's something you'd lik
- [x] Caching of search for optimal route
- [ ] Standalone compiler
- [x] In-process EVM execution (geth)
- [x] Full control of configuration (e.g. `params.ChainConfig` and `vm.Config`)
- [x] State preloading (e.g. other contracts to call) and inspection (e.g. `SSTORE` testing)
- [x] Message overrides (caller and value)
- [x] Debugger
- [x] Single call frame (via `vm.EVMInterpreter`)
- [ ] Multiple call frames; i.e. support `*CALL` methods
* [x] Stepping
* [ ] Breakpoints
* [x] Programmatic inspection (e.g. native Go tests at opcode resolution)
Expand Down
1 change: 1 addition & 0 deletions evmdebug/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ go_library(
visibility = ["//visibility:public"],
deps = [
"//internal/sync",
"@com_github_ethereum_go_ethereum//core",
"@com_github_ethereum_go_ethereum//core/tracing",
"@com_github_ethereum_go_ethereum//core/vm",
"@com_github_gdamore_tcell_v2//:tcell",
Expand Down
29 changes: 18 additions & 11 deletions evmdebug/ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,18 @@ package evmdebug
import (
"fmt"

"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
)

// Context describes the debugging context.
type Context struct {
Bytecode, CallData []byte
Results func() (*core.ExecutionResult, error)
}

// RunTerminalUI starts a UI that controls the Debugger and displays opcodes,
// memory, stack etc. Because of the current Debugger limitation of a single
// call frame, only that exact Contract can be displayed. The callData is
Expand All @@ -16,15 +23,15 @@ import (
// As the Debugger only has access via a vm.EVMLogger, it can't retrieve the
// final result. The `results` argument MUST return the returned buffer / error
// after d.Done() returns true.
func (d *Debugger) RunTerminalUI(callData []byte, results func() ([]byte, error), contract *vm.Contract) error {
func (d *Debugger) RunTerminalUI(dbgCtx *Context) error {
t := &termDBG{
Debugger: d,
results: results,
dbgCtx: dbgCtx,
}
t.initComponents()
t.initApp()
t.populateCallData(callData)
t.populateCode(contract)
t.populateCallData()
t.populateCode()
return t.app.Run()
}

Expand All @@ -38,7 +45,7 @@ type termDBG struct {
code *tview.List
pcToCodeItem map[uint64]int

results func() ([]byte, error)
dbgCtx *Context
}

func (*termDBG) styleBox(b *tview.Box, title string) *tview.Box {
Expand Down Expand Up @@ -102,15 +109,15 @@ func (t *termDBG) createLayout() tview.Primitive {
return root
}

func (t *termDBG) populateCallData(cd []byte) {
t.callData.SetText(fmt.Sprintf("%x", cd))
func (t *termDBG) populateCallData() {
t.callData.SetText(fmt.Sprintf("%x", t.dbgCtx.CallData))
}

func (t *termDBG) populateCode(c *vm.Contract) {
func (t *termDBG) populateCode() {
t.pcToCodeItem = make(map[uint64]int)

var skip int
for i, o := range c.Code {
for i, o := range t.dbgCtx.Bytecode {
if skip > 0 {
skip--
continue
Expand All @@ -123,7 +130,7 @@ func (t *termDBG) populateCode(c *vm.Contract) {

case op.IsPush():
skip += int(op - vm.PUSH0)
text = fmt.Sprintf("%s %#x", op.String(), c.Code[i+1:i+1+skip])
text = fmt.Sprintf("%s %#x", op.String(), t.dbgCtx.Bytecode[i+1:i+1+skip])

default:
text = op.String()
Expand All @@ -149,7 +156,7 @@ func (t *termDBG) onStep() {
}

func (t *termDBG) resultToDisplay() string {
out, err := t.results()
out, err := t.dbgCtx.Results()
if err != nil {
return fmt.Sprintf("ERROR: %v", err)
}
Expand Down
2 changes: 1 addition & 1 deletion examples_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -677,5 +677,5 @@ func compileAndRun[T interface{ []byte | [32]byte }](code Code, callData T) []by
if err != nil {
log.Fatal(err)
}
return got
return got.ReturnData
}
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ require (

require (
github.com/DataDog/zstd v1.4.5 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/StackExchange/wmi v1.2.1 // indirect
github.com/VictoriaMetrics/fastcache v1.12.2 // indirect
github.com/beorn7/perks v1.0.1 // indirect
Expand All @@ -30,6 +31,7 @@ require (
github.com/consensys/gnark-crypto v0.12.1 // indirect
github.com/crate-crypto/go-ipa v0.0.0-20240223125850-b1e8a79f509c // indirect
github.com/crate-crypto/go-kzg-4844 v1.0.0 // indirect
github.com/deckarep/golang-set/v2 v2.6.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
github.com/ethereum/c-kzg-4844 v1.0.0 // indirect
github.com/ethereum/go-verkle v0.1.1-0.20240306133620-7d920df305f0 // indirect
Expand All @@ -40,6 +42,7 @@ require (
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect
github.com/gorilla/websocket v1.4.2 // indirect
github.com/holiman/bloomfilter/v2 v2.0.3 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/klauspost/compress v1.16.0 // indirect
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA=
github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8=
github.com/VictoriaMetrics/fastcache v1.12.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI=
Expand Down Expand Up @@ -94,6 +96,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM=
github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0=
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc=
Expand Down Expand Up @@ -197,6 +201,8 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao=
Expand Down
9 changes: 9 additions & 0 deletions revert/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
load("@rules_go//go:def.bzl", "go_library")

go_library(
name = "revert",
srcs = ["revert.go"],
importpath = "github.com/solidifylabs/specops/revert",
visibility = ["//visibility:public"],
deps = ["@com_github_ethereum_go_ethereum//core"],
)
51 changes: 51 additions & 0 deletions revert/revert.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Package revert provides errors and error handling for EVM smart contracts
// that revert.
package revert

import (
"errors"

"github.com/ethereum/go-ethereum/core"
)

// An Error is an error signalling that code reverted.
type Error struct {
Data []byte // [core.ExecutionResult.Revert()]
Err error // [core.ExecutionResult.Err]
}

// Data returns the revert data from the error if it is an [Error]. The returned
// boolean indicates whether the possibly zero-length data was found; similar to
// the second return value from a map.
func Data(err error) (_ []byte, ok bool) {
e := new(Error)
if !errors.As(err, &e) {
return nil, false
}
return e.Data, true
}

// ErrFrom converts a [core.ExecutionResult] into an error, or nil if the
// execution completely successfully. The returned error is non-nil i.f.f.
// r.Failed() is true.
func ErrFrom(r *core.ExecutionResult) error {
if !r.Failed() {
return nil
}
return &Error{
Data: r.Revert(),
Err: r.Err,
}
}

var _ error = (*Error)(nil)

// Error returns the error string from the [core.ExecutionResult.Err].
func (e *Error) Error() string {
return e.Err.Error()
}

// Unwrap returns the wrapped [core.ExecutionResult.Err] value.
func (e *Error) Unwrap() error {
return e.Err
}
Loading

0 comments on commit a211290

Please sign in to comment.