Commit e2b59c2c authored by protolambda's avatar protolambda

preimage: preimage interface without geth dependency

parent c8998968
module github.com/ethereum-optimism/cannon/preimage
go 1.20
require (
github.com/stretchr/testify v1.8.2
golang.org/x/crypto v0.8.0
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/sys v0.7.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
package preimage
import (
"encoding/binary"
"fmt"
"io"
)
// HintWriter writes hints to an io.Writer (e.g. a special file descriptor, or a debug log),
// for a pre-image oracle service to prepare specific pre-images.
type HintWriter struct {
rw io.ReadWriter
}
var _ Hinter = (*HintWriter)(nil)
func NewHintWriter(rw io.ReadWriter) *HintWriter {
return &HintWriter{rw: rw}
}
func (hw *HintWriter) Hint(v Hint) {
hint := v.Hint()
var hintBytes []byte
hintBytes = binary.BigEndian.AppendUint32(hintBytes, uint32(len(hint)))
hintBytes = append(hintBytes, []byte(hint)...)
_, err := hw.rw.Write(hintBytes)
if err != nil {
panic(fmt.Errorf("failed to write pre-image hint: %w", err))
}
_, err = hw.rw.Read([]byte{0})
if err != nil {
panic(fmt.Errorf("failed to read pre-image hint ack: %w", err))
}
}
// HintReader reads the hints of HintWriter and passes them to a router for preparation of the requested pre-images.
// Onchain the written hints are no-op.
type HintReader struct {
rw io.ReadWriter
}
func NewHintReader(rw io.ReadWriter) *HintReader {
return &HintReader{rw: rw}
}
func (hr *HintReader) NextHint(router func(hint string) error) error {
var length uint32
if err := binary.Read(hr.rw, binary.BigEndian, &length); err != nil {
if err == io.EOF {
return io.EOF
}
return fmt.Errorf("failed to read hint length prefix: %w", err)
}
payload := make([]byte, length)
if length > 0 {
if _, err := io.ReadFull(hr.rw, payload); err != nil {
return fmt.Errorf("failed to read hint payload (length %d): %w", length, err)
}
}
if err := router(string(payload)); err != nil {
// write back on error to unblock the HintWriter
_, _ = hr.rw.Write([]byte{0})
return fmt.Errorf("failed to handle hint: %w", err)
}
if _, err := hr.rw.Write([]byte{0}); err != nil {
return fmt.Errorf("failed to write trailing no-op byte to unblock hint writer: %w", err)
}
return nil
}
package preimage
import (
"bytes"
"crypto/rand"
"errors"
"io"
"sync"
"testing"
"time"
"github.com/stretchr/testify/require"
)
type rawHint string
func (rh rawHint) Hint() string {
return string(rh)
}
func TestHints(t *testing.T) {
// Note: pretty much every string is valid communication:
// length, payload, 0. Worst case you run out of data, or allocate too much.
testHint := func(hints ...string) {
a, b := bidirectionalPipe()
var wg sync.WaitGroup
wg.Add(2)
go func() {
hw := NewHintWriter(a)
for _, h := range hints {
hw.Hint(rawHint(h))
}
wg.Done()
}()
got := make(chan string, len(hints))
go func() {
defer wg.Done()
hr := NewHintReader(b)
for i := 0; i < len(hints); i++ {
err := hr.NextHint(func(hint string) error {
got <- hint
return nil
})
if err == io.EOF {
break
}
require.NoError(t, err)
}
}()
if waitTimeout(&wg) {
t.Error("hint read/write stuck")
}
require.Equal(t, len(hints), len(got), "got all hints")
for _, h := range hints {
require.Equal(t, h, <-got, "hints match")
}
}
t.Run("empty hint", func(t *testing.T) {
testHint("")
})
t.Run("hello world", func(t *testing.T) {
testHint("hello world")
})
t.Run("zero byte", func(t *testing.T) {
testHint(string([]byte{0}))
})
t.Run("many zeroes", func(t *testing.T) {
testHint(string(make([]byte, 1000)))
})
t.Run("random data", func(t *testing.T) {
dat := make([]byte, 1000)
_, _ = rand.Read(dat[:])
testHint(string(dat))
})
t.Run("multiple hints", func(t *testing.T) {
testHint("give me header a", "also header b", "foo bar")
})
t.Run("unexpected EOF", func(t *testing.T) {
var buf bytes.Buffer
hw := NewHintWriter(&buf)
hw.Hint(rawHint("hello"))
_, _ = buf.Read(make([]byte, 1)) // read one byte so it falls short, see if it's detected
hr := NewHintReader(&buf)
err := hr.NextHint(func(hint string) error { return nil })
require.ErrorIs(t, err, io.ErrUnexpectedEOF)
})
t.Run("cb error", func(t *testing.T) {
a, b := bidirectionalPipe()
var wg sync.WaitGroup
wg.Add(2)
go func() {
hw := NewHintWriter(a)
hw.Hint(rawHint("one"))
hw.Hint(rawHint("two"))
wg.Done()
}()
go func() {
defer wg.Done()
hr := NewHintReader(b)
cbErr := errors.New("fail")
err := hr.NextHint(func(hint string) error { return cbErr })
require.ErrorIs(t, err, cbErr)
var readHint string
err = hr.NextHint(func(hint string) error {
readHint = hint
return nil
})
require.NoError(t, err)
require.Equal(t, readHint, "two")
}()
if waitTimeout(&wg) {
t.Error("read/write hint stuck")
}
})
}
// waitTimeout returns true iff wg.Wait timed out
func waitTimeout(wg *sync.WaitGroup) bool {
done := make(chan struct{})
go func() {
wg.Wait()
close(done)
}()
select {
case <-time.After(time.Second * 30):
return true
case <-done:
return false
}
}
package preimage
import (
"encoding/binary"
"encoding/hex"
)
type Key interface {
// PreimageKey changes the Key commitment into a
// 32-byte type-prefixed preimage key.
PreimageKey() [32]byte
}
type Oracle interface {
// Get the full pre-image of a given pre-image key.
// This returns no error: the client state-transition
// is invalid if there is any missing pre-image data.
Get(key Key) []byte
}
type OracleFn func(key Key) []byte
func (fn OracleFn) Get(key Key) []byte {
return fn(key)
}
// KeyType is the key-type of a pre-image, used to prefix the pre-image key with.
type KeyType byte
const (
// The zero key type is illegal to use, ensuring all keys are non-zero.
_ KeyType = 0
// LocalKeyType is for input-type pre-images, specific to the local program instance.
LocalKeyType KeyType = 1
// Keccak256KeyType is for keccak256 pre-images, for any global shared pre-images.
Keccak256KeyType KeyType = 2
)
// LocalIndexKey is a key local to the program, indexing a special program input.
type LocalIndexKey uint64
func (k LocalIndexKey) PreimageKey() (out [32]byte) {
out[0] = byte(LocalKeyType)
binary.BigEndian.PutUint64(out[24:], uint64(k))
return
}
// Keccak256Key wraps a keccak256 hash to use it as a typed pre-image key.
type Keccak256Key [32]byte
func (k Keccak256Key) PreimageKey() (out [32]byte) {
out = k // copy the keccak hash
out[0] = byte(Keccak256KeyType) // apply prefix
return
}
func (k Keccak256Key) String() string {
return "0x" + hex.EncodeToString(k[:])
}
func (k Keccak256Key) TerminalString() string {
return "0x" + hex.EncodeToString(k[:])
}
// Hint is an interface to enable any program type to function as a hint,
// when passed to the Hinter interface, returning a string representation
// of what data the host should prepare pre-images for.
type Hint interface {
Hint() string
}
// Hinter is an interface to write hints to the host.
// This may be implemented as a no-op or logging hinter
// if the program is executing in a read-only environment
// where the host is expected to have all pre-images ready.
type Hinter interface {
Hint(v Hint)
}
type HinterFn func(v Hint)
func (fn HinterFn) Hint(v Hint) {
fn(v)
}
package preimage
import (
"encoding/binary"
"fmt"
"io"
)
// OracleClient implements the Oracle by writing the pre-image key to the given stream,
// and reading back a length-prefixed value.
type OracleClient struct {
rw io.ReadWriter
}
func NewOracleClient(rw io.ReadWriter) *OracleClient {
return &OracleClient{rw: rw}
}
var _ Oracle = (*OracleClient)(nil)
func (o *OracleClient) Get(key Key) []byte {
h := key.PreimageKey()
if _, err := o.rw.Write(h[:]); err != nil {
panic(fmt.Errorf("failed to write key %s (%T) to pre-image oracle: %w", key, key, err))
}
var length uint64
if err := binary.Read(o.rw, binary.BigEndian, &length); err != nil {
panic(fmt.Errorf("failed to read pre-image length of key %s (%T) from pre-image oracle: %w", key, key, err))
}
payload := make([]byte, length)
if _, err := io.ReadFull(o.rw, payload); err != nil {
panic(fmt.Errorf("failed to read pre-image payload (length %d) of key %s (%T) from pre-image oracle: %w", length, key, key, err))
}
return payload
}
// OracleServer serves the pre-image requests of the OracleClient, implementing the same protocol as the onchain VM.
type OracleServer struct {
rw io.ReadWriter
}
func NewOracleServer(rw io.ReadWriter) *OracleServer {
return &OracleServer{rw: rw}
}
func (o *OracleServer) NextPreimageRequest(getPreimage func(key [32]byte) ([]byte, error)) error {
var key [32]byte
if _, err := io.ReadFull(o.rw, key[:]); err != nil {
if err == io.EOF {
return io.EOF
}
return fmt.Errorf("failed to read requested pre-image key: %w", err)
}
value, err := getPreimage(key)
if err != nil {
return fmt.Errorf("failed to serve pre-image %s request: %w", key, err)
}
if err := binary.Write(o.rw, binary.BigEndian, uint64(len(value))); err != nil {
return fmt.Errorf("failed to write length-prefix %d: %w", len(value), err)
}
if len(value) == 0 {
return nil
}
if _, err := o.rw.Write(value); err != nil {
return fmt.Errorf("failed to write pre-image value (%d long): %w", len(value), err)
}
return nil
}
package preimage
import (
"bytes"
"crypto/rand"
"fmt"
"io"
"sync"
"testing"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/sha3"
)
func keccak256(v []byte) (out [32]byte) {
s := sha3.NewLegacyKeccak256()
s.Write(v)
s.Sum(out[:0])
return
}
type readWritePair struct {
io.Reader
io.Writer
}
func bidirectionalPipe() (a, b io.ReadWriter) {
ar, bw := io.Pipe()
br, aw := io.Pipe()
return readWritePair{Reader: ar, Writer: aw}, readWritePair{Reader: br, Writer: bw}
}
func TestOracle(t *testing.T) {
testPreimage := func(preimages ...[]byte) {
a, b := bidirectionalPipe()
cl := NewOracleClient(a)
srv := NewOracleServer(b)
preimageByHash := make(map[[32]byte][]byte)
for _, p := range preimages {
k := Keccak256Key(keccak256(p))
preimageByHash[k.PreimageKey()] = p
}
for _, p := range preimages {
k := Keccak256Key(keccak256(p))
var wg sync.WaitGroup
wg.Add(2)
go func(k Key, p []byte) {
result := cl.Get(k)
wg.Done()
expected := preimageByHash[k.PreimageKey()]
require.True(t, bytes.Equal(expected, result), "need correct preimage %x, got %x", expected, result)
}(k, p)
go func() {
err := srv.NextPreimageRequest(func(key [32]byte) ([]byte, error) {
dat, ok := preimageByHash[key]
if !ok {
return nil, fmt.Errorf("cannot find %s", key)
}
return dat, nil
})
wg.Done()
require.NoError(t, err)
}()
wg.Wait()
}
}
t.Run("empty preimage", func(t *testing.T) {
testPreimage([]byte{})
})
t.Run("nil preimage", func(t *testing.T) {
testPreimage(nil)
})
t.Run("zero", func(t *testing.T) {
testPreimage([]byte{0})
})
t.Run("multiple", func(t *testing.T) {
testPreimage([]byte("tx from alice"), []byte{0x13, 0x37}, []byte("tx from bob"))
})
t.Run("zeroes", func(t *testing.T) {
testPreimage(make([]byte, 1000))
})
t.Run("random", func(t *testing.T) {
dat := make([]byte, 1000)
_, _ = rand.Read(dat[:])
testPreimage(dat)
})
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment