From 65c4dc4177f9038f4fd6b1183b9eefad76628ba8 Mon Sep 17 00:00:00 2001 From: bruce-riley <96066700+bruce-riley@users.noreply.github.com> Date: Mon, 11 Mar 2024 16:13:43 -0500 Subject: [PATCH] CCQ/EVM: sol_pda support (#3790) * CCQ/EVM: sol_pda support * Code review rework * Code review rework --- ethereum/contracts/query/QueryResponse.sol | 118 ++++++++++++-- ethereum/forge-test/query/QueryResponse.t.sol | 146 +++++++++++++++--- ethereum/forge-test/query/QueryTest.sol | 111 ++++++++++++- ethereum/forge-test/query/QueryTest.t.sol | 121 +++++++++++++++ 4 files changed, 461 insertions(+), 35 deletions(-) diff --git a/ethereum/contracts/query/QueryResponse.sol b/ethereum/contracts/query/QueryResponse.sol index b74f621fc..91194655f 100644 --- a/ethereum/contracts/query/QueryResponse.sol +++ b/ethereum/contracts/query/QueryResponse.sol @@ -24,7 +24,7 @@ struct ParsedPerChainQueryResponse { bytes response; } -// @dev EthCallQueryResponse describes an ETH call per-chain query. +// @dev EthCallQueryResponse describes the response to an ETH call per-chain query. struct EthCallQueryResponse { bytes requestBlockId; uint64 blockNum; @@ -33,7 +33,7 @@ struct EthCallQueryResponse { EthCallData [] result; } -// @dev EthCallByTimestampQueryResponse describes an ETH call by timestamp per-chain query. +// @dev EthCallByTimestampQueryResponse describes the response to an ETH call by timestamp per-chain query. struct EthCallByTimestampQueryResponse { bytes requestTargetBlockIdHint; bytes requestFollowingBlockIdHint; @@ -47,7 +47,7 @@ struct EthCallByTimestampQueryResponse { EthCallData [] result; } -// @dev EthCallWithFinalityQueryResponse describes an ETH call with finality per-chain query. +// @dev EthCallWithFinalityQueryResponse describes the response to an ETH call with finality per-chain query. struct EthCallWithFinalityQueryResponse { bytes requestBlockId; bytes requestFinality; @@ -64,7 +64,7 @@ struct EthCallData { bytes result; } -// @dev SolanaAccountQueryResponse describes a Solana Account query per-chain query. +// @dev SolanaAccountQueryResponse describes the response to a Solana Account query per-chain query. struct SolanaAccountQueryResponse { bytes requestCommitment; uint64 requestMinContextSlot; @@ -86,6 +86,31 @@ struct SolanaAccountResult { 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(); @@ -94,7 +119,8 @@ error ZeroQueries(); error NumberOfResponsesMismatch(); error ChainIdMismatch(); error RequestTypeMismatch(); -error UnsupportedQueryType(); +error UnsupportedQueryType(uint8 received); +error WrongQueryType(uint8 received, uint8 expected); error UnexpectedNumberOfResults(); error InvalidPayloadLength(uint256 received, uint256 expected); error InvalidContractAddress(); @@ -104,6 +130,8 @@ 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; @@ -111,11 +139,14 @@ abstract contract QueryResponse { 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_MAX = 5; // Keep this last + uint8 public constant QT_SOL_PDA = 5; + uint8 public constant QT_MAX = 6; // Keep this last constructor(address _wormhole) { if (_wormhole == address(0)) { @@ -207,7 +238,7 @@ abstract contract QueryResponse { } if (r.responses[idx].queryType < QT_ETH_CALL || r.responses[idx].queryType >= QT_MAX) { - revert UnsupportedQueryType(); + revert UnsupportedQueryType(r.responses[idx].queryType); } (len, reqIdx) = response.asUint32Unchecked(reqIdx); @@ -231,7 +262,7 @@ abstract contract QueryResponse { /// @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 UnsupportedQueryType(); + revert WrongQueryType(pcr.queryType, QT_ETH_CALL); } uint reqIdx; @@ -280,7 +311,7 @@ abstract contract QueryResponse { /// @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 UnsupportedQueryType(); + revert WrongQueryType(pcr.queryType, QT_ETH_CALL_BY_TIMESTAMP); } uint reqIdx; @@ -334,7 +365,7 @@ abstract contract QueryResponse { /// @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 UnsupportedQueryType(); + revert WrongQueryType(pcr.queryType, QT_ETH_CALL_WITH_FINALITY); } uint reqIdx; @@ -384,7 +415,7 @@ abstract contract QueryResponse { /// @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 UnsupportedQueryType(); + revert WrongQueryType(pcr.queryType, QT_SOL_ACCOUNT); } uint reqIdx; @@ -434,6 +465,71 @@ abstract contract QueryResponse { 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.asBoolUnckecked(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 diff --git a/ethereum/forge-test/query/QueryResponse.t.sol b/ethereum/forge-test/query/QueryResponse.t.sol index cc431258e..ae4569914 100644 --- a/ethereum/forge-test/query/QueryResponse.t.sol +++ b/ethereum/forge-test/query/QueryResponse.t.sol @@ -31,16 +31,27 @@ contract TestQueryResponse is Test { bytes perChainResponses = hex"000501000000b90000000002a61ac4c1adff9f6e180309e7d0d94c063338ddc61c1c4474cd6957c960efe659534d040005ff312e4f90c002000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d57726170706564204d6174696300000000000000000000000000000000000000000000200000000000000000000000000000000000000000007ae5649beabeddf889364a"; bytes perChainResponsesInner = hex"00000009307832613631616334020d500b1d8e8ef31e21c99d1db9a6444d3adf12700000000406fdde030d500b1d8e8ef31e21c99d1db9a6444d3adf12700000000418160ddd"; - bytes solanaSignature = hex"acb1d93cdfe60f9776e3e05d7fafaf9d83a1d14db70317230f6b0b6f3a60708a1a64dddac02d3843f4c516f2509b89454a2e73c360fea47beee1c1a091ff9f3201"; - uint32 solanaQueryRequestLen = 0x00000073; - uint8 solanaQueryRequestVersion = 0x01; - uint32 solanaQueryRequestNonce = 0x0000002a; - uint8 solanaNumPerChainQueries = 0x01; - bytes solanaPerChainQueries = hex"000104000000660000000966696e616c697a656400000000000000000000000000000000000000000000000002165809739240a0ac03b98440fe8985548e3aa683cd0d4d9df5b5659669faa3019c006c48c8cbf33849cb07a3f936159cc523f9591cb1999abd45890ec5fee9b7"; - bytes solanaPerChainQueriesInner = hex"0000000966696e616c697a656400000000000000000000000000000000000000000000000002165809739240a0ac03b98440fe8985548e3aa683cd0d4d9df5b5659669faa3019c006c48c8cbf33849cb07a3f936159cc523f9591cb1999abd45890ec5fee9b7"; - uint8 solanaNumPerChainResponses = 0x01; - bytes solanaPerChainResponses = hex"010001040000013f000000000000d85f00060f3e9915ddc03a8de2b1de609020bb0a0dcee594a8c06801619cf9ea2a498b9d910f9a25772b020000000000164d6000000000000000000006ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a90000005201000000574108aed69daf7e625a361864b1f74d13702f2ca56de9660e566d1d8691848d0000e8890423c78a09010000000000000000000000000000000000000000000000000000000000000000000000000000000000164d6000000000000000000006ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a90000005201000000574108aed69daf7e625a361864b1f74d13702f2ca56de9660e566d1d8691848d01000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000"; - bytes solanaPerChainResponsesInner = hex"000000000000d85f00060f3e9915ddc03a8de2b1de609020bb0a0dcee594a8c06801619cf9ea2a498b9d910f9a25772b020000000000164d6000000000000000000006ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a90000005201000000574108aed69daf7e625a361864b1f74d13702f2ca56de9660e566d1d8691848d0000e8890423c78a09010000000000000000000000000000000000000000000000000000000000000000000000000000000000164d6000000000000000000006ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a90000005201000000574108aed69daf7e625a361864b1f74d13702f2ca56de9660e566d1d8691848d01000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000"; + 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; @@ -191,7 +202,7 @@ contract TestQueryResponse is Test { response: hex"0000000002a61ac4c1adff9f6e180309e7d0d94c063338ddc61c1c4474cd6957c960efe659534d040005ff312e4f90c002000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d57726170706564204d6174696300000000000000000000000000000000000000000000200000000000000000000000000000000000000000007ae5649beabeddf889364a" }); - vm.expectRevert(UnsupportedQueryType.selector); + vm.expectRevert(abi.encodeWithSelector(WrongQueryType.selector, 2, queryResponse.QT_ETH_CALL())); queryResponse.parseEthCallQueryResponse(r); } @@ -262,7 +273,7 @@ contract TestQueryResponse is Test { response: hex"0000000000004271ec70d2f70cf1933770ae760050a75334ce650aa091665ee43a6ed488cd154b0800000003f4810cc000000000000042720b1608c2cddfd9d7fb4ec94f79ec1389e2410e611a2c2bbde94e9ad37519ebbb00000003f4904f0002000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d5772617070656420457468657200000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000" }); - vm.expectRevert(UnsupportedQueryType.selector); + vm.expectRevert(abi.encodeWithSelector(WrongQueryType.selector, 1, queryResponse.QT_ETH_CALL_BY_TIMESTAMP())); queryResponse.parseEthCallByTimestampQueryResponse(r); } @@ -301,14 +312,14 @@ contract TestQueryResponse is Test { response: hex"00000000000060299eb9c56ffdae81214867ed217f5ab37e295c196b4f04b23a795d3e4aea6ff3d700000005bb1bd58002000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d5772617070656420457468657200000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000" }); - vm.expectRevert(UnsupportedQueryType.selector); + 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, solanaSignature, solanaQueryRequestVersion, solanaQueryRequestNonce, solanaNumPerChainQueries, solanaPerChainQueries, solanaNumPerChainResponses, solanaPerChainResponses); + 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}); @@ -321,8 +332,8 @@ contract TestQueryResponse is Test { ParsedPerChainQueryResponse memory r = ParsedPerChainQueryResponse({ chainId: 1, queryType: 4, - request: solanaPerChainQueriesInner, - response: solanaPerChainResponsesInner + request: solanaAccountPerChainQueriesInner, + response: solanaAccountPerChainResponsesInner }); SolanaAccountQueryResponse memory sar = queryResponse.parseSolanaAccountQueryResponse(r); @@ -356,11 +367,11 @@ contract TestQueryResponse is Test { ParsedPerChainQueryResponse memory r = ParsedPerChainQueryResponse({ chainId: 2, queryType: 1, - request: solanaPerChainQueriesInner, - response: solanaPerChainResponsesInner + request: solanaAccountPerChainQueriesInner, + response: solanaAccountPerChainResponsesInner }); - vm.expectRevert(UnsupportedQueryType.selector); + vm.expectRevert(abi.encodeWithSelector(WrongQueryType.selector, 1, queryResponse.QT_SOL_ACCOUNT())); queryResponse.parseSolanaAccountQueryResponse(r); } @@ -371,7 +382,7 @@ contract TestQueryResponse is Test { chainId: 1, queryType: 4, request: requestWithOnlyOneAccount, - response: solanaPerChainResponsesInner + response: solanaAccountPerChainResponsesInner }); vm.expectRevert(UnexpectedNumberOfResults.selector); @@ -385,7 +396,7 @@ contract TestQueryResponse is Test { chainId: 1, queryType: 4, request: requestWithExtraBytes, - response: solanaPerChainResponsesInner + response: solanaAccountPerChainResponsesInner }); vm.expectRevert(abi.encodeWithSelector(InvalidPayloadLength.selector, 106, 102)); @@ -398,7 +409,7 @@ contract TestQueryResponse is Test { ParsedPerChainQueryResponse memory r = ParsedPerChainQueryResponse({ chainId: 1, queryType: 4, - request: solanaPerChainQueriesInner, + request: solanaAccountPerChainQueriesInner, response: responseWithExtraBytes }); @@ -406,6 +417,95 @@ contract TestQueryResponse is Test { 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 ************* ***********************************/ @@ -542,7 +642,7 @@ contract TestQueryResponse is Test { (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(UnsupportedQueryType.selector); + vm.expectRevert(abi.encodeWithSelector(UnsupportedQueryType.selector, _requestQueryType)); queryResponse.parseAndVerifyQueryResponse(resp, signatures); } diff --git a/ethereum/forge-test/query/QueryTest.sol b/ethereum/forge-test/query/QueryTest.sol index 0f5a9675a..1b750b334 100644 --- a/ethereum/forge-test/query/QueryTest.sol +++ b/ethereum/forge-test/query/QueryTest.sol @@ -6,6 +6,11 @@ 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 // @@ -101,7 +106,7 @@ library QueryTest { ); } - /// @dev buildSolanaAccountRequestBytes builds an sol_account query request from the specified fields. + /// @dev buildSolanaAccountRequestBytes builds a sol_account query request from the specified fields. function buildSolanaAccountRequestBytes( bytes memory _commitment, uint64 _minContextSlot, @@ -120,6 +125,93 @@ library QueryTest { _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 @@ -242,4 +334,21 @@ library QueryTest { _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/ethereum/forge-test/query/QueryTest.t.sol b/ethereum/forge-test/query/QueryTest.t.sol index 70ac80f07..0c541519e 100644 --- a/ethereum/forge-test/query/QueryTest.t.sol +++ b/ethereum/forge-test/query/QueryTest.t.sol @@ -86,6 +86,116 @@ contract TestQueryTest is Test { 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 // @@ -168,4 +278,15 @@ contract TestQueryTest is Test { ); 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"); + } }