1use std::borrow::Cow;
24
25use borsh::{BorshDeserialize, BorshSerialize};
26use calimero_crypto::Nonce;
27use calimero_network_primitives::specialized_node_invite::SpecializedNodeType;
28use calimero_primitives::context::ContextId;
29use calimero_primitives::hash::Hash;
30use calimero_primitives::identity::PublicKey;
31
32use super::hash_comparison::LeafMetadata;
33
34pub const DEFAULT_SNAPSHOT_PAGE_SIZE: u32 = 256 * 1024;
42
43pub const MAX_SNAPSHOT_PAGE_SIZE: u32 = 4 * 1024 * 1024;
47
48pub const MAX_ENTITIES_PER_PAGE: usize = 1_000;
53
54pub const MAX_SNAPSHOT_PAGES: usize = 10_000;
59
60pub const MAX_ENTITY_DATA_SIZE: usize = 1_048_576;
64
65pub const MAX_DAG_HEADS: usize = 100;
69
70pub const MAX_COMPRESSED_PAYLOAD_SIZE: usize = 8 * 1024 * 1024;
76
77#[derive(Clone, Debug, PartialEq, BorshSerialize, BorshDeserialize)]
83pub struct SnapshotBoundaryRequest {
84 pub context_id: ContextId,
86
87 pub requested_cutoff_timestamp: Option<u64>,
89}
90
91#[derive(Clone, Debug, PartialEq, BorshSerialize, BorshDeserialize)]
96pub struct SnapshotBoundaryResponse {
97 pub boundary_timestamp: u64,
99
100 pub boundary_root_hash: Hash,
102
103 pub dag_heads: Vec<[u8; 32]>,
105}
106
107impl SnapshotBoundaryResponse {
108 #[must_use]
112 pub fn is_valid(&self) -> bool {
113 self.dag_heads.len() <= MAX_DAG_HEADS
114 }
115}
116
117#[derive(Clone, Debug, PartialEq, BorshSerialize, BorshDeserialize)]
119pub struct SnapshotStreamRequest {
120 pub context_id: ContextId,
122
123 pub boundary_root_hash: Hash,
125
126 pub page_limit: u16,
128
129 pub byte_limit: u32,
131
132 pub resume_cursor: Option<Vec<u8>>,
134}
135
136impl SnapshotStreamRequest {
137 #[must_use]
141 pub fn validated_byte_limit(&self) -> u32 {
142 if self.byte_limit == 0 {
143 DEFAULT_SNAPSHOT_PAGE_SIZE
144 } else {
145 self.byte_limit.min(MAX_SNAPSHOT_PAGE_SIZE)
146 }
147 }
148}
149
150#[derive(Clone, Debug, PartialEq, BorshSerialize, BorshDeserialize)]
152pub struct SnapshotPage {
153 pub payload: Vec<u8>,
155 pub uncompressed_len: u32,
157 pub cursor: Option<Vec<u8>>,
159 pub page_count: u64,
161 pub sent_count: u64,
163}
164
165impl SnapshotPage {
166 #[must_use]
168 pub fn is_last(&self) -> bool {
169 self.cursor.is_none()
170 }
171
172 #[must_use]
174 pub fn is_valid(&self) -> bool {
175 self.uncompressed_len <= MAX_SNAPSHOT_PAGE_SIZE
176 && self.page_count <= MAX_SNAPSHOT_PAGES as u64
177 && self.sent_count <= self.page_count
178 && self.payload.len() <= MAX_COMPRESSED_PAYLOAD_SIZE
179 }
180}
181
182#[derive(Clone, Debug, PartialEq, BorshSerialize, BorshDeserialize)]
184pub struct SnapshotCursor {
185 pub last_key: [u8; 32],
187}
188
189#[derive(Clone, Debug, PartialEq, BorshSerialize, BorshDeserialize)]
198pub struct SnapshotRequest {
199 pub compressed: bool,
201
202 pub max_page_size: u32,
204
205 pub is_fresh_node: bool,
208}
209
210impl SnapshotRequest {
211 #[must_use]
213 pub fn compressed() -> Self {
214 Self {
215 compressed: true,
216 max_page_size: 0,
217 is_fresh_node: true,
218 }
219 }
220
221 #[must_use]
223 pub fn uncompressed() -> Self {
224 Self {
225 compressed: false,
226 max_page_size: 0,
227 is_fresh_node: true,
228 }
229 }
230
231 #[must_use]
233 pub fn with_max_page_size(mut self, size: u32) -> Self {
234 self.max_page_size = size;
235 self
236 }
237
238 #[must_use]
242 pub fn validated_page_size(&self) -> u32 {
243 if self.max_page_size == 0 {
244 DEFAULT_SNAPSHOT_PAGE_SIZE
245 } else {
246 self.max_page_size.min(MAX_SNAPSHOT_PAGE_SIZE)
247 }
248 }
249}
250
251#[derive(Clone, Debug, PartialEq, BorshSerialize, BorshDeserialize)]
255pub struct SnapshotEntity {
256 pub id: [u8; 32],
258
259 pub data: Vec<u8>,
261
262 pub metadata: LeafMetadata,
264
265 pub collection_id: [u8; 32],
267
268 pub parent_id: Option<[u8; 32]>,
270}
271
272impl SnapshotEntity {
273 #[must_use]
275 pub fn new(
276 id: [u8; 32],
277 data: Vec<u8>,
278 metadata: LeafMetadata,
279 collection_id: [u8; 32],
280 ) -> Self {
281 Self {
282 id,
283 data,
284 metadata,
285 collection_id,
286 parent_id: None,
287 }
288 }
289
290 #[must_use]
292 pub fn with_parent(mut self, parent_id: [u8; 32]) -> Self {
293 self.parent_id = Some(parent_id);
294 self
295 }
296
297 #[must_use]
299 pub fn is_root(&self) -> bool {
300 self.parent_id.is_none()
301 }
302
303 #[must_use]
308 pub fn is_valid(&self) -> bool {
309 self.data.len() <= MAX_ENTITY_DATA_SIZE
310 }
311}
312
313#[derive(Clone, Debug, PartialEq, BorshSerialize, BorshDeserialize)]
315pub struct SnapshotEntityPage {
316 pub page_number: usize,
318
319 pub total_pages: usize,
321
322 pub entities: Vec<SnapshotEntity>,
324
325 pub is_last: bool,
327}
328
329impl SnapshotEntityPage {
330 #[must_use]
332 pub fn new(
333 page_number: usize,
334 total_pages: usize,
335 entities: Vec<SnapshotEntity>,
336 is_last: bool,
337 ) -> Self {
338 Self {
339 page_number,
340 total_pages,
341 entities,
342 is_last,
343 }
344 }
345
346 #[must_use]
348 pub fn entity_count(&self) -> usize {
349 self.entities.len()
350 }
351
352 #[must_use]
354 pub fn is_empty(&self) -> bool {
355 self.entities.is_empty()
356 }
357
358 #[must_use]
362 pub fn is_valid(&self) -> bool {
363 if self.entities.len() > MAX_ENTITIES_PER_PAGE {
365 return false;
366 }
367
368 if self.total_pages > MAX_SNAPSHOT_PAGES {
370 return false;
371 }
372
373 if self.total_pages > 0 && self.page_number >= self.total_pages {
375 return false;
376 }
377
378 if self.is_last && self.total_pages > 0 && self.page_number + 1 != self.total_pages {
380 return false;
381 }
382
383 self.entities.iter().all(SnapshotEntity::is_valid)
385 }
386}
387
388#[derive(Clone, Debug, PartialEq, BorshSerialize, BorshDeserialize)]
393pub struct SnapshotComplete {
394 pub root_hash: [u8; 32],
397
398 pub total_entities: usize,
400
401 pub total_pages: usize,
403
404 pub uncompressed_size: u64,
406
407 pub compressed_size: Option<u64>,
409
410 pub dag_heads: Vec<[u8; 32]>,
413}
414
415impl SnapshotComplete {
416 #[must_use]
418 pub fn new(
419 root_hash: [u8; 32],
420 total_entities: usize,
421 total_pages: usize,
422 uncompressed_size: u64,
423 ) -> Self {
424 Self {
425 root_hash,
426 total_entities,
427 total_pages,
428 uncompressed_size,
429 compressed_size: None,
430 dag_heads: vec![],
431 }
432 }
433
434 #[must_use]
436 pub fn with_compressed_size(mut self, size: u64) -> Self {
437 self.compressed_size = Some(size);
438 self
439 }
440
441 #[must_use]
443 pub fn with_dag_heads(mut self, heads: Vec<[u8; 32]>) -> Self {
444 self.dag_heads = heads;
445 self
446 }
447
448 #[must_use]
450 pub fn compression_ratio(&self) -> Option<f64> {
451 self.compressed_size
452 .map(|c| c as f64 / self.uncompressed_size.max(1) as f64)
453 }
454
455 #[must_use]
457 pub fn is_valid(&self) -> bool {
458 self.total_pages <= MAX_SNAPSHOT_PAGES && self.dag_heads.len() <= MAX_DAG_HEADS
459 }
460}
461
462#[derive(Clone, Debug, PartialEq)]
468pub enum SnapshotVerifyResult {
469 Valid,
471
472 RootHashMismatch {
474 expected: [u8; 32],
475 computed: [u8; 32],
476 },
477
478 EntityCountMismatch { expected: usize, actual: usize },
480
481 MissingPages { missing: Vec<usize> },
483}
484
485impl SnapshotVerifyResult {
486 #[must_use]
488 pub fn is_valid(&self) -> bool {
489 matches!(self, Self::Valid)
490 }
491
492 #[must_use]
494 pub fn to_error(&self) -> Option<SnapshotError> {
495 match self {
496 Self::Valid => None,
497 Self::RootHashMismatch { expected, computed } => {
498 Some(SnapshotError::RootHashMismatch {
499 expected: *expected,
500 computed: *computed,
501 })
502 }
503 Self::EntityCountMismatch { expected, actual } => {
504 Some(SnapshotError::EntityCountMismatch {
505 expected: *expected,
506 actual: *actual,
507 })
508 }
509 Self::MissingPages { missing } => Some(SnapshotError::MissingPages {
510 missing: missing.clone(),
511 }),
512 }
513 }
514}
515
516#[derive(Clone, Debug, PartialEq, BorshSerialize, BorshDeserialize)]
522pub enum SnapshotError {
523 SnapshotRequired,
525
526 InvalidBoundary,
528
529 ResumeCursorInvalid,
531
532 SnapshotOnInitializedNode,
535
536 RootHashMismatch {
539 expected: [u8; 32],
540 computed: [u8; 32],
541 },
542
543 TransferInterrupted { pages_received: usize },
545
546 DecompressionFailed,
548
549 EntityCountMismatch { expected: usize, actual: usize },
551
552 MissingPages { missing: Vec<usize> },
554}
555
556pub fn check_snapshot_safety(has_local_state: bool) -> Result<(), SnapshotError> {
565 if has_local_state {
566 Err(SnapshotError::SnapshotOnInitializedNode)
567 } else {
568 Ok(())
569 }
570}
571
572#[derive(Debug, BorshSerialize, BorshDeserialize)]
577#[non_exhaustive]
578#[expect(clippy::large_enum_variant, reason = "Of no consequence here")]
579pub enum BroadcastMessage<'a> {
580 StateDelta {
581 context_id: ContextId,
582 author_id: PublicKey,
583
584 delta_id: [u8; 32],
586
587 parent_ids: Vec<[u8; 32]>,
589
590 hlc: calimero_storage::logical_clock::HybridTimestamp,
592
593 root_hash: Hash, artifact: Cow<'a, [u8]>,
595 nonce: Nonce,
596
597 events: Option<Cow<'a, [u8]>>,
600 },
601
602 HashHeartbeat {
607 context_id: ContextId,
608 root_hash: Hash,
610 dag_heads: Vec<[u8; 32]>,
612 },
613
614 SpecializedNodeDiscovery {
623 nonce: [u8; 32],
625 node_type: SpecializedNodeType,
627 },
628
629 SpecializedNodeJoinConfirmation {
634 nonce: [u8; 32],
636 },
637}
638
639#[cfg(test)]
646mod tests {
647 use super::*;
648 use crate::sync::hash_comparison::CrdtType;
649
650 fn make_metadata() -> LeafMetadata {
655 LeafMetadata::new(CrdtType::lww_register("test"), 100, [1; 32])
656 }
657
658 fn make_entity(id: u8, data: Vec<u8>) -> SnapshotEntity {
659 SnapshotEntity::new([id; 32], data, make_metadata(), [2; 32])
660 }
661
662 #[test]
667 fn test_snapshot_request_compressed() {
668 let request = SnapshotRequest::compressed();
669
670 assert!(request.compressed);
671 assert!(request.is_fresh_node);
672 assert_eq!(request.max_page_size, 0);
673 assert_eq!(request.validated_page_size(), DEFAULT_SNAPSHOT_PAGE_SIZE);
674 }
675
676 #[test]
677 fn test_snapshot_request_uncompressed() {
678 let request = SnapshotRequest::uncompressed().with_max_page_size(1024 * 1024);
679
680 assert!(!request.compressed);
681 assert_eq!(request.max_page_size, 1024 * 1024);
682 assert_eq!(request.validated_page_size(), 1024 * 1024);
683 }
684
685 #[test]
686 fn test_snapshot_request_page_size_clamping() {
687 let request = SnapshotRequest::compressed().with_max_page_size(u32::MAX);
688
689 assert_eq!(request.validated_page_size(), MAX_SNAPSHOT_PAGE_SIZE);
691 }
692
693 #[test]
694 fn test_snapshot_request_roundtrip() {
695 let request = SnapshotRequest::compressed().with_max_page_size(65536);
696
697 let encoded = borsh::to_vec(&request).expect("serialize");
698 let decoded: SnapshotRequest = borsh::from_slice(&encoded).expect("deserialize");
699
700 assert_eq!(request, decoded);
701 }
702
703 #[test]
708 fn test_snapshot_entity_new() {
709 let entity = make_entity(1, vec![1, 2, 3]);
710
711 assert_eq!(entity.id, [1; 32]);
712 assert!(entity.is_root());
713 assert!(entity.parent_id.is_none());
714 assert!(entity.is_valid());
715 }
716
717 #[test]
718 fn test_snapshot_entity_with_parent() {
719 let entity = make_entity(2, vec![4, 5, 6]).with_parent([1; 32]);
720
721 assert!(!entity.is_root());
722 assert_eq!(entity.parent_id, Some([1; 32]));
723 assert!(entity.is_valid());
724 }
725
726 #[test]
727 fn test_snapshot_entity_validation() {
728 let valid = make_entity(1, vec![1, 2, 3]);
730 assert!(valid.is_valid());
731
732 let oversized = make_entity(1, vec![0u8; MAX_ENTITY_DATA_SIZE + 1]);
734 assert!(!oversized.is_valid());
735 }
736
737 #[test]
738 fn test_snapshot_entity_roundtrip() {
739 let entity = make_entity(3, vec![7, 8, 9]).with_parent([2; 32]);
740
741 let encoded = borsh::to_vec(&entity).expect("serialize");
742 let decoded: SnapshotEntity = borsh::from_slice(&encoded).expect("deserialize");
743
744 assert_eq!(entity, decoded);
745 }
746
747 #[test]
752 fn test_snapshot_entity_page() {
753 let entity1 = make_entity(1, vec![1, 2]);
754 let entity2 = make_entity(2, vec![3, 4]);
755
756 let page = SnapshotEntityPage::new(0, 3, vec![entity1, entity2], false);
757
758 assert_eq!(page.page_number, 0);
759 assert_eq!(page.total_pages, 3);
760 assert_eq!(page.entity_count(), 2);
761 assert!(!page.is_last);
762 assert!(!page.is_empty());
763 assert!(page.is_valid());
764 }
765
766 #[test]
767 fn test_snapshot_entity_page_last() {
768 let entity = make_entity(1, vec![1, 2, 3]);
769 let page = SnapshotEntityPage::new(2, 3, vec![entity], true);
770
771 assert!(page.is_last);
772 assert!(page.is_valid());
773 }
774
775 #[test]
776 fn test_snapshot_entity_page_empty() {
777 let page = SnapshotEntityPage::new(0, 1, vec![], true);
778
779 assert!(page.is_empty());
780 assert_eq!(page.entity_count(), 0);
781 assert!(page.is_valid());
782 }
783
784 #[test]
785 fn test_snapshot_entity_page_validation() {
786 let entities: Vec<SnapshotEntity> = (0..MAX_ENTITIES_PER_PAGE)
788 .map(|i| make_entity(i as u8, vec![i as u8]))
789 .collect();
790 let at_limit = SnapshotEntityPage::new(0, 1, entities, true);
791 assert!(at_limit.is_valid());
792
793 let entities: Vec<SnapshotEntity> = (0..=MAX_ENTITIES_PER_PAGE)
795 .map(|i| make_entity(i as u8, vec![i as u8]))
796 .collect();
797 let over_limit = SnapshotEntityPage::new(0, 1, entities, true);
798 assert!(!over_limit.is_valid());
799
800 let entity = make_entity(1, vec![1]);
802 let over_pages = SnapshotEntityPage::new(0, MAX_SNAPSHOT_PAGES + 1, vec![entity], false);
803 assert!(!over_pages.is_valid());
804
805 let entity = make_entity(1, vec![1]);
807 let invalid_page_num = SnapshotEntityPage::new(5, 3, vec![entity], false);
808 assert!(!invalid_page_num.is_valid());
809
810 let entity = make_entity(1, vec![1]);
812 let invalid_last = SnapshotEntityPage::new(0, 3, vec![entity], true);
813 assert!(!invalid_last.is_valid());
814
815 let entity = make_entity(1, vec![1]);
817 let valid_last = SnapshotEntityPage::new(2, 3, vec![entity], true);
818 assert!(valid_last.is_valid());
819 }
820
821 #[test]
822 fn test_snapshot_entity_page_roundtrip() {
823 let entity = make_entity(4, vec![10, 11]);
824 let page = SnapshotEntityPage::new(1, 5, vec![entity], false);
825
826 let encoded = borsh::to_vec(&page).expect("serialize");
827 let decoded: SnapshotEntityPage = borsh::from_slice(&encoded).expect("deserialize");
828
829 assert_eq!(page, decoded);
830 }
831
832 #[test]
837 fn test_snapshot_complete() {
838 let complete = SnapshotComplete::new([1; 32], 1000, 10, 1024 * 1024)
839 .with_compressed_size(256 * 1024)
840 .with_dag_heads(vec![[2; 32], [3; 32]]);
841
842 assert_eq!(complete.root_hash, [1; 32]);
843 assert_eq!(complete.total_entities, 1000);
844 assert_eq!(complete.total_pages, 10);
845 assert_eq!(complete.dag_heads.len(), 2);
846 assert!(complete.is_valid());
847
848 let ratio = complete.compression_ratio().unwrap();
850 assert!((ratio - 0.25).abs() < 0.01);
851 }
852
853 #[test]
854 fn test_snapshot_complete_no_compression() {
855 let complete = SnapshotComplete::new([1; 32], 100, 1, 10000);
856
857 assert!(complete.compression_ratio().is_none());
858 assert!(complete.is_valid());
859 }
860
861 #[test]
862 fn test_snapshot_complete_validation() {
863 let valid = SnapshotComplete::new([1; 32], 1000, 10, 1024 * 1024);
865 assert!(valid.is_valid());
866
867 let over_pages = SnapshotComplete::new([1; 32], 1000, MAX_SNAPSHOT_PAGES + 1, 1024);
869 assert!(!over_pages.is_valid());
870
871 let heads: Vec<[u8; 32]> = (0..=MAX_DAG_HEADS).map(|i| [i as u8; 32]).collect();
873 let over_heads = SnapshotComplete::new([1; 32], 1000, 10, 1024).with_dag_heads(heads);
874 assert!(!over_heads.is_valid());
875 }
876
877 #[test]
878 fn test_snapshot_complete_roundtrip() {
879 let complete = SnapshotComplete::new([1; 32], 500, 5, 512 * 1024)
880 .with_compressed_size(128 * 1024)
881 .with_dag_heads(vec![[2; 32]]);
882
883 let encoded = borsh::to_vec(&complete).expect("serialize");
884 let decoded: SnapshotComplete = borsh::from_slice(&encoded).expect("deserialize");
885
886 assert_eq!(complete, decoded);
887 }
888
889 #[test]
894 fn test_snapshot_verify_result_valid() {
895 let result = SnapshotVerifyResult::Valid;
896 assert!(result.is_valid());
897 assert!(result.to_error().is_none());
898 }
899
900 #[test]
901 fn test_snapshot_verify_result_hash_mismatch() {
902 let result = SnapshotVerifyResult::RootHashMismatch {
903 expected: [1; 32],
904 computed: [2; 32],
905 };
906 assert!(!result.is_valid());
907
908 let error = result.to_error().unwrap();
909 assert!(matches!(error, SnapshotError::RootHashMismatch { .. }));
910 }
911
912 #[test]
913 fn test_snapshot_verify_result_entity_count() {
914 let result = SnapshotVerifyResult::EntityCountMismatch {
915 expected: 100,
916 actual: 99,
917 };
918 assert!(!result.is_valid());
919 let error = result.to_error().unwrap();
920 assert!(matches!(
921 error,
922 SnapshotError::EntityCountMismatch {
923 expected: 100,
924 actual: 99
925 }
926 ));
927 }
928
929 #[test]
930 fn test_snapshot_verify_result_missing_pages() {
931 let result = SnapshotVerifyResult::MissingPages {
932 missing: vec![3, 5, 7],
933 };
934 assert!(!result.is_valid());
935 let error = result.to_error().unwrap();
936 match error {
937 SnapshotError::MissingPages { missing } => {
938 assert_eq!(missing, vec![3, 5, 7]);
939 }
940 _ => panic!("Expected MissingPages error"),
941 }
942 }
943
944 #[test]
949 fn test_check_snapshot_safety_fresh_node() {
950 assert!(check_snapshot_safety(false).is_ok());
952 }
953
954 #[test]
955 fn test_check_snapshot_safety_initialized_node() {
956 let result = check_snapshot_safety(true);
958 assert!(result.is_err());
959 assert!(matches!(
960 result.unwrap_err(),
961 SnapshotError::SnapshotOnInitializedNode
962 ));
963 }
964
965 #[test]
970 fn test_snapshot_error_roundtrip() {
971 let errors = vec![
972 SnapshotError::SnapshotRequired,
973 SnapshotError::InvalidBoundary,
974 SnapshotError::ResumeCursorInvalid,
975 SnapshotError::SnapshotOnInitializedNode,
976 SnapshotError::RootHashMismatch {
977 expected: [1; 32],
978 computed: [2; 32],
979 },
980 SnapshotError::TransferInterrupted { pages_received: 5 },
981 SnapshotError::DecompressionFailed,
982 SnapshotError::EntityCountMismatch {
983 expected: 100,
984 actual: 99,
985 },
986 SnapshotError::MissingPages {
987 missing: vec![3, 5, 7],
988 },
989 ];
990
991 for error in errors {
992 let encoded = borsh::to_vec(&error).expect("serialize");
993 let decoded: SnapshotError = borsh::from_slice(&encoded).expect("deserialize");
994 assert_eq!(error, decoded);
995 }
996 }
997
998 #[test]
1003 fn test_snapshot_boundary_response_validation() {
1004 let valid = SnapshotBoundaryResponse {
1006 boundary_timestamp: 12345,
1007 boundary_root_hash: Hash::default(),
1008 dag_heads: vec![[1; 32], [2; 32]],
1009 };
1010 assert!(valid.is_valid());
1011
1012 let heads: Vec<[u8; 32]> = (0..=MAX_DAG_HEADS).map(|i| [i as u8; 32]).collect();
1014 let invalid = SnapshotBoundaryResponse {
1015 boundary_timestamp: 12345,
1016 boundary_root_hash: Hash::default(),
1017 dag_heads: heads,
1018 };
1019 assert!(!invalid.is_valid());
1020 }
1021
1022 #[test]
1027 fn test_snapshot_stream_request_byte_limit() {
1028 let request = SnapshotStreamRequest {
1030 context_id: ContextId::zero(),
1031 boundary_root_hash: Hash::default(),
1032 page_limit: 10,
1033 byte_limit: 0,
1034 resume_cursor: None,
1035 };
1036 assert_eq!(request.validated_byte_limit(), DEFAULT_SNAPSHOT_PAGE_SIZE);
1037
1038 let request2 = SnapshotStreamRequest {
1040 context_id: ContextId::zero(),
1041 boundary_root_hash: Hash::default(),
1042 page_limit: 10,
1043 byte_limit: 100_000,
1044 resume_cursor: None,
1045 };
1046 assert_eq!(request2.validated_byte_limit(), 100_000);
1047
1048 let request3 = SnapshotStreamRequest {
1050 context_id: ContextId::zero(),
1051 boundary_root_hash: Hash::default(),
1052 page_limit: 10,
1053 byte_limit: u32::MAX,
1054 resume_cursor: None,
1055 };
1056 assert_eq!(request3.validated_byte_limit(), MAX_SNAPSHOT_PAGE_SIZE);
1057 }
1058
1059 #[test]
1064 fn test_snapshot_page_is_last() {
1065 let page_not_last = SnapshotPage {
1066 payload: vec![1, 2, 3],
1067 uncompressed_len: 100,
1068 cursor: Some(vec![4, 5]),
1069 page_count: 10,
1070 sent_count: 5,
1071 };
1072 assert!(!page_not_last.is_last());
1073
1074 let page_is_last = SnapshotPage {
1075 payload: vec![1, 2, 3],
1076 uncompressed_len: 100,
1077 cursor: None,
1078 page_count: 10,
1079 sent_count: 10,
1080 };
1081 assert!(page_is_last.is_last());
1082 }
1083
1084 #[test]
1085 fn test_snapshot_page_validation() {
1086 let valid = SnapshotPage {
1088 payload: vec![1, 2, 3],
1089 uncompressed_len: 100,
1090 cursor: None,
1091 page_count: 10,
1092 sent_count: 10,
1093 };
1094 assert!(valid.is_valid());
1095
1096 let oversized = SnapshotPage {
1098 payload: vec![1, 2, 3],
1099 uncompressed_len: MAX_SNAPSHOT_PAGE_SIZE + 1,
1100 cursor: None,
1101 page_count: 10,
1102 sent_count: 10,
1103 };
1104 assert!(!oversized.is_valid());
1105
1106 let too_many = SnapshotPage {
1108 payload: vec![1, 2, 3],
1109 uncompressed_len: 100,
1110 cursor: None,
1111 page_count: MAX_SNAPSHOT_PAGES as u64 + 1,
1112 sent_count: 10,
1113 };
1114 assert!(!too_many.is_valid());
1115
1116 let invalid_sent = SnapshotPage {
1118 payload: vec![1, 2, 3],
1119 uncompressed_len: 100,
1120 cursor: None,
1121 page_count: 5,
1122 sent_count: 10,
1123 };
1124 assert!(!invalid_sent.is_valid());
1125
1126 let oversized_payload = SnapshotPage {
1128 payload: vec![0u8; MAX_COMPRESSED_PAYLOAD_SIZE + 1],
1129 uncompressed_len: 100,
1130 cursor: None,
1131 page_count: 10,
1132 sent_count: 10,
1133 };
1134 assert!(!oversized_payload.is_valid());
1135 }
1136
1137 #[test]
1142 fn test_snapshot_entity_data_at_limit() {
1143 let at_limit = make_entity(1, vec![0u8; MAX_ENTITY_DATA_SIZE]);
1145 assert!(at_limit.is_valid());
1146
1147 let over_limit = make_entity(1, vec![0u8; MAX_ENTITY_DATA_SIZE + 1]);
1149 assert!(!over_limit.is_valid());
1150 }
1151
1152 #[test]
1153 fn test_snapshot_entity_page_at_entity_limit() {
1154 let entities: Vec<SnapshotEntity> = (0..MAX_ENTITIES_PER_PAGE)
1156 .map(|i| make_entity((i % 256) as u8, vec![(i % 256) as u8]))
1157 .collect();
1158 let at_limit = SnapshotEntityPage::new(0, 1, entities, true);
1159 assert!(at_limit.is_valid());
1160 assert_eq!(at_limit.entity_count(), MAX_ENTITIES_PER_PAGE);
1161
1162 let entities: Vec<SnapshotEntity> = (0..=MAX_ENTITIES_PER_PAGE)
1164 .map(|i| make_entity((i % 256) as u8, vec![(i % 256) as u8]))
1165 .collect();
1166 let over_limit = SnapshotEntityPage::new(0, 1, entities, true);
1167 assert!(!over_limit.is_valid());
1168 }
1169
1170 #[test]
1171 fn test_snapshot_complete_at_page_limit() {
1172 let at_limit = SnapshotComplete::new([1; 32], 1000, MAX_SNAPSHOT_PAGES, 1024);
1174 assert!(at_limit.is_valid());
1175
1176 let over_limit = SnapshotComplete::new([1; 32], 1000, MAX_SNAPSHOT_PAGES + 1, 1024);
1178 assert!(!over_limit.is_valid());
1179 }
1180
1181 #[test]
1182 fn test_snapshot_complete_at_dag_heads_limit() {
1183 let heads: Vec<[u8; 32]> = (0..MAX_DAG_HEADS).map(|i| [(i % 256) as u8; 32]).collect();
1185 let at_limit = SnapshotComplete::new([1; 32], 1000, 10, 1024).with_dag_heads(heads);
1186 assert!(at_limit.is_valid());
1187
1188 let heads: Vec<[u8; 32]> = (0..=MAX_DAG_HEADS).map(|i| [(i % 256) as u8; 32]).collect();
1190 let over_limit = SnapshotComplete::new([1; 32], 1000, 10, 1024).with_dag_heads(heads);
1191 assert!(!over_limit.is_valid());
1192 }
1193
1194 #[test]
1195 fn test_snapshot_page_at_size_limit() {
1196 let at_limit = SnapshotPage {
1198 payload: vec![1, 2, 3],
1199 uncompressed_len: MAX_SNAPSHOT_PAGE_SIZE,
1200 cursor: None,
1201 page_count: 10,
1202 sent_count: 10,
1203 };
1204 assert!(at_limit.is_valid());
1205
1206 let over_limit = SnapshotPage {
1208 payload: vec![1, 2, 3],
1209 uncompressed_len: MAX_SNAPSHOT_PAGE_SIZE + 1,
1210 cursor: None,
1211 page_count: 10,
1212 sent_count: 10,
1213 };
1214 assert!(!over_limit.is_valid());
1215 }
1216
1217 #[test]
1222 fn test_snapshot_request_memory_exhaustion_prevention() {
1223 let request = SnapshotRequest::compressed().with_max_page_size(u32::MAX);
1225 assert_eq!(request.validated_page_size(), MAX_SNAPSHOT_PAGE_SIZE);
1226 }
1227
1228 #[test]
1229 fn test_snapshot_stream_request_memory_exhaustion_prevention() {
1230 let request = SnapshotStreamRequest {
1232 context_id: ContextId::zero(),
1233 boundary_root_hash: Hash::default(),
1234 page_limit: u16::MAX,
1235 byte_limit: u32::MAX,
1236 resume_cursor: None,
1237 };
1238 assert_eq!(request.validated_byte_limit(), MAX_SNAPSHOT_PAGE_SIZE);
1239 }
1240
1241 #[test]
1242 fn test_snapshot_entity_page_cross_validation() {
1243 let invalid_entity = make_entity(1, vec![0u8; MAX_ENTITY_DATA_SIZE + 1]);
1245 let page = SnapshotEntityPage::new(0, 1, vec![invalid_entity], true);
1246 assert!(!page.is_valid());
1247
1248 let valid_entity = make_entity(1, vec![1, 2, 3]);
1250 let invalid_entity = make_entity(2, vec![0u8; MAX_ENTITY_DATA_SIZE + 1]);
1251 let mixed_page = SnapshotEntityPage::new(0, 1, vec![valid_entity, invalid_entity], true);
1252 assert!(!mixed_page.is_valid());
1253 }
1254
1255 #[test]
1256 fn test_snapshot_complete_compression_ratio_zero_uncompressed() {
1257 let complete = SnapshotComplete::new([1; 32], 0, 0, 0).with_compressed_size(100);
1259
1260 let ratio = complete.compression_ratio().unwrap();
1261 assert_eq!(ratio, 100.0);
1263 }
1264
1265 #[test]
1270 fn test_snapshot_entity_all_zeros() {
1271 let entity = SnapshotEntity::new([0u8; 32], vec![], make_metadata(), [0u8; 32]);
1272 assert!(entity.is_valid());
1273 assert!(entity.is_root());
1274 assert!(entity.data.is_empty());
1275
1276 let encoded = borsh::to_vec(&entity).expect("serialize");
1278 let decoded: SnapshotEntity = borsh::from_slice(&encoded).expect("deserialize");
1279 assert_eq!(entity, decoded);
1280 }
1281
1282 #[test]
1283 fn test_snapshot_entity_all_ones() {
1284 let entity = SnapshotEntity::new([0xFF; 32], vec![0xFF; 100], make_metadata(), [0xFF; 32])
1285 .with_parent([0xFF; 32]);
1286 assert!(entity.is_valid());
1287 assert!(!entity.is_root());
1288
1289 let encoded = borsh::to_vec(&entity).expect("serialize");
1291 let decoded: SnapshotEntity = borsh::from_slice(&encoded).expect("deserialize");
1292 assert_eq!(entity, decoded);
1293 }
1294
1295 #[test]
1296 fn test_snapshot_complete_all_zeros() {
1297 let complete = SnapshotComplete::new([0u8; 32], 0, 0, 0);
1298 assert!(complete.is_valid());
1299 assert!(complete.compression_ratio().is_none());
1300 assert!(complete.dag_heads.is_empty());
1301
1302 let encoded = borsh::to_vec(&complete).expect("serialize");
1304 let decoded: SnapshotComplete = borsh::from_slice(&encoded).expect("deserialize");
1305 assert_eq!(complete, decoded);
1306 }
1307
1308 #[test]
1309 fn test_snapshot_complete_max_values() {
1310 let complete = SnapshotComplete::new([0xFF; 32], usize::MAX, MAX_SNAPSHOT_PAGES, u64::MAX)
1311 .with_compressed_size(u64::MAX);
1312 assert!(complete.is_valid());
1313
1314 let encoded = borsh::to_vec(&complete).expect("serialize");
1316 let decoded: SnapshotComplete = borsh::from_slice(&encoded).expect("deserialize");
1317 assert_eq!(complete, decoded);
1318 }
1319
1320 #[test]
1321 fn test_snapshot_request_all_flags() {
1322 let compressed = SnapshotRequest::compressed();
1324 assert!(compressed.compressed);
1325 assert!(compressed.is_fresh_node);
1326
1327 let uncompressed = SnapshotRequest::uncompressed();
1328 assert!(!uncompressed.compressed);
1329 assert!(uncompressed.is_fresh_node);
1330
1331 let mut not_fresh = SnapshotRequest::compressed();
1333 not_fresh.is_fresh_node = false;
1334 assert!(!not_fresh.is_fresh_node);
1335 }
1336
1337 #[test]
1342 fn test_snapshot_entity_page_with_many_entities_roundtrip() {
1343 let entities: Vec<SnapshotEntity> = (0..1000)
1345 .map(|i| make_entity((i % 256) as u8, vec![(i % 256) as u8; 10]))
1346 .collect();
1347 let page = SnapshotEntityPage::new(5, 100, entities, false);
1348 assert!(page.is_valid());
1349
1350 let encoded = borsh::to_vec(&page).expect("serialize");
1351 let decoded: SnapshotEntityPage = borsh::from_slice(&encoded).expect("deserialize");
1352
1353 assert_eq!(page, decoded);
1354 assert_eq!(decoded.entity_count(), 1000);
1355 }
1356
1357 #[test]
1358 fn test_snapshot_page_with_large_cursor_roundtrip() {
1359 let page = SnapshotPage {
1360 payload: vec![1; 1000],
1361 uncompressed_len: 5000,
1362 cursor: Some(vec![0xAB; 256]), page_count: 1000,
1364 sent_count: 500,
1365 };
1366 assert!(page.is_valid());
1367
1368 let encoded = borsh::to_vec(&page).expect("serialize");
1369 let decoded: SnapshotPage = borsh::from_slice(&encoded).expect("deserialize");
1370
1371 assert_eq!(page, decoded);
1372 assert!(!decoded.is_last());
1373 }
1374
1375 #[test]
1376 fn test_snapshot_verify_result_all_variants_behavior() {
1377 assert!(SnapshotVerifyResult::Valid.is_valid());
1379 assert!(!SnapshotVerifyResult::RootHashMismatch {
1380 expected: [1; 32],
1381 computed: [2; 32]
1382 }
1383 .is_valid());
1384 assert!(!SnapshotVerifyResult::EntityCountMismatch {
1385 expected: 100,
1386 actual: 50
1387 }
1388 .is_valid());
1389 assert!(!SnapshotVerifyResult::MissingPages { missing: vec![1] }.is_valid());
1390
1391 assert!(SnapshotVerifyResult::Valid.to_error().is_none());
1393 assert!(SnapshotVerifyResult::RootHashMismatch {
1394 expected: [1; 32],
1395 computed: [2; 32]
1396 }
1397 .to_error()
1398 .is_some());
1399 assert!(SnapshotVerifyResult::EntityCountMismatch {
1400 expected: 100,
1401 actual: 50
1402 }
1403 .to_error()
1404 .is_some());
1405 assert!(SnapshotVerifyResult::MissingPages { missing: vec![1] }
1406 .to_error()
1407 .is_some());
1408 }
1409
1410 #[test]
1415 fn test_snapshot_entity_empty_data() {
1416 let entity = make_entity(1, vec![]);
1417 assert!(entity.is_valid());
1418 assert!(entity.data.is_empty());
1419 }
1420
1421 #[test]
1422 fn test_snapshot_complete_empty_dag_heads() {
1423 let complete = SnapshotComplete::new([1; 32], 100, 1, 1000);
1424 assert!(complete.dag_heads.is_empty());
1425 assert!(complete.is_valid());
1426 }
1427
1428 #[test]
1429 fn test_snapshot_boundary_response_empty_dag_heads() {
1430 let response = SnapshotBoundaryResponse {
1431 boundary_timestamp: 12345,
1432 boundary_root_hash: Hash::default(),
1433 dag_heads: vec![],
1434 };
1435 assert!(response.is_valid());
1436 }
1437
1438 #[test]
1439 fn test_snapshot_verify_result_missing_pages_empty() {
1440 let result = SnapshotVerifyResult::MissingPages { missing: vec![] };
1442 assert!(!result.is_valid()); assert!(result.to_error().is_some());
1444 }
1445
1446 #[test]
1451 fn test_invariant_i5_snapshot_safety() {
1452 assert!(check_snapshot_safety(false).is_ok());
1456
1457 let err = check_snapshot_safety(true).unwrap_err();
1459 assert!(matches!(err, SnapshotError::SnapshotOnInitializedNode));
1460 }
1461
1462 #[test]
1463 fn test_invariant_i7_verification_errors() {
1464 let result = SnapshotVerifyResult::RootHashMismatch {
1468 expected: [1; 32],
1469 computed: [2; 32],
1470 };
1471 let error = result.to_error().unwrap();
1472 match error {
1473 SnapshotError::RootHashMismatch { expected, computed } => {
1474 assert_eq!(expected, [1; 32]);
1475 assert_eq!(computed, [2; 32]);
1476 }
1477 _ => panic!("Expected RootHashMismatch error"),
1478 }
1479 }
1480
1481 #[test]
1482 fn test_snapshot_error_transfer_interrupted_preserves_count() {
1483 let error = SnapshotError::TransferInterrupted { pages_received: 42 };
1484 let encoded = borsh::to_vec(&error).expect("serialize");
1485 let decoded: SnapshotError = borsh::from_slice(&encoded).expect("deserialize");
1486
1487 match decoded {
1488 SnapshotError::TransferInterrupted { pages_received } => {
1489 assert_eq!(pages_received, 42);
1490 }
1491 _ => panic!("Expected TransferInterrupted"),
1492 }
1493 }
1494
1495 #[test]
1496 fn test_snapshot_cursor_roundtrip() {
1497 let cursor = SnapshotCursor {
1498 last_key: [0xAB; 32],
1499 };
1500
1501 let encoded = borsh::to_vec(&cursor).expect("serialize");
1502 let decoded: SnapshotCursor = borsh::from_slice(&encoded).expect("deserialize");
1503
1504 assert_eq!(cursor, decoded);
1505 assert_eq!(decoded.last_key, [0xAB; 32]);
1506 }
1507
1508 #[test]
1509 fn test_snapshot_boundary_request_roundtrip() {
1510 let request = SnapshotBoundaryRequest {
1511 context_id: ContextId::zero(),
1512 requested_cutoff_timestamp: Some(1234567890),
1513 };
1514
1515 let encoded = borsh::to_vec(&request).expect("serialize");
1516 let decoded: SnapshotBoundaryRequest = borsh::from_slice(&encoded).expect("deserialize");
1517
1518 assert_eq!(request, decoded);
1519 assert_eq!(decoded.requested_cutoff_timestamp, Some(1234567890));
1520
1521 let request_none = SnapshotBoundaryRequest {
1523 context_id: ContextId::zero(),
1524 requested_cutoff_timestamp: None,
1525 };
1526 let encoded = borsh::to_vec(&request_none).expect("serialize");
1527 let decoded: SnapshotBoundaryRequest = borsh::from_slice(&encoded).expect("deserialize");
1528 assert_eq!(request_none, decoded);
1529 assert!(decoded.requested_cutoff_timestamp.is_none());
1530 }
1531}