Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Warp Payload Types #2116

Merged
merged 11 commits into from
Oct 5, 2023
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 is parsed into one of the types included in this package to be further handled by the VM.

## AddressedPayload

AddressedPayload:
```
+---------------------+--------+----------------------------------+
| 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 `0x00000000` for `AddressedPayload`
- `sourceAddress` is the address that called `sendWarpPrecompile` on the source chain
nytzuga marked this conversation as resolved.
Show resolved Hide resolved
nytzuga marked this conversation as resolved.
Show resolved Hide resolved
- `payload` is an arbitrary byte array payload

## BlockHashPayload

BlockHashPayload:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason this is BlockHashPayload rather than a Hash payload? Seems unnecessarily specific.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is meant to be specific to the BlockHash as opposed to an arbitrary hash. If we changed to an arbitrary hash, the caller would need to have a different way to differentiate between hash types 🤷 .

I don't think there's a need for an arbitrary hash payload at this time, so lmk if you'd prefer I change it anyways.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Going to leave this as BlockHash after removing the Payload suffix.

```
+-----------------+----------+-----------+
| codecID : uint16 | 2 bytes |
+-----------------+----------+-----------+
| typeID : uint32 | 4 bytes |
+-----------------+----------+-----------+
| blockHash : [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 `0x00000001` for `BlockHashPayload`
- `blockHash` is a blockHash from the `sourceChainID`. A signed block hash payload indicates that the signer has accepted the block on the source chain.
56 changes: 56 additions & 0 deletions vms/platformvm/warp/payload/addressed_payload.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package payload

import "fmt"

// AddressedPayload defines the format for delivering a point to point message across VMs
// ie. (ChainA, AddressA) -> (ChainB, AddressB)
type AddressedPayload struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These types seem to stutter payload.AddressedPayload, payload.BlockHashPayload. I think we could improve the naming here.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could go with AddressedCaller and BlockHash. I'll think a bit more about it and push a change here today.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Going with AddressedCall and BlockHash

SourceAddress []byte `serialize:"true"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we want to make this generic for all possible chains? (EVM chains use 20 bytes addresses)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, the intent here is to generalize this so that other VMs can use the same format. This will require changing the unpacking in Subnet-EVM / Teleporter as well.

Payload []byte `serialize:"true"`

bytes []byte
}

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

// ParseAddressedPayload converts a slice of bytes into an initialized
// AddressedPayload.
func ParseAddressedPayload(b []byte) (*AddressedPayload, error) {
var unmarshalledPayloadIntf any
if _, err := c.Unmarshal(b, &unmarshalledPayloadIntf); err != nil {
return nil, err
}
payload, ok := unmarshalledPayloadIntf.(*AddressedPayload)
if !ok {
return nil, fmt.Errorf("%w: %T", errWrongType, unmarshalledPayloadIntf)
}
payload.bytes = b
return payload, nil
}

// initialize recalculates the result of Bytes().
func (a *AddressedPayload) initialize() error {
payloadIntf := any(a)
bytes, err := c.Marshal(codecVersion, &payloadIntf)
if err != nil {
return fmt.Errorf("couldn't marshal warp addressed payload: %w", err)
}
a.bytes = bytes
return nil
}

// Bytes returns the binary representation of this payload. It assumes that the
// payload is initialized from either NewAddressedPayload or ParseAddressedPayload.
func (a *AddressedPayload) Bytes() []byte {
return a.bytes
}
57 changes: 57 additions & 0 deletions vms/platformvm/warp/payload/block_hash_payload.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// 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"
)

// BlockHashPayload includes the block hash
type BlockHashPayload struct {
BlockHash ids.ID `serialize:"true"`

bytes []byte
}

// NewBlockHashPayload creates a new *BlockHashPayload and initializes it.
func NewBlockHashPayload(blockHash ids.ID) (*BlockHashPayload, error) {
bhp := &BlockHashPayload{
BlockHash: blockHash,
}
return bhp, bhp.initialize()
}

// ParseBlockHashPayload converts a slice of bytes into an initialized
// BlockHashPayload
func ParseBlockHashPayload(b []byte) (*BlockHashPayload, error) {
var unmarshalledPayloadIntf any
if _, err := c.Unmarshal(b, &unmarshalledPayloadIntf); err != nil {
return nil, err
}
payload, ok := unmarshalledPayloadIntf.(*BlockHashPayload)
if !ok {
return nil, fmt.Errorf("%w: %T", errWrongType, unmarshalledPayloadIntf)
}
payload.bytes = b
return payload, nil
}

// initialize recalculates the result of Bytes().
func (b *BlockHashPayload) initialize() error {
payloadIntf := any(b)
bytes, err := c.Marshal(codecVersion, &payloadIntf)
if err != nil {
return fmt.Errorf("couldn't marshal block hash payload: %w", err)
}
b.bytes = bytes
return nil
}

// Bytes returns the binary representation of this payload. It assumes that the
// payload is initialized from either NewBlockHashPayload or ParseBlockHashPayload.
func (b *BlockHashPayload) Bytes() []byte {
return b.bytes
}
43 changes: 43 additions & 0 deletions vms/platformvm/warp/payload/codec.go
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we have some way to parse either payload? It seems a bit odd to me that we expect the user to know which payload format to expect

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is currently used in Subnet-EVM to serve two execution functions in the Warp precompile. In that case, the function returns an error if the warp message includes anything other than the expected payload and does not differentiate between an invalid signature and requesting the wrong type, so it was not needed.

Will add as a convenience function and so we can add a check when verifying warp messages that it matches a valid payload type rather than just checking the signature's validity.

Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package payload

import (
"errors"

"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"
)

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

const (
codecVersion = 0

MaxMessageSize = 24 * units.KiB

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

// 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(&AddressedPayload{}),
lc.RegisterType(&BlockHashPayload{}),
c.RegisterCodec(codecVersion, lc),
)
if errs.Errored() {
panic(errs.Err)
}
}
105 changes: 105 additions & 0 deletions vms/platformvm/warp/payload/payload_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// 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/ava-labs/avalanchego/codec"
"github.com/ava-labs/avalanchego/ids"
"github.com/ava-labs/avalanchego/utils"

"github.com/stretchr/testify/require"
nytzuga marked this conversation as resolved.
Show resolved Hide resolved
)

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

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

addressedPayloadBytes := addressedPayload.Bytes()
addressedPayload2, err := ParseAddressedPayload(addressedPayloadBytes)
require.NoError(err)
require.Equal(addressedPayload, addressedPayload2)
}

func TestParseAddressedPayloadJunk(t *testing.T) {
require := require.New(t)
_, err := ParseAddressedPayload(utils.RandomBytes(1024))
require.ErrorIs(err, codec.ErrUnknownVersion)
}

func TestParseAddressedPayload(t *testing.T) {
base64Payload := "AAAAAAAAAAAAEAECAwAAAAAAAAAAAAAAAAAAAAADCgsM"
payload := &AddressedPayload{
SourceAddress: []byte{1, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
Payload: []byte{10, 11, 12},
}

require.NoError(t, payload.initialize())

require.Equal(t, base64Payload, base64.StdEncoding.EncodeToString(payload.Bytes()))

parsedPayload, err := ParseAddressedPayload(payload.Bytes())
require.NoError(t, err)
require.Equal(t, payload, parsedPayload)
}

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

blockHashPayload, err := NewBlockHashPayload(ids.GenerateTestID())
require.NoError(err)

blockHashPayloadBytes := blockHashPayload.Bytes()
blockHashPayload2, err := ParseBlockHashPayload(blockHashPayloadBytes)
require.NoError(err)
require.Equal(blockHashPayload, blockHashPayload2)
}

func TestParseBlockHashPayloadJunk(t *testing.T) {
require := require.New(t)
_, err := ParseBlockHashPayload(utils.RandomBytes(1024))
require.ErrorIs(err, codec.ErrUnknownVersion)
}

func TestParseBlockHashPayload(t *testing.T) {
base64Payload := "AAAAAAABBAUGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
payload := &BlockHashPayload{
BlockHash: ids.ID{4, 5, 6},
}

require.NoError(t, payload.initialize())

require.Equal(t, base64Payload, base64.StdEncoding.EncodeToString(payload.Bytes()))

parsedPayload, err := ParseBlockHashPayload(payload.Bytes())
require.NoError(t, err)
require.Equal(t, payload, parsedPayload)
}

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

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

_, err = ParseAddressedPayload(blockHashPayload.Bytes())
require.ErrorIs(err, errWrongType)

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