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