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