package query import ( "bytes" "encoding/binary" "fmt" "math" "strings" "github.com/certusone/wormhole/node/pkg/common" gossipv1 "github.com/certusone/wormhole/node/pkg/proto/gossip/v1" "github.com/wormhole-foundation/wormhole/sdk/vaa" ethCommon "github.com/ethereum/go-ethereum/common" ethCrypto "github.com/ethereum/go-ethereum/crypto" solana "github.com/gagliardetto/solana-go" ) // MSG_VERSION is the current version of the CCQ message protocol. const MSG_VERSION uint8 = 1 // QueryRequest defines a cross chain query request to be submitted to the guardians. // It is the payload of the SignedQueryRequest gossip message. type QueryRequest struct { Nonce uint32 PerChainQueries []*PerChainQueryRequest } // PerChainQueryRequest represents a query request for a single chain. type PerChainQueryRequest struct { // ChainId indicates which chain this query is destine for. ChainId vaa.ChainID // Query is the chain specific query data. Query ChainSpecificQuery } // ChainSpecificQuery is the interface that must be implemented by a chain specific query. type ChainSpecificQuery interface { Type() ChainSpecificQueryType Marshal() ([]byte, error) Unmarshal(data []byte) error UnmarshalFromReader(reader *bytes.Reader) error Validate() error } // ChainSpecificQueryType is used to interpret the data in a per chain query request. type ChainSpecificQueryType uint8 // EthCallQueryRequestType is the type of an EVM eth_call query request. const EthCallQueryRequestType ChainSpecificQueryType = 1 // EthCallQueryRequest implements ChainSpecificQuery for an EVM eth_call query request. type EthCallQueryRequest struct { // BlockId identifies the block to be queried. It must be a hex string starting with 0x. It may be a block number or a block hash. BlockId string // CallData is an array of specific queries to be performed on the specified block, in a single RPC call. CallData []*EthCallData } func (ecr *EthCallQueryRequest) CallDataList() []*EthCallData { return ecr.CallData } // EthCallByTimestampQueryRequestType is the type of an EVM eth_call_by_timestamp query request. const EthCallByTimestampQueryRequestType ChainSpecificQueryType = 2 // EthCallByTimestampQueryRequest implements ChainSpecificQuery for an EVM eth_call_by_timestamp query request. type EthCallByTimestampQueryRequest struct { // TargetTimeInUs specifies the desired timestamp in microseconds. TargetTimestamp uint64 // TargetBlockIdHint is optional. If specified, it identifies the block prior to the desired timestamp. It must be a hex string starting with 0x. It may be a block number or a block hash. TargetBlockIdHint string // FollowingBlockIdHint is optional. If specified, it identifies the block immediately following the desired timestamp. It must be a hex string starting with 0x. It may be a block number or a block hash. FollowingBlockIdHint string // CallData is an array of specific queries to be performed on the specified block, in a single RPC call. CallData []*EthCallData } func (ecr *EthCallByTimestampQueryRequest) CallDataList() []*EthCallData { return ecr.CallData } // EthCallWithFinalityQueryRequestType is the type of an EVM eth_call_with_finality query request. const EthCallWithFinalityQueryRequestType ChainSpecificQueryType = 3 // EthCallWithFinalityQueryRequest implements ChainSpecificQuery for an EVM eth_call_with_finality query request. type EthCallWithFinalityQueryRequest struct { // BlockId identifies the block to be queried. It must be a hex string starting with 0x. It may be a block number or a block hash. BlockId string // Finality is required. It identifies the level of finality the block must reach before the query is performed. Valid values are "finalized" and "safe". Finality string // CallData is an array of specific queries to be performed on the specified block, in a single RPC call. CallData []*EthCallData } func (ecr *EthCallWithFinalityQueryRequest) CallDataList() []*EthCallData { return ecr.CallData } // EthCallData specifies the parameters to a single EVM eth_call request. type EthCallData struct { // To specifies the contract address to be queried. To []byte // Data is the ABI encoded parameters to the query. Data []byte } const EvmContractAddressLength = 20 ////////////////////////////////// Solana Queries //////////////////////////////////////////////// // SolanaAccountQueryRequestType is the type of a Solana sol_account query request. const SolanaAccountQueryRequestType ChainSpecificQueryType = 4 // SolanaAccountQueryRequest implements ChainSpecificQuery for a Solana sol_account query request. type SolanaAccountQueryRequest struct { // Commitment identifies the commitment level to be used in the queried. Currently it may only "finalized". // Before we can support "confirmed", we need a way to read the account data and the block information atomically. // We would also need to deal with the fact that queries are only handled in the finalized watcher and it does not // have access to the latest confirmed slot needed for MinContextSlot retries. Commitment string // The minimum slot that the request can be evaluated at. Zero means unused. MinContextSlot uint64 // The offset of the start of data to be returned. Unused if DataSliceLength is zero. DataSliceOffset uint64 // The length of the data to be returned. Zero means all data is returned. DataSliceLength uint64 // Accounts is an array of accounts to be queried. Accounts [][SolanaPublicKeyLength]byte } // Solana public keys are fixed length. const SolanaPublicKeyLength = solana.PublicKeyLength // According to the Solana spec, the longest comment string is nine characters. Allow a few more, just in case. // https://pkg.go.dev/github.com/gagliardetto/solana-go/rpc#CommitmentType const SolanaMaxCommitmentLength = 12 // According to the spec, the query only supports up to 100 accounts. // https://github.com/solana-labs/solana/blob/9d132441fdc6282a8be4bff0bc77d6a2fefe8b59/rpc-client-api/src/request.rs#L204 const SolanaMaxAccountsPerQuery = 100 func (saq *SolanaAccountQueryRequest) AccountList() [][SolanaPublicKeyLength]byte { return saq.Accounts } // SolanaPdaQueryRequestType is the type of a Solana sol_pda query request. const SolanaPdaQueryRequestType ChainSpecificQueryType = 5 // SolanaPdaQueryRequest implements ChainSpecificQuery for a Solana sol_pda query request. type SolanaPdaQueryRequest struct { // Commitment identifies the commitment level to be used in the queried. Currently it may only "finalized". // Before we can support "confirmed", we need a way to read the account data and the block information atomically. // We would also need to deal with the fact that queries are only handled in the finalized watcher and it does not // have access to the latest confirmed slot needed for MinContextSlot retries. Commitment string // The minimum slot that the request can be evaluated at. Zero means unused. MinContextSlot uint64 // The offset of the start of data to be returned. Unused if DataSliceLength is zero. DataSliceOffset uint64 // The length of the data to be returned. Zero means all data is returned. DataSliceLength uint64 // PDAs is an array of PDAs to be queried. PDAs []SolanaPDAEntry } // SolanaPDAEntry defines a single Solana Program derived address (PDA). type SolanaPDAEntry struct { ProgramAddress [SolanaPublicKeyLength]byte Seeds [][]byte } // According to the spec, there may be at most 16 seeds. // https://github.com/gagliardetto/solana-go/blob/6fe3aea02e3660d620433444df033fc3fe6e64c1/keys.go#L559 const SolanaMaxSeeds = solana.MaxSeeds // According to the spec, a seed may be at most 32 bytes. // https://github.com/gagliardetto/solana-go/blob/6fe3aea02e3660d620433444df033fc3fe6e64c1/keys.go#L557 const SolanaMaxSeedLen = solana.MaxSeedLength func (spda *SolanaPdaQueryRequest) PDAList() []SolanaPDAEntry { return spda.PDAs } // PerChainQueryInternal is an internal representation of a query request that is passed to the watcher. type PerChainQueryInternal struct { RequestID string RequestIdx int Request *PerChainQueryRequest } func (pcqi *PerChainQueryInternal) ID() string { return fmt.Sprintf("%s:%d", pcqi.RequestID, pcqi.RequestIdx) } // QueryRequestDigest returns the query signing prefix based on the environment. func QueryRequestDigest(env common.Environment, b []byte) ethCommon.Hash { var queryRequestPrefix []byte if env == common.MainNet { queryRequestPrefix = []byte("mainnet_query_request_000000000000|") } else if env == common.TestNet { queryRequestPrefix = []byte("testnet_query_request_000000000000|") } else { queryRequestPrefix = []byte("devnet_query_request_0000000000000|") } return ethCrypto.Keccak256Hash(append(queryRequestPrefix, b...)) } // PostSignedQueryRequest posts a signed query request to the specified channel. func PostSignedQueryRequest(signedQueryReqSendC chan<- *gossipv1.SignedQueryRequest, req *gossipv1.SignedQueryRequest) error { select { case signedQueryReqSendC <- req: return nil default: return common.ErrChanFull } } func SignedQueryRequestEqual(left *gossipv1.SignedQueryRequest, right *gossipv1.SignedQueryRequest) bool { if !bytes.Equal(left.QueryRequest, right.QueryRequest) { return false } if !bytes.Equal(left.Signature, right.Signature) { return false } return true } // // Implementation of QueryRequest. // // Marshal serializes the binary representation of a query request. // This method calls Validate() and relies on it to range checks lengths, etc. func (queryRequest *QueryRequest) Marshal() ([]byte, error) { if err := queryRequest.Validate(); err != nil { return nil, err } buf := new(bytes.Buffer) vaa.MustWrite(buf, binary.BigEndian, MSG_VERSION) // version vaa.MustWrite(buf, binary.BigEndian, queryRequest.Nonce) // uint32 vaa.MustWrite(buf, binary.BigEndian, uint8(len(queryRequest.PerChainQueries))) for _, perChainQuery := range queryRequest.PerChainQueries { pcqBuf, err := perChainQuery.Marshal() if err != nil { return nil, fmt.Errorf("failed to marshal per chain query: %w", err) } buf.Write(pcqBuf) } return buf.Bytes(), nil } // Unmarshal deserializes the binary representation of a query request from a byte array func (queryRequest *QueryRequest) Unmarshal(data []byte) error { reader := bytes.NewReader(data[:]) return queryRequest.UnmarshalFromReader(reader) } // UnmarshalFromReader deserializes the binary representation of a query request from an existing reader func (queryRequest *QueryRequest) UnmarshalFromReader(reader *bytes.Reader) error { var version uint8 if err := binary.Read(reader, binary.BigEndian, &version); err != nil { return fmt.Errorf("failed to read message version: %w", err) } if version != MSG_VERSION { return fmt.Errorf("unsupported message version: %d", version) } if err := binary.Read(reader, binary.BigEndian, &queryRequest.Nonce); err != nil { return fmt.Errorf("failed to read request nonce: %w", err) } numPerChainQueries := uint8(0) if err := binary.Read(reader, binary.BigEndian, &numPerChainQueries); err != nil { return fmt.Errorf("failed to read number of per chain queries: %w", err) } for count := 0; count < int(numPerChainQueries); count++ { perChainQuery := PerChainQueryRequest{} err := perChainQuery.UnmarshalFromReader(reader) if err != nil { return fmt.Errorf("failed to Unmarshal per chain query: %w", err) } queryRequest.PerChainQueries = append(queryRequest.PerChainQueries, &perChainQuery) } if reader.Len() != 0 { return fmt.Errorf("excess bytes in unmarshal") } if err := queryRequest.Validate(); err != nil { return fmt.Errorf("unmarshaled request failed validation: %w", err) } return nil } // Validate does basic validation on a received query request. func (queryRequest *QueryRequest) Validate() error { // Nothing to validate on the Nonce. if len(queryRequest.PerChainQueries) <= 0 { return fmt.Errorf("request does not contain any per chain queries") } if len(queryRequest.PerChainQueries) > math.MaxUint8 { return fmt.Errorf("too many per chain queries") } for idx, perChainQuery := range queryRequest.PerChainQueries { if err := perChainQuery.Validate(); err != nil { return fmt.Errorf("failed to validate per chain query %d: %w", idx, err) } } return nil } // Equal verifies that two query requests are equal. func (left *QueryRequest) Equal(right *QueryRequest) bool { if left.Nonce != right.Nonce { return false } if len(left.PerChainQueries) != len(right.PerChainQueries) { return false } for idx := range left.PerChainQueries { if !left.PerChainQueries[idx].Equal(right.PerChainQueries[idx]) { return false } } return true } // // Implementation of PerChainQueryRequest. // // Marshal serializes the binary representation of a per chain query request. // This method calls Validate() and relies on it to range checks lengths, etc. func (perChainQuery *PerChainQueryRequest) Marshal() ([]byte, error) { if err := perChainQuery.Validate(); err != nil { return nil, err } buf := new(bytes.Buffer) vaa.MustWrite(buf, binary.BigEndian, perChainQuery.ChainId) vaa.MustWrite(buf, binary.BigEndian, perChainQuery.Query.Type()) queryBuf, err := perChainQuery.Query.Marshal() if err != nil { return nil, err } // Write the length of the query to facilitate on-chain parsing. if len(queryBuf) > math.MaxUint32 { return nil, fmt.Errorf("query too long") } vaa.MustWrite(buf, binary.BigEndian, uint32(len(queryBuf))) buf.Write(queryBuf) return buf.Bytes(), nil } // Unmarshal deserializes the binary representation of a per chain query request from a byte array func (perChainQuery *PerChainQueryRequest) Unmarshal(data []byte) error { reader := bytes.NewReader(data[:]) return perChainQuery.UnmarshalFromReader(reader) } // UnmarshalFromReader deserializes the binary representation of a per chain query request from an existing reader func (perChainQuery *PerChainQueryRequest) UnmarshalFromReader(reader *bytes.Reader) error { if err := binary.Read(reader, binary.BigEndian, &perChainQuery.ChainId); err != nil { return fmt.Errorf("failed to read request chain: %w", err) } qt := uint8(0) if err := binary.Read(reader, binary.BigEndian, &qt); err != nil { return fmt.Errorf("failed to read request type: %w", err) } queryType := ChainSpecificQueryType(qt) if err := ValidatePerChainQueryRequestType(queryType); err != nil { return err } // Skip the query length. var queryLength uint32 if err := binary.Read(reader, binary.BigEndian, &queryLength); err != nil { return fmt.Errorf("failed to read query length: %w", err) } switch queryType { case EthCallQueryRequestType: q := EthCallQueryRequest{} if err := q.UnmarshalFromReader(reader); err != nil { return fmt.Errorf("failed to unmarshal eth call request: %w", err) } perChainQuery.Query = &q case EthCallByTimestampQueryRequestType: q := EthCallByTimestampQueryRequest{} if err := q.UnmarshalFromReader(reader); err != nil { return fmt.Errorf("failed to unmarshal eth call by timestamp request: %w", err) } perChainQuery.Query = &q case EthCallWithFinalityQueryRequestType: q := EthCallWithFinalityQueryRequest{} if err := q.UnmarshalFromReader(reader); err != nil { return fmt.Errorf("failed to unmarshal eth call with finality request: %w", err) } perChainQuery.Query = &q case SolanaAccountQueryRequestType: q := SolanaAccountQueryRequest{} if err := q.UnmarshalFromReader(reader); err != nil { return fmt.Errorf("failed to unmarshal solana account query request: %w", err) } perChainQuery.Query = &q case SolanaPdaQueryRequestType: q := SolanaPdaQueryRequest{} if err := q.UnmarshalFromReader(reader); err != nil { return fmt.Errorf("failed to unmarshal solana PDA query request: %w", err) } perChainQuery.Query = &q default: return fmt.Errorf("unsupported query type: %d", queryType) } return nil } // Validate does basic validation on a per chain query request. func (perChainQuery *PerChainQueryRequest) Validate() error { str := perChainQuery.ChainId.String() if _, err := vaa.ChainIDFromString(str); err != nil { return fmt.Errorf("invalid chainID: %d", uint16(perChainQuery.ChainId)) } if perChainQuery.Query == nil { return fmt.Errorf("query is nil") } if err := ValidatePerChainQueryRequestType(perChainQuery.Query.Type()); err != nil { return err } if err := perChainQuery.Query.Validate(); err != nil { return fmt.Errorf("chain specific query is invalid: %w", err) } return nil } func ValidatePerChainQueryRequestType(qt ChainSpecificQueryType) error { if qt != EthCallQueryRequestType && qt != EthCallByTimestampQueryRequestType && qt != EthCallWithFinalityQueryRequestType && qt != SolanaAccountQueryRequestType && qt != SolanaPdaQueryRequestType { return fmt.Errorf("invalid query request type: %d", qt) } return nil } // Equal verifies that two query requests are equal. func (left *PerChainQueryRequest) Equal(right *PerChainQueryRequest) bool { if left.ChainId != right.ChainId { return false } if left.Query == nil && right.Query == nil { return true } if left.Query == nil || right.Query == nil { return false } if left.Query.Type() != right.Query.Type() { return false } switch leftQuery := left.Query.(type) { case *EthCallQueryRequest: switch rightQuery := right.Query.(type) { case *EthCallQueryRequest: return leftQuery.Equal(rightQuery) default: panic("unsupported query type on right, must be eth_call") } case *EthCallByTimestampQueryRequest: switch rightQuery := right.Query.(type) { case *EthCallByTimestampQueryRequest: return leftQuery.Equal(rightQuery) default: panic("unsupported query type on right, must be eth_call_by_timestamp") } case *EthCallWithFinalityQueryRequest: switch rightQuery := right.Query.(type) { case *EthCallWithFinalityQueryRequest: return leftQuery.Equal(rightQuery) default: panic("unsupported query type on right, must be eth_call_with_finality") } case *SolanaAccountQueryRequest: switch rightQuery := right.Query.(type) { case *SolanaAccountQueryRequest: return leftQuery.Equal(rightQuery) default: panic("unsupported query type on right, must be sol_account") } case *SolanaPdaQueryRequest: switch rightQuery := right.Query.(type) { case *SolanaPdaQueryRequest: return leftQuery.Equal(rightQuery) default: panic("unsupported query type on right, must be sol_pda") } default: panic("unsupported query type on left") } } // // Implementation of EthCallQueryRequest, which implements the ChainSpecificQuery interface. // func (e *EthCallQueryRequest) Type() ChainSpecificQueryType { return EthCallQueryRequestType } // Marshal serializes the binary representation of an EVM eth_call request. // This method calls Validate() and relies on it to range checks lengths, etc. func (ecd *EthCallQueryRequest) Marshal() ([]byte, error) { if err := ecd.Validate(); err != nil { return nil, err } buf := new(bytes.Buffer) vaa.MustWrite(buf, binary.BigEndian, uint32(len(ecd.BlockId))) buf.Write([]byte(ecd.BlockId)) vaa.MustWrite(buf, binary.BigEndian, uint8(len(ecd.CallData))) for _, callData := range ecd.CallData { buf.Write(callData.To) vaa.MustWrite(buf, binary.BigEndian, uint32(len(callData.Data))) buf.Write(callData.Data) } return buf.Bytes(), nil } // Unmarshal deserializes an EVM eth_call query from a byte array func (ecd *EthCallQueryRequest) Unmarshal(data []byte) error { reader := bytes.NewReader(data[:]) return ecd.UnmarshalFromReader(reader) } // UnmarshalFromReader deserializes an EVM eth_call query from a byte array func (ecd *EthCallQueryRequest) UnmarshalFromReader(reader *bytes.Reader) error { blockIdLen := uint32(0) if err := binary.Read(reader, binary.BigEndian, &blockIdLen); err != nil { return fmt.Errorf("failed to read block id len: %w", err) } blockId := make([]byte, blockIdLen) if n, err := reader.Read(blockId[:]); err != nil || n != int(blockIdLen) { return fmt.Errorf("failed to read block id [%d]: %w", n, err) } ecd.BlockId = string(blockId[:]) numCallData := uint8(0) if err := binary.Read(reader, binary.BigEndian, &numCallData); err != nil { return fmt.Errorf("failed to read number of call data entries: %w", err) } for count := 0; count < int(numCallData); count++ { to := [EvmContractAddressLength]byte{} if n, err := reader.Read(to[:]); err != nil || n != EvmContractAddressLength { return fmt.Errorf("failed to read call To [%d]: %w", n, err) } dataLen := uint32(0) if err := binary.Read(reader, binary.BigEndian, &dataLen); err != nil { return fmt.Errorf("failed to read call Data len: %w", err) } data := make([]byte, dataLen) if n, err := reader.Read(data[:]); err != nil || n != int(dataLen) { return fmt.Errorf("failed to read call data [%d]: %w", n, err) } callData := &EthCallData{ To: to[:], Data: data[:], } ecd.CallData = append(ecd.CallData, callData) } return nil } // Validate does basic validation on an EVM eth_call query. func (ecd *EthCallQueryRequest) Validate() error { if len(ecd.BlockId) > math.MaxUint32 { return fmt.Errorf("block id too long") } if !strings.HasPrefix(ecd.BlockId, "0x") { return fmt.Errorf("block id must be a hex number or hash starting with 0x") } if len(ecd.CallData) <= 0 { return fmt.Errorf("does not contain any call data") } if len(ecd.CallData) > math.MaxUint8 { return fmt.Errorf("too many call data entries") } for _, callData := range ecd.CallData { if callData.To == nil || len(callData.To) <= 0 { return fmt.Errorf("no call data to") } if len(callData.To) != EvmContractAddressLength { return fmt.Errorf("invalid length for To contract") } if callData.Data == nil || len(callData.Data) <= 0 { return fmt.Errorf("no call data data") } if len(callData.Data) > math.MaxUint32 { return fmt.Errorf("call data data too long") } } return nil } // Equal verifies that two EVM eth_call queries are equal. func (left *EthCallQueryRequest) Equal(right *EthCallQueryRequest) bool { if left.BlockId != right.BlockId { return false } if len(left.CallData) != len(right.CallData) { return false } for idx := range left.CallData { if !bytes.Equal(left.CallData[idx].To, right.CallData[idx].To) { return false } if !bytes.Equal(left.CallData[idx].Data, right.CallData[idx].Data) { return false } } return true } // // Implementation of EthCallByTimestampQueryRequest, which implements the ChainSpecificQuery interface. // func (e *EthCallByTimestampQueryRequest) Type() ChainSpecificQueryType { return EthCallByTimestampQueryRequestType } // Marshal serializes the binary representation of an EVM eth_call_by_timestamp request. // This method calls Validate() and relies on it to range checks lengths, etc. func (ecd *EthCallByTimestampQueryRequest) Marshal() ([]byte, error) { if err := ecd.Validate(); err != nil { return nil, err } buf := new(bytes.Buffer) vaa.MustWrite(buf, binary.BigEndian, ecd.TargetTimestamp) vaa.MustWrite(buf, binary.BigEndian, uint32(len(ecd.TargetBlockIdHint))) buf.Write([]byte(ecd.TargetBlockIdHint)) vaa.MustWrite(buf, binary.BigEndian, uint32(len(ecd.FollowingBlockIdHint))) buf.Write([]byte(ecd.FollowingBlockIdHint)) vaa.MustWrite(buf, binary.BigEndian, uint8(len(ecd.CallData))) for _, callData := range ecd.CallData { buf.Write(callData.To) vaa.MustWrite(buf, binary.BigEndian, uint32(len(callData.Data))) buf.Write(callData.Data) } return buf.Bytes(), nil } // Unmarshal deserializes an EVM eth_call_by_timestamp query from a byte array func (ecd *EthCallByTimestampQueryRequest) Unmarshal(data []byte) error { reader := bytes.NewReader(data[:]) return ecd.UnmarshalFromReader(reader) } // UnmarshalFromReader deserializes an EVM eth_call_by_timestamp query from a byte array func (ecd *EthCallByTimestampQueryRequest) UnmarshalFromReader(reader *bytes.Reader) error { if err := binary.Read(reader, binary.BigEndian, &ecd.TargetTimestamp); err != nil { return fmt.Errorf("failed to read timestamp: %w", err) } blockIdHintLen := uint32(0) if err := binary.Read(reader, binary.BigEndian, &blockIdHintLen); err != nil { return fmt.Errorf("failed to read target block id hint len: %w", err) } targetBlockIdHint := make([]byte, blockIdHintLen) if n, err := reader.Read(targetBlockIdHint[:]); err != nil || n != int(blockIdHintLen) { return fmt.Errorf("failed to read target block id hint [%d]: %w", n, err) } ecd.TargetBlockIdHint = string(targetBlockIdHint[:]) blockIdHintLen = uint32(0) if err := binary.Read(reader, binary.BigEndian, &blockIdHintLen); err != nil { return fmt.Errorf("failed to read following block id hint len: %w", err) } followingBlockIdHint := make([]byte, blockIdHintLen) if n, err := reader.Read(followingBlockIdHint[:]); err != nil || n != int(blockIdHintLen) { return fmt.Errorf("failed to read following block id hint [%d]: %w", n, err) } ecd.FollowingBlockIdHint = string(followingBlockIdHint[:]) numCallData := uint8(0) if err := binary.Read(reader, binary.BigEndian, &numCallData); err != nil { return fmt.Errorf("failed to read number of call data entries: %w", err) } for count := 0; count < int(numCallData); count++ { to := [EvmContractAddressLength]byte{} if n, err := reader.Read(to[:]); err != nil || n != EvmContractAddressLength { return fmt.Errorf("failed to read call To [%d]: %w", n, err) } dataLen := uint32(0) if err := binary.Read(reader, binary.BigEndian, &dataLen); err != nil { return fmt.Errorf("failed to read call Data len: %w", err) } data := make([]byte, dataLen) if n, err := reader.Read(data[:]); err != nil || n != int(dataLen) { return fmt.Errorf("failed to read call data [%d]: %w", n, err) } callData := &EthCallData{ To: to[:], Data: data[:], } ecd.CallData = append(ecd.CallData, callData) } return nil } // Validate does basic validation on an EVM eth_call_by_timestamp query. func (ecd *EthCallByTimestampQueryRequest) Validate() error { if ecd.TargetTimestamp == 0 { return fmt.Errorf("target timestamp may not be zero") } if len(ecd.TargetBlockIdHint) > math.MaxUint32 { return fmt.Errorf("target block id hint too long") } if (ecd.TargetBlockIdHint == "") != (ecd.FollowingBlockIdHint == "") { return fmt.Errorf("if either the target or following block id is unset, they both must be unset") } if ecd.TargetBlockIdHint != "" && !strings.HasPrefix(ecd.TargetBlockIdHint, "0x") { return fmt.Errorf("target block id must be a hex number or hash starting with 0x") } if len(ecd.FollowingBlockIdHint) > math.MaxUint32 { return fmt.Errorf("following block id hint too long") } if ecd.FollowingBlockIdHint != "" && !strings.HasPrefix(ecd.FollowingBlockIdHint, "0x") { return fmt.Errorf("following block id must be a hex number or hash starting with 0x") } if len(ecd.CallData) <= 0 { return fmt.Errorf("does not contain any call data") } if len(ecd.CallData) > math.MaxUint8 { return fmt.Errorf("too many call data entries") } for _, callData := range ecd.CallData { if callData.To == nil || len(callData.To) <= 0 { return fmt.Errorf("no call data to") } if len(callData.To) != EvmContractAddressLength { return fmt.Errorf("invalid length for To contract") } if callData.Data == nil || len(callData.Data) <= 0 { return fmt.Errorf("no call data data") } if len(callData.Data) > math.MaxUint32 { return fmt.Errorf("call data data too long") } } return nil } // Equal verifies that two EVM eth_call_by_timestamp queries are equal. func (left *EthCallByTimestampQueryRequest) Equal(right *EthCallByTimestampQueryRequest) bool { if left.TargetTimestamp != right.TargetTimestamp { return false } if left.TargetBlockIdHint != right.TargetBlockIdHint { return false } if left.FollowingBlockIdHint != right.FollowingBlockIdHint { return false } if len(left.CallData) != len(right.CallData) { return false } for idx := range left.CallData { if !bytes.Equal(left.CallData[idx].To, right.CallData[idx].To) { return false } if !bytes.Equal(left.CallData[idx].Data, right.CallData[idx].Data) { return false } } return true } // // Implementation of EthCallWithFinalityQueryRequest, which implements the ChainSpecificQuery interface. // func (e *EthCallWithFinalityQueryRequest) Type() ChainSpecificQueryType { return EthCallWithFinalityQueryRequestType } // Marshal serializes the binary representation of an EVM eth_call_with_finality request. // This method calls Validate() and relies on it to range checks lengths, etc. func (ecd *EthCallWithFinalityQueryRequest) Marshal() ([]byte, error) { if err := ecd.Validate(); err != nil { return nil, err } buf := new(bytes.Buffer) vaa.MustWrite(buf, binary.BigEndian, uint32(len(ecd.BlockId))) buf.Write([]byte(ecd.BlockId)) vaa.MustWrite(buf, binary.BigEndian, uint32(len(ecd.Finality))) buf.Write([]byte(ecd.Finality)) vaa.MustWrite(buf, binary.BigEndian, uint8(len(ecd.CallData))) for _, callData := range ecd.CallData { buf.Write(callData.To) vaa.MustWrite(buf, binary.BigEndian, uint32(len(callData.Data))) buf.Write(callData.Data) } return buf.Bytes(), nil } // Unmarshal deserializes an EVM eth_call_with_finality query from a byte array func (ecd *EthCallWithFinalityQueryRequest) Unmarshal(data []byte) error { reader := bytes.NewReader(data[:]) return ecd.UnmarshalFromReader(reader) } // UnmarshalFromReader deserializes an EVM eth_call_with_finality query from a byte array func (ecd *EthCallWithFinalityQueryRequest) UnmarshalFromReader(reader *bytes.Reader) error { blockIdLen := uint32(0) if err := binary.Read(reader, binary.BigEndian, &blockIdLen); err != nil { return fmt.Errorf("failed to read target block id len: %w", err) } blockId := make([]byte, blockIdLen) if n, err := reader.Read(blockId[:]); err != nil || n != int(blockIdLen) { return fmt.Errorf("failed to read target block id [%d]: %w", n, err) } ecd.BlockId = string(blockId[:]) finalityLen := uint32(0) if err := binary.Read(reader, binary.BigEndian, &finalityLen); err != nil { return fmt.Errorf("failed to read finality len: %w", err) } finality := make([]byte, finalityLen) if n, err := reader.Read(finality[:]); err != nil || n != int(finalityLen) { return fmt.Errorf("failed to read finality [%d]: %w", n, err) } ecd.Finality = string(finality[:]) numCallData := uint8(0) if err := binary.Read(reader, binary.BigEndian, &numCallData); err != nil { return fmt.Errorf("failed to read number of call data entries: %w", err) } for count := 0; count < int(numCallData); count++ { to := [EvmContractAddressLength]byte{} if n, err := reader.Read(to[:]); err != nil || n != EvmContractAddressLength { return fmt.Errorf("failed to read call To [%d]: %w", n, err) } dataLen := uint32(0) if err := binary.Read(reader, binary.BigEndian, &dataLen); err != nil { return fmt.Errorf("failed to read call Data len: %w", err) } data := make([]byte, dataLen) if n, err := reader.Read(data[:]); err != nil || n != int(dataLen) { return fmt.Errorf("failed to read call data [%d]: %w", n, err) } callData := &EthCallData{ To: to[:], Data: data[:], } ecd.CallData = append(ecd.CallData, callData) } return nil } // Validate does basic validation on an EVM eth_call_with_finality query. func (ecd *EthCallWithFinalityQueryRequest) Validate() error { if len(ecd.BlockId) > math.MaxUint32 { return fmt.Errorf("block id too long") } if ecd.BlockId == "" { return fmt.Errorf("block id is required") } if !strings.HasPrefix(ecd.BlockId, "0x") { return fmt.Errorf("block id must be a hex number or hash starting with 0x") } if len(ecd.Finality) > math.MaxUint32 { return fmt.Errorf("finality too long") } if ecd.Finality == "" { return fmt.Errorf("finality is required") } if ecd.Finality != "finalized" && ecd.Finality != "safe" { return fmt.Errorf(`finality must be "finalized" or "safe", is "%s"`, ecd.Finality) } if len(ecd.CallData) <= 0 { return fmt.Errorf("does not contain any call data") } if len(ecd.CallData) > math.MaxUint8 { return fmt.Errorf("too many call data entries") } for _, callData := range ecd.CallData { if callData.To == nil || len(callData.To) <= 0 { return fmt.Errorf("no call data to") } if len(callData.To) != EvmContractAddressLength { return fmt.Errorf("invalid length for To contract") } if callData.Data == nil || len(callData.Data) <= 0 { return fmt.Errorf("no call data data") } if len(callData.Data) > math.MaxUint32 { return fmt.Errorf("call data data too long") } } return nil } // Equal verifies that two EVM eth_call_with_finality queries are equal. func (left *EthCallWithFinalityQueryRequest) Equal(right *EthCallWithFinalityQueryRequest) bool { if left.BlockId != right.BlockId { return false } if left.Finality != right.Finality { return false } if len(left.CallData) != len(right.CallData) { return false } for idx := range left.CallData { if !bytes.Equal(left.CallData[idx].To, right.CallData[idx].To) { return false } if !bytes.Equal(left.CallData[idx].Data, right.CallData[idx].Data) { return false } } return true } // // Implementation of SolanaAccountQueryRequest, which implements the ChainSpecificQuery interface. // func (e *SolanaAccountQueryRequest) Type() ChainSpecificQueryType { return SolanaAccountQueryRequestType } // Marshal serializes the binary representation of a Solana sol_account request. // This method calls Validate() and relies on it to range checks lengths, etc. func (saq *SolanaAccountQueryRequest) Marshal() ([]byte, error) { if err := saq.Validate(); err != nil { return nil, err } buf := new(bytes.Buffer) vaa.MustWrite(buf, binary.BigEndian, uint32(len(saq.Commitment))) buf.Write([]byte(saq.Commitment)) vaa.MustWrite(buf, binary.BigEndian, saq.MinContextSlot) vaa.MustWrite(buf, binary.BigEndian, saq.DataSliceOffset) vaa.MustWrite(buf, binary.BigEndian, saq.DataSliceLength) vaa.MustWrite(buf, binary.BigEndian, uint8(len(saq.Accounts))) for _, acct := range saq.Accounts { buf.Write(acct[:]) } return buf.Bytes(), nil } // Unmarshal deserializes a Solana sol_account query from a byte array func (saq *SolanaAccountQueryRequest) Unmarshal(data []byte) error { reader := bytes.NewReader(data[:]) return saq.UnmarshalFromReader(reader) } // UnmarshalFromReader deserializes a Solana sol_account query from a byte array func (saq *SolanaAccountQueryRequest) UnmarshalFromReader(reader *bytes.Reader) error { len := uint32(0) if err := binary.Read(reader, binary.BigEndian, &len); err != nil { return fmt.Errorf("failed to read commitment len: %w", err) } if len > SolanaMaxCommitmentLength { return fmt.Errorf("commitment string is too long, may not be more than %d characters", SolanaMaxCommitmentLength) } commitment := make([]byte, len) if n, err := reader.Read(commitment[:]); err != nil || n != int(len) { return fmt.Errorf("failed to read commitment [%d]: %w", n, err) } saq.Commitment = string(commitment) if err := binary.Read(reader, binary.BigEndian, &saq.MinContextSlot); err != nil { return fmt.Errorf("failed to read min slot: %w", err) } if err := binary.Read(reader, binary.BigEndian, &saq.DataSliceOffset); err != nil { return fmt.Errorf("failed to read data slice offset: %w", err) } if err := binary.Read(reader, binary.BigEndian, &saq.DataSliceLength); err != nil { return fmt.Errorf("failed to read data slice length: %w", err) } numAccounts := uint8(0) if err := binary.Read(reader, binary.BigEndian, &numAccounts); err != nil { return fmt.Errorf("failed to read number of account entries: %w", err) } for count := 0; count < int(numAccounts); count++ { account := [SolanaPublicKeyLength]byte{} if n, err := reader.Read(account[:]); err != nil || n != SolanaPublicKeyLength { return fmt.Errorf("failed to read account [%d]: %w", n, err) } saq.Accounts = append(saq.Accounts, account) } return nil } // Validate does basic validation on a Solana sol_account query. func (saq *SolanaAccountQueryRequest) Validate() error { if len(saq.Commitment) > SolanaMaxCommitmentLength { return fmt.Errorf("commitment too long") } if saq.Commitment != "finalized" { return fmt.Errorf(`commitment must be "finalized"`) } if saq.DataSliceLength == 0 && saq.DataSliceOffset != 0 { return fmt.Errorf("data slice offset may not be set if data slice length is zero") } if len(saq.Accounts) <= 0 { return fmt.Errorf("does not contain any account entries") } if len(saq.Accounts) > SolanaMaxAccountsPerQuery { return fmt.Errorf("too many account entries, may not be more than %d", SolanaMaxAccountsPerQuery) } for _, acct := range saq.Accounts { // The account is fixed length, so don't need to check for nil. if len(acct) != SolanaPublicKeyLength { return fmt.Errorf("invalid account length") } } return nil } // Equal verifies that two Solana sol_account queries are equal. func (left *SolanaAccountQueryRequest) Equal(right *SolanaAccountQueryRequest) bool { if left.Commitment != right.Commitment || left.MinContextSlot != right.MinContextSlot || left.DataSliceOffset != right.DataSliceOffset || left.DataSliceLength != right.DataSliceLength { return false } if len(left.Accounts) != len(right.Accounts) { return false } for idx := range left.Accounts { if !bytes.Equal(left.Accounts[idx][:], right.Accounts[idx][:]) { return false } } return true } // // Implementation of SolanaPdaQueryRequest, which implements the ChainSpecificQuery interface. // func (e *SolanaPdaQueryRequest) Type() ChainSpecificQueryType { return SolanaPdaQueryRequestType } // Marshal serializes the binary representation of a Solana sol_pda request. // This method calls Validate() and relies on it to range checks lengths, etc. func (spda *SolanaPdaQueryRequest) Marshal() ([]byte, error) { if err := spda.Validate(); err != nil { return nil, err } buf := new(bytes.Buffer) vaa.MustWrite(buf, binary.BigEndian, uint32(len(spda.Commitment))) buf.Write([]byte(spda.Commitment)) vaa.MustWrite(buf, binary.BigEndian, spda.MinContextSlot) vaa.MustWrite(buf, binary.BigEndian, spda.DataSliceOffset) vaa.MustWrite(buf, binary.BigEndian, spda.DataSliceLength) vaa.MustWrite(buf, binary.BigEndian, uint8(len(spda.PDAs))) for _, pda := range spda.PDAs { buf.Write(pda.ProgramAddress[:]) vaa.MustWrite(buf, binary.BigEndian, uint8(len(pda.Seeds))) for _, seed := range pda.Seeds { vaa.MustWrite(buf, binary.BigEndian, uint32(len(seed))) buf.Write(seed) } } return buf.Bytes(), nil } // Unmarshal deserializes a Solana sol_pda query from a byte array func (spda *SolanaPdaQueryRequest) Unmarshal(data []byte) error { reader := bytes.NewReader(data[:]) return spda.UnmarshalFromReader(reader) } // UnmarshalFromReader deserializes a Solana sol_pda query from a byte array func (spda *SolanaPdaQueryRequest) UnmarshalFromReader(reader *bytes.Reader) error { len := uint32(0) if err := binary.Read(reader, binary.BigEndian, &len); err != nil { return fmt.Errorf("failed to read commitment len: %w", err) } if len > SolanaMaxCommitmentLength { return fmt.Errorf("commitment string is too long, may not be more than %d characters", SolanaMaxCommitmentLength) } commitment := make([]byte, len) if n, err := reader.Read(commitment[:]); err != nil || n != int(len) { return fmt.Errorf("failed to read commitment [%d]: %w", n, err) } spda.Commitment = string(commitment) if err := binary.Read(reader, binary.BigEndian, &spda.MinContextSlot); err != nil { return fmt.Errorf("failed to read min slot: %w", err) } if err := binary.Read(reader, binary.BigEndian, &spda.DataSliceOffset); err != nil { return fmt.Errorf("failed to read data slice offset: %w", err) } if err := binary.Read(reader, binary.BigEndian, &spda.DataSliceLength); err != nil { return fmt.Errorf("failed to read data slice length: %w", err) } numPDAs := uint8(0) if err := binary.Read(reader, binary.BigEndian, &numPDAs); err != nil { return fmt.Errorf("failed to read number of PDAs: %w", err) } for count := 0; count < int(numPDAs); count++ { programAddress := [SolanaPublicKeyLength]byte{} if n, err := reader.Read(programAddress[:]); err != nil || n != SolanaPublicKeyLength { return fmt.Errorf("failed to read program address [%d]: %w", n, err) } pda := SolanaPDAEntry{ProgramAddress: programAddress} numSeeds := uint8(0) if err := binary.Read(reader, binary.BigEndian, &numSeeds); err != nil { return fmt.Errorf("failed to read number of seeds: %w", err) } for count := 0; count < int(numSeeds); count++ { seedLen := uint32(0) if err := binary.Read(reader, binary.BigEndian, &seedLen); err != nil { return fmt.Errorf("failed to read call Data len: %w", err) } seed := make([]byte, seedLen) if n, err := reader.Read(seed[:]); err != nil || n != int(seedLen) { return fmt.Errorf("failed to read seed [%d]: %w", n, err) } pda.Seeds = append(pda.Seeds, seed) } spda.PDAs = append(spda.PDAs, pda) } return nil } // Validate does basic validation on a Solana sol_pda query. func (spda *SolanaPdaQueryRequest) Validate() error { if len(spda.Commitment) > SolanaMaxCommitmentLength { return fmt.Errorf("commitment too long") } if spda.Commitment != "finalized" { return fmt.Errorf(`commitment must be "finalized"`) } if spda.DataSliceLength == 0 && spda.DataSliceOffset != 0 { return fmt.Errorf("data slice offset may not be set if data slice length is zero") } if len(spda.PDAs) <= 0 { return fmt.Errorf("does not contain any PDAs entries") } if len(spda.PDAs) > SolanaMaxAccountsPerQuery { return fmt.Errorf("too many PDA entries, may not be more than %d", SolanaMaxAccountsPerQuery) } for _, pda := range spda.PDAs { // The program address is fixed length, so don't need to check for nil. if len(pda.ProgramAddress) != SolanaPublicKeyLength { return fmt.Errorf("invalid program address length") } if len(pda.Seeds) == 0 { return fmt.Errorf("PDA does not contain any seeds") } if len(pda.Seeds) > SolanaMaxSeeds { return fmt.Errorf("PDA contains too many seeds") } for _, seed := range pda.Seeds { if len(seed) == 0 { return fmt.Errorf("seed is null") } if len(seed) > SolanaMaxSeedLen { return fmt.Errorf("seed is too long") } } } return nil } // Equal verifies that two Solana sol_pda queries are equal. func (left *SolanaPdaQueryRequest) Equal(right *SolanaPdaQueryRequest) bool { if left.Commitment != right.Commitment || left.MinContextSlot != right.MinContextSlot || left.DataSliceOffset != right.DataSliceOffset || left.DataSliceLength != right.DataSliceLength { return false } if len(left.PDAs) != len(right.PDAs) { return false } for idx := range left.PDAs { if !bytes.Equal(left.PDAs[idx].ProgramAddress[:], right.PDAs[idx].ProgramAddress[:]) { return false } if len(left.PDAs[idx].Seeds) != len(right.PDAs[idx].Seeds) { return false } for idx2 := range left.PDAs[idx].Seeds { if !bytes.Equal(left.PDAs[idx].Seeds[idx2][:], right.PDAs[idx].Seeds[idx2][:]) { return false } } } return true }