Skip to content

Commit c605eef

Browse files
authored
feat(era-e2s): add E2StoreWriter (paradigmxyz#15560)
1 parent 6b8f5c5 commit c605eef

File tree

2 files changed

+232
-5
lines changed

2 files changed

+232
-5
lines changed

crates/era/src/e2s_file.rs

+230-3
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22
//!
33
//! See also <https://github.com/status-im/nimbus-eth2/blob/stable/docs/e2store.md>
44
5-
use crate::e2s_types::{E2sError, Entry};
6-
use std::io::{BufReader, Read, Seek, SeekFrom};
5+
use crate::e2s_types::{E2sError, Entry, Version};
6+
use std::io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write};
7+
8+
/// A reader for `E2Store` files that wraps a [`BufReader`].
79
8-
/// A reader for `E2Store` files that wraps a [`BufReader`]
910
#[derive(Debug)]
1011
pub struct E2StoreReader<R: Read> {
12+
/// Buffered reader
1113
reader: BufReader<R>,
1214
}
1315

@@ -49,6 +51,58 @@ impl<R: Read + Seek> E2StoreReader<R> {
4951
}
5052
}
5153

54+
/// A writer for `E2Store` files that wraps a [`BufWriter`].
55+
#[derive(Debug)]
56+
pub struct E2StoreWriter<W: Write> {
57+
/// Buffered writer
58+
writer: BufWriter<W>,
59+
/// Tracks whether this writer has written a version entry
60+
has_written_version: bool,
61+
}
62+
63+
impl<W: Write> E2StoreWriter<W> {
64+
/// Create a new [`E2StoreWriter`]
65+
pub fn new(writer: W) -> Self {
66+
Self { writer: BufWriter::new(writer), has_written_version: false }
67+
}
68+
69+
/// Create a new [`E2StoreWriter`] and write the version entry
70+
pub fn with_version(writer: W) -> Result<Self, E2sError> {
71+
let mut writer = Self::new(writer);
72+
writer.write_version()?;
73+
Ok(writer)
74+
}
75+
76+
/// Write the version entry as the first entry in the file.
77+
/// This must be called before writing any other entries.
78+
pub fn write_version(&mut self) -> Result<(), E2sError> {
79+
if self.has_written_version {
80+
return Ok(());
81+
}
82+
83+
let version = Version;
84+
version.encode(&mut self.writer)?;
85+
self.has_written_version = true;
86+
Ok(())
87+
}
88+
89+
/// Write an entry to the file.
90+
/// If a version entry has not been written yet, it will be added.
91+
pub fn write_entry(&mut self, entry: &Entry) -> Result<(), E2sError> {
92+
if !self.has_written_version {
93+
self.write_version()?;
94+
}
95+
96+
entry.write(&mut self.writer)?;
97+
Ok(())
98+
}
99+
100+
/// Flush any buffered data to the underlying writer
101+
pub fn flush(&mut self) -> Result<(), E2sError> {
102+
self.writer.flush().map_err(E2sError::Io)
103+
}
104+
}
105+
52106
#[cfg(test)]
53107
mod tests {
54108
use super::*;
@@ -194,4 +248,177 @@ mod tests {
194248

195249
Ok(())
196250
}
251+
252+
#[test]
253+
fn test_e2store_writer() -> Result<(), E2sError> {
254+
let mut buffer = Vec::new();
255+
256+
{
257+
let mut writer = E2StoreWriter::new(&mut buffer);
258+
259+
// Write version entry
260+
writer.write_version()?;
261+
262+
// Write a block index entry
263+
let block_entry = Entry::new(SLOT_INDEX, create_slot_index_data(1, &[1024]));
264+
writer.write_entry(&block_entry)?;
265+
266+
// Write a custom entry
267+
let custom_type = [0x99, 0x99];
268+
let custom_entry = Entry::new(custom_type, vec![10, 11, 12]);
269+
writer.write_entry(&custom_entry)?;
270+
271+
writer.flush()?;
272+
}
273+
274+
let cursor = Cursor::new(&buffer);
275+
let mut reader = E2StoreReader::new(cursor);
276+
277+
let entries = reader.entries()?;
278+
assert_eq!(entries.len(), 3);
279+
assert!(entries[0].is_version());
280+
assert!(entries[1].is_slot_index());
281+
assert_eq!(entries[2].entry_type, [0x99, 0x99]);
282+
283+
Ok(())
284+
}
285+
286+
#[test]
287+
fn test_writer_implicit_version_insertion() -> Result<(), E2sError> {
288+
let mut buffer = Vec::new();
289+
290+
{
291+
// Writer without explicitly writing the version
292+
let mut writer = E2StoreWriter::new(&mut buffer);
293+
294+
// Write an entry, it should automatically add a version first
295+
let custom_type = [0x42, 0x42];
296+
let custom_entry = Entry::new(custom_type, vec![1, 2, 3, 4]);
297+
writer.write_entry(&custom_entry)?;
298+
299+
// Write another entry
300+
let another_custom = Entry::new([0x43, 0x43], vec![5, 6, 7, 8]);
301+
writer.write_entry(&another_custom)?;
302+
303+
writer.flush()?;
304+
}
305+
306+
let cursor = Cursor::new(&buffer);
307+
let mut reader = E2StoreReader::new(cursor);
308+
309+
let version = reader.read_version()?;
310+
assert!(version.is_some(), "Version entry should have been auto-added");
311+
312+
let entries = reader.entries()?;
313+
assert_eq!(entries.len(), 3);
314+
assert!(entries[0].is_version());
315+
assert_eq!(entries[1].entry_type, [0x42, 0x42]);
316+
assert_eq!(entries[1].data, vec![1, 2, 3, 4]);
317+
assert_eq!(entries[2].entry_type, [0x43, 0x43]);
318+
assert_eq!(entries[2].data, vec![5, 6, 7, 8]);
319+
320+
Ok(())
321+
}
322+
323+
#[test]
324+
fn test_writer_prevents_duplicate_versions() -> Result<(), E2sError> {
325+
let mut buffer = Vec::new();
326+
327+
{
328+
let mut writer = E2StoreWriter::new(&mut buffer);
329+
330+
// Call write_version multiple times, it should only write once
331+
writer.write_version()?;
332+
writer.write_version()?;
333+
writer.write_version()?;
334+
335+
// Write an entry
336+
let block_entry = Entry::new(SLOT_INDEX, create_slot_index_data(42, &[8192]));
337+
writer.write_entry(&block_entry)?;
338+
339+
writer.flush()?;
340+
}
341+
342+
// Verify only one version entry was written
343+
let cursor = Cursor::new(&buffer);
344+
let mut reader = E2StoreReader::new(cursor);
345+
346+
let entries = reader.entries()?;
347+
assert_eq!(entries.len(), 2);
348+
assert!(entries[0].is_version());
349+
assert!(entries[1].is_slot_index());
350+
351+
Ok(())
352+
}
353+
354+
#[test]
355+
fn test_e2store_multiple_roundtrip_conversions() -> Result<(), E2sError> {
356+
// Initial set of entries to test with varied types and sizes
357+
let entry1 = Entry::new([0x01, 0x01], vec![1, 2, 3, 4, 5]);
358+
let entry2 = Entry::new([0x02, 0x02], vec![10, 20, 30, 40, 50]);
359+
let entry3 = Entry::new(SLOT_INDEX, create_slot_index_data(123, &[45678]));
360+
let entry4 = Entry::new([0xFF, 0xFF], Vec::new());
361+
362+
println!("Initial entries count: 4");
363+
364+
// First write cycle : create initial buffer
365+
let mut buffer1 = Vec::new();
366+
{
367+
let mut writer = E2StoreWriter::new(&mut buffer1);
368+
writer.write_version()?;
369+
writer.write_entry(&entry1)?;
370+
writer.write_entry(&entry2)?;
371+
writer.write_entry(&entry3)?;
372+
writer.write_entry(&entry4)?;
373+
writer.flush()?;
374+
}
375+
376+
// First read cycle : read from initial buffer
377+
let mut reader1 = E2StoreReader::new(Cursor::new(&buffer1));
378+
let entries1 = reader1.entries()?;
379+
380+
println!("First read entries:");
381+
for (i, entry) in entries1.iter().enumerate() {
382+
println!("Entry {}: type {:?}, data len {}", i, entry.entry_type, entry.data.len());
383+
}
384+
println!("First read entries count: {}", entries1.len());
385+
386+
// Verify first read content
387+
assert_eq!(entries1.len(), 5, "Should have 5 entries (version + 4 data)");
388+
assert!(entries1[0].is_version());
389+
assert_eq!(entries1[1].entry_type, [0x01, 0x01]);
390+
assert_eq!(entries1[1].data, vec![1, 2, 3, 4, 5]);
391+
assert_eq!(entries1[2].entry_type, [0x02, 0x02]);
392+
assert_eq!(entries1[2].data, vec![10, 20, 30, 40, 50]);
393+
assert!(entries1[3].is_slot_index());
394+
assert_eq!(entries1[4].entry_type, [0xFF, 0xFF]);
395+
assert_eq!(entries1[4].data.len(), 0);
396+
397+
// Second write cycle : write what we just read
398+
let mut buffer2 = Vec::new();
399+
{
400+
let mut writer = E2StoreWriter::new(&mut buffer2);
401+
// Only write version once
402+
writer.write_version()?;
403+
404+
// Skip the first entry ie the version since we already wrote it
405+
for entry in &entries1[1..] {
406+
writer.write_entry(entry)?;
407+
}
408+
writer.flush()?;
409+
}
410+
411+
// Second read cycle - read the second buffer
412+
let mut reader2 = E2StoreReader::new(Cursor::new(&buffer2));
413+
let entries2 = reader2.entries()?;
414+
415+
// Verify second read matches first read
416+
assert_eq!(entries1.len(), entries2.len());
417+
for i in 0..entries1.len() {
418+
assert_eq!(entries1[i].entry_type, entries2[i].entry_type);
419+
assert_eq!(entries1[i].data, entries2[i].data);
420+
}
421+
422+
Ok(())
423+
}
197424
}

crates/era/src/e2s_types.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! Types to build e2store files
2-
//! ie. with .e2s extension
2+
//! ie. with `.e2s` extension
33
//!
4-
//! e2store contains header and entry
4+
//! e2store file contains header and entry
55
//!
66
//! The [`Header`] is an 8-byte structure at the beginning of each record in the file
77
//!

0 commit comments

Comments
 (0)