1use std::{
2 fs::File,
3 io::{BufReader, BufWriter, Write},
4 path::{Path, PathBuf},
5};
6
7use crate::wal::{WalError, WalResult};
8use reth_ethereum_primitives::EthPrimitives;
9use reth_exex_types::ExExNotification;
10use reth_node_api::NodePrimitives;
11use reth_tracing::tracing::debug;
12use tracing::instrument;
13
14static FILE_EXTENSION: &str = "wal";
15
16#[derive(Debug, Clone)]
21pub struct Storage<N: NodePrimitives = EthPrimitives> {
22 path: PathBuf,
24 _pd: std::marker::PhantomData<N>,
25}
26
27impl<N> Storage<N>
28where
29 N: NodePrimitives,
30{
31 pub(super) fn new(path: impl AsRef<Path>) -> WalResult<Self> {
34 reth_fs_util::create_dir_all(&path)?;
35
36 Ok(Self { path: path.as_ref().to_path_buf(), _pd: std::marker::PhantomData })
37 }
38
39 fn file_path(&self, id: u32) -> PathBuf {
40 self.path.join(format!("{id}.{FILE_EXTENSION}"))
41 }
42
43 fn parse_filename(filename: &str) -> WalResult<u32> {
44 filename
45 .strip_suffix(".wal")
46 .and_then(|s| s.parse().ok())
47 .ok_or_else(|| WalError::Parse(filename.to_string()))
48 }
49
50 #[instrument(skip(self))]
56 fn remove_notification(&self, file_id: u32) -> Option<u64> {
57 let path = self.file_path(file_id);
58 let size = path.metadata().ok()?.len();
59
60 match reth_fs_util::remove_file(self.file_path(file_id)) {
61 Ok(()) => {
62 debug!(target: "exex::wal::storage", "Notification was removed from the storage");
63 Some(size)
64 }
65 Err(err) => {
66 debug!(target: "exex::wal::storage", ?err, "Failed to remove notification from the storage");
67 None
68 }
69 }
70 }
71
72 pub(super) fn file_ids(&self) -> WalResult<Vec<u32>> {
74 let mut file_ids = Vec::new();
75
76 for entry in reth_fs_util::read_dir(&self.path)? {
77 let entry = entry.map_err(|err| WalError::DirEntry(self.path.clone(), err))?;
78
79 if entry.path().extension() == Some(FILE_EXTENSION.as_ref()) {
80 let file_name = entry.file_name();
81 let file_id = Self::parse_filename(&file_name.to_string_lossy())?;
82 file_ids.push(file_id);
83 }
84 }
85
86 file_ids.sort_unstable();
87 Ok(file_ids)
88 }
89
90 pub(super) fn remove_notifications(
96 &self,
97 file_ids: impl IntoIterator<Item = u32>,
98 ) -> WalResult<(usize, u64)> {
99 let mut deleted_total = 0;
100 let mut deleted_size = 0;
101
102 for id in file_ids {
103 if let Some(size) = self.remove_notification(id) {
104 deleted_total += 1;
105 deleted_size += size;
106 }
107 }
108
109 Ok((deleted_total, deleted_size))
110 }
111
112 pub(super) fn iter_notifications<'a>(
113 &'a self,
114 file_ids: impl IntoIterator<Item = u32> + 'a,
115 ) -> impl Iterator<Item = WalResult<(u32, u64, ExExNotification<N>)>> + 'a {
116 file_ids.into_iter().map(move |id| {
117 let (notification, size) =
118 self.read_notification(id)?.ok_or(WalError::FileNotFound(id))?;
119
120 Ok((id, size, notification))
121 })
122 }
123
124 #[instrument(skip(self))]
126 pub(super) fn read_notification(
127 &self,
128 file_id: u32,
129 ) -> WalResult<Option<(ExExNotification<N>, u64)>> {
130 let file_path = self.file_path(file_id);
131 debug!(target: "exex::wal::storage", ?file_path, "Reading notification from WAL");
132
133 let mut file = match File::open(&file_path) {
134 Ok(file) => file,
135 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
136 Err(err) => return Err(reth_fs_util::FsPathError::open(err, &file_path).into()),
137 };
138 let size = file.metadata().map_err(|err| WalError::FileMetadata(file_id, err))?.len();
139
140 let notification: reth_exex_types::serde_bincode_compat::ExExNotification<'_, N> =
142 rmp_serde::decode::from_read(BufReader::new(&mut file))
143 .map_err(|err| WalError::Decode(file_id, file_path, err))?;
144
145 Ok(Some((notification.into(), size)))
146 }
147
148 #[instrument(skip(self, notification))]
154 pub(super) fn write_notification(
155 &self,
156 file_id: u32,
157 notification: &ExExNotification<N>,
158 ) -> WalResult<u64> {
159 let file_path = self.file_path(file_id);
160 debug!(target: "exex::wal::storage", ?file_path, "Writing notification to WAL");
161
162 let notification =
164 reth_exex_types::serde_bincode_compat::ExExNotification::<N>::from(notification);
165
166 reth_fs_util::atomic_write_file(&file_path, |file| {
167 let mut writer = BufWriter::new(file);
168 rmp_serde::encode::write(&mut writer, ¬ification)?;
169 writer.flush()?;
172 Ok::<_, Box<dyn core::error::Error + Send + Sync>>(())
173 })?;
174
175 Ok(file_path.metadata().map_err(|err| WalError::FileMetadata(file_id, err))?.len())
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::Storage;
182 use alloy_consensus::BlockHeader;
183 use alloy_primitives::{
184 map::{HashMap, HashSet},
185 B256, U256,
186 };
187 use reth_exex_types::ExExNotification;
188 use reth_primitives_traits::Account;
189 use reth_provider::Chain;
190 use reth_testing_utils::generators::{self, random_block};
191 use reth_trie_common::{
192 serde_bincode_compat,
193 updates::{StorageTrieUpdates, StorageTrieUpdatesSorted, TrieUpdates},
194 BranchNodeCompact, ComputedTrieData, HashedPostState, HashedStorage, HashedStorageSorted,
195 LazyTrieData, Nibbles,
196 };
197 use std::{collections::BTreeMap, fs::File, sync::Arc};
198
199 #[test]
200 fn test_roundtrip() -> eyre::Result<()> {
201 let mut rng = generators::rng();
202
203 let temp_dir = tempfile::tempdir()?;
204 let storage: Storage = Storage::new(&temp_dir)?;
205
206 let old_block = random_block(&mut rng, 0, Default::default()).try_recover()?;
207 let new_block = random_block(&mut rng, 0, Default::default()).try_recover()?;
208
209 let notification = ExExNotification::ChainReorged {
210 new: Arc::new(Chain::new(vec![new_block], Default::default(), BTreeMap::new())),
211 old: Arc::new(Chain::new(vec![old_block], Default::default(), BTreeMap::new())),
212 };
213
214 let file_id = 0;
216 storage.write_notification(file_id, ¬ification)?;
217 let deserialized_notification = storage.read_notification(file_id)?;
218 assert_eq!(
219 deserialized_notification.map(|(notification, _)| notification),
220 Some(notification)
221 );
222
223 Ok(())
224 }
225
226 #[test]
227 fn test_decode_legacy_sorted_trie_data() -> eyre::Result<()> {
228 let storage_nodes =
229 vec![(Nibbles::from_nibbles_unchecked([0x01]), Some(BranchNodeCompact::default()))];
230 let encoded = rmp_serde::encode::to_vec(&(false, &storage_nodes))?;
231 let decoded: serde_bincode_compat::updates::StorageTrieUpdatesSorted<'_> =
232 rmp_serde::decode::from_slice(&encoded)?;
233 let decoded: StorageTrieUpdatesSorted = decoded.into();
234 assert_eq!(decoded.storage_nodes, storage_nodes);
235
236 let storage_slots = vec![(B256::from([1; 32]), U256::from(1))];
237 let encoded = rmp_serde::encode::to_vec(&(&storage_slots, false))?;
238 let decoded: serde_bincode_compat::hashed_state::HashedStorageSorted<'_> =
239 rmp_serde::decode::from_slice(&encoded)?;
240 let decoded: HashedStorageSorted = decoded.into();
241 assert_eq!(decoded.storage_slots, storage_slots);
242
243 Ok(())
244 }
245
246 #[test]
253 #[ignore]
254 fn generate_test_wal() -> eyre::Result<()> {
255 use std::io::Write;
256
257 let notification = get_test_notification_data()?;
258
259 let notification_compat =
261 reth_exex_types::serde_bincode_compat::ExExNotification::from(¬ification);
262 let encoded = rmp_serde::encode::to_vec(¬ification_compat)?;
263
264 let test_data_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("test-data");
266 std::fs::create_dir_all(&test_data_dir)?;
267
268 let output_path = test_data_dir.join("new_format.wal");
269 let mut file = File::create(&output_path)?;
270 file.write_all(&encoded)?;
271
272 println!("Generated WAL file at: {}", output_path.display());
273 println!("File size: {} bytes", encoded.len());
274 println!("✓ WAL file created successfully!");
275
276 Ok(())
277 }
278
279 fn get_test_notification_data(
281 ) -> eyre::Result<ExExNotification<reth_ethereum_primitives::EthPrimitives>> {
282 use reth_ethereum_primitives::Block;
283 use reth_primitives_traits::Block as _;
284
285 let block = Block::default().seal_slow().try_recover()?;
287 let block_number = block.header().number();
288
289 let hashed_address = B256::from([1; 32]);
290 let storage_key = B256::from([2; 32]);
291
292 let trie_updates = TrieUpdates {
293 account_nodes: HashMap::from_iter([
294 (Nibbles::from_nibbles_unchecked([0x01]), BranchNodeCompact::default()),
295 (Nibbles::from_nibbles_unchecked([0x02]), BranchNodeCompact::default()),
296 ]),
297 removed_nodes: HashSet::from_iter([Nibbles::from_nibbles_unchecked([0x03])]),
298 storage_tries: HashMap::from_iter([(
299 hashed_address,
300 StorageTrieUpdates {
301 storage_nodes: HashMap::from_iter([(
302 Nibbles::from_nibbles_unchecked([0x04]),
303 BranchNodeCompact::default(),
304 )]),
305 removed_nodes: Default::default(),
306 },
307 )]),
308 };
309
310 let hashed_state = HashedPostState {
311 accounts: HashMap::from_iter([(
312 hashed_address,
313 Some(Account { nonce: 1, ..Default::default() }),
314 )]),
315 storages: HashMap::from_iter([(
316 hashed_address,
317 HashedStorage { storage: HashMap::from_iter([(storage_key, U256::from(101))]) },
318 )]),
319 };
320
321 let trie_data = LazyTrieData::ready(ComputedTrieData::new(
322 Arc::new(hashed_state.into_sorted()),
323 Arc::new(trie_updates.into_sorted()),
324 ));
325
326 let notification: ExExNotification<reth_ethereum_primitives::EthPrimitives> =
327 ExExNotification::ChainCommitted {
328 new: Arc::new(Chain::new(
329 vec![block],
330 Default::default(),
331 BTreeMap::from([(block_number, trie_data)]),
332 )),
333 };
334 Ok(notification)
335 }
336
337 #[test]
338 fn test_file_ids() -> eyre::Result<()> {
339 let temp_dir = tempfile::tempdir()?;
340 let storage: Storage = Storage::new(&temp_dir)?;
341
342 File::create(storage.file_path(1))?;
344 File::create(storage.file_path(3))?;
345
346 File::create(temp_dir.path().join("0.tmp"))?;
348 File::create(temp_dir.path().join("4.tmp"))?;
349
350 assert_eq!(storage.file_ids()?, vec![1, 3]);
352
353 Ok(())
354 }
355}