use std::borrow::Cow;
use borsh::{BorshDeserialize, BorshSerialize};
use calimero_crypto::Nonce;
use calimero_network_primitives::specialized_node_invite::SpecializedNodeType;
use calimero_primitives::context::ContextId;
use calimero_primitives::hash::Hash;
use calimero_primitives::identity::PublicKey;
use super::hash_comparison::LeafMetadata;
pub const DEFAULT_SNAPSHOT_PAGE_SIZE: u32 = 256 * 1024;
pub const MAX_SNAPSHOT_PAGE_SIZE: u32 = 4 * 1024 * 1024;
pub const MAX_ENTITIES_PER_PAGE: usize = 1_000;
pub const MAX_SNAPSHOT_PAGES: usize = 10_000;
pub const MAX_ENTITY_DATA_SIZE: usize = 1_048_576;
pub const MAX_DAG_HEADS: usize = 100;
pub const MAX_COMPRESSED_PAYLOAD_SIZE: usize = 8 * 1024 * 1024;
#[derive(Clone, Debug, PartialEq, BorshSerialize, BorshDeserialize)]
pub struct SnapshotBoundaryRequest {
pub context_id: ContextId,
pub requested_cutoff_timestamp: Option<u64>,
}
#[derive(Clone, Debug, PartialEq, BorshSerialize, BorshDeserialize)]
pub struct SnapshotBoundaryResponse {
pub boundary_timestamp: u64,
pub boundary_root_hash: Hash,
pub dag_heads: Vec<[u8; 32]>,
}
impl SnapshotBoundaryResponse {
#[must_use]
pub fn is_valid(&self) -> bool {
self.dag_heads.len() <= MAX_DAG_HEADS
}
}
#[derive(Clone, Debug, PartialEq, BorshSerialize, BorshDeserialize)]
pub struct SnapshotStreamRequest {
pub context_id: ContextId,
pub boundary_root_hash: Hash,
pub page_limit: u16,
pub byte_limit: u32,
pub resume_cursor: Option<Vec<u8>>,
}
impl SnapshotStreamRequest {
#[must_use]
pub fn validated_byte_limit(&self) -> u32 {
if self.byte_limit == 0 {
DEFAULT_SNAPSHOT_PAGE_SIZE
} else {
self.byte_limit.min(MAX_SNAPSHOT_PAGE_SIZE)
}
}
}
#[derive(Clone, Debug, PartialEq, BorshSerialize, BorshDeserialize)]
pub struct SnapshotPage {
pub payload: Vec<u8>,
pub uncompressed_len: u32,
pub cursor: Option<Vec<u8>>,
pub page_count: u64,
pub sent_count: u64,
}
impl SnapshotPage {
#[must_use]
pub fn is_last(&self) -> bool {
self.cursor.is_none()
}
#[must_use]
pub fn is_valid(&self) -> bool {
self.uncompressed_len <= MAX_SNAPSHOT_PAGE_SIZE
&& self.page_count <= MAX_SNAPSHOT_PAGES as u64
&& self.sent_count <= self.page_count
&& self.payload.len() <= MAX_COMPRESSED_PAYLOAD_SIZE
}
}
#[derive(Clone, Debug, PartialEq, BorshSerialize, BorshDeserialize)]
pub struct SnapshotCursor {
pub last_key: [u8; 32],
}
#[derive(Clone, Debug, PartialEq, BorshSerialize, BorshDeserialize)]
pub struct SnapshotRequest {
pub compressed: bool,
pub max_page_size: u32,
pub is_fresh_node: bool,
}
impl SnapshotRequest {
#[must_use]
pub fn compressed() -> Self {
Self {
compressed: true,
max_page_size: 0,
is_fresh_node: true,
}
}
#[must_use]
pub fn uncompressed() -> Self {
Self {
compressed: false,
max_page_size: 0,
is_fresh_node: true,
}
}
#[must_use]
pub fn with_max_page_size(mut self, size: u32) -> Self {
self.max_page_size = size;
self
}
#[must_use]
pub fn validated_page_size(&self) -> u32 {
if self.max_page_size == 0 {
DEFAULT_SNAPSHOT_PAGE_SIZE
} else {
self.max_page_size.min(MAX_SNAPSHOT_PAGE_SIZE)
}
}
}
#[derive(Clone, Debug, PartialEq, BorshSerialize, BorshDeserialize)]
pub struct SnapshotEntity {
pub id: [u8; 32],
pub data: Vec<u8>,
pub metadata: LeafMetadata,
pub collection_id: [u8; 32],
pub parent_id: Option<[u8; 32]>,
}
impl SnapshotEntity {
#[must_use]
pub fn new(
id: [u8; 32],
data: Vec<u8>,
metadata: LeafMetadata,
collection_id: [u8; 32],
) -> Self {
Self {
id,
data,
metadata,
collection_id,
parent_id: None,
}
}
#[must_use]
pub fn with_parent(mut self, parent_id: [u8; 32]) -> Self {
self.parent_id = Some(parent_id);
self
}
#[must_use]
pub fn is_root(&self) -> bool {
self.parent_id.is_none()
}
#[must_use]
pub fn is_valid(&self) -> bool {
self.data.len() <= MAX_ENTITY_DATA_SIZE
}
}
#[derive(Clone, Debug, PartialEq, BorshSerialize, BorshDeserialize)]
pub struct SnapshotEntityPage {
pub page_number: usize,
pub total_pages: usize,
pub entities: Vec<SnapshotEntity>,
pub is_last: bool,
}
impl SnapshotEntityPage {
#[must_use]
pub fn new(
page_number: usize,
total_pages: usize,
entities: Vec<SnapshotEntity>,
is_last: bool,
) -> Self {
Self {
page_number,
total_pages,
entities,
is_last,
}
}
#[must_use]
pub fn entity_count(&self) -> usize {
self.entities.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entities.is_empty()
}
#[must_use]
pub fn is_valid(&self) -> bool {
if self.entities.len() > MAX_ENTITIES_PER_PAGE {
return false;
}
if self.total_pages > MAX_SNAPSHOT_PAGES {
return false;
}
if self.total_pages > 0 && self.page_number >= self.total_pages {
return false;
}
if self.is_last && self.total_pages > 0 && self.page_number + 1 != self.total_pages {
return false;
}
self.entities.iter().all(SnapshotEntity::is_valid)
}
}
#[derive(Clone, Debug, PartialEq, BorshSerialize, BorshDeserialize)]
pub struct SnapshotComplete {
pub root_hash: [u8; 32],
pub total_entities: usize,
pub total_pages: usize,
pub uncompressed_size: u64,
pub compressed_size: Option<u64>,
pub dag_heads: Vec<[u8; 32]>,
}
impl SnapshotComplete {
#[must_use]
pub fn new(
root_hash: [u8; 32],
total_entities: usize,
total_pages: usize,
uncompressed_size: u64,
) -> Self {
Self {
root_hash,
total_entities,
total_pages,
uncompressed_size,
compressed_size: None,
dag_heads: vec![],
}
}
#[must_use]
pub fn with_compressed_size(mut self, size: u64) -> Self {
self.compressed_size = Some(size);
self
}
#[must_use]
pub fn with_dag_heads(mut self, heads: Vec<[u8; 32]>) -> Self {
self.dag_heads = heads;
self
}
#[must_use]
pub fn compression_ratio(&self) -> Option<f64> {
self.compressed_size
.map(|c| c as f64 / self.uncompressed_size.max(1) as f64)
}
#[must_use]
pub fn is_valid(&self) -> bool {
self.total_pages <= MAX_SNAPSHOT_PAGES && self.dag_heads.len() <= MAX_DAG_HEADS
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum SnapshotVerifyResult {
Valid,
RootHashMismatch {
expected: [u8; 32],
computed: [u8; 32],
},
EntityCountMismatch { expected: usize, actual: usize },
MissingPages { missing: Vec<usize> },
}
impl SnapshotVerifyResult {
#[must_use]
pub fn is_valid(&self) -> bool {
matches!(self, Self::Valid)
}
#[must_use]
pub fn to_error(&self) -> Option<SnapshotError> {
match self {
Self::Valid => None,
Self::RootHashMismatch { expected, computed } => {
Some(SnapshotError::RootHashMismatch {
expected: *expected,
computed: *computed,
})
}
Self::EntityCountMismatch { expected, actual } => {
Some(SnapshotError::EntityCountMismatch {
expected: *expected,
actual: *actual,
})
}
Self::MissingPages { missing } => Some(SnapshotError::MissingPages {
missing: missing.clone(),
}),
}
}
}
#[derive(Clone, Debug, PartialEq, BorshSerialize, BorshDeserialize)]
pub enum SnapshotError {
SnapshotRequired,
InvalidBoundary,
ResumeCursorInvalid,
SnapshotOnInitializedNode,
RootHashMismatch {
expected: [u8; 32],
computed: [u8; 32],
},
TransferInterrupted { pages_received: usize },
DecompressionFailed,
EntityCountMismatch { expected: usize, actual: usize },
MissingPages { missing: Vec<usize> },
}
pub fn check_snapshot_safety(has_local_state: bool) -> Result<(), SnapshotError> {
if has_local_state {
Err(SnapshotError::SnapshotOnInitializedNode)
} else {
Ok(())
}
}
#[derive(Debug, BorshSerialize, BorshDeserialize)]
#[non_exhaustive]
#[expect(clippy::large_enum_variant, reason = "Of no consequence here")]
pub enum BroadcastMessage<'a> {
StateDelta {
context_id: ContextId,
author_id: PublicKey,
delta_id: [u8; 32],
parent_ids: Vec<[u8; 32]>,
hlc: calimero_storage::logical_clock::HybridTimestamp,
root_hash: Hash, artifact: Cow<'a, [u8]>,
nonce: Nonce,
events: Option<Cow<'a, [u8]>>,
},
HashHeartbeat {
context_id: ContextId,
root_hash: Hash,
dag_heads: Vec<[u8; 32]>,
},
SpecializedNodeDiscovery {
nonce: [u8; 32],
node_type: SpecializedNodeType,
},
SpecializedNodeJoinConfirmation {
nonce: [u8; 32],
},
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sync::hash_comparison::CrdtType;
fn make_metadata() -> LeafMetadata {
LeafMetadata::new(CrdtType::lww_register("test"), 100, [1; 32])
}
fn make_entity(id: u8, data: Vec<u8>) -> SnapshotEntity {
SnapshotEntity::new([id; 32], data, make_metadata(), [2; 32])
}
#[test]
fn test_snapshot_request_compressed() {
let request = SnapshotRequest::compressed();
assert!(request.compressed);
assert!(request.is_fresh_node);
assert_eq!(request.max_page_size, 0);
assert_eq!(request.validated_page_size(), DEFAULT_SNAPSHOT_PAGE_SIZE);
}
#[test]
fn test_snapshot_request_uncompressed() {
let request = SnapshotRequest::uncompressed().with_max_page_size(1024 * 1024);
assert!(!request.compressed);
assert_eq!(request.max_page_size, 1024 * 1024);
assert_eq!(request.validated_page_size(), 1024 * 1024);
}
#[test]
fn test_snapshot_request_page_size_clamping() {
let request = SnapshotRequest::compressed().with_max_page_size(u32::MAX);
assert_eq!(request.validated_page_size(), MAX_SNAPSHOT_PAGE_SIZE);
}
#[test]
fn test_snapshot_request_roundtrip() {
let request = SnapshotRequest::compressed().with_max_page_size(65536);
let encoded = borsh::to_vec(&request).expect("serialize");
let decoded: SnapshotRequest = borsh::from_slice(&encoded).expect("deserialize");
assert_eq!(request, decoded);
}
#[test]
fn test_snapshot_entity_new() {
let entity = make_entity(1, vec![1, 2, 3]);
assert_eq!(entity.id, [1; 32]);
assert!(entity.is_root());
assert!(entity.parent_id.is_none());
assert!(entity.is_valid());
}
#[test]
fn test_snapshot_entity_with_parent() {
let entity = make_entity(2, vec![4, 5, 6]).with_parent([1; 32]);
assert!(!entity.is_root());
assert_eq!(entity.parent_id, Some([1; 32]));
assert!(entity.is_valid());
}
#[test]
fn test_snapshot_entity_validation() {
let valid = make_entity(1, vec![1, 2, 3]);
assert!(valid.is_valid());
let oversized = make_entity(1, vec![0u8; MAX_ENTITY_DATA_SIZE + 1]);
assert!(!oversized.is_valid());
}
#[test]
fn test_snapshot_entity_roundtrip() {
let entity = make_entity(3, vec![7, 8, 9]).with_parent([2; 32]);
let encoded = borsh::to_vec(&entity).expect("serialize");
let decoded: SnapshotEntity = borsh::from_slice(&encoded).expect("deserialize");
assert_eq!(entity, decoded);
}
#[test]
fn test_snapshot_entity_page() {
let entity1 = make_entity(1, vec![1, 2]);
let entity2 = make_entity(2, vec![3, 4]);
let page = SnapshotEntityPage::new(0, 3, vec![entity1, entity2], false);
assert_eq!(page.page_number, 0);
assert_eq!(page.total_pages, 3);
assert_eq!(page.entity_count(), 2);
assert!(!page.is_last);
assert!(!page.is_empty());
assert!(page.is_valid());
}
#[test]
fn test_snapshot_entity_page_last() {
let entity = make_entity(1, vec![1, 2, 3]);
let page = SnapshotEntityPage::new(2, 3, vec![entity], true);
assert!(page.is_last);
assert!(page.is_valid());
}
#[test]
fn test_snapshot_entity_page_empty() {
let page = SnapshotEntityPage::new(0, 1, vec![], true);
assert!(page.is_empty());
assert_eq!(page.entity_count(), 0);
assert!(page.is_valid());
}
#[test]
fn test_snapshot_entity_page_validation() {
let entities: Vec<SnapshotEntity> = (0..MAX_ENTITIES_PER_PAGE)
.map(|i| make_entity(i as u8, vec![i as u8]))
.collect();
let at_limit = SnapshotEntityPage::new(0, 1, entities, true);
assert!(at_limit.is_valid());
let entities: Vec<SnapshotEntity> = (0..=MAX_ENTITIES_PER_PAGE)
.map(|i| make_entity(i as u8, vec![i as u8]))
.collect();
let over_limit = SnapshotEntityPage::new(0, 1, entities, true);
assert!(!over_limit.is_valid());
let entity = make_entity(1, vec![1]);
let over_pages = SnapshotEntityPage::new(0, MAX_SNAPSHOT_PAGES + 1, vec![entity], false);
assert!(!over_pages.is_valid());
let entity = make_entity(1, vec![1]);
let invalid_page_num = SnapshotEntityPage::new(5, 3, vec![entity], false);
assert!(!invalid_page_num.is_valid());
let entity = make_entity(1, vec![1]);
let invalid_last = SnapshotEntityPage::new(0, 3, vec![entity], true);
assert!(!invalid_last.is_valid());
let entity = make_entity(1, vec![1]);
let valid_last = SnapshotEntityPage::new(2, 3, vec![entity], true);
assert!(valid_last.is_valid());
}
#[test]
fn test_snapshot_entity_page_roundtrip() {
let entity = make_entity(4, vec![10, 11]);
let page = SnapshotEntityPage::new(1, 5, vec![entity], false);
let encoded = borsh::to_vec(&page).expect("serialize");
let decoded: SnapshotEntityPage = borsh::from_slice(&encoded).expect("deserialize");
assert_eq!(page, decoded);
}
#[test]
fn test_snapshot_complete() {
let complete = SnapshotComplete::new([1; 32], 1000, 10, 1024 * 1024)
.with_compressed_size(256 * 1024)
.with_dag_heads(vec![[2; 32], [3; 32]]);
assert_eq!(complete.root_hash, [1; 32]);
assert_eq!(complete.total_entities, 1000);
assert_eq!(complete.total_pages, 10);
assert_eq!(complete.dag_heads.len(), 2);
assert!(complete.is_valid());
let ratio = complete.compression_ratio().unwrap();
assert!((ratio - 0.25).abs() < 0.01);
}
#[test]
fn test_snapshot_complete_no_compression() {
let complete = SnapshotComplete::new([1; 32], 100, 1, 10000);
assert!(complete.compression_ratio().is_none());
assert!(complete.is_valid());
}
#[test]
fn test_snapshot_complete_validation() {
let valid = SnapshotComplete::new([1; 32], 1000, 10, 1024 * 1024);
assert!(valid.is_valid());
let over_pages = SnapshotComplete::new([1; 32], 1000, MAX_SNAPSHOT_PAGES + 1, 1024);
assert!(!over_pages.is_valid());
let heads: Vec<[u8; 32]> = (0..=MAX_DAG_HEADS).map(|i| [i as u8; 32]).collect();
let over_heads = SnapshotComplete::new([1; 32], 1000, 10, 1024).with_dag_heads(heads);
assert!(!over_heads.is_valid());
}
#[test]
fn test_snapshot_complete_roundtrip() {
let complete = SnapshotComplete::new([1; 32], 500, 5, 512 * 1024)
.with_compressed_size(128 * 1024)
.with_dag_heads(vec![[2; 32]]);
let encoded = borsh::to_vec(&complete).expect("serialize");
let decoded: SnapshotComplete = borsh::from_slice(&encoded).expect("deserialize");
assert_eq!(complete, decoded);
}
#[test]
fn test_snapshot_verify_result_valid() {
let result = SnapshotVerifyResult::Valid;
assert!(result.is_valid());
assert!(result.to_error().is_none());
}
#[test]
fn test_snapshot_verify_result_hash_mismatch() {
let result = SnapshotVerifyResult::RootHashMismatch {
expected: [1; 32],
computed: [2; 32],
};
assert!(!result.is_valid());
let error = result.to_error().unwrap();
assert!(matches!(error, SnapshotError::RootHashMismatch { .. }));
}
#[test]
fn test_snapshot_verify_result_entity_count() {
let result = SnapshotVerifyResult::EntityCountMismatch {
expected: 100,
actual: 99,
};
assert!(!result.is_valid());
let error = result.to_error().unwrap();
assert!(matches!(
error,
SnapshotError::EntityCountMismatch {
expected: 100,
actual: 99
}
));
}
#[test]
fn test_snapshot_verify_result_missing_pages() {
let result = SnapshotVerifyResult::MissingPages {
missing: vec![3, 5, 7],
};
assert!(!result.is_valid());
let error = result.to_error().unwrap();
match error {
SnapshotError::MissingPages { missing } => {
assert_eq!(missing, vec![3, 5, 7]);
}
_ => panic!("Expected MissingPages error"),
}
}
#[test]
fn test_check_snapshot_safety_fresh_node() {
assert!(check_snapshot_safety(false).is_ok());
}
#[test]
fn test_check_snapshot_safety_initialized_node() {
let result = check_snapshot_safety(true);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
SnapshotError::SnapshotOnInitializedNode
));
}
#[test]
fn test_snapshot_error_roundtrip() {
let errors = vec![
SnapshotError::SnapshotRequired,
SnapshotError::InvalidBoundary,
SnapshotError::ResumeCursorInvalid,
SnapshotError::SnapshotOnInitializedNode,
SnapshotError::RootHashMismatch {
expected: [1; 32],
computed: [2; 32],
},
SnapshotError::TransferInterrupted { pages_received: 5 },
SnapshotError::DecompressionFailed,
SnapshotError::EntityCountMismatch {
expected: 100,
actual: 99,
},
SnapshotError::MissingPages {
missing: vec![3, 5, 7],
},
];
for error in errors {
let encoded = borsh::to_vec(&error).expect("serialize");
let decoded: SnapshotError = borsh::from_slice(&encoded).expect("deserialize");
assert_eq!(error, decoded);
}
}
#[test]
fn test_snapshot_boundary_response_validation() {
let valid = SnapshotBoundaryResponse {
boundary_timestamp: 12345,
boundary_root_hash: Hash::default(),
dag_heads: vec![[1; 32], [2; 32]],
};
assert!(valid.is_valid());
let heads: Vec<[u8; 32]> = (0..=MAX_DAG_HEADS).map(|i| [i as u8; 32]).collect();
let invalid = SnapshotBoundaryResponse {
boundary_timestamp: 12345,
boundary_root_hash: Hash::default(),
dag_heads: heads,
};
assert!(!invalid.is_valid());
}
#[test]
fn test_snapshot_stream_request_byte_limit() {
let request = SnapshotStreamRequest {
context_id: ContextId::zero(),
boundary_root_hash: Hash::default(),
page_limit: 10,
byte_limit: 0,
resume_cursor: None,
};
assert_eq!(request.validated_byte_limit(), DEFAULT_SNAPSHOT_PAGE_SIZE);
let request2 = SnapshotStreamRequest {
context_id: ContextId::zero(),
boundary_root_hash: Hash::default(),
page_limit: 10,
byte_limit: 100_000,
resume_cursor: None,
};
assert_eq!(request2.validated_byte_limit(), 100_000);
let request3 = SnapshotStreamRequest {
context_id: ContextId::zero(),
boundary_root_hash: Hash::default(),
page_limit: 10,
byte_limit: u32::MAX,
resume_cursor: None,
};
assert_eq!(request3.validated_byte_limit(), MAX_SNAPSHOT_PAGE_SIZE);
}
#[test]
fn test_snapshot_page_is_last() {
let page_not_last = SnapshotPage {
payload: vec![1, 2, 3],
uncompressed_len: 100,
cursor: Some(vec![4, 5]),
page_count: 10,
sent_count: 5,
};
assert!(!page_not_last.is_last());
let page_is_last = SnapshotPage {
payload: vec![1, 2, 3],
uncompressed_len: 100,
cursor: None,
page_count: 10,
sent_count: 10,
};
assert!(page_is_last.is_last());
}
#[test]
fn test_snapshot_page_validation() {
let valid = SnapshotPage {
payload: vec![1, 2, 3],
uncompressed_len: 100,
cursor: None,
page_count: 10,
sent_count: 10,
};
assert!(valid.is_valid());
let oversized = SnapshotPage {
payload: vec![1, 2, 3],
uncompressed_len: MAX_SNAPSHOT_PAGE_SIZE + 1,
cursor: None,
page_count: 10,
sent_count: 10,
};
assert!(!oversized.is_valid());
let too_many = SnapshotPage {
payload: vec![1, 2, 3],
uncompressed_len: 100,
cursor: None,
page_count: MAX_SNAPSHOT_PAGES as u64 + 1,
sent_count: 10,
};
assert!(!too_many.is_valid());
let invalid_sent = SnapshotPage {
payload: vec![1, 2, 3],
uncompressed_len: 100,
cursor: None,
page_count: 5,
sent_count: 10,
};
assert!(!invalid_sent.is_valid());
let oversized_payload = SnapshotPage {
payload: vec![0u8; MAX_COMPRESSED_PAYLOAD_SIZE + 1],
uncompressed_len: 100,
cursor: None,
page_count: 10,
sent_count: 10,
};
assert!(!oversized_payload.is_valid());
}
#[test]
fn test_snapshot_entity_data_at_limit() {
let at_limit = make_entity(1, vec![0u8; MAX_ENTITY_DATA_SIZE]);
assert!(at_limit.is_valid());
let over_limit = make_entity(1, vec![0u8; MAX_ENTITY_DATA_SIZE + 1]);
assert!(!over_limit.is_valid());
}
#[test]
fn test_snapshot_entity_page_at_entity_limit() {
let entities: Vec<SnapshotEntity> = (0..MAX_ENTITIES_PER_PAGE)
.map(|i| make_entity((i % 256) as u8, vec![(i % 256) as u8]))
.collect();
let at_limit = SnapshotEntityPage::new(0, 1, entities, true);
assert!(at_limit.is_valid());
assert_eq!(at_limit.entity_count(), MAX_ENTITIES_PER_PAGE);
let entities: Vec<SnapshotEntity> = (0..=MAX_ENTITIES_PER_PAGE)
.map(|i| make_entity((i % 256) as u8, vec![(i % 256) as u8]))
.collect();
let over_limit = SnapshotEntityPage::new(0, 1, entities, true);
assert!(!over_limit.is_valid());
}
#[test]
fn test_snapshot_complete_at_page_limit() {
let at_limit = SnapshotComplete::new([1; 32], 1000, MAX_SNAPSHOT_PAGES, 1024);
assert!(at_limit.is_valid());
let over_limit = SnapshotComplete::new([1; 32], 1000, MAX_SNAPSHOT_PAGES + 1, 1024);
assert!(!over_limit.is_valid());
}
#[test]
fn test_snapshot_complete_at_dag_heads_limit() {
let heads: Vec<[u8; 32]> = (0..MAX_DAG_HEADS).map(|i| [(i % 256) as u8; 32]).collect();
let at_limit = SnapshotComplete::new([1; 32], 1000, 10, 1024).with_dag_heads(heads);
assert!(at_limit.is_valid());
let heads: Vec<[u8; 32]> = (0..=MAX_DAG_HEADS).map(|i| [(i % 256) as u8; 32]).collect();
let over_limit = SnapshotComplete::new([1; 32], 1000, 10, 1024).with_dag_heads(heads);
assert!(!over_limit.is_valid());
}
#[test]
fn test_snapshot_page_at_size_limit() {
let at_limit = SnapshotPage {
payload: vec![1, 2, 3],
uncompressed_len: MAX_SNAPSHOT_PAGE_SIZE,
cursor: None,
page_count: 10,
sent_count: 10,
};
assert!(at_limit.is_valid());
let over_limit = SnapshotPage {
payload: vec![1, 2, 3],
uncompressed_len: MAX_SNAPSHOT_PAGE_SIZE + 1,
cursor: None,
page_count: 10,
sent_count: 10,
};
assert!(!over_limit.is_valid());
}
#[test]
fn test_snapshot_request_memory_exhaustion_prevention() {
let request = SnapshotRequest::compressed().with_max_page_size(u32::MAX);
assert_eq!(request.validated_page_size(), MAX_SNAPSHOT_PAGE_SIZE);
}
#[test]
fn test_snapshot_stream_request_memory_exhaustion_prevention() {
let request = SnapshotStreamRequest {
context_id: ContextId::zero(),
boundary_root_hash: Hash::default(),
page_limit: u16::MAX,
byte_limit: u32::MAX,
resume_cursor: None,
};
assert_eq!(request.validated_byte_limit(), MAX_SNAPSHOT_PAGE_SIZE);
}
#[test]
fn test_snapshot_entity_page_cross_validation() {
let invalid_entity = make_entity(1, vec![0u8; MAX_ENTITY_DATA_SIZE + 1]);
let page = SnapshotEntityPage::new(0, 1, vec![invalid_entity], true);
assert!(!page.is_valid());
let valid_entity = make_entity(1, vec![1, 2, 3]);
let invalid_entity = make_entity(2, vec![0u8; MAX_ENTITY_DATA_SIZE + 1]);
let mixed_page = SnapshotEntityPage::new(0, 1, vec![valid_entity, invalid_entity], true);
assert!(!mixed_page.is_valid());
}
#[test]
fn test_snapshot_complete_compression_ratio_zero_uncompressed() {
let complete = SnapshotComplete::new([1; 32], 0, 0, 0).with_compressed_size(100);
let ratio = complete.compression_ratio().unwrap();
assert_eq!(ratio, 100.0);
}
#[test]
fn test_snapshot_entity_all_zeros() {
let entity = SnapshotEntity::new([0u8; 32], vec![], make_metadata(), [0u8; 32]);
assert!(entity.is_valid());
assert!(entity.is_root());
assert!(entity.data.is_empty());
let encoded = borsh::to_vec(&entity).expect("serialize");
let decoded: SnapshotEntity = borsh::from_slice(&encoded).expect("deserialize");
assert_eq!(entity, decoded);
}
#[test]
fn test_snapshot_entity_all_ones() {
let entity = SnapshotEntity::new([0xFF; 32], vec![0xFF; 100], make_metadata(), [0xFF; 32])
.with_parent([0xFF; 32]);
assert!(entity.is_valid());
assert!(!entity.is_root());
let encoded = borsh::to_vec(&entity).expect("serialize");
let decoded: SnapshotEntity = borsh::from_slice(&encoded).expect("deserialize");
assert_eq!(entity, decoded);
}
#[test]
fn test_snapshot_complete_all_zeros() {
let complete = SnapshotComplete::new([0u8; 32], 0, 0, 0);
assert!(complete.is_valid());
assert!(complete.compression_ratio().is_none());
assert!(complete.dag_heads.is_empty());
let encoded = borsh::to_vec(&complete).expect("serialize");
let decoded: SnapshotComplete = borsh::from_slice(&encoded).expect("deserialize");
assert_eq!(complete, decoded);
}
#[test]
fn test_snapshot_complete_max_values() {
let complete = SnapshotComplete::new([0xFF; 32], usize::MAX, MAX_SNAPSHOT_PAGES, u64::MAX)
.with_compressed_size(u64::MAX);
assert!(complete.is_valid());
let encoded = borsh::to_vec(&complete).expect("serialize");
let decoded: SnapshotComplete = borsh::from_slice(&encoded).expect("deserialize");
assert_eq!(complete, decoded);
}
#[test]
fn test_snapshot_request_all_flags() {
let compressed = SnapshotRequest::compressed();
assert!(compressed.compressed);
assert!(compressed.is_fresh_node);
let uncompressed = SnapshotRequest::uncompressed();
assert!(!uncompressed.compressed);
assert!(uncompressed.is_fresh_node);
let mut not_fresh = SnapshotRequest::compressed();
not_fresh.is_fresh_node = false;
assert!(!not_fresh.is_fresh_node);
}
#[test]
fn test_snapshot_entity_page_with_many_entities_roundtrip() {
let entities: Vec<SnapshotEntity> = (0..1000)
.map(|i| make_entity((i % 256) as u8, vec![(i % 256) as u8; 10]))
.collect();
let page = SnapshotEntityPage::new(5, 100, entities, false);
assert!(page.is_valid());
let encoded = borsh::to_vec(&page).expect("serialize");
let decoded: SnapshotEntityPage = borsh::from_slice(&encoded).expect("deserialize");
assert_eq!(page, decoded);
assert_eq!(decoded.entity_count(), 1000);
}
#[test]
fn test_snapshot_page_with_large_cursor_roundtrip() {
let page = SnapshotPage {
payload: vec![1; 1000],
uncompressed_len: 5000,
cursor: Some(vec![0xAB; 256]), page_count: 1000,
sent_count: 500,
};
assert!(page.is_valid());
let encoded = borsh::to_vec(&page).expect("serialize");
let decoded: SnapshotPage = borsh::from_slice(&encoded).expect("deserialize");
assert_eq!(page, decoded);
assert!(!decoded.is_last());
}
#[test]
fn test_snapshot_verify_result_all_variants_behavior() {
assert!(SnapshotVerifyResult::Valid.is_valid());
assert!(!SnapshotVerifyResult::RootHashMismatch {
expected: [1; 32],
computed: [2; 32]
}
.is_valid());
assert!(!SnapshotVerifyResult::EntityCountMismatch {
expected: 100,
actual: 50
}
.is_valid());
assert!(!SnapshotVerifyResult::MissingPages { missing: vec![1] }.is_valid());
assert!(SnapshotVerifyResult::Valid.to_error().is_none());
assert!(SnapshotVerifyResult::RootHashMismatch {
expected: [1; 32],
computed: [2; 32]
}
.to_error()
.is_some());
assert!(SnapshotVerifyResult::EntityCountMismatch {
expected: 100,
actual: 50
}
.to_error()
.is_some());
assert!(SnapshotVerifyResult::MissingPages { missing: vec![1] }
.to_error()
.is_some());
}
#[test]
fn test_snapshot_entity_empty_data() {
let entity = make_entity(1, vec![]);
assert!(entity.is_valid());
assert!(entity.data.is_empty());
}
#[test]
fn test_snapshot_complete_empty_dag_heads() {
let complete = SnapshotComplete::new([1; 32], 100, 1, 1000);
assert!(complete.dag_heads.is_empty());
assert!(complete.is_valid());
}
#[test]
fn test_snapshot_boundary_response_empty_dag_heads() {
let response = SnapshotBoundaryResponse {
boundary_timestamp: 12345,
boundary_root_hash: Hash::default(),
dag_heads: vec![],
};
assert!(response.is_valid());
}
#[test]
fn test_snapshot_verify_result_missing_pages_empty() {
let result = SnapshotVerifyResult::MissingPages { missing: vec![] };
assert!(!result.is_valid()); assert!(result.to_error().is_some());
}
#[test]
fn test_invariant_i5_snapshot_safety() {
assert!(check_snapshot_safety(false).is_ok());
let err = check_snapshot_safety(true).unwrap_err();
assert!(matches!(err, SnapshotError::SnapshotOnInitializedNode));
}
#[test]
fn test_invariant_i7_verification_errors() {
let result = SnapshotVerifyResult::RootHashMismatch {
expected: [1; 32],
computed: [2; 32],
};
let error = result.to_error().unwrap();
match error {
SnapshotError::RootHashMismatch { expected, computed } => {
assert_eq!(expected, [1; 32]);
assert_eq!(computed, [2; 32]);
}
_ => panic!("Expected RootHashMismatch error"),
}
}
#[test]
fn test_snapshot_error_transfer_interrupted_preserves_count() {
let error = SnapshotError::TransferInterrupted { pages_received: 42 };
let encoded = borsh::to_vec(&error).expect("serialize");
let decoded: SnapshotError = borsh::from_slice(&encoded).expect("deserialize");
match decoded {
SnapshotError::TransferInterrupted { pages_received } => {
assert_eq!(pages_received, 42);
}
_ => panic!("Expected TransferInterrupted"),
}
}
#[test]
fn test_snapshot_cursor_roundtrip() {
let cursor = SnapshotCursor {
last_key: [0xAB; 32],
};
let encoded = borsh::to_vec(&cursor).expect("serialize");
let decoded: SnapshotCursor = borsh::from_slice(&encoded).expect("deserialize");
assert_eq!(cursor, decoded);
assert_eq!(decoded.last_key, [0xAB; 32]);
}
#[test]
fn test_snapshot_boundary_request_roundtrip() {
let request = SnapshotBoundaryRequest {
context_id: ContextId::zero(),
requested_cutoff_timestamp: Some(1234567890),
};
let encoded = borsh::to_vec(&request).expect("serialize");
let decoded: SnapshotBoundaryRequest = borsh::from_slice(&encoded).expect("deserialize");
assert_eq!(request, decoded);
assert_eq!(decoded.requested_cutoff_timestamp, Some(1234567890));
let request_none = SnapshotBoundaryRequest {
context_id: ContextId::zero(),
requested_cutoff_timestamp: None,
};
let encoded = borsh::to_vec(&request_none).expect("serialize");
let decoded: SnapshotBoundaryRequest = borsh::from_slice(&encoded).expect("deserialize");
assert_eq!(request_none, decoded);
assert!(decoded.requested_cutoff_timestamp.is_none());
}
}