176 lines
8.7 KiB
Markdown
176 lines
8.7 KiB
Markdown
|
# Batch VAAs
|
|||
|
|
|||
|
[TOC]
|
|||
|
|
|||
|
## Objective
|
|||
|
|
|||
|
Add batch VAAs to Wormhole to allow for efficient verification of multiple messages, as well as allowing for better patterns for xDapps to compose with each other.
|
|||
|
|
|||
|
## Background
|
|||
|
|
|||
|
Currently, composing between different cross-chain applications is often leading to design patterns where developers are nesting all actions into a common VAA that contains all instructions to save gas and ensure atomic execution. This pattern is sub-optimal as it often requires multiple composing xDapps to integrate each other and extend their functionality for new use-cases and delivery methods. This is undesirable as it adds more code-complexity over time and slows down integration efforts.
|
|||
|
|
|||
|
## Goals
|
|||
|
|
|||
|
Extend Wormhole with the core-primitives needed to build better composability patterns by leveraging batching.
|
|||
|
|
|||
|
- Individual VAAs included in a batch should stay backwards compatible with existing smart contract integrations.
|
|||
|
- Batch VAAs should be usable in a gas-efficient manner.
|
|||
|
- Allow for cheaper verification of individual VAAs included in a batch.
|
|||
|
|
|||
|
## Non-Goals
|
|||
|
|
|||
|
This design document focuses only on the extension of the current implementation of Wormhole’s generic message passing ([0001_generic_message_passing.md](https://github.com/wormhole-foundation/wormhole/blob/dev.v2/whitepapers/0001_generic_message_passing.md)) and does not attempt to solve the following problems, leaving them for future design iterations:
|
|||
|
|
|||
|
- Replacing VAAv1s with batch VAAs (VAAv2) containing only a single `Observation`.
|
|||
|
- Ensuring backwards compatibility of batch VAAs (VAAv2) with only a single `Observation` using the existing Wormhole APIs.
|
|||
|
- The specifics of implementing xDapps leveraging batch VAAs, other than ensuring the right APIs are provided.
|
|||
|
|
|||
|
## Overview
|
|||
|
|
|||
|
For now, all Wormhole messages that are emitted during a transaction will continue to receive individual signatures (VAAv1). This is mainly to ensure backwards compatibility and might be deprecated in the future.
|
|||
|
|
|||
|
Guardians will start producing batch-signatures for messages emitted within the same transaction and that share the same nonce. However, messages with a nonce of zero will not receive batch-signatures, as this is a way of opting out of including messages in a batch VAA.
|
|||
|
|
|||
|
We will add support for two new VAA payload types to the Wormhole core contract to allow handling of these:
|
|||
|
|
|||
|
- The VAAv2 payload that holds the batch-signatures, an array of the signed hashes and an array of `IndexedObservations` (full batch or subset of a batch). This VAAv2 payload can be verified using a new Wormhole core contract endpoint `verifyBatchVM` . This payload will also be produced for individual messages with a nonce greater than zero to offer integrators flexibility when deciding which Wormhole core endpoint to verify messages with.
|
|||
|
- The VAAv3 payload, which is a “headless” payload that only carries an `Observation` . This payload can only be verified when its hash is cached by the Wormhole core contract during VAAv2 signature verification. This payload type is created when a VAAv2 is parsed using the Wormhole core endpoint `parseBatchVM` by prepending the version type to the `Observation` bytes. Although the payload format for VAAv3 is new, it will be parsed and verified using the existing Wormhole core endpoints and parsed into the existing `VM` struct (`signatures[]` and `guardianSetIndex` will be null) to ensure backwards compatibility.
|
|||
|
|
|||
|
## Detailed Design
|
|||
|
|
|||
|
### VAAv2
|
|||
|
|
|||
|
To create a VAAv2 payload (which is eventually parsed into the `VM2` struct) an xDapp will invoke the `publishMessage` method at least one time with a nonce greater than zero. The guardian will then produce a VAAv2 payload by grouping all messages with the same `nonce` (in the same transaction) and create a batch-signature by signing the hash of all hashes of the `Observations`:
|
|||
|
|
|||
|
`hash(hash(Observation1), hash(Observation2), ...)`
|
|||
|
|
|||
|
Once the batch is signed by the guardian, the VAAv2 can be parsed and verified by calling the new Wormhole core endpoint `parseAndVerifyBatchVM`. This method parses the VAAv2 into the `VM2` struct by calling `parseBatchVM` , calls `verifyBatchVM` to verify the batch-signatures, and stores the hash of each `Observation` in a cache when specified by the caller (for reasons explained in the **VAAv3** section of this detailed design).
|
|||
|
|
|||
|
`verifyBatchVM` also validates that the hash of each `Observation` is stored in the `hashes` array at the index specified in each `IndexedObservation` . The `hashes` array can never be altered (batch-signature verification will fail) but users can prune VAAv2 payloads by removing `Observations` . This allows VAAv2 delivery with a subset (partial batch) of the original `Observations` to reduce the payload size and gas overhead associated with verifying the batch. The structure of the VAAv2 payload can be found in the **Payloads** section of this design.
|
|||
|
|
|||
|
### VAAv3
|
|||
|
|
|||
|
When a VAAv2 payload is parsed into a `VM2` struct, each `Observation` is stored as bytes in the `IndexedObservation` struct. The `uint8(3)` version type is prepended to the bytes to specify that they are considered a VAAv3 payload. Each VAAv3 payload can be parsed into the existing `VM` struct with `parseVM` to ensure backwards compatibility with existing smart contract integrations. Since VAAv3 payloads are considered “headless” and do not contain signatures, the `Signatures[]` and `guardianSetIndex` fields are left as null in the `VM` struct.
|
|||
|
|
|||
|
A parsed VAAv3 payload can then be verified by calling the existing method `verifyVM` . This method will check that the hash of the VAAv3 payload (hash of the `Observation`) is stored in the `verifiedHashCache` and bypass signature verification (allowing for cheap verification of individual messages). The VAAv3 payload hash will only be stored in the `verifiedHashCache` if the caller sets the `cache` argument to `true` when verifying the associated VAAv2 payload with `verifyBatchVM` .
|
|||
|
|
|||
|
At the end of a batch execution, the handler contract should call `clearBatchCache` which will clear the `verifiedHashCache` of provided hashes and reduce the gas costs associated with storing the hashes in the Wormhole contract’s state. A parsed VAAv3 payload will no longer be considered a verified message once its hash is removed from the `verifiedHashCache` .
|
|||
|
|
|||
|
### API
|
|||
|
|
|||
|
```solidity
|
|||
|
function parseAndVerifyBatchVM(bytes calldata encodedVM, bool cache)
|
|||
|
function verifyBatchVM(Structs.VM2 memory vm, bool cache)
|
|||
|
function parseBatchVM(bytes memory encodedVM)
|
|||
|
function clearBatchCache(bytes32[] memory hashesToClear)
|
|||
|
```
|
|||
|
|
|||
|
### Structs
|
|||
|
|
|||
|
```solidity
|
|||
|
struct Header {
|
|||
|
uint32 guardianSetIndex;
|
|||
|
Signature[] signatures;
|
|||
|
bytes32 hash;
|
|||
|
}
|
|||
|
|
|||
|
struct Observation {
|
|||
|
uint32 timestamp;
|
|||
|
uint32 nonce;
|
|||
|
uint16 emitterChainId;
|
|||
|
bytes32 emitterAddress;
|
|||
|
uint64 sequence;
|
|||
|
uint8 consistencyLevel;
|
|||
|
bytes payload;
|
|||
|
}
|
|||
|
|
|||
|
struct IndexedObservation {
|
|||
|
// Index of the observation in the batch
|
|||
|
uint8 index;
|
|||
|
// Observation stored as tightly packed bytes. These bytes
|
|||
|
// are stored with a prepended uint8 (uint8(3)) to specify that
|
|||
|
// they represent a VAAv3 payload.
|
|||
|
bytes observation;
|
|||
|
}
|
|||
|
|
|||
|
// This struct exists already, but now has an additional version type 3.
|
|||
|
struct VM {
|
|||
|
uint8 version; // Version = 1 or 3
|
|||
|
// Inlined Observation
|
|||
|
uint32 timestamp;
|
|||
|
uint32 nonce;
|
|||
|
uint16 emitterChainId;
|
|||
|
bytes32 emitterAddress;
|
|||
|
uint64 sequence;
|
|||
|
uint8 consistencyLevel;
|
|||
|
bytes payload;
|
|||
|
// End of observation
|
|||
|
|
|||
|
// Inlined Header
|
|||
|
uint32 guardianSetIndex;
|
|||
|
Signature[] signatures;
|
|||
|
|
|||
|
// Hash of the Observation
|
|||
|
bytes32 hash;
|
|||
|
}
|
|||
|
|
|||
|
struct VM2 {
|
|||
|
uint8 version; // Version = 2
|
|||
|
|
|||
|
// Inlined header
|
|||
|
uint32 guardianSetIndex;
|
|||
|
Signature[] signatures;
|
|||
|
|
|||
|
// Array of Observation hashes
|
|||
|
bytes32[] hashes;
|
|||
|
|
|||
|
// Computed Batch Hash - hash(hash(Observation1), hash(Observation2), ...)
|
|||
|
bytes32 hash;
|
|||
|
|
|||
|
// Array of IndexedObservations
|
|||
|
IndexedObservation[] indexedObservations;
|
|||
|
}
|
|||
|
```
|
|||
|
|
|||
|
### Payloads (Encoded Messages)
|
|||
|
|
|||
|
VAAv2:
|
|||
|
|
|||
|
```solidity
|
|||
|
// Version uint8 = 2;
|
|||
|
uint8 version;
|
|||
|
// Guardian set index
|
|||
|
uint32 guardianSetIndex;
|
|||
|
// Number of signatures
|
|||
|
uint8 signersLen;
|
|||
|
// Signatures: 66 bytes per signature
|
|||
|
Signature[] signatures;
|
|||
|
// Number of hashes
|
|||
|
uint8 hashesLen;
|
|||
|
// Array of Observation hashes
|
|||
|
bytes32[] hashes;
|
|||
|
// Number of observations, should be less than or
|
|||
|
// equal to the hashesLen if the batch has a subset
|
|||
|
// of observations.
|
|||
|
uint8 observationsLen;
|
|||
|
|
|||
|
// Repeated for observationLen times, bytes[] Observation
|
|||
|
// Index of the observation
|
|||
|
uint8 index;
|
|||
|
// Number of bytes in the Observation
|
|||
|
uint32 observationBytesLen;
|
|||
|
// Encoded Observation, see the Structs section
|
|||
|
// for details on the Observation structure.
|
|||
|
bytes observation;
|
|||
|
```
|
|||
|
|
|||
|
VAAv3:
|
|||
|
|
|||
|
```solidity
|
|||
|
// Version uint8 = 3;
|
|||
|
uint8 version;
|
|||
|
// Observation bytes
|
|||
|
bytes observation;
|
|||
|
```
|