L2 Oracle
In the OpStack system, trusted proposers periodically submit L2 state commitments (output roots) to the L1 chain. These submissions occur at designated intervals. Anyone acting as a challenger can dispute an invalid output root. If no challenge arises within a specific timeframe, the submitted output root is considered final. This approach leverages a permissioned proposer and challenger.
L2 Output Root Construction
The output_root
is a 32 byte value, which is derived based on the a versioned scheme:
payload = state_root || withdrawal_storage_root || latest_block_hash
output_root = keccak256(version_byte || payload)
- The
latest_block_hash
is the block hash for the latest L2 block at the time the proposer constructed the output root. - The
state_root
is the Merkle-Patricia-Trie (MPT) root of the L2 state db. - The
withdrawal_storage_root
is the state root of the Message Passer contract storage. - The
version_byte
a simple 32 byte version string which increments anytime the construction of the output root is changed.
Verifying the Output Root
The finalization of an L1 block soft finalizes all output roots proposed up until the finalized L1 block. To verify an output root, a light client needs to verify it's existence in the state trie of the finalized L1 chain. The algorithm for doing that is described below:
type H256 = [u8; 32]
type U256 = [u64; 4];
struct OpstackProof {
/// Actual state root of the opstack execution layer
state_root: H256,
/// Storage root hash of the opstack withdrawal contracts
withdrawal_storage_root: H256,
/// Optimism Block hash at which the values aboved were fetched
l2_block_hash: H256,
/// L2Oracle contract version
version: H256,
/// Membership Proof for the L2Oracle contract account in the ethereum world trie
l2_oracle_proof: Vec<Vec<u8>>,
/// Membership proof for output root in l2Outputs array
output_root_proof: Vec<Vec<u8>>,
/// Membership proof Timestamp and block number in the l2Outputs array
multi_proof: Vec<Vec<u8>>,
/// Index of the output root that needs to be proved in the l2Outputs array
output_root_index: u64,
/// Block number
block_number: u64,
/// Timestamp
timestamp: u64,
}
Given the proof described above, we would verify the existence of the output root as follows:
Inputs:
payload
: OpstackProof
containing proofs and state information
root
: Merkle root of the L1 chain
l2_oracle_address
: Address of the L2 oracle contract
Outputs:
state_root
: The verified l2 state root
Steps:
- Verify L2 Oracle Storage Root:
Use the provided proof (payload.l2_oracle_proof
) and oracle address to retrieve the storage root of the l2 oracle contract from the L1 state proof.
- Compute the Output Root:
Compute the output root as decribed in the previous section.
- Verify Output Root Proof:
Derive the slot hash for the output root within the l2 oracle contract storage slots.
Use the proof (payload.output_root_proof
) and l2 oracle storage root to retrieve the value associated with the slot hash.
Check if a value was found. If not, return an error indicating the proof failed verification.
Decode the retrieved value as a byte array and compare it to the calculated output root. If they don't match, return an error indicating an invalid proof.
- Verify Timestamp and Block Number:
Derive the slot hash for the block number and timestamp within the l2 oracle contract storage slots.
Use the multi-proof (payload.multi_proof
) and l2 oracle storage root to retrieve the value associated with the slot hash.
Check if a value was found. If not, return an error indicating the proof failed verification.
Decode the retrieved value as a byte array and convert it to a U256 value.
Extract the timestamp (first two u64 values) and block number (last two u64 values) from the U256 value.
Compare the extracted timestamp and block number with the corresponding values in the payload. If they don't match, return an error indicating an invalid proof.
If all steps complete successfully then we can accept the state_root
, timestamp
and block_number
as valid.