Skip to content

Commit

Permalink
Shortest PUSH codes (#7)
Browse files Browse the repository at this point in the history
* feat!: `PUSH*` with zero-like -> `PUSH0` + `PUSHBytes()` strips leading zeroes

* doc: PUSH-length detection in README

* doc: clarified comment on `PUSHBytes()`
  • Loading branch information
aschlosberg authored Mar 1, 2024
1 parent 6e670da commit 2a33450
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 3 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ bytecode unchanged.
- [x] `JUMPDEST` labels (absolute)
- [ ] `JUMPDEST` labels (relative to `PC`)
- [x] Function-like syntax (optional)
- [x] Inverted `DUP`/`SWAP` special opcodes from "bottom" of stack (i.e. pseudo-variables)
- [x] Inverted `DUP`/`SWAP` special opcodes from "bottom" of stack (a.k.a. pseudo-variables)
- [x] `PUSH<T>` for native Go types
- [X] `PUSH(v)` length detection
- [x] Macros
- [x] Compiler-state assertions (e.g. expected stack depth)
- [ ] Automatic stack permutation
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.20

require (
github.com/ethereum/go-ethereum v1.13.14
github.com/google/go-cmp v0.5.9
github.com/holiman/uint256 v1.2.4
)

Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao=
github.com/holiman/uint256 v1.2.4 h1:jUc4Nk8fm9jZabQuqr2JzednajVmBpC+oiTiXZJEApU=
Expand Down
23 changes: 21 additions & 2 deletions specialops.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,24 @@ func (p pusher) Bytecode() ([]byte, error) {
if n == 0 || n > 32 {
return nil, fmt.Errorf("len(%T.ToPush()) == %d must be in [1,32]", p.StackPusher, n)
}
return append([]byte{byte(vm.PUSH1 + vm.OpCode(n) - 1)}, buf...), nil

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
Expand All @@ -126,7 +143,9 @@ func PUSHSelector(sig string) Bytecoder {
return PUSH(crypto.Keccak256([]byte(sig))[:4])
}

// PUSHBytes returns a PUSH<n> Bytecoder that pushes the `n` bytes.
// PUSHBytes accepts [1,32] bytes, returning a PUSH<x> 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))
}
Expand Down
58 changes: 58 additions & 0 deletions specialops_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import (
"log"
"testing"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/crypto"
"github.com/google/go-cmp/cmp"
"github.com/holiman/uint256"
)

Expand Down Expand Up @@ -199,3 +202,58 @@ func TestRunCompiled(t *testing.T) {
})
}
}

func bytecode(t *testing.T, b Bytecoder) []byte {
t.Helper()
buf, err := b.Bytecode()
if err != nil {
t.Fatalf("%T.Bytecode() error %v", b, err)
}
return buf
}

func TestPUSHBytesZeroes(t *testing.T) {
push0 := []byte{byte(vm.PUSH0)}

t.Run("all-zero bytes", func(t *testing.T) {
for i := 1; i <= 32; i++ {
got := bytecode(t, PUSHBytes(make([]byte, i)...))
if !bytes.Equal(got, push0) {
t.Errorf("PUSHBytes([%d zero bytes]).Bytecode() got %#x; want {vm.PUSH0}", i, got)
}
}
})

t.Run("various types zero", func(t *testing.T) {
for _, b := range []Bytecoder{
PUSH(int(0)),
PUSH(uint64(0)),
PUSH(common.Address{}),
PUSH(*uint256.NewInt(0)),
PUSH(byte(0)),
} {
got := bytecode(t, b)
if !bytes.Equal(got, push0) {
t.Errorf("%#x; want {vm.PUSH0}", got)
}
}

})

t.Run("leading zeros stripped", func(t *testing.T) {
for i := 0; i < 32; i++ {
var word [32]byte
word[i] = 1

equiv := make([]byte, 32-i)
equiv[0] = 1

got := bytecode(t, PUSHBytes(word[:]...))
want := bytecode(t, PUSHBytes(equiv...))

if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("Bytecode mismatch between long PUSHBytes(%#x) and short PUSHBytes(%#x); diff (-short +long):\n%s", word, equiv, diff)
}
}
})
}

0 comments on commit 2a33450

Please sign in to comment.