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}