diff --git a/.gitignore b/.gitignore index 891f80c..a731ec5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,9 @@ # IDEs +workspace.code-workspace -workspace.code-workspace \ No newline at end of file +# Scratchpad +experimental/** + +# Coverage +cover.out +lcov.info diff --git a/compile.go b/compile.go index becdbbf..1dcdf6f 100644 --- a/compile.go +++ b/compile.go @@ -5,6 +5,8 @@ import ( "fmt" "github.com/ethereum/go-ethereum/core/vm" + + "github.com/solidifylabs/specialops/types" ) // A splice is a (possibly empty) buffer of bytecode, followed by either a @@ -13,7 +15,7 @@ import ( // determine. A splice allows for lazy determination of locations. type splice struct { buf bytes.Buffer - op Bytecoder // either JUMPDEST or PUSHJUMPDEST + op types.Bytecoder // either JUMPDEST or PUSHJUMPDEST // If op is a JUMPDEST offset *int // Current estimate of offset in the bytecode, or nil if not yet estimated // If op is a PUSHJUMPDEST @@ -43,7 +45,7 @@ func (s *spliceConcat) curr() *splice { // to the previous splice. func newSpliceBuffer[T interface{ JUMPDEST | PUSHJUMPDEST }](s *spliceConcat, op T) *bytes.Buffer { curr := s.curr() - curr.op = Bytecoder(op) + curr.op = types.Bytecoder(op) if j, ok := any(op).(JUMPDEST); ok { s.dests[j] = curr } @@ -59,7 +61,7 @@ func (c Code) flatten() Code { out := make(Code, 0, len(c)) for _, bc := range c { switch bc := bc.(type) { - case BytecodeHolder: + case types.BytecodeHolder: out = append(out, Code(bc.Bytecoders()).flatten()...) default: out = append(out, bc) diff --git a/specialops.go b/specialops.go index 59e1661..651254a 100644 --- a/specialops.go +++ b/specialops.go @@ -18,11 +18,13 @@ import ( "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" "github.com/holiman/uint256" + + "github.com/solidifylabs/specialops/types" ) // Code is a slice of Bytecoders; it is itself a Bytecoder, allowing for // nesting. -type Code []Bytecoder +type Code []types.Bytecoder // Bytecode always returns an error; use Code.Compile instead(), which flattens // nested Code instances. @@ -31,21 +33,8 @@ func (c Code) Bytecode() ([]byte, error) { } // Bytecoders returns the Code as a slice of Bytecoders. -func (c Code) Bytecoders() []Bytecoder { - return []Bytecoder(c) -} - -// A Bytecoder returns raw EVM bytecode. If the returned bytecode is the -// concatenation of multiple Bytecoder outputs, the type MUST also implement -// BytecodeHolder. -type Bytecoder interface { - Bytecode() ([]byte, error) -} - -// A BytecodeHolder is a concatenation of Bytecoders. -type BytecodeHolder interface { - Bytecoder - Bytecoders() []Bytecoder +func (c Code) Bytecoders() []types.Bytecoder { + return []types.Bytecoder(c) } // Fn returns a Bytecoder that returns the concatenation of the *reverse* of @@ -55,7 +44,7 @@ type BytecodeHolder interface { // // Although the returned BytecodeHolder can contain JUMPDESTs, they're hard to // reason about so should be used with care. -func Fn(bcs ...Bytecoder) BytecodeHolder { +func Fn(bcs ...types.Bytecoder) types.BytecodeHolder { c := Code(bcs) for i, n := 0, len(c); i < n/2; i++ { j := n - i - 1 @@ -96,58 +85,17 @@ func (p PUSHJUMPDEST) Bytecode() ([]byte, error) { return nil, fmt.Errorf("direct call to %T.Bytecode()", p) } -// A StackPusher returns [1,32] bytes to be pushed to the stack. -type StackPusher interface { - ToPush() []byte -} - -// BytecoderFromStackPusher returns a Bytecoder that calls s.ToPush() and -// prepends the appropriate PUSH opcode to the returned bytecode. -func BytecoderFromStackPusher(s StackPusher) Bytecoder { - return pusher{s} -} - -type pusher struct { - StackPusher -} - -func (p pusher) Bytecode() ([]byte, error) { - buf := p.ToPush() - n := len(buf) - if n == 0 || n > 32 { - return nil, fmt.Errorf("len(%T.ToPush()) == %d must be in [1,32]", p.StackPusher, n) - } - - size := n - for _, b := range buf { - if b == 0 { - size-- - } else { - break - } - } - if size == 0 { - return []byte{byte(vm.PUSH0)}, nil - } - - return append( - // PUSH0 to PUSH32 are contiguous, so we can perform arithmetic on them. - []byte{byte(vm.PUSH0 + vm.OpCode(size))}, - buf[n-size:]..., - ), nil -} - // PUSHSelector returns a PUSH4 Bytecoder that pushes the selector of the // signature, i.e. `sha3(sig)[:4]`. -func PUSHSelector(sig string) Bytecoder { +func PUSHSelector(sig string) types.Bytecoder { return PUSH(crypto.Keccak256([]byte(sig))[:4]) } // PUSHBytes accepts [1,32] bytes, returning a PUSH Bytecoder where x is the // smallest number of bytes (possibly zero) that can represent the concatenated // values; i.e. x = len(bs) - leadingZeros(bs). -func PUSHBytes(bs ...byte) Bytecoder { - return BytecoderFromStackPusher(bytesPusher(bs)) +func PUSHBytes(bs ...byte) types.Bytecoder { + return types.BytecoderFromStackPusher(bytesPusher(bs)) } type bytesPusher []byte @@ -159,8 +107,8 @@ func (p bytesPusher) ToPush() []byte { return []byte(p) } func PUSH[P interface { int | uint64 | common.Address | uint256.Int | byte | []byte | JUMPDEST | string }](v P, -) Bytecoder { - pToB := BytecoderFromStackPusher +) types.Bytecoder { + pToB := types.BytecoderFromStackPusher switch v := any(v).(type) { case int: diff --git a/specialops_test.go b/specialops_test.go index 4c6dc0c..163c3c2 100644 --- a/specialops_test.go +++ b/specialops_test.go @@ -11,6 +11,7 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/google/go-cmp/cmp" "github.com/holiman/uint256" + "github.com/solidifylabs/specialops/types" ) // mustRunByteCode propagates arguments to runBytecode, calling log.Fatal() on @@ -203,7 +204,7 @@ func TestRunCompiled(t *testing.T) { } } -func bytecode(t *testing.T, b Bytecoder) []byte { +func bytecode(t *testing.T, b types.Bytecoder) []byte { t.Helper() buf, err := b.Bytecode() if err != nil { @@ -212,7 +213,7 @@ func bytecode(t *testing.T, b Bytecoder) []byte { return buf } -func TestPUSHBytesZeroes(t *testing.T) { +func TestPUSHZeroes(t *testing.T) { push0 := []byte{byte(vm.PUSH0)} t.Run("all-zero bytes", func(t *testing.T) { @@ -225,7 +226,7 @@ func TestPUSHBytesZeroes(t *testing.T) { }) t.Run("various types zero", func(t *testing.T) { - for _, b := range []Bytecoder{ + for _, b := range []types.Bytecoder{ PUSH(int(0)), PUSH(uint64(0)), PUSH(common.Address{}), diff --git a/types/types.go b/types/types.go new file mode 100644 index 0000000..f425413 --- /dev/null +++ b/types/types.go @@ -0,0 +1,63 @@ +// Package types defines types used by the specialops package, which is intended +// to be dot-imported so requires a minimal footprint of exported symbols. +package types + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/core/vm" +) + +// A Bytecoder returns raw EVM bytecode. If the returned bytecode is the +// concatenation of multiple Bytecoder outputs, the type MUST also implement +// BytecodeHolder. +type Bytecoder interface { + Bytecode() ([]byte, error) +} + +// A BytecodeHolder is a concatenation of Bytecoders. +type BytecodeHolder interface { + Bytecoder + Bytecoders() []Bytecoder +} + +// A StackPusher returns [1,32] bytes to be pushed to the stack. +type StackPusher interface { + ToPush() []byte +} + +// BytecoderFromStackPusher returns a Bytecoder that calls s.ToPush() and +// prepends the appropriate PUSH opcode to the returned bytecode. +func BytecoderFromStackPusher(s StackPusher) Bytecoder { + return pusher{s} +} + +type pusher struct { + StackPusher +} + +func (p pusher) Bytecode() ([]byte, error) { + buf := p.ToPush() + n := len(buf) + if n == 0 || n > 32 { + return nil, fmt.Errorf("len(%T.ToPush()) == %d must be in [1,32]", p.StackPusher, n) + } + + size := n + for _, b := range buf { + if b == 0 { + size-- + } else { + break + } + } + if size == 0 { + return []byte{byte(vm.PUSH0)}, nil + } + + return append( + // PUSH0 to PUSH32 are contiguous, so we can perform arithmetic on them. + []byte{byte(vm.PUSH0 + vm.OpCode(size))}, + buf[n-size:]..., + ), nil +}