use std::error::Error;
use super::restore_checkpoint::{
CheckpointError, CheckpointLiveState, CheckpointPositions, CheckpointSchema, RestoreCheckpoint,
};
#[derive(Debug, PartialEq, Eq)]
struct LiveBytes(Vec<u8>);
impl CheckpointLiveState for LiveBytes {
type RestoreContext = u8;
fn encode_live(&self) -> Result<Vec<u8>, CheckpointError> {
Ok(self.0.clone())
}
fn decode_live(
bytes: &[u8],
positions: &CheckpointPositions,
history_root: &[u8],
context: &Self::RestoreContext,
) -> Result<Self, CheckpointError> {
let live = Self(bytes.to_vec());
if live.0.first() != Some(context) {
return Err(CheckpointError::LiveState {
source: std::io::Error::other("configured live tag differs").into(),
});
}
live.validate_live(positions, history_root)?;
Ok(live)
}
fn validate_live(
&self,
positions: &CheckpointPositions,
history_root: &[u8],
) -> Result<(), CheckpointError> {
if history_root != [9; 32] {
return Err(CheckpointError::LiveState {
source: std::io::Error::other("fixture root differs").into(),
});
}
if self.0.first().copied() != Some(7) || positions.conversation_id != 7 {
return Err(CheckpointError::LiveState {
source: std::io::Error::other("fixture identity differs from its live owner")
.into(),
});
}
Ok(())
}
}
fn positions(history: u64) -> CheckpointPositions {
CheckpointPositions {
conversation_id: 7,
covered_log_sequence: history,
next_event_ordinal: history + 1,
next_log_sequence: history + 1,
next_seq: history + 1,
next_order: history,
next_participant: 2,
extension_sequence: history,
highest_delivery_seq: history,
observer_progress: history,
merged_base_sequence: history + 1,
merged_extension_sequence: history,
schema: CheckpointSchema::V4Suffix,
}
}
fn checkpoint(history: u64) -> Result<RestoreCheckpoint<LiveBytes, 32>, CheckpointError> {
RestoreCheckpoint::new(positions(history), [9; 32], LiveBytes(vec![7, 1, 4]))
}
#[test]
fn checkpoint_round_trip_preserves_every_position_root_and_live_byte() -> Result<(), Box<dyn Error>>
{
let original = checkpoint(1_000)?;
let encoded = original.encode_canonical()?;
let restored = RestoreCheckpoint::<LiveBytes, 32>::decode_canonical(&encoded, 7, &7)?;
assert_eq!(original, restored);
assert_eq!(restored.encode_canonical()?, encoded);
let (saved, root, live) = restored.into_parts();
assert_eq!(saved, positions(1_000));
assert_eq!(root, [9; 32]);
assert_eq!(live, LiveBytes(vec![7, 1, 4]));
Ok(())
}
#[test]
fn live_bytes_and_fixed_position_length_do_not_grow_with_history() -> Result<(), Box<dyn Error>> {
let mut lengths = Vec::new();
for history in [1_000, 42_005, 420_000] {
let saved = checkpoint(history)?;
assert_eq!(saved.live().0, [7, 1, 4]);
lengths.push(saved.encode_canonical()?.len());
}
assert!(lengths.windows(2).all(|pair| pair[0] == pair[1]));
assert_eq!(
checkpoint(42_005)?.encode_canonical()?,
checkpoint(42_005)?.encode_canonical()?
);
let wrong_varint_lengths = [1_000_u64, 420_000].map(|value| {
let mut value = value;
let mut length = 1;
while value >= 128 {
value >>= 7;
length += 1;
}
length
});
assert_ne!(wrong_varint_lengths[0], wrong_varint_lengths[1]);
Ok(())
}
#[test]
fn every_truncation_and_trailing_byte_is_refused() -> Result<(), Box<dyn Error>> {
let bytes = checkpoint(1_000)?.encode_canonical()?;
for end in 0..bytes.len() {
assert!(
RestoreCheckpoint::<LiveBytes, 32>::decode_canonical(&bytes[..end], 7, &7).is_err()
);
}
let mut trailing = bytes;
trailing.push(0);
assert!(RestoreCheckpoint::<LiveBytes, 32>::decode_canonical(&trailing, 7, &7).is_err());
Ok(())
}
#[test]
fn every_changed_byte_fails_integrity_or_canonical_validation() -> Result<(), Box<dyn Error>> {
let bytes = checkpoint(1_000)?.encode_canonical()?;
for at in 0..bytes.len() {
let mut changed = bytes.clone();
changed[at] ^= 1;
assert!(RestoreCheckpoint::<LiveBytes, 32>::decode_canonical(&changed, 7, &7).is_err());
}
Ok(())
}
#[test]
fn a_valid_checksum_does_not_bypass_the_live_owner_validator() -> Result<(), Box<dyn Error>> {
let mut bytes = checkpoint(1_000)?.encode_canonical()?;
let live_at = bytes.len() - 32 - 3;
bytes[live_at] = 8;
let digest_at = bytes.len() - 32;
let digest = *blake3::hash(&bytes[..digest_at]).as_bytes();
bytes[digest_at..].copy_from_slice(&digest);
assert!(matches!(
RestoreCheckpoint::<LiveBytes, 32>::decode_canonical(&bytes, 7, &7),
Err(CheckpointError::LiveState { .. })
));
Ok(())
}
#[test]
fn foreign_identity_and_root_schema_are_refused() -> Result<(), Box<dyn Error>> {
let bytes = checkpoint(1_000)?.encode_canonical()?;
assert!(matches!(
RestoreCheckpoint::<LiveBytes, 32>::decode_canonical(&bytes, 8, &7),
Err(CheckpointError::ForeignConversation {
expected: 8,
actual: 7
})
));
assert!(matches!(
RestoreCheckpoint::<LiveBytes, 16>::decode_canonical(&bytes, 7, &7),
Err(CheckpointError::RootWidth {
expected: 16,
actual: 32
})
));
Ok(())
}
#[test]
fn invalid_heads_and_zero_root_width_are_refused_before_encoding() {
let mut invalid = positions(1_000);
invalid.next_log_sequence = invalid.covered_log_sequence;
assert!(matches!(
RestoreCheckpoint::new(invalid, [9; 32], LiveBytes(vec![7])),
Err(CheckpointError::Position { .. })
));
assert!(matches!(
RestoreCheckpoint::new(positions(1_000), [], LiveBytes(vec![7])),
Err(CheckpointError::RootWidth {
expected: 0,
actual: 0
})
));
}
fn reseal(bytes: &mut [u8]) -> Result<(), Box<dyn Error>> {
let at = bytes.len().checked_sub(32).ok_or("fixture has no digest")?;
let digest = *blake3::hash(&bytes[..at]).as_bytes();
bytes[at..].copy_from_slice(&digest);
Ok(())
}
#[test]
fn checksummed_invalid_positions_schema_and_sections_retain_named_refusals()
-> Result<(), Box<dyn Error>> {
let original = checkpoint(1_000)?.encode_canonical()?;
let mut bad_head = original.clone();
bad_head[38..46].copy_from_slice(&1_000_u64.to_be_bytes());
reseal(&mut bad_head)?;
assert!(matches!(
RestoreCheckpoint::<LiveBytes, 32>::decode_canonical(&bad_head, 7, &7),
Err(CheckpointError::Position {
field: "next log sequence"
})
));
let mut bad_schema = original.clone();
bad_schema[110] = 5;
reseal(&mut bad_schema)?;
assert!(matches!(
RestoreCheckpoint::<LiveBytes, 32>::decode_canonical(&bad_schema, 7, &7),
Err(CheckpointError::Schema { tag: 5 })
));
let mut bad_section = original;
bad_section[145..147].copy_from_slice(&3_u16.to_be_bytes());
reseal(&mut bad_section)?;
assert!(matches!(
RestoreCheckpoint::<LiveBytes, 32>::decode_canonical(&bad_section, 7, &7),
Err(CheckpointError::Section { tag: 2 })
));
Ok(())
}
#[derive(Debug)]
struct LooseLive(LiveBytes);
impl CheckpointLiveState for LooseLive {
type RestoreContext = u8;
fn encode_live(&self) -> Result<Vec<u8>, CheckpointError> {
self.0.encode_live()
}
fn decode_live(
bytes: &[u8],
positions: &CheckpointPositions,
history_root: &[u8],
context: &Self::RestoreContext,
) -> Result<Self, CheckpointError> {
let decoded = LiveBytes::decode_live(bytes, positions, history_root, context)?;
let first = decoded
.0
.first()
.copied()
.ok_or(CheckpointError::Truncated {
required: 1,
available: decoded.0.len(),
})?;
Ok(Self(LiveBytes(vec![first])))
}
fn validate_live(
&self,
positions: &CheckpointPositions,
history_root: &[u8],
) -> Result<(), CheckpointError> {
self.0.validate_live(positions, history_root)
}
}
#[test]
fn an_owner_decoder_that_discards_unknown_live_bytes_cannot_install_a_checkpoint()
-> Result<(), Box<dyn Error>> {
let checkpoint =
RestoreCheckpoint::new(positions(1_000), [9; 32], LooseLive(LiveBytes(vec![7])))?;
let mut encoded = checkpoint.encode_canonical()?;
let extra_at = encoded.len() - 32;
encoded.insert(extra_at, 1);
encoded[147..151].copy_from_slice(&2_u32.to_be_bytes());
reseal(&mut encoded)?;
assert!(matches!(
RestoreCheckpoint::<LooseLive, 32>::decode_canonical(&encoded, 7, &7),
Err(CheckpointError::LiveEncoding)
));
Ok(())
}
#[derive(Debug)]
struct HistoryLookupLive(Vec<u8>);
impl CheckpointLiveState for HistoryLookupLive {
type RestoreContext = std::cell::RefCell<Vec<&'static str>>;
fn encode_live(&self) -> Result<Vec<u8>, CheckpointError> {
Ok(self.0.clone())
}
fn decode_live(
bytes: &[u8],
positions: &CheckpointPositions,
history_root: &[u8],
context: &Self::RestoreContext,
) -> Result<Self, CheckpointError> {
context.borrow_mut().push("history lookup");
let owner = Self(bytes.to_vec());
owner.validate_live(positions, history_root)?;
Ok(owner)
}
fn validate_live(
&self,
positions: &CheckpointPositions,
history_root: &[u8],
) -> Result<(), CheckpointError> {
if positions.conversation_id != 7 || history_root.len() != 32 || self.0 != [7] {
return Err(CheckpointError::LiveEncoding);
}
Ok(())
}
}
#[test]
fn checkpoint_root_association_precedes_any_owner_history_lookup() -> Result<(), Box<dyn Error>> {
let encoded = RestoreCheckpoint::new(positions(1_000), [9; 32], HistoryLookupLive(vec![7]))?
.encode_canonical()?;
let expected_root = [8; 32];
let calls = std::cell::RefCell::new(Vec::new());
let result = RestoreCheckpoint::<HistoryLookupLive, 32>::decode_canonical_with_association(
&encoded,
7,
&calls,
|_, root| {
if root != &expected_root {
return Err(CheckpointError::Association {
field: "history root",
});
}
Ok(())
},
);
assert!(
calls.borrow().is_empty(),
"unassociated root triggered a history lookup"
);
assert!(matches!(
result,
Err(CheckpointError::Association {
field: "history root"
})
));
Ok(())
}
#[test]
fn associated_checkpoint_decodes_the_owner_once_and_in_the_required_order()
-> Result<(), Box<dyn Error>> {
let encoded = RestoreCheckpoint::new(positions(1_000), [9; 32], HistoryLookupLive(vec![7]))?
.encode_canonical()?;
let calls = std::cell::RefCell::new(Vec::new());
let restored = RestoreCheckpoint::<HistoryLookupLive, 32>::decode_canonical_with_association(
&encoded,
7,
&calls,
|saved, root| {
assert_eq!(*saved, positions(1_000));
assert_eq!(*root, [9; 32]);
calls.borrow_mut().push("association");
Ok(())
},
)?;
assert_eq!(*calls.borrow(), ["association", "history lookup"]);
assert_eq!(restored.live().0, [7]);
Ok(())
}
#[test]
fn malformed_checkpoint_never_calls_association_or_the_owner() -> Result<(), Box<dyn Error>> {
let mut encoded =
RestoreCheckpoint::new(positions(1_000), [9; 32], HistoryLookupLive(vec![7]))?
.encode_canonical()?;
let last = encoded.len() - 1;
encoded[last] ^= 1;
let calls = std::cell::RefCell::new(Vec::new());
let result = RestoreCheckpoint::<HistoryLookupLive, 32>::decode_canonical_with_association(
&encoded,
7,
&calls,
|_, _| {
calls.borrow_mut().push("association");
Ok(())
},
);
assert!(matches!(result, Err(CheckpointError::Integrity)));
assert!(calls.borrow().is_empty());
Ok(())
}
#[test]
fn post_decode_association_is_a_positive_control_for_the_wrong_lookup_order()
-> Result<(), Box<dyn Error>> {
let encoded = RestoreCheckpoint::new(positions(1_000), [9; 32], HistoryLookupLive(vec![7]))?
.encode_canonical()?;
let calls = std::cell::RefCell::new(Vec::new());
let result = RestoreCheckpoint::<HistoryLookupLive, 32>::decode_canonical(&encoded, 7, &calls)
.and_then(|saved| {
if saved.history_root() != &[8; 32] {
return Err(CheckpointError::Association {
field: "history root",
});
}
Ok(saved)
});
assert!(matches!(
result,
Err(CheckpointError::Association {
field: "history root"
})
));
assert_eq!(*calls.borrow(), ["history lookup"]);
Ok(())
}