add BatchVAA structs to Go sdk(#1700)

* go sdk - BatchVAA structs

* make VAA body unmarshaling DRY

* implement binary encoding interfaces for VAAs

* validate observation length before unmarshaling

* move shared VerifySignatures logic to new function

* make SigningMsg a delegate call

* normalize ID of vaa types

* add BatchVAA version to signingBody

* add Attestation interface with shared VAA methods

* add data integrity checks to batchVAA unmarshal
This commit is contained in:
Justin Schuldt 2022-10-28 13:50:49 -05:00 committed by GitHub
parent 346b68582a
commit e4096297ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 460 additions and 59 deletions

View File

@ -3,6 +3,7 @@ package vaa
import ( import (
"bytes" "bytes"
"crypto/ecdsa" "crypto/ecdsa"
"encoding"
"encoding/binary" "encoding/binary"
"encoding/hex" "encoding/hex"
"errors" "errors"
@ -42,6 +43,27 @@ type (
Payload []byte Payload []byte
} }
BatchVAA struct {
// Version of the VAA schema
Version uint8
// GuardianSetIndex is the index of the guardian set that signed this VAA
GuardianSetIndex uint32
// SignatureData is the signature of the guardian set
Signatures []*Signature
// EmitterChain the VAAs were emitted on
EmitterChain ChainID
// The chain-native identifier of the transaction that created the batch VAA.
TransactionID common.Hash
// array of Observation VAA hashes
Hashes []common.Hash
// Observations in the batch
Observations []*Observation
}
// ChainID of a Wormhole chain // ChainID of a Wormhole chain
ChainID uint16 ChainID uint16
// Action of a VAA // Action of a VAA
@ -61,6 +83,13 @@ type (
SignatureData [65]byte SignatureData [65]byte
Observation struct {
// Index of the observation in a Batch array
Index uint8
// Signed Observation data
Observation *VAA
}
TransferPayloadHdr struct { TransferPayloadHdr struct {
Type uint8 Type uint8
Amount *big.Int Amount *big.Int
@ -69,6 +98,20 @@ type (
TargetAddress Address TargetAddress Address
TargetChain ChainID TargetChain ChainID
} }
// Attestation interface contains the methods common to all VAA types
Attestation interface {
encoding.BinaryMarshaler
encoding.BinaryUnmarshaler
serializeBody()
signingBody() []byte
SigningMsg() common.Hash
VerifySignatures(addrs []common.Address) bool
UniqueID() string
HexDigest() string
AddSignature(key *ecdsa.PrivateKey, index uint8)
GetEmitterChain() ChainID
}
) )
const ( const (
@ -287,60 +330,35 @@ const (
// - sequence (8 bytes) // - sequence (8 bytes)
// - consistency level (1 byte) // - consistency level (1 byte)
// - payload (0 bytes) // - payload (0 bytes)
// // BATCH
// From Above: 1 + 4 + 1 + 0 + 4 + 4 + 2 + 32 + 8 + 1 + 0 // Equals 57 // - Length of Observation Hashes (1 byte) <== minimum one
// - Observation Hash (32 bytes)
// - Length of Observations (1 byte) <== minimum one
// - Observation Index (1 byte)
// - Observation Length (1 byte)
// - Observation, aka BODY, aka Headless (51 bytes)
// From Above:
// HEADER: 1 + 4 + 1 + 0 = 6
// BODY: 4 + 4 + 2 + 32 + 8 + 1 + 0 = 51
// BATCH: 1 + 32 + 1 + 1 + 1 + 51 = 88
// //
// More details here: https://docs.wormholenetwork.com/wormhole/vaas // More details here: https://docs.wormholenetwork.com/wormhole/vaas
minVAALength = 57 minHeadlessVAALength = 51 // HEADER
minVAALength = 57 // HEADER + BODY
minBatchVAALength = 94 // HEADER + BATCH
SupportedVAAVersion = 0x01 SupportedVAAVersion = 0x01
BatchVAAVersion = 0x02
InternalTruncatedPayloadSafetyLimit = 1000 InternalTruncatedPayloadSafetyLimit = 1000
) )
// Unmarshal deserializes the binary representation of a VAA // UnmarshalBody deserializes the binary representation of a VAA's "BODY" properties
// The BODY fields are common among multiple types of VAA - v1, v2 (BatchVAA), etc
// //
// WARNING: Unmarshall will truncate payloads at 1000 bytes, this is done mainly to avoid denial of service // WARNING: UnmarshallBody will truncate payloads at 1000 bytes, this is done mainly to avoid denial of service
// - If you need to access the full payload, consider parsing VAA from Bytes instead of Unmarshal // - If you need to access the full payload, consider parsing VAA from Bytes instead of Unmarshal
func Unmarshal(data []byte) (*VAA, error) { func UnmarshalBody(data []byte, reader *bytes.Reader, v *VAA) (*VAA, error) {
if len(data) < minVAALength {
return nil, fmt.Errorf("VAA is too short")
}
v := &VAA{}
v.Version = data[0]
if v.Version != SupportedVAAVersion {
return nil, fmt.Errorf("unsupported VAA version: %d", v.Version)
}
reader := bytes.NewReader(data[1:])
if err := binary.Read(reader, binary.BigEndian, &v.GuardianSetIndex); err != nil {
return nil, fmt.Errorf("failed to read guardian set index: %w", err)
}
lenSignatures, er := reader.ReadByte()
if er != nil {
return nil, fmt.Errorf("failed to read signature length")
}
v.Signatures = make([]*Signature, lenSignatures)
for i := 0; i < int(lenSignatures); i++ {
index, err := reader.ReadByte()
if err != nil {
return nil, fmt.Errorf("failed to read validator index [%d]", i)
}
signature := [65]byte{}
if n, err := reader.Read(signature[:]); err != nil || n != 65 {
return nil, fmt.Errorf("failed to read signature [%d]: %w", i, err)
}
v.Signatures[i] = &Signature{
Index: index,
Signature: signature,
}
}
unixSeconds := uint32(0) unixSeconds := uint32(0)
if err := binary.Read(reader, binary.BigEndian, &unixSeconds); err != nil { if err := binary.Read(reader, binary.BigEndian, &unixSeconds); err != nil {
return nil, fmt.Errorf("failed to read timestamp: %w", err) return nil, fmt.Errorf("failed to read timestamp: %w", err)
@ -385,32 +403,234 @@ func Unmarshal(data []byte) (*VAA, error) {
return v, nil return v, nil
} }
// Unmarshal deserializes the binary representation of a VAA
func Unmarshal(data []byte) (*VAA, error) {
if len(data) < minVAALength {
return nil, fmt.Errorf("VAA is too short")
}
v := &VAA{}
v.Version = data[0]
if v.Version != SupportedVAAVersion {
return nil, fmt.Errorf("unsupported VAA version: %d", v.Version)
}
reader := bytes.NewReader(data[1:])
if err := binary.Read(reader, binary.BigEndian, &v.GuardianSetIndex); err != nil {
return nil, fmt.Errorf("failed to read guardian set index: %w", err)
}
lenSignatures, er := reader.ReadByte()
if er != nil {
return nil, fmt.Errorf("failed to read signature length")
}
v.Signatures = make([]*Signature, lenSignatures)
for i := 0; i < int(lenSignatures); i++ {
index, err := reader.ReadByte()
if err != nil {
return nil, fmt.Errorf("failed to read validator index [%d]", i)
}
signature := [65]byte{}
if n, err := reader.Read(signature[:]); err != nil || n != 65 {
return nil, fmt.Errorf("failed to read signature [%d]: %w", i, err)
}
v.Signatures[i] = &Signature{
Index: index,
Signature: signature,
}
}
return UnmarshalBody(data, reader, v)
}
// UnmarshalBatch deserializes the binary representation of a BatchVAA
func UnmarshalBatch(data []byte) (*BatchVAA, error) {
if len(data) < minBatchVAALength {
return nil, fmt.Errorf("BatchVAA.Observation is too short")
}
v := &BatchVAA{}
v.Version = data[0]
if v.Version != BatchVAAVersion {
return nil, fmt.Errorf("unsupported VAA version: %d", v.Version)
}
reader := bytes.NewReader(data[1:])
if err := binary.Read(reader, binary.BigEndian, &v.GuardianSetIndex); err != nil {
return nil, fmt.Errorf("failed to read guardian set index: %w", err)
}
lenSignatures, er := reader.ReadByte()
if er != nil {
return nil, fmt.Errorf("failed to read signature length")
}
v.Signatures = make([]*Signature, int(lenSignatures))
for i := 0; i < int(lenSignatures); i++ {
index, err := reader.ReadByte()
if err != nil {
return nil, fmt.Errorf("failed to read validator index [%d]", i)
}
signature := [65]byte{}
if n, err := reader.Read(signature[:]); err != nil || n != 65 {
return nil, fmt.Errorf("failed to read signature [%d]: %w", i, err)
}
v.Signatures[i] = &Signature{
Index: uint8(index),
Signature: signature,
}
}
lenHashes, err := reader.ReadByte()
if err != nil {
return nil, fmt.Errorf("failed to read hashes length [%w]", err)
}
numHashes := int(lenHashes)
v.Hashes = make([]common.Hash, numHashes)
for i := 0; i < int(lenHashes); i++ {
hash := [32]byte{}
if n, err := reader.Read(hash[:]); err != nil || n != 32 {
return nil, fmt.Errorf("failed to read hash [%d]: %w", i, err)
}
v.Hashes[i] = common.BytesToHash(hash[:])
}
lenObservations, err := reader.ReadByte()
if err != nil {
return nil, fmt.Errorf("failed to read observations length: %w", err)
}
numObservations := int(lenObservations)
if numHashes != numObservations {
// should never happen, check anyway
return nil, fmt.Errorf(
"failed unmarshaling BatchVAA, observations differs from hashes")
}
v.Observations = make([]*Observation, numObservations)
for i := 0; i < int(lenObservations); i++ {
index, err := reader.ReadByte()
if err != nil {
return nil, fmt.Errorf("failed to read Observation index [%d]: %w", i, err)
}
obsvIndex := uint8(index)
obsvLength := uint32(0)
if err := binary.Read(reader, binary.BigEndian, &obsvLength); err != nil {
return nil, fmt.Errorf("failed to read Observation length: %w", err)
}
numBytes := int(obsvLength)
// ensure numBytes is within expected bounds before allocating arrays
// cannot be negative
if numBytes < 0 {
return nil, fmt.Errorf(
"failed to read Observation index: %v, byte length is negative", i)
}
// cannot be longer than what is left in the array
if numBytes > reader.Len() {
return nil, fmt.Errorf(
"failed to read Observation index: %v, byte length is erroneous", i)
}
obs := make([]byte, numBytes)
if n, err := reader.Read(obs[:]); err != nil || n == 0 {
return nil, fmt.Errorf("failed to read Observation bytes [%d]: %w", n, err)
}
// ensure the observation meets the minimum length of headless VAAs
if len(obs) < minHeadlessVAALength {
return nil, fmt.Errorf(
"BatchVAA.Observation is too short. Index: %v", obsvIndex)
}
// decode the observation, which is just the "BODY" fields of a v1 VAA
headless, err := UnmarshalBody(data, bytes.NewReader(obs[:]), &VAA{})
if err != nil {
return nil, fmt.Errorf("failed to unmarshal Observation VAA. %w", err)
}
// check for malformed data - verify that the hash of the observation matches what was supplied
// the guardian has no interest in or use for observations after the batch has been signed, but still check
obsHash := headless.SigningMsg()
if obsHash != v.Hashes[obsvIndex] {
return nil, fmt.Errorf(
"BatchVAA Observation %v does not match supplied hash", obsvIndex)
}
v.Observations[i] = &Observation{
Index: obsvIndex,
Observation: headless,
}
}
return v, nil
}
// signingBody returns the binary representation of the data that is relevant for signing and verifying the VAA // signingBody returns the binary representation of the data that is relevant for signing and verifying the VAA
func (v *VAA) signingBody() []byte { func (v *VAA) signingBody() []byte {
return v.serializeBody() return v.serializeBody()
} }
// SigningMsg returns the hash of the signing body. This is used for signature generation and verification // signingBody returns the binary representation of the data that is relevant for signing and verifying the VAA
func (v *VAA) SigningMsg() common.Hash { func (v *BatchVAA) signingBody() []byte {
// In order to save space in the solana signature verification instruction, we hash twice so we only need to pass in buf := new(bytes.Buffer)
// the first hash (32 bytes) vs the full body data.
hash := crypto.Keccak256Hash(crypto.Keccak256Hash(v.signingBody()).Bytes()) // add the VAA version
return hash MustWrite(buf, binary.BigEndian, v.Version)
// create the hash array from the Observations of the BatchVAA
hashes := v.ObsvHashArray()
MustWrite(buf, binary.BigEndian, hashes)
return buf.Bytes()
} }
// VerifySignatures verifies the signature of the VAA given the signer addresses. // SigningMsg returns the hash of the signing body.
// Returns true if the signatures were verified successfully. func SigningMsg(data []byte) common.Hash {
func (v *VAA) VerifySignatures(addresses []common.Address) bool { // In order to save space in the solana signature verification instruction, we hash twice so we only need to pass in
if len(addresses) < len(v.Signatures) { // the first hash (32 bytes) vs the full body data.
return false return crypto.Keccak256Hash(crypto.Keccak256Hash(data).Bytes())
}
// SigningMsg returns the hash of the signing body. This is used for signature generation and verification
func (v *VAA) SigningMsg() common.Hash {
return SigningMsg(v.signingBody())
}
// SigningMsg returns the hash of the signing body. This is used for signature generation and verification
func (v *BatchVAA) SigningMsg() common.Hash {
return SigningMsg(v.signingBody())
}
// ObsvHashArray creates an array of hashes of Observation.
// hashes in the array have the index position of their Observation.Index.
func (v *BatchVAA) ObsvHashArray() []common.Hash {
hashes := make([]common.Hash, len(v.Observations))
for _, msg := range v.Observations {
obsIndex := msg.Index
hashes[obsIndex] = msg.Observation.SigningMsg()
} }
h := v.SigningMsg() return hashes
}
func VerifySignatures(data []byte, signatures []*Signature, addresses []common.Address) bool {
last_index := -1 last_index := -1
signing_addresses := []common.Address{} signing_addresses := []common.Address{}
for _, sig := range v.Signatures { for _, sig := range signatures {
if int(sig.Index) >= len(addresses) { if int(sig.Index) >= len(addresses) {
return false return false
} }
@ -422,7 +642,7 @@ func (v *VAA) VerifySignatures(addresses []common.Address) bool {
last_index = int(sig.Index) last_index = int(sig.Index)
// Get pubKey to determine who signers address // Get pubKey to determine who signers address
pubKey, err := crypto.Ecrecover(h.Bytes(), sig.Signature[:]) pubKey, err := crypto.Ecrecover(data, sig.Signature[:])
if err != nil { if err != nil {
return false return false
} }
@ -445,6 +665,69 @@ func (v *VAA) VerifySignatures(addresses []common.Address) bool {
return true return true
} }
// VerifySignatures verifies the signature of the VAA given the signer addresses.
// Returns true if the signatures were verified successfully.
func (v *VAA) VerifySignatures(addresses []common.Address) bool {
if len(addresses) < len(v.Signatures) {
return false
}
return VerifySignatures(v.SigningMsg().Bytes(), v.Signatures, addresses)
}
// VerifySignatures verifies the signature of the BatchVAA given the signer addresses.
// Returns true if the signatures were verified successfully.
func (v *BatchVAA) VerifySignatures(addresses []common.Address) bool {
if len(addresses) < len(v.Signatures) {
return false
}
return VerifySignatures(v.SigningMsg().Bytes(), v.Signatures, addresses)
}
// Marshal returns the binary representation of the BatchVAA
func (v *BatchVAA) Marshal() ([]byte, error) {
buf := new(bytes.Buffer)
MustWrite(buf, binary.BigEndian, v.Version)
MustWrite(buf, binary.BigEndian, v.GuardianSetIndex)
// Write signatures
MustWrite(buf, binary.BigEndian, uint8(len(v.Signatures)))
for _, sig := range v.Signatures {
MustWrite(buf, binary.BigEndian, sig.Index)
buf.Write(sig.Signature[:])
}
// Write Body
buf.Write(v.serializeBody())
return buf.Bytes(), nil
}
// Serializes the body of the BatchVAA.
func (v *BatchVAA) serializeBody() []byte {
buf := new(bytes.Buffer)
hashes := v.ObsvHashArray()
MustWrite(buf, binary.BigEndian, uint8(len(hashes)))
MustWrite(buf, binary.BigEndian, hashes)
MustWrite(buf, binary.BigEndian, uint8(len(v.Observations)))
for _, obsv := range v.Observations {
MustWrite(buf, binary.BigEndian, uint8(obsv.Index))
obsvBytes := obsv.Observation.serializeBody()
lenBytes := len(obsvBytes)
MustWrite(buf, binary.BigEndian, uint32(lenBytes))
buf.Write(obsvBytes)
}
return buf.Bytes()
}
// Verify is a function on the VAA that takes a complete set of guardian keys as input and attempts certain checks with respect to this guardian. // Verify is a function on the VAA that takes a complete set of guardian keys as input and attempts certain checks with respect to this guardian.
// Verify will return nil if the VAA passes checks. Otherwise, Verify will return an error containing the text of the first check to fail. // Verify will return nil if the VAA passes checks. Otherwise, Verify will return an error containing the text of the first check to fail.
// NOTE: Verify will not work correctly if a subset of the guardian set keys is passed in. The complete guardian set must be passed in. // NOTE: Verify will not work correctly if a subset of the guardian set keys is passed in. The complete guardian set must be passed in.
@ -495,16 +778,81 @@ func (v *VAA) Marshal() ([]byte, error) {
return buf.Bytes(), nil return buf.Bytes(), nil
} }
// implement encoding.BinaryMarshaler interface for the VAA struct
func (v VAA) MarshalBinary() ([]byte, error) {
return v.Marshal()
}
// implement encoding.BinaryUnmarshaler interface for the VAA struct
func (v *VAA) UnmarshalBinary(data []byte) error {
vaa, err := Unmarshal(data)
if err != nil {
return err
}
// derefernce the stuct created by Unmarshal, and assign it to the method's context
*v = *vaa
return nil
}
// implement encoding.BinaryMarshaler interface for BatchVAA struct
func (b BatchVAA) MarshalBinary() ([]byte, error) {
return b.Marshal()
}
// implement encoding.BinaryUnmarshaler interface for BatchVAA struct
func (b *BatchVAA) UnmarshalBinary(data []byte) error {
batch, err := UnmarshalBatch(data)
if err != nil {
return err
}
// derefernce the stuct created by Unmarshal, and assign it to the method's context
*b = *batch
return nil
}
// MessageID returns a human-readable emitter_chain/emitter_address/sequence tuple. // MessageID returns a human-readable emitter_chain/emitter_address/sequence tuple.
func (v *VAA) MessageID() string { func (v *VAA) MessageID() string {
return fmt.Sprintf("%d/%s/%d", v.EmitterChain, v.EmitterAddress, v.Sequence) return fmt.Sprintf("%d/%s/%d", v.EmitterChain, v.EmitterAddress, v.Sequence)
} }
// BatchID returns a human-readable emitter_chain/transaction_hex
func (v *BatchVAA) BatchID() string {
if len(v.Observations) == 0 {
// cant have a batch without Observations, but check just be safe
panic("Cannot create a BatchID from BatchVAA with no Observations.")
}
nonce := v.Observations[0].Observation.Nonce
return fmt.Sprintf("%d/%s/%d", v.EmitterChain, hex.EncodeToString(v.TransactionID.Bytes()), nonce)
}
// UniqueID normalizes the ID of the VAA (any type) for the Attestation interface
// UniqueID returns the MessageID that uniquely identifies the Attestation
func (v *VAA) UniqueID() string {
return v.MessageID()
}
// UniqueID returns the BatchID that uniquely identifies the Attestation
func (b *BatchVAA) UniqueID() string {
return b.BatchID()
}
// GetTransactionID implements the processor.Batch interface for *BatchVAA.
func (v *BatchVAA) GetTransactionID() common.Hash {
return v.TransactionID
}
// HexDigest returns the hex-encoded digest. // HexDigest returns the hex-encoded digest.
func (v *VAA) HexDigest() string { func (v *VAA) HexDigest() string {
return hex.EncodeToString(v.SigningMsg().Bytes()) return hex.EncodeToString(v.SigningMsg().Bytes())
} }
// HexDigest returns the hex-encoded digest.
func (b *BatchVAA) HexDigest() string {
return hex.EncodeToString(b.SigningMsg().Bytes())
}
/* /*
SECURITY: Do not change this code! Changing it could result in two different hashes for SECURITY: Do not change this code! Changing it could result in two different hashes for
the same observation. But xDapps rely on the hash of an observation for replay protection. the same observation. But xDapps rely on the hash of an observation for replay protection.
@ -536,6 +884,22 @@ func (v *VAA) AddSignature(key *ecdsa.PrivateKey, index uint8) {
}) })
} }
// creates signature of BatchVAA.Hashes and adds it to BatchVAA.Signatures.
func (v *BatchVAA) AddSignature(key *ecdsa.PrivateKey, index uint8) {
sig, err := crypto.Sign(v.SigningMsg().Bytes(), key)
if err != nil {
panic(err)
}
sigData := [65]byte{}
copy(sigData[:], sig)
v.Signatures = append(v.Signatures, &Signature{
Index: index,
Signature: sigData,
})
}
// NOTE: This function assumes that the caller has verified that the VAA is from the token bridge. // NOTE: This function assumes that the caller has verified that the VAA is from the token bridge.
func IsTransfer(payload []byte) bool { func IsTransfer(payload []byte) bool {
return (len(payload) > 0) && ((payload[0] == 1) || (payload[0] == 3)) return (len(payload) > 0) && ((payload[0] == 1) || (payload[0] == 3))
@ -593,6 +957,11 @@ func (v *VAA) GetEmitterChain() ChainID {
return v.EmitterChain return v.EmitterChain
} }
// GetEmitterChain implements the processor.Batch interface for *BatchVAA.
func (v *BatchVAA) GetEmitterChain() ChainID {
return v.EmitterChain
}
// MustWrite calls binary.Write and panics on errors // MustWrite calls binary.Write and panics on errors
func MustWrite(w io.Writer, order binary.ByteOrder, data interface{}) { func MustWrite(w io.Writer, order binary.ByteOrder, data interface{}) {
if err := binary.Write(w, order, data); err != nil { if err := binary.Write(w, order, data); err != nil {
@ -636,3 +1005,35 @@ func BytesToAddress(b []byte) (Address, error) {
copy(address[32-len(b):], b) copy(address[32-len(b):], b)
return address, nil return address, nil
} }
// StringToHash converts a hex-encoded string into a common.Hash
func StringToHash(value string) (common.Hash, error) {
var tx common.Hash
// Make sure we have enough to decode
if len(value) < 2 {
return tx, fmt.Errorf("value must be at least 1 byte")
}
// Trim any preceding "0x" to the address
value = strings.TrimPrefix(value, "0x")
res, err := hex.DecodeString(value)
if err != nil {
return tx, err
}
tx = common.BytesToHash(res)
return tx, nil
}
func BytesToHash(b []byte) (common.Hash, error) {
var hash common.Hash
if len(b) > 32 {
return hash, fmt.Errorf("value must be no more than 32 bytes")
}
hash = common.BytesToHash(b)
return hash, nil
}