Skip to content

Commit 3679c0b

Browse files
authored
Merge pull request #28 from base-org/jack/proof-libraries
move proof verification logic into libraries
2 parents fe6ab96 + 5cf3717 commit 3679c0b

14 files changed

+476
-482
lines changed

contracts/src/RIP7755Outbox.sol

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -110,22 +110,16 @@ abstract contract RIP7755Outbox {
110110
/// can prove it with a valid nested storage proof
111111
///
112112
/// @param request A cross chain request structured as a `CrossChainRequest`
113-
/// @param fulfillmentInfo The fill info that should be in storage in `RIP7755Inbox` on destination chain
114113
/// @param proof A proof that cryptographically verifies that `fulfillmentInfo` does, indeed, exist in
115114
/// storage on the destination chain
116115
/// @param payTo The address the Filler wants to receive the reward
117-
function claimReward(
118-
CrossChainRequest calldata request,
119-
RIP7755Inbox.FulfillmentInfo calldata fulfillmentInfo,
120-
bytes calldata proof,
121-
address payTo
122-
) external {
116+
function claimReward(CrossChainRequest calldata request, bytes calldata proof, address payTo) external {
123117
bytes32 requestHash = hashRequest(request);
124118
bytes memory storageKey = abi.encode(keccak256(abi.encodePacked(requestHash, _VERIFIER_STORAGE_LOCATION)));
125119

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

128-
_validateProof(storageKey, fulfillmentInfo, request, proof);
122+
_validateProof(storageKey, request, proof);
129123

130124
_requestStatus[requestHash] = CrossChainCallStatus.Completed;
131125

@@ -231,14 +225,27 @@ abstract contract RIP7755Outbox {
231225
///
232226
/// @param inboxContractStorageKey The storage location of the data to verify on the destination chain
233227
/// `RIP7755Inbox` contract
234-
/// @param fulfillmentInfo The fulfillment info that should be located at `inboxContractStorageKey` in storage
235-
/// on the destination chain `RIP7755Inbox` contract
236228
/// @param request The original cross chain request submitted to this contract
237229
/// @param proofData The proof to validate
238230
function _validateProof(
239231
bytes memory inboxContractStorageKey,
240-
RIP7755Inbox.FulfillmentInfo calldata fulfillmentInfo,
241232
CrossChainRequest calldata request,
242233
bytes calldata proofData
243234
) internal virtual;
235+
236+
/// @notice Decodes the `FulfillmentInfo` struct from the `RIP7755Inbox` storage slot
237+
///
238+
/// @param inboxContractStorageValue The storage value of the `RIP7755Inbox` storage slot
239+
///
240+
/// @return fulfillmentInfo The decoded `FulfillmentInfo` struct
241+
function _decodeFulfillmentInfo(bytes32 inboxContractStorageValue)
242+
internal
243+
pure
244+
returns (RIP7755Inbox.FulfillmentInfo memory)
245+
{
246+
RIP7755Inbox.FulfillmentInfo memory fulfillmentInfo;
247+
fulfillmentInfo.filler = address(uint160((uint256(inboxContractStorageValue) >> 96) & type(uint160).max));
248+
fulfillmentInfo.timestamp = uint96(uint256(inboxContractStorageValue));
249+
return fulfillmentInfo;
250+
}
244251
}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
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+
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
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 OPStackProver
12+
///
13+
/// @author Coinbase (https://github.com/base-org/RIP-7755-poc)
14+
///
15+
/// @notice This is a utility library for validating OP Stack storage proofs.
16+
library OPStackProver {
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 Optimism's AnchorStateRegistry 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 Optimism'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
35+
struct RIP7755Proof {
36+
/// @dev The storage root of Optimism's MessagePasser contract - used to compute our L1 storage value
37+
bytes32 l2MessagePasserStorageRoot;
38+
/// @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.
39+
bytes encodedBlockArray;
40+
/// @dev Parameters needed to validate the authenticity of Ethereum's execution client's state root
41+
StateValidator.StateProofParameters stateProofParams;
42+
/// @dev Parameters needed to validate the authenticity of the l2Oracle for the destination L2 chain on Eth
43+
/// mainnet
44+
StateValidator.AccountProofParameters dstL2StateRootProofParams;
45+
/// @dev Parameters needed to validate the authenticity of a specified storage location in `RIP7755Inbox` on
46+
/// the destination L2 chain
47+
StateValidator.AccountProofParameters dstL2AccountProofParams;
48+
}
49+
50+
/// @notice This error is thrown when verification of the authenticity of the l2Oracle for the destination L2 chain
51+
/// on Eth mainnet fails
52+
error InvalidL1Storage();
53+
54+
/// @notice This error is thrown when verification of the authenticity of the `RIP7755Inbox` storage on the
55+
/// destination L2 chain fails
56+
error InvalidL2Storage();
57+
58+
/// @notice This error is thrown when the supplied l2StateRoot does not correspond to our validated L1 state
59+
error InvalidL2StateRoot();
60+
61+
/// @notice This error is thrown when the encoded block headers does not contain all 16 fields
62+
error InvalidBlockFieldRLP();
63+
64+
/// @notice Validates storage proofs and verifies fulfillment
65+
///
66+
/// @custom:reverts If storage proof invalid.
67+
/// @custom:reverts If fulfillmentInfo not found at inboxContractStorageKey on request.inboxContract
68+
/// @custom:reverts If fulfillmentInfo.timestamp is less than request.finalityDelaySeconds from current destination
69+
/// chain block timestamp.
70+
/// @custom:reverts If the L2StateRoot does not correspond to the validated L1 storage slot
71+
///
72+
/// @dev Implementation will vary by L2
73+
///
74+
/// @param proof The proof to validate
75+
/// @param target The proof target on L1 and dst L2
76+
///
77+
/// @return l2Timestamp The timestamp of the validated L2 state root
78+
/// @return l2StorageValue The storage value of the `RIP7755Inbox` storage slot
79+
function validate(bytes calldata proof, Target memory target) internal view returns (uint256, bytes memory) {
80+
RIP7755Proof memory proofData = abi.decode(proof, (RIP7755Proof));
81+
82+
// Set the expected storage key and value for the `RIP7755Inbox` on the destination OP Stack chain
83+
// NOTE: the following two lines are temporarily commented out for hacky tests
84+
// proofData.dstL2AccountProofParams.storageKey = target.l2StorageKey;
85+
// proofData.dstL2AccountProofParams.storageValue = _encodeFulfillmentInfo(fulfillmentInfo);
86+
87+
// We first need to validate knowledge of the destination L2 chain's state root.
88+
// StateValidator.validateState will accomplish each of the following 4 steps:
89+
// 1. Confirm beacon root
90+
// 2. Validate L1 state root
91+
// 3. Validate L1 account proof where `account` here is the destination chain's AnchorStateRegistry contract
92+
// 4. Validate storage proof proving destination L2 root stored in L1 AnchorStateRegistry contract
93+
bool validState =
94+
target.l1Address.validateState(proofData.stateProofParams, proofData.dstL2StateRootProofParams);
95+
96+
if (!validState) {
97+
revert InvalidL1Storage();
98+
}
99+
100+
// As an intermediate step, we need to prove that `proofData.dstL2StateRootProofParams.storageValue` is linked
101+
// to the correct l2StateRoot before we can prove l2Storage
102+
103+
bytes32 version;
104+
// Extract the L2 stateRoot and timestamp from the RLP-encoded block array
105+
(bytes32 l2StateRoot, uint256 l2Timestamp) = _extractL2StateRootAndTimestamp(proofData.encodedBlockArray);
106+
// Derive the L2 blockhash
107+
bytes32 l2BlockHash = keccak256(proofData.encodedBlockArray);
108+
109+
// Compute the expected destination chain output root (which is the value we just proved is in the L1 storage slot)
110+
bytes32 expectedOutputRoot =
111+
keccak256(abi.encodePacked(version, l2StateRoot, proofData.l2MessagePasserStorageRoot, l2BlockHash));
112+
// If this checks out, it means we know the correct l2StateRoot
113+
if (bytes32(proofData.dstL2StateRootProofParams.storageValue) != expectedOutputRoot) {
114+
revert InvalidL2StateRoot();
115+
}
116+
117+
// Because the previous step confirmed L1 state, we do not need to repeat steps 1 and 2 again
118+
// We now just need to validate account storage on the destination L2 using StateValidator.validateAccountStorage
119+
// This library function will accomplish the following 2 steps:
120+
// 5. Validate L2 account proof where `account` here is `RIP7755Inbox` on destination chain
121+
// 6. Validate storage proof proving FulfillmentInfo in `RIP7755Inbox` storage
122+
// NOTE: the following line is a temporary line used to validate proof logic. Will be removed in the near future.
123+
bool validL2Storage = 0xAd6A7addf807D846A590E76C5830B609F831Ba2E.validateAccountStorage(
124+
l2StateRoot, proofData.dstL2AccountProofParams
125+
);
126+
// bool validL2Storage =
127+
// target.l2Address.validateAccountStorage(proofData.l2StateRoot, proofData.dstL2AccountProofParams);
128+
129+
if (!validL2Storage) {
130+
revert InvalidL2Storage();
131+
}
132+
133+
return (l2Timestamp, proofData.dstL2AccountProofParams.storageValue);
134+
}
135+
136+
/// @notice Extracts the l2StateRoot and l2Timestamp from the RLP-encoded block headers array
137+
///
138+
/// @custom:reverts If the encoded block array has less than 15 elements
139+
///
140+
/// @dev The stateRoot should be the 4th element, and the timestamp should be the 12th element
141+
function _extractL2StateRootAndTimestamp(bytes memory encodedBlockArray) private pure returns (bytes32, uint256) {
142+
RLPReader.RLPItem[] memory blockFields = encodedBlockArray.readList();
143+
144+
if (blockFields.length < 15) {
145+
revert InvalidBlockFieldRLP();
146+
}
147+
148+
return (bytes32(blockFields[3].readBytes()), uint256(bytes32(blockFields[11].readBytes())));
149+
}
150+
}

0 commit comments

Comments
 (0)