Skip to content

Commit

Permalink
Add Warp Payload Types (#2116)
Browse files Browse the repository at this point in the history
Signed-off-by: Cesar <[email protected]>
Co-authored-by: Stephen Buttolph <[email protected]>
Co-authored-by: aaronbuchwald <[email protected]>
  • Loading branch information
3 people authored Oct 5, 2023
1 parent 1157e2d commit 1eaf40b
Show file tree
Hide file tree
Showing 8 changed files with 369 additions and 0 deletions.
44 changes: 44 additions & 0 deletions vms/platformvm/warp/payload/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Payload

An Avalanche Unsigned Warp Message already includes a `networkID`, `sourceChainID`, and `payload` field. The `payload` field can be parsed into one of the types included in this package to be further handled by the VM.

## Hash

Hash:
```
+-----------------+----------+-----------+
| codecID : uint16 | 2 bytes |
+-----------------+----------+-----------+
| typeID : uint32 | 4 bytes |
+-----------------+----------+-----------+
| hash : [32]byte | 32 bytes |
+-----------------+----------+-----------+
| 38 bytes |
+-----------+
```

- `codecID` is the codec version used to serialize the payload and is hardcoded to `0x0000`
- `typeID` is the payload type identifier and is `0x00000000` for `Hash`
- `hash` is a hash from the `sourceChainID`. As an example, this may be the hash of a block that was accepted on the source chain

## AddressedCall

AddressedCall:
```
+---------------------+--------+----------------------------------+
| codecID : uint16 | 2 bytes |
+---------------------+--------+----------------------------------+
| typeID : uint32 | 4 bytes |
+---------------------+--------+----------------------------------+
| sourceAddress : []byte | 4 + len(address) |
+---------------------+--------+----------------------------------+
| payload : []byte | 4 + len(payload) |
+---------------------+--------+----------------------------------+
| 14 + len(payload) + len(address) |
+----------------------------------+
```

- `codecID` is the codec version used to serialize the payload and is hardcoded to `0x0000`
- `typeID` is the payload type identifier and is `0x00000001` for `AddressedCall`
- `sourceAddress` is the address that sent this message from the source chain
- `payload` is an arbitrary byte array payload
53 changes: 53 additions & 0 deletions vms/platformvm/warp/payload/addressed_call.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package payload

import "fmt"

var _ Payload = (*AddressedCall)(nil)

// AddressedCall defines the format for delivering a call across VMs including a
// source address and a payload.
//
// Note: If a destination address is expected, it should be encoded in the
// payload.
type AddressedCall struct {
SourceAddress []byte `serialize:"true"`
Payload []byte `serialize:"true"`

bytes []byte
}

// NewAddressedCall creates a new *AddressedCall and initializes it.
func NewAddressedCall(sourceAddress []byte, payload []byte) (*AddressedCall, error) {
ap := &AddressedCall{
SourceAddress: sourceAddress,
Payload: payload,
}
return ap, initialize(ap)
}

// ParseAddressedCall converts a slice of bytes into an initialized
// AddressedCall.
func ParseAddressedCall(b []byte) (*AddressedCall, error) {
payloadIntf, err := Parse(b)
if err != nil {
return nil, err
}
payload, ok := payloadIntf.(*AddressedCall)
if !ok {
return nil, fmt.Errorf("%w: %T", errWrongType, payloadIntf)
}
return payload, nil
}

// Bytes returns the binary representation of this payload. It assumes that the
// payload is initialized from either NewAddressedCall or Parse.
func (a *AddressedCall) Bytes() []byte {
return a.bytes
}

func (a *AddressedCall) initialize(bytes []byte) {
a.bytes = bytes
}
46 changes: 46 additions & 0 deletions vms/platformvm/warp/payload/addressed_call_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package payload

import (
"encoding/base64"
"testing"

"github.com/stretchr/testify/require"

"github.com/ava-labs/avalanchego/codec"
"github.com/ava-labs/avalanchego/ids"
)

func TestAddressedCall(t *testing.T) {
require := require.New(t)
shortID := ids.GenerateTestShortID()

addressedPayload, err := NewAddressedCall(
shortID[:],
[]byte{1, 2, 3},
)
require.NoError(err)

addressedPayloadBytes := addressedPayload.Bytes()
parsedAddressedPayload, err := ParseAddressedCall(addressedPayloadBytes)
require.NoError(err)
require.Equal(addressedPayload, parsedAddressedPayload)
}

func TestParseAddressedCallJunk(t *testing.T) {
_, err := ParseAddressedCall(junkBytes)
require.ErrorIs(t, err, codec.ErrUnknownVersion)
}

func TestAddressedCallBytes(t *testing.T) {
require := require.New(t)
base64Payload := "AAAAAAABAAAAEAECAwAAAAAAAAAAAAAAAAAAAAADCgsM"
addressedPayload, err := NewAddressedCall(
[]byte{1, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
[]byte{10, 11, 12},
)
require.NoError(err)
require.Equal(base64Payload, base64.StdEncoding.EncodeToString(addressedPayload.Bytes()))
}
39 changes: 39 additions & 0 deletions vms/platformvm/warp/payload/codec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package payload

import (
"github.com/ava-labs/avalanchego/codec"
"github.com/ava-labs/avalanchego/codec/linearcodec"
"github.com/ava-labs/avalanchego/utils/units"
"github.com/ava-labs/avalanchego/utils/wrappers"
)

const (
codecVersion = 0

MaxMessageSize = 24 * units.KiB

// Note: Modifying this variable can have subtle implications on memory
// usage when parsing malformed payloads.
MaxSliceLen = 24 * 1024
)

// Codec does serialization and deserialization for Warp messages.
var c codec.Manager

func init() {
c = codec.NewManager(MaxMessageSize)
lc := linearcodec.NewCustomMaxLength(MaxSliceLen)

errs := wrappers.Errs{}
errs.Add(
lc.RegisterType(&Hash{}),
lc.RegisterType(&AddressedCall{}),
c.RegisterCodec(codecVersion, lc),
)
if errs.Errored() {
panic(errs.Err)
}
}
49 changes: 49 additions & 0 deletions vms/platformvm/warp/payload/hash.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package payload

import (
"fmt"

"github.com/ava-labs/avalanchego/ids"
)

var _ Payload = (*Hash)(nil)

type Hash struct {
Hash ids.ID `serialize:"true"`

bytes []byte
}

// NewHash creates a new *Hash and initializes it.
func NewHash(hash ids.ID) (*Hash, error) {
bhp := &Hash{
Hash: hash,
}
return bhp, initialize(bhp)
}

// ParseHash converts a slice of bytes into an initialized Hash.
func ParseHash(b []byte) (*Hash, error) {
payloadIntf, err := Parse(b)
if err != nil {
return nil, err
}
payload, ok := payloadIntf.(*Hash)
if !ok {
return nil, fmt.Errorf("%w: %T", errWrongType, payloadIntf)
}
return payload, nil
}

// Bytes returns the binary representation of this payload. It assumes that the
// payload is initialized from either NewHash or Parse.
func (b *Hash) Bytes() []byte {
return b.bytes
}

func (b *Hash) initialize(bytes []byte) {
b.bytes = bytes
}
39 changes: 39 additions & 0 deletions vms/platformvm/warp/payload/hash_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package payload

import (
"encoding/base64"
"testing"

"github.com/stretchr/testify/require"

"github.com/ava-labs/avalanchego/codec"
"github.com/ava-labs/avalanchego/ids"
)

func TestHash(t *testing.T) {
require := require.New(t)

hashPayload, err := NewHash(ids.GenerateTestID())
require.NoError(err)

hashPayloadBytes := hashPayload.Bytes()
parsedHashPayload, err := ParseHash(hashPayloadBytes)
require.NoError(err)
require.Equal(hashPayload, parsedHashPayload)
}

func TestParseHashJunk(t *testing.T) {
_, err := ParseHash(junkBytes)
require.ErrorIs(t, err, codec.ErrUnknownVersion)
}

func TestHashBytes(t *testing.T) {
require := require.New(t)
base64Payload := "AAAAAAAABAUGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
hashPayload, err := NewHash(ids.ID{4, 5, 6})
require.NoError(err)
require.Equal(base64Payload, base64.StdEncoding.EncodeToString(hashPayload.Bytes()))
}
39 changes: 39 additions & 0 deletions vms/platformvm/warp/payload/payload.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package payload

import (
"errors"
"fmt"
)

var errWrongType = errors.New("wrong payload type")

// Payload provides a common interface for all payloads implemented by this
// package.
type Payload interface {
// Bytes returns the binary representation of this payload.
Bytes() []byte

// initialize the payload with the provided binary representation.
initialize(b []byte)
}

func Parse(bytes []byte) (Payload, error) {
var payload Payload
if _, err := c.Unmarshal(bytes, &payload); err != nil {
return nil, err
}
payload.initialize(bytes)
return payload, nil
}

func initialize(p Payload) error {
bytes, err := c.Marshal(codecVersion, &p)
if err != nil {
return fmt.Errorf("couldn't marshal %T payload: %w", p, err)
}
p.initialize(bytes)
return nil
}
60 changes: 60 additions & 0 deletions vms/platformvm/warp/payload/payload_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package payload

import (
"testing"

"github.com/stretchr/testify/require"

"github.com/ava-labs/avalanchego/codec"
"github.com/ava-labs/avalanchego/ids"
)

var junkBytes = []byte{0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88}

func TestParseJunk(t *testing.T) {
require := require.New(t)
_, err := Parse(junkBytes)
require.ErrorIs(err, codec.ErrUnknownVersion)
}

func TestParseWrongPayloadType(t *testing.T) {
require := require.New(t)
hashPayload, err := NewHash(ids.GenerateTestID())
require.NoError(err)

shortID := ids.GenerateTestShortID()
addressedPayload, err := NewAddressedCall(
shortID[:],
[]byte{1, 2, 3},
)
require.NoError(err)

_, err = ParseAddressedCall(hashPayload.Bytes())
require.ErrorIs(err, errWrongType)

_, err = ParseHash(addressedPayload.Bytes())
require.ErrorIs(err, errWrongType)
}

func TestParse(t *testing.T) {
require := require.New(t)
hashPayload, err := NewHash(ids.ID{4, 5, 6})
require.NoError(err)

parsedHashPayload, err := Parse(hashPayload.Bytes())
require.NoError(err)
require.Equal(hashPayload, parsedHashPayload)

addressedPayload, err := NewAddressedCall(
[]byte{1, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
[]byte{10, 11, 12},
)
require.NoError(err)

parsedAddressedPayload, err := Parse(addressedPayload.Bytes())
require.NoError(err)
require.Equal(addressedPayload, parsedAddressedPayload)
}

0 comments on commit 1eaf40b

Please sign in to comment.