EVM/CCQ: Parse Solana Account Query Response (#3720)

* EVM/CCQ: Parse Solana Account Query Response

* Code review rework

* Code review rework
This commit is contained in:
bruce-riley 2024-01-26 19:05:35 -06:00 committed by GitHub
parent 10b83f78f5
commit 5fa8379b17
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 205 additions and 18 deletions

View File

@ -1,8 +1,9 @@
// contracts/query/QueryResponse.sol
// SPDX-License-Identifier: Apache 2
pragma solidity ^0.8.0;
pragma solidity ^0.8.4;
// TODO: Change this to use the version of BytesParsing.sol in wormhole-solidity-sdk once it is release.
import {BytesParsing} from "../relayer/libraries/BytesParsing.sol";
import "../interfaces/IWormhole.sol";
@ -63,6 +64,28 @@ struct EthCallData {
bytes result;
}
// @dev SolanaAccountQueryResponse describes 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;
}
// Custom errors
error EmptyWormholeAddress();
error InvalidResponseVersion();
@ -91,7 +114,8 @@ abstract contract QueryResponse {
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_MAX = 4; // Keep this last
uint8 public constant QT_SOL_ACCOUNT = 4;
uint8 public constant QT_MAX = 5; // Keep this last
constructor(address _wormhole) {
if (_wormhole == address(0)) {
@ -115,7 +139,7 @@ abstract contract QueryResponse {
function parseAndVerifyQueryResponse(bytes memory response, IWormhole.Signature[] memory signatures) public view returns (ParsedQueryResponse memory r) {
verifyQueryResponseSignatures(response, signatures);
uint index = 0;
uint index;
(r.version, index) = response.asUint8Unchecked(index);
if (r.version != VERSION) {
@ -167,7 +191,7 @@ abstract contract QueryResponse {
r.responses = new ParsedPerChainQueryResponse[](numPerChainQueries);
// Walk through the requests and responses in lock step.
for (uint idx = 0; idx < numPerChainQueries;) {
for (uint idx; idx < numPerChainQueries;) {
(r.responses[idx].chainId, reqIdx) = response.asUint16Unchecked(reqIdx);
uint16 respChainId;
(respChainId, respIdx) = response.asUint16Unchecked(respIdx);
@ -210,8 +234,8 @@ abstract contract QueryResponse {
revert UnsupportedQueryType();
}
uint reqIdx = 0;
uint respIdx = 0;
uint reqIdx;
uint respIdx;
uint32 len;
(len, reqIdx) = pcr.request.asUint32Unchecked(reqIdx); // block_id_len
@ -236,7 +260,7 @@ abstract contract QueryResponse {
r.result = new EthCallData[](numBatchCallData);
// Walk through the call data and results in lock step.
for (uint idx = 0; idx < numBatchCallData;) {
for (uint idx; idx < numBatchCallData;) {
(r.result[idx].contractAddress, reqIdx) = pcr.request.asAddressUnchecked(reqIdx);
(len, reqIdx) = pcr.request.asUint32Unchecked(reqIdx); // call_data_len
@ -259,8 +283,8 @@ abstract contract QueryResponse {
revert UnsupportedQueryType();
}
uint reqIdx = 0;
uint respIdx = 0;
uint reqIdx;
uint respIdx;
uint32 len;
(r.requestTargetTimestamp, reqIdx) = pcr.request.asUint64Unchecked(reqIdx); // Request target_time_us
@ -291,7 +315,7 @@ abstract contract QueryResponse {
r.result = new EthCallData[](numBatchCallData);
// Walk through the call data and results in lock step.
for (uint idx = 0; idx < numBatchCallData;) {
for (uint idx; idx < numBatchCallData;) {
(r.result[idx].contractAddress, reqIdx) = pcr.request.asAddressUnchecked(reqIdx);
(len, reqIdx) = pcr.request.asUint32Unchecked(reqIdx); // call_data_len
@ -313,8 +337,8 @@ abstract contract QueryResponse {
revert UnsupportedQueryType();
}
uint reqIdx = 0;
uint respIdx = 0;
uint reqIdx;
uint respIdx;
uint32 len;
(len, reqIdx) = pcr.request.asUint32Unchecked(reqIdx); // Request block_id_len
@ -341,7 +365,7 @@ abstract contract QueryResponse {
r.result = new EthCallData[](numBatchCallData);
// Walk through the call data and results in lock step.
for (uint idx = 0; idx < numBatchCallData;) {
for (uint idx; idx < numBatchCallData;) {
(r.result[idx].contractAddress, reqIdx) = pcr.request.asAddressUnchecked(reqIdx);
(len, reqIdx) = pcr.request.asUint32Unchecked(reqIdx); // call_data_len
@ -357,6 +381,59 @@ abstract contract QueryResponse {
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 UnsupportedQueryType();
}
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.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
@ -381,7 +458,7 @@ abstract contract QueryResponse {
uint256 numChainIds = _validChainIds.length;
for (uint256 idx = 0; idx < numChainIds;) {
for (uint256 idx; idx < numChainIds;) {
if (chainId == _validChainIds[idx]) {
validChainId = true;
break;
@ -397,7 +474,7 @@ abstract contract QueryResponse {
function validateMultipleEthCallData(EthCallData[] memory r, address[] memory _expectedContractAddresses, bytes4[] memory _expectedFunctionSignatures) public pure {
uint256 callDatasLength = r.length;
for (uint256 idx = 0; idx < callDatasLength;) {
for (uint256 idx; idx < callDatasLength;) {
validateEthCallData(r[idx], _expectedContractAddresses, _expectedFunctionSignatures);
unchecked { ++idx; }
@ -420,7 +497,7 @@ abstract contract QueryResponse {
uint256 contractAddressesLength = _expectedContractAddresses.length;
// Check that the contract address called in the request is expected
for (uint256 idx = 0; idx < contractAddressesLength;) {
for (uint256 idx; idx < contractAddressesLength;) {
if (r.contractAddress == _expectedContractAddresses[idx]) {
validContractAddress = true;
break;
@ -437,7 +514,7 @@ abstract contract QueryResponse {
uint256 functionSignaturesLength = _expectedFunctionSignatures.length;
// Check that the function signature called is expected
for (uint256 idx = 0; idx < functionSignaturesLength;) {
for (uint256 idx; idx < functionSignaturesLength;) {
(bytes4 funcSig,) = r.callData.asBytes4Unchecked(0);
if (funcSig == _expectedFunctionSignatures[idx]) {
validFunctionSignature = true;

View File

@ -2,7 +2,7 @@
// forge test --match-contract QueryResponse
pragma solidity ^0.8.0;
pragma solidity ^0.8.4;
import "../../contracts/query/QueryResponse.sol";
import "../../contracts/Implementation.sol";
@ -30,6 +30,17 @@ 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";
uint8 sigGuardianIndex = 0;
Wormhole wormhole;
@ -292,7 +303,106 @@ contract TestQueryResponse is Test {
queryResponse.parseEthCallWithFinalityQueryResponse(r);
}
// Start of Solana Stuff ///////////////////////////////////////////////////////////////////////////
function test_verifyQueryResponseSignaturesForSolana() public view {
bytes memory resp = concatenateQueryResponseBytesOffChain(version, senderChainId, solanaSignature, solanaQueryRequestLen, solanaQueryRequestVersion, solanaQueryRequestNonce, solanaNumPerChainQueries, solanaPerChainQueries, solanaNumPerChainResponses, solanaPerChainResponses);
(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: solanaPerChainQueriesInner,
response: solanaPerChainResponsesInner
});
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: solanaPerChainQueriesInner,
response: solanaPerChainResponsesInner
});
vm.expectRevert(UnsupportedQueryType.selector);
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: solanaPerChainResponsesInner
});
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: solanaPerChainResponsesInner
});
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: solanaPerChainQueriesInner,
response: responseWithExtraBytes
});
vm.expectRevert(abi.encodeWithSelector(InvalidPayloadLength.selector, 323, 319));
queryResponse.parseSolanaAccountQueryResponse(r);
}
/***********************************
*********** FUZZ TESTS *************