1#![allow(dead_code)]
2
3mod cache;
4pub use cache::BlockCache;
5mod storage;
6use reth_ethereum_primitives::EthPrimitives;
7use reth_node_api::NodePrimitives;
8pub use storage::Storage;
9mod metrics;
10use metrics::Metrics;
11mod error;
12pub use error::{WalError, WalResult};
13
14use std::{
15 path::Path,
16 sync::{
17 atomic::{AtomicU32, Ordering},
18 Arc,
19 },
20};
21
22use alloy_eips::BlockNumHash;
23use alloy_primitives::B256;
24use parking_lot::{RwLock, RwLockReadGuard};
25use reth_exex_types::ExExNotification;
26use reth_tracing::tracing::{debug, instrument};
27
28#[derive(Debug, Clone)]
39pub struct Wal<N: NodePrimitives = EthPrimitives> {
40 inner: Arc<WalInner<N>>,
41}
42
43impl<N> Wal<N>
44where
45 N: NodePrimitives,
46{
47 pub fn new(directory: impl AsRef<Path>) -> WalResult<Self> {
49 Ok(Self { inner: Arc::new(WalInner::new(directory)?) })
50 }
51
52 pub fn handle(&self) -> WalHandle<N> {
54 WalHandle { wal: self.inner.clone() }
55 }
56
57 pub fn commit(&self, notification: &ExExNotification<N>) -> WalResult<()> {
59 self.inner.commit(notification)
60 }
61
62 pub fn finalize(&self, to_block: BlockNumHash) -> WalResult<()> {
67 self.inner.finalize(to_block)
68 }
69
70 pub fn iter_notifications(
72 &self,
73 ) -> WalResult<Box<dyn Iterator<Item = WalResult<ExExNotification<N>>> + '_>> {
74 self.inner.iter_notifications()
75 }
76
77 pub fn num_blocks(&self) -> usize {
79 self.inner.block_cache().num_blocks()
80 }
81}
82
83#[derive(Debug)]
85struct WalInner<N: NodePrimitives> {
86 next_file_id: AtomicU32,
87 storage: Storage<N>,
89 block_cache: RwLock<BlockCache>,
91 metrics: Metrics,
92}
93
94impl<N> WalInner<N>
95where
96 N: NodePrimitives,
97{
98 fn new(directory: impl AsRef<Path>) -> WalResult<Self> {
99 let wal = Self {
100 next_file_id: AtomicU32::new(0),
101 storage: Storage::new(directory)?,
102 block_cache: RwLock::new(BlockCache::default()),
103 metrics: Metrics::default(),
104 };
105 wal.fill_block_cache()?;
106 Ok(wal)
107 }
108
109 fn block_cache(&self) -> RwLockReadGuard<'_, BlockCache> {
110 self.block_cache.read()
111 }
112
113 #[instrument(skip(self))]
115 fn fill_block_cache(&self) -> WalResult<()> {
116 let file_ids = self.storage.file_ids()?;
117 let Some(last_file_id) = file_ids.last() else { return Ok(()) };
118 self.next_file_id.store(last_file_id + 1, Ordering::Relaxed);
119
120 let mut block_cache = self.block_cache.write();
121 let mut notifications_size = 0;
122
123 for entry in self.storage.iter_notifications(file_ids) {
124 let (file_id, size, notification) = entry?;
125
126 notifications_size += size;
127
128 let committed_chain = notification.committed_chain();
129 let reverted_chain = notification.reverted_chain();
130
131 debug!(
132 target: "exex::wal",
133 ?file_id,
134 reverted_block_range = ?reverted_chain.as_ref().map(|chain| chain.range()),
135 committed_block_range = ?committed_chain.as_ref().map(|chain| chain.range()),
136 "Inserting block cache entries"
137 );
138
139 block_cache.insert_notification_blocks_with_file_id(file_id, ¬ification);
140 }
141
142 self.update_metrics(&block_cache, notifications_size as i64);
143
144 Ok(())
145 }
146
147 #[instrument(skip_all, fields(
148 reverted_block_range = ?notification.reverted_chain().as_ref().map(|chain| chain.range()),
149 committed_block_range = ?notification.committed_chain().as_ref().map(|chain| chain.range())
150 ))]
151 fn commit(&self, notification: &ExExNotification<N>) -> WalResult<()> {
152 let mut block_cache = self.block_cache.write();
153
154 let file_id = self.next_file_id.fetch_add(1, Ordering::Relaxed);
155 let size = self.storage.write_notification(file_id, notification)?;
156
157 debug!(target: "exex::wal", ?file_id, "Inserting notification blocks into the block cache");
158 block_cache.insert_notification_blocks_with_file_id(file_id, notification);
159
160 self.update_metrics(&block_cache, size as i64);
161
162 Ok(())
163 }
164
165 #[instrument(skip(self))]
166 fn finalize(&self, to_block: BlockNumHash) -> WalResult<()> {
167 let mut block_cache = self.block_cache.write();
168 let file_ids = block_cache.remove_before(to_block.number);
169
170 if file_ids.is_empty() {
172 debug!(target: "exex::wal", "No notifications were finalized from the storage");
173 return Ok(())
174 }
175
176 let (removed_notifications, removed_size) = self.storage.remove_notifications(file_ids)?;
177 debug!(target: "exex::wal", ?removed_notifications, ?removed_size, "Storage was finalized");
178
179 self.update_metrics(&block_cache, -(removed_size as i64));
180
181 Ok(())
182 }
183
184 fn update_metrics(&self, block_cache: &BlockCache, size_delta: i64) {
185 self.metrics.size_bytes.increment(size_delta as f64);
186 self.metrics.notifications_count.set(block_cache.notification_max_blocks.len() as f64);
187 self.metrics.committed_blocks_count.set(block_cache.committed_blocks.len() as f64);
188
189 if let Some(lowest_committed_block_height) = block_cache.lowest_committed_block_height {
190 self.metrics.lowest_committed_block_height.set(lowest_committed_block_height as f64);
191 }
192
193 if let Some(highest_committed_block_height) = block_cache.highest_committed_block_height {
194 self.metrics.highest_committed_block_height.set(highest_committed_block_height as f64);
195 }
196 }
197
198 fn iter_notifications(
200 &self,
201 ) -> WalResult<Box<dyn Iterator<Item = WalResult<ExExNotification<N>>> + '_>> {
202 Ok(Box::new(
203 self.storage.iter_notifications(self.storage.file_ids()?).map(|entry| Ok(entry?.2)),
204 ))
205 }
206}
207
208#[derive(Debug)]
210pub struct WalHandle<N: NodePrimitives> {
211 wal: Arc<WalInner<N>>,
212}
213
214impl<N> WalHandle<N>
215where
216 N: NodePrimitives,
217{
218 pub fn get_committed_notification_by_block_hash(
220 &self,
221 block_hash: &B256,
222 ) -> WalResult<Option<ExExNotification<N>>> {
223 let Some(file_id) = self.wal.block_cache().get_file_id_by_committed_block_hash(block_hash)
224 else {
225 return Ok(None)
226 };
227
228 self.wal
229 .storage
230 .read_notification(file_id)
231 .map(|entry| entry.map(|(notification, _)| notification))
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use crate::wal::{cache::CachedBlock, error::WalResult, Wal};
238 use alloy_primitives::B256;
239 use itertools::Itertools;
240 use reth_ethereum_primitives::EthPrimitives;
241 use reth_exex_types::ExExNotification;
242 use reth_provider::Chain;
243 use reth_testing_utils::generators::{
244 self, random_block, random_block_range, BlockParams, BlockRangeParams,
245 };
246 use std::{collections::BTreeMap, sync::Arc};
247
248 fn read_notifications(wal: &Wal) -> WalResult<Vec<ExExNotification>> {
249 wal.inner
250 .storage
251 .iter_notifications(wal.inner.storage.file_ids()?)
252 .map(|entry| entry.map(|(_, _, n)| n))
253 .collect()
254 }
255
256 fn sort_committed_blocks(
257 committed_blocks: Vec<(B256, u32, CachedBlock)>,
258 ) -> Vec<(B256, u32, CachedBlock)> {
259 committed_blocks
260 .into_iter()
261 .sorted_by_key(|(_, _, block)| (block.block.number, block.block.hash))
262 .collect()
263 }
264
265 #[test]
266 fn test_wal() -> eyre::Result<()> {
267 reth_tracing::init_test_tracing();
268
269 let mut rng = generators::rng();
270
271 let temp_dir = tempfile::tempdir()?;
273 let wal = Wal::new(&temp_dir)?;
274 assert!(wal.inner.block_cache().is_empty());
275
276 let blocks = random_block_range(&mut rng, 0..=3, BlockRangeParams::default())
278 .into_iter()
279 .map(|block| block.try_recover())
280 .collect::<Result<Vec<_>, _>>()?;
281 let block_1_reorged = random_block(
282 &mut rng,
283 1,
284 BlockParams { parent: Some(blocks[0].hash()), ..Default::default() },
285 )
286 .try_recover()?;
287 let block_2_reorged = random_block(
288 &mut rng,
289 2,
290 BlockParams { parent: Some(blocks[1].hash()), ..Default::default() },
291 )
292 .try_recover()?;
293
294 let committed_notification_1 = ExExNotification::ChainCommitted {
301 new: Arc::new(Chain::new(
302 vec![blocks[0].clone(), blocks[1].clone()],
303 Default::default(),
304 BTreeMap::new(),
305 )),
306 };
307 let reverted_notification = ExExNotification::ChainReverted {
308 old: Arc::new(Chain::new(vec![blocks[1].clone()], Default::default(), BTreeMap::new())),
309 };
310 let committed_notification_2 = ExExNotification::ChainCommitted {
311 new: Arc::new(Chain::new(
312 vec![block_1_reorged.clone(), blocks[2].clone()],
313 Default::default(),
314 BTreeMap::new(),
315 )),
316 };
317 let reorged_notification = ExExNotification::ChainReorged {
318 old: Arc::new(Chain::new(vec![blocks[2].clone()], Default::default(), BTreeMap::new())),
319 new: Arc::new(Chain::new(
320 vec![block_2_reorged.clone(), blocks[3].clone()],
321 Default::default(),
322 BTreeMap::new(),
323 )),
324 };
325
326 let file_id = 0;
331 let committed_notification_1_cache_blocks = (blocks[1].number, file_id);
332 let committed_notification_1_cache_committed_blocks = vec![
333 (
334 blocks[0].hash(),
335 file_id,
336 CachedBlock {
337 block: (blocks[0].number, blocks[0].hash()).into(),
338 parent_hash: blocks[0].parent_hash,
339 },
340 ),
341 (
342 blocks[1].hash(),
343 file_id,
344 CachedBlock {
345 block: (blocks[1].number, blocks[1].hash()).into(),
346 parent_hash: blocks[1].parent_hash,
347 },
348 ),
349 ];
350 wal.commit(&committed_notification_1)?;
351 assert_eq!(
352 wal.inner.block_cache().blocks_sorted(),
353 [committed_notification_1_cache_blocks]
354 );
355 assert_eq!(
356 wal.inner.block_cache().committed_blocks_sorted(),
357 committed_notification_1_cache_committed_blocks
358 );
359 assert_eq!(read_notifications(&wal)?, vec![committed_notification_1.clone()]);
360
361 wal.commit(&reverted_notification)?;
363 let file_id = 1;
364 let reverted_notification_cache_blocks = (blocks[1].number, file_id);
365 assert_eq!(
366 wal.inner.block_cache().blocks_sorted(),
367 [reverted_notification_cache_blocks, committed_notification_1_cache_blocks]
368 );
369 assert_eq!(
370 wal.inner.block_cache().committed_blocks_sorted(),
371 committed_notification_1_cache_committed_blocks
372 );
373 assert_eq!(
374 read_notifications(&wal)?,
375 vec![committed_notification_1.clone(), reverted_notification.clone()]
376 );
377
378 wal.commit(&committed_notification_2)?;
380 let file_id = 2;
381 let committed_notification_2_cache_blocks = (blocks[2].number, file_id);
382 let committed_notification_2_cache_committed_blocks = vec![
383 (
384 block_1_reorged.hash(),
385 file_id,
386 CachedBlock {
387 block: (block_1_reorged.number, block_1_reorged.hash()).into(),
388 parent_hash: block_1_reorged.parent_hash,
389 },
390 ),
391 (
392 blocks[2].hash(),
393 file_id,
394 CachedBlock {
395 block: (blocks[2].number, blocks[2].hash()).into(),
396 parent_hash: blocks[2].parent_hash,
397 },
398 ),
399 ];
400 assert_eq!(
401 wal.inner.block_cache().blocks_sorted(),
402 [
403 committed_notification_2_cache_blocks,
404 reverted_notification_cache_blocks,
405 committed_notification_1_cache_blocks,
406 ]
407 );
408 assert_eq!(
409 wal.inner.block_cache().committed_blocks_sorted(),
410 sort_committed_blocks(
411 [
412 committed_notification_1_cache_committed_blocks.clone(),
413 committed_notification_2_cache_committed_blocks.clone()
414 ]
415 .concat()
416 )
417 );
418 assert_eq!(
419 read_notifications(&wal)?,
420 vec![
421 committed_notification_1.clone(),
422 reverted_notification.clone(),
423 committed_notification_2.clone()
424 ]
425 );
426
427 wal.commit(&reorged_notification)?;
429 let file_id = 3;
430 let reorged_notification_cache_blocks = (blocks[3].number, file_id);
431 let reorged_notification_cache_committed_blocks = vec![
432 (
433 block_2_reorged.hash(),
434 file_id,
435 CachedBlock {
436 block: (block_2_reorged.number, block_2_reorged.hash()).into(),
437 parent_hash: block_2_reorged.parent_hash,
438 },
439 ),
440 (
441 blocks[3].hash(),
442 file_id,
443 CachedBlock {
444 block: (blocks[3].number, blocks[3].hash()).into(),
445 parent_hash: blocks[3].parent_hash,
446 },
447 ),
448 ];
449 assert_eq!(
450 wal.inner.block_cache().blocks_sorted(),
451 [
452 reorged_notification_cache_blocks,
453 committed_notification_2_cache_blocks,
454 reverted_notification_cache_blocks,
455 committed_notification_1_cache_blocks,
456 ]
457 );
458 assert_eq!(
459 wal.inner.block_cache().committed_blocks_sorted(),
460 sort_committed_blocks(
461 [
462 committed_notification_1_cache_committed_blocks,
463 committed_notification_2_cache_committed_blocks.clone(),
464 reorged_notification_cache_committed_blocks.clone()
465 ]
466 .concat()
467 )
468 );
469 assert_eq!(
470 read_notifications(&wal)?,
471 vec![
472 committed_notification_1,
473 reverted_notification,
474 committed_notification_2.clone(),
475 reorged_notification.clone()
476 ]
477 );
478
479 wal.finalize((block_1_reorged.number, block_1_reorged.hash()).into())?;
484 assert_eq!(
485 wal.inner.block_cache().blocks_sorted(),
486 [reorged_notification_cache_blocks, committed_notification_2_cache_blocks]
487 );
488 assert_eq!(
489 wal.inner.block_cache().committed_blocks_sorted(),
490 sort_committed_blocks(
491 [
492 committed_notification_2_cache_committed_blocks.clone(),
493 reorged_notification_cache_committed_blocks.clone()
494 ]
495 .concat()
496 )
497 );
498 assert_eq!(
499 read_notifications(&wal)?,
500 vec![committed_notification_2.clone(), reorged_notification.clone()]
501 );
502
503 let wal = Wal::new(&temp_dir)?;
505 assert_eq!(
506 wal.inner.block_cache().blocks_sorted(),
507 [reorged_notification_cache_blocks, committed_notification_2_cache_blocks]
508 );
509 assert_eq!(
510 wal.inner.block_cache().committed_blocks_sorted(),
511 sort_committed_blocks(
512 [
513 committed_notification_2_cache_committed_blocks,
514 reorged_notification_cache_committed_blocks
515 ]
516 .concat()
517 )
518 );
519 assert_eq!(read_notifications(&wal)?, vec![committed_notification_2, reorged_notification]);
520
521 Ok(())
522 }
523
524 #[test]
525 fn test_finalize_with_sparse_file_ids() -> eyre::Result<()> {
526 let mut rng = generators::rng();
527
528 let temp_dir = tempfile::tempdir()?;
529 let wal = Wal::<EthPrimitives>::new(&temp_dir)?;
530
531 let blocks = random_block_range(&mut rng, 0..=5, BlockRangeParams::default())
532 .into_iter()
533 .map(|block| block.try_recover())
534 .collect::<Result<Vec<_>, _>>()?;
535 let chain = |range: std::ops::RangeInclusive<usize>| {
536 Arc::new(Chain::new(blocks[range].to_vec(), Default::default(), BTreeMap::new()))
537 };
538
539 let commit_to_four = ExExNotification::ChainCommitted { new: chain(1..=4) };
540 let revert_to_two = ExExNotification::ChainReverted { old: chain(2..=4) };
541 let commit_two = ExExNotification::ChainCommitted { new: chain(2..=2) };
542 let commit_to_five = ExExNotification::ChainCommitted { new: chain(3..=5) };
543
544 wal.commit(&commit_to_four)?;
545 wal.commit(&revert_to_two)?;
546 wal.commit(&commit_two)?;
547 wal.commit(&commit_to_five)?;
548 assert_eq!(wal.inner.storage.file_ids()?, vec![0, 1, 2, 3]);
549
550 wal.finalize((blocks[2].number, blocks[2].hash()).into())?;
553 assert_eq!(wal.inner.storage.file_ids()?, vec![0, 1, 3]);
554
555 let wal = Wal::<EthPrimitives>::new(&temp_dir)?;
556 assert_eq!(read_notifications(&wal)?, vec![commit_to_four, revert_to_two, commit_to_five]);
557
558 wal.commit(&commit_two)?;
559 assert_eq!(wal.inner.storage.file_ids()?, vec![0, 1, 3, 4]);
560
561 Ok(())
562 }
563}