use crate::compare::HashKind;
use crate::dump::StateDump;
use crate::format::{Chunk, FormatError, Header, RecReader, RecWriter};
use crate::probe::DeterminismProbe;
use std::collections::{BTreeMap, BTreeSet};
use std::io::{BufReader, BufWriter, Read, Seek, Write};
use std::path::Path;
#[derive(Debug)]
pub enum ReplayError {
Format(FormatError),
InputFormatMismatch {
recorded: u64,
expected: u64,
},
HashMismatch {
tick: u64,
kind: HashKind,
recorded: u64,
actual: u64,
},
TickOutOfRange {
tick: u64,
first: u64,
last: u64,
},
EmptyRecording,
NoPendingStep,
StepSkipped {
tick: u64,
},
}
impl std::fmt::Display for ReplayError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Format(err) => write!(f, "{err}"),
Self::InputFormatMismatch { recorded, expected } => write!(
f,
"input format mismatch: the recording declares id {recorded} but this build \
expects id {expected}, replaying would feed misinterpreted inputs"
),
Self::HashMismatch {
tick,
kind,
recorded,
actual,
} => write!(
f,
"replay diverged from the recording at tick {tick}: {} hash recorded \
{recorded:016x}, replay produced {actual:016x}. Your simulation is not \
reproducing the session, run the self-check before hunting cross-client desyncs",
match kind {
HashKind::Light => "light",
HashKind::Full => "full",
}
),
Self::TickOutOfRange { tick, first, last } => write!(
f,
"tick {tick} is outside the recording, which covers ticks {first} to {last}"
),
Self::EmptyRecording => write!(f, "the recording holds no ticks"),
Self::NoPendingStep => write!(
f,
"after_tick called without a pending step, call next_step first"
),
Self::StepSkipped { tick } => write!(
f,
"next_step was called again before after_tick for tick {tick}, \
every step needs exactly one after_tick"
),
}
}
}
impl std::error::Error for ReplayError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Format(err) => Some(err),
_ => None,
}
}
}
impl From<FormatError> for ReplayError {
fn from(err: FormatError) -> Self {
Self::Format(err)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ReplayConfig {
pub dump_at_ticks: Vec<u64>,
pub verify_hashes: bool,
pub expected_input_format_id: Option<u64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Step<'a> {
tick: u64,
inputs: &'a [u8],
}
impl<'a> Step<'a> {
pub fn tick(&self) -> u64 {
self.tick
}
pub fn inputs(&self) -> &[u8] {
self.inputs
}
#[cfg(feature = "serde")]
pub fn inputs_typed<I: serde::Deserialize<'a>>(&self) -> Result<I, postcard::Error> {
postcard::from_bytes(self.inputs)
}
}
pub struct Replayer {
header: Header,
tick_count: u64,
frames: Vec<(u64, Vec<u8>)>,
light: BTreeMap<u64, u64>,
full: BTreeMap<u64, u64>,
snapshots: Vec<(u64, Vec<u8>)>,
first_tick: u64,
last_tick: u64,
next_tick: u64,
frame_index: usize,
pending: Option<u64>,
skipped: Option<u64>,
dump_ticks: BTreeSet<u64>,
verify_hashes: bool,
dumps: Vec<(u64, StateDump)>,
}
impl Replayer {
pub fn open<P: AsRef<Path>>(path: P, config: ReplayConfig) -> Result<Self, ReplayError> {
let file = std::fs::File::open(path).map_err(FormatError::from)?;
let mut reader = RecReader::open(BufReader::new(file))?;
Self::from_reader(&mut reader, config)
}
pub fn from_reader<R: Read + Seek>(
reader: &mut RecReader<R>,
config: ReplayConfig,
) -> Result<Self, ReplayError> {
let header = reader.header().clone();
if let Some(expected) = config.expected_input_format_id
&& header.config.input_format_id != expected
{
return Err(ReplayError::InputFormatMismatch {
recorded: header.config.input_format_id,
expected,
});
}
let mut frames = Vec::new();
let mut light = BTreeMap::new();
let mut full = BTreeMap::new();
let mut snapshots = Vec::new();
for item in reader.chunks()? {
match item? {
Chunk::InputFrame { tick, data } => frames.push((tick, data)),
Chunk::LightHashBatch { first_tick, hashes } => {
for (offset, hash) in hashes.iter().enumerate() {
light.insert(first_tick + offset as u64, *hash);
}
}
Chunk::FullHash { tick, hash } => {
full.insert(tick, hash);
}
Chunk::Snapshot { tick, data } => snapshots.push((tick, data)),
_ => {}
}
}
frames.sort_by_key(|(tick, _)| *tick);
snapshots.sort_by_key(|(tick, _)| *tick);
let (first_tick, last_tick) = match (light.keys().next(), light.keys().next_back()) {
(Some(first), Some(last)) => (*first, *last),
_ => return Err(ReplayError::EmptyRecording),
};
let dump_ticks: BTreeSet<u64> = config.dump_at_ticks.iter().copied().collect();
for tick in &dump_ticks {
if *tick < first_tick || *tick > last_tick {
return Err(ReplayError::TickOutOfRange {
tick: *tick,
first: first_tick,
last: last_tick,
});
}
}
Ok(Self {
header,
tick_count: reader.tick_count(),
frames,
light,
full,
snapshots,
first_tick,
last_tick,
next_tick: first_tick,
frame_index: 0,
pending: None,
skipped: None,
dump_ticks,
verify_hashes: config.verify_hashes,
dumps: Vec::new(),
})
}
pub fn header(&self) -> &Header {
&self.header
}
pub fn tick_range(&self) -> (u64, u64) {
(self.first_tick, self.last_tick)
}
pub fn upcoming_tick(&self) -> Option<u64> {
(self.next_tick <= self.last_tick).then_some(self.next_tick)
}
pub fn snapshot_ticks(&self) -> Vec<u64> {
self.snapshots.iter().map(|(tick, _)| *tick).collect()
}
pub fn nearest_snapshot_before(&self, tick: u64) -> Option<(u64, &[u8])> {
self.snapshots
.iter()
.rev()
.find(|(t, _)| *t <= tick)
.map(|(t, data)| (*t, data.as_slice()))
}
pub fn seek_to(&mut self, tick: u64) -> Result<(), ReplayError> {
if tick < self.first_tick || tick > self.last_tick + 1 {
return Err(ReplayError::TickOutOfRange {
tick,
first: self.first_tick,
last: self.last_tick,
});
}
self.next_tick = tick;
self.frame_index = 0;
self.pending = None;
Ok(())
}
pub fn next_step(&mut self) -> Option<Step<'_>> {
if self.next_tick > self.last_tick {
return None;
}
if let Some(pending) = self.pending
&& self.skipped.is_none()
{
self.skipped = Some(pending);
}
let tick = self.next_tick;
while self.frame_index + 1 < self.frames.len()
&& self.frames[self.frame_index + 1].0 <= tick
{
self.frame_index += 1;
}
let inputs = match self.frames.get(self.frame_index) {
Some((frame_tick, data)) if *frame_tick <= tick => data.as_slice(),
_ => &[],
};
self.pending = Some(tick);
self.next_tick = tick + 1;
Some(Step { tick, inputs })
}
pub fn after_tick(&mut self, probe: &dyn DeterminismProbe) -> Result<(), ReplayError> {
let tick = self.pending.take().ok_or(ReplayError::NoPendingStep)?;
if self.dump_ticks.contains(&tick) {
self.dumps.push((tick, probe.state_dump()));
}
if self.verify_hashes {
if let Some(recorded) = self.light.get(&tick) {
let actual = probe.light_hash();
if actual != *recorded {
return Err(ReplayError::HashMismatch {
tick,
kind: HashKind::Light,
recorded: *recorded,
actual,
});
}
}
if let Some(recorded) = self.full.get(&tick) {
let actual = probe.full_hash();
if actual != *recorded {
return Err(ReplayError::HashMismatch {
tick,
kind: HashKind::Full,
recorded: *recorded,
actual,
});
}
}
}
Ok(())
}
fn check_protocol(&self) -> Result<(), ReplayError> {
if let Some(tick) = self.skipped.or(self.pending) {
return Err(ReplayError::StepSkipped { tick });
}
Ok(())
}
pub fn into_dumps(self) -> Result<Vec<(u64, StateDump)>, ReplayError> {
self.check_protocol()?;
Ok(self.dumps)
}
pub fn finish<P: AsRef<Path>>(self, path: P) -> Result<(), ReplayError> {
let file = std::fs::File::create(path).map_err(FormatError::from)?;
self.finish_into(BufWriter::new(file))?;
Ok(())
}
pub fn finish_into<W: Write>(self, sink: W) -> Result<W, ReplayError> {
self.check_protocol()?;
let mut writer = RecWriter::new(sink, &self.header)?;
for (tick, dump) in &self.dumps {
writer.write_chunk(&Chunk::StateDump {
tick: *tick,
dump: dump.clone(),
})?;
}
Ok(writer.finish(self.tick_count)?)
}
}