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