Skip to content

Commit

Permalink
Merge pull request #28 from base-org/jack/proof-libraries
Browse files Browse the repository at this point in the history
move proof verification logic into libraries
  • Loading branch information
jackchuma authored Nov 22, 2024
2 parents fe6ab96 + 5cf3717 commit 3679c0b
Show file tree
Hide file tree
Showing 14 changed files with 476 additions and 482 deletions.
29 changes: 18 additions & 11 deletions contracts/src/RIP7755Outbox.sol
Original file line number Diff line number Diff line change
Expand Up @@ -110,22 +110,16 @@ abstract contract RIP7755Outbox {
/// can prove it with a valid nested storage proof
///
/// @param request A cross chain request structured as a `CrossChainRequest`
/// @param fulfillmentInfo The fill info that should be in storage in `RIP7755Inbox` on destination chain
/// @param proof A proof that cryptographically verifies that `fulfillmentInfo` does, indeed, exist in
/// storage on the destination chain
/// @param payTo The address the Filler wants to receive the reward
function claimReward(
CrossChainRequest calldata request,
RIP7755Inbox.FulfillmentInfo calldata fulfillmentInfo,
bytes calldata proof,
address payTo
) external {
function claimReward(CrossChainRequest calldata request, bytes calldata proof, address payTo) external {
bytes32 requestHash = hashRequest(request);
bytes memory storageKey = abi.encode(keccak256(abi.encodePacked(requestHash, _VERIFIER_STORAGE_LOCATION)));

_checkValidStatus({requestHash: requestHash, expectedStatus: CrossChainCallStatus.Requested});

_validateProof(storageKey, fulfillmentInfo, request, proof);
_validateProof(storageKey, request, proof);

_requestStatus[requestHash] = CrossChainCallStatus.Completed;

Expand Down Expand Up @@ -231,14 +225,27 @@ abstract contract RIP7755Outbox {
///
/// @param inboxContractStorageKey The storage location of the data to verify on the destination chain
/// `RIP7755Inbox` contract
/// @param fulfillmentInfo The fulfillment info that should be located at `inboxContractStorageKey` in storage
/// on the destination chain `RIP7755Inbox` contract
/// @param request The original cross chain request submitted to this contract
/// @param proofData The proof to validate
function _validateProof(
bytes memory inboxContractStorageKey,
RIP7755Inbox.FulfillmentInfo calldata fulfillmentInfo,
CrossChainRequest calldata request,
bytes calldata proofData
) internal virtual;

/// @notice Decodes the `FulfillmentInfo` struct from the `RIP7755Inbox` storage slot
///
/// @param inboxContractStorageValue The storage value of the `RIP7755Inbox` storage slot
///
/// @return fulfillmentInfo The decoded `FulfillmentInfo` struct
function _decodeFulfillmentInfo(bytes32 inboxContractStorageValue)
internal
pure
returns (RIP7755Inbox.FulfillmentInfo memory)
{
RIP7755Inbox.FulfillmentInfo memory fulfillmentInfo;
fulfillmentInfo.filler = address(uint160((uint256(inboxContractStorageValue) >> 96) & type(uint160).max));
fulfillmentInfo.timestamp = uint96(uint256(inboxContractStorageValue));
return fulfillmentInfo;
}
}
160 changes: 160 additions & 0 deletions contracts/src/libraries/provers/ArbitrumProver.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import {RLPReader} from "optimism/packages/contracts-bedrock/src/libraries/rlp/RLPReader.sol";

import {StateValidator} from "../StateValidator.sol";
import {RIP7755Inbox} from "../../RIP7755Inbox.sol";
import {RIP7755Outbox} from "../../RIP7755Outbox.sol";
import {CrossChainRequest} from "../../RIP7755Structs.sol";

/// @title ArbitrumProver
///
/// @author Coinbase (https://github.com/base-org/RIP-7755-poc)
///
/// @notice This is a utility library for validating Arbitrum storage proofs.
library ArbitrumProver {
using StateValidator for address;
using RLPReader for RLPReader.RLPItem;
using RLPReader for bytes;

/// @notice The address and storage keys to validate on L1 and L2
struct Target {
/// @dev The address of the L1 contract to validate. Should be Arbitrum's Rollup contract
address l1Address;
/// @dev The storage key on L1 to validate
bytes32 l1StorageKey;
/// @dev The address of the L2 contract to validate. Should be Arbitrum's `RIP7755Inbox` contract
address l2Address;
/// @dev The storage key on L2 to validate. Should be the `RIP7755Inbox` storage slot containing the
/// `FulfillmentInfo` struct
bytes32 l2StorageKey;
}

/// @notice Parameters needed for a full nested cross-L2 storage proof with Arbitrum as the destination chain
struct RIP7755Proof {
/// @dev The root hash of a Merkle tree that contains all the messages sent from Arbitrum to L1
bytes sendRoot;
/// @dev The index of Arbitrum's RBlock containing the state root to use in our storage proof
uint64 nodeIndex;
/// @dev The RLP-encoded array of block headers of Arbitrum's L2 block corresponding to the above RBlock. Hashing this bytes string should produce the blockhash.
bytes encodedBlockArray;
/// @dev Parameters needed to validate the authenticity of Ethereum's execution client's state root
StateValidator.StateProofParameters stateProofParams;
/// @dev Parameters needed to validate the authenticity of the l2Oracle for the destination L2 chain on Eth
/// mainnet
StateValidator.AccountProofParameters dstL2StateRootProofParams;
/// @dev Parameters needed to validate the authenticity of a specified storage location in `RIP7755Inbox` on
/// the destination L2 chain
StateValidator.AccountProofParameters dstL2AccountProofParams;
}

/// @notice The storage slot offset of the `confirmData` field in an Arbitrum RBlock
uint256 private constant _ARBITRUM_RBLOCK_CONFIRMDATA_STORAGE_OFFSET = 2;

/// @notice This error is thrown when verification of the authenticity of the l2Oracle for the destination L2 chain
/// on Eth mainnet fails
error InvalidStateRoot();

/// @notice This error is thrown when verification of the authenticity of the `RIP7755Inbox` storage on the
/// destination L2 chain fails
error InvalidL2Storage();

/// @notice This error is thrown when the derived `confirmData` does not match the value in the validated L1 storage slot
error InvalidConfirmData();

/// @notice This error is thrown when the encoded block headers does not contain all 16 fields
error InvalidBlockFieldRLP();

/// @notice Validates storage proofs and verifies fulfillment
///
/// @custom:reverts If storage proof invalid.
/// @custom:reverts If fulfillmentInfo not found at verifyingContractStorageKey on request.verifyingContract
/// @custom:reverts If fulfillmentInfo.timestamp is less than request.finalityDelaySeconds from current destination
/// chain block timestamp.
/// @custom:reverts If the L2StorageRoot does not correspond to our validated L1 storage slot
///
/// @param proof The proof to validate
/// @param target The proof target on L1 and dst L2
///
/// @return l2Timestamp The timestamp of the validated L2 state root
/// @return l2StorageValue The storage value of the `RIP7755Inbox` storage slot
function validate(bytes calldata proof, Target memory target) internal view returns (uint256, bytes memory) {
RIP7755Proof memory proofData = abi.decode(proof, (RIP7755Proof));

// Set the expected storage key and value for the `RIP7755Inbox` on Arbitrum
proofData.dstL2AccountProofParams.storageKey = abi.encode(target.l2StorageKey);

// Derive the L1 storage key to use in the storage proof. For Arbitrum, we will use the storage slot containing
// the `confirmData` field in a posted RBlock
// See https://github.com/OffchainLabs/nitro-contracts/blob/main/src/rollup/Node.sol#L21 for the RBlock structure
// See https://github.com/OffchainLabs/nitro-contracts/blob/main/src/rollup/RollupCore.sol#L64 for the mapping location
proofData.dstL2StateRootProofParams.storageKey = _deriveL1StorageKey(proofData, target.l1StorageKey);

// We first need to validate knowledge of the destination L2 chain's state root.
// StateValidator.validateState will accomplish each of the following 4 steps:
// 1. Confirm beacon root
// 2. Validate L1 state root
// 3. Validate L1 account proof where `account` here is Arbitrum's Rollup contract
// 4. Validate storage proof proving destination L2 root stored in Rollup contract
bool validState =
target.l1Address.validateState(proofData.stateProofParams, proofData.dstL2StateRootProofParams);

if (!validState) {
revert InvalidStateRoot();
}

// As an intermediate step, we need to prove that `proofData.dstL2StateRootProofParams.storageValue` is linked
// to the correct l2StateRoot before we can prove l2Storage

// Derive the L2 blockhash
bytes32 l2BlockHash = keccak256(proofData.encodedBlockArray);
// Derive the RBlock's `confirmData` field
bytes32 confirmData = keccak256(abi.encodePacked(l2BlockHash, proofData.sendRoot));
// Extract the L2 stateRoot and timestamp from the RLP-encoded block array
(bytes32 l2StateRoot, uint256 l2Timestamp) = _extractL2StateRootAndTimestamp(proofData.encodedBlockArray);

// The L1 storage value we proved was the node's confirmData
if (bytes32(proofData.dstL2StateRootProofParams.storageValue) != confirmData) {
revert InvalidConfirmData();
}

// Because the previous step confirmed L1 state, we do not need to repeat steps 1 and 2 again
// We now just need to validate account storage on the destination L2 using StateValidator.validateAccountStorage
// This library function will accomplish the following 2 steps:
// 5. Validate L2 account proof where `account` here is `RIP7755Inbox` on destination chain
// 6. Validate storage proof proving FulfillmentInfo in `RIP7755Inbox` storage
bool validL2Storage = target.l2Address.validateAccountStorage(l2StateRoot, proofData.dstL2AccountProofParams);

if (!validL2Storage) {
revert InvalidL2Storage();
}

return (l2Timestamp, proofData.dstL2AccountProofParams.storageValue);
}

/// @notice Derives the L1 storageKey using the supplied `nodeIndex` and the `confirmData` storage slot offset
function _deriveL1StorageKey(RIP7755Proof memory proofData, bytes32 l1StorageKey)
private
pure
returns (bytes memory)
{
uint256 startingStorageSlot = uint256(keccak256(abi.encode(proofData.nodeIndex, l1StorageKey)));
return abi.encodePacked(startingStorageSlot + _ARBITRUM_RBLOCK_CONFIRMDATA_STORAGE_OFFSET);
}

/// @notice Extracts the l2StateRoot and l2Timestamp from the RLP-encoded block headers array
///
/// @custom:reverts If the encoded block array has less than 15 elements
///
/// @dev The stateRoot should be the 4th element, and the timestamp should be the 12th element
function _extractL2StateRootAndTimestamp(bytes memory encodedBlockArray) private pure returns (bytes32, uint256) {
RLPReader.RLPItem[] memory blockFields = encodedBlockArray.readList();

if (blockFields.length < 15) {
revert InvalidBlockFieldRLP();
}

return (bytes32(blockFields[3].readBytes()), uint256(bytes32(blockFields[11].readBytes())));
}
}
150 changes: 150 additions & 0 deletions contracts/src/libraries/provers/OPStackProver.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import {RLPReader} from "optimism/packages/contracts-bedrock/src/libraries/rlp/RLPReader.sol";

import {StateValidator} from "../StateValidator.sol";
import {RIP7755Inbox} from "../../RIP7755Inbox.sol";
import {RIP7755Outbox} from "../../RIP7755Outbox.sol";
import {CrossChainRequest} from "../../RIP7755Structs.sol";

/// @title OPStackProver
///
/// @author Coinbase (https://github.com/base-org/RIP-7755-poc)
///
/// @notice This is a utility library for validating OP Stack storage proofs.
library OPStackProver {
using StateValidator for address;
using RLPReader for RLPReader.RLPItem;
using RLPReader for bytes;

/// @notice The address and storage keys to validate on L1 and L2
struct Target {
/// @dev The address of the L1 contract to validate. Should be Optimism's AnchorStateRegistry contract
address l1Address;
/// @dev The storage key on L1 to validate
bytes32 l1StorageKey;
/// @dev The address of the L2 contract to validate. Should be Optimism's `RIP7755Inbox` contract
address l2Address;
/// @dev The storage key on L2 to validate. Should be the `RIP7755Inbox` storage slot containing the
/// `FulfillmentInfo` struct
bytes32 l2StorageKey;
}

/// @notice Parameters needed for a full nested cross-L2 storage proof
struct RIP7755Proof {
/// @dev The storage root of Optimism's MessagePasser contract - used to compute our L1 storage value
bytes32 l2MessagePasserStorageRoot;
/// @dev The RLP-encoded array of block headers of the chain's L2 block used for the proof. Hashing this bytes string should produce the blockhash.
bytes encodedBlockArray;
/// @dev Parameters needed to validate the authenticity of Ethereum's execution client's state root
StateValidator.StateProofParameters stateProofParams;
/// @dev Parameters needed to validate the authenticity of the l2Oracle for the destination L2 chain on Eth
/// mainnet
StateValidator.AccountProofParameters dstL2StateRootProofParams;
/// @dev Parameters needed to validate the authenticity of a specified storage location in `RIP7755Inbox` on
/// the destination L2 chain
StateValidator.AccountProofParameters dstL2AccountProofParams;
}

/// @notice This error is thrown when verification of the authenticity of the l2Oracle for the destination L2 chain
/// on Eth mainnet fails
error InvalidL1Storage();

/// @notice This error is thrown when verification of the authenticity of the `RIP7755Inbox` storage on the
/// destination L2 chain fails
error InvalidL2Storage();

/// @notice This error is thrown when the supplied l2StateRoot does not correspond to our validated L1 state
error InvalidL2StateRoot();

/// @notice This error is thrown when the encoded block headers does not contain all 16 fields
error InvalidBlockFieldRLP();

/// @notice Validates storage proofs and verifies fulfillment
///
/// @custom:reverts If storage proof invalid.
/// @custom:reverts If fulfillmentInfo not found at inboxContractStorageKey on request.inboxContract
/// @custom:reverts If fulfillmentInfo.timestamp is less than request.finalityDelaySeconds from current destination
/// chain block timestamp.
/// @custom:reverts If the L2StateRoot does not correspond to the validated L1 storage slot
///
/// @dev Implementation will vary by L2
///
/// @param proof The proof to validate
/// @param target The proof target on L1 and dst L2
///
/// @return l2Timestamp The timestamp of the validated L2 state root
/// @return l2StorageValue The storage value of the `RIP7755Inbox` storage slot
function validate(bytes calldata proof, Target memory target) internal view returns (uint256, bytes memory) {
RIP7755Proof memory proofData = abi.decode(proof, (RIP7755Proof));

// Set the expected storage key and value for the `RIP7755Inbox` on the destination OP Stack chain
// NOTE: the following two lines are temporarily commented out for hacky tests
// proofData.dstL2AccountProofParams.storageKey = target.l2StorageKey;
// proofData.dstL2AccountProofParams.storageValue = _encodeFulfillmentInfo(fulfillmentInfo);

// We first need to validate knowledge of the destination L2 chain's state root.
// StateValidator.validateState will accomplish each of the following 4 steps:
// 1. Confirm beacon root
// 2. Validate L1 state root
// 3. Validate L1 account proof where `account` here is the destination chain's AnchorStateRegistry contract
// 4. Validate storage proof proving destination L2 root stored in L1 AnchorStateRegistry contract
bool validState =
target.l1Address.validateState(proofData.stateProofParams, proofData.dstL2StateRootProofParams);

if (!validState) {
revert InvalidL1Storage();
}

// As an intermediate step, we need to prove that `proofData.dstL2StateRootProofParams.storageValue` is linked
// to the correct l2StateRoot before we can prove l2Storage

bytes32 version;
// Extract the L2 stateRoot and timestamp from the RLP-encoded block array
(bytes32 l2StateRoot, uint256 l2Timestamp) = _extractL2StateRootAndTimestamp(proofData.encodedBlockArray);
// Derive the L2 blockhash
bytes32 l2BlockHash = keccak256(proofData.encodedBlockArray);

// Compute the expected destination chain output root (which is the value we just proved is in the L1 storage slot)
bytes32 expectedOutputRoot =
keccak256(abi.encodePacked(version, l2StateRoot, proofData.l2MessagePasserStorageRoot, l2BlockHash));
// If this checks out, it means we know the correct l2StateRoot
if (bytes32(proofData.dstL2StateRootProofParams.storageValue) != expectedOutputRoot) {
revert InvalidL2StateRoot();
}

// Because the previous step confirmed L1 state, we do not need to repeat steps 1 and 2 again
// We now just need to validate account storage on the destination L2 using StateValidator.validateAccountStorage
// This library function will accomplish the following 2 steps:
// 5. Validate L2 account proof where `account` here is `RIP7755Inbox` on destination chain
// 6. Validate storage proof proving FulfillmentInfo in `RIP7755Inbox` storage
// NOTE: the following line is a temporary line used to validate proof logic. Will be removed in the near future.
bool validL2Storage = 0xAd6A7addf807D846A590E76C5830B609F831Ba2E.validateAccountStorage(
l2StateRoot, proofData.dstL2AccountProofParams
);
// bool validL2Storage =
// target.l2Address.validateAccountStorage(proofData.l2StateRoot, proofData.dstL2AccountProofParams);

if (!validL2Storage) {
revert InvalidL2Storage();
}

return (l2Timestamp, proofData.dstL2AccountProofParams.storageValue);
}

/// @notice Extracts the l2StateRoot and l2Timestamp from the RLP-encoded block headers array
///
/// @custom:reverts If the encoded block array has less than 15 elements
///
/// @dev The stateRoot should be the 4th element, and the timestamp should be the 12th element
function _extractL2StateRootAndTimestamp(bytes memory encodedBlockArray) private pure returns (bytes32, uint256) {
RLPReader.RLPItem[] memory blockFields = encodedBlockArray.readList();

if (blockFields.length < 15) {
revert InvalidBlockFieldRLP();
}

return (bytes32(blockFields[3].readBytes()), uint256(bytes32(blockFields[11].readBytes())));
}
}
Loading

0 comments on commit 3679c0b

Please sign in to comment.