Skip to content

Commit

Permalink
Generalize into StorageProofIsm
Browse files Browse the repository at this point in the history
  • Loading branch information
ltyu committed Nov 1, 2024
1 parent b5a8f8f commit df5731c
Show file tree
Hide file tree
Showing 3 changed files with 197 additions and 134 deletions.
143 changes: 12 additions & 131 deletions solidity/contracts/isms/ccip-read/SP1LightClientIsm.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

/**
Expand All @@ -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,
Expand All @@ -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);
}
}
181 changes: 181 additions & 0 deletions solidity/contracts/isms/ccip-read/StorageProofIsm.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
7 changes: 4 additions & 3 deletions solidity/test/isms/Sp1LightClientIsm.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit df5731c

Please sign in to comment.