Skip to main content

reth_evm/
witness.rs

1use alloc::vec::Vec;
2use alloy_primitives::{keccak256, Bytes, B256};
3use alloy_rpc_types_debug::ExecutionWitness;
4use evm2::evm::CacheDB;
5use reth_execution_types::BundleSource;
6use reth_primitives_traits::Account as PrimitiveAccount;
7use reth_storage_api::{
8    AccountReader, HashedPostStateProvider, HeaderProvider, StateProofProvider,
9};
10use reth_storage_errors::provider::ProviderResult;
11use reth_trie_common::{bundle_state::BundleState, ExecutionWitnessMode, HashedPostState};
12
13/// Borrows finalized execution state for witness generation.
14#[derive(Debug, Clone)]
15pub struct ExecutionWitnessRecord<'a, DB> {
16    /// State after execution.
17    state: &'a CacheDB<DB>,
18    /// Additional hashed state to include in the witness.
19    additional_state: Option<HashedPostState>,
20    /// Code read from parent state, before execution output was committed.
21    canonical_codes: Option<Vec<Bytes>>,
22}
23
24impl<'a, DB> ExecutionWitnessRecord<'a, DB> {
25    /// Creates a new record from the state after execution.
26    pub const fn new(state: &'a CacheDB<DB>) -> Self {
27        Self { state, additional_state: None, canonical_codes: None }
28    }
29
30    /// Adds hashed state that should be included when generating the witness.
31    ///
32    /// State recorded during execution takes precedence over additional state for overlapping
33    /// accounts and storage slots.
34    pub fn with_additional_state(mut self, additional_state: HashedPostState) -> Self {
35        self.additional_state.get_or_insert_default().extend(additional_state);
36        self
37    }
38
39    /// Converts this record into a complete [`ExecutionWitness`] by generating state proofs and
40    /// fetching ancestor block headers.
41    ///
42    /// The `block_number` is the number of the block being witnessed. Ancestor headers are
43    /// included based on the lowest block number referenced by BLOCKHASH opcodes during
44    /// execution, or just the parent header if BLOCKHASH was not called.
45    pub fn into_execution_witness<SP, HP>(
46        self,
47        state_provider: &SP,
48        headers_provider: &HP,
49        block_number: u64,
50        mode: ExecutionWitnessMode,
51    ) -> ProviderResult<ExecutionWitness>
52    where
53        SP: StateProofProvider + HashedPostStateProvider + AccountReader + ?Sized,
54        HP: HeaderProvider + ?Sized,
55        HP::Header: alloy_rlp::Encodable,
56    {
57        let lowest_block_number =
58            self.state.cache.block_hashes.keys().map(|number| number.saturating_to::<u64>()).min();
59        let mut exec_witness = self.into_execution_witness_without_headers(state_provider, mode)?;
60
61        let smallest = lowest_block_number.unwrap_or_else(|| block_number.saturating_sub(1));
62        exec_witness.headers = headers_provider
63            .headers_range(smallest..block_number)?
64            .into_iter()
65            .map(|header| {
66                let mut buf = Vec::new();
67                alloy_rlp::Encodable::encode(&header, &mut buf);
68                buf.into()
69            })
70            .collect();
71
72        Ok(exec_witness)
73    }
74
75    /// Generates state proofs and codes without fetching ancestor headers.
76    ///
77    /// Callers witnessing non-canonical blocks can supply headers by following parent hashes.
78    pub fn into_execution_witness_without_headers<SP>(
79        mut self,
80        state_provider: &SP,
81        mode: ExecutionWitnessMode,
82    ) -> ProviderResult<ExecutionWitness>
83    where
84        SP: StateProofProvider + HashedPostStateProvider + AccountReader + ?Sized,
85    {
86        let codes = if mode.is_canonical() {
87            self.canonical_codes.take().unwrap_or_else(|| Self::capture_codes(self.state))
88        } else {
89            self.state
90                .cache
91                .contracts
92                .iter()
93                .filter(|(hash, _)| !hash.is_zero())
94                .map(|(_, code)| code.original_bytes())
95                .collect()
96        };
97
98        let mut wiped_state = reth_trie_common::bundle_state::BundleState::default();
99        for (address, storage) in &self.state.cache.storage {
100            if storage.wiped {
101                let info =
102                    self.state.cache.accounts.get(address).and_then(|info| info.as_ref()).cloned();
103                wiped_state.state.insert(
104                    *address,
105                    reth_trie_common::bundle_state::BundleAccount::new(
106                        // Original existence determines whether parent storage proofs are needed.
107                        state_provider.basic_account(address)?.map(Into::into),
108                        info.clone(),
109                        Default::default(),
110                        if info.is_some() {
111                            reth_trie_common::bundle_state::AccountStatus::DestroyedChanged
112                        } else {
113                            reth_trie_common::bundle_state::AccountStatus::Destroyed
114                        },
115                    ),
116                );
117            }
118        }
119        if !wiped_state.is_empty() {
120            self.additional_state
121                .get_or_insert_default()
122                .extend(state_provider.hashed_post_state(&wiped_state)?);
123        }
124        let (hashed_state, keys) = self.hashed_post_state();
125        let state = state_provider.witness(Default::default(), hashed_state, mode)?;
126        Ok(ExecutionWitness { state, codes, keys, ..Default::default() })
127    }
128
129    /// Captures canonical witness bytecode after execution, before committing its output.
130    ///
131    /// The cache must contain only parent-state reads, excluding code produced by execution.
132    pub fn capture_codes(state: &CacheDB<DB>) -> Vec<Bytes> {
133        let mut codes = state
134            .cache
135            .contracts
136            .iter()
137            .filter(|(hash, code)| !hash.is_zero() && !code.is_empty())
138            .map(|(_, code)| code.original_bytes())
139            .collect::<Vec<_>>();
140        codes.sort_unstable();
141        codes
142    }
143
144    /// Captures the parent-state bytecode read during execution, then commits the execution
145    /// `output` to `state`, and returns the captured codes for [`Self::with_canonical_codes`].
146    ///
147    /// The codes are captured before the commit, so code deployed by execution is excluded from
148    /// canonical witnesses.
149    pub fn commit_execution(state: &mut CacheDB<DB>, output: &BundleState) -> Vec<Bytes> {
150        let codes = Self::capture_codes(state);
151        state.commit_source(&BundleSource(output));
152        codes
153    }
154
155    /// Uses captured parent-state bytecode for canonical witnesses, retaining all code in legacy
156    /// mode.
157    pub fn with_canonical_codes(mut self, codes: Vec<Bytes>) -> Self {
158        self.canonical_codes = Some(codes);
159        self
160    }
161
162    fn hashed_post_state(self) -> (HashedPostState, Vec<Bytes>) {
163        let mut hashed_state = self.additional_state.unwrap_or_default();
164        let mut keys = Vec::new();
165        for (address, account) in &self.state.cache.accounts {
166            let hashed_address = keccak256(address);
167            hashed_state.accounts.insert(
168                hashed_address,
169                account.as_ref().map(|account| PrimitiveAccount {
170                    nonce: account.nonce,
171                    balance: account.balance,
172                    bytecode_hash: (!account.code_hash.is_zero() && !account.is_empty_code_hash())
173                        .then_some(account.code_hash),
174                    #[cfg(feature = "account-ext")]
175                    extension: reth_primitives_traits::AccountExtension::from_shared(
176                        account.extension.clone().into_shared(),
177                    ),
178                }),
179            );
180            if account.is_some() {
181                keys.push(address.to_vec().into());
182            }
183
184            if let Some(storage) = self.state.cache.storage.get(address) {
185                let hashed_storage = hashed_state.storages.entry(hashed_address).or_default();
186                for (slot, value) in &storage.slots {
187                    let slot = B256::from(*slot);
188                    hashed_storage.storage.insert(keccak256(slot), *value);
189                    keys.push(slot.into());
190                }
191            }
192        }
193        (hashed_state, keys)
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200    use alloy_primitives::{Address, U256};
201    use evm2::evm::{AccountInfo, EmptyDB};
202    use reth_trie_common::HashedStorage;
203
204    #[test]
205    fn destroyed_account_is_recorded_for_witness() {
206        let address = Address::with_last_byte(1);
207        let hashed_address = keccak256(address);
208
209        let mut state = CacheDB::<EmptyDB>::default();
210        state.cache.accounts.insert(address, None);
211        state.cache.storage.entry(address).or_default().wipe();
212
213        let (hashed_state, _) = ExecutionWitnessRecord::new(&state).hashed_post_state();
214        assert_eq!(hashed_state.accounts[&hashed_address], None);
215        assert!(hashed_state.storages[&hashed_address].storage.is_empty());
216    }
217
218    #[cfg(feature = "account-ext")]
219    #[test]
220    fn witness_preserves_account_extension() {
221        let address = Address::with_last_byte(1);
222        let mut state = CacheDB::<EmptyDB>::default();
223        state.insert_account_info(
224            &address,
225            AccountInfo {
226                extension: evm2::evm::AccountExtension::copy_from_slice(b"account extension"),
227                ..Default::default()
228            },
229        );
230        let (hashed, _) = ExecutionWitnessRecord::new(&state).hashed_post_state();
231        assert_eq!(
232            hashed.accounts[&keccak256(address)].as_ref().unwrap().extension.as_ref(),
233            b"account extension"
234        );
235    }
236
237    #[test]
238    fn additional_state_is_merged_with_executed_state() {
239        let address = Address::with_last_byte(1);
240        let hashed_address = keccak256(address);
241        let slot = U256::from(1);
242        let additional_slot = B256::with_last_byte(2);
243
244        let mut state = CacheDB::<EmptyDB>::default();
245        state.insert_account_info(&address, AccountInfo::default());
246        state.insert_account_storage(&address, &slot, &U256::from(2));
247
248        let additional_state = HashedPostState::default().with_storages([(
249            hashed_address,
250            HashedStorage::from_iter([
251                (keccak256(B256::from(slot)), U256::from(1)),
252                (additional_slot, U256::from(3)),
253            ]),
254        )]);
255
256        let (hashed_state, _) = ExecutionWitnessRecord::new(&state)
257            .with_additional_state(additional_state)
258            .hashed_post_state();
259        let storage = &hashed_state.storages[&hashed_address].storage;
260        assert_eq!(storage[&keccak256(B256::from(slot))], U256::from(2));
261        assert_eq!(storage[&additional_slot], U256::from(3));
262    }
263}