Skip to content

Commit

Permalink
Implement Code.Run() method (#5)
Browse files Browse the repository at this point in the history
* feat: `Code.Run()` implementation with hard-coded params

* feat: `runopts` package to configure `Code.Run()` calls
  • Loading branch information
aschlosberg authored Feb 29, 2024
1 parent a1e570a commit aa08c2b
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 37 deletions.
7 changes: 1 addition & 6 deletions examples_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -368,11 +368,6 @@ func ExampleCode_sqrt() {
}

func compileAndRun[T interface{ []byte | [32]byte }](code Code, callData T) []byte {
compiled, err := code.Compile()
if err != nil {
log.Fatal(err)
}

var slice []byte
switch c := any(callData).(type) {
case []byte:
Expand All @@ -381,7 +376,7 @@ func compileAndRun[T interface{ []byte | [32]byte }](code Code, callData T) []by
slice = c[:]
}

got, err := runBytecode(compiled, slice)
got, err := code.Run(slice)
if err != nil {
log.Fatal(err)
}
Expand Down
67 changes: 67 additions & 0 deletions run.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package specialops

import (
"fmt"
"math/big"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/params"
"github.com/solidifylabs/specialops/runopts"
)

// Run calls c.Compile() and runs the compiled bytecode on a freshly
// instantiated vm.EVMInterpreter. The default EVM parameters MUST NOT be
// considered stable: they are currently such that code runs on the Cancun fork
// with no state DB.
func (c Code) Run(callData []byte, opts ...runopts.Option) ([]byte, error) {
compiled, err := c.Compile()
if err != nil {
return nil, fmt.Errorf("%T.Compile(): %v", c, err)
}
return runBytecode(compiled, callData, opts...)
}

func runBytecode(compiled, callData []byte, opts ...runopts.Option) ([]byte, error) {
cfg, err := newRunConfig(opts...)
if err != nil {
return nil, err
}
interp := vm.NewEVM(
cfg.BlockCtx,
cfg.TxCtx,
cfg.StateDB,
cfg.ChainConfig,
cfg.VMConfig,
).Interpreter()

cc := &vm.Contract{
Code: compiled,
Gas: 30e6,
}

out, err := interp.Run(cc, callData, cfg.ReadOnly)
if err != nil {
return nil, fmt.Errorf("%T.Run([%T.Compile()], [callData], readOnly=%t): %v", interp, Code{}, cfg.ReadOnly, err)
}
return out, nil
}

func newRunConfig(opts ...runopts.Option) (*runopts.Configuration, error) {
cfg := &runopts.Configuration{
BlockCtx: vm.BlockContext{
BlockNumber: big.NewInt(0),
Random: &common.Hash{}, // post merge
},
ChainConfig: &params.ChainConfig{
LondonBlock: big.NewInt(0),
CancunTime: new(uint64),
},
}
for _, o := range opts {
if err := o.Apply(cfg); err != nil {
return nil, fmt.Errorf("runopts.Option[%T].Apply(): %v", o, err)
}
}
return cfg, nil
}
44 changes: 44 additions & 0 deletions runopts/runopts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Package runopts provides configuration options for specialops.Code.Run().
package runopts

import (
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/params"
)

// A Configuration carries all values that can be modified to configure a call
// to specialops.Code.Run(). It is intially set by Run() and then passed to all
// Options to be modified.
type Configuration struct {
// vm.NewEVM()
BlockCtx vm.BlockContext
TxCtx vm.TxContext
StateDB vm.StateDB
ChainConfig *params.ChainConfig
VMConfig vm.Config
// EVMInterpreter.Run()
ReadOnly bool // static call
}

// An Option modifies a Configuration.
type Option interface {
Apply(*Configuration) error
}

// A FuncOption converts any function into an Option by calling itself as
// Apply().
type FuncOption func(*Configuration) error

// Apply returns f(c).
func (f FuncOption) Apply(c *Configuration) error {
return f(c)
}

// ReadOnly sets the `readOnly` argument to true when calling
// EVMInterpreter.Run(), equivalent to a static call.
func ReadOnly() Option {
return FuncOption(func(c *Configuration) error {
c.ReadOnly = true
return nil
})
}
35 changes: 4 additions & 31 deletions specialops_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,39 +4,12 @@ import (
"bytes"
"fmt"
"log"
"math/big"
"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/params"
"github.com/holiman/uint256"
)

func runBytecode(compiled, callData []byte) ([]byte, error) {
interp := vm.NewEVM(
vm.BlockContext{
BlockNumber: big.NewInt(99),
Random: &common.MaxHash, // non-nil -> post merge
},
vm.TxContext{},
nil, /*statedb*/
&params.ChainConfig{
LondonBlock: big.NewInt(0),
CancunTime: new(uint64),
},
vm.Config{},
).Interpreter()

contract := &vm.Contract{
Code: compiled,
Gas: 30e6,
}

return interp.Run(contract, callData, false /*static*/)
}

// mustRunByteCode propagates arguments to runBytecode, calling log.Fatal() on
// error, otherwise returning the result. It's useful for testable examples that
// don't have access to t.Fatal().
Expand Down Expand Up @@ -211,14 +184,14 @@ func TestRunCompiled(t *testing.T) {
}
t.Logf("Bytecode: %#x", compiled)

got, err := runBytecode(compiled, tt.callData)
got, err := tt.code.Run(tt.callData)
if err != nil {
t.Fatalf("%T.Run([%T.Compile() output]) error %v", &vm.EVMInterpreter{}, tt.code, err)
t.Fatalf("%T.Run(%#x) error %v", tt.code, tt.callData, err)
}
if !bytes.Equal(got, tt.want) {
t.Errorf(
"%T.Run([%T.Compile() output]) got:\n%#x\n%v\n\nwant:\n%#x\n%v",
&vm.EVMInterpreter{}, tt.code,
"%T.Run(%#x) got:\n%#x\n%v\n\nwant:\n%#x\n%v",
tt.code, tt.callData,
got, new(uint256.Int).SetBytes(got),
tt.want, new(uint256.Int).SetBytes(tt.want),
)
Expand Down

0 comments on commit aa08c2b

Please sign in to comment.