reth_optimism_consensus/validation/
isthmus.rs

1//! Block verification w.r.t. consensus rules new in Isthmus hardfork.
2
3use crate::OpConsensusError;
4use alloy_consensus::BlockHeader;
5use alloy_primitives::{address, Address, B256};
6use alloy_trie::EMPTY_ROOT_HASH;
7use reth_storage_api::{errors::ProviderResult, StorageRootProvider};
8use reth_trie_common::HashedStorage;
9use revm::database::BundleState;
10use tracing::warn;
11
12/// The L2 contract `L2ToL1MessagePasser`, stores commitments to withdrawal transactions.
13pub const ADDRESS_L2_TO_L1_MESSAGE_PASSER: Address =
14    address!("0x4200000000000000000000000000000000000016");
15
16/// Verifies that `withdrawals_root` (i.e. `l2tol1-msg-passer` storage root since Isthmus) field is
17/// set in block header.
18pub fn ensure_withdrawals_storage_root_is_some<H: BlockHeader>(
19    header: H,
20) -> Result<(), OpConsensusError> {
21    header.withdrawals_root().ok_or(OpConsensusError::L2WithdrawalsRootMissing)?;
22
23    Ok(())
24}
25
26/// Computes the storage root of predeploy `L2ToL1MessagePasser.sol`.
27///
28/// Uses state updates from block execution. See also [`withdrawals_root_prehashed`].
29pub fn withdrawals_root<DB: StorageRootProvider>(
30    state_updates: &BundleState,
31    state: DB,
32) -> ProviderResult<B256> {
33    // if l2 withdrawals transactions were executed there will be storage updates for
34    // `L2ToL1MessagePasser.sol` predeploy
35    withdrawals_root_prehashed(
36        state_updates
37            .state()
38            .get(&ADDRESS_L2_TO_L1_MESSAGE_PASSER)
39            .map(|acc| {
40                HashedStorage::from_plain_storage(
41                    acc.status,
42                    acc.storage.iter().map(|(slot, value)| (slot, &value.present_value)),
43                )
44            })
45            .unwrap_or_default(),
46        state,
47    )
48}
49
50/// Computes the storage root of predeploy `L2ToL1MessagePasser.sol`.
51///
52/// Uses pre-hashed storage updates of `L2ToL1MessagePasser.sol` predeploy, resulting from
53/// execution of L2 withdrawals transactions. If none, takes empty [`HashedStorage::default`].
54pub fn withdrawals_root_prehashed<DB: StorageRootProvider>(
55    hashed_storage_updates: HashedStorage,
56    state: DB,
57) -> ProviderResult<B256> {
58    state.storage_root(ADDRESS_L2_TO_L1_MESSAGE_PASSER, hashed_storage_updates)
59}
60
61/// Verifies block header field `withdrawals_root` against storage root of
62/// `L2ToL1MessagePasser.sol` predeploy post block execution.
63///
64/// Takes state updates resulting from execution of block.
65///
66/// See <https://specs.optimism.io/protocol/isthmus/exec-engine.html#l2tol1messagepasser-storage-root-in-header>.
67pub fn verify_withdrawals_root<DB, H>(
68    state_updates: &BundleState,
69    state: DB,
70    header: H,
71) -> Result<(), OpConsensusError>
72where
73    DB: StorageRootProvider,
74    H: BlockHeader,
75{
76    let header_storage_root =
77        header.withdrawals_root().ok_or(OpConsensusError::L2WithdrawalsRootMissing)?;
78
79    let storage_root = withdrawals_root(state_updates, state)
80        .map_err(OpConsensusError::L2WithdrawalsRootCalculationFail)?;
81
82    if storage_root == EMPTY_ROOT_HASH {
83        // if there was no MessagePasser contract storage, something is wrong
84        // (it should at least store an implementation address and owner address)
85        warn!("isthmus: no storage root for L2ToL1MessagePasser contract");
86    }
87
88    if header_storage_root != storage_root {
89        return Err(OpConsensusError::L2WithdrawalsRootMismatch {
90            header: header_storage_root,
91            exec_res: storage_root,
92        })
93    }
94
95    Ok(())
96}
97
98/// Verifies block header field `withdrawals_root` against storage root of
99/// `L2ToL1MessagePasser.sol` predeploy post block execution.
100///
101/// Takes pre-hashed storage updates of `L2ToL1MessagePasser.sol` predeploy, resulting from
102/// execution of block, if any. Otherwise takes empty [`HashedStorage::default`].
103///
104/// See <https://specs.optimism.io/protocol/isthmus/exec-engine.html#l2tol1messagepasser-storage-root-in-header>.
105pub fn verify_withdrawals_root_prehashed<DB, H>(
106    hashed_storage_updates: HashedStorage,
107    state: DB,
108    header: H,
109) -> Result<(), OpConsensusError>
110where
111    DB: StorageRootProvider,
112    H: BlockHeader,
113{
114    let header_storage_root =
115        header.withdrawals_root().ok_or(OpConsensusError::L2WithdrawalsRootMissing)?;
116
117    let storage_root = withdrawals_root_prehashed(hashed_storage_updates, state)
118        .map_err(OpConsensusError::L2WithdrawalsRootCalculationFail)?;
119
120    if header_storage_root != storage_root {
121        return Err(OpConsensusError::L2WithdrawalsRootMismatch {
122            header: header_storage_root,
123            exec_res: storage_root,
124        })
125    }
126
127    Ok(())
128}
129
130#[cfg(test)]
131mod test {
132    use super::*;
133    use alloc::sync::Arc;
134    use alloy_chains::Chain;
135    use alloy_consensus::Header;
136    use alloy_primitives::{keccak256, B256, U256};
137    use core::str::FromStr;
138    use reth_db_common::init::init_genesis;
139    use reth_optimism_chainspec::OpChainSpecBuilder;
140    use reth_optimism_node::OpNode;
141    use reth_optimism_primitives::ADDRESS_L2_TO_L1_MESSAGE_PASSER;
142    use reth_provider::{
143        providers::BlockchainProvider, test_utils::create_test_provider_factory_with_node_types,
144        StateWriter,
145    };
146    use reth_revm::db::BundleState;
147    use reth_storage_api::StateProviderFactory;
148    use reth_trie::{test_utils::storage_root_prehashed, HashedStorage};
149    use reth_trie_common::HashedPostState;
150
151    #[test]
152    fn l2tol1_message_passer_no_withdrawals() {
153        let hashed_address = keccak256(ADDRESS_L2_TO_L1_MESSAGE_PASSER);
154
155        // create account storage
156        let init_storage = HashedStorage::from_iter(
157            false,
158            [
159                "50000000000000000000000000000004253371b55351a08cb3267d4d265530b6",
160                "512428ed685fff57294d1a9cbb147b18ae5db9cf6ae4b312fa1946ba0561882e",
161                "51e6784c736ef8548f856909870b38e49ef7a4e3e77e5e945e0d5e6fcaa3037f",
162            ]
163            .into_iter()
164            .map(|str| (B256::from_str(str).unwrap(), U256::from(1))),
165        );
166        let mut state = HashedPostState::default();
167        state.storages.insert(hashed_address, init_storage.clone());
168
169        // init test db
170        // note: must be empty (default) chain spec to ensure storage is empty after init genesis,
171        // otherwise can't use `storage_root_prehashed` to determine storage root later
172        let provider_factory = create_test_provider_factory_with_node_types::<OpNode>(Arc::new(
173            OpChainSpecBuilder::default().chain(Chain::dev()).genesis(Default::default()).build(),
174        ));
175        let _ = init_genesis(&provider_factory).unwrap();
176
177        // write account storage to database
178        let provider_rw = provider_factory.provider_rw().unwrap();
179        provider_rw.write_hashed_state(&state.clone().into_sorted()).unwrap();
180        provider_rw.commit().unwrap();
181
182        // create block header with withdrawals root set to storage root of l2tol1-msg-passer
183        let header = Header {
184            withdrawals_root: Some(storage_root_prehashed(init_storage.storage)),
185            ..Default::default()
186        };
187
188        // create state provider factory
189        let state_provider_factory = BlockchainProvider::new(provider_factory).unwrap();
190
191        // validate block against existing state by passing empty state updates
192        verify_withdrawals_root(
193            &BundleState::default(),
194            state_provider_factory.latest().expect("load state"),
195            &header,
196        )
197        .unwrap();
198    }
199}