signet_hot/db/read.rs
1use crate::{db::HistoryError, model::HotKvRead, tables};
2use alloy::primitives::{Address, B256, U256};
3use signet_storage_types::{Account, BlockNumberList, SealedHeader, ShardedKey};
4use trevm::revm::bytecode::Bytecode;
5
6/// Trait for database read operations on standard hot tables.
7///
8/// This is a high-level trait that provides convenient methods for reading
9/// common data types from predefined hot storage tables. It builds upon the
10/// lower-level [`HotKvRead`] trait, which provides raw key-value access.
11///
12/// Users should prefer this trait unless customizations are needed to the
13/// table set.
14pub trait HotDbRead: HotKvRead + super::sealed::Sealed {
15 /// Read a block header by its number.
16 fn get_header(&self, number: u64) -> Result<Option<SealedHeader>, Self::Error> {
17 self.get::<tables::Headers>(&number)
18 }
19
20 /// Read a block number by its hash.
21 fn get_header_number(&self, hash: &B256) -> Result<Option<u64>, Self::Error> {
22 self.get::<tables::HeaderNumbers>(hash)
23 }
24
25 /// Read contract Bytecode by its hash.
26 fn get_bytecode(&self, code_hash: &B256) -> Result<Option<Bytecode>, Self::Error> {
27 self.get::<tables::Bytecodes>(code_hash)
28 }
29
30 /// Read an account by its address.
31 fn get_account(&self, address: &Address) -> Result<Option<Account>, Self::Error> {
32 self.get::<tables::PlainAccountState>(address)
33 }
34
35 /// Read a storage slot by its address and key.
36 fn get_storage(&self, address: &Address, key: &U256) -> Result<Option<U256>, Self::Error> {
37 self.get_dual::<tables::PlainStorageState>(address, key)
38 }
39
40 /// Read a block header by its hash.
41 fn header_by_hash(&self, hash: &B256) -> Result<Option<SealedHeader>, Self::Error> {
42 let Some(number) = self.get_header_number(hash)? else {
43 return Ok(None);
44 };
45 self.get_header(number)
46 }
47}
48
49impl<T> HotDbRead for T where T: HotKvRead {}
50
51/// Trait for history read operations.
52///
53/// These tables maintain historical information about accounts and storage
54/// changes, and their contents can be used to reconstruct past states or
55/// roll back changes.
56///
57/// This is a high-level trait that provides convenient methods for reading
58/// common data types from predefined hot storage history tables. It builds
59/// upon the lower-level [`HotDbRead`] trait, which provides raw key-value
60/// access.
61///
62/// Users should prefer this trait unless customizations are needed to the
63/// table set.
64pub trait HistoryRead: HotDbRead {
65 /// Get the list of block numbers where an account was touched.
66 /// Get the list of block numbers where an account was touched.
67 fn get_account_history(
68 &self,
69 address: &Address,
70 latest_height: u64,
71 ) -> Result<Option<BlockNumberList>, Self::Error> {
72 self.get_dual::<tables::AccountsHistory>(address, &latest_height)
73 }
74
75 /// Get the last (highest) account history entry for an address.
76 fn last_account_history(
77 &self,
78 address: Address,
79 ) -> Result<Option<(u64, BlockNumberList)>, Self::Error> {
80 let mut cursor = self.traverse_dual::<tables::AccountsHistory>()?;
81
82 // Move the cursor to the last entry for the given address
83 let Some(res) = cursor.last_of_k1(&address)? else {
84 return Ok(None);
85 };
86
87 Ok(Some((res.1, res.2)))
88 }
89
90 /// Get the account change (pre-state) for an account at a specific block.
91 ///
92 /// If the return value is `None`, the account was not changed in that
93 /// block.
94 fn get_account_change(
95 &self,
96 block_number: u64,
97 address: &Address,
98 ) -> Result<Option<Account>, Self::Error> {
99 self.get_dual::<tables::AccountChangeSets>(&block_number, address)
100 }
101
102 /// Get the storage history for an account and storage slot. The returned
103 /// list will contain block numbers where the storage slot was changed.
104 fn get_storage_history(
105 &self,
106 address: &Address,
107 slot: U256,
108 highest_block_number: u64,
109 ) -> Result<Option<BlockNumberList>, Self::Error> {
110 let sharded_key = ShardedKey::new(slot, highest_block_number);
111 self.get_dual::<tables::StorageHistory>(address, &sharded_key)
112 }
113
114 /// Get the last (highest) storage history entry for an address and slot.
115 fn last_storage_history(
116 &self,
117 address: &Address,
118 slot: &U256,
119 ) -> Result<Option<(ShardedKey<U256>, BlockNumberList)>, Self::Error> {
120 let mut cursor = self.traverse_dual::<tables::StorageHistory>()?;
121
122 // Seek to the highest possible key for this (address, slot) combination.
123 // ShardedKey encodes as slot || highest_block_number, so seeking to
124 // (address, ShardedKey::new(slot, u64::MAX)) positions us at or after
125 // the last shard for this slot.
126 let target = ShardedKey::new(*slot, u64::MAX);
127 let result = cursor.next_dual_above(address, &target)?;
128
129 // Check if we found an exact match for this address and slot
130 if let Some((addr, sharded_key, list)) = result
131 && addr == *address
132 && sharded_key.key == *slot
133 {
134 return Ok(Some((sharded_key, list)));
135 }
136
137 // The cursor is positioned at or after our target. Go backwards to find
138 // the last entry for this (address, slot).
139 let Some((addr, sharded_key, list)) = cursor.previous_k2()? else {
140 return Ok(None);
141 };
142
143 if addr == *address && sharded_key.key == *slot {
144 Ok(Some((sharded_key, list)))
145 } else {
146 Ok(None)
147 }
148 }
149
150 /// Get the storage change (before state) for a specific storage slot at a
151 /// specific block.
152 ///
153 /// If the return value is `None`, the storage slot was not changed in that
154 /// block. If the return value is `Some(value)`, the value is the pre-state
155 /// of the storage slot before the change in that block. If the value is
156 /// `U256::ZERO`, that indicates that the storage slot was not set before
157 /// the change.
158 fn get_storage_change(
159 &self,
160 block_number: u64,
161 address: &Address,
162 slot: &U256,
163 ) -> Result<Option<U256>, Self::Error> {
164 let block_number_address = (block_number, *address);
165 self.get_dual::<tables::StorageChangeSets>(&block_number_address, slot)
166 }
167
168 /// Get the last (highest) header in the database.
169 /// Returns None if the database is empty.
170 fn last_header(&self) -> Result<Option<SealedHeader>, Self::Error> {
171 let mut cursor = self.traverse::<tables::Headers>()?;
172 Ok(cursor.last()?.map(|(_, header)| header))
173 }
174
175 /// Get the last (highest) block number in the database.
176 /// Returns None if the database is empty.
177 fn last_block_number(&self) -> Result<Option<u64>, Self::Error> {
178 let mut cursor = self.traverse::<tables::Headers>()?;
179 Ok(cursor.last()?.map(|(number, _)| number))
180 }
181
182 /// Get the first (lowest) header in the database.
183 /// Returns None if the database is empty.
184 fn first_header(&self) -> Result<Option<SealedHeader>, Self::Error> {
185 let mut cursor = self.traverse::<tables::Headers>()?;
186 Ok(cursor.first()?.map(|(_, header)| header))
187 }
188
189 /// Get the current chain tip (highest block number and hash).
190 /// Returns None if the database is empty.
191 fn get_chain_tip(&self) -> Result<Option<(u64, B256)>, Self::Error> {
192 let mut cursor = self.traverse::<tables::Headers>()?;
193 let Some((number, header)) = cursor.last()? else {
194 return Ok(None);
195 };
196 let hash = header.hash();
197 Ok(Some((number, hash)))
198 }
199
200 /// Get the execution range (first and last block numbers with headers).
201 /// Returns None if the database is empty.
202 fn get_execution_range(&self) -> Result<Option<(u64, u64)>, Self::Error> {
203 let mut cursor = self.traverse::<tables::Headers>()?;
204 let Some((first, _)) = cursor.first()? else {
205 return Ok(None);
206 };
207 let Some((last, _)) = cursor.last()? else {
208 return Ok(None);
209 };
210 Ok(Some((first, last)))
211 }
212
213 /// Get account state, optionally at a specific historical block height.
214 ///
215 /// When `height` is `Some`, reconstructs the account state as it was at
216 /// that block height by consulting history and change set tables. When
217 /// `None`, returns the current value from `PlainAccountState`.
218 ///
219 /// If no changes exist after the given height, the current value is
220 /// returned (the account has not been modified since that height).
221 ///
222 /// # Note
223 ///
224 /// This method does **not** validate `height` against the stored block
225 /// range. Heights past the chain tip silently return current state, and
226 /// heights before the first block return the pre-state of the earliest
227 /// change. Use [`Self::get_account_at_height_checked`] or
228 /// [`HotKv::revm_reader_at_height`] for validated access.
229 ///
230 /// [`HotKv::revm_reader_at_height`]: crate::model::HotKv::revm_reader_at_height
231 fn get_account_at_height(
232 &self,
233 address: &Address,
234 height: Option<u64>,
235 ) -> Result<Option<Account>, Self::Error> {
236 let Some(height) = height else {
237 return self.get_account(address);
238 };
239
240 let mut cursor = self.traverse_dual::<tables::AccountsHistory>()?;
241
242 // Seek to the first shard with key2 >= height + 1
243 let result = cursor.next_dual_above(address, &(height + 1))?;
244
245 // Verify address matches; seek could overshoot to the next address
246 let Some((_, _, list)) = result.filter(|(addr, _, _)| *addr == *address) else {
247 // No history after height — account unchanged, use current value
248 return self.get_account(address);
249 };
250
251 // rank(height) = count of values <= height; select(rank) = first value > height
252 let rank = list.rank(height);
253 let Some(first_change) = list.select(rank) else {
254 // Defensive: shard key2 > height and is in the list, so this
255 // should not happen. Fall back to current value.
256 return self.get_account(address);
257 };
258
259 self.get_account_change(first_change, address)
260 }
261
262 /// Get storage slot value, optionally at a specific historical block
263 /// height.
264 ///
265 /// When `height` is `Some`, reconstructs the storage value as it was at
266 /// that block height by consulting history and change set tables. When
267 /// `None`, returns the current value from `PlainStorageState`.
268 ///
269 /// If no changes exist after the given height, the current value is
270 /// returned (the slot has not been modified since that height).
271 ///
272 /// # Note
273 ///
274 /// This method does **not** validate `height` against the stored block
275 /// range. Heights past the chain tip silently return current state, and
276 /// heights before the first block return the pre-state of the earliest
277 /// change. Use [`Self::get_storage_at_height_checked`] or
278 /// [`HotKv::revm_reader_at_height`] for validated access.
279 ///
280 /// [`HotKv::revm_reader_at_height`]: crate::model::HotKv::revm_reader_at_height
281 fn get_storage_at_height(
282 &self,
283 address: &Address,
284 slot: &U256,
285 height: Option<u64>,
286 ) -> Result<Option<U256>, Self::Error> {
287 let Some(height) = height else {
288 return self.get_storage(address, slot);
289 };
290
291 let mut cursor = self.traverse_dual::<tables::StorageHistory>()?;
292
293 // Seek to first shard with (address, ShardedKey { slot, block >= height+1 })
294 let target = ShardedKey::new(*slot, height + 1);
295 let result = cursor.next_dual_above(address, &target)?;
296
297 // Verify address AND slot match; seek could land on a different slot
298 let Some((_, _, list)) =
299 result.filter(|(addr, sk, _)| *addr == *address && sk.key == *slot)
300 else {
301 // No history after height — slot unchanged, use current value
302 return self.get_storage(address, slot);
303 };
304
305 let rank = list.rank(height);
306 let Some(first_change) = list.select(rank) else {
307 return self.get_storage(address, slot);
308 };
309
310 self.get_storage_change(first_change, address, slot)
311 }
312
313 /// Validate that `height` is within the stored block range.
314 ///
315 /// Returns `Ok(())` if `height` is `None` (current state) or within the
316 /// range of stored blocks. Returns an error if the database has no
317 /// blocks or if the height is out of range.
318 fn check_height(&self, height: Option<u64>) -> Result<(), HistoryError<Self::Error>> {
319 let Some(height) = height else { return Ok(()) };
320 let Some((first, last)) = self.get_execution_range().map_err(HistoryError::Db)? else {
321 return Err(HistoryError::NoBlocks);
322 };
323 if height < first || height > last {
324 return Err(HistoryError::HeightOutOfRange { height, first, last });
325 }
326 Ok(())
327 }
328
329 /// Get account state at a height, with range validation.
330 ///
331 /// Validates that `height` is within the stored block range before
332 /// delegating to [`Self::get_account_at_height`].
333 fn get_account_at_height_checked(
334 &self,
335 address: &Address,
336 height: Option<u64>,
337 ) -> Result<Option<Account>, HistoryError<Self::Error>> {
338 self.check_height(height)?;
339 self.get_account_at_height(address, height).map_err(HistoryError::Db)
340 }
341
342 /// Get storage slot value at a height, with range validation.
343 ///
344 /// Validates that `height` is within the stored block range before
345 /// delegating to [`Self::get_storage_at_height`].
346 fn get_storage_at_height_checked(
347 &self,
348 address: &Address,
349 slot: &U256,
350 height: Option<u64>,
351 ) -> Result<Option<U256>, HistoryError<Self::Error>> {
352 self.check_height(height)?;
353 self.get_storage_at_height(address, slot, height).map_err(HistoryError::Db)
354 }
355
356 /// Check if a specific block number exists in history.
357 fn has_block(&self, number: u64) -> Result<bool, Self::Error> {
358 self.get_header(number).map(|opt| opt.is_some())
359 }
360
361 /// Get headers in a range (inclusive).
362 fn get_headers_range(&self, start: u64, end: u64) -> Result<Vec<SealedHeader>, Self::Error> {
363 self.traverse::<tables::Headers>()?
364 .iter_from(&start)?
365 .take_while(|r| r.as_ref().is_ok_and(|(num, _)| *num <= end))
366 .map(|r| r.map(|(_, header)| header))
367 .collect()
368 }
369}
370
371impl<T> HistoryRead for T where T: HotDbRead {}