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            mode: Mode::ReadWrite { sync_mode: SyncMode::Durable },
52            ..Default::default()
53        });
54
55        let env = builder.open(path).wrap_err_with(|| {
56            format!("failed to open slot-preimage MDBX env at {}", path.display())
57        })?;
58
59        // Ensure the unnamed default DB exists.
60        {
61            let tx = env.begin_rw_txn()?;
62            let _db = tx.create_db(None, DatabaseFlags::empty())?;
63            tx.commit()?;
64        }
65
66        trace!(target: "stages::slot_preimages", ?path, "Opened slot-preimage store");
67
68        Ok(Self { env })
69    }
70
71    /// Batch-insert `hashed_slot → plain_slot` preimage entries.
72    ///
73    /// Entries must be pre-sorted by key for optimal insert performance.
74    /// Existing keys are skipped after cursor lookup.
75    fn insert_preimages(&self, entries: &[(B256, B256)]) -> eyre::Result<()> {
76        let tx = self.env.begin_rw_txn()?;
77        let db = tx.open_db(None)?;
78        let mut cursor = tx.cursor(db.dbi())?;
79
80        for (hashed_slot, plain_slot) in entries {
81            if cursor.set_key::<[u8; 32], [u8; 32]>(hashed_slot.as_slice())?.is_some() {
82                continue;
83            }
84            cursor.put(hashed_slot.as_slice(), plain_slot.as_slice(), WriteFlags::empty())?;
85        }
86
87        tx.commit()?;
88
89        trace!(target: "stages::slot_preimages", count = entries.len(), "Inserted slot preimages");
90
91        Ok(())
92    }
93
94    /// Opens a read-only transaction for batch lookups.
95    ///
96    /// Reuse the returned [`SlotPreimagesReader`] for multiple `get` calls to avoid
97    /// the overhead of opening a new RO transaction per lookup.
98    fn reader(&self) -> eyre::Result<SlotPreimagesReader> {
99        let tx = self.env.begin_ro_txn()?;
100        let dbi = tx.open_db(None)?.dbi();
101        Ok(SlotPreimagesReader { tx, dbi })
102    }
103}
104
105/// Read-only handle for batch slot-preimage lookups within a single MDBX transaction.
106struct SlotPreimagesReader {
107    tx: reth_libmdbx::Transaction<RO>,
108    dbi: reth_libmdbx::ffi::MDBX_dbi,
109}
110
111impl SlotPreimagesReader {
112    /// Point-lookup of a slot preimage by its keccak256 hash.
113    fn get(&self, hashed_slot: &B256) -> eyre::Result<Option<B256>> {
114        let result: Option<[u8; 32]> = self.tx.get(self.dbi, hashed_slot.as_ref())?;
115        Ok(result.map(B256::from))
116    }
117}
118
119/// Collects `keccak256(slot) → slot` preimage entries from the bundle state and stores
120/// them in the auxiliary preimage database, then rewrites wipe reverts for self-destructed
121/// accounts to use plain slot keys instead of relying on the hashed-storage DB walk.
122///
123/// This eliminates the need for the changeset writer to read from `HashedStorages` during
124/// storage wipes, keeping all changeset keys in plain format.
125pub(super) fn inject_plain_wipe_slots<P: DBProvider, R>(
126    slot_preimages_path: &Path,
127    provider: &P,
128    state: &mut ExecutionOutcome<R>,
129) -> Result<(), StageError> {
130    // Collect preimage entries from bundle state and reverts.
131    // StorageKey in revm is U256, representing a plain EVM slot index.
132    let mut preimage_entries = Vec::new();
133    let mut seen_hashes = HashSet::new();
134    for account in state.bundle.state().values() {
135        for &slot_key in account.storage.keys() {
136            let plain = B256::from(slot_key.to_be_bytes());
137            let hashed = keccak256(plain);
138            if seen_hashes.insert(hashed) {
139                preimage_entries.push((hashed, plain));
140            }
141        }
142    }
143    for block_reverts in state.bundle.reverts.iter() {
144        for (_, revert) in block_reverts {
145            for &slot_key in revert.storage.keys() {
146                let plain = B256::from(slot_key.to_be_bytes());
147                let hashed = keccak256(plain);
148                if seen_hashes.insert(hashed) {
149                    preimage_entries.push((hashed, plain));
150                }
151            }
152        }
153    }
154
155    // Pre-sort entries by hash key for optimal MDBX insert performance.
156    preimage_entries.par_sort_unstable_by_key(|(hash, _)| *hash);
157
158    // Lazily open the preimage store and insert entries.
159    let preimages = SlotPreimages::open(slot_preimages_path).map_err(fatal)?;
160
161    if !preimage_entries.is_empty() {
162        preimages.insert_preimages(&preimage_entries).map_err(fatal)?;
163    }
164
165    // Find all wipe reverts (self-destructed accounts) and inject plain slot keys.
166
167    // Open a single RO transaction for all preimage lookups in this batch.
168    let reader = preimages.reader().map_err(fatal)?;
169
170    for block_reverts in state.bundle.reverts.iter_mut() {
171        for (address, revert) in block_reverts.iter_mut() {
172            if !revert.wipe_storage {
173                continue;
174            }
175
176            // Walk all hashed storage slots for this account in the DB and look up
177            // their plain-key preimages.
178            let addr = *address;
179            let hashed_address = keccak256(addr);
180            let mut cursor = provider.tx_ref().cursor_dup_read::<tables::HashedStorages>()?;
181
182            if let Some((_, entry)) = cursor.seek_exact(hashed_address)? {
183                inject_preimage_entry(&reader, revert, addr, entry.key, entry.value)?;
184                while let Some(entry) = cursor.next_dup_val()? {
185                    inject_preimage_entry(&reader, revert, addr, entry.key, entry.value)?;
186                }
187            }
188        }
189    }
190
191    Ok(())
192}
193
194/// Looks up the plain-key preimage for a single hashed storage slot and inserts it
195/// into the account revert if not already present.
196fn inject_preimage_entry(
197    reader: &SlotPreimagesReader,
198    revert: &mut reth_revm::revm::database::AccountRevert,
199    address: alloy_primitives::Address,
200    hashed_slot: B256,
201    value: alloy_primitives::U256,
202) -> Result<(), StageError> {
203    let plain_slot = reader.get(&hashed_slot).map_err(fatal)?.ok_or_else(|| {
204        fatal(eyre::eyre!("missing slot preimage for {hashed_slot:?} (addr={address:?})"))
205    })?;
206
207    // Convert B256 plain slot to U256 StorageKey for the revert map.
208    let plain_key = alloy_primitives::U256::from_be_bytes(plain_slot.0);
209    revert.storage.entry(plain_key).or_insert(RevertToSlot::Some(value));
210    Ok(())
211}
212
213#[inline]
214fn fatal<E>(err: E) -> StageError
215where
216    E: Into<Box<dyn std::error::Error + Send + Sync>>,
217{
218    StageError::Fatal(err.into())
219}