Skip to content

Commit

Permalink
CCQ/EVM: sol_pda support
Browse files Browse the repository at this point in the history
  • Loading branch information
bruce-riley committed Mar 4, 2024
1 parent 0a49c6e commit 2fe7312
Show file tree
Hide file tree
Showing 4 changed files with 340 additions and 19 deletions.
93 changes: 92 additions & 1 deletion ethereum/contracts/query/QueryResponse.sol
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,31 @@ struct SolanaAccountResult {
bytes data;
}

// @dev SolanaPdaQueryResponse describes a Solana PDA 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 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();
Expand Down Expand Up @@ -115,7 +140,8 @@ abstract contract QueryResponse {
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)) {
Expand Down Expand Up @@ -434,6 +460,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 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 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
Expand Down
136 changes: 118 additions & 18 deletions ethereum/forge-test/query/QueryResponse.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -308,7 +319,7 @@ contract TestQueryResponse is Test {
// 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});
Expand All @@ -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);
Expand Down Expand Up @@ -356,8 +367,8 @@ contract TestQueryResponse is Test {
ParsedPerChainQueryResponse memory r = ParsedPerChainQueryResponse({
chainId: 2,
queryType: 1,
request: solanaPerChainQueriesInner,
response: solanaPerChainResponsesInner
request: solanaAccountPerChainQueriesInner,
response: solanaAccountPerChainResponsesInner
});

vm.expectRevert(UnsupportedQueryType.selector);
Expand All @@ -371,7 +382,7 @@ contract TestQueryResponse is Test {
chainId: 1,
queryType: 4,
request: requestWithOnlyOneAccount,
response: solanaPerChainResponsesInner
response: solanaAccountPerChainResponsesInner
});

vm.expectRevert(UnexpectedNumberOfResults.selector);
Expand All @@ -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));
Expand All @@ -398,14 +409,103 @@ contract TestQueryResponse is Test {
ParsedPerChainQueryResponse memory r = ParsedPerChainQueryResponse({
chainId: 1,
queryType: 4,
request: solanaPerChainQueriesInner,
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(UnsupportedQueryType.selector);
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 *************
***********************************/
Expand Down
Loading

0 comments on commit 2fe7312

Please sign in to comment.