use std::borrow::Cow;
use borsh::{BorshDeserialize, BorshSerialize};
use calimero_context_config::types::GovernancePosition;
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)]
pub enum SnapshotRecord {
Entity {
id: [u8; 32],
entry: Vec<u8>,
index: Vec<u8>,
schema_app_key: Option<[u8; 32]>,
},
Auxiliary {
kind: u8,
id: [u8; 32],
value: Vec<u8>,
},
}
impl BorshDeserialize for SnapshotRecord {
fn deserialize_reader<R: borsh::io::Read>(reader: &mut R) -> borsh::io::Result<Self> {
let variant = u8::deserialize_reader(reader)?;
match variant {
0 => {
let id = <[u8; 32]>::deserialize_reader(reader)?;
let entry = Vec::<u8>::deserialize_reader(reader)?;
let index = Vec::<u8>::deserialize_reader(reader)?;
let mut first = [0u8; 1];
let schema_app_key =
match crate::sync::hash_comparison::read_option_tag(reader, &mut first)? {
None => None,
Some(0) => None,
Some(1) => Some(<[u8; 32]>::deserialize_reader(reader)?),
Some(tag) => {
return Err(borsh::io::Error::new(
borsh::io::ErrorKind::InvalidData,
format!(
"invalid Option tag {tag} for \
SnapshotRecord::Entity.schema_app_key"
),
))
}
};
Ok(Self::Entity {
id,
entry,
index,
schema_app_key,
})
}
1 => {
let kind = u8::deserialize_reader(reader)?;
let id = <[u8; 32]>::deserialize_reader(reader)?;
let value = Vec::<u8>::deserialize_reader(reader)?;
Ok(Self::Auxiliary { kind, id, value })
}
other => Err(borsh::io::Error::new(
borsh::io::ErrorKind::InvalidData,
format!("invalid SnapshotRecord variant discriminant {other}"),
)),
}
}
}
pub mod snapshot_record_kind {
pub const INDEX: u8 = 0;
pub const ENTRY: u8 = 1;
pub const SYNC_STATE: u8 = 2;
pub const ROTATION_LOG: u8 = 3;
}
#[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(())
}
}
pub const MAX_SIGNED_GROUP_OP_PAYLOAD_BYTES: usize = 64 * 1024;
#[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]>>,
governance_position: Option<GovernancePosition>,
key_id: [u8; 32],
delta_signature: Option<[u8; 64]>,
producing_app_key: Option<[u8; 32]>,
},
HashHeartbeat {
context_id: ContextId,
root_hash: Hash,
dag_heads: Vec<[u8; 32]>,
},
SpecializedNodeDiscovery {
nonce: [u8; 32],
node_type: SpecializedNodeType,
},
SpecializedNodeJoinConfirmation {
nonce: [u8; 32],
},
TeeAttestationAnnounce {
quote_bytes: Vec<u8>,
public_key: PublicKey,
nonce: [u8; 32],
node_type: SpecializedNodeType,
},
NamespaceGovernanceDelta {
namespace_id: [u8; 32],
delta_id: [u8; 32],
parent_ids: Vec<[u8; 32]>,
payload: Vec<u8>,
},
NamespaceStateHeartbeat {
namespace_id: [u8; 32],
dag_heads: Vec<[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_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_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_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_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_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_entity_schema_app_key_defaults_none_and_round_trips() {
let bare = SnapshotRecord::Entity {
id: [1u8; 32],
entry: vec![1, 2, 3],
index: vec![4, 5, 6],
schema_app_key: None,
};
let encoded = borsh::to_vec(&bare).expect("serialize");
let decoded: SnapshotRecord = borsh::from_slice(&encoded).expect("deserialize");
assert_eq!(bare, decoded);
let stamped = SnapshotRecord::Entity {
id: [2u8; 32],
entry: vec![7, 8],
index: vec![9],
schema_app_key: Some([7u8; 32]),
};
let encoded = borsh::to_vec(&stamped).expect("serialize");
let decoded: SnapshotRecord = borsh::from_slice(&encoded).expect("deserialize");
assert_eq!(stamped, decoded);
match decoded {
SnapshotRecord::Entity { schema_app_key, .. } => {
assert_eq!(schema_app_key, Some([7u8; 32]));
}
SnapshotRecord::Auxiliary { .. } => panic!("expected Entity"),
}
let aux = SnapshotRecord::Auxiliary {
kind: snapshot_record_kind::ROTATION_LOG,
id: [3u8; 32],
value: vec![1],
};
let encoded = borsh::to_vec(&aux).expect("serialize");
let decoded: SnapshotRecord = borsh::from_slice(&encoded).expect("deserialize");
assert_eq!(aux, decoded);
}
#[test]
fn test_snapshot_entity_legacy_bytes_decode_as_none() {
let id = [9u8; 32];
let entry = vec![1u8, 2, 3];
let index = vec![4u8, 5];
let mut legacy = Vec::new();
legacy.push(0u8);
legacy.extend_from_slice(&id);
legacy.extend_from_slice(&borsh::to_vec(&entry).unwrap());
legacy.extend_from_slice(&borsh::to_vec(&index).unwrap());
let decoded: SnapshotRecord = borsh::from_slice(&legacy).expect("deserialize legacy");
assert_eq!(
decoded,
SnapshotRecord::Entity {
id,
entry,
index,
schema_app_key: None,
}
);
}
}