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