|
| 1 | +// SPDX-License-Identifier: MIT |
| 2 | +pragma solidity 0.8.24; |
| 3 | + |
| 4 | +import {RLPReader} from "optimism/packages/contracts-bedrock/src/libraries/rlp/RLPReader.sol"; |
| 5 | + |
| 6 | +import {StateValidator} from "../StateValidator.sol"; |
| 7 | +import {RIP7755Inbox} from "../../RIP7755Inbox.sol"; |
| 8 | +import {RIP7755Outbox} from "../../RIP7755Outbox.sol"; |
| 9 | +import {CrossChainRequest} from "../../RIP7755Structs.sol"; |
| 10 | + |
| 11 | +/// @title ArbitrumProver |
| 12 | +/// |
| 13 | +/// @author Coinbase (https://github.com/base-org/RIP-7755-poc) |
| 14 | +/// |
| 15 | +/// @notice This is a utility library for validating Arbitrum storage proofs. |
| 16 | +library ArbitrumProver { |
| 17 | + using StateValidator for address; |
| 18 | + using RLPReader for RLPReader.RLPItem; |
| 19 | + using RLPReader for bytes; |
| 20 | + |
| 21 | + /// @notice The address and storage keys to validate on L1 and L2 |
| 22 | + struct Target { |
| 23 | + /// @dev The address of the L1 contract to validate. Should be Arbitrum's Rollup contract |
| 24 | + address l1Address; |
| 25 | + /// @dev The storage key on L1 to validate |
| 26 | + bytes32 l1StorageKey; |
| 27 | + /// @dev The address of the L2 contract to validate. Should be Arbitrum's `RIP7755Inbox` contract |
| 28 | + address l2Address; |
| 29 | + /// @dev The storage key on L2 to validate. Should be the `RIP7755Inbox` storage slot containing the |
| 30 | + /// `FulfillmentInfo` struct |
| 31 | + bytes32 l2StorageKey; |
| 32 | + } |
| 33 | + |
| 34 | + /// @notice Parameters needed for a full nested cross-L2 storage proof with Arbitrum as the destination chain |
| 35 | + struct RIP7755Proof { |
| 36 | + /// @dev The root hash of a Merkle tree that contains all the messages sent from Arbitrum to L1 |
| 37 | + bytes sendRoot; |
| 38 | + /// @dev The index of Arbitrum's RBlock containing the state root to use in our storage proof |
| 39 | + uint64 nodeIndex; |
| 40 | + /// @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. |
| 41 | + bytes encodedBlockArray; |
| 42 | + /// @dev Parameters needed to validate the authenticity of Ethereum's execution client's state root |
| 43 | + StateValidator.StateProofParameters stateProofParams; |
| 44 | + /// @dev Parameters needed to validate the authenticity of the l2Oracle for the destination L2 chain on Eth |
| 45 | + /// mainnet |
| 46 | + StateValidator.AccountProofParameters dstL2StateRootProofParams; |
| 47 | + /// @dev Parameters needed to validate the authenticity of a specified storage location in `RIP7755Inbox` on |
| 48 | + /// the destination L2 chain |
| 49 | + StateValidator.AccountProofParameters dstL2AccountProofParams; |
| 50 | + } |
| 51 | + |
| 52 | + /// @notice The storage slot offset of the `confirmData` field in an Arbitrum RBlock |
| 53 | + uint256 private constant _ARBITRUM_RBLOCK_CONFIRMDATA_STORAGE_OFFSET = 2; |
| 54 | + |
| 55 | + /// @notice This error is thrown when verification of the authenticity of the l2Oracle for the destination L2 chain |
| 56 | + /// on Eth mainnet fails |
| 57 | + error InvalidStateRoot(); |
| 58 | + |
| 59 | + /// @notice This error is thrown when verification of the authenticity of the `RIP7755Inbox` storage on the |
| 60 | + /// destination L2 chain fails |
| 61 | + error InvalidL2Storage(); |
| 62 | + |
| 63 | + /// @notice This error is thrown when the derived `confirmData` does not match the value in the validated L1 storage slot |
| 64 | + error InvalidConfirmData(); |
| 65 | + |
| 66 | + /// @notice This error is thrown when the encoded block headers does not contain all 16 fields |
| 67 | + error InvalidBlockFieldRLP(); |
| 68 | + |
| 69 | + /// @notice Validates storage proofs and verifies fulfillment |
| 70 | + /// |
| 71 | + /// @custom:reverts If storage proof invalid. |
| 72 | + /// @custom:reverts If fulfillmentInfo not found at verifyingContractStorageKey on request.verifyingContract |
| 73 | + /// @custom:reverts If fulfillmentInfo.timestamp is less than request.finalityDelaySeconds from current destination |
| 74 | + /// chain block timestamp. |
| 75 | + /// @custom:reverts If the L2StorageRoot does not correspond to our validated L1 storage slot |
| 76 | + /// |
| 77 | + /// @param proof The proof to validate |
| 78 | + /// @param target The proof target on L1 and dst L2 |
| 79 | + /// |
| 80 | + /// @return l2Timestamp The timestamp of the validated L2 state root |
| 81 | + /// @return l2StorageValue The storage value of the `RIP7755Inbox` storage slot |
| 82 | + function validate(bytes calldata proof, Target memory target) internal view returns (uint256, bytes memory) { |
| 83 | + RIP7755Proof memory proofData = abi.decode(proof, (RIP7755Proof)); |
| 84 | + |
| 85 | + // Set the expected storage key and value for the `RIP7755Inbox` on Arbitrum |
| 86 | + proofData.dstL2AccountProofParams.storageKey = abi.encode(target.l2StorageKey); |
| 87 | + |
| 88 | + // Derive the L1 storage key to use in the storage proof. For Arbitrum, we will use the storage slot containing |
| 89 | + // the `confirmData` field in a posted RBlock |
| 90 | + // See https://github.com/OffchainLabs/nitro-contracts/blob/main/src/rollup/Node.sol#L21 for the RBlock structure |
| 91 | + // See https://github.com/OffchainLabs/nitro-contracts/blob/main/src/rollup/RollupCore.sol#L64 for the mapping location |
| 92 | + proofData.dstL2StateRootProofParams.storageKey = _deriveL1StorageKey(proofData, target.l1StorageKey); |
| 93 | + |
| 94 | + // We first need to validate knowledge of the destination L2 chain's state root. |
| 95 | + // StateValidator.validateState will accomplish each of the following 4 steps: |
| 96 | + // 1. Confirm beacon root |
| 97 | + // 2. Validate L1 state root |
| 98 | + // 3. Validate L1 account proof where `account` here is Arbitrum's Rollup contract |
| 99 | + // 4. Validate storage proof proving destination L2 root stored in Rollup contract |
| 100 | + bool validState = |
| 101 | + target.l1Address.validateState(proofData.stateProofParams, proofData.dstL2StateRootProofParams); |
| 102 | + |
| 103 | + if (!validState) { |
| 104 | + revert InvalidStateRoot(); |
| 105 | + } |
| 106 | + |
| 107 | + // As an intermediate step, we need to prove that `proofData.dstL2StateRootProofParams.storageValue` is linked |
| 108 | + // to the correct l2StateRoot before we can prove l2Storage |
| 109 | + |
| 110 | + // Derive the L2 blockhash |
| 111 | + bytes32 l2BlockHash = keccak256(proofData.encodedBlockArray); |
| 112 | + // Derive the RBlock's `confirmData` field |
| 113 | + bytes32 confirmData = keccak256(abi.encodePacked(l2BlockHash, proofData.sendRoot)); |
| 114 | + // Extract the L2 stateRoot and timestamp from the RLP-encoded block array |
| 115 | + (bytes32 l2StateRoot, uint256 l2Timestamp) = _extractL2StateRootAndTimestamp(proofData.encodedBlockArray); |
| 116 | + |
| 117 | + // The L1 storage value we proved was the node's confirmData |
| 118 | + if (bytes32(proofData.dstL2StateRootProofParams.storageValue) != confirmData) { |
| 119 | + revert InvalidConfirmData(); |
| 120 | + } |
| 121 | + |
| 122 | + // Because the previous step confirmed L1 state, we do not need to repeat steps 1 and 2 again |
| 123 | + // We now just need to validate account storage on the destination L2 using StateValidator.validateAccountStorage |
| 124 | + // This library function will accomplish the following 2 steps: |
| 125 | + // 5. Validate L2 account proof where `account` here is `RIP7755Inbox` on destination chain |
| 126 | + // 6. Validate storage proof proving FulfillmentInfo in `RIP7755Inbox` storage |
| 127 | + bool validL2Storage = target.l2Address.validateAccountStorage(l2StateRoot, proofData.dstL2AccountProofParams); |
| 128 | + |
| 129 | + if (!validL2Storage) { |
| 130 | + revert InvalidL2Storage(); |
| 131 | + } |
| 132 | + |
| 133 | + return (l2Timestamp, proofData.dstL2AccountProofParams.storageValue); |
| 134 | + } |
| 135 | + |
| 136 | + /// @notice Derives the L1 storageKey using the supplied `nodeIndex` and the `confirmData` storage slot offset |
| 137 | + function _deriveL1StorageKey(RIP7755Proof memory proofData, bytes32 l1StorageKey) |
| 138 | + private |
| 139 | + pure |
| 140 | + returns (bytes memory) |
| 141 | + { |
| 142 | + uint256 startingStorageSlot = uint256(keccak256(abi.encode(proofData.nodeIndex, l1StorageKey))); |
| 143 | + return abi.encodePacked(startingStorageSlot + _ARBITRUM_RBLOCK_CONFIRMDATA_STORAGE_OFFSET); |
| 144 | + } |
| 145 | + |
| 146 | + /// @notice Extracts the l2StateRoot and l2Timestamp from the RLP-encoded block headers array |
| 147 | + /// |
| 148 | + /// @custom:reverts If the encoded block array has less than 15 elements |
| 149 | + /// |
| 150 | + /// @dev The stateRoot should be the 4th element, and the timestamp should be the 12th element |
| 151 | + function _extractL2StateRootAndTimestamp(bytes memory encodedBlockArray) private pure returns (bytes32, uint256) { |
| 152 | + RLPReader.RLPItem[] memory blockFields = encodedBlockArray.readList(); |
| 153 | + |
| 154 | + if (blockFields.length < 15) { |
| 155 | + revert InvalidBlockFieldRLP(); |
| 156 | + } |
| 157 | + |
| 158 | + return (bytes32(blockFields[3].readBytes()), uint256(bytes32(blockFields[11].readBytes()))); |
| 159 | + } |
| 160 | +} |
0 commit comments