use std::io::Read;
use std::io::Seek;
use crate::error::E2StoreError;
use crate::error::Error;
use crate::format::Header;
use crate::format::e2store::E2StoreReader;
use crate::format::era::SlotIndex;
use crate::format::era::decompress_entry;
use crate::format::era1::B256;
use crate::format::era1::U256;
use crate::format::types::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EraFormat {
Era,
Era1,
}
impl std::fmt::Display for EraFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
EraFormat::Era => write!(f, "ERA"),
EraFormat::Era1 => write!(f, "ERA1"),
}
}
}
fn detect_format(typ: &[u8; 2]) -> EraFormat {
match *typ {
TYPE_COMPRESSED_SIGNED_BEACON_BLOCK | TYPE_COMPRESSED_BEACON_STATE | TYPE_SLOT_INDEX => {
EraFormat::Era
}
_ => EraFormat::Era1,
}
}
#[derive(Debug, Clone)]
pub enum TypedEntry {
BeaconBlock { data: Vec<u8> },
BeaconState { data: Vec<u8> },
BlockIndex { index: SlotIndex },
StateIndex { index: SlotIndex },
Header { data: Vec<u8> },
BlockBody { data: Vec<u8> },
Receipts { data: Vec<u8> },
TotalDifficulty { value: U256 },
BlockAccumulator { root: B256 },
Unknown { typ: [u8; 2], data: Vec<u8> },
}
impl TypedEntry {
pub fn entry_type(&self) -> [u8; 2] {
match self {
TypedEntry::BeaconBlock { .. } => TYPE_COMPRESSED_SIGNED_BEACON_BLOCK,
TypedEntry::BeaconState { .. } => TYPE_COMPRESSED_BEACON_STATE,
TypedEntry::BlockIndex { .. } => TYPE_SLOT_INDEX,
TypedEntry::StateIndex { .. } => TYPE_SLOT_INDEX,
TypedEntry::Header { .. } => TYPE_COMPRESSED_HEADER,
TypedEntry::BlockBody { .. } => TYPE_COMPRESSED_BODY,
TypedEntry::Receipts { .. } => TYPE_COMPRESSED_RECEIPTS,
TypedEntry::TotalDifficulty { .. } => TYPE_TOTAL_DIFFICULTY,
TypedEntry::BlockAccumulator { .. } => TYPE_ACCUMULATOR,
TypedEntry::Unknown { typ, .. } => *typ,
}
}
pub fn type_name(&self) -> &'static str {
match self {
TypedEntry::BeaconBlock { .. } => "BeaconBlock",
TypedEntry::BeaconState { .. } => "BeaconState",
TypedEntry::BlockIndex { .. } => "SlotIndex",
TypedEntry::StateIndex { .. } => "SlotIndex",
TypedEntry::Header { .. } => "Header",
TypedEntry::BlockBody { .. } => "BlockBody",
TypedEntry::Receipts { .. } => "Receipts",
TypedEntry::TotalDifficulty { .. } => "TotalDifficulty",
TypedEntry::BlockAccumulator { .. } => "BlockAccumulator",
TypedEntry::Unknown { .. } => "Unknown",
}
}
pub fn data_len(&self) -> usize {
match self {
TypedEntry::BeaconBlock { data } => data.len(),
TypedEntry::BeaconState { data } => data.len(),
TypedEntry::BlockIndex { index } => index.encode().len(),
TypedEntry::StateIndex { index } => index.encode().len(),
TypedEntry::Header { data } => data.len(),
TypedEntry::BlockBody { data } => data.len(),
TypedEntry::Receipts { data } => data.len(),
TypedEntry::TotalDifficulty { .. } => 32,
TypedEntry::BlockAccumulator { .. } => 32,
TypedEntry::Unknown { data, .. } => data.len(),
}
}
}
fn decode_entry(entry: crate::format::Entry) -> Result<TypedEntry, Error> {
let typ = entry.header.typ;
let data = entry.data;
match typ {
TYPE_COMPRESSED_SIGNED_BEACON_BLOCK => {
let decompressed = decompress_entry(&data)?;
Ok(TypedEntry::BeaconBlock { data: decompressed })
}
TYPE_COMPRESSED_BEACON_STATE => {
let decompressed = decompress_entry(&data)?;
Ok(TypedEntry::BeaconState { data: decompressed })
}
TYPE_SLOT_INDEX => {
let index = SlotIndex::decode(&data)
.map_err(|e| E2StoreError::InvalidEra(format!("bad slot index: {e}")))?;
Ok(TypedEntry::BlockIndex { index })
}
TYPE_COMPRESSED_HEADER => {
let decompressed = decompress_entry(&data)?;
Ok(TypedEntry::Header { data: decompressed })
}
TYPE_COMPRESSED_BODY => Ok(TypedEntry::BlockBody { data }),
TYPE_COMPRESSED_RECEIPTS => Ok(TypedEntry::Receipts { data }),
TYPE_TOTAL_DIFFICULTY => {
if data.len() != 32 {
return Err(E2StoreError::InvalidEra1(format!(
"total difficulty must be 32 bytes, got {}",
data.len()
))
.into());
}
let value = U256(data.try_into().unwrap());
Ok(TypedEntry::TotalDifficulty { value })
}
TYPE_ACCUMULATOR => {
if data.len() != 32 {
return Err(E2StoreError::InvalidEra1(format!(
"total difficulty must be 32 bytes, got {}",
data.len()
))
.into());
}
let root: B256 = data.try_into().unwrap();
Ok(TypedEntry::BlockAccumulator { root })
}
_ => Ok(TypedEntry::Unknown { typ, data }),
}
}
pub struct EraReader<R> {
inner: E2StoreReader<R>,
format: Option<EraFormat>,
version_consumed: bool,
block_index_seen: bool,
}
impl<R: Read> EraReader<R> {
pub fn new(inner: R) -> Self {
Self {
inner: E2StoreReader::new(inner),
format: None,
version_consumed: false,
block_index_seen: false,
}
}
pub fn format(&self) -> Option<EraFormat> {
self.format
}
pub fn next_entry(&mut self) -> Result<Option<TypedEntry>, Error> {
loop {
let raw = match self.inner.next_entry() {
Ok(Some(e)) => e,
Ok(None) => {
if !self.version_consumed {
return Err(E2StoreError::MissingVersion.into());
}
return Ok(None);
}
Err(e) => return Err(Error::Io(e)),
};
if !self.version_consumed {
self.version_consumed = true;
if !raw.header.is_version() {
return Err(E2StoreError::MissingVersion.into());
}
continue; }
if self.format.is_none() {
self.format = Some(detect_format(&raw.header.typ));
}
let mut typed = decode_entry(raw)?;
if typed.entry_type() == TYPE_SLOT_INDEX {
if self.block_index_seen {
if let TypedEntry::BlockIndex { index } = typed {
typed = TypedEntry::StateIndex { index };
}
} else {
self.block_index_seen = true;
}
}
return Ok(Some(typed));
}
}
pub fn read_all(&mut self) -> Result<Vec<TypedEntry>, Error> {
let mut entries = Vec::new();
while let Some(entry) = self.next_entry()? {
entries.push(entry);
}
Ok(entries)
}
pub fn into_inner(self) -> R {
self.inner.into_inner()
}
}
#[derive(Debug, Clone)]
pub struct Era1Block {
pub header: Vec<u8>,
pub body: Vec<u8>,
pub receipts: Vec<u8>,
pub total_difficulty: U256,
}
#[derive(Debug, Clone)]
pub struct EraBlock {
pub block: Vec<u8>,
}
#[derive(Debug, Clone)]
pub enum Block {
Era(EraBlock),
Era1(Era1Block),
}
impl Block {
pub fn primary_data(&self) -> &[u8] {
match self {
Block::Era(b) => &b.block,
Block::Era1(b) => &b.header,
}
}
pub fn total_size(&self) -> usize {
match self {
Block::Era(b) => b.block.len(),
Block::Era1(b) => b.header.len() + b.body.len() + b.receipts.len() + 32,
}
}
fn from_raw(typ: &[u8; 2], data: Vec<u8>) -> Self {
match *typ {
TYPE_COMPRESSED_SIGNED_BEACON_BLOCK => Block::Era(EraBlock { block: data }),
TYPE_COMPRESSED_HEADER | TYPE_COMPRESSED_HEADER_WITH_PROOF => Block::Era1(Era1Block {
header: data,
body: Vec::new(),
receipts: Vec::new(),
total_difficulty: U256::zero(),
}),
_ => Block::Era(EraBlock { block: data }),
}
}
}
pub fn iter_blocks(data: &[u8]) -> Result<impl Iterator<Item = Block> + '_, Error> {
let mut pos = 0u64;
let mut index_pos = None;
let mut index_typ = [0u8; 2];
while pos < data.len() as u64 {
if pos + 8 > data.len() as u64 {
break;
}
let typ = [data[pos as usize], data[pos as usize + 1]];
let length = u32::from_le_bytes(
data[pos as usize + 2..pos as usize + 6]
.try_into()
.map_err(|_| Error::E2Store(E2StoreError::TruncatedHeader(0)))?,
);
let entry_end = pos + 8 + length as u64;
if typ == TYPE_SLOT_INDEX || typ == TYPE_BLOCK_INDEX {
index_pos = Some(pos);
index_typ = typ;
}
pos = entry_end;
}
let (idx_pos, idx_typ) = index_pos
.zip(Some(index_typ))
.ok_or(Error::E2Store(E2StoreError::MissingVersion))?;
let idx_data = &data[idx_pos as usize + 8..];
let idx =
SlotIndex::decode(idx_data).map_err(|e| Error::E2Store(E2StoreError::InvalidEra(e)))?;
let is_era1 = idx_typ == TYPE_BLOCK_INDEX;
let offsets = idx.offsets;
let mut slot_i = 0usize;
Ok(std::iter::from_fn(move || {
while slot_i < offsets.len() {
let offset = offsets[slot_i];
slot_i += 1;
if offset == 0 {
continue;
}
let abs = (idx_pos as i64 + offset) as u64;
if abs + 8 > data.len() as u64 {
continue;
}
let bytes: [u8; 4] = data[abs as usize + 2..abs as usize + 6].try_into().ok()?;
let entry_len = u32::from_le_bytes(bytes);
let entry_end = (abs + 8 + entry_len as u64) as usize;
if entry_end > data.len() {
continue;
}
let block_data = data[abs as usize + 8..entry_end].to_vec();
let typ = if is_era1 {
TYPE_COMPRESSED_HEADER
} else {
TYPE_COMPRESSED_SIGNED_BEACON_BLOCK
};
return Some(Block::from_raw(&typ, block_data));
}
None
})
.fuse())
}
pub struct EraBlockReader<R> {
inner: EraReader<R>,
format: Option<EraFormat>,
header: Option<Vec<u8>>,
body: Option<Vec<u8>>,
receipts: Option<Vec<u8>>,
total_difficulty: Option<U256>,
}
impl<R: Read> EraBlockReader<R> {
pub fn new(inner: R) -> Self {
Self {
inner: EraReader::new(inner),
format: None,
header: None,
body: None,
receipts: None,
total_difficulty: None,
}
}
pub fn format(&self) -> Option<EraFormat> {
self.format.or(self.inner.format())
}
pub fn next_block(&mut self) -> Result<Option<Block>, Error> {
loop {
let entry = match self.inner.next_entry()? {
Some(e) => e,
None => {
if self.format == Some(EraFormat::Era1) {
return Ok(self.flush_era1_block());
}
return Ok(None);
}
};
if self.format.is_none() {
self.format = self.inner.format();
}
match entry {
TypedEntry::BeaconBlock { data } => {
return Ok(Some(Block::Era(EraBlock { block: data })));
}
TypedEntry::Header { data } => {
let pending = self.flush_era1_block();
self.header = Some(data);
self.body = None;
self.body = None;
self.receipts = None;
self.total_difficulty = None;
if pending.is_some() {
return Ok(pending);
}
}
TypedEntry::BlockBody { data } => {
self.body = Some(data);
}
TypedEntry::Receipts { data } => {
self.receipts = Some(data);
}
TypedEntry::TotalDifficulty { value } => {
self.total_difficulty = Some(value);
}
TypedEntry::BeaconState { .. }
| TypedEntry::BlockIndex { .. }
| TypedEntry::StateIndex { .. }
| TypedEntry::BlockAccumulator { .. }
| TypedEntry::Unknown { .. } => {}
}
}
}
fn flush_era1_block(&mut self) -> Option<Block> {
let header = self.header.take()?;
let body = self.body.take().unwrap_or_default();
let receipts = self.receipts.take().unwrap_or_default();
let total_difficulty = self.total_difficulty.take().unwrap_or_else(U256::zero);
Some(Block::Era1(Era1Block {
header,
body,
receipts,
total_difficulty,
}))
}
pub fn into_inner(self) -> R {
self.inner.into_inner()
}
}
#[derive(Debug, Clone)]
pub enum SlotLookupError {
EmptySlot,
OutOfRange { slot: u64, start: u64, end: u64 },
IndexNotFound,
}
impl std::fmt::Display for SlotLookupError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SlotLookupError::EmptySlot => write!(f, "no block at this slot (skipped)"),
SlotLookupError::OutOfRange { slot, start, end } => {
write!(f, "slot {slot} outside index range [{start}, {end}]")
}
SlotLookupError::IndexNotFound => write!(f, "block index not found in file"),
}
}
}
impl std::error::Error for SlotLookupError {}
pub struct EraRandomReader<R> {
reader: R,
format: Option<EraFormat>,
block_index: Option<SlotIndex>,
block_index_positions: Vec<u64>,
}
impl<R: Read + Seek> EraRandomReader<R> {
pub fn new(mut reader: R) -> Result<Self, Error> {
let mut e2s = E2StoreReader::new(&mut reader);
let first = e2s
.next_entry()
.map_err(Error::Io)?
.ok_or(E2StoreError::MissingVersion)?;
if !first.header.is_version() {
return Err(E2StoreError::MissingVersion.into());
}
reader
.seek(std::io::SeekFrom::Start(0))
.map_err(Error::Io)?;
let mut scanner = E2StoreReader::new(&mut reader);
let mut format = None;
let mut block_index = None;
let mut index_positions: Vec<u64> = Vec::new();
let mut pos: u64 = 0;
while let Some(header) = scanner.next_header_only().map_err(Error::Io)? {
if format.is_none() && !header.is_version() {
format = Some(detect_format(&header.typ));
}
if header.typ == TYPE_SLOT_INDEX {
reader
.seek(std::io::SeekFrom::Start(pos))
.map_err(Error::Io)?;
let mut full_reader = E2StoreReader::new(&mut reader);
let entry = full_reader
.next_entry()
.map_err(Error::Io)?
.expect("index entry disappeared after seek");
block_index = Some(
SlotIndex::decode(&entry.data)
.map_err(|e| E2StoreError::InvalidEra(format!("bad block index: {e}")))?,
);
index_positions.push(pos);
break;
}
pos += Header::SIZE as u64 + header.length as u64;
}
Ok(Self {
reader,
format,
block_index,
block_index_positions: index_positions,
})
}
pub fn format(&self) -> Option<EraFormat> {
self.format
}
pub fn block_index(&self) -> Option<&SlotIndex> {
self.block_index.as_ref()
}
pub fn slot_count(&self) -> Option<usize> {
self.block_index.as_ref().map(|idx| idx.offsets.len())
}
pub fn starting_slot(&self) -> Option<u64> {
self.block_index.as_ref().map(|idx| idx.starting_slot)
}
pub fn slot_to_offset(&self, slot: u64) -> Result<u64, SlotLookupError> {
let idx = self
.block_index
.as_ref()
.ok_or(SlotLookupError::IndexNotFound)?;
let end = idx.starting_slot + idx.offsets.len() as u64;
if slot < idx.starting_slot || slot >= end {
return Err(SlotLookupError::OutOfRange {
slot,
start: idx.starting_slot,
end,
});
}
let i = (slot - idx.starting_slot) as usize;
let relative_offset = idx.offsets[i];
if relative_offset == 0 {
return Err(SlotLookupError::EmptySlot);
}
let index_start = self
.block_index_positions
.first()
.copied()
.ok_or(SlotLookupError::IndexNotFound)?;
let absolute = (index_start as i64 + relative_offset) as u64;
Ok(absolute)
}
pub fn read_block_at_slot(&mut self, slot: u64) -> Result<Option<Block>, Error> {
let offset = match self.slot_to_offset(slot) {
Ok(off) => off,
Err(SlotLookupError::EmptySlot) => return Ok(None),
Err(e) => return Err(Error::E2Store(E2StoreError::InvalidEra(e.to_string()))),
};
self.reader
.seek(std::io::SeekFrom::Start(offset))
.map_err(Error::Io)?;
let mut e2s = E2StoreReader::new(&mut self.reader);
let entry = e2s
.next_entry()
.map_err(Error::Io)?
.ok_or_else(|| E2StoreError::InvalidEra("unexpected EOF at indexed position".into()))?;
let typed = decode_entry(entry)?;
match typed {
TypedEntry::BeaconBlock { data } => Ok(Some(Block::Era(EraBlock { block: data }))),
TypedEntry::Header { data } => Ok(Some(Block::Era1(Era1Block {
header: data,
body: Vec::new(),
receipts: Vec::new(),
total_difficulty: U256::zero(),
}))),
other => Err(E2StoreError::InvalidEra(format!(
"expected block entry at slot {}, got {} ({:02x}{:02x})",
slot,
other.type_name(),
other.entry_type()[0],
other.entry_type()[1],
))
.into()),
}
}
pub fn read_state(&mut self) -> Result<Option<Vec<u8>>, Error> {
self.reader
.seek(std::io::SeekFrom::Start(0))
.map_err(Error::Io)?;
let mut e2s = E2StoreReader::new(&mut self.reader);
let first = e2s
.next_entry()
.map_err(Error::Io)?
.ok_or(E2StoreError::MissingVersion)?;
if !first.header.is_version() {
return Err(E2StoreError::MissingVersion.into());
}
while let Some(entry) = e2s.next_entry().map_err(Error::Io)? {
if entry.header.typ == TYPE_COMPRESSED_BEACON_STATE {
let decompressed = decompress_entry(&entry.data)?;
return Ok(Some(decompressed));
}
}
Ok(None)
}
pub fn into_inner(self) -> R {
self.reader
}
pub fn read_full_era1_block_at_slot(&mut self, slot: u64) -> Result<Option<Era1Block>, Error> {
let offset = match self.slot_to_offset(slot) {
Ok(off) => off,
Err(SlotLookupError::EmptySlot) => return Ok(None),
Err(e) => {
return Err(Error::E2Store(E2StoreError::InvalidEra(e.to_string())));
}
};
self.reader
.seek(std::io::SeekFrom::Start(offset))
.map_err(Error::Io)?;
let mut e2s = E2StoreReader::new(&mut self.reader);
let first = e2s
.next_entry()
.map_err(Error::Io)?
.ok_or_else(|| E2StoreError::InvalidEra("unexpected EOF at indexed position".into()))?;
if first.header.typ != TYPE_COMPRESSED_HEADER {
return Err(E2StoreError::InvalidEra(format!(
"expected header entry at slot {}, got {:02x}{:02x}",
slot, first.header.typ[0], first.header.typ[1]
))
.into());
}
let header = decompress_entry(&first.data)?;
let mut body = Vec::new();
let mut receipts = Vec::new();
let mut total_difficulty = U256::zero();
while let Some(entry) = e2s.next_entry().map_err(Error::Io)? {
match entry.header.typ {
TYPE_COMPRESSED_BODY => body = entry.data,
TYPE_COMPRESSED_RECEIPTS => receipts = entry.data,
TYPE_TOTAL_DIFFICULTY => {
if entry.data.len() == 32 {
total_difficulty = U256(entry.data.try_into().unwrap());
}
}
_ => break, }
}
Ok(Some(Era1Block {
header,
body,
receipts,
total_difficulty,
}))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
fn build_test_era1() -> Vec<u8> {
use crate::format::e2store::Entry;
use snap::write::FrameEncoder;
use std::io::Write;
let mut buf = Vec::new();
let version = Entry::version();
buf.extend_from_slice(&version.header.encode());
let fake_rlp_header = vec![0xf8, 0x40];
let compressed_header = {
let mut enc = FrameEncoder::new(Vec::new());
enc.write_all(&fake_rlp_header).unwrap();
enc.into_inner().unwrap()
};
let header_entry = Entry::new(TYPE_COMPRESSED_HEADER, compressed_header);
buf.extend_from_slice(&header_entry.header.encode());
buf.extend_from_slice(&header_entry.data);
let body_entry = Entry::new(TYPE_COMPRESSED_BODY, vec![0x01, 0x02, 0x03]);
buf.extend_from_slice(&body_entry.header.encode());
buf.extend_from_slice(&body_entry.data);
let receipts_entry = Entry::new(TYPE_COMPRESSED_RECEIPTS, vec![0x04, 0x05]);
buf.extend_from_slice(&receipts_entry.header.encode());
buf.extend_from_slice(&receipts_entry.data);
let mut td_bytes = [0u8; 32];
td_bytes[31] = 0x0f;
let td_entry = Entry::new(TYPE_TOTAL_DIFFICULTY, td_bytes.to_vec());
buf.extend_from_slice(&td_entry.header.encode());
buf.extend_from_slice(&td_entry.data);
let block_index = SlotIndex::new(0, vec![8i64]);
let idx_entry = Entry::new(TYPE_SLOT_INDEX, block_index.encode());
let idx_offset = buf.len() as i64;
buf.extend_from_slice(&idx_entry.header.encode());
buf.extend_from_slice(&idx_entry.data);
let correct_offset = 8i64 - idx_offset;
let fixed_index = SlotIndex::new(0, vec![correct_offset]);
buf.truncate(idx_offset as usize);
let idx_entry = Entry::new(TYPE_SLOT_INDEX, fixed_index.encode());
buf.extend_from_slice(&idx_entry.header.encode());
buf.extend_from_slice(&idx_entry.data);
buf
}
#[test]
fn era_reader_detects_era1_format() {
let data = build_test_era1();
let mut reader = EraReader::new(Cursor::new(data));
assert!(reader.format().is_none());
let first = reader.next_entry().unwrap().unwrap();
assert_eq!(reader.format(), Some(EraFormat::Era1));
assert!(matches!(first, TypedEntry::Header { .. }));
}
#[test]
fn era_reader_yields_all_typed_entries() {
let data = build_test_era1();
let mut reader = EraReader::new(Cursor::new(data));
let entries = reader.read_all().unwrap();
assert_eq!(entries.len(), 5); assert!(matches!(&entries[0], TypedEntry::Header { .. }));
assert!(matches!(&entries[1], TypedEntry::BlockBody { .. }));
assert!(matches!(&entries[2], TypedEntry::Receipts { .. }));
assert!(matches!(&entries[3], TypedEntry::TotalDifficulty { .. }));
assert!(matches!(&entries[4], TypedEntry::BlockIndex { .. }));
}
#[test]
fn era_reader_rejects_missing_version() {
let buf = [0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0xAB, 0xCD];
let mut reader = EraReader::new(Cursor::new(&buf[..]));
let err = reader.next_entry().unwrap_err();
assert!(err.to_string().contains("missing version"));
}
#[test]
fn era_reader_handles_empty_file() {
let mut reader = EraReader::new(Cursor::new(&[][..]));
let err = reader.next_entry().unwrap_err();
assert!(err.to_string().contains("missing version"));
}
#[test]
fn era_block_reader_assembles_era1_block() {
let data = build_test_era1();
let mut reader = EraBlockReader::new(Cursor::new(data));
let block = reader.next_block().unwrap().unwrap();
assert!(matches!(block, Block::Era1(_)));
if let Block::Era1(b) = block {
assert_eq!(b.header.len(), 2); assert_eq!(b.body, vec![0x01, 0x02, 0x03]);
assert_eq!(b.receipts, vec![0x04, 0x05]);
assert_eq!(b.total_difficulty.0[31], 0x0f);
}
assert!(reader.next_block().unwrap().is_none());
}
#[test]
fn era_random_reader_finds_index() {
let data = build_test_era1();
let reader = EraRandomReader::new(Cursor::new(data)).unwrap();
assert_eq!(reader.format(), Some(EraFormat::Era1));
assert_eq!(reader.slot_count(), Some(1));
assert_eq!(reader.starting_slot(), Some(0));
}
#[test]
fn era_random_reader_reads_slot_zero() {
let data = build_test_era1();
let mut reader = EraRandomReader::new(Cursor::new(data)).unwrap();
let block = reader.read_block_at_slot(0).unwrap().unwrap();
assert_eq!(block.primary_data().len(), 2);
}
#[test]
fn era_random_reader_empty_slot() {
let data = build_test_era1();
let reader = EraRandomReader::new(Cursor::new(data)).unwrap();
let err = reader.slot_to_offset(1).unwrap_err();
assert!(matches!(err, SlotLookupError::OutOfRange { .. }));
}
#[test]
fn slot_lookup_error_display() {
let e = SlotLookupError::EmptySlot;
assert!(!e.to_string().is_empty());
let e = SlotLookupError::OutOfRange {
slot: 100,
start: 0,
end: 50,
};
assert!(e.to_string().contains("100"));
let e = SlotLookupError::IndexNotFound;
assert!(e.to_string().contains("not found"));
}
#[test]
fn typed_entry_type_name() {
let e = TypedEntry::Header { data: vec![] };
assert_eq!(e.type_name(), "Header");
let e = TypedEntry::BeaconBlock { data: vec![] };
assert_eq!(e.type_name(), "BeaconBlock");
let e = TypedEntry::Unknown {
typ: [0xFF, 0xFF],
data: vec![],
};
assert_eq!(e.type_name(), "Unknown");
}
#[test]
fn typed_entry_data_len() {
let e = TypedEntry::TotalDifficulty {
value: U256::zero(),
};
assert_eq!(e.data_len(), 32);
let e = TypedEntry::BlockBody { data: vec![0; 100] };
assert_eq!(e.data_len(), 100);
}
#[test]
fn block_total_size() {
let era_block = Block::Era(EraBlock {
block: vec![0; 500],
});
assert_eq!(era_block.total_size(), 500);
let era1_block = Block::Era1(Era1Block {
header: vec![0; 100],
body: vec![0; 200],
receipts: vec![0; 50],
total_difficulty: U256::zero(),
});
assert_eq!(era1_block.total_size(), 382); }
#[test]
fn block_primary_data() {
let era = Block::Era(EraBlock {
block: vec![1, 2, 3],
});
assert_eq!(era.primary_data(), &[1, 2, 3]);
let era1 = Block::Era1(Era1Block {
header: vec![4, 5, 6],
body: vec![7, 8],
receipts: vec![],
total_difficulty: U256::zero(),
});
assert_eq!(era1.primary_data(), &[4, 5, 6]);
}
#[test]
fn era_format_display() {
assert_eq!(EraFormat::Era.to_string(), "ERA");
assert_eq!(EraFormat::Era1.to_string(), "ERA1");
}
#[test]
fn unknown_entry_preserved() {
let mut buf = Vec::new();
buf.extend_from_slice(&[0x65, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
buf.extend_from_slice(&[0xFF, 0xEE, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00]);
buf.extend_from_slice(&[0xAA, 0xBB, 0xCC]);
let mut reader = EraReader::new(Cursor::new(buf));
let entry = reader.next_entry().unwrap().unwrap();
assert!(matches!(entry, TypedEntry::Unknown { .. }));
if let TypedEntry::Unknown { typ, data } = entry {
assert_eq!(typ, [0xFF, 0xEE]);
assert_eq!(data, vec![0xAA, 0xBB, 0xCC]);
}
}
#[test]
fn total_difficulty_rejects_wrong_size() {
let mut buf = Vec::new();
buf.extend_from_slice(&[0x65, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
buf.extend_from_slice(&[0x06, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00]);
buf.extend_from_slice(&[0u8; 16]);
let mut reader = EraReader::new(Cursor::new(buf));
let err = reader.next_entry().unwrap_err();
assert!(err.to_string().contains("32 bytes"));
}
#[test]
fn block_accumulator_rejects_wrong_size() {
let mut buf = Vec::new();
buf.extend_from_slice(&[0x65, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
buf.extend_from_slice(&[0x07, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00]);
buf.extend_from_slice(&[0u8; 16]);
let mut reader = EraReader::new(Cursor::new(buf));
let err = reader.next_entry().unwrap_err();
assert!(err.to_string().contains("32 bytes"));
}
}