diff --git a/src/QueryResponse.sol b/src/QueryResponse.sol new file mode 100644 index 0000000..b52bb9a --- /dev/null +++ b/src/QueryResponse.sol @@ -0,0 +1,675 @@ +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.4; + +import {BytesParsing} from "./libraries/BytesParsing.sol"; +import "./interfaces/IWormhole.sol"; + +// @dev ParsedQueryResponse is returned by QueryResponse.parseAndVerifyQueryResponse(). +struct ParsedQueryResponse { + uint8 version; + uint16 senderChainId; + uint32 nonce; + bytes requestId; // 65 byte sig for off-chain, 32 byte vaaHash for on-chain + ParsedPerChainQueryResponse [] responses; +} + +// @dev ParsedPerChainQueryResponse describes a single per-chain response. +struct ParsedPerChainQueryResponse { + uint16 chainId; + uint8 queryType; + bytes request; + bytes response; +} + +// @dev EthCallQueryResponse describes the response to an ETH call per-chain query. +struct EthCallQueryResponse { + bytes requestBlockId; + uint64 blockNum; + uint64 blockTime; + bytes32 blockHash; + EthCallData [] result; +} + +// @dev EthCallByTimestampQueryResponse describes the response to an ETH call by timestamp per-chain query. +struct EthCallByTimestampQueryResponse { + bytes requestTargetBlockIdHint; + bytes requestFollowingBlockIdHint; + uint64 requestTargetTimestamp; + uint64 targetBlockNum; + uint64 targetBlockTime; + uint64 followingBlockNum; + bytes32 targetBlockHash; + bytes32 followingBlockHash; + uint64 followingBlockTime; + EthCallData [] result; +} + +// @dev EthCallWithFinalityQueryResponse describes the response to an ETH call with finality per-chain query. +struct EthCallWithFinalityQueryResponse { + bytes requestBlockId; + bytes requestFinality; + uint64 blockNum; + uint64 blockTime; + bytes32 blockHash; + EthCallData [] result; +} + +// @dev EthCallData describes a single ETH call query / response pair. +struct EthCallData { + address contractAddress; + bytes callData; + bytes result; +} + +// @dev SolanaAccountQueryResponse describes the response to a Solana Account query per-chain query. +struct SolanaAccountQueryResponse { + bytes requestCommitment; + uint64 requestMinContextSlot; + uint64 requestDataSliceOffset; + uint64 requestDataSliceLength; + uint64 slotNumber; + uint64 blockTime; + bytes32 blockHash; + SolanaAccountResult [] results; +} + +// @dev SolanaAccountResult describes a single Solana Account query result. +struct SolanaAccountResult { + bytes32 account; + uint64 lamports; + uint64 rentEpoch; + bool executable; + bytes32 owner; + bytes data; +} + +// @dev SolanaPdaQueryResponse describes the response to a Solana PDA (Program Derived Address) query per-chain query. +struct SolanaPdaQueryResponse { + bytes requestCommitment; + uint64 requestMinContextSlot; + uint64 requestDataSliceOffset; + uint64 requestDataSliceLength; + uint64 slotNumber; + uint64 blockTime; + bytes32 blockHash; + SolanaPdaResult [] results; +} + +// @dev SolanaPdaResult describes a single Solana PDA (Program Derived Address) query result. +struct SolanaPdaResult { + bytes32 programId; + bytes[] seeds; + bytes32 account; + uint64 lamports; + uint64 rentEpoch; + bool executable; + bytes32 owner; + bytes data; + uint8 bump; +} + +// Custom errors +error EmptyWormholeAddress(); +error InvalidResponseVersion(); +error VersionMismatch(); +error ZeroQueries(); +error NumberOfResponsesMismatch(); +error ChainIdMismatch(); +error RequestTypeMismatch(); +error UnsupportedQueryType(uint8 received); +error WrongQueryType(uint8 received, uint8 expected); +error UnexpectedNumberOfResults(); +error InvalidPayloadLength(uint256 received, uint256 expected); +error InvalidContractAddress(); +error InvalidFunctionSignature(); +error InvalidChainId(); +error StaleBlockNum(); +error StaleBlockTime(); + +// @dev QueryResponse is a library that implements the parsing and verification of Cross Chain Query (CCQ) responses. +// For a detailed discussion of these query responses, please see the white paper: +// https://github.com/wormhole-foundation/wormhole/blob/main/whitepapers/0013_ccq.md +abstract contract QueryResponse { + using BytesParsing for bytes; + + IWormhole public immutable wormhole; + + bytes public constant responsePrefix = bytes("query_response_0000000000000000000|"); + uint8 public constant VERSION = 1; + + // TODO: Consider changing these to an enum. + uint8 public constant QT_ETH_CALL = 1; + uint8 public constant QT_ETH_CALL_BY_TIMESTAMP = 2; + uint8 public constant QT_ETH_CALL_WITH_FINALITY = 3; + uint8 public constant QT_SOL_ACCOUNT = 4; + uint8 public constant QT_SOL_PDA = 5; + uint8 public constant QT_MAX = 6; // Keep this last + + constructor(address _wormhole) { + if (_wormhole == address(0)) { + revert EmptyWormholeAddress(); + } + + wormhole = IWormhole(_wormhole); + } + + /// @dev getResponseHash computes the hash of the specified query response. + function getResponseHash(bytes memory response) public pure returns (bytes32) { + return keccak256(response); + } + + /// @dev getResponseDigest computes the digest of the specified query response. + function getResponseDigest(bytes memory response) public pure returns (bytes32) { + return keccak256(abi.encodePacked(responsePrefix,getResponseHash(response))); + } + + /// @dev parseAndVerifyQueryResponse verifies the query response and returns the parsed response. + function parseAndVerifyQueryResponse(bytes memory response, IWormhole.Signature[] memory signatures) public view returns (ParsedQueryResponse memory r) { + verifyQueryResponseSignatures(response, signatures); + + uint index; + + (r.version, index) = response.asUint8Unchecked(index); + if (r.version != VERSION) { + revert InvalidResponseVersion(); + } + + (r.senderChainId, index) = response.asUint16Unchecked(index); + + // For off chain requests (chainID zero), the requestId is the 65 byte signature. For on chain requests, it is the 32 byte VAA hash. + if (r.senderChainId == 0) { + (r.requestId, index) = response.sliceUnchecked(index, 65); + } else { + (r.requestId, index) = response.sliceUnchecked(index, 32); + } + + uint32 len; + (len, index) = response.asUint32Unchecked(index); // query_request_len + uint reqIdx = index; + + // Scope to avoid stack-too-deep error + { + uint8 version; + (version, reqIdx) = response.asUint8Unchecked(reqIdx); + if (version != r.version) { + revert VersionMismatch(); + } + } + + (r.nonce, reqIdx) = response.asUint32Unchecked(reqIdx); + + uint8 numPerChainQueries; + (numPerChainQueries, reqIdx) = response.asUint8Unchecked(reqIdx); + + // A valid query request has at least one per chain query + if (numPerChainQueries == 0) { + revert ZeroQueries(); + } + + // The response starts after the request. + uint respIdx = index + len; + uint startOfResponse = respIdx; + + uint8 respNumPerChainQueries; + (respNumPerChainQueries, respIdx) = response.asUint8Unchecked(respIdx); + if (respNumPerChainQueries != numPerChainQueries) { + revert NumberOfResponsesMismatch(); + } + + r.responses = new ParsedPerChainQueryResponse[](numPerChainQueries); + + // Walk through the requests and responses in lock step. + for (uint idx; idx < numPerChainQueries;) { + (r.responses[idx].chainId, reqIdx) = response.asUint16Unchecked(reqIdx); + uint16 respChainId; + (respChainId, respIdx) = response.asUint16Unchecked(respIdx); + if (respChainId != r.responses[idx].chainId) { + revert ChainIdMismatch(); + } + + (r.responses[idx].queryType, reqIdx) = response.asUint8Unchecked(reqIdx); + uint8 respQueryType; + (respQueryType, respIdx) = response.asUint8Unchecked(respIdx); + if (respQueryType != r.responses[idx].queryType) { + revert RequestTypeMismatch(); + } + + if (r.responses[idx].queryType < QT_ETH_CALL || r.responses[idx].queryType >= QT_MAX) { + revert UnsupportedQueryType(r.responses[idx].queryType); + } + + (len, reqIdx) = response.asUint32Unchecked(reqIdx); + (r.responses[idx].request, reqIdx) = response.sliceUnchecked(reqIdx, len); + + (len, respIdx) = response.asUint32Unchecked(respIdx); + (r.responses[idx].response, respIdx) = response.sliceUnchecked(respIdx, len); + + unchecked { ++idx; } + } + + // End of request body should align with start of response body + if (startOfResponse != reqIdx) { + revert InvalidPayloadLength(startOfResponse, reqIdx); + } + + checkLength(response, respIdx); + return r; + } + + /// @dev parseEthCallQueryResponse parses a ParsedPerChainQueryResponse for an ETH call per-chain query. + function parseEthCallQueryResponse(ParsedPerChainQueryResponse memory pcr) public pure returns (EthCallQueryResponse memory r) { + if (pcr.queryType != QT_ETH_CALL) { + revert WrongQueryType(pcr.queryType, QT_ETH_CALL); + } + + uint reqIdx; + uint respIdx; + + uint32 len; + (len, reqIdx) = pcr.request.asUint32Unchecked(reqIdx); // block_id_len + + (r.requestBlockId, reqIdx) = pcr.request.sliceUnchecked(reqIdx, len); + + uint8 numBatchCallData; + (numBatchCallData, reqIdx) = pcr.request.asUint8Unchecked(reqIdx); + + (r.blockNum, respIdx) = pcr.response.asUint64Unchecked(respIdx); + + (r.blockHash, respIdx) = pcr.response.asBytes32Unchecked(respIdx); + + (r.blockTime, respIdx) = pcr.response.asUint64Unchecked(respIdx); + + uint8 respNumResults; + (respNumResults, respIdx) = pcr.response.asUint8Unchecked(respIdx); + if (respNumResults != numBatchCallData) { + revert UnexpectedNumberOfResults(); + } + + r.result = new EthCallData[](numBatchCallData); + + // Walk through the call data and results in lock step. + for (uint idx; idx < numBatchCallData;) { + (r.result[idx].contractAddress, reqIdx) = pcr.request.asAddressUnchecked(reqIdx); + + (len, reqIdx) = pcr.request.asUint32Unchecked(reqIdx); // call_data_len + (r.result[idx].callData, reqIdx) = pcr.request.sliceUnchecked(reqIdx, len); + + (len, respIdx) = pcr.response.asUint32Unchecked(respIdx); // result_len + (r.result[idx].result, respIdx) = pcr.response.sliceUnchecked(respIdx, len); + + unchecked { ++idx; } + } + + checkLength(pcr.request, reqIdx); + checkLength(pcr.response, respIdx); + return r; + } + + /// @dev parseEthCallByTimestampQueryResponse parses a ParsedPerChainQueryResponse for an ETH call per-chain query. + function parseEthCallByTimestampQueryResponse(ParsedPerChainQueryResponse memory pcr) public pure returns (EthCallByTimestampQueryResponse memory r) { + if (pcr.queryType != QT_ETH_CALL_BY_TIMESTAMP) { + revert WrongQueryType(pcr.queryType, QT_ETH_CALL_BY_TIMESTAMP); + } + + uint reqIdx; + uint respIdx; + uint32 len; + + (r.requestTargetTimestamp, reqIdx) = pcr.request.asUint64Unchecked(reqIdx); // Request target_time_us + + (len, reqIdx) = pcr.request.asUint32Unchecked(reqIdx); // Request target_block_id_hint_len + (r.requestTargetBlockIdHint, reqIdx) = pcr.request.sliceUnchecked(reqIdx, len); // Request target_block_id_hint + + (len, reqIdx) = pcr.request.asUint32Unchecked(reqIdx); // following_block_id_hint_len + (r.requestFollowingBlockIdHint, reqIdx) = pcr.request.sliceUnchecked(reqIdx, len); // Request following_block_id_hint + + uint8 numBatchCallData; + (numBatchCallData, reqIdx) = pcr.request.asUint8Unchecked(reqIdx); // Request num_batch_call_data + + (r.targetBlockNum, respIdx) = pcr.response.asUint64Unchecked(respIdx); // Response target_block_number + (r.targetBlockHash, respIdx) = pcr.response.asBytes32Unchecked(respIdx); // Response target_block_hash + (r.targetBlockTime, respIdx) = pcr.response.asUint64Unchecked(respIdx); // Response target_block_time_us + + (r.followingBlockNum, respIdx) = pcr.response.asUint64Unchecked(respIdx); // Response following_block_number + (r.followingBlockHash, respIdx) = pcr.response.asBytes32Unchecked(respIdx); // Response following_block_hash + (r.followingBlockTime, respIdx) = pcr.response.asUint64Unchecked(respIdx); // Response following_block_time_us + + uint8 respNumResults; + (respNumResults, respIdx) = pcr.response.asUint8Unchecked(respIdx); // Response num_results + if (respNumResults != numBatchCallData) { + revert UnexpectedNumberOfResults(); + } + + r.result = new EthCallData[](numBatchCallData); + + // Walk through the call data and results in lock step. + for (uint idx; idx < numBatchCallData;) { + (r.result[idx].contractAddress, reqIdx) = pcr.request.asAddressUnchecked(reqIdx); + + (len, reqIdx) = pcr.request.asUint32Unchecked(reqIdx); // call_data_len + (r.result[idx].callData, reqIdx) = pcr.request.sliceUnchecked(reqIdx, len); + + (len, respIdx) = pcr.response.asUint32Unchecked(respIdx); // result_len + (r.result[idx].result, respIdx) = pcr.response.sliceUnchecked(respIdx, len); + + unchecked { ++idx; } + } + + checkLength(pcr.request, reqIdx); + checkLength(pcr.response, respIdx); + } + + /// @dev parseEthCallWithFinalityQueryResponse parses a ParsedPerChainQueryResponse for an ETH call per-chain query. + function parseEthCallWithFinalityQueryResponse(ParsedPerChainQueryResponse memory pcr) public pure returns (EthCallWithFinalityQueryResponse memory r) { + if (pcr.queryType != QT_ETH_CALL_WITH_FINALITY) { + revert WrongQueryType(pcr.queryType, QT_ETH_CALL_WITH_FINALITY); + } + + uint reqIdx; + uint respIdx; + uint32 len; + + (len, reqIdx) = pcr.request.asUint32Unchecked(reqIdx); // Request block_id_len + (r.requestBlockId, reqIdx) = pcr.request.sliceUnchecked(reqIdx, len); // Request block_id + + (len, reqIdx) = pcr.request.asUint32Unchecked(reqIdx); // Request finality_len + (r.requestFinality, reqIdx) = pcr.request.sliceUnchecked(reqIdx, len); // Request finality + + uint8 numBatchCallData; + (numBatchCallData, reqIdx) = pcr.request.asUint8Unchecked(reqIdx); // Request num_batch_call_data + + (r.blockNum, respIdx) = pcr.response.asUint64Unchecked(respIdx); // Response block_number + + (r.blockHash, respIdx) = pcr.response.asBytes32Unchecked(respIdx); // Response block_hash + + (r.blockTime, respIdx) = pcr.response.asUint64Unchecked(respIdx); // Response block_time_us + + uint8 respNumResults; + (respNumResults, respIdx) = pcr.response.asUint8Unchecked(respIdx); // Response num_results + if (respNumResults != numBatchCallData) { + revert UnexpectedNumberOfResults(); + } + + r.result = new EthCallData[](numBatchCallData); + + // Walk through the call data and results in lock step. + for (uint idx; idx < numBatchCallData;) { + (r.result[idx].contractAddress, reqIdx) = pcr.request.asAddressUnchecked(reqIdx); + + (len, reqIdx) = pcr.request.asUint32Unchecked(reqIdx); // call_data_len + (r.result[idx].callData, reqIdx) = pcr.request.sliceUnchecked(reqIdx, len); + + (len, respIdx) = pcr.response.asUint32Unchecked(respIdx); // result_len + (r.result[idx].result, respIdx) = pcr.response.sliceUnchecked(respIdx, len); + + unchecked { ++idx; } + } + + checkLength(pcr.request, reqIdx); + checkLength(pcr.response, respIdx); + } + + /// @dev parseSolanaAccountQueryResponse parses a ParsedPerChainQueryResponse for a Solana Account per-chain query. + function parseSolanaAccountQueryResponse(ParsedPerChainQueryResponse memory pcr) public pure returns (SolanaAccountQueryResponse memory r) { + if (pcr.queryType != QT_SOL_ACCOUNT) { + revert WrongQueryType(pcr.queryType, QT_SOL_ACCOUNT); + } + + uint reqIdx; + uint respIdx; + uint32 len; + + (len, reqIdx) = pcr.request.asUint32Unchecked(reqIdx); // Request commitment_len + (r.requestCommitment, reqIdx) = pcr.request.sliceUnchecked(reqIdx, len); // Request commitment + (r.requestMinContextSlot, reqIdx) = pcr.request.asUint64Unchecked(reqIdx); // Request min_context_slot + (r.requestDataSliceOffset, reqIdx) = pcr.request.asUint64Unchecked(reqIdx); // Request data_slice_offset + (r.requestDataSliceLength, reqIdx) = pcr.request.asUint64Unchecked(reqIdx); // Request data_slice_length + + uint8 numAccounts; + (numAccounts, reqIdx) = pcr.request.asUint8Unchecked(reqIdx); // Request num_accounts + + (r.slotNumber, respIdx) = pcr.response.asUint64Unchecked(respIdx); // Response slot_number + (r.blockTime, respIdx) = pcr.response.asUint64Unchecked(respIdx); // Response block_time_us + (r.blockHash, respIdx) = pcr.response.asBytes32Unchecked(respIdx); // Response block_hash + + uint8 respNumResults; + (respNumResults, respIdx) = pcr.response.asUint8Unchecked(respIdx); // Response num_results + if (respNumResults != numAccounts) { + revert UnexpectedNumberOfResults(); + } + + r.results = new SolanaAccountResult[](numAccounts); + + // Walk through the call data and results in lock step. + for (uint idx; idx < numAccounts;) { + (r.results[idx].account, reqIdx) = pcr.request.asBytes32Unchecked(reqIdx); // Request account + + (r.results[idx].lamports, respIdx) = pcr.response.asUint64Unchecked(respIdx); // Response lamports + (r.results[idx].rentEpoch, respIdx) = pcr.response.asUint64Unchecked(respIdx); // Response rent_epoch + + (r.results[idx].executable, respIdx) = pcr.response.asBoolUnchecked(respIdx); // Response executable + + (r.results[idx].owner, respIdx) = pcr.response.asBytes32Unchecked(respIdx); // Response owner + + + (len, respIdx) = pcr.response.asUint32Unchecked(respIdx); // result_len + (r.results[idx].data, respIdx) = pcr.response.sliceUnchecked(respIdx, len); + + unchecked { ++idx; } + } + + checkLength(pcr.request, reqIdx); + checkLength(pcr.response, respIdx); + } + + /// @dev parseSolanaPdaQueryResponse parses a ParsedPerChainQueryResponse for a Solana Pda per-chain query. + function parseSolanaPdaQueryResponse(ParsedPerChainQueryResponse memory pcr) public pure returns (SolanaPdaQueryResponse memory r) { + if (pcr.queryType != QT_SOL_PDA) { + revert WrongQueryType(pcr.queryType, QT_SOL_PDA); + } + + uint reqIdx; + uint respIdx; + uint32 len; + + (len, reqIdx) = pcr.request.asUint32Unchecked(reqIdx); // Request commitment_len + (r.requestCommitment, reqIdx) = pcr.request.sliceUnchecked(reqIdx, len); // Request commitment + (r.requestMinContextSlot, reqIdx) = pcr.request.asUint64Unchecked(reqIdx); // Request min_context_slot + (r.requestDataSliceOffset, reqIdx) = pcr.request.asUint64Unchecked(reqIdx); // Request data_slice_offset + (r.requestDataSliceLength, reqIdx) = pcr.request.asUint64Unchecked(reqIdx); // Request data_slice_length + + uint8 numPdas; + (numPdas, reqIdx) = pcr.request.asUint8Unchecked(reqIdx); // Request num_Pdas + + (r.slotNumber, respIdx) = pcr.response.asUint64Unchecked(respIdx); // Response slot_number + (r.blockTime, respIdx) = pcr.response.asUint64Unchecked(respIdx); // Response block_time_us + (r.blockHash, respIdx) = pcr.response.asBytes32Unchecked(respIdx); // Response block_hash + + uint8 respNumResults; + (respNumResults, respIdx) = pcr.response.asUint8Unchecked(respIdx); // Response num_results + if (respNumResults != numPdas) { + revert UnexpectedNumberOfResults(); + } + + r.results = new SolanaPdaResult[](numPdas); + + // Walk through the call data and results in lock step. + for (uint idx; idx < numPdas;) { + (r.results[idx].programId, reqIdx) = pcr.request.asBytes32Unchecked(reqIdx); // Request programId + + uint8 numSeeds; // Request number of seeds + (numSeeds, reqIdx) = pcr.request.asUint8Unchecked(reqIdx); + r.results[idx].seeds = new bytes[](numSeeds); + for (uint idx2; idx2 < numSeeds;) { + uint32 seedLen; + (seedLen, reqIdx) = pcr.request.asUint32Unchecked(reqIdx); + (r.results[idx].seeds[idx2], reqIdx) = pcr.request.sliceUnchecked(reqIdx, seedLen); + unchecked { ++idx2; } + } + + (r.results[idx].account, respIdx) = pcr.response.asBytes32Unchecked(respIdx); // Response account + (r.results[idx].bump, respIdx) = pcr.response.asUint8Unchecked(respIdx); // Response bump + + (r.results[idx].lamports, respIdx) = pcr.response.asUint64Unchecked(respIdx); // Response lamports + (r.results[idx].rentEpoch, respIdx) = pcr.response.asUint64Unchecked(respIdx); // Response rent_epoch + + (r.results[idx].executable, respIdx) = pcr.response.asBoolUnchecked(respIdx); // Response executable + + (r.results[idx].owner, respIdx) = pcr.response.asBytes32Unchecked(respIdx); // Response owner + + (len, respIdx) = pcr.response.asUint32Unchecked(respIdx); // result_len + (r.results[idx].data, respIdx) = pcr.response.sliceUnchecked(respIdx, len); + + unchecked { ++idx; } + } + + checkLength(pcr.request, reqIdx); + checkLength(pcr.response, respIdx); + } + + /// @dev validateBlockTime validates that the parsed block time isn't stale + /// @param _blockTime Wormhole block time in MICROseconds + /// @param _minBlockTime Minium block time in seconds + function validateBlockTime(uint64 _blockTime, uint256 _minBlockTime) public pure { + uint256 blockTimeInSeconds = _blockTime / 1_000_000; // Rounds down + + if (blockTimeInSeconds < _minBlockTime) { + revert StaleBlockTime(); + } + } + + /// @dev validateBlockNum validates that the parsed blockNum isn't stale + function validateBlockNum(uint64 _blockNum, uint256 _minBlockNum) public pure { + if (_blockNum < _minBlockNum) { + revert StaleBlockNum(); + } + } + + /// @dev validateChainId validates that the parsed chainId is one of an array of chainIds we expect + function validateChainId(uint16 chainId, uint16[] memory _validChainIds) public pure { + bool validChainId = false; + + uint256 numChainIds = _validChainIds.length; + + for (uint256 idx; idx < numChainIds;) { + if (chainId == _validChainIds[idx]) { + validChainId = true; + break; + } + + unchecked { ++idx; } + } + + if (!validChainId) revert InvalidChainId(); + } + + /// @dev validateMutlipleEthCallData validates that each EthCallData in an array comes from a function signature and contract address we expect + function validateMultipleEthCallData(EthCallData[] memory r, address[] memory _expectedContractAddresses, bytes4[] memory _expectedFunctionSignatures) public pure { + uint256 callDatasLength = r.length; + + for (uint256 idx; idx < callDatasLength;) { + validateEthCallData(r[idx], _expectedContractAddresses, _expectedFunctionSignatures); + + unchecked { ++idx; } + } + } + + /// @dev validateEthCallData validates that EthCallData comes from a function signature and contract address we expect + /// @dev An empty array means we accept all addresses/function signatures + /// @dev Example 1: To accept signatures 0xaaaaaaaa and 0xbbbbbbbb from `address(abcd)` you'd pass in [0xaaaaaaaa, 0xbbbbbbbb], [address(abcd)] + /// @dev Example 2: To accept any function signatures from `address(abcd)` or `address(efab)` you'd pass in [], [address(abcd), address(efab)] + /// @dev Example 3: To accept function signature 0xaaaaaaaa from any address you'd pass in [0xaaaaaaaa], [] + /// @dev WARNING Example 4: If you want to accept signature 0xaaaaaaaa from `address(abcd)` and signature 0xbbbbbbbb from `address(efab)` the following input would be incorrect: + /// @dev [0xaaaaaaaa, 0xbbbbbbbb], [address(abcd), address(efab)] + /// @dev This would accept both 0xaaaaaaaa and 0xbbbbbbbb from `address(abcd)` AND `address(efab)`. Instead you should make 2 calls to this method + /// @dev using the pattern in Example 1. [0xaaaaaaaa], [address(abcd)] OR [0xbbbbbbbb], [address(efab)] + function validateEthCallData(EthCallData memory r, address[] memory _expectedContractAddresses, bytes4[] memory _expectedFunctionSignatures) public pure { + bool validContractAddress = _expectedContractAddresses.length == 0 ? true : false; + bool validFunctionSignature = _expectedFunctionSignatures.length == 0 ? true : false; + + uint256 contractAddressesLength = _expectedContractAddresses.length; + + // Check that the contract address called in the request is expected + for (uint256 idx; idx < contractAddressesLength;) { + if (r.contractAddress == _expectedContractAddresses[idx]) { + validContractAddress = true; + break; + } + + unchecked { ++idx; } + } + + // Early exit to save gas + if (!validContractAddress) { + revert InvalidContractAddress(); + } + + uint256 functionSignaturesLength = _expectedFunctionSignatures.length; + + // Check that the function signature called is expected + for (uint256 idx; idx < functionSignaturesLength;) { + (bytes4 funcSig,) = r.callData.asBytes4Unchecked(0); + if (funcSig == _expectedFunctionSignatures[idx]) { + validFunctionSignature = true; + break; + } + + unchecked { ++idx; } + } + + if (!validFunctionSignature) { + revert InvalidFunctionSignature(); + } + } + + /** + * @dev verifyQueryResponseSignatures verifies the signatures on a query response. It calls into the Wormhole contract. + * IWormhole.Signature expects the last byte to be bumped by 27 + * see https://github.com/wormhole-foundation/wormhole/blob/637b1ee657de7de05f783cbb2078dd7d8bfda4d0/ethereum/contracts/Messages.sol#L174 + */ + function verifyQueryResponseSignatures(bytes memory response, IWormhole.Signature[] memory signatures) public view { + // It might be worth adding a verifyCurrentQuorum call on the core bridge so that there is only 1 cross call instead of 4. + uint32 gsi = wormhole.getCurrentGuardianSetIndex(); + IWormhole.GuardianSet memory guardianSet = wormhole.getGuardianSet(gsi); + + bytes32 responseHash = getResponseDigest(response); + + /** + * @dev Checks whether the guardianSet has zero keys + * WARNING: This keys check is critical to ensure the guardianSet has keys present AND to ensure + * that guardianSet key size doesn't fall to zero and negatively impact quorum assessment. If guardianSet + * key length is 0 and vm.signatures length is 0, this could compromise the integrity of both vm and + * signature verification. + */ + if(guardianSet.keys.length == 0){ + revert("invalid guardian set"); + } + + /** + * @dev We're using a fixed point number transformation with 1 decimal to deal with rounding. + * WARNING: This quorum check is critical to assessing whether we have enough Guardian signatures to validate a VM + * if making any changes to this, obtain additional peer review. If guardianSet key length is 0 and + * vm.signatures length is 0, this could compromise the integrity of both vm and signature verification. + */ + if (signatures.length < wormhole.quorum(guardianSet.keys.length)){ + revert("no quorum"); + } + + /// @dev Verify the proposed vm.signatures against the guardianSet + (bool signaturesValid, string memory invalidReason) = wormhole.verifySignatures(responseHash, signatures, guardianSet); + if(!signaturesValid){ + revert(invalidReason); + } + + /// If we are here, we've validated the VM is a valid multi-sig that matches the current guardianSet. + } + + /// @dev checkLength verifies that the message was fully consumed. + function checkLength(bytes memory encoded, uint256 expected) private pure { + if (encoded.length != expected) { + revert InvalidPayloadLength(encoded.length, expected); + } + } +} + diff --git a/src/testing/helpers/QueryTest.sol b/src/testing/helpers/QueryTest.sol new file mode 100644 index 0000000..7005d31 --- /dev/null +++ b/src/testing/helpers/QueryTest.sol @@ -0,0 +1,352 @@ +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.4; + +// @dev QueryTest is a library to build Cross Chain Query (CCQ) responses for testing purposes. +library QueryTest { + // Custom errors + error SolanaTooManyPDAs(); + error SolanaTooManySeeds(); + error SolanaSeedTooLong(); + + // + // Query Request stuff + // + + /// @dev buildOffChainQueryRequestBytes builds an off chain query request from the specified fields. + function buildOffChainQueryRequestBytes( + uint8 _version, + uint32 _nonce, + uint8 _numPerChainQueries, + bytes memory _perChainQueries + ) internal pure returns (bytes memory){ + return abi.encodePacked( + _version, + _nonce, + _numPerChainQueries, + _perChainQueries // Each created by buildPerChainRequestBytes() + ); + } + + /// @dev buildPerChainRequestBytes builds a per chain request from the specified fields. + function buildPerChainRequestBytes( + uint16 _chainId, + uint8 _queryType, + bytes memory _queryBytes + ) internal pure returns (bytes memory){ + return abi.encodePacked( + _chainId, + _queryType, + uint32(_queryBytes.length), + _queryBytes + ); + } + + /// @dev buildEthCallRequestBytes builds an eth_call query request from the specified fields. + function buildEthCallRequestBytes( + bytes memory _blockId, + uint8 _numCallData, + bytes memory _callData // Created with buildEthCallDataBytes() + ) internal pure returns (bytes memory){ + return abi.encodePacked( + uint32(_blockId.length), + _blockId, + _numCallData, + _callData + ); + } + + /// @dev buildEthCallByTimestampRequestBytes builds an eth_call_by_timestamp query request from the specified fields. + function buildEthCallByTimestampRequestBytes( + uint64 _targetTimeUs, + bytes memory _targetBlockHint, + bytes memory _followingBlockHint, + uint8 _numCallData, + bytes memory _callData // Created with buildEthCallDataBytes() + ) internal pure returns (bytes memory){ + return abi.encodePacked( + _targetTimeUs, + uint32(_targetBlockHint.length), + _targetBlockHint, + uint32(_followingBlockHint.length), + _followingBlockHint, + _numCallData, + _callData + ); + } + + /// @dev buildEthCallWithFinalityRequestBytes builds an eth_call_with_finality query request from the specified fields. + function buildEthCallWithFinalityRequestBytes( + bytes memory _blockId, + bytes memory _finality, + uint8 _numCallData, + bytes memory _callData // Created with buildEthCallDataBytes() + ) internal pure returns (bytes memory){ + return abi.encodePacked( + uint32(_blockId.length), + _blockId, + uint32(_finality.length), + _finality, + _numCallData, + _callData + ); + } + + /// @dev buildEthCallDataBytes builds the call data associated with one of the eth_call family of queries. + function buildEthCallDataBytes( + address _contractAddress, + bytes memory _callData + ) internal pure returns (bytes memory){ + return abi.encodePacked( + _contractAddress, + uint32(_callData.length), + _callData + ); + } + + /// @dev buildSolanaAccountRequestBytes builds a sol_account query request from the specified fields. + function buildSolanaAccountRequestBytes( + bytes memory _commitment, + uint64 _minContextSlot, + uint64 _dataSliceOffset, + uint64 _dataSliceLength, + uint8 _numAccounts, + bytes memory _accounts // Each account is 32 bytes. + ) internal pure returns (bytes memory){ + return abi.encodePacked( + uint32(_commitment.length), + _commitment, + _minContextSlot, + _dataSliceOffset, + _dataSliceLength, + _numAccounts, + _accounts + ); + } + + /// @dev buildSolanaPdaRequestBytes builds a sol_pda query request from the specified fields. + function buildSolanaPdaRequestBytes( + bytes memory _commitment, + uint64 _minContextSlot, + uint64 _dataSliceOffset, + uint64 _dataSliceLength, + bytes[] memory _pdas // Created with multiple calls to buildSolanaPdaEntry() + ) internal pure returns (bytes memory){ + uint numPdas = _pdas.length; + if (numPdas > 255) { + revert SolanaTooManyPDAs(); + } + bytes memory result = abi.encodePacked( + uint32(_commitment.length), + _commitment, + _minContextSlot, + _dataSliceOffset, + _dataSliceLength, + uint8(numPdas) + ); + + for (uint idx; idx < numPdas;) { + result = abi.encodePacked( + result, + _pdas[idx] + ); + + unchecked { ++idx; } + } + + return result; + } + + /// @dev buildSolanaPdaEntry builds a PDA entry for a sol_pda query. + function buildSolanaPdaEntry( + bytes32 _programId, + uint8 _numSeeds, + bytes memory _seeds // Created with buildSolanaPdaSeedBytes() + ) internal pure returns (bytes memory){ + if (_numSeeds > SolanaMaxSeeds) { + revert SolanaTooManySeeds(); + } + return abi.encodePacked( + _programId, + _numSeeds, + _seeds + ); + } + + // According to the spec, there may be at most 16 seeds. + // https://github.com/gagliardetto/solana-go/blob/6fe3aea02e3660d620433444df033fc3fe6e64c1/keys.go#L559 + uint public constant SolanaMaxSeeds = 16; + + // According to the spec, a seed may be at most 32 bytes. + // https://github.com/gagliardetto/solana-go/blob/6fe3aea02e3660d620433444df033fc3fe6e64c1/keys.go#L557 + uint public constant SolanaMaxSeedLen = 32; + + /// @dev buildSolanaPdaSeedBytes packs the seeds for a PDA entry into an array of bytes. + function buildSolanaPdaSeedBytes( + bytes[] memory _seeds + ) internal pure returns (bytes memory, uint8){ + uint numSeeds = _seeds.length; + if (numSeeds > SolanaMaxSeeds) { + revert SolanaTooManySeeds(); + } + + bytes memory result; + + for (uint idx; idx < numSeeds;) { + uint seedLen = _seeds[idx].length; + if (seedLen > SolanaMaxSeedLen) { + revert SolanaSeedTooLong(); + } + result = abi.encodePacked( + result, + abi.encodePacked( + uint32(seedLen), + _seeds[idx] + ) + ); + + unchecked { ++idx; } + } + + return (result, uint8(numSeeds)); + } + + // + // Query Response stuff + // + + /// @dev buildQueryResponseBytes builds a query response from the specified fields. + function buildQueryResponseBytes( + uint8 _version, + uint16 _senderChainId, + bytes memory _signature, + bytes memory _queryRequest, + uint8 _numPerChainResponses, + bytes memory _perChainResponses + ) internal pure returns (bytes memory){ + return abi.encodePacked( + _version, + _senderChainId, + _signature, + uint32(_queryRequest.length), + _queryRequest, + _numPerChainResponses, + _perChainResponses // Each created by buildPerChainResponseBytes() + ); + } + + /// @dev buildPerChainResponseBytes builds a per chain response from the specified fields. + function buildPerChainResponseBytes( + uint16 _chainId, + uint8 _queryType, + bytes memory _responseBytes + ) internal pure returns (bytes memory){ + return abi.encodePacked( + _chainId, + _queryType, + uint32(_responseBytes.length), + _responseBytes + ); + } + + /// @dev buildEthCallResponseBytes builds an eth_call response from the specified fields. + function buildEthCallResponseBytes( + uint64 _blockNumber, + bytes32 _blockHash, + uint64 _blockTimeUs, + uint8 _numResults, + bytes memory _results // Created with buildEthCallResultBytes() + ) internal pure returns (bytes memory){ + return abi.encodePacked( + _blockNumber, + _blockHash, + _blockTimeUs, + _numResults, + _results + ); + } + + /// @dev buildEthCallByTimestampResponseBytes builds an eth_call_by_timestamp response from the specified fields. + function buildEthCallByTimestampResponseBytes( + uint64 _targetBlockNumber, + bytes32 _targetBlockHash, + uint64 _targetBlockTimeUs, + uint64 _followingBlockNumber, + bytes32 _followingBlockHash, + uint64 _followingBlockTimeUs, + uint8 _numResults, + bytes memory _results // Created with buildEthCallResultBytes() + ) internal pure returns (bytes memory){ + return abi.encodePacked( + _targetBlockNumber, + _targetBlockHash, + _targetBlockTimeUs, + _followingBlockNumber, + _followingBlockHash, + _followingBlockTimeUs, + _numResults, + _results + ); + } + + /// @dev buildEthCallWithFinalityResponseBytes builds an eth_call_with_finality response from the specified fields. Note that it is currently the same as buildEthCallResponseBytes. + function buildEthCallWithFinalityResponseBytes( + uint64 _blockNumber, + bytes32 _blockHash, + uint64 _blockTimeUs, + uint8 _numResults, + bytes memory _results // Created with buildEthCallResultBytes() + ) internal pure returns (bytes memory){ + return abi.encodePacked( + _blockNumber, + _blockHash, + _blockTimeUs, + _numResults, + _results + ); + } + + /// @dev buildEthCallResultBytes builds an eth_call result from the specified fields. + function buildEthCallResultBytes( + bytes memory _result + ) internal pure returns (bytes memory){ + return abi.encodePacked( + uint32(_result.length), + _result + ); + } + + /// @dev buildSolanaAccountResponseBytes builds a sol_account response from the specified fields. + function buildSolanaAccountResponseBytes( + uint64 _slotNumber, + uint64 _blockTimeUs, + bytes32 _blockHash, + uint8 _numResults, + bytes memory _results // Created with buildEthCallResultBytes() + ) internal pure returns (bytes memory){ + return abi.encodePacked( + _slotNumber, + _blockTimeUs, + _blockHash, + _numResults, + _results + ); + } + + /// @dev buildSolanaPdaResponseBytes builds a sol_pda response from the specified fields. + function buildSolanaPdaResponseBytes( + uint64 _slotNumber, + uint64 _blockTimeUs, + bytes32 _blockHash, + uint8 _numResults, + bytes memory _results // Created with buildEthCallResultBytes() + ) internal pure returns (bytes memory){ + return abi.encodePacked( + _slotNumber, + _blockTimeUs, + _blockHash, + _numResults, + _results + ); + } +} diff --git a/src/testing/helpers/WormholeMock.sol b/src/testing/helpers/WormholeMock.sol new file mode 100644 index 0000000..d0d6cc1 --- /dev/null +++ b/src/testing/helpers/WormholeMock.sol @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.13; + +import { IWormhole } from '../../interfaces/IWormhole.sol'; + +contract WormholeMock is IWormhole { + constructor() {} + + // INIT_SIGNERS=["0xbeFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe"] + function publishMessage( + uint32 nonce, + bytes memory payload, + uint8 consistencyLevel + ) external payable override returns (uint64 sequence) {} + + function initialize() external override {} + + function parseAndVerifyVM( + bytes calldata encodedVM + ) external view override returns (VM memory vm, bool valid, string memory reason) {} + + function verifyVM( + VM memory vm + ) external view override returns (bool valid, string memory reason) {} + + function verifySignatures( + bytes32 hash, + Signature[] memory signatures, + GuardianSet memory guardianSet + ) external pure override returns (bool valid, string memory reason) { + uint8 lastIndex = 0; + uint256 guardianCount = guardianSet.keys.length; + for (uint i = 0; i < signatures.length; i++) { + Signature memory sig = signatures[i]; + address signatory = ecrecover(hash, sig.v, sig.r, sig.s); + // ecrecover returns 0 for invalid signatures. We explicitly require valid signatures to avoid unexpected + // behaviour due to the default storage slot value also being 0. + require(signatory != address(0), "ecrecover failed with signature"); + + /// Ensure that provided signature indices are ascending only + require(i == 0 || sig.guardianIndex > lastIndex, "signature indices must be ascending"); + lastIndex = sig.guardianIndex; + + /// @dev Ensure that the provided signature index is within the + /// bounds of the guardianSet. This is implicitly checked by the array + /// index operation below, so this check is technically redundant. + /// However, reverting explicitly here ensures that a bug is not + /// introduced accidentally later due to the nontrivial storage + /// semantics of solidity. + require(sig.guardianIndex < guardianCount, "guardian index out of bounds"); + + /// Check to see if the signer of the signature does not match a specific Guardian key at the provided index + if(signatory != guardianSet.keys[sig.guardianIndex]){ + return (false, "VM signature invalid"); + } + } + + /// If we are here, we've validated that the provided signatures are valid for the provided guardianSet + return (true, ""); + } + + function parseVM(bytes memory encodedVM) external pure override returns (VM memory vm) {} + + function quorum( + uint numGuardians + ) external pure override returns (uint numSignaturesRequiredForQuorum) {} + + function getGuardianSet(uint32 index) external pure override returns (GuardianSet memory) { + index = 0; + address[] memory keys = new address[](1); + keys[0] = 0xbeFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe; + + GuardianSet memory gset = GuardianSet({ + keys: keys, + expirationTime: 999999999 + }); + return gset; + } + + function getCurrentGuardianSetIndex() external view override returns (uint32) {} + + function getGuardianSetExpiry() external view override returns (uint32) {} + + function governanceActionIsConsumed(bytes32 hash) external view override returns (bool) {} + + function isInitialized(address impl) external view override returns (bool) {} + + function chainId() external view override returns (uint16) {} + + function isFork() external view override returns (bool) {} + + function governanceChainId() external view override returns (uint16) {} + + function governanceContract() external view override returns (bytes32) {} + + function messageFee() external view override returns (uint256) {} + + function evmChainId() external view override returns (uint256) {} + + function nextSequence(address emitter) external view override returns (uint64) {} + + function parseContractUpgrade( + bytes memory encodedUpgrade + ) external pure override returns (ContractUpgrade memory cu) {} + + function parseGuardianSetUpgrade( + bytes memory encodedUpgrade + ) external pure override returns (GuardianSetUpgrade memory gsu) {} + + function parseSetMessageFee( + bytes memory encodedSetMessageFee + ) external pure override returns (SetMessageFee memory smf) {} + + function parseTransferFees( + bytes memory encodedTransferFees + ) external pure override returns (TransferFees memory tf) {} + + function parseRecoverChainId( + bytes memory encodedRecoverChainId + ) external pure override returns (RecoverChainId memory rci) {} + + function submitContractUpgrade(bytes memory _vm) external override {} + + function submitSetMessageFee(bytes memory _vm) external override {} + + function submitNewGuardianSet(bytes memory _vm) external override {} + + function submitTransferFees(bytes memory _vm) external override {} + + function submitRecoverChainId(bytes memory _vm) external override {} +} diff --git a/test/QueryResponse.t.sol b/test/QueryResponse.t.sol new file mode 100644 index 0000000..89f6200 --- /dev/null +++ b/test/QueryResponse.t.sol @@ -0,0 +1,803 @@ +// SPDX-License-Identifier: Apache 2 + +// forge test --match-contract QueryResponse + +pragma solidity ^0.8.4; + +import "../src/QueryResponse.sol"; +import "forge-std/Test.sol"; +import "../src/testing/helpers/QueryTest.sol"; +import {WormholeMock} from "../src/testing/helpers/WormholeMock.sol"; + + +// @dev A non-abstract QueryResponse contract +contract QueryResponseContract is QueryResponse { + constructor(address _wormhole) QueryResponse(_wormhole) {} +} + +contract TestQueryResponse is Test { + // Some happy case defaults + uint8 version = 0x01; + uint16 senderChainId = 0x0000; + bytes signature = hex"ff0c222dc9e3655ec38e212e9792bf1860356d1277462b6bf747db865caca6fc08e6317b64ee3245264e371146b1d315d38c867fe1f69614368dc4430bb560f200"; + uint32 queryRequestLen = 0x00000053; + uint8 queryRequestVersion = 0x01; + uint32 queryRequestNonce = 0xdd9914c6; + uint8 numPerChainQueries = 0x01; + bytes perChainQueries = hex"0005010000004600000009307832613631616334020d500b1d8e8ef31e21c99d1db9a6444d3adf12700000000406fdde030d500b1d8e8ef31e21c99d1db9a6444d3adf12700000000418160ddd"; + bytes perChainQueriesInner = hex"00000009307832613631616334020d500b1d8e8ef31e21c99d1db9a6444d3adf12700000000406fdde030d500b1d8e8ef31e21c99d1db9a6444d3adf12700000000418160ddd"; + uint8 numPerChainResponses = 0x01; + bytes perChainResponses = hex"000501000000b90000000002a61ac4c1adff9f6e180309e7d0d94c063338ddc61c1c4474cd6957c960efe659534d040005ff312e4f90c002000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d57726170706564204d6174696300000000000000000000000000000000000000000000200000000000000000000000000000000000000000007ae5649beabeddf889364a"; + bytes perChainResponsesInner = hex"00000009307832613631616334020d500b1d8e8ef31e21c99d1db9a6444d3adf12700000000406fdde030d500b1d8e8ef31e21c99d1db9a6444d3adf12700000000418160ddd"; + + bytes solanaAccountSignature = hex"acb1d93cdfe60f9776e3e05d7fafaf9d83a1d14db70317230f6b0b6f3a60708a1a64dddac02d3843f4c516f2509b89454a2e73c360fea47beee1c1a091ff9f3201"; + uint32 solanaAccountQueryRequestLen = 0x00000073; + uint8 solanaAccountQueryRequestVersion = 0x01; + uint32 solanaAccountQueryRequestNonce = 0x0000002a; + uint8 solanaAccountNumPerChainQueries = 0x01; + bytes solanaAccountPerChainQueries = hex"000104000000660000000966696e616c697a656400000000000000000000000000000000000000000000000002165809739240a0ac03b98440fe8985548e3aa683cd0d4d9df5b5659669faa3019c006c48c8cbf33849cb07a3f936159cc523f9591cb1999abd45890ec5fee9b7"; + bytes solanaAccountPerChainQueriesInner = hex"0000000966696e616c697a656400000000000000000000000000000000000000000000000002165809739240a0ac03b98440fe8985548e3aa683cd0d4d9df5b5659669faa3019c006c48c8cbf33849cb07a3f936159cc523f9591cb1999abd45890ec5fee9b7"; + uint8 solanaAccountNumPerChainResponses = 0x01; + bytes solanaAccountPerChainResponses = hex"010001040000013f000000000000d85f00060f3e9915ddc03a8de2b1de609020bb0a0dcee594a8c06801619cf9ea2a498b9d910f9a25772b020000000000164d6000000000000000000006ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a90000005201000000574108aed69daf7e625a361864b1f74d13702f2ca56de9660e566d1d8691848d0000e8890423c78a09010000000000000000000000000000000000000000000000000000000000000000000000000000000000164d6000000000000000000006ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a90000005201000000574108aed69daf7e625a361864b1f74d13702f2ca56de9660e566d1d8691848d01000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000"; + bytes solanaAccountPerChainResponsesInner = hex"000000000000d85f00060f3e9915ddc03a8de2b1de609020bb0a0dcee594a8c06801619cf9ea2a498b9d910f9a25772b020000000000164d6000000000000000000006ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a90000005201000000574108aed69daf7e625a361864b1f74d13702f2ca56de9660e566d1d8691848d0000e8890423c78a09010000000000000000000000000000000000000000000000000000000000000000000000000000000000164d6000000000000000000006ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a90000005201000000574108aed69daf7e625a361864b1f74d13702f2ca56de9660e566d1d8691848d01000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000"; + + bytes solanaPdaSignature = hex"0c8418d81c00aad6283ba3eb30e141ccdd9296e013ca44e5cc713418921253004b93107ba0d858a548ce989e2bca4132e4c2f9a57a9892e3a87a8304cdb36d8f00"; + uint32 solanaPdaQueryRequestLen = 0x0000006b; + uint8 solanaPdaQueryRequestVersion = 0x01; + uint32 solanaPdaQueryRequestNonce = 0x0000002b; + uint8 solanaPdaNumPerChainQueries = 0x01; + bytes solanaPdaPerChainQueries = hex"010001050000005e0000000966696e616c697a656400000000000008ff000000000000000c00000000000000140102c806312cbe5b79ef8aa6c17e3f423d8fdfe1d46909fb1f6cdf65ee8e2e6faa020000000b477561726469616e5365740000000400000000"; + bytes solanaPdaPerChainQueriesInner = hex"0000000966696e616c697a656400000000000008ff000000000000000c00000000000000140102c806312cbe5b79ef8aa6c17e3f423d8fdfe1d46909fb1f6cdf65ee8e2e6faa020000000b477561726469616e5365740000000400000000"; + uint8 solanaPdaNumPerChainResponses = 0x01; + bytes solanaPdaPerChainResponses = hex"0001050000009b00000000000008ff0006115e3f6d7540e05035785e15056a8559815e71343ce31db2abf23f65b19c982b68aee7bf207b014fa9188b339cfd573a0778c5deaeeee94d4bcfb12b345bf8e417e5119dae773efd0000000000116ac000000000000000000002c806312cbe5b79ef8aa6c17e3f423d8fdfe1d46909fb1f6cdf65ee8e2e6faa0000001457cd18b7f8a4d91a2da9ab4af05d0fbece2dcd65"; + bytes solanaPdaPerChainResponsesInner = hex"00000000000008ff0006115e3f6d7540e05035785e15056a8559815e71343ce31db2abf23f65b19c982b68aee7bf207b014fa9188b339cfd573a0778c5deaeeee94d4bcfb12b345bf8e417e5119dae773efd0000000000116ac000000000000000000002c806312cbe5b79ef8aa6c17e3f423d8fdfe1d46909fb1f6cdf65ee8e2e6faa0000001457cd18b7f8a4d91a2da9ab4af05d0fbece2dcd65"; + + uint8 sigGuardianIndex = 0; + + QueryResponse queryResponse; + + function setUp() public { + WormholeMock wormholeMock = new WormholeMock(); + queryResponse = new QueryResponseContract(address(wormholeMock)); + } + + uint16 constant TEST_CHAIN_ID = 2; + address constant DEVNET_GUARDIAN = 0xbeFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe; + uint256 constant DEVNET_GUARDIAN_PRIVATE_KEY = 0xcfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0; + uint16 constant GOVERNANCE_CHAIN_ID = 1; + bytes32 constant GOVERNANCE_CONTRACT = 0x0000000000000000000000000000000000000000000000000000000000000004; + + function getSignature(bytes memory response) internal view returns (uint8 v, bytes32 r, bytes32 s) { + bytes32 responseDigest = queryResponse.getResponseDigest(response); + (v, r, s) = vm.sign(DEVNET_GUARDIAN_PRIVATE_KEY, responseDigest); + } + + function concatenateQueryResponseBytesOffChain( + uint8 _version, + uint16 _senderChainId, + bytes memory _signature, + uint8 _queryRequestVersion, + uint32 _queryRequestNonce, + uint8 _numPerChainQueries, + bytes memory _perChainQueries, + uint8 _numPerChainResponses, + bytes memory _perChainResponses + ) internal pure returns (bytes memory){ + bytes memory queryRequest = QueryTest.buildOffChainQueryRequestBytes( + _queryRequestVersion, + _queryRequestNonce, + _numPerChainQueries, + _perChainQueries + ); + return QueryTest.buildQueryResponseBytes( + _version, + _senderChainId, + _signature, + queryRequest, + _numPerChainResponses, + _perChainResponses + ); + } + + function test_getResponseHash() public { + bytes memory resp = concatenateQueryResponseBytesOffChain(version, senderChainId, signature, queryRequestVersion, queryRequestNonce, numPerChainQueries, perChainQueries, numPerChainResponses, perChainResponses); + bytes32 hash = queryResponse.getResponseHash(resp); + bytes32 expectedHash = 0xed18e80906ffa80ce953a132a9cbbcf84186955f8fc8ce0322cd68622a58570e; + assertEq(hash, expectedHash); + } + + function test_getResponseDigest() public { + bytes memory resp = concatenateQueryResponseBytesOffChain(version, senderChainId, signature, queryRequestVersion, queryRequestNonce, numPerChainQueries, perChainQueries, numPerChainResponses, perChainResponses); + bytes32 digest = queryResponse.getResponseDigest(resp); + bytes32 expectedDigest = 0x5b84b19c68ee0b37899230175a92ee6eda4c5192e8bffca1d057d811bb3660e2; + assertEq(digest, expectedDigest); + } + + function test_verifyQueryResponseSignatures() public view { + bytes memory resp = concatenateQueryResponseBytesOffChain(version, senderChainId, signature, queryRequestVersion, queryRequestNonce, numPerChainQueries, perChainQueries, numPerChainResponses, perChainResponses); + (uint8 sigV, bytes32 sigR, bytes32 sigS) = getSignature(resp); + IWormhole.Signature[] memory signatures = new IWormhole.Signature[](1); + signatures[0] = IWormhole.Signature({r: sigR, s: sigS, v: sigV, guardianIndex: sigGuardianIndex}); + queryResponse.verifyQueryResponseSignatures(resp, signatures); + // TODO: There are no assertions for this test + } + + function test_parseAndVerifyQueryResponse() public { + bytes memory resp = concatenateQueryResponseBytesOffChain(version, senderChainId, signature, queryRequestVersion, queryRequestNonce, numPerChainQueries, perChainQueries, numPerChainResponses, perChainResponses); + (uint8 sigV, bytes32 sigR, bytes32 sigS) = getSignature(resp); + IWormhole.Signature[] memory signatures = new IWormhole.Signature[](1); + signatures[0] = IWormhole.Signature({r: sigR, s: sigS, v: sigV, guardianIndex: sigGuardianIndex}); + ParsedQueryResponse memory r = queryResponse.parseAndVerifyQueryResponse(resp, signatures); + assertEq(r.version, 1); + assertEq(r.senderChainId, 0); + assertEq(r.requestId, hex"ff0c222dc9e3655ec38e212e9792bf1860356d1277462b6bf747db865caca6fc08e6317b64ee3245264e371146b1d315d38c867fe1f69614368dc4430bb560f200"); + assertEq(r.nonce, 3717797062); + assertEq(r.responses.length, 1); + assertEq(r.responses[0].chainId, 5); + assertEq(r.responses[0].queryType, 1); + assertEq(r.responses[0].request, hex"00000009307832613631616334020d500b1d8e8ef31e21c99d1db9a6444d3adf12700000000406fdde030d500b1d8e8ef31e21c99d1db9a6444d3adf12700000000418160ddd"); + assertEq(r.responses[0].response, hex"0000000002a61ac4c1adff9f6e180309e7d0d94c063338ddc61c1c4474cd6957c960efe659534d040005ff312e4f90c002000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d57726170706564204d6174696300000000000000000000000000000000000000000000200000000000000000000000000000000000000000007ae5649beabeddf889364a"); + } + + function test_parseEthCallQueryResponse() public { + // Take the data extracted by the previous test and break it down even further. + ParsedPerChainQueryResponse memory r = ParsedPerChainQueryResponse({ + chainId: 5, + queryType: 1, + request: hex"00000009307832613631616334020d500b1d8e8ef31e21c99d1db9a6444d3adf12700000000406fdde030d500b1d8e8ef31e21c99d1db9a6444d3adf12700000000418160ddd", + response: hex"0000000002a61ac4c1adff9f6e180309e7d0d94c063338ddc61c1c4474cd6957c960efe659534d040005ff312e4f90c002000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d57726170706564204d6174696300000000000000000000000000000000000000000000200000000000000000000000000000000000000000007ae5649beabeddf889364a" + }); + + EthCallQueryResponse memory eqr = queryResponse.parseEthCallQueryResponse(r); + assertEq(eqr.requestBlockId, hex"307832613631616334"); + assertEq(eqr.blockNum, 44440260); + assertEq(eqr.blockHash, hex"c1adff9f6e180309e7d0d94c063338ddc61c1c4474cd6957c960efe659534d04"); + assertEq(eqr.blockTime, 1687961579000000); + assertEq(eqr.result.length, 2); + + assertEq(eqr.result[0].contractAddress, address(0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270)); + assertEq(eqr.result[0].callData, hex"06fdde03"); + assertEq(eqr.result[0].result, hex"0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d57726170706564204d6174696300000000000000000000000000000000000000"); + + assertEq(eqr.result[1].contractAddress, address(0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270)); + assertEq(eqr.result[1].callData, hex"18160ddd"); + assertEq(eqr.result[1].result, hex"0000000000000000000000000000000000000000007ae5649beabeddf889364a"); + } + + function test_parseEthCallQueryResponseRevertWrongQueryType() public { + // Take the data extracted by the previous test and break it down even further. + ParsedPerChainQueryResponse memory r = ParsedPerChainQueryResponse({ + chainId: 5, + queryType: 2, + request: hex"00000009307832613631616334020d500b1d8e8ef31e21c99d1db9a6444d3adf12700000000406fdde030d500b1d8e8ef31e21c99d1db9a6444d3adf12700000000418160ddd", + response: hex"0000000002a61ac4c1adff9f6e180309e7d0d94c063338ddc61c1c4474cd6957c960efe659534d040005ff312e4f90c002000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d57726170706564204d6174696300000000000000000000000000000000000000000000200000000000000000000000000000000000000000007ae5649beabeddf889364a" + }); + + vm.expectRevert(abi.encodeWithSelector(WrongQueryType.selector, 2, queryResponse.QT_ETH_CALL())); + queryResponse.parseEthCallQueryResponse(r); + } + + function test_parseEthCallQueryResponseComparison() public { + ParsedPerChainQueryResponse memory r = ParsedPerChainQueryResponse({ + chainId: 23, + queryType: 1, + request: hex"00000009307832376433333433013ce792601c936b1c81f73ea2fa77208c0a478bae00000004916d5743", + response: hex"00000000027d3343b9848f128b3658a0b9b50aa174e3ddc15ac4e54c84ee534b6d247adbdfc300c90006056cda47a84001000000200000000000000000000000000000000000000000000000000000000000000004" + }); + + EthCallQueryResponse memory eqr = queryResponse.parseEthCallQueryResponse(r); + assertEq(eqr.requestBlockId, "0x27d3343"); + assertEq(eqr.blockNum, 0x27d3343); + assertEq(eqr.blockHash, hex"b9848f128b3658a0b9b50aa174e3ddc15ac4e54c84ee534b6d247adbdfc300c9"); + vm.warp(1694814937); + assertEq(eqr.blockTime / 1_000_000, block.timestamp); + assertEq(eqr.result.length, 1); + + assertEq(eqr.result[0].contractAddress, address(0x3ce792601c936b1c81f73Ea2fa77208C0A478BaE)); + assertEq(eqr.result[0].callData, hex"916d5743"); + bytes memory callData = eqr.result[0].callData; + bytes4 callSignature; + assembly { + callSignature := mload(add(callData, 32)) + } + assertEq(callSignature, bytes4(keccak256("getMyCounter()"))); + assertEq(eqr.result[0].result, hex"0000000000000000000000000000000000000000000000000000000000000004"); + assertEq(abi.decode(eqr.result[0].result, (uint256)), 4); + } + + function test_parseEthCallByTimestampQueryResponse() public { + // Take the data extracted by the previous test and break it down even further. + ParsedPerChainQueryResponse memory r = ParsedPerChainQueryResponse({ + chainId: 2, + queryType: 2, + request: hex"00000003f4810cc0000000063078343237310000000630783432373202ddb64fe46a91d46ee29420539fc25fd07c5fea3e0000000406fdde03ddb64fe46a91d46ee29420539fc25fd07c5fea3e0000000418160ddd", + response: hex"0000000000004271ec70d2f70cf1933770ae760050a75334ce650aa091665ee43a6ed488cd154b0800000003f4810cc000000000000042720b1608c2cddfd9d7fb4ec94f79ec1389e2410e611a2c2bbde94e9ad37519ebbb00000003f4904f0002000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d5772617070656420457468657200000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000" + }); + + EthCallByTimestampQueryResponse memory eqr = queryResponse.parseEthCallByTimestampQueryResponse(r); + assertEq(eqr.requestTargetBlockIdHint, hex"307834323731"); + assertEq(eqr.requestFollowingBlockIdHint, hex"307834323732"); + assertEq(eqr.requestTargetTimestamp, 0x03f4810cc0); + assertEq(eqr.targetBlockNum, 0x0000000000004271); + assertEq(eqr.targetBlockHash, hex"ec70d2f70cf1933770ae760050a75334ce650aa091665ee43a6ed488cd154b08"); + assertEq(eqr.targetBlockTime, 0x03f4810cc0); + assertEq(eqr.followingBlockNum, 0x0000000000004272); + assertEq(eqr.followingBlockHash, hex"0b1608c2cddfd9d7fb4ec94f79ec1389e2410e611a2c2bbde94e9ad37519ebbb"); + assertEq(eqr.followingBlockTime, 0x03f4904f00); + assertEq(eqr.result.length, 2); + + assertEq(eqr.result[0].contractAddress, address(0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E)); + assertEq(eqr.result[0].callData, hex"06fdde03"); + assertEq(eqr.result[0].result, hex"0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d5772617070656420457468657200000000000000000000000000000000000000"); + + assertEq(eqr.result[1].contractAddress, address(0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E)); + assertEq(eqr.result[1].callData, hex"18160ddd"); + assertEq(eqr.result[1].result, hex"0000000000000000000000000000000000000000000000000000000000000000"); + } + + function test_parseEthCallByTimestampQueryResponseRevertWrongQueryType() public { + // Take the data extracted by the previous test and break it down even further. + ParsedPerChainQueryResponse memory r = ParsedPerChainQueryResponse({ + chainId: 2, + queryType: 1, + request: hex"00000003f4810cc0000000063078343237310000000630783432373202ddb64fe46a91d46ee29420539fc25fd07c5fea3e0000000406fdde03ddb64fe46a91d46ee29420539fc25fd07c5fea3e0000000418160ddd", + response: hex"0000000000004271ec70d2f70cf1933770ae760050a75334ce650aa091665ee43a6ed488cd154b0800000003f4810cc000000000000042720b1608c2cddfd9d7fb4ec94f79ec1389e2410e611a2c2bbde94e9ad37519ebbb00000003f4904f0002000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d5772617070656420457468657200000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000" + }); + + vm.expectRevert(abi.encodeWithSelector(WrongQueryType.selector, 1, queryResponse.QT_ETH_CALL_BY_TIMESTAMP())); + queryResponse.parseEthCallByTimestampQueryResponse(r); + } + + function test_parseEthCallWithFinalityQueryResponse() public { + // Take the data extracted by the previous test and break it down even further. + ParsedPerChainQueryResponse memory r = ParsedPerChainQueryResponse({ + chainId: 2, + queryType: 3, + request: hex"000000063078363032390000000966696e616c697a656402ddb64fe46a91d46ee29420539fc25fd07c5fea3e0000000406fdde03ddb64fe46a91d46ee29420539fc25fd07c5fea3e0000000418160ddd", + response: hex"00000000000060299eb9c56ffdae81214867ed217f5ab37e295c196b4f04b23a795d3e4aea6ff3d700000005bb1bd58002000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d5772617070656420457468657200000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000" + }); + + EthCallWithFinalityQueryResponse memory eqr = queryResponse.parseEthCallWithFinalityQueryResponse(r); + assertEq(eqr.requestBlockId, hex"307836303239"); + assertEq(eqr.requestFinality, hex"66696e616c697a6564"); + assertEq(eqr.blockNum, 0x6029); + assertEq(eqr.blockHash, hex"9eb9c56ffdae81214867ed217f5ab37e295c196b4f04b23a795d3e4aea6ff3d7"); + assertEq(eqr.blockTime, 0x05bb1bd580); + assertEq(eqr.result.length, 2); + + assertEq(eqr.result[0].contractAddress, address(0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E)); + assertEq(eqr.result[0].callData, hex"06fdde03"); + assertEq(eqr.result[0].result, hex"0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d5772617070656420457468657200000000000000000000000000000000000000"); + + assertEq(eqr.result[1].contractAddress, address(0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E)); + assertEq(eqr.result[1].callData, hex"18160ddd"); + assertEq(eqr.result[1].result, hex"0000000000000000000000000000000000000000000000000000000000000000"); + } + + function test_parseEthCallWithFinalityQueryResponseRevertWrongQueryType() public { + // Take the data extracted by the previous test and break it down even further. + ParsedPerChainQueryResponse memory r = ParsedPerChainQueryResponse({ + chainId: 2, + queryType: 1, + request: hex"000000063078363032390000000966696e616c697a656402ddb64fe46a91d46ee29420539fc25fd07c5fea3e0000000406fdde03ddb64fe46a91d46ee29420539fc25fd07c5fea3e0000000418160ddd", + response: hex"00000000000060299eb9c56ffdae81214867ed217f5ab37e295c196b4f04b23a795d3e4aea6ff3d700000005bb1bd58002000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d5772617070656420457468657200000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000" + }); + + vm.expectRevert(abi.encodeWithSelector(WrongQueryType.selector, 1, queryResponse.QT_ETH_CALL_WITH_FINALITY())); + queryResponse.parseEthCallWithFinalityQueryResponse(r); + } + + // Start of Solana Stuff /////////////////////////////////////////////////////////////////////////// + + function test_verifyQueryResponseSignaturesForSolana() public view { + bytes memory resp = concatenateQueryResponseBytesOffChain(version, senderChainId, solanaAccountSignature, solanaAccountQueryRequestVersion, solanaAccountQueryRequestNonce, solanaAccountNumPerChainQueries, solanaAccountPerChainQueries, solanaAccountNumPerChainResponses, solanaAccountPerChainResponses); + (uint8 sigV, bytes32 sigR, bytes32 sigS) = getSignature(resp); + IWormhole.Signature[] memory signatures = new IWormhole.Signature[](1); + signatures[0] = IWormhole.Signature({r: sigR, s: sigS, v: sigV, guardianIndex: sigGuardianIndex}); + queryResponse.verifyQueryResponseSignatures(resp, signatures); + // TODO: There are no assertions for this test + } + + function test_parseSolanaAccountQueryResponse() public { + // Take the data extracted by the previous test and break it down even further. + ParsedPerChainQueryResponse memory r = ParsedPerChainQueryResponse({ + chainId: 1, + queryType: 4, + request: solanaAccountPerChainQueriesInner, + response: solanaAccountPerChainResponsesInner + }); + + SolanaAccountQueryResponse memory sar = queryResponse.parseSolanaAccountQueryResponse(r); + + assertEq(sar.requestCommitment, "finalized"); + assertEq(sar.requestMinContextSlot, 0); + assertEq(sar.requestDataSliceOffset, 0); + assertEq(sar.requestDataSliceLength, 0); + assertEq(sar.slotNumber, 0xd85f); + assertEq(sar.blockTime, 0x00060f3e9915ddc0); + assertEq(sar.blockHash, hex"3a8de2b1de609020bb0a0dcee594a8c06801619cf9ea2a498b9d910f9a25772b"); + assertEq(sar.results.length, 2); + + assertEq(sar.results[0].account, hex"165809739240a0ac03b98440fe8985548e3aa683cd0d4d9df5b5659669faa301"); + assertEq(sar.results[0].lamports, 0x164d60); + assertEq(sar.results[0].rentEpoch, 0); + assertEq(sar.results[0].executable, false); + assertEq(sar.results[0].owner, hex"06ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a9"); + assertEq(sar.results[0].data, hex"01000000574108aed69daf7e625a361864b1f74d13702f2ca56de9660e566d1d8691848d0000e8890423c78a0901000000000000000000000000000000000000000000000000000000000000000000000000"); + + assertEq(sar.results[1].account, hex"9c006c48c8cbf33849cb07a3f936159cc523f9591cb1999abd45890ec5fee9b7"); + assertEq(sar.results[1].lamports, 0x164d60); + assertEq(sar.results[1].rentEpoch, 0); + assertEq(sar.results[1].executable, false); + assertEq(sar.results[1].owner, hex"06ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a9"); + assertEq(sar.results[1].data, hex"01000000574108aed69daf7e625a361864b1f74d13702f2ca56de9660e566d1d8691848d01000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000"); + } + + function test_parseSolanaAccountQueryResponseRevertWrongQueryType() public { + // Pass an ETH per chain response into the Solana parser. + ParsedPerChainQueryResponse memory r = ParsedPerChainQueryResponse({ + chainId: 2, + queryType: 1, + request: solanaAccountPerChainQueriesInner, + response: solanaAccountPerChainResponsesInner + }); + + vm.expectRevert(abi.encodeWithSelector(WrongQueryType.selector, 1, queryResponse.QT_SOL_ACCOUNT())); + queryResponse.parseSolanaAccountQueryResponse(r); + } + + function test_parseSolanaAccountQueryResponseRevertUnexpectedNumberOfResults() public { + // Only one account on the request but two in the response. + bytes memory requestWithOnlyOneAccount = hex"0000000966696e616c697a656400000000000000000000000000000000000000000000000001165809739240a0ac03b98440fe8985548e3aa683cd0d4d9df5b5659669faa301"; + ParsedPerChainQueryResponse memory r = ParsedPerChainQueryResponse({ + chainId: 1, + queryType: 4, + request: requestWithOnlyOneAccount, + response: solanaAccountPerChainResponsesInner + }); + + vm.expectRevert(UnexpectedNumberOfResults.selector); + queryResponse.parseSolanaAccountQueryResponse(r); + } + + function test_parseSolanaAccountQueryResponseExtraRequestBytesRevertInvalidPayloadLength() public { + // Extra bytes at the end of the request. + bytes memory requestWithExtraBytes = hex"0000000966696e616c697a656400000000000000000000000000000000000000000000000002165809739240a0ac03b98440fe8985548e3aa683cd0d4d9df5b5659669faa3019c006c48c8cbf33849cb07a3f936159cc523f9591cb1999abd45890ec5fee9b7DEADBEEF"; + ParsedPerChainQueryResponse memory r = ParsedPerChainQueryResponse({ + chainId: 1, + queryType: 4, + request: requestWithExtraBytes, + response: solanaAccountPerChainResponsesInner + }); + + vm.expectRevert(abi.encodeWithSelector(InvalidPayloadLength.selector, 106, 102)); + queryResponse.parseSolanaAccountQueryResponse(r); + } + + function test_parseSolanaAccountQueryResponseExtraResponseBytesRevertInvalidPayloadLength() public { + // Extra bytes at the end of the response. + bytes memory responseWithExtraBytes = hex"000000000000d85f00060f3e9915ddc03a8de2b1de609020bb0a0dcee594a8c06801619cf9ea2a498b9d910f9a25772b020000000000164d6000000000000000000006ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a90000005201000000574108aed69daf7e625a361864b1f74d13702f2ca56de9660e566d1d8691848d0000e8890423c78a09010000000000000000000000000000000000000000000000000000000000000000000000000000000000164d6000000000000000000006ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a90000005201000000574108aed69daf7e625a361864b1f74d13702f2ca56de9660e566d1d8691848d01000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000DEADBEEF"; + ParsedPerChainQueryResponse memory r = ParsedPerChainQueryResponse({ + chainId: 1, + queryType: 4, + request: solanaAccountPerChainQueriesInner, + response: responseWithExtraBytes + }); + + vm.expectRevert(abi.encodeWithSelector(InvalidPayloadLength.selector, 323, 319)); + queryResponse.parseSolanaAccountQueryResponse(r); + } + + function test_parseSolanaPdaQueryResponse() public { + // Take the data extracted by the previous test and break it down even further. + ParsedPerChainQueryResponse memory r = ParsedPerChainQueryResponse({ + chainId: 1, + queryType: 5, + request: solanaPdaPerChainQueriesInner, + response: solanaPdaPerChainResponsesInner + }); + + SolanaPdaQueryResponse memory sar = queryResponse.parseSolanaPdaQueryResponse(r); + + assertEq(sar.requestCommitment, "finalized"); + assertEq(sar.requestMinContextSlot, 2303); + assertEq(sar.requestDataSliceOffset, 12); + assertEq(sar.requestDataSliceLength, 20); + assertEq(sar.slotNumber, 2303); + assertEq(sar.blockTime, 0x0006115e3f6d7540); + assertEq(sar.blockHash, hex"e05035785e15056a8559815e71343ce31db2abf23f65b19c982b68aee7bf207b"); + assertEq(sar.results.length, 1); + + assertEq(sar.results[0].programId, hex"02c806312cbe5b79ef8aa6c17e3f423d8fdfe1d46909fb1f6cdf65ee8e2e6faa"); + assertEq(sar.results[0].seeds.length, 2); + assertEq(sar.results[0].seeds[0], hex"477561726469616e536574"); + assertEq(sar.results[0].seeds[1], hex"00000000"); + + assertEq(sar.results[0].account, hex"4fa9188b339cfd573a0778c5deaeeee94d4bcfb12b345bf8e417e5119dae773e"); + assertEq(sar.results[0].bump, 253); + assertEq(sar.results[0].lamports, 0x116ac0); + assertEq(sar.results[0].rentEpoch, 0); + assertEq(sar.results[0].executable, false); + assertEq(sar.results[0].owner, hex"02c806312cbe5b79ef8aa6c17e3f423d8fdfe1d46909fb1f6cdf65ee8e2e6faa"); + assertEq(sar.results[0].data, hex"57cd18b7f8a4d91a2da9ab4af05d0fbece2dcd65"); + } + + function test_parseSolanaPdaQueryResponseRevertWrongQueryType() public { + // Pass an ETH per chain response into the Solana parser. + ParsedPerChainQueryResponse memory r = ParsedPerChainQueryResponse({ + chainId: 2, + queryType: 1, + request: solanaPdaPerChainQueriesInner, + response: solanaPdaPerChainResponsesInner + }); + + vm.expectRevert(abi.encodeWithSelector(WrongQueryType.selector, 1, queryResponse.QT_SOL_PDA())); + queryResponse.parseSolanaPdaQueryResponse(r); + } + + function test_parseSolanaPdaQueryResponseRevertUnexpectedNumberOfResults() public { + // Only one Pda on the request but two in the response. + bytes memory requestWithTwoPdas = hex"0000000966696e616c697a656400000000000008ff000000000000000c00000000000000140202c806312cbe5b79ef8aa6c17e3f423d8fdfe1d46909fb1f6cdf65ee8e2e6faa020000000b477561726469616e536574000000040000000002c806312cbe5b79ef8aa6c17e3f423d8fdfe1d46909fb1f6cdf65ee8e2e6faa020000000b477561726469616e5365740000000400000000"; + ParsedPerChainQueryResponse memory r = ParsedPerChainQueryResponse({ + chainId: 1, + queryType: 5, + request: requestWithTwoPdas, + response: solanaPdaPerChainResponsesInner + }); + + vm.expectRevert(UnexpectedNumberOfResults.selector); + queryResponse.parseSolanaPdaQueryResponse(r); + } + + function test_parseSolanaPdaQueryResponseExtraRequestBytesRevertInvalidPayloadLength() public { + // Extra bytes at the end of the request. + bytes memory requestWithExtraBytes = hex"0000000966696e616c697a656400000000000008ff000000000000000c00000000000000140102c806312cbe5b79ef8aa6c17e3f423d8fdfe1d46909fb1f6cdf65ee8e2e6faa020000000b477561726469616e5365740000000400000000DEADBEEF"; + ParsedPerChainQueryResponse memory r = ParsedPerChainQueryResponse({ + chainId: 1, + queryType: 5, + request: requestWithExtraBytes, + response: solanaPdaPerChainResponsesInner + }); + + vm.expectRevert(abi.encodeWithSelector(InvalidPayloadLength.selector, 98, 94)); + queryResponse.parseSolanaPdaQueryResponse(r); + } + + function test_parseSolanaPdaQueryResponseExtraResponseBytesRevertInvalidPayloadLength() public { + // Extra bytes at the end of the response. + bytes memory responseWithExtraBytes = hex"00000000000008ff0006115e3f6d7540e05035785e15056a8559815e71343ce31db2abf23f65b19c982b68aee7bf207b014fa9188b339cfd573a0778c5deaeeee94d4bcfb12b345bf8e417e5119dae773efd0000000000116ac000000000000000000002c806312cbe5b79ef8aa6c17e3f423d8fdfe1d46909fb1f6cdf65ee8e2e6faa0000001457cd18b7f8a4d91a2da9ab4af05d0fbece2dcd65DEADBEEF"; + ParsedPerChainQueryResponse memory r = ParsedPerChainQueryResponse({ + chainId: 1, + queryType: 5, + request: solanaPdaPerChainQueriesInner, + response: responseWithExtraBytes + }); + + vm.expectRevert(abi.encodeWithSelector(InvalidPayloadLength.selector, 159, 155)); + queryResponse.parseSolanaPdaQueryResponse(r); + } + + /*********************************** + *********** FUZZ TESTS ************* + ***********************************/ + + + + function testFuzz_parseAndVerifyQueryResponse_fuzzVersion(uint8 _version) public { + vm.assume(_version != 1); + + bytes memory resp = concatenateQueryResponseBytesOffChain(_version, senderChainId, signature, queryRequestVersion, queryRequestNonce, numPerChainQueries, perChainQueries, numPerChainResponses, perChainResponses); + (uint8 sigV, bytes32 sigR, bytes32 sigS) = getSignature(resp); + IWormhole.Signature[] memory signatures = new IWormhole.Signature[](1); + signatures[0] = IWormhole.Signature({r: sigR, s: sigS, v: sigV, guardianIndex: sigGuardianIndex}); + vm.expectRevert(InvalidResponseVersion.selector); + queryResponse.parseAndVerifyQueryResponse(resp, signatures); + } + + function testFuzz_parseAndVerifyQueryResponse_fuzzSenderChainId(uint16 _senderChainId) public { + vm.assume(_senderChainId != 0); + + bytes memory resp = concatenateQueryResponseBytesOffChain(version, _senderChainId, signature, queryRequestVersion, queryRequestNonce, numPerChainQueries, perChainQueries, numPerChainResponses, perChainResponses); + (uint8 sigV, bytes32 sigR, bytes32 sigS) = getSignature(resp); + IWormhole.Signature[] memory signatures = new IWormhole.Signature[](1); + signatures[0] = IWormhole.Signature({r: sigR, s: sigS, v: sigV, guardianIndex: sigGuardianIndex}); + // This could revert for multiple reasons. But the checkLength to ensure all the bytes are consumed is the backstop. + vm.expectRevert(); + queryResponse.parseAndVerifyQueryResponse(resp, signatures); + } + + function testFuzz_parseAndVerifyQueryResponse_fuzzSignatureHappyCase(bytes memory _signature) public { + // This signature isn't validated in the QueryResponse library, therefore it could be an 65 byte hex string + vm.assume(_signature.length == 65); + + bytes memory resp = concatenateQueryResponseBytesOffChain(version, senderChainId, _signature, queryRequestVersion, queryRequestNonce, numPerChainQueries, perChainQueries, numPerChainResponses, perChainResponses); + (uint8 sigV, bytes32 sigR, bytes32 sigS) = getSignature(resp); + IWormhole.Signature[] memory signatures = new IWormhole.Signature[](1); + signatures[0] = IWormhole.Signature({r: sigR, s: sigS, v: sigV, guardianIndex: sigGuardianIndex}); + ParsedQueryResponse memory r = queryResponse.parseAndVerifyQueryResponse(resp, signatures); + + assertEq(r.requestId, _signature); + } + + function testFuzz_parseAndVerifyQueryResponse_fuzzSignatureUnhappyCase(bytes memory _signature) public { + // A signature that isn't 65 bytes long will always lead to a revert. The type of revert is unknown since it could be one of many. + vm.assume(_signature.length != 65); + + bytes memory resp = concatenateQueryResponseBytesOffChain(version, senderChainId, _signature, queryRequestVersion, queryRequestNonce, numPerChainQueries, perChainQueries, numPerChainResponses, perChainResponses); + (uint8 sigV, bytes32 sigR, bytes32 sigS) = getSignature(resp); + IWormhole.Signature[] memory signatures = new IWormhole.Signature[](1); + signatures[0] = IWormhole.Signature({r: sigR, s: sigS, v: sigV, guardianIndex: sigGuardianIndex}); + vm.expectRevert(); + queryResponse.parseAndVerifyQueryResponse(resp, signatures); + } + + function testFuzz_parseAndVerifyQueryResponse_fuzzQueryRequestLen(uint32 _queryRequestLen, bytes calldata _perChainQueries) public { + // We add 6 to account for version + nonce + numPerChainQueries + vm.assume(_queryRequestLen != _perChainQueries.length + 6); + + bytes memory resp = concatenateQueryResponseBytesOffChain(version, senderChainId, signature, queryRequestVersion, queryRequestNonce, numPerChainQueries, _perChainQueries, numPerChainResponses, perChainResponses); + (uint8 sigV, bytes32 sigR, bytes32 sigS) = getSignature(resp); + IWormhole.Signature[] memory signatures = new IWormhole.Signature[](1); + signatures[0] = IWormhole.Signature({r: sigR, s: sigS, v: sigV, guardianIndex: sigGuardianIndex}); + vm.expectRevert(); + queryResponse.parseAndVerifyQueryResponse(resp, signatures); + } + + function testFuzz_parseAndVerifyQueryResponse_fuzzQueryRequestVersion(uint8 _version, uint8 _queryRequestVersion) public { + vm.assume(_version != _queryRequestVersion); + + bytes memory resp = concatenateQueryResponseBytesOffChain(_version, senderChainId, signature, _queryRequestVersion, queryRequestNonce, numPerChainQueries, perChainQueries, numPerChainResponses, perChainResponses); + (uint8 sigV, bytes32 sigR, bytes32 sigS) = getSignature(resp); + IWormhole.Signature[] memory signatures = new IWormhole.Signature[](1); + signatures[0] = IWormhole.Signature({r: sigR, s: sigS, v: sigV, guardianIndex: sigGuardianIndex}); + vm.expectRevert(); + queryResponse.parseAndVerifyQueryResponse(resp, signatures); + } + + function testFuzz_parseAndVerifyQueryResponse_fuzzQueryRequestNonce(uint32 _queryRequestNonce) public { + bytes memory resp = concatenateQueryResponseBytesOffChain(version, senderChainId, signature, queryRequestVersion, _queryRequestNonce, numPerChainQueries, perChainQueries, numPerChainResponses, perChainResponses); + (uint8 sigV, bytes32 sigR, bytes32 sigS) = getSignature(resp); + IWormhole.Signature[] memory signatures = new IWormhole.Signature[](1); + signatures[0] = IWormhole.Signature({r: sigR, s: sigS, v: sigV, guardianIndex: sigGuardianIndex}); + ParsedQueryResponse memory r = queryResponse.parseAndVerifyQueryResponse(resp, signatures); + + assertEq(r.nonce, _queryRequestNonce); + } + + function testFuzz_parseAndVerifyQueryResponse_fuzzNumPerChainQueriesAndResponses(uint8 _numPerChainQueries, uint8 _numPerChainResponses) public { + vm.assume(_numPerChainQueries != _numPerChainResponses); + + bytes memory resp = concatenateQueryResponseBytesOffChain(version, senderChainId, signature, queryRequestVersion, queryRequestNonce, _numPerChainQueries, perChainQueries, _numPerChainResponses, perChainResponses); + (uint8 sigV, bytes32 sigR, bytes32 sigS) = getSignature(resp); + IWormhole.Signature[] memory signatures = new IWormhole.Signature[](1); + signatures[0] = IWormhole.Signature({r: sigR, s: sigS, v: sigV, guardianIndex: sigGuardianIndex}); + vm.expectRevert(); + queryResponse.parseAndVerifyQueryResponse(resp, signatures); + } + + function testFuzz_parseAndVerifyQueryResponse_fuzzChainIds(uint16 _requestChainId, uint16 _responseChainId, uint256 _requestQueryType) public { + vm.assume(_requestChainId != _responseChainId); + _requestQueryType = bound({x: _requestQueryType, min: queryResponse.QT_ETH_CALL(), max: queryResponse.QT_MAX() - 1}); + + bytes memory packedPerChainQueries = abi.encodePacked(_requestChainId, uint8(_requestQueryType), uint32(perChainQueriesInner.length), perChainQueriesInner); + bytes memory packedPerChainResponses = abi.encodePacked(_responseChainId, uint8(_requestQueryType), uint32(perChainResponsesInner.length), perChainResponsesInner); + bytes memory resp = concatenateQueryResponseBytesOffChain(version, senderChainId, signature, queryRequestVersion, queryRequestNonce, numPerChainQueries, packedPerChainQueries, numPerChainResponses, packedPerChainResponses); + (uint8 sigV, bytes32 sigR, bytes32 sigS) = getSignature(resp); + IWormhole.Signature[] memory signatures = new IWormhole.Signature[](1); + signatures[0] = IWormhole.Signature({r: sigR, s: sigS, v: sigV, guardianIndex: sigGuardianIndex}); + vm.expectRevert(ChainIdMismatch.selector); + queryResponse.parseAndVerifyQueryResponse(resp, signatures); + } + + function testFuzz_parseAndVerifyQueryResponse_fuzzMistmatchedRequestType(uint256 _requestQueryType, uint256 _responseQueryType) public { + _requestQueryType = bound({x: _requestQueryType, min: queryResponse.QT_ETH_CALL(), max: queryResponse.QT_MAX() - 1}); + _responseQueryType = bound({x: _responseQueryType, min: queryResponse.QT_ETH_CALL(), max: queryResponse.QT_MAX() - 1}); + vm.assume(_requestQueryType != _responseQueryType); + + bytes memory packedPerChainQueries = abi.encodePacked(uint16(0x0005), uint8(_requestQueryType), uint32(perChainQueriesInner.length), perChainQueriesInner); + bytes memory packedPerChainResponses = abi.encodePacked(uint16(0x0005), uint8(_responseQueryType), uint32(perChainResponsesInner.length), perChainResponsesInner); + bytes memory resp = concatenateQueryResponseBytesOffChain(version, senderChainId, signature, queryRequestVersion, queryRequestNonce, numPerChainQueries, packedPerChainQueries, numPerChainResponses, packedPerChainResponses); + (uint8 sigV, bytes32 sigR, bytes32 sigS) = getSignature(resp); + IWormhole.Signature[] memory signatures = new IWormhole.Signature[](1); + signatures[0] = IWormhole.Signature({r: sigR, s: sigS, v: sigV, guardianIndex: sigGuardianIndex}); + vm.expectRevert(RequestTypeMismatch.selector); + queryResponse.parseAndVerifyQueryResponse(resp, signatures); + } + + function testFuzz_parseAndVerifyQueryResponse_fuzzUnsupportedRequestType(uint8 _requestQueryType) public { + vm.assume(_requestQueryType < queryResponse.QT_ETH_CALL() || _requestQueryType >= queryResponse.QT_MAX()); + + bytes memory packedPerChainQueries = abi.encodePacked(uint16(0x0005), uint8(_requestQueryType), uint32(perChainQueriesInner.length), perChainQueriesInner); + bytes memory packedPerChainResponses = abi.encodePacked(uint16(0x0005), uint8(_requestQueryType), uint32(perChainResponsesInner.length), perChainResponsesInner); + bytes memory resp = concatenateQueryResponseBytesOffChain(version, senderChainId, signature, queryRequestVersion, queryRequestNonce, numPerChainQueries, packedPerChainQueries, numPerChainResponses, packedPerChainResponses); + (uint8 sigV, bytes32 sigR, bytes32 sigS) = getSignature(resp); + IWormhole.Signature[] memory signatures = new IWormhole.Signature[](1); + signatures[0] = IWormhole.Signature({r: sigR, s: sigS, v: sigV, guardianIndex: sigGuardianIndex}); + vm.expectRevert(abi.encodeWithSelector(UnsupportedQueryType.selector, _requestQueryType)); + queryResponse.parseAndVerifyQueryResponse(resp, signatures); + } + + function testFuzz_parseAndVerifyQueryResponse_fuzzQueryBytesLength(uint32 _queryLength) public { + vm.assume(_queryLength != uint32(perChainQueriesInner.length)); + + bytes memory packedPerChainQueries = abi.encodePacked(uint16(0x0005), uint8(0x01), _queryLength, perChainQueriesInner); + bytes memory packedPerChainResponses = abi.encodePacked(uint16(0x0005), uint8(0x01), uint32(perChainResponsesInner.length), perChainResponsesInner); + bytes memory resp = concatenateQueryResponseBytesOffChain(version, senderChainId, signature, queryRequestVersion, queryRequestNonce, numPerChainQueries, packedPerChainQueries, numPerChainResponses, packedPerChainResponses); + (uint8 sigV, bytes32 sigR, bytes32 sigS) = getSignature(resp); + IWormhole.Signature[] memory signatures = new IWormhole.Signature[](1); + signatures[0] = IWormhole.Signature({r: sigR, s: sigS, v: sigV, guardianIndex: sigGuardianIndex}); + vm.expectRevert(); + queryResponse.parseAndVerifyQueryResponse(resp, signatures); + } + + function testFuzz_verifyQueryResponseSignatures_validSignature(bytes calldata resp) public view { + // This should pass with a valid signature of any payload + (uint8 sigV, bytes32 sigR, bytes32 sigS) = getSignature(resp); + IWormhole.Signature[] memory signatures = new IWormhole.Signature[](1); + signatures[0] = IWormhole.Signature({r: sigR, s: sigS, v: sigV, guardianIndex: sigGuardianIndex}); + queryResponse.verifyQueryResponseSignatures(resp, signatures); + } + + function testFuzz_verifyQueryResponseSignatures_invalidSignature(bytes calldata resp, uint256 privateKey) public { + vm.assume(privateKey != DEVNET_GUARDIAN_PRIVATE_KEY); + // Less than secp256k1 curve + vm.assume(privateKey < 115792089237316195423570985008687907852837564279074904382605163141518161494337); + vm.assume(privateKey != 0); + + (uint8 sigV, bytes32 sigR, bytes32 sigS) = vm.sign(privateKey, queryResponse.getResponseDigest(resp)); + IWormhole.Signature[] memory signatures = new IWormhole.Signature[](1); + signatures[0] = IWormhole.Signature({r: sigR, s: sigS, v: sigV, guardianIndex: sigGuardianIndex}); + vm.expectRevert("VM signature invalid"); + queryResponse.verifyQueryResponseSignatures(resp, signatures); + } + + function testFuzz_verifyQueryResponseSignatures_validSignatureWrongPrefix(bytes calldata responsePrefix) public { + vm.assume(keccak256(responsePrefix) != keccak256(queryResponse.responsePrefix())); + + bytes memory resp = concatenateQueryResponseBytesOffChain(version, senderChainId, signature, queryRequestVersion, queryRequestNonce, numPerChainQueries, perChainQueries, numPerChainResponses, perChainResponses); + bytes32 responseDigest = keccak256(abi.encodePacked(responsePrefix, keccak256(resp))); + + (uint8 sigV, bytes32 sigR, bytes32 sigS) = vm.sign(DEVNET_GUARDIAN_PRIVATE_KEY, responseDigest); + IWormhole.Signature[] memory signatures = new IWormhole.Signature[](1); + signatures[0] = IWormhole.Signature({r: sigR, s: sigS, v: sigV, guardianIndex: sigGuardianIndex}); + vm.expectRevert("VM signature invalid"); + queryResponse.verifyQueryResponseSignatures(resp, signatures); + } + + function testFuzz_validateBlockTime_success(uint256 _blockTime, uint256 _minBlockTime) public view { + _blockTime = bound(_blockTime, 0, type(uint64).max/1_000_000); + vm.assume(_blockTime >= _minBlockTime); + + queryResponse.validateBlockTime(uint64(_blockTime * 1_000_000), _minBlockTime); + } + + function testFuzz_validateBlockTime_fail(uint256 _blockTime, uint256 _minBlockTime) public { + _blockTime = bound(_blockTime, 0, type(uint64).max/1_000_000); + vm.assume(_blockTime < _minBlockTime); + + vm.expectRevert(StaleBlockTime.selector); + queryResponse.validateBlockTime(uint64(_blockTime * 1_000_000), _minBlockTime); + } + + function testFuzz_validateBlockNum_success(uint64 _blockNum, uint256 _minBlockNum) public view { + vm.assume(_blockNum >= _minBlockNum); + + queryResponse.validateBlockNum(_blockNum, _minBlockNum); + } + + function testFuzz_validateBlockNum_fail(uint64 _blockNum, uint256 _minBlockNum) public { + vm.assume(_blockNum < _minBlockNum); + + vm.expectRevert(StaleBlockNum.selector); + queryResponse.validateBlockNum(_blockNum, _minBlockNum); + } + + function testFuzz_validateChainId_success(uint16 _validChainIndex, uint16[] memory _validChainIds) public view { + vm.assume(_validChainIndex < _validChainIds.length); + + queryResponse.validateChainId(_validChainIds[_validChainIndex], _validChainIds); + } + + function testFuzz_validateChainId_fail(uint16 _chainId, uint16[] memory _validChainIds) public { + for (uint16 i = 0; i < _validChainIds.length; ++i) { + vm.assume(_chainId != _validChainIds[i]); + } + + vm.expectRevert(InvalidChainId.selector); + queryResponse.validateChainId(_chainId, _validChainIds); + } + + function testFuzz_validateEthCallData_success(bytes memory randomBytes, uint256 _contractAddressIndex, uint256 _functionSignatureIndex, address[] memory _expectedContractAddresses, bytes4[] memory _expectedFunctionSignatures) public view { + vm.assume(_contractAddressIndex < _expectedContractAddresses.length); + vm.assume(_functionSignatureIndex < _expectedFunctionSignatures.length); + + EthCallData memory callData = EthCallData({ + contractAddress: _expectedContractAddresses[_contractAddressIndex], + callData: bytes.concat(_expectedFunctionSignatures[_functionSignatureIndex], randomBytes), + result: randomBytes + }); + + queryResponse.validateEthCallData(callData, _expectedContractAddresses, _expectedFunctionSignatures); + } + + function testFuzz_validateEthCallData_successZeroSignatures(bytes4 randomSignature, bytes memory randomBytes, uint256 _contractAddressIndex, address[] memory _expectedContractAddresses) public view { + vm.assume(_contractAddressIndex < _expectedContractAddresses.length); + + EthCallData memory callData = EthCallData({ + contractAddress: _expectedContractAddresses[_contractAddressIndex], + callData: bytes.concat(randomSignature, randomBytes), + result: randomBytes + }); + + bytes4[] memory validSignatures = new bytes4[](0); + + queryResponse.validateEthCallData(callData, _expectedContractAddresses, validSignatures); + } + + function testFuzz_validateEthCallData_successZeroAddresses(address randomAddress, bytes memory randomBytes, uint256 _functionSignatureIndex, bytes4[] memory _expectedFunctionSignatures) public view { + vm.assume(_functionSignatureIndex < _expectedFunctionSignatures.length); + + EthCallData memory callData = EthCallData({ + contractAddress: randomAddress, + callData: bytes.concat(_expectedFunctionSignatures[_functionSignatureIndex], randomBytes), + result: randomBytes + }); + + address[] memory validAddresses = new address[](0); + + queryResponse.validateEthCallData(callData, validAddresses, _expectedFunctionSignatures); + } + + function testFuzz_validateEthCallData_failSignature(bytes memory randomBytes, uint256 _contractAddressIndex, address[] memory _expectedContractAddresses, bytes4[] memory _expectedFunctionSignatures) public { + vm.assume(_contractAddressIndex < _expectedContractAddresses.length); + vm.assume(_expectedFunctionSignatures.length > 0); + + for (uint256 i = 0; i < _expectedFunctionSignatures.length; ++i) { + vm.assume(bytes4(randomBytes) != _expectedFunctionSignatures[i]); + } + + EthCallData memory callData = EthCallData({ + contractAddress: _expectedContractAddresses[_contractAddressIndex], + callData: randomBytes, + result: randomBytes + }); + + vm.expectRevert(InvalidFunctionSignature.selector); + queryResponse.validateEthCallData(callData, _expectedContractAddresses, _expectedFunctionSignatures); + } + + function testFuzz_validateEthCallData_failAddress(bytes memory randomBytes, address randomAddress, uint256 _functionSignatureIndex, address[] memory _expectedContractAddresses, bytes4[] memory _expectedFunctionSignatures) public { + vm.assume(_functionSignatureIndex < _expectedFunctionSignatures.length); + vm.assume(_expectedContractAddresses.length > 0); + + for (uint256 i = 0; i < _expectedContractAddresses.length; ++i) { + vm.assume(randomAddress != _expectedContractAddresses[i]); + } + + EthCallData memory callData = EthCallData({ + contractAddress: randomAddress, + callData: bytes.concat(_expectedFunctionSignatures[_functionSignatureIndex], randomBytes), + result: randomBytes + }); + + vm.expectRevert(InvalidContractAddress.selector); + queryResponse.validateEthCallData(callData, _expectedContractAddresses, _expectedFunctionSignatures); + } + + function testFuzz_validateMultipleEthCallData_success(uint8 numInputs, bytes memory randomBytes, uint256 _contractAddressIndex, uint256 _functionSignatureIndex, address[] memory _expectedContractAddresses, bytes4[] memory _expectedFunctionSignatures) public view { + vm.assume(_contractAddressIndex < _expectedContractAddresses.length); + vm.assume(_functionSignatureIndex < _expectedFunctionSignatures.length); + + EthCallData[] memory callDatas = new EthCallData[](numInputs); + + for (uint256 i = 0; i < numInputs; ++i) { + callDatas[i] = EthCallData({ + contractAddress: _expectedContractAddresses[_contractAddressIndex], + callData: bytes.concat(_expectedFunctionSignatures[_functionSignatureIndex], randomBytes), + result: randomBytes + }); + } + + queryResponse.validateMultipleEthCallData(callDatas, _expectedContractAddresses, _expectedFunctionSignatures); + } +} diff --git a/test/QueryTest.t.sol b/test/QueryTest.t.sol new file mode 100644 index 0000000..1d63ac6 --- /dev/null +++ b/test/QueryTest.t.sol @@ -0,0 +1,292 @@ +// SPDX-License-Identifier: Apache 2 + +// forge test --match-contract QueryTest + +pragma solidity ^0.8.4; + +import "forge-std/Test.sol"; +import "../src/testing/helpers/QueryTest.sol"; + +contract TestQueryTest is Test { + // + // Query Request tests + // + + function test_buildOffChainQueryRequestBytes() public { + bytes memory req = QueryTest.buildOffChainQueryRequestBytes( + /* version */ 1, + /* nonce */ 1, + /* numPerChainQueries */ 1, + /* perChainQueries */ hex"0002010000004200000005307837343402ddb64fe46a91d46ee29420539fc25fd07c5fea3e0000000406fdde03ddb64fe46a91d46ee29420539fc25fd07c5fea3e00000004313ce567" + ); + assertEq(req, hex"0100000001010002010000004200000005307837343402ddb64fe46a91d46ee29420539fc25fd07c5fea3e0000000406fdde03ddb64fe46a91d46ee29420539fc25fd07c5fea3e00000004313ce567"); + } + + function test_buildPerChainRequestBytes() public { + bytes memory pcr = QueryTest.buildPerChainRequestBytes( + /* chainId */ 2, + /* queryType */ 1, + /* queryBytes */ hex"00000005307837343402ddb64fe46a91d46ee29420539fc25fd07c5fea3e0000000406fdde03ddb64fe46a91d46ee29420539fc25fd07c5fea3e00000004313ce567" + ); + assertEq(pcr, hex"0002010000004200000005307837343402ddb64fe46a91d46ee29420539fc25fd07c5fea3e0000000406fdde03ddb64fe46a91d46ee29420539fc25fd07c5fea3e00000004313ce567"); + } + + function test_buildEthCallRequestBytes() public { + bytes memory ecr = QueryTest.buildEthCallRequestBytes( + /* blockId */ "0x744", + /* numCallData */ 2, + /* callData */ hex"ddb64fe46a91d46ee29420539fc25fd07c5fea3e0000000406fdde03ddb64fe46a91d46ee29420539fc25fd07c5fea3e00000004313ce567" + ); + assertEq(ecr, hex"00000005307837343402ddb64fe46a91d46ee29420539fc25fd07c5fea3e0000000406fdde03ddb64fe46a91d46ee29420539fc25fd07c5fea3e00000004313ce567"); + } + + function test_buildEthCallByTimestampRequestBytes() public { + bytes memory ecr = QueryTest.buildEthCallByTimestampRequestBytes( + /* targetTimeUs */ 0x10642ac0, + /* targetBlockHint */ "0x15d", + /* followingBlockHint */ "0x15e", + /* numCallData */ 2, + /* callData */ hex"ddb64fe46a91d46ee29420539fc25fd07c5fea3e0000000406fdde03ddb64fe46a91d46ee29420539fc25fd07c5fea3e00000004313ce567" + ); + assertEq(ecr, hex"0000000010642ac000000005307831356400000005307831356502ddb64fe46a91d46ee29420539fc25fd07c5fea3e0000000406fdde03ddb64fe46a91d46ee29420539fc25fd07c5fea3e00000004313ce567"); + } + + function test_buildEthCallWithFinalityRequestBytes() public { + bytes memory ecr = QueryTest.buildEthCallWithFinalityRequestBytes( + /* blockId */ "0x1f8", + /* finality */ "finalized", + /* numCallData */ 2, + /* callData */ hex"ddb64fe46a91d46ee29420539fc25fd07c5fea3e0000000406fdde03ddb64fe46a91d46ee29420539fc25fd07c5fea3e00000004313ce567" + ); + assertEq(ecr, hex"0000000530783166380000000966696e616c697a656402ddb64fe46a91d46ee29420539fc25fd07c5fea3e0000000406fdde03ddb64fe46a91d46ee29420539fc25fd07c5fea3e00000004313ce567"); + } + + function test_buildEthCallDataBytes() public { + bytes memory ecd1 = QueryTest.buildEthCallDataBytes( + /* contractAddress */ 0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E, + /* callData */ hex"06fdde03" + ); + assertEq(ecd1, hex"ddb64fe46a91d46ee29420539fc25fd07c5fea3e0000000406fdde03"); + bytes memory ecd2 = QueryTest.buildEthCallDataBytes( + /* contractAddress */ 0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E, + /* callData */ hex"313ce567" + ); + assertEq(ecd2, hex"ddb64fe46a91d46ee29420539fc25fd07c5fea3e00000004313ce567"); + } + + function test_buildSolanaAccountRequestBytes() public { + bytes memory ecr = QueryTest.buildSolanaAccountRequestBytes( + /* commitment */ "finalized", + /* minContextSlot */ 8069, + /* dataSliceOffset */ 10, + /* dataSliceLength */ 20, + /* numAccounts */ 2, + /* accounts */ hex"165809739240a0ac03b98440fe8985548e3aa683cd0d4d9df5b5659669faa3019c006c48c8cbf33849cb07a3f936159cc523f9591cb1999abd45890ec5fee9b7" + ); + assertEq(ecr, hex"0000000966696e616c697a65640000000000001f85000000000000000a000000000000001402165809739240a0ac03b98440fe8985548e3aa683cd0d4d9df5b5659669faa3019c006c48c8cbf33849cb07a3f936159cc523f9591cb1999abd45890ec5fee9b7"); + } + + function test_buildSolanaPdaRequestBytes() public { + bytes32 programId = hex"02c806312cbe5b79ef8aa6c17e3f423d8fdfe1d46909fb1f6cdf65ee8e2e6faa"; + bytes[] memory pdas = new bytes[](2); + + bytes[] memory seeds = new bytes[](2); + seeds[0] = hex"477561726469616e536574"; + seeds[1] = hex"00000000"; + (bytes memory seedBytes, uint8 numSeeds) = QueryTest.buildSolanaPdaSeedBytes(seeds); + assertEq(seedBytes, hex"0000000b477561726469616e5365740000000400000000"); + + pdas[0] = QueryTest.buildSolanaPdaEntry( + programId, + numSeeds, + seedBytes + ); + assertEq(pdas[0], hex"02c806312cbe5b79ef8aa6c17e3f423d8fdfe1d46909fb1f6cdf65ee8e2e6faa020000000b477561726469616e5365740000000400000000"); + assertEq(numSeeds, uint8(seeds.length)); + + bytes[] memory seeds2 = new bytes[](2); + seeds2[0] = hex"477561726469616e536574"; + seeds2[1] = hex"00000001"; + (bytes memory seedBytes2, uint8 numSeeds2) = QueryTest.buildSolanaPdaSeedBytes(seeds2); + assertEq(seedBytes2, hex"0000000b477561726469616e5365740000000400000001"); + + pdas[1] = QueryTest.buildSolanaPdaEntry( + programId, + numSeeds2, + seedBytes2 + ); + assertEq(pdas[1], hex"02c806312cbe5b79ef8aa6c17e3f423d8fdfe1d46909fb1f6cdf65ee8e2e6faa020000000b477561726469616e5365740000000400000001"); + assertEq(numSeeds2, uint8(seeds2.length)); + + bytes memory ecr = QueryTest.buildSolanaPdaRequestBytes( + /* commitment */ "finalized", + /* minContextSlot */ 2303, + /* dataSliceOffset */ 12, + /* dataSliceLength */ 20, + /* pdas */ pdas + ); + assertEq(ecr, hex"0000000966696e616c697a656400000000000008ff000000000000000c00000000000000140202c806312cbe5b79ef8aa6c17e3f423d8fdfe1d46909fb1f6cdf65ee8e2e6faa020000000b477561726469616e536574000000040000000002c806312cbe5b79ef8aa6c17e3f423d8fdfe1d46909fb1f6cdf65ee8e2e6faa020000000b477561726469616e5365740000000400000001"); + } + + function test_buildSolanaPdaRequestBytesTooManyPDAs() public { + bytes32 programId = hex"02c806312cbe5b79ef8aa6c17e3f423d8fdfe1d46909fb1f6cdf65ee8e2e6faa"; + bytes[] memory pdas = new bytes[](256); + + uint numPDAs = pdas.length; + for (uint idx; idx < numPDAs;) { + bytes[] memory seeds = new bytes[](2); + seeds[0] = hex"477561726469616e536574"; + seeds[1] = hex"00000000"; + (bytes memory seedBytes, uint8 numSeeds) = QueryTest.buildSolanaPdaSeedBytes(seeds); + + pdas[idx] = QueryTest.buildSolanaPdaEntry( + programId, + numSeeds, + seedBytes + ); + + unchecked { ++idx; } + } + + vm.expectRevert(QueryTest.SolanaTooManyPDAs.selector); + QueryTest.buildSolanaPdaRequestBytes( + /* commitment */ "finalized", + /* minContextSlot */ 2303, + /* dataSliceOffset */ 12, + /* dataSliceLength */ 20, + /* pdas */ pdas + ); + } + + function test_buildSolanaPdaEntryTooManySeeds() public { + bytes[] memory seeds = new bytes[](2); + seeds[0] = hex"477561726469616e536574"; + seeds[1] = hex"00000000"; + (bytes memory seedBytes,) = QueryTest.buildSolanaPdaSeedBytes(seeds); + assertEq(seedBytes, hex"0000000b477561726469616e5365740000000400000000"); + + bytes32 programId = hex"02c806312cbe5b79ef8aa6c17e3f423d8fdfe1d46909fb1f6cdf65ee8e2e6faa"; + + vm.expectRevert(QueryTest.SolanaTooManySeeds.selector); + QueryTest.buildSolanaPdaEntry( + programId, + uint8(QueryTest.SolanaMaxSeeds + 1), + seedBytes + ); + } + + function test_buildSolanaPdaSeedBytesTooManySeeds() public { + bytes[] memory seeds = new bytes[](QueryTest.SolanaMaxSeeds + 1); + uint numSeeds = seeds.length; + for (uint idx; idx < numSeeds;) { + seeds[idx] = "junk"; + unchecked { ++idx; } + } + + vm.expectRevert(QueryTest.SolanaTooManySeeds.selector); + QueryTest.buildSolanaPdaSeedBytes(seeds); + } + + function test_buildSolanaPdaSeedBytesSeedTooLong() public { + bytes[] memory seeds = new bytes[](2); + seeds[0] = "junk"; + seeds[1] = "This seed is too long!!!!!!!!!!!!"; + + vm.expectRevert(QueryTest.SolanaSeedTooLong.selector); + QueryTest.buildSolanaPdaSeedBytes(seeds); + } + + // + // Query Response tests + // + + function test_buildQueryResponseBytes() public { + bytes memory resp = QueryTest.buildQueryResponseBytes( + /* version */ 1, + /* senderChainId */ 0, + /* signature */ hex"11b03bdbbe15a8f12b803d2193de5ddff72d92eaabd2763553ec3c3133182d1443719a05e2b65c87b923c6bd8aeff49f34937f90f3ab7cd33449388c60fa30a301", + /* queryRequest */ hex"0100000001010002010000004200000005307837343402ddb64fe46a91d46ee29420539fc25fd07c5fea3e0000000406fdde03ddb64fe46a91d46ee29420539fc25fd07c5fea3e00000004313ce567", + /* numPerChainResponses */ 1, + /* perChainResponses */ hex"000201000000b900000000000007446a0b819aee8945e659e37537a0bdbe03c06275be23e499819138d1eee8337e9b000000006ab13b8002000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d5772617070656420457468657200000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000012" + ); + assertEq(resp, hex"01000011b03bdbbe15a8f12b803d2193de5ddff72d92eaabd2763553ec3c3133182d1443719a05e2b65c87b923c6bd8aeff49f34937f90f3ab7cd33449388c60fa30a3010000004f0100000001010002010000004200000005307837343402ddb64fe46a91d46ee29420539fc25fd07c5fea3e0000000406fdde03ddb64fe46a91d46ee29420539fc25fd07c5fea3e00000004313ce56701000201000000b900000000000007446a0b819aee8945e659e37537a0bdbe03c06275be23e499819138d1eee8337e9b000000006ab13b8002000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d5772617070656420457468657200000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000012"); + } + + function test_buildPerChainResponseBytes() public { + bytes memory pcr = QueryTest.buildPerChainResponseBytes( + /* chainId */ 2, + /* queryType */ 1, + /* responseBytes */ hex"00000000000007446a0b819aee8945e659e37537a0bdbe03c06275be23e499819138d1eee8337e9b000000006ab13b8002000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d5772617070656420457468657200000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000012" + ); + assertEq(pcr, hex"000201000000b900000000000007446a0b819aee8945e659e37537a0bdbe03c06275be23e499819138d1eee8337e9b000000006ab13b8002000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d5772617070656420457468657200000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000012"); + } + + function test_buildEthCallResponseBytes() public { + bytes memory ecr = QueryTest.buildEthCallResponseBytes( + /* blockNumber */ 1860, + /* blockHash */ hex"6a0b819aee8945e659e37537a0bdbe03c06275be23e499819138d1eee8337e9b", + /* blockTimeUs */ 0x6ab13b80, + /* numResults */ 2, + /* results */ hex"000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d5772617070656420457468657200000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000012" + ); + assertEq(ecr, hex"00000000000007446a0b819aee8945e659e37537a0bdbe03c06275be23e499819138d1eee8337e9b000000006ab13b8002000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d5772617070656420457468657200000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000012"); + } + + function test_buildEthCallByTimestampResponseBytes() public { + bytes memory ecr = QueryTest.buildEthCallByTimestampResponseBytes( + /* targetBlockNumber */ 349, + /* targetBlockHash */ hex"966cd846f812be43c4ee2d310f962bc592ba944c66de878e53584b8e75c6051f", + /* targetBlockTimeUs */ 0x10642ac0, + /* followingBlockNumber */ 350, + /* followingBlockHash */ hex"04b022afaab8da2dd80bd8e6ae55e6303473a5e1de846a5de76d619e162429ce", + /* followingBlockTimeUs */ 0x10736d00, + /* numResults */ 2, + /* results */ hex"000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d5772617070656420457468657200000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000012" + ); + assertEq(ecr, hex"000000000000015d966cd846f812be43c4ee2d310f962bc592ba944c66de878e53584b8e75c6051f0000000010642ac0000000000000015e04b022afaab8da2dd80bd8e6ae55e6303473a5e1de846a5de76d619e162429ce0000000010736d0002000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d5772617070656420457468657200000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000012"); + } + + function test_buildEthCallWithFinalityResponseBytes() public { + bytes memory ecr = QueryTest.buildEthCallWithFinalityResponseBytes( + /* blockNumber */ 1860, + /* blockHash */ hex"6a0b819aee8945e659e37537a0bdbe03c06275be23e499819138d1eee8337e9b", + /* blockTimeUs */ 0x6ab13b80, + /* numResults */ 2, + /* results */ hex"000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d5772617070656420457468657200000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000012" + ); + assertEq(ecr, hex"00000000000007446a0b819aee8945e659e37537a0bdbe03c06275be23e499819138d1eee8337e9b000000006ab13b8002000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d5772617070656420457468657200000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000012"); + } + + function test_buildEthCallResultBytes() public { + bytes memory ecr1 = QueryTest.buildEthCallResultBytes( + /* result */ hex"0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d5772617070656420457468657200000000000000000000000000000000000000" + ); + assertEq(ecr1, hex"000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d5772617070656420457468657200000000000000000000000000000000000000"); + bytes memory ecr2 = QueryTest.buildEthCallResultBytes( + /* result */ hex"0000000000000000000000000000000000000000000000000000000000000012" + ); + assertEq(ecr2, hex"000000200000000000000000000000000000000000000000000000000000000000000012"); + } + + function test_buildSolanaAccountResponseBytes() public { + bytes memory ecr = QueryTest.buildSolanaAccountResponseBytes( + /* slotNumber */ 5603, + /* blockTimeUs */ 0x610cdf2510500, + /* blockHash */ hex"e0eca895a92c0347e30538cd07c50777440de58e896dd13ff86ef0dae3e12552", + /* numResults */ 2, + /* results */ hex"0000000000164d6000000000000000000006ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a90000005201000000574108aed69daf7e625a361864b1f74d13702f2ca56de9660e566d1d8691848d0000e8890423c78a09010000000000000000000000000000000000000000000000000000000000000000000000000000000000164d6000000000000000000006ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a90000005201000000574108aed69daf7e625a361864b1f74d13702f2ca56de9660e566d1d8691848d01000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000" + ); + assertEq(ecr, hex"00000000000015e3000610cdf2510500e0eca895a92c0347e30538cd07c50777440de58e896dd13ff86ef0dae3e12552020000000000164d6000000000000000000006ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a90000005201000000574108aed69daf7e625a361864b1f74d13702f2ca56de9660e566d1d8691848d0000e8890423c78a09010000000000000000000000000000000000000000000000000000000000000000000000000000000000164d6000000000000000000006ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a90000005201000000574108aed69daf7e625a361864b1f74d13702f2ca56de9660e566d1d8691848d01000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000"); + } + + function test_buildSolanaPdaResponseBytes() public { + bytes memory ecr = QueryTest.buildSolanaPdaResponseBytes( + /* slotNumber */ 2303, + /* blockTimeUs */ 0x6115e3f6d7540, + /* blockHash */ hex"e05035785e15056a8559815e71343ce31db2abf23f65b19c982b68aee7bf207b", + /* numResults */ 1, + /* results */ hex"4fa9188b339cfd573a0778c5deaeeee94d4bcfb12b345bf8e417e5119dae773efd0000000000116ac000000000000000000002c806312cbe5b79ef8aa6c17e3f423d8fdfe1d46909fb1f6cdf65ee8e2e6faa0000001457cd18b7f8a4d91a2da9ab4af05d0fbece2dcd65" + ); + assertEq(ecr, hex"00000000000008ff0006115e3f6d7540e05035785e15056a8559815e71343ce31db2abf23f65b19c982b68aee7bf207b014fa9188b339cfd573a0778c5deaeeee94d4bcfb12b345bf8e417e5119dae773efd0000000000116ac000000000000000000002c806312cbe5b79ef8aa6c17e3f423d8fdfe1d46909fb1f6cdf65ee8e2e6faa0000001457cd18b7f8a4d91a2da9ab4af05d0fbece2dcd65"); + } +}