Skip to main content

reth_provider/providers/state/
historical.rs

1use alloy_primitives::BlockNumber;
2use reth_db_api::{cursor::DbCursorRO, table::Table, BlockNumberList};
3use reth_storage_api::HistoryInfo as ReaderHistoryInfo;
4use reth_storage_errors::provider::ProviderResult;
5
6/// Result of a history lookup for an account or storage slot.
7///
8/// Indicates where to find the historical value for a given key at a specific block.
9#[derive(Debug, Eq, PartialEq)]
10pub enum HistoryInfo {
11    /// The key is written to, but only after our block (not yet written at the target block). Or
12    /// it has never been written.
13    NotYetWritten,
14    /// The chunk contains an entry for a write after our block at the given block number.
15    /// The value should be looked up in the changeset at this block.
16    InChangeset(u64),
17    /// The chunk does not contain an entry for a write after our block. This can only
18    /// happen if this is the last chunk, so we need to look in the plain state.
19    InPlainState,
20    /// The key may have been written, but due to pruning we may not have changesets and
21    /// history, so we need to make a plain state lookup.
22    MaybeInPlainState,
23}
24
25impl HistoryInfo {
26    /// Determines where to find the historical value based on computed shard lookup results.
27    ///
28    /// This is a pure function shared by both MDBX and `RocksDB` backends.
29    ///
30    /// # Arguments
31    /// * `found_block` - The block number from the shard lookup
32    /// * `is_before_first_write` - True if the target block is before the first write to this key.
33    ///   This should be computed as: `rank == 0 && found_block != Some(block_number) &&
34    ///   !has_previous_shard` where `has_previous_shard` comes from a lazy `cursor.prev()` check.
35    /// * `lowest_available` - Lowest block where history is available (pruning boundary)
36    pub const fn from_lookup(
37        found_block: Option<u64>,
38        is_before_first_write: bool,
39        lowest_available: Option<BlockNumber>,
40    ) -> Self {
41        if is_before_first_write {
42            if let (Some(_), Some(block_number)) = (lowest_available, found_block) {
43                // The key may have been written, but due to pruning we may not have changesets
44                // and history, so we need to make a changeset lookup.
45                return Self::InChangeset(block_number)
46            }
47            // The key is written to, but only after our block.
48            return Self::NotYetWritten
49        }
50
51        if let Some(block_number) = found_block {
52            // The chunk contains an entry for a write after our block, return it.
53            Self::InChangeset(block_number)
54        } else {
55            // The chunk does not contain an entry for a write after our block. This can only
56            // happen if this is the last chunk and so we need to look in the plain state.
57            Self::InPlainState
58        }
59    }
60}
61
62impl From<ReaderHistoryInfo> for HistoryInfo {
63    fn from(info: ReaderHistoryInfo) -> Self {
64        match info {
65            ReaderHistoryInfo::NotYetWritten => Self::NotYetWritten,
66            ReaderHistoryInfo::InChangeset(block_number) => Self::InChangeset(block_number),
67            ReaderHistoryInfo::InPlainState => Self::InPlainState,
68            ReaderHistoryInfo::MaybeInPlainState => Self::MaybeInPlainState,
69        }
70    }
71}
72
73impl From<HistoryInfo> for ReaderHistoryInfo {
74    fn from(info: HistoryInfo) -> Self {
75        match info {
76            HistoryInfo::NotYetWritten => Self::NotYetWritten,
77            HistoryInfo::InChangeset(block_number) => Self::InChangeset(block_number),
78            HistoryInfo::InPlainState => Self::InPlainState,
79            HistoryInfo::MaybeInPlainState => Self::MaybeInPlainState,
80        }
81    }
82}
83
84/// Computes the rank and finds the next modification block in a history shard.
85///
86/// Given a `block_number`, this function returns:
87/// - `rank`: The number of entries strictly before `block_number` in the shard
88/// - `found_block`: The block number at position `rank` (i.e., the first block >= `block_number`
89///   where a modification occurred), or `None` if `rank` is out of bounds
90///
91/// The rank is adjusted when `block_number` exactly matches an entry in the shard,
92/// so that `found_block` always returns the modification at or after the target.
93///
94/// This logic is shared between MDBX cursor-based lookups and `RocksDB` iterator lookups.
95#[inline]
96pub fn compute_history_rank(
97    chunk: &BlockNumberList,
98    block_number: BlockNumber,
99) -> (u64, Option<u64>) {
100    let mut rank = chunk.rank(block_number);
101    // `rank(block_number)` returns count of entries <= block_number.
102    // We want the first entry >= block_number, so if block_number is in the shard,
103    // we need to step back one position to point at it (not past it).
104    if rank.checked_sub(1).and_then(|r| chunk.select(r)) == Some(block_number) {
105        rank -= 1;
106    }
107    (rank, chunk.select(rank))
108}
109
110/// Checks if a previous shard lookup is needed to determine if we're before the first write.
111///
112/// Returns `true` when `rank == 0` (first entry in shard) and the found block doesn't match
113/// the target block number. In this case, we need to check if there's a previous shard.
114#[inline]
115pub fn needs_prev_shard_check(
116    rank: u64,
117    found_block: Option<u64>,
118    block_number: BlockNumber,
119) -> bool {
120    rank == 0 && found_block != Some(block_number)
121}
122
123/// Generic history lookup for sharded history tables.
124///
125/// Seeks to the shard containing `block_number`, verifies the key via `key_filter`,
126/// and checks previous shard to detect if we're before the first write.
127pub fn history_info<T, K, C>(
128    cursor: &mut C,
129    key: K,
130    block_number: BlockNumber,
131    key_filter: impl Fn(&K) -> bool,
132    lowest_available_block_number: Option<BlockNumber>,
133) -> ProviderResult<HistoryInfo>
134where
135    T: Table<Key = K, Value = BlockNumberList>,
136    C: DbCursorRO<T>,
137{
138    // Lookup the history chunk in the history index. If the key does not appear in the
139    // index, the first chunk for the next key will be returned so we filter out chunks that
140    // have a different key.
141    if let Some(chunk) = cursor.seek(key)?.filter(|(k, _)| key_filter(k)).map(|x| x.1) {
142        let (rank, found_block) = compute_history_rank(&chunk, block_number);
143
144        // If our block is before the first entry in the index chunk and this first entry
145        // doesn't equal to our block, it might be before the first write ever. To check, we
146        // look at the previous entry and check if the key is the same.
147        // This check is worth it, the `cursor.prev()` check is rarely triggered (the if will
148        // short-circuit) and when it passes we save a full seek into the changeset/plain state
149        // table.
150        let is_before_first_write = needs_prev_shard_check(rank, found_block, block_number) &&
151            !cursor.prev()?.is_some_and(|(k, _)| key_filter(&k));
152
153        Ok(HistoryInfo::from_lookup(
154            found_block,
155            is_before_first_write,
156            lowest_available_block_number,
157        ))
158    } else if lowest_available_block_number.is_some() {
159        // The key may have been written, but due to pruning we may not have changesets and
160        // history, so we need to make a plain state lookup.
161        Ok(HistoryInfo::MaybeInPlainState)
162    } else {
163        // The key has not been written to at all.
164        Ok(HistoryInfo::NotYetWritten)
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::{needs_prev_shard_check, HistoryInfo};
171
172    #[test]
173    fn test_history_info_from_lookup() {
174        // Before first write, no pruning → not yet written
175        assert_eq!(HistoryInfo::from_lookup(Some(10), true, None), HistoryInfo::NotYetWritten);
176        assert_eq!(HistoryInfo::from_lookup(None, true, None), HistoryInfo::NotYetWritten);
177
178        // Before first write WITH pruning → check changeset (pruning may have removed history)
179        assert_eq!(HistoryInfo::from_lookup(Some(10), true, Some(5)), HistoryInfo::InChangeset(10));
180        assert_eq!(HistoryInfo::from_lookup(None, true, Some(5)), HistoryInfo::NotYetWritten);
181
182        // Not before first write → check changeset or plain state
183        assert_eq!(HistoryInfo::from_lookup(Some(10), false, None), HistoryInfo::InChangeset(10));
184        assert_eq!(HistoryInfo::from_lookup(None, false, None), HistoryInfo::InPlainState);
185    }
186
187    #[test]
188    fn test_needs_prev_shard_check() {
189        // Only needs check when rank == 0 and found_block != block_number
190        assert!(needs_prev_shard_check(0, Some(10), 5));
191        assert!(needs_prev_shard_check(0, None, 5));
192        assert!(!needs_prev_shard_check(0, Some(5), 5)); // found_block == block_number
193        assert!(!needs_prev_shard_check(1, Some(10), 5)); // rank > 0
194    }
195}