package vaa import ( "bytes" "crypto/ecdsa" "encoding/binary" "encoding/hex" "fmt" "io" "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" ) type ( // VAA is a verifiable action approval of the Wormhole protocol VAA 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 // Timestamp when the VAA was created Timestamp time.Time // Nonce of the VAA Nonce uint32 // Sequence of the VAA Sequence uint64 /// ConsistencyLevel of the VAA ConsistencyLevel uint8 // EmitterChain the VAA was emitted on EmitterChain ChainID // EmitterAddress of the contract that emitted the Message EmitterAddress Address // Payload of the message Payload []byte } // ChainID of a Wormhole chain ChainID uint16 // Action of a VAA Action uint8 // Address is a Wormhole protocol address, it contains the native chain's address. If the address data type of a // chain is < 32bytes the value is zero-padded on the left. Address [32]byte // Signature of a single guardian Signature struct { // Index of the validator Index uint8 // Signature data Signature SignatureData } SignatureData [65]byte ) func (a Address) MarshalJSON() ([]byte, error) { return []byte(fmt.Sprintf(`"%s"`, a)), nil } func (a Address) String() string { return hex.EncodeToString(a[:]) } func (a SignatureData) MarshalJSON() ([]byte, error) { return []byte(fmt.Sprintf(`"%s"`, a)), nil } func (a SignatureData) String() string { return hex.EncodeToString(a[:]) } func (c ChainID) String() string { switch c { case ChainIDSolana: return "solana" case ChainIDEthereum: return "ethereum" case ChainIDTerra: return "terra" case ChainIDBSC: return "bsc" default: return fmt.Sprintf("unknown chain ID: %d", c) } } const ( // ChainIDSolana is the ChainID of Solana ChainIDSolana ChainID = 1 // ChainIDEthereum is the ChainID of Ethereum ChainIDEthereum ChainID = 2 // ChainIDTerra is the ChainID of Terra ChainIDTerra ChainID = 3 // ChainIDBSC is the ChainID of Binance Smart Chain ChainIDBSC ChainID = 4 minVAALength = 1 + 4 + 52 + 4 + 1 + 1 SupportedVAAVersion = 0x01 ) // 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, } } unixSeconds := uint32(0) if err := binary.Read(reader, binary.BigEndian, &unixSeconds); err != nil { return nil, fmt.Errorf("failed to read timestamp: %w", err) } v.Timestamp = time.Unix(int64(unixSeconds), 0) if err := binary.Read(reader, binary.BigEndian, &v.Nonce); err != nil { return nil, fmt.Errorf("failed to read nonce: %w", err) } if err := binary.Read(reader, binary.BigEndian, &v.EmitterChain); err != nil { return nil, fmt.Errorf("failed to read emitter chain: %w", err) } emitterAddress := Address{} if n, err := reader.Read(emitterAddress[:]); err != nil || n != 32 { return nil, fmt.Errorf("failed to read emitter address [%d]: %w", n, err) } v.EmitterAddress = emitterAddress if err := binary.Read(reader, binary.BigEndian, &v.Sequence); err != nil { return nil, fmt.Errorf("failed to read sequence: %w", err) } if err := binary.Read(reader, binary.BigEndian, &v.ConsistencyLevel); err != nil { return nil, fmt.Errorf("failed to read commitment: %w", err) } payload := make([]byte, 1000) n, err := reader.Read(payload) if err != nil || n == 0 { return nil, fmt.Errorf("failed to read payload [%d]: %w", n, err) } v.Payload = payload[:n] 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, error) { 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, error) { body, err := v.signingBody() if err != nil { // Should never happen on a successfully parsed VAA return common.Hash{}, fmt.Errorf("failed to serialize signing body: %w", err) } // 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(body).Bytes()) return hash, nil } // VerifySignature verifies the signature of the VAA given the signer addresses. func (v *VAA) VerifySignatures(addresses []common.Address) bool { if len(addresses) < len(v.Signatures) { return false } h, err := v.SigningMsg() if err != nil { return false } for _, sig := range v.Signatures { if int(sig.Index) >= len(addresses) { return false } pubKey, err := crypto.Ecrecover(h.Bytes(), sig.Signature[:]) if err != nil { return false } addr := common.BytesToAddress(crypto.Keccak256(pubKey[1:])[12:]) if addr != addresses[sig.Index] { return false } } return true } // Marshal returns the binary representation of the VAA func (v *VAA) 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 body, err := v.serializeBody() if err != nil { return nil, fmt.Errorf("failed to serialize body: %w", err) } buf.Write(body) return buf.Bytes(), 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) } func (v *VAA) serializeBody() ([]byte, error) { buf := new(bytes.Buffer) MustWrite(buf, binary.BigEndian, uint32(v.Timestamp.Unix())) MustWrite(buf, binary.BigEndian, v.Nonce) MustWrite(buf, binary.BigEndian, v.EmitterChain) buf.Write(v.EmitterAddress[:]) MustWrite(buf, binary.BigEndian, v.Sequence) MustWrite(buf, binary.BigEndian, v.ConsistencyLevel) buf.Write(v.Payload) return buf.Bytes(), nil } func (v *VAA) AddSignature(key *ecdsa.PrivateKey, index uint8) { data, err := v.SigningMsg() if err != nil { panic(err) } sig, err := crypto.Sign(data.Bytes(), key) if err != nil { panic(err) } sigData := [65]byte{} copy(sigData[:], sig) v.Signatures = append(v.Signatures, &Signature{ Index: index, Signature: sigData, }) } // 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 { panic(fmt.Errorf("failed to write binary data: %v", data).Error()) } }