1use rvm_types::{OwnedRegionId, RvmError, RvmResult};
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
26pub struct CheckpointId(u64);
27
28impl CheckpointId {
29 #[must_use]
31 pub const fn new(id: u64) -> Self {
32 Self(id)
33 }
34
35 #[must_use]
37 pub const fn as_u64(self) -> u64 {
38 self.0
39 }
40}
41
42#[derive(Debug, Clone, Copy)]
47pub struct WitnessDelta {
48 pub sequence: u64,
50 pub offset: u32,
52 pub length: u16,
54 pub data_hash: u64,
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub struct CompressedCheckpoint {
64 pub id: CheckpointId,
66 pub region_id: OwnedRegionId,
68 pub witness_sequence: u64,
70 pub uncompressed_hash: u64,
72 pub uncompressed_size: u32,
74 pub compressed_size: u32,
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub struct ReconstructionResult {
81 pub region_id: OwnedRegionId,
83 pub size_bytes: u32,
85 pub deltas_applied: u32,
87 pub final_hash: u64,
89}
90
91pub struct ReconstructionPipeline<const MAX_DELTAS: usize> {
99 deltas: [Option<WitnessDelta>; MAX_DELTAS],
101 delta_count: usize,
103}
104
105impl<const MAX_DELTAS: usize> Default for ReconstructionPipeline<MAX_DELTAS> {
106 fn default() -> Self {
107 Self::new()
108 }
109}
110
111impl<const MAX_DELTAS: usize> ReconstructionPipeline<MAX_DELTAS> {
112 const EMPTY_DELTA: Option<WitnessDelta> = None;
114
115 #[must_use]
117 pub const fn new() -> Self {
118 Self {
119 deltas: [Self::EMPTY_DELTA; MAX_DELTAS],
120 delta_count: 0,
121 }
122 }
123
124 #[must_use]
126 pub const fn delta_count(&self) -> usize {
127 self.delta_count
128 }
129
130 pub fn clear(&mut self) {
132 self.deltas.fill(None);
133 self.delta_count = 0;
134 }
135
136 pub fn add_delta(&mut self, delta: WitnessDelta) -> RvmResult<()> {
145 if self.delta_count >= MAX_DELTAS {
146 return Err(RvmError::ResourceLimitExceeded);
147 }
148
149 if self.delta_count > 0 {
151 if let Some(last) = &self.deltas[self.delta_count - 1] {
152 if delta.sequence <= last.sequence {
153 return Err(RvmError::WitnessChainBroken);
154 }
155 }
156 }
157
158 self.deltas[self.delta_count] = Some(delta);
159 self.delta_count += 1;
160 Ok(())
161 }
162
163 pub fn reconstruct<F>(
182 &self,
183 checkpoint: &CompressedCheckpoint,
184 compressed_data: &[u8],
185 output: &mut [u8],
186 delta_data_fn: F,
187 ) -> RvmResult<ReconstructionResult>
188 where
189 F: Fn(&WitnessDelta) -> &[u8],
190 {
191 let uncompressed_size = checkpoint.uncompressed_size as usize;
192
193 if compressed_data.len() < checkpoint.compressed_size as usize {
195 return Err(RvmError::CheckpointCorrupted);
196 }
197 if output.len() < uncompressed_size {
198 return Err(RvmError::ResourceLimitExceeded);
199 }
200
201 let decompressed_size = decompress(
203 &compressed_data[..checkpoint.compressed_size as usize],
204 &mut output[..uncompressed_size],
205 )?;
206
207 if decompressed_size != uncompressed_size {
208 return Err(RvmError::CheckpointCorrupted);
209 }
210
211 let hash = fnv1a_hash(&output[..uncompressed_size]);
213 if hash != checkpoint.uncompressed_hash {
214 return Err(RvmError::CheckpointCorrupted);
215 }
216
217 let mut deltas_applied = 0u32;
219 for i in 0..self.delta_count {
220 if let Some(delta) = &self.deltas[i] {
221 let data = delta_data_fn(delta);
222
223 let end = delta.offset as usize + delta.length as usize;
225 if end > uncompressed_size {
226 return Err(RvmError::CheckpointCorrupted);
227 }
228 if data.len() < delta.length as usize {
229 return Err(RvmError::CheckpointCorrupted);
230 }
231
232 let data_hash = fnv1a_hash(&data[..delta.length as usize]);
234 if data_hash != delta.data_hash {
235 return Err(RvmError::WitnessVerificationFailed);
236 }
237
238 let offset = delta.offset as usize;
240 let length = delta.length as usize;
241 output[offset..offset + length].copy_from_slice(&data[..length]);
242 deltas_applied += 1;
243 }
244 }
245
246 let final_hash = fnv1a_hash(&output[..uncompressed_size]);
248
249 #[allow(clippy::cast_possible_truncation)]
250 Ok(ReconstructionResult {
251 region_id: checkpoint.region_id,
252 size_bytes: uncompressed_size as u32,
253 deltas_applied,
254 final_hash,
255 })
256 }
257}
258
259pub fn create_checkpoint(
278 region_id: OwnedRegionId,
279 checkpoint_id: CheckpointId,
280 witness_sequence: u64,
281 data: &[u8],
282 compressed_out: &mut [u8],
283) -> RvmResult<(CompressedCheckpoint, usize)> {
284 if data.is_empty() {
285 return Err(RvmError::ResourceLimitExceeded);
286 }
287
288 let uncompressed_hash = fnv1a_hash(data);
289 let compressed_size = compress(data, compressed_out)?;
290
291 #[allow(clippy::cast_possible_truncation)]
292 let checkpoint = CompressedCheckpoint {
293 id: checkpoint_id,
294 region_id,
295 witness_sequence,
296 uncompressed_hash,
297 uncompressed_size: data.len() as u32,
298 compressed_size: compressed_size as u32,
299 };
300
301 Ok((checkpoint, compressed_size))
302}
303
304const TAG_ZERO_RUN: u8 = 0x00;
324const TAG_LITERAL: u8 = 0x01;
326
327fn compress(input: &[u8], output: &mut [u8]) -> RvmResult<usize> {
331 if output.len() < 4 {
333 return Err(RvmError::ResourceLimitExceeded);
334 }
335
336 #[allow(clippy::cast_possible_truncation)]
338 let len_bytes = (input.len() as u32).to_le_bytes();
339 output[0..4].copy_from_slice(&len_bytes);
340
341 let mut out_pos = 4;
342 let mut in_pos = 0;
343
344 while in_pos < input.len() {
345 if input[in_pos] == 0 {
346 let run_start = in_pos;
348 while in_pos < input.len() && input[in_pos] == 0 && (in_pos - run_start) < 0xFFFF {
349 in_pos += 1;
350 }
351 let run_len = in_pos - run_start;
352
353 if out_pos + 3 > output.len() {
355 return Err(RvmError::ResourceLimitExceeded);
356 }
357 output[out_pos] = TAG_ZERO_RUN;
358 #[allow(clippy::cast_possible_truncation)]
359 let rl = (run_len as u16).to_le_bytes();
360 output[out_pos + 1] = rl[0];
361 output[out_pos + 2] = rl[1];
362 out_pos += 3;
363 } else {
364 let lit_start = in_pos;
366 while in_pos < input.len() && input[in_pos] != 0 && (in_pos - lit_start) < 0xFFFF {
367 in_pos += 1;
368 }
369 let lit_len = in_pos - lit_start;
370
371 if out_pos + 3 + lit_len > output.len() {
373 return Err(RvmError::ResourceLimitExceeded);
374 }
375 output[out_pos] = TAG_LITERAL;
376 #[allow(clippy::cast_possible_truncation)]
377 let ll = (lit_len as u16).to_le_bytes();
378 output[out_pos + 1] = ll[0];
379 output[out_pos + 2] = ll[1];
380 output[out_pos + 3..out_pos + 3 + lit_len]
381 .copy_from_slice(&input[lit_start..lit_start + lit_len]);
382 out_pos += 3 + lit_len;
383 }
384 }
385
386 Ok(out_pos)
387}
388
389fn decompress(input: &[u8], output: &mut [u8]) -> RvmResult<usize> {
391 if input.len() < 4 {
392 return Err(RvmError::CheckpointCorrupted);
393 }
394
395 let mut len_bytes = [0u8; 4];
396 len_bytes.copy_from_slice(&input[0..4]);
397 let uncompressed_len = u32::from_le_bytes(len_bytes) as usize;
398
399 if output.len() < uncompressed_len {
400 return Err(RvmError::ResourceLimitExceeded);
401 }
402
403 let mut in_pos = 4;
404 let mut out_pos = 0;
405
406 while in_pos < input.len() && out_pos < uncompressed_len {
407 if in_pos + 3 > input.len() {
408 return Err(RvmError::CheckpointCorrupted);
409 }
410 let tag = input[in_pos];
411 let block_len = u16::from_le_bytes([input[in_pos + 1], input[in_pos + 2]]) as usize;
412 in_pos += 3;
413
414 match tag {
415 TAG_ZERO_RUN => {
416 if out_pos + block_len > uncompressed_len {
417 return Err(RvmError::CheckpointCorrupted);
418 }
419 for b in &mut output[out_pos..out_pos + block_len] {
420 *b = 0;
421 }
422 out_pos += block_len;
423 }
424 TAG_LITERAL => {
425 if in_pos + block_len > input.len() {
426 return Err(RvmError::CheckpointCorrupted);
427 }
428 if out_pos + block_len > uncompressed_len {
429 return Err(RvmError::CheckpointCorrupted);
430 }
431 output[out_pos..out_pos + block_len]
432 .copy_from_slice(&input[in_pos..in_pos + block_len]);
433 in_pos += block_len;
434 out_pos += block_len;
435 }
436 _ => {
437 return Err(RvmError::CheckpointCorrupted);
438 }
439 }
440 }
441
442 if out_pos != uncompressed_len {
443 return Err(RvmError::CheckpointCorrupted);
444 }
445
446 Ok(uncompressed_len)
447}
448
449fn fnv1a_hash(data: &[u8]) -> u64 {
451 const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
452 const FNV_PRIME: u64 = 0x0100_0000_01b3;
453
454 let mut hash = FNV_OFFSET;
455 for &byte in data {
456 hash ^= u64::from(byte);
457 hash = hash.wrapping_mul(FNV_PRIME);
458 }
459 hash
460}
461
462#[cfg(test)]
463mod tests {
464 use super::*;
465
466 fn rid(id: u64) -> OwnedRegionId {
467 OwnedRegionId::new(id)
468 }
469
470 #[test]
471 fn compress_decompress_round_trip() {
472 let data = b"Hello, dormant memory reconstruction!";
473 let mut compressed = [0u8; 256];
474 let compressed_len = compress(data, &mut compressed).unwrap();
475 assert_eq!(compressed_len, data.len() + 4 + 3);
478
479 let mut decompressed = [0u8; 256];
480 let decompressed_len =
481 decompress(&compressed[..compressed_len], &mut decompressed).unwrap();
482 assert_eq!(decompressed_len, data.len());
483 assert_eq!(&decompressed[..decompressed_len], data.as_slice());
484 }
485
486 #[test]
487 fn compress_empty_output_fails() {
488 let data = b"data";
489 let mut out = [0u8; 2];
490 assert_eq!(
491 compress(data, &mut out),
492 Err(RvmError::ResourceLimitExceeded)
493 );
494 }
495
496 #[test]
497 fn decompress_truncated_fails() {
498 let input = [0u8; 2]; let mut out = [0u8; 256];
500 assert_eq!(
501 decompress(&input, &mut out),
502 Err(RvmError::CheckpointCorrupted)
503 );
504 }
505
506 #[test]
507 fn fnv1a_hash_deterministic() {
508 let data = b"test data";
509 let h1 = fnv1a_hash(data);
510 let h2 = fnv1a_hash(data);
511 assert_eq!(h1, h2);
512 }
513
514 #[test]
515 fn fnv1a_hash_different_data() {
516 let h1 = fnv1a_hash(b"alpha");
517 let h2 = fnv1a_hash(b"beta");
518 assert_ne!(h1, h2);
519 }
520
521 #[test]
522 fn checkpoint_creation() {
523 let data = b"region state snapshot";
524 let mut compressed = [0u8; 256];
525 let (ckpt, csize) =
526 create_checkpoint(rid(1), CheckpointId::new(100), 42, data, &mut compressed).unwrap();
527
528 assert_eq!(ckpt.id, CheckpointId::new(100));
529 assert_eq!(ckpt.region_id, rid(1));
530 assert_eq!(ckpt.witness_sequence, 42);
531 assert_eq!(ckpt.uncompressed_size, u32::try_from(data.len()).unwrap());
532 assert_eq!(ckpt.compressed_size, u32::try_from(csize).unwrap());
533 assert_eq!(ckpt.uncompressed_hash, fnv1a_hash(data));
534 }
535
536 #[test]
537 fn checkpoint_empty_data_fails() {
538 let mut compressed = [0u8; 256];
539 assert!(matches!(
540 create_checkpoint(rid(1), CheckpointId::new(1), 0, &[], &mut compressed),
541 Err(RvmError::ResourceLimitExceeded)
542 ));
543 }
544
545 #[test]
546 fn pipeline_no_deltas() {
547 let pipeline = ReconstructionPipeline::<16>::new();
548
549 let data = b"original state";
550 let mut compressed = [0u8; 256];
551 let (ckpt, csize) =
552 create_checkpoint(rid(1), CheckpointId::new(1), 0, data, &mut compressed).unwrap();
553
554 let mut output = [0u8; 256];
555 let result = pipeline
556 .reconstruct(&ckpt, &compressed[..csize], &mut output, |_| &[])
557 .unwrap();
558
559 assert_eq!(result.region_id, rid(1));
560 assert_eq!(result.size_bytes, u32::try_from(data.len()).unwrap());
561 assert_eq!(result.deltas_applied, 0);
562 assert_eq!(&output[..data.len()], data.as_slice());
563 }
564
565 #[test]
566 fn pipeline_with_deltas() {
567 let mut pipeline = ReconstructionPipeline::<16>::new();
568
569 let data = b"Hello, World!!!"; let mut compressed = [0u8; 256];
571 let (ckpt, csize) =
572 create_checkpoint(rid(1), CheckpointId::new(1), 0, data, &mut compressed).unwrap();
573
574 let patch = b"Rust!";
576 let delta = WitnessDelta {
577 sequence: 1,
578 offset: 7,
579 length: 5,
580 data_hash: fnv1a_hash(patch),
581 };
582 pipeline.add_delta(delta).unwrap();
583
584 let mut output = [0u8; 256];
585 let result = pipeline
586 .reconstruct(&ckpt, &compressed[..csize], &mut output, |_d| {
587 patch.as_slice()
588 })
589 .unwrap();
590
591 assert_eq!(result.deltas_applied, 1);
592 assert_eq!(&output[..15], b"Hello, Rust!!!!");
593 }
594
595 #[test]
596 fn pipeline_multiple_deltas() {
597 static PATCH1: [u8; 4] = [0xAA, 0xAA, 0xAA, 0xAA];
598 static PATCH2: [u8; 4] = [0xBB, 0xBB, 0xBB, 0xBB];
599
600 let mut pipeline = ReconstructionPipeline::<16>::new();
601
602 let data = [0u8; 16];
603 let mut compressed = [0u8; 256];
604 let (ckpt, csize) =
605 create_checkpoint(rid(1), CheckpointId::new(1), 0, &data, &mut compressed).unwrap();
606
607 pipeline
609 .add_delta(WitnessDelta {
610 sequence: 1,
611 offset: 0,
612 length: 4,
613 data_hash: fnv1a_hash(&PATCH1),
614 })
615 .unwrap();
616
617 pipeline
619 .add_delta(WitnessDelta {
620 sequence: 2,
621 offset: 8,
622 length: 4,
623 data_hash: fnv1a_hash(&PATCH2),
624 })
625 .unwrap();
626
627 let mut output = [0u8; 256];
628 let result = pipeline
629 .reconstruct(&ckpt, &compressed[..csize], &mut output, |d| {
630 if d.sequence == 1 {
632 &PATCH1
633 } else {
634 &PATCH2
635 }
636 })
637 .unwrap();
638
639 assert_eq!(result.deltas_applied, 2);
640 assert_eq!(&output[0..4], &[0xAA; 4]);
641 assert_eq!(&output[4..8], &[0x00; 4]);
642 assert_eq!(&output[8..12], &[0xBB; 4]);
643 assert_eq!(&output[12..16], &[0x00; 4]);
644 }
645
646 #[test]
647 fn pipeline_out_of_order_delta_fails() {
648 let mut pipeline = ReconstructionPipeline::<16>::new();
649 pipeline
650 .add_delta(WitnessDelta {
651 sequence: 5,
652 offset: 0,
653 length: 1,
654 data_hash: 0,
655 })
656 .unwrap();
657 assert_eq!(
659 pipeline.add_delta(WitnessDelta {
660 sequence: 3,
661 offset: 0,
662 length: 1,
663 data_hash: 0,
664 }),
665 Err(RvmError::WitnessChainBroken)
666 );
667 }
668
669 #[test]
670 fn pipeline_overflow_fails() {
671 let mut pipeline = ReconstructionPipeline::<2>::new();
672 pipeline
673 .add_delta(WitnessDelta {
674 sequence: 1,
675 offset: 0,
676 length: 1,
677 data_hash: 0,
678 })
679 .unwrap();
680 pipeline
681 .add_delta(WitnessDelta {
682 sequence: 2,
683 offset: 0,
684 length: 1,
685 data_hash: 0,
686 })
687 .unwrap();
688 assert_eq!(
689 pipeline.add_delta(WitnessDelta {
690 sequence: 3,
691 offset: 0,
692 length: 1,
693 data_hash: 0,
694 }),
695 Err(RvmError::ResourceLimitExceeded)
696 );
697 }
698
699 #[test]
700 fn pipeline_clear() {
701 let mut pipeline = ReconstructionPipeline::<4>::new();
702 pipeline
703 .add_delta(WitnessDelta {
704 sequence: 1,
705 offset: 0,
706 length: 1,
707 data_hash: 0,
708 })
709 .unwrap();
710 assert_eq!(pipeline.delta_count(), 1);
711
712 pipeline.clear();
713 assert_eq!(pipeline.delta_count(), 0);
714 }
715
716 #[test]
717 fn reconstruction_corrupted_checkpoint_hash() {
718 let pipeline = ReconstructionPipeline::<16>::new();
719
720 let data = b"valid data";
721 let mut compressed = [0u8; 256];
722 let (mut ckpt, csize) =
723 create_checkpoint(rid(1), CheckpointId::new(1), 0, data, &mut compressed).unwrap();
724
725 ckpt.uncompressed_hash = 0xDEAD_BEEF;
727
728 let mut output = [0u8; 256];
729 assert_eq!(
730 pipeline.reconstruct(&ckpt, &compressed[..csize], &mut output, |_| &[]),
731 Err(RvmError::CheckpointCorrupted)
732 );
733 }
734
735 #[test]
736 fn reconstruction_delta_hash_mismatch() {
737 let mut pipeline = ReconstructionPipeline::<16>::new();
738
739 let data = b"some state";
740 let mut compressed = [0u8; 256];
741 let (ckpt, csize) =
742 create_checkpoint(rid(1), CheckpointId::new(1), 0, data, &mut compressed).unwrap();
743
744 pipeline
745 .add_delta(WitnessDelta {
746 sequence: 1,
747 offset: 0,
748 length: 4,
749 data_hash: 0xBAD_0000, })
751 .unwrap();
752
753 let patch = b"good";
754 let mut output = [0u8; 256];
755 assert_eq!(
756 pipeline.reconstruct(&ckpt, &compressed[..csize], &mut output, |_| patch
757 .as_slice()),
758 Err(RvmError::WitnessVerificationFailed)
759 );
760 }
761
762 #[test]
763 fn reconstruction_delta_out_of_bounds() {
764 let mut pipeline = ReconstructionPipeline::<16>::new();
765
766 let data = b"short";
767 let mut compressed = [0u8; 256];
768 let (ckpt, csize) =
769 create_checkpoint(rid(1), CheckpointId::new(1), 0, data, &mut compressed).unwrap();
770
771 let patch = b"overrun!";
772 pipeline
773 .add_delta(WitnessDelta {
774 sequence: 1,
775 offset: 3,
776 length: 8, data_hash: fnv1a_hash(patch),
778 })
779 .unwrap();
780
781 let mut output = [0u8; 256];
782 assert_eq!(
783 pipeline.reconstruct(&ckpt, &compressed[..csize], &mut output, |_| patch
784 .as_slice()),
785 Err(RvmError::CheckpointCorrupted)
786 );
787 }
788
789 #[test]
790 fn checkpoint_id_accessors() {
791 let id = CheckpointId::new(42);
792 assert_eq!(id.as_u64(), 42);
793 }
794
795 #[test]
800 fn reconstruction_at_max_delta_capacity() {
801 static PATCHES: [[u8; 1]; 4] = [[0xAA], [0xBB], [0xCC], [0xDD]];
802 let mut pipeline = ReconstructionPipeline::<4>::new();
804
805 let data = [0u8; 32];
806 let mut compressed = [0u8; 256];
807 let (ckpt, csize) =
808 create_checkpoint(rid(1), CheckpointId::new(1), 0, &data, &mut compressed).unwrap();
809
810 for (i, patch) in PATCHES.iter().enumerate() {
812 pipeline
813 .add_delta(WitnessDelta {
814 sequence: u64::try_from(i + 1).unwrap(),
815 offset: u32::try_from(i * 4).unwrap(),
816 length: 1,
817 data_hash: fnv1a_hash(patch),
818 })
819 .unwrap();
820 }
821 assert_eq!(pipeline.delta_count(), 4);
822
823 assert_eq!(
825 pipeline.add_delta(WitnessDelta {
826 sequence: 5,
827 offset: 20,
828 length: 1,
829 data_hash: 0,
830 }),
831 Err(RvmError::ResourceLimitExceeded)
832 );
833
834 let mut output = [0u8; 256];
836 let result = pipeline
837 .reconstruct(&ckpt, &compressed[..csize], &mut output, |d| {
838 &PATCHES[usize::try_from(d.sequence - 1).unwrap()]
839 })
840 .unwrap();
841
842 assert_eq!(result.deltas_applied, 4);
843 assert_eq!(output[0], 0xAA);
844 assert_eq!(output[4], 0xBB);
845 assert_eq!(output[8], 0xCC);
846 assert_eq!(output[12], 0xDD);
847 }
848
849 #[test]
850 fn reconstruction_single_delta_capacity() {
851 static PATCH_ZERO: [u8; 1] = [0x00];
852 let mut pipeline = ReconstructionPipeline::<1>::new();
853
854 let data = [0xFF; 8];
855 let mut compressed = [0u8; 64];
856 let (ckpt, csize) =
857 create_checkpoint(rid(1), CheckpointId::new(1), 0, &data, &mut compressed).unwrap();
858 pipeline
859 .add_delta(WitnessDelta {
860 sequence: 1,
861 offset: 0,
862 length: 1,
863 data_hash: fnv1a_hash(&PATCH_ZERO),
864 })
865 .unwrap();
866
867 assert_eq!(
869 pipeline.add_delta(WitnessDelta {
870 sequence: 2,
871 offset: 1,
872 length: 1,
873 data_hash: 0,
874 }),
875 Err(RvmError::ResourceLimitExceeded)
876 );
877
878 let mut output = [0u8; 64];
879 let result = pipeline
880 .reconstruct(&ckpt, &compressed[..csize], &mut output, |_| &PATCH_ZERO)
881 .unwrap();
882 assert_eq!(result.deltas_applied, 1);
883 assert_eq!(output[0], 0x00);
884 assert_eq!(output[1], 0xFF); }
886
887 #[test]
888 fn reconstruction_clear_allows_reuse() {
889 let mut pipeline = ReconstructionPipeline::<2>::new();
890
891 pipeline
892 .add_delta(WitnessDelta {
893 sequence: 1,
894 offset: 0,
895 length: 1,
896 data_hash: 0,
897 })
898 .unwrap();
899 pipeline
900 .add_delta(WitnessDelta {
901 sequence: 2,
902 offset: 0,
903 length: 1,
904 data_hash: 0,
905 })
906 .unwrap();
907 assert_eq!(pipeline.delta_count(), 2);
908
909 pipeline.clear();
910 assert_eq!(pipeline.delta_count(), 0);
911
912 pipeline
914 .add_delta(WitnessDelta {
915 sequence: 10,
916 offset: 0,
917 length: 1,
918 data_hash: 0,
919 })
920 .unwrap();
921 pipeline
922 .add_delta(WitnessDelta {
923 sequence: 11,
924 offset: 0,
925 length: 1,
926 data_hash: 0,
927 })
928 .unwrap();
929 assert_eq!(pipeline.delta_count(), 2);
930 }
931
932 #[test]
933 fn reconstruction_output_buffer_too_small() {
934 let pipeline = ReconstructionPipeline::<4>::new();
935
936 let data = [0u8; 32];
937 let mut compressed = [0u8; 256];
938 let (ckpt, csize) =
939 create_checkpoint(rid(1), CheckpointId::new(1), 0, &data, &mut compressed).unwrap();
940
941 let mut small_output = [0u8; 16];
943 assert_eq!(
944 pipeline.reconstruct(&ckpt, &compressed[..csize], &mut small_output, |_| &[]),
945 Err(RvmError::ResourceLimitExceeded)
946 );
947 }
948
949 #[test]
950 fn reconstruction_compressed_data_truncated() {
951 let pipeline = ReconstructionPipeline::<4>::new();
952
953 let data = [0u8; 32];
954 let mut compressed = [0u8; 256];
955 let (ckpt, _csize) =
956 create_checkpoint(rid(1), CheckpointId::new(1), 0, &data, &mut compressed).unwrap();
957
958 let mut output = [0u8; 256];
960 assert_eq!(
961 pipeline.reconstruct(&ckpt, &compressed[..2], &mut output, |_| &[]),
962 Err(RvmError::CheckpointCorrupted)
963 );
964 }
965
966 #[test]
967 fn reconstruction_delta_data_shorter_than_length() {
968 static SHORT_PATCH: [u8; 2] = [0xAA, 0xBB];
969 let mut pipeline = ReconstructionPipeline::<4>::new();
970
971 let data = [0u8; 16];
972 let mut compressed = [0u8; 256];
973 let (ckpt, csize) =
974 create_checkpoint(rid(1), CheckpointId::new(1), 0, &data, &mut compressed).unwrap();
975
976 pipeline
978 .add_delta(WitnessDelta {
979 sequence: 1,
980 offset: 0,
981 length: 4,
982 data_hash: fnv1a_hash(&SHORT_PATCH),
983 })
984 .unwrap();
985
986 let mut output = [0u8; 256];
987 assert_eq!(
988 pipeline.reconstruct(&ckpt, &compressed[..csize], &mut output, |_| &SHORT_PATCH),
989 Err(RvmError::CheckpointCorrupted)
990 );
991 }
992
993 #[test]
994 fn reconstruction_final_hash_changes_with_deltas() {
995 static XPATCH: [u8; 1] = [b'X'];
996 let data = b"original data!!"; let mut compressed = [0u8; 256];
998 let (ckpt, csize) =
999 create_checkpoint(rid(1), CheckpointId::new(1), 0, data, &mut compressed).unwrap();
1000
1001 let pipeline_no_deltas = ReconstructionPipeline::<4>::new();
1003 let mut out1 = [0u8; 256];
1004 let r1 = pipeline_no_deltas
1005 .reconstruct(&ckpt, &compressed[..csize], &mut out1, |_| &[])
1006 .unwrap();
1007
1008 let mut pipeline_with_delta = ReconstructionPipeline::<4>::new();
1010 pipeline_with_delta
1011 .add_delta(WitnessDelta {
1012 sequence: 1,
1013 offset: 0,
1014 length: 1,
1015 data_hash: fnv1a_hash(&XPATCH),
1016 })
1017 .unwrap();
1018 let mut out2 = [0u8; 256];
1019 let r2 = pipeline_with_delta
1020 .reconstruct(&ckpt, &compressed[..csize], &mut out2, |_| &XPATCH)
1021 .unwrap();
1022
1023 assert_ne!(r1.final_hash, r2.final_hash);
1025 }
1026
1027 #[test]
1028 fn reconstruction_overlapping_deltas() {
1029 static FIRST: [u8; 2] = [0xAA, 0xAA];
1030 static SECOND: [u8; 2] = [0xBB, 0xBB];
1031 let mut pipeline = ReconstructionPipeline::<4>::new();
1033
1034 let data = [0u8; 8];
1035 let mut compressed = [0u8; 64];
1036 let (ckpt, csize) =
1037 create_checkpoint(rid(1), CheckpointId::new(1), 0, &data, &mut compressed).unwrap();
1038
1039 pipeline
1040 .add_delta(WitnessDelta {
1041 sequence: 1,
1042 offset: 0,
1043 length: 2,
1044 data_hash: fnv1a_hash(&FIRST),
1045 })
1046 .unwrap();
1047 pipeline
1048 .add_delta(WitnessDelta {
1049 sequence: 2,
1050 offset: 0,
1051 length: 2,
1052 data_hash: fnv1a_hash(&SECOND),
1053 })
1054 .unwrap();
1055
1056 let mut output = [0u8; 64];
1057 let result = pipeline
1058 .reconstruct(&ckpt, &compressed[..csize], &mut output, |d| {
1059 if d.sequence == 1 {
1060 &FIRST
1061 } else {
1062 &SECOND
1063 }
1064 })
1065 .unwrap();
1066
1067 assert_eq!(result.deltas_applied, 2);
1068 assert_eq!(&output[0..2], &[0xBB, 0xBB]);
1070 }
1071
1072 #[test]
1077 fn compress_decompress_rle_round_trip() {
1078 let data = b"Hello, dormant memory reconstruction!";
1079 let mut compressed = [0u8; 256];
1080 let compressed_len = compress(data, &mut compressed).unwrap();
1081
1082 let mut decompressed = [0u8; 256];
1083 let decompressed_len =
1084 decompress(&compressed[..compressed_len], &mut decompressed).unwrap();
1085 assert_eq!(decompressed_len, data.len());
1086 assert_eq!(&decompressed[..decompressed_len], data.as_slice());
1087 }
1088
1089 #[test]
1090 fn compress_zero_heavy_data_achieves_ratio() {
1091 let mut data = [0u8; 1024];
1093 data[0] = 0xAA;
1095 data[512] = 0xBB;
1096 data[1023] = 0xCC;
1097
1098 let mut compressed = [0u8; 1024];
1099 let compressed_len = compress(&data, &mut compressed).unwrap();
1100
1101 assert!(
1104 compressed_len < data.len() / 2,
1105 "compressed {compressed_len} should be less than {}",
1106 data.len() / 2
1107 );
1108
1109 let mut decompressed = [0u8; 1024];
1111 let decompressed_len =
1112 decompress(&compressed[..compressed_len], &mut decompressed).unwrap();
1113 assert_eq!(decompressed_len, 1024);
1114 assert_eq!(&decompressed[..], &data[..]);
1115 }
1116
1117 #[test]
1118 fn compress_all_zeros() {
1119 let data = [0u8; 512];
1120 let mut compressed = [0u8; 64];
1121 let compressed_len = compress(&data, &mut compressed).unwrap();
1122
1123 assert_eq!(compressed_len, 7);
1125
1126 let mut decompressed = [0u8; 512];
1127 let decompressed_len =
1128 decompress(&compressed[..compressed_len], &mut decompressed).unwrap();
1129 assert_eq!(decompressed_len, 512);
1130 assert_eq!(&decompressed[..], &data[..]);
1131 }
1132
1133 #[test]
1134 fn compress_all_nonzero() {
1135 let data = [0xFFu8; 64];
1137 let mut compressed = [0u8; 256];
1138 let compressed_len = compress(&data, &mut compressed).unwrap();
1139
1140 assert_eq!(compressed_len, 4 + 3 + 64);
1142
1143 let mut decompressed = [0u8; 64];
1144 let decompressed_len =
1145 decompress(&compressed[..compressed_len], &mut decompressed).unwrap();
1146 assert_eq!(decompressed_len, 64);
1147 assert_eq!(&decompressed[..], &data[..]);
1148 }
1149
1150 #[test]
1151 fn compress_alternating_zero_nonzero() {
1152 let data = [0, 0xAA, 0, 0xBB, 0, 0xCC];
1154 let mut compressed = [0u8; 128];
1155 let compressed_len = compress(&data, &mut compressed).unwrap();
1156
1157 let mut decompressed = [0u8; 128];
1158 let decompressed_len =
1159 decompress(&compressed[..compressed_len], &mut decompressed).unwrap();
1160 assert_eq!(decompressed_len, data.len());
1161 assert_eq!(&decompressed[..data.len()], &data[..]);
1162 }
1163
1164 #[test]
1165 fn decompress_invalid_tag() {
1166 let mut bad = [0u8; 16];
1168 bad[0..4].copy_from_slice(&4u32.to_le_bytes());
1170 bad[4] = 0xFF; bad[5] = 4;
1172 bad[6] = 0;
1173
1174 let mut output = [0u8; 16];
1175 assert_eq!(
1176 decompress(&bad[..7], &mut output),
1177 Err(RvmError::CheckpointCorrupted)
1178 );
1179 }
1180
1181 #[test]
1182 fn compress_empty_input_round_trip() {
1183 let data: [u8; 0] = [];
1185 let mut compressed = [0u8; 16];
1186 let compressed_len = compress(&data, &mut compressed).unwrap();
1187 assert_eq!(compressed_len, 4);
1188
1189 let mut decompressed = [0u8; 1];
1190 let decompressed_len =
1191 decompress(&compressed[..compressed_len], &mut decompressed).unwrap();
1192 assert_eq!(decompressed_len, 0);
1193 }
1194}