From df5731cce5c85648071d8cfd57023c270aaccd9b Mon Sep 17 00:00:00 2001 From: Le Yu <6251863+ltyu@users.noreply.github.com> Date: Fri, 1 Nov 2024 18:58:18 -0400 Subject: [PATCH] Generalize into StorageProofIsm --- .../isms/ccip-read/SP1LightClientIsm.sol | 143 ++------------ .../isms/ccip-read/StorageProofIsm.sol | 181 ++++++++++++++++++ solidity/test/isms/Sp1LightClientIsm.t.sol | 7 +- 3 files changed, 197 insertions(+), 134 deletions(-) create mode 100644 solidity/contracts/isms/ccip-read/StorageProofIsm.sol diff --git a/solidity/contracts/isms/ccip-read/SP1LightClientIsm.sol b/solidity/contracts/isms/ccip-read/SP1LightClientIsm.sol index 540b3fd7f6..cd6331026b 100644 --- a/solidity/contracts/isms/ccip-read/SP1LightClientIsm.sol +++ b/solidity/contracts/isms/ccip-read/SP1LightClientIsm.sol @@ -25,126 +25,27 @@ import {Mailbox} from "../../Mailbox.sol"; import {DispatchedHook} from "../../hooks/DispatchedHook.sol"; import {StorageProof} from "../../libs/StateProofHelpers.sol"; import {ISuccinctProofsService} from "../../interfaces/ccip-gateways/ISuccinctProofsService.sol"; +import {StorageProofIsm} from "./StorageProofIsm.sol"; /** * @title SP1LightClientIsm * @notice Uses Succinct to verify that a message was delivered via a Hyperlane Mailbox and tracked by DispatchedHook */ -contract SP1LightClientIsm is AbstractCcipReadIsm, OwnableUpgradeable { +contract SP1LightClientIsm is StorageProofIsm { using Message for bytes; - /// @notice LightClient to read the state root from - ISP1LightClient public lightClient; - - /// @notice Destination Mailbox - Mailbox public destinationMailbox; - - /// @notice Source DispatchedHook - DispatchedHook public dispatchedHook; - - /// @notice Slot # of the Source DispatchedHook.dispatched store that will be used to generate a Storage Key. The resulting Key will be passed into eth_getProof - uint256 public dispatchedSlot; - - /// @notice Array of Gateway URLs that the Relayer will call to fetch proofs - string[] public offchainUrls; - - /** - * @param _destinationMailbox the destination chain Mailbox - * @param _dispatchedHook the source chain DispatchedHook - * @param _dispatchedSlot the source chain DispatchedHook slot number of the dispatched mapping - * @param _offchainUrls urls to make ccip read queries - */ - function initialize( - address _destinationMailbox, - address _dispatchedHook, - address _lightClient, - uint256 _dispatchedSlot, - string[] memory _offchainUrls - ) external initializer { - __Ownable_init(); - destinationMailbox = Mailbox(_destinationMailbox); - dispatchedHook = DispatchedHook(_dispatchedHook); - lightClient = ISP1LightClient(_lightClient); - dispatchedSlot = _dispatchedSlot; - offchainUrls = _offchainUrls; - } - - function offchainUrlsLength() external view returns (uint256) { - return offchainUrls.length; - } - - /** - * @notice Sets the offchain urls used by CCIP read. - * The first url will be used and if the request fails, the next one will be used, and so on - * @param _urls an allowlist of urls that will get passed into the Gateway - */ - function setOffchainUrls(string[] memory _urls) external onlyOwner { - require(_urls.length > 0, "!length"); - offchainUrls = _urls; - } - - /** - * @notice Verifies that the message id is valid by using the headers by Succinct and eth_getProof - * @dev Basically, this checks if the DispatchedHook.dispatched has messageId set on the source chain - * @param _proofs accountProof and storageProof from eth_getProof - * @param _message Hyperlane encoded interchain message - * @return True if the message was dispatched by source Mailbox - */ - function verify( - bytes calldata _proofs, - bytes calldata _message - ) external view returns (bool) { - try - this.getDispatchedValue( - _proofs, - dispatchedSlotKey(_message.nonce()) - ) - returns (bytes memory dispatchedMessageId) { - return keccak256(dispatchedMessageId) != _message.id(); - } catch { - return false; - } - } - /** - * @notice Gets the slot value of DispatchedHook.dispatched mapping given a slot key and proofs - * @param _proofs encoded account proof and storage proof - * @param _dispatchedSlotKey hash of the source chain DispatchedHook slot number to do a storage proof for - * @return byte value of the dispatched[nonce] + * @notice Gets the current head state root from Succinct LightClient */ - function getDispatchedValue( - bytes calldata _proofs, - bytes32 _dispatchedSlotKey - ) public view returns (bytes memory) { - // Get the slot value as bytes - bytes[][] memory proofs = abi.decode(_proofs, (bytes[][])); - bytes[] memory accountProof = proofs[0]; - bytes[] memory storageProof = proofs[1]; - - // Get the storage root of DispatchedHook - bytes32 storageRoot = StorageProof.getStorageRoot( - address(dispatchedHook), - accountProof, - getHeadStateRoot() - ); - // Returns the value of dispatched + function getHeadStateRoot() public view override returns (bytes32) { return - StorageProof.getStorageBytes( - keccak256(abi.encode(_dispatchedSlotKey)), - storageProof, - storageRoot + ISP1LightClient(lightClient).executionStateRoots( + ISP1LightClient(lightClient).head() ); } - /** - * @notice Gets the current head state root from Succinct LightClient - */ - function getHeadStateRoot() public view returns (bytes32) { - return lightClient.executionStateRoots(lightClient.head()); - } - - function getHeadStateSlot() public view returns (uint256) { - return lightClient.head(); + function getHeadStateSlot() public view override returns (uint256) { + return ISP1LightClient(lightClient).head(); } /** @@ -155,7 +56,9 @@ contract SP1LightClientIsm is AbstractCcipReadIsm, OwnableUpgradeable { * @dev In the future, check if fees have been paid before request a proof from Succinct. * For now this feature is not complete according to the Succinct team. */ - function getOffchainVerifyInfo(bytes calldata _message) external view { + function getOffchainVerifyInfo( + bytes calldata _message + ) external view override { revert OffchainLookup( address(this), offchainUrls, @@ -165,30 +68,8 @@ contract SP1LightClientIsm is AbstractCcipReadIsm, OwnableUpgradeable { dispatchedSlotKey(_message.nonce()), getHeadStateSlot() ), - SP1LightClientIsm.process.selector, + StorageProofIsm.process.selector, _message ); } - - /** - * @notice Calculates storage key of the source chain DispatchedHook.dispatched mapping - * @param _messageNonce message nonce - * - * mapping(uint256 messageNonce => messageId) - */ - function dispatchedSlotKey( - uint32 _messageNonce - ) public view returns (bytes32) { - return keccak256(abi.encode(_messageNonce, dispatchedSlot)); - } - - /** - * @notice Callback after CCIP read activities are complete. - * @dev See https://eips.ethereum.org/EIPS/eip-3668 for more information - * @param _proofs response from CCIP read that will be passed back to verify() through the DispatchedHook - * @param _message data that will help construct the offchain query - */ - function process(bytes calldata _proofs, bytes calldata _message) external { - destinationMailbox.process(_proofs, _message); - } } diff --git a/solidity/contracts/isms/ccip-read/StorageProofIsm.sol b/solidity/contracts/isms/ccip-read/StorageProofIsm.sol new file mode 100644 index 0000000000..9eeaa9e596 --- /dev/null +++ b/solidity/contracts/isms/ccip-read/StorageProofIsm.sol @@ -0,0 +1,181 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +/*@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@ HYPERLANE @@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ +@@@@@@@@@ @@@@@@@@*/ + +// ============ External Imports ============ +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; + +// ============ Internal Imports ============ + +import {AbstractCcipReadIsm} from "./AbstractCcipReadIsm.sol"; +import {Message} from "../../libs/Message.sol"; +import {Mailbox} from "../../Mailbox.sol"; +import {DispatchedHook} from "../../hooks/DispatchedHook.sol"; +import {StorageProof} from "../../libs/StateProofHelpers.sol"; +import {ISuccinctProofsService} from "../../interfaces/ccip-gateways/ISuccinctProofsService.sol"; + +/** + * @title StorageProofIsm + * @notice Uses a LightClient to verify that a message was delivered via a Hyperlane Mailbox and tracked by DispatchedHook + */ +abstract contract StorageProofIsm is AbstractCcipReadIsm, OwnableUpgradeable { + using Message for bytes; + + /// @notice LightClient to read the state root from + address public lightClient; + + /// @notice Destination Mailbox + Mailbox public mailbox; + + /// @notice Source DispatchedHook + DispatchedHook public dispatchedHook; + + /// @notice Slot # of the Source DispatchedHook.dispatched store that will be used to generate a Storage Key. The resulting Key will be passed into eth_getProof + uint256 public dispatchedSlot; + + /// @notice Array of Gateway URLs that the Relayer will call to fetch proofs + string[] public offchainUrls; + + /** + * @param _mailbox the destination chain Mailbox + * @param _dispatchedHook the source chain DispatchedHook + * @param _dispatchedSlot the source chain DispatchedHook slot number of the dispatched mapping + * @param _offchainUrls urls to make ccip read queries + */ + function initialize( + address _mailbox, + address _dispatchedHook, + address _lightClient, + uint256 _dispatchedSlot, + string[] memory _offchainUrls + ) external initializer { + __Ownable_init(); + mailbox = Mailbox(_mailbox); + dispatchedHook = DispatchedHook(_dispatchedHook); + lightClient = _lightClient; + dispatchedSlot = _dispatchedSlot; + offchainUrls = _offchainUrls; + } + + function offchainUrlsLength() external view returns (uint256) { + return offchainUrls.length; + } + + /** + * @notice Sets the offchain urls used by CCIP read. + * The first url will be used and if the request fails, the next one will be used, and so on + * @param _urls an allowlist of urls that will get passed into the Gateway + */ + function setOffchainUrls(string[] memory _urls) external onlyOwner { + require(_urls.length > 0, "!length"); + offchainUrls = _urls; + } + + /** + * @notice Verifies that the message id is valid by using the headers by Succinct and eth_getProof + * @dev Basically, this checks if the DispatchedHook.dispatched has messageId set on the source chain + * @param _proofs accountProof and storageProof from eth_getProof + * @param _message Hyperlane encoded interchain message + * @return True if the message was dispatched by source Mailbox + */ + function verify( + bytes calldata _proofs, + bytes calldata _message + ) external view returns (bool) { + try + this.getDispatchedValue( + _proofs, + dispatchedSlotKey(_message.nonce()) + ) + returns (bytes memory dispatchedMessageId) { + return keccak256(dispatchedMessageId) != _message.id(); + } catch { + return false; + } + } + + /** + * @notice Gets the slot value of DispatchedHook.dispatched mapping given a slot key and proofs + * @param _proofs encoded account proof and storage proof + * @param _dispatchedSlotKey hash of the source chain DispatchedHook slot number to do a storage proof for + * @return byte value of the dispatched[nonce] + */ + function getDispatchedValue( + bytes calldata _proofs, + bytes32 _dispatchedSlotKey + ) public view returns (bytes memory) { + // Get the slot value as bytes + bytes[][] memory proofs = abi.decode(_proofs, (bytes[][])); + bytes[] memory accountProof = proofs[0]; + bytes[] memory storageProof = proofs[1]; + + // Get the storage root of DispatchedHook + bytes32 storageRoot = StorageProof.getStorageRoot( + address(dispatchedHook), + accountProof, + getHeadStateRoot() + ); + // Returns the value of dispatched + return + StorageProof.getStorageBytes( + keccak256(abi.encode(_dispatchedSlotKey)), + storageProof, + storageRoot + ); + } + + /** + * @notice Gets the current head state root from LightClient + */ + function getHeadStateRoot() public view virtual returns (bytes32); + + /** + * @notice Gets the current head state slot from LightClient + */ + function getHeadStateSlot() public view virtual returns (uint256); + + /** + * @notice Reverts with the data needed to query for header proofs + * @dev See https://eips.ethereum.org/EIPS/eip-3668 for more information + * @param _message encoded Message that will be included in offchain query + * + * @dev In the future, check if fees have been paid before request a proof from. + * For now this feature is not complete according to the team. + */ + function getOffchainVerifyInfo( + bytes calldata _message + ) external view virtual; + + /** + * @notice Calculates storage key of the source chain DispatchedHook.dispatched mapping + * @param _messageNonce message nonce + * + * mapping(uint256 messageNonce => messageId) + */ + function dispatchedSlotKey( + uint32 _messageNonce + ) public view returns (bytes32) { + return keccak256(abi.encode(_messageNonce, dispatchedSlot)); + } + + /** + * @notice Callback after CCIP read activities are complete. + * @dev See https://eips.ethereum.org/EIPS/eip-3668 for more information + * @param _proofs response from CCIP read that will be passed back to verify() through the DispatchedHook + * @param _message data that will help construct the offchain query + */ + function process(bytes calldata _proofs, bytes calldata _message) external { + mailbox.process(_proofs, _message); + } +} diff --git a/solidity/test/isms/Sp1LightClientIsm.t.sol b/solidity/test/isms/Sp1LightClientIsm.t.sol index b25700c75b..51da212f46 100644 --- a/solidity/test/isms/Sp1LightClientIsm.t.sol +++ b/solidity/test/isms/Sp1LightClientIsm.t.sol @@ -10,6 +10,7 @@ import {TypeCasts} from "../../contracts/libs/TypeCasts.sol"; import {MessageUtils} from "../isms/IsmTestUtils.sol"; import {MockMailbox} from "../../contracts/mock/MockMailbox.sol"; import {SP1LightClientIsm} from "../../contracts/isms/ccip-read/SP1LightClientIsm.sol"; +import {StorageProofIsm} from "../../contracts/isms/ccip-read/StorageProofIsm.sol"; import {DispatchedHook} from "../../contracts/hooks/DispatchedHook.sol"; import {ICcipReadIsm} from "../../contracts/interfaces/isms/ICcipReadIsm.sol"; import {StateProofHelpersTest} from "../lib/StateProofHelpers.t.sol"; @@ -48,7 +49,7 @@ contract SP1LightClientIsmTest is StateProofHelpersTest { hook = DispatchedHook(HOOK_ADDR); sp1LightClientIsm.initialize({ - _destinationMailbox: address(mailbox), + _mailbox: address(mailbox), _dispatchedHook: address(hook), _lightClient: address(lightClient), _dispatchedSlot: DISPATCHED_SLOT, @@ -130,9 +131,9 @@ contract SP1LightClientIsmTest is StateProofHelpersTest { ISuccinctProofsService.getProofs.selector, address(HOOK_ADDR), sp1LightClientIsm.dispatchedSlotKey(_messageNonce), - sp1LightClientIsm.getHeadStateRoot() + sp1LightClientIsm.getHeadStateSlot() ), - SP1LightClientIsm.process.selector, + StorageProofIsm.process.selector, encodedMessage ); vm.expectRevert(offChainLookupError);