Skip to main content

reth_revm/
witness.rs

1use alloc::vec::Vec;
2use alloy_primitives::{keccak256, Bytes, B256};
3use reth_trie::{ExecutionWitnessMode, HashedPostState, HashedStorage};
4use revm::database::State;
5
6/// Borrows finalized execution state for witness generation.
7#[derive(Debug, Clone, Copy)]
8pub struct ExecutionWitnessRecord<'a, DB> {
9    /// State after execution.
10    state: &'a State<DB>,
11}
12
13impl<'a, DB> ExecutionWitnessRecord<'a, DB> {
14    /// Creates a new record from the state after execution.
15    pub const fn new(state: &'a State<DB>) -> Self {
16        Self { state }
17    }
18
19    /// Converts this record into a complete [`alloy_rpc_types_debug::ExecutionWitness`] by
20    /// generating state proofs and fetching ancestor block headers.
21    ///
22    /// The `block_number` is the number of the block being witnessed. Ancestor headers are
23    /// included based on the lowest block number referenced by BLOCKHASH opcodes during
24    /// execution, or just the parent header if BLOCKHASH was not called.
25    #[cfg(feature = "witness")]
26    pub fn into_execution_witness<SP, HP>(
27        self,
28        state_provider: &SP,
29        headers_provider: &HP,
30        block_number: u64,
31        mode: ExecutionWitnessMode,
32    ) -> reth_storage_errors::provider::ProviderResult<alloy_rpc_types_debug::ExecutionWitness>
33    where
34        SP: reth_storage_api::HashedPostStateProvider
35            + reth_storage_api::StateProofProvider
36            + ?Sized,
37        HP: reth_storage_api::HeaderProvider + ?Sized,
38        HP::Header: alloy_rlp::Encodable,
39    {
40        let codes = match mode {
41            ExecutionWitnessMode::Legacy => self
42                .state
43                .cache
44                .contracts
45                .values()
46                .map(|code| code.original_bytes())
47                .chain(
48                    // cache state does not have all the contracts, especially when
49                    // a contract is created within the block
50                    // the contract only exists in bundle state, therefore we need
51                    // to include them as well
52                    self.state.bundle_state.contracts.values().map(|code| code.original_bytes()),
53                )
54                .collect(),
55            ExecutionWitnessMode::Canonical => {
56                let mut codes: Vec<_> = self
57                    .state
58                    .cache
59                    .contracts
60                    .values()
61                    .map(|c| c.original_bytes())
62                    .filter(|code| !code.is_empty())
63                    .collect();
64                codes.sort_unstable();
65                codes
66            }
67        };
68
69        let (hashed_state, keys) = self.hashed_post_state(state_provider)?;
70
71        let state = state_provider.witness(Default::default(), hashed_state, mode)?;
72        let mut exec_witness =
73            alloy_rpc_types_debug::ExecutionWitness { state, codes, keys, ..Default::default() };
74
75        let lowest_block_number =
76            self.state.block_hashes.lowest().map(|(block_number, _)| block_number);
77        let smallest = lowest_block_number.unwrap_or_else(|| block_number.saturating_sub(1));
78        let range = smallest..block_number;
79
80        exec_witness.headers = headers_provider
81            .headers_range(range)?
82            .into_iter()
83            .map(|header| {
84                let mut buf = Vec::new();
85                alloy_rlp::Encodable::encode(&header, &mut buf);
86                buf.into()
87            })
88            .collect();
89
90        Ok(exec_witness)
91    }
92
93    #[cfg(feature = "witness")]
94    fn hashed_post_state<SP>(
95        &self,
96        state_provider: &SP,
97    ) -> reth_storage_errors::provider::ProviderResult<(HashedPostState, Vec<Bytes>)>
98    where
99        SP: reth_storage_api::HashedPostStateProvider + ?Sized,
100    {
101        let mut hashed_state = HashedPostState::default();
102        let mut keys = Vec::new();
103        for (address, account) in &self.state.cache.accounts {
104            let hashed_address = keccak256(address);
105            hashed_state
106                .accounts
107                .insert(hashed_address, account.account.as_ref().map(|a| (&a.info).into()));
108
109            let storage = hashed_state
110                .storages
111                .entry(hashed_address)
112                .or_insert_with(|| HashedStorage::new(false));
113
114            if let Some(account) = &account.account {
115                keys.push(address.to_vec().into());
116
117                for (slot, value) in &account.storage {
118                    let slot = B256::from(*slot);
119                    let hashed_slot = keccak256(slot);
120                    storage.storage.insert(hashed_slot, *value);
121
122                    keys.push(slot.into());
123                }
124            }
125        }
126
127        // The execution cache does not contain untouched slots of a destroyed account. The
128        // provider expands them into explicit zero writes from the parent state; extending it last
129        // also ensures the bundle's final values override those collected from the cache.
130        hashed_state.extend(state_provider.hashed_post_state(&self.state.bundle_state)?);
131        Ok((hashed_state, keys))
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138    use alloy_primitives::{Address, U256};
139    use reth_storage_api::HashedPostStateProvider;
140    use reth_storage_errors::provider::ProviderResult;
141    use revm::{
142        database::{states::CacheAccount, AccountStatus, BundleAccount, EmptyDB},
143        state::AccountInfo,
144    };
145
146    #[derive(Debug)]
147    struct ExpandedStateProvider(HashedPostState);
148
149    impl HashedPostStateProvider for ExpandedStateProvider {
150        fn hashed_post_state(
151            &self,
152            bundle_state: &revm::database::BundleState,
153        ) -> ProviderResult<HashedPostState> {
154            assert!(bundle_state.state.values().any(BundleAccount::was_destroyed));
155            Ok(self.0.clone())
156        }
157    }
158
159    #[test]
160    fn destroyed_account_storage_is_zero_expanded_without_wipe() {
161        let address = Address::with_last_byte(1);
162        let hashed_address = keccak256(address);
163        let hashed_slot = B256::with_last_byte(2);
164
165        let mut state = State::builder().with_database(EmptyDB::default()).build();
166        state.cache.accounts.insert(address, CacheAccount::new_destroyed());
167        state.bundle_state.state.insert(
168            address,
169            BundleAccount::new(
170                Some(AccountInfo::default()),
171                None,
172                Default::default(),
173                AccountStatus::Destroyed,
174            ),
175        );
176
177        let provider = ExpandedStateProvider(
178            HashedPostState::default().with_accounts([(hashed_address, None)]).with_storages([(
179                hashed_address,
180                HashedStorage::from_iter([(hashed_slot, U256::ZERO)]),
181            )]),
182        );
183
184        let (hashed_state, _) =
185            ExecutionWitnessRecord::new(&state).hashed_post_state(&provider).unwrap();
186        let storage = hashed_state.storages.get(&hashed_address).unwrap();
187        assert!(!storage.wiped);
188        assert_eq!(storage.storage.get(&hashed_slot), Some(&U256::ZERO));
189    }
190}