diff --git a/sdk/vaa/structs.go b/sdk/vaa/structs.go index 0239dc60c..ddb5b14e8 100644 --- a/sdk/vaa/structs.go +++ b/sdk/vaa/structs.go @@ -3,6 +3,7 @@ package vaa import ( "bytes" "crypto/ecdsa" + "encoding" "encoding/binary" "encoding/hex" "errors" @@ -42,6 +43,27 @@ type ( 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 uint16 // Action of a VAA @@ -61,6 +83,13 @@ type ( SignatureData [65]byte + Observation struct { + // Index of the observation in a Batch array + Index uint8 + // Signed Observation data + Observation *VAA + } + TransferPayloadHdr struct { Type uint8 Amount *big.Int @@ -69,6 +98,20 @@ type ( TargetAddress Address 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 ( @@ -287,60 +330,35 @@ const ( // - sequence (8 bytes) // - consistency level (1 byte) // - payload (0 bytes) - // - // From Above: 1 + 4 + 1 + 0 + 4 + 4 + 2 + 32 + 8 + 1 + 0 // Equals 57 + // BATCH + // - 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 - minVAALength = 57 + minHeadlessVAALength = 51 // HEADER + minVAALength = 57 // HEADER + BODY + minBatchVAALength = 94 // HEADER + BATCH + SupportedVAAVersion = 0x01 + BatchVAAVersion = 0x02 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 -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, - } - } - +func UnmarshalBody(data []byte, reader *bytes.Reader, v *VAA) (*VAA, error) { unixSeconds := uint32(0) if err := binary.Read(reader, binary.BigEndian, &unixSeconds); err != nil { return nil, fmt.Errorf("failed to read timestamp: %w", err) @@ -385,32 +403,234 @@ func Unmarshal(data []byte) (*VAA, error) { 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 func (v *VAA) signingBody() []byte { return v.serializeBody() } -// SigningMsg returns the hash of the signing body. This is used for signature generation and verification -func (v *VAA) SigningMsg() common.Hash { - // In order to save space in the solana signature verification instruction, we hash twice so we only need to pass in - // the first hash (32 bytes) vs the full body data. - hash := crypto.Keccak256Hash(crypto.Keccak256Hash(v.signingBody()).Bytes()) - return hash +// signingBody returns the binary representation of the data that is relevant for signing and verifying the VAA +func (v *BatchVAA) signingBody() []byte { + buf := new(bytes.Buffer) + + // add the VAA version + 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. -// Returns true if the signatures were verified successfully. -func (v *VAA) VerifySignatures(addresses []common.Address) bool { - if len(addresses) < len(v.Signatures) { - return false +// SigningMsg returns the hash of the signing body. +func SigningMsg(data []byte) common.Hash { + // In order to save space in the solana signature verification instruction, we hash twice so we only need to pass in + // the first hash (32 bytes) vs the full body data. + 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 signing_addresses := []common.Address{} - for _, sig := range v.Signatures { + for _, sig := range signatures { if int(sig.Index) >= len(addresses) { return false } @@ -422,7 +642,7 @@ func (v *VAA) VerifySignatures(addresses []common.Address) bool { last_index = int(sig.Index) // 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 { return false } @@ -445,6 +665,69 @@ func (v *VAA) VerifySignatures(addresses []common.Address) bool { 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 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. @@ -495,16 +778,81 @@ func (v *VAA) Marshal() ([]byte, error) { 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. func (v *VAA) MessageID() string { 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. func (v *VAA) HexDigest() string { 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 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. func IsTransfer(payload []byte) bool { return (len(payload) > 0) && ((payload[0] == 1) || (payload[0] == 3)) @@ -593,6 +957,11 @@ func (v *VAA) GetEmitterChain() ChainID { 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 func MustWrite(w io.Writer, order binary.ByteOrder, data interface{}) { 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) 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 +}