From dcec08cb7232a72afb61bd06aa242ee5fd123ab2 Mon Sep 17 00:00:00 2001 From: Bruce Riley Date: Wed, 25 Oct 2023 10:40:07 -0500 Subject: [PATCH] CCQ/EVM: ethCallByTimestamp & ethCallWithFinality --- ethereum/contracts/query/QueryResponse.sol | 135 +++++++++++++++++- ethereum/forge-test/query/QueryResponse.t.sol | 81 +++++++++++ 2 files changed, 215 insertions(+), 1 deletion(-) diff --git a/ethereum/contracts/query/QueryResponse.sol b/ethereum/contracts/query/QueryResponse.sol index 5af4e266a4..03a68e9e17 100644 --- a/ethereum/contracts/query/QueryResponse.sol +++ b/ethereum/contracts/query/QueryResponse.sol @@ -32,6 +32,30 @@ struct EthCallQueryResponse { EthCallData [] result; } +// @dev EthCallByTimestampQueryResponse describes an ETH call by timestamp per-chain query. +struct EthCallByTimestampQueryResponse { + bytes requestTargetBlockIdHint; + bytes requestFollowingBlockIdHint; + uint64 requestTargetTimestamp; + uint64 targetBlockNum; + bytes32 targetBlockHash; + uint64 targetBlockTime; + uint64 followingBlockNum; + bytes32 followingBlockHash; + uint64 followingBlockTime; + EthCallData [] result; +} + +// @dev EthCallWithFinalityQueryResponse describes 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; @@ -55,6 +79,9 @@ abstract contract QueryResponse { bytes public constant responsePrefix = bytes("query_response_0000000000000000000|"); 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_ETH_CALL_MAX = 4; // Keep this last /// @dev getResponseHash computes the hash of the specified query response. function getResponseHash(bytes memory response) public pure returns (bytes32) { @@ -127,7 +154,7 @@ abstract contract QueryResponse { revert RequestTypeMismatch(); } - if (r.responses[idx].queryType != QT_ETH_CALL) { + if (r.responses[idx].queryType < QT_ETH_CALL || r.responses[idx].queryType >= QT_ETH_CALL_MAX) { revert UnsupportedQueryType(); } @@ -193,6 +220,112 @@ abstract contract QueryResponse { 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 UnsupportedQueryType(); + } + + uint reqIdx = 0; + uint respIdx = 0; + 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 = 0; 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 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(); + } + + uint reqIdx = 0; + uint respIdx = 0; + 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 = 0; 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 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 diff --git a/ethereum/forge-test/query/QueryResponse.t.sol b/ethereum/forge-test/query/QueryResponse.t.sol index 7e62bcd4ff..a139b8aabe 100644 --- a/ethereum/forge-test/query/QueryResponse.t.sol +++ b/ethereum/forge-test/query/QueryResponse.t.sol @@ -159,6 +159,87 @@ contract TestQueryResponse is Test { 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(UnsupportedQueryType.selector); + 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(UnsupportedQueryType.selector); + queryResponse.parseEthCallWithFinalityQueryResponse(r); } }