Skip to main content

reth_stages/stages/execution/
slot_preimages.rs

1use alloy_primitives::{keccak256, map::HashSet, B256};
2use eyre::Context;
3use rayon::slice::ParallelSliceMut;
4use reth_db::tables;
5use reth_db_api::{
6    cursor::{DbCursorRO, DbDupCursorRO},
7    transaction::DbTx,
8};
9use reth_libmdbx::{
10    DatabaseFlags, Environment, EnvironmentFlags, Geometry, Mode, SyncMode, WriteFlags, RO,
11};
12use reth_provider::{DBProvider, ExecutionOutcome};
13use reth_revm::revm::database::states::RevertToSlot;
14use reth_stages_api::StageError;
15use std::path::Path;
16use tracing::trace;
17
18/// Separate MDBX environment for storing `keccak256(slot) → slot` preimage mappings.
19///
20/// Used during [`super::ExecutionStage`] for pre-Cancun selfdestruct handling where
21/// the original storage slot keys must be recovered from their hashed representation.
22///
23/// Also usable by downstream state-dump importers (e.g. `op-reth init-state`) to seed
24/// the database when state is imported from a snapshot rather than executed block-by-block.
25///
26/// The database is append-only and not unwound — duplicate inserts are silently skipped.
27/// After Cancun (where `SELFDESTRUCT` no longer destroys storage) the database can be pruned.
28#[derive(Debug)]
29pub struct SlotPreimages {
30    env: Environment,
31}
32
33impl SlotPreimages {
34    /// Opens (or creates) the slot-preimage MDBX environment at the given directory `path`.
35    ///
36    /// Uses subdir mode (`no_sub_dir = false`), so MDBX creates `mdbx.dat` / `mdbx.lck`
37    /// under the directory (e.g. `db/preimage/mdbx.dat`).
38    pub fn open(path: &Path) -> eyre::Result<Self> {
39        const GIGABYTE: usize = 1024 * 1024 * 1024;
40        const TERABYTE: usize = GIGABYTE * 1024;
41
42        let mut builder = Environment::builder();
43        builder.set_max_dbs(1);
44        let os_page_size = page_size::get().clamp(4096, 0x10000);
45        builder.set_geometry(Geometry {
46            size: Some(0..(8 * TERABYTE)),
47            growth_step: Some(4 * GIGABYTE as isize),
48            shrink_threshold: Some(0),
49            page_size: Some(reth_libmdbx::PageSize::Set(os_page_size)),
50        });
51        builder.write_map();
52        builder.set_flags(EnvironmentFlags {
53            no_sub_dir: false,
54            no_rdahead: true,
55            mode: Mode::ReadWrite { sync_mode: SyncMode::Durable },
56            ..Default::default()
57        });
58
59        let env = builder.open(path).wrap_err_with(|| {
60            format!("failed to open slot-preimage MDBX env at {}", path.display())
61        })?;
62
63        // Ensure the unnamed default DB exists.
64        {
65            let tx = env.begin_rw_txn()?;
66            let _db = tx.create_db(None, DatabaseFlags::empty())?;
67            tx.commit()?;
68        }
69
70        trace!(target: "stages::slot_preimages", ?path, "Opened slot-preimage store");
71
72        Ok(Self { env })
73    }
74
75    /// Batch-insert `hashed_slot → plain_slot` preimage entries.
76    ///
77    /// Entries should be pre-sorted by key for optimal insert performance.
78    /// Unsorted entries are still inserted correctly but will be slower due to
79    /// loss of btree cursor locality.
80    /// Existing keys are silently skipped.
81    pub fn insert_preimages(&self, entries: &[(B256, B256)]) -> eyre::Result<()> {
82        let tx = self.env.begin_rw_txn()?;
83        let db = tx.open_db(None)?;
84        let mut cursor = tx.cursor(db.dbi())?;
85
86        for (hashed_slot, plain_slot) in entries {
87            if cursor.set_key::<[u8; 32], [u8; 32]>(hashed_slot.as_slice())?.is_some() {
88                continue;
89            }
90            cursor.put(hashed_slot.as_slice(), plain_slot.as_slice(), WriteFlags::empty())?;
91        }
92
93        tx.commit()?;
94
95        trace!(target: "stages::slot_preimages", count = entries.len(), "Inserted slot preimages");
96
97        Ok(())
98    }
99
100    /// Opens a read-only transaction for batch lookups.
101    ///
102    /// Reuse the returned [`SlotPreimagesReader`] for multiple `get` calls to avoid
103    /// the overhead of opening a new RO transaction per lookup.
104    pub fn reader(&self) -> eyre::Result<SlotPreimagesReader> {
105        let tx = self.env.begin_ro_txn()?;
106        let dbi = tx.open_db(None)?.dbi();
107        Ok(SlotPreimagesReader { tx, dbi })
108    }
109}
110
111/// Read-only handle for batch slot-preimage lookups within a single MDBX transaction.
112#[derive(Debug)]
113pub struct SlotPreimagesReader {
114    tx: reth_libmdbx::Transaction<RO>,
115    dbi: reth_libmdbx::ffi::MDBX_dbi,
116}
117
118impl SlotPreimagesReader {
119    /// Point-lookup of a slot preimage by its keccak256 hash.
120    pub fn get(&self, hashed_slot: &B256) -> eyre::Result<Option<B256>> {
121        let result: Option<[u8; 32]> = self.tx.get(self.dbi, hashed_slot.as_ref())?;
122        Ok(result.map(B256::from))
123    }
124}
125
126/// Collects `keccak256(slot) → slot` preimage entries from the bundle state and stores
127/// them in the auxiliary preimage database, then rewrites wipe reverts for self-destructed
128/// accounts to use plain slot keys instead of relying on the hashed-storage DB walk.
129///
130/// This eliminates the need for the changeset writer to read from `HashedStorages` during
131/// storage wipes, keeping all changeset keys in plain format.
132pub(super) fn inject_plain_wipe_slots<P: DBProvider, R>(
133    slot_preimages_path: &Path,
134    provider: &P,
135    state: &mut ExecutionOutcome<R>,
136) -> Result<(), StageError> {
137    // Collect preimage entries from bundle state and reverts.
138    // StorageKey in revm is U256, representing a plain EVM slot index.
139    let mut preimage_entries = Vec::new();
140    let mut seen_hashes = HashSet::new();
141    for account in state.bundle.state().values() {
142        for &slot_key in account.storage.keys() {
143            let plain = B256::from(slot_key.to_be_bytes());
144            let hashed = keccak256(plain);
145            if seen_hashes.insert(hashed) {
146                preimage_entries.push((hashed, plain));
147            }
148        }
149    }
150    for block_reverts in state.bundle.reverts.iter() {
151        for (_, revert) in block_reverts {
152            for &slot_key in revert.storage.keys() {
153                let plain = B256::from(slot_key.to_be_bytes());
154                let hashed = keccak256(plain);
155                if seen_hashes.insert(hashed) {
156                    preimage_entries.push((hashed, plain));
157                }
158            }
159        }
160    }
161
162    // Pre-sort entries by hash key for optimal MDBX insert performance.
163    preimage_entries.par_sort_unstable_by_key(|(hash, _)| *hash);
164
165    // Lazily open the preimage store and insert entries.
166    let preimages = SlotPreimages::open(slot_preimages_path).map_err(fatal)?;
167
168    if !preimage_entries.is_empty() {
169        preimages.insert_preimages(&preimage_entries).map_err(fatal)?;
170    }
171
172    // Find all wipe reverts (self-destructed accounts) and inject plain slot keys.
173
174    // Open a single RO transaction for all preimage lookups in this batch.
175    let reader = preimages.reader().map_err(fatal)?;
176
177    for block_reverts in state.bundle.reverts.iter_mut() {
178        for (address, revert) in block_reverts.iter_mut() {
179            if !revert.wipe_storage {
180                continue;
181            }
182
183            // Walk all hashed storage slots for this account in the DB and look up
184            // their plain-key preimages.
185            let addr = *address;
186            let hashed_address = keccak256(addr);
187            let mut cursor = provider.tx_ref().cursor_dup_read::<tables::HashedStorages>()?;
188
189            if let Some((_, entry)) = cursor.seek_exact(hashed_address)? {
190                inject_preimage_entry(&reader, revert, addr, entry.key, entry.value)?;
191                while let Some(entry) = cursor.next_dup_val()? {
192                    inject_preimage_entry(&reader, revert, addr, entry.key, entry.value)?;
193                }
194            }
195        }
196    }
197
198    Ok(())
199}
200
201/// Looks up the plain-key preimage for a single hashed storage slot and inserts it
202/// into the account revert if not already present.
203fn inject_preimage_entry(
204    reader: &SlotPreimagesReader,
205    revert: &mut reth_revm::revm::database::AccountRevert,
206    address: alloy_primitives::Address,
207    hashed_slot: B256,
208    value: alloy_primitives::U256,
209) -> Result<(), StageError> {
210    let plain_slot = reader.get(&hashed_slot).map_err(fatal)?.ok_or_else(|| {
211        fatal(eyre::eyre!("missing slot preimage for {hashed_slot:?} (addr={address:?})"))
212    })?;
213
214    // Convert B256 plain slot to U256 StorageKey for the revert map.
215    let plain_key = alloy_primitives::U256::from_be_bytes(plain_slot.0);
216    // When a contract is selfdestructed and then re-created at the same address via
217    // CREATE2 in the same block, revm treats the new contract as fresh and never reads
218    // the slot's original DB value. Slots touched by the new contract are marked as
219    // `Destroyed` instead of `Some(previous_value)`. We must overwrite these with the
220    // actual DB value here, otherwise `to_previous_value()` resolves them to zero.
221    revert
222        .storage
223        .entry(plain_key)
224        .and_modify(|slot| {
225            if matches!(slot, RevertToSlot::Destroyed) {
226                *slot = RevertToSlot::Some(value);
227            }
228        })
229        .or_insert(RevertToSlot::Some(value));
230    Ok(())
231}
232
233#[inline]
234fn fatal<E>(err: E) -> StageError
235where
236    E: Into<Box<dyn std::error::Error + Send + Sync>>,
237{
238    StageError::Fatal(err.into())
239}