1use crate::error::{Result, StorageError};
35use crate::kv::{KvStore, RocksDbStore, WriteOp, CF_SNAPSHOTS};
36use flate2::read::{GzDecoder, GzEncoder};
37use flate2::Compression;
38use parking_lot::RwLock;
39use rocksdb::checkpoint::Checkpoint;
40use serde::{Deserialize, Serialize};
41use std::io::Read;
42use std::path::Path;
43use std::sync::Arc;
44use tenzro_types::{BlockHeight, Hash, Timestamp};
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct Snapshot {
49 pub height: BlockHeight,
51 pub state_root: Hash,
53 pub block_hash: Hash,
55 pub timestamp: Timestamp,
57 pub metadata: SnapshotMetadata,
59 pub state_data: Vec<u8>,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct SnapshotMetadata {
66 pub account_count: u64,
68 pub state_size: u64,
70 pub compressed_size: u64,
72 pub compression: CompressionType,
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
78pub enum CompressionType {
79 None,
81 Lz4,
83 Zstd,
85 Gzip,
87}
88
89fn compress_gzip(data: &[u8]) -> Result<Vec<u8>> {
94 let mut encoder = GzEncoder::new(data, Compression::default());
95 let mut compressed = Vec::new();
96 encoder.read_to_end(&mut compressed).map_err(|e| {
97 StorageError::InvalidSnapshot(format!("gzip compression failed: {}", e))
98 })?;
99 Ok(compressed)
100}
101
102fn decompress_gzip(data: &[u8]) -> Result<Vec<u8>> {
107 let mut decoder = GzDecoder::new(data);
108 let mut decompressed = Vec::new();
109 decoder.read_to_end(&mut decompressed).map_err(|e| {
110 StorageError::InvalidSnapshot(format!("gzip decompression failed: {}", e))
111 })?;
112 Ok(decompressed)
113}
114
115pub struct SnapshotManager<K: KvStore> {
117 kv_store: Arc<K>,
118 retention_count: Arc<RwLock<u64>>,
119 state_lock: Arc<RwLock<()>>,
123}
124
125impl<K: KvStore> SnapshotManager<K> {
126 pub fn new(kv_store: Arc<K>, retention_count: u64) -> Self {
128 Self {
129 kv_store,
130 retention_count: Arc::new(RwLock::new(retention_count)),
131 state_lock: Arc::new(RwLock::new(())),
132 }
133 }
134
135 pub fn state_lock(&self) -> Arc<RwLock<()>> {
153 self.state_lock.clone()
154 }
155
156 #[allow(clippy::await_holding_lock)]
177 pub async fn create_consistent_snapshot<F>(
178 &self,
179 height: BlockHeight,
180 state_root: Hash,
181 block_hash: Hash,
182 collect_state: F,
183 ) -> Result<Snapshot>
184 where
185 F: FnOnce() -> Vec<u8>,
186 {
187 let _guard = self.state_lock.write();
190
191 let state_data = collect_state();
193
194 let state_size = state_data.len() as u64;
195 let compressed_data = compress_gzip(&state_data)?;
196 let compressed_size = compressed_data.len() as u64;
197
198 let snapshot = Snapshot {
199 height,
200 state_root,
201 block_hash,
202 timestamp: Timestamp::now(),
203 metadata: SnapshotMetadata {
204 account_count: 0,
205 state_size,
206 compressed_size,
207 compression: CompressionType::Gzip,
208 },
209 state_data: compressed_data,
210 };
211
212 self.store_snapshot(&snapshot).await?;
214
215 self.prune_snapshots(height).await?;
217
218 Ok(snapshot)
219 }
220
221 #[allow(clippy::await_holding_lock)]
242 pub async fn create_snapshot(
243 &self,
244 height: BlockHeight,
245 state_root: Hash,
246 block_hash: Hash,
247 state_data: Vec<u8>,
248 ) -> Result<Snapshot> {
249 let _guard = self.state_lock.read();
251
252 let state_size = state_data.len() as u64;
253
254 let compressed_data = compress_gzip(&state_data)?;
256 let compressed_size = compressed_data.len() as u64;
257
258 let snapshot = Snapshot {
259 height,
260 state_root,
261 block_hash,
262 timestamp: Timestamp::now(),
263 metadata: SnapshotMetadata {
264 account_count: 0, state_size,
266 compressed_size,
267 compression: CompressionType::Gzip,
268 },
269 state_data: compressed_data,
270 };
271
272 self.store_snapshot(&snapshot).await?;
274
275 self.prune_snapshots(height).await?;
277
278 Ok(snapshot)
279 }
280
281 async fn store_snapshot(&self, snapshot: &Snapshot) -> Result<()> {
283 let key = Self::snapshot_key(snapshot.height);
284 let data = bincode::serialize(snapshot)?;
285 self.kv_store.put(CF_SNAPSHOTS, &key, &data)?;
286
287 let index_key = Self::snapshot_index_key();
289 let mut index = self.load_snapshot_index().await?;
290 index.push(snapshot.height);
291 index.sort_by_key(|h| h.0);
292 let index_data = bincode::serialize(&index)?;
293 self.kv_store.put(CF_SNAPSHOTS, &index_key, &index_data)?;
294
295 Ok(())
296 }
297
298 pub async fn load_snapshot(&self, height: BlockHeight) -> Result<Option<Snapshot>> {
300 let key = Self::snapshot_key(height);
301 if let Some(data) = self.kv_store.get(CF_SNAPSHOTS, &key)? {
302 let snapshot: Snapshot = bincode::deserialize(&data)?;
303 Ok(Some(snapshot))
304 } else {
305 Ok(None)
306 }
307 }
308
309 pub async fn delete_snapshot(&self, height: BlockHeight) -> Result<()> {
311 let key = Self::snapshot_key(height);
312 self.kv_store.delete(CF_SNAPSHOTS, &key)?;
313
314 let index_key = Self::snapshot_index_key();
316 let mut index = self.load_snapshot_index().await?;
317 index.retain(|h| *h != height);
318 let index_data = bincode::serialize(&index)?;
319 self.kv_store.put(CF_SNAPSHOTS, &index_key, &index_data)?;
320
321 Ok(())
322 }
323
324 pub async fn list_snapshots(&self) -> Result<Vec<BlockHeight>> {
326 self.load_snapshot_index().await
327 }
328
329 pub async fn latest_snapshot(&self) -> Result<Option<Snapshot>> {
331 let index = self.load_snapshot_index().await?;
332 if let Some(height) = index.last() {
333 self.load_snapshot(*height).await
334 } else {
335 Ok(None)
336 }
337 }
338
339 async fn prune_snapshots(&self, _current_height: BlockHeight) -> Result<()> {
341 let retention = *self.retention_count.read();
342 let index = self.load_snapshot_index().await?;
343
344 if index.len() as u64 <= retention {
345 return Ok(());
346 }
347
348 let to_delete = index.len() as u64 - retention;
350 let mut ops = Vec::new();
351
352 for height in index.iter().take(to_delete as usize) {
353 let key = Self::snapshot_key(*height);
354 ops.push(WriteOp::Delete {
355 cf: CF_SNAPSHOTS.to_string(),
356 key,
357 });
358 }
359
360 let new_index: Vec<BlockHeight> = index.into_iter().skip(to_delete as usize).collect();
362 let index_key = Self::snapshot_index_key();
363 let index_data = bincode::serialize(&new_index)?;
364 ops.push(WriteOp::Put {
365 cf: CF_SNAPSHOTS.to_string(),
366 key: index_key,
367 value: index_data,
368 });
369
370 self.kv_store.write_batch(ops)?;
371
372 Ok(())
373 }
374
375 async fn load_snapshot_index(&self) -> Result<Vec<BlockHeight>> {
377 let key = Self::snapshot_index_key();
378 if let Some(data) = self.kv_store.get(CF_SNAPSHOTS, &key)? {
379 let index: Vec<BlockHeight> = bincode::deserialize(&data)?;
380 Ok(index)
381 } else {
382 Ok(Vec::new())
383 }
384 }
385
386 fn snapshot_key(height: BlockHeight) -> Vec<u8> {
388 let mut key = b"snapshot:".to_vec();
389 key.extend_from_slice(&height.0.to_be_bytes());
390 key
391 }
392
393 fn snapshot_index_key() -> Vec<u8> {
395 b"snapshot_index".to_vec()
396 }
397
398 pub fn set_retention_count(&self, count: u64) {
400 *self.retention_count.write() = count;
401 }
402
403 pub fn retention_count(&self) -> u64 {
405 *self.retention_count.read()
406 }
407}
408
409impl SnapshotManager<RocksDbStore> {
410 pub async fn create_checkpoint_snapshot(
414 &self,
415 height: BlockHeight,
416 state_root: Hash,
417 block_hash: Hash,
418 checkpoint_path: &Path,
419 ) -> Result<Snapshot> {
420 let db = self.kv_store.db();
422 let checkpoint = Checkpoint::new(db)
423 .map_err(|e| StorageError::InvalidSnapshot(format!("Failed to create checkpoint: {}", e)))?;
424
425 checkpoint.create_checkpoint(checkpoint_path)
426 .map_err(|e| StorageError::InvalidSnapshot(format!("Failed to create checkpoint at path: {}", e)))?;
427
428 let checkpoint_size = calculate_directory_size(checkpoint_path)?;
431
432 let snapshot = Snapshot {
433 height,
434 state_root,
435 block_hash,
436 timestamp: Timestamp::now(),
437 metadata: SnapshotMetadata {
438 account_count: 0, state_size: checkpoint_size,
440 compressed_size: checkpoint_size,
441 compression: CompressionType::None,
442 },
443 state_data: checkpoint_path.to_string_lossy().as_bytes().to_vec(),
444 };
445
446 let key = Self::snapshot_key(snapshot.height);
448 let data = bincode::serialize(&snapshot)?;
449 self.kv_store.put(CF_SNAPSHOTS, &key, &data)?;
450
451 let index_key = Self::snapshot_index_key();
453 let index_data_opt = self.kv_store.get(CF_SNAPSHOTS, &index_key)?;
454 let mut index: Vec<BlockHeight> = if let Some(data) = index_data_opt {
455 bincode::deserialize(&data)?
456 } else {
457 Vec::new()
458 };
459 index.push(snapshot.height);
460 index.sort_by_key(|h| h.0);
461 let index_data = bincode::serialize(&index)?;
462 self.kv_store.put(CF_SNAPSHOTS, &index_key, &index_data)?;
463
464 let retention = *self.retention_count.read();
466 if index.len() as u64 > retention {
467 let to_delete = index.len() as u64 - retention;
468 let mut ops = Vec::new();
469
470 for height in index.iter().take(to_delete as usize) {
471 let key = Self::snapshot_key(*height);
472 ops.push(WriteOp::Delete {
473 cf: CF_SNAPSHOTS.to_string(),
474 key,
475 });
476 }
477
478 let new_index: Vec<BlockHeight> = index.into_iter().skip(to_delete as usize).collect();
480 let index_data = bincode::serialize(&new_index)?;
481 ops.push(WriteOp::Put {
482 cf: CF_SNAPSHOTS.to_string(),
483 key: index_key,
484 value: index_data,
485 });
486
487 self.kv_store.write_batch(ops)?;
488 }
489
490 Ok(snapshot)
491 }
492}
493
494fn calculate_directory_size(path: &Path) -> Result<u64> {
496 let mut total_size = 0u64;
497
498 if path.is_file() {
499 return Ok(std::fs::metadata(path)?.len());
500 }
501
502 if path.is_dir() {
503 for entry in std::fs::read_dir(path)? {
504 let entry = entry?;
505 let metadata = entry.metadata()?;
506 if metadata.is_file() {
507 total_size += metadata.len();
508 } else if metadata.is_dir() {
509 total_size += calculate_directory_size(&entry.path())?;
510 }
511 }
512 }
513
514 Ok(total_size)
515}
516
517#[derive(Debug, Clone, Serialize, Deserialize)]
523pub struct SnapshotEntry {
524 pub cf: String,
526 pub key: Vec<u8>,
528 pub value: Vec<u8>,
530}
531
532pub fn serialize_snapshot_entries(entries: &[SnapshotEntry]) -> Result<Vec<u8>> {
545 bincode::serialize(entries).map_err(|e| {
546 StorageError::SerializationError(format!("Failed to serialize snapshot entries: {}", e))
547 })
548}
549
550pub fn compute_state_root(entries: &[SnapshotEntry]) -> Hash {
569 use sha2::{Digest, Sha256};
570 const DOMAIN: &[u8] = b"tenzro/snapshot/state-root/v1";
571
572 let mut sorted: Vec<&SnapshotEntry> = entries.iter().collect();
573 sorted.sort_by(|a, b| {
574 a.cf.as_bytes()
575 .cmp(b.cf.as_bytes())
576 .then_with(|| a.key.cmp(&b.key))
577 });
578
579 let mut hasher = Sha256::new();
580 hasher.update(DOMAIN);
581 for e in sorted {
582 let cf = e.cf.as_bytes();
583 hasher.update((cf.len() as u32).to_le_bytes());
584 hasher.update(cf);
585 hasher.update((e.key.len() as u32).to_le_bytes());
586 hasher.update(&e.key);
587 hasher.update((e.value.len() as u32).to_le_bytes());
588 hasher.update(&e.value);
589 }
590 let digest = hasher.finalize();
591 let mut out = [0u8; 32];
592 out.copy_from_slice(&digest);
593 Hash::new(out)
594}
595
596pub struct SnapshotRestorer;
598
599impl SnapshotRestorer {
600 pub async fn restore_from_snapshot(
615 snapshot: &Snapshot,
616 store: &dyn KvStore,
617 ) -> Result<RestoredState> {
618 if snapshot.state_data.is_empty() {
619 return Err(StorageError::InvalidSnapshot(
620 "snapshot contains no state data".to_string(),
621 ));
622 }
623
624 let raw_data = match snapshot.metadata.compression {
626 CompressionType::Gzip => decompress_gzip(&snapshot.state_data)?,
627 CompressionType::None => snapshot.state_data.clone(),
628 other => {
629 return Err(StorageError::InvalidSnapshot(format!(
630 "unsupported compression type: {:?}",
631 other
632 )));
633 }
634 };
635
636 let entries: Vec<SnapshotEntry> =
640 bincode::deserialize(&raw_data).map_err(|e| {
641 StorageError::InvalidSnapshot(format!(
642 "state_data is not a valid SnapshotEntry list \
643 (is this a RocksDB checkpoint snapshot?): {}",
644 e
645 ))
646 })?;
647
648 let entry_count = entries.len();
649 tracing::info!(
650 "Restoring snapshot at height {} with {} entries",
651 snapshot.height.0,
652 entry_count
653 );
654
655 let ops: Vec<WriteOp> = entries
657 .iter()
658 .map(|e| WriteOp::Put {
659 cf: e.cf.clone(),
660 key: e.key.clone(),
661 value: e.value.clone(),
662 })
663 .collect();
664
665 if !ops.is_empty() {
666 store.write_batch_sync(ops)?;
667 }
668
669 tracing::info!(
670 "Snapshot at height {} restored successfully ({} entries)",
671 snapshot.height.0,
672 entry_count
673 );
674
675 Ok(RestoredState {
676 height: snapshot.height,
677 state_root: snapshot.state_root,
678 account_count: snapshot.metadata.account_count,
679 entries_restored: entry_count as u64,
680 })
681 }
682
683 pub fn validate_snapshot(snapshot: &Snapshot) -> Result<bool> {
694 if snapshot.state_data.is_empty() {
695 return Ok(false);
696 }
697 if snapshot.metadata.compressed_size != snapshot.state_data.len() as u64 {
698 return Ok(false);
699 }
700
701 if snapshot.state_root != Hash::zero() {
704 let raw = match snapshot.metadata.compression {
705 CompressionType::Gzip => match decompress_gzip(&snapshot.state_data) {
706 Ok(d) => d,
707 Err(_) => return Ok(true), },
709 CompressionType::None => snapshot.state_data.clone(),
710 _ => return Ok(true),
711 };
712 if let Ok(entries) = bincode::deserialize::<Vec<SnapshotEntry>>(&raw) {
713 let recomputed = compute_state_root(&entries);
714 if recomputed != snapshot.state_root {
715 tracing::warn!(
716 height = snapshot.height.0,
717 recorded = %hex::encode(snapshot.state_root.as_bytes()),
718 recomputed = %hex::encode(recomputed.as_bytes()),
719 "snapshot state_root mismatch"
720 );
721 return Ok(false);
722 }
723 }
724 }
725 Ok(true)
726 }
727}
728
729#[derive(Debug, Clone)]
731pub struct RestoredState {
732 pub height: BlockHeight,
734 pub state_root: Hash,
736 pub account_count: u64,
738 pub entries_restored: u64,
740}
741
742#[cfg(test)]
743mod tests {
744 use super::*;
745 use crate::kv::MemoryStore;
746
747 #[tokio::test]
748 async fn test_snapshot_creation() {
749 let kv_store = Arc::new(MemoryStore::new());
750 let manager = SnapshotManager::new(kv_store, 5);
751
752 let state_data = vec![1, 2, 3, 4, 5];
753 let snapshot = manager
754 .create_snapshot(
755 BlockHeight::new(100),
756 Hash::zero(),
757 Hash::zero(),
758 state_data,
759 )
760 .await
761 .unwrap();
762
763 assert_eq!(snapshot.height, BlockHeight::new(100));
764 assert_eq!(snapshot.metadata.state_size, 5);
765 }
766
767 #[tokio::test]
768 async fn test_snapshot_load() {
769 let kv_store = Arc::new(MemoryStore::new());
770 let manager = SnapshotManager::new(kv_store, 5);
771
772 let state_data = vec![1, 2, 3, 4, 5];
773 manager
774 .create_snapshot(
775 BlockHeight::new(100),
776 Hash::zero(),
777 Hash::zero(),
778 state_data,
779 )
780 .await
781 .unwrap();
782
783 let loaded = manager
784 .load_snapshot(BlockHeight::new(100))
785 .await
786 .unwrap();
787 assert!(loaded.is_some());
788 assert_eq!(loaded.unwrap().height, BlockHeight::new(100));
789 }
790
791 #[tokio::test]
792 async fn test_snapshot_pruning() {
793 let kv_store = Arc::new(MemoryStore::new());
794 let manager = SnapshotManager::new(kv_store, 3);
795
796 for i in 1..=5 {
798 manager
799 .create_snapshot(
800 BlockHeight::new(i * 100),
801 Hash::zero(),
802 Hash::zero(),
803 vec![i as u8],
804 )
805 .await
806 .unwrap();
807 }
808
809 let snapshots = manager.list_snapshots().await.unwrap();
811 assert_eq!(snapshots.len(), 3);
812
813 assert_eq!(snapshots[0], BlockHeight::new(300));
815 assert_eq!(snapshots[1], BlockHeight::new(400));
816 assert_eq!(snapshots[2], BlockHeight::new(500));
817 }
818
819 #[tokio::test]
820 async fn test_latest_snapshot() {
821 let kv_store = Arc::new(MemoryStore::new());
822 let manager = SnapshotManager::new(kv_store, 5);
823
824 manager
825 .create_snapshot(
826 BlockHeight::new(100),
827 Hash::zero(),
828 Hash::zero(),
829 vec![1],
830 )
831 .await
832 .unwrap();
833
834 manager
835 .create_snapshot(
836 BlockHeight::new(200),
837 Hash::zero(),
838 Hash::zero(),
839 vec![2],
840 )
841 .await
842 .unwrap();
843
844 let latest = manager.latest_snapshot().await.unwrap();
845 assert!(latest.is_some());
846 assert_eq!(latest.unwrap().height, BlockHeight::new(200));
847 }
848
849 #[tokio::test]
850 async fn test_snapshot_deletion() {
851 let kv_store = Arc::new(MemoryStore::new());
852 let manager = SnapshotManager::new(kv_store, 5);
853
854 manager
855 .create_snapshot(
856 BlockHeight::new(100),
857 Hash::zero(),
858 Hash::zero(),
859 vec![1],
860 )
861 .await
862 .unwrap();
863
864 manager.delete_snapshot(BlockHeight::new(100)).await.unwrap();
865
866 let loaded = manager.load_snapshot(BlockHeight::new(100)).await.unwrap();
867 assert!(loaded.is_none());
868 }
869
870 #[tokio::test]
871 async fn test_restore_from_snapshot_writes_entries() {
872 use crate::kv::{MemoryStore, CF_STATE, CF_ACCOUNTS};
873
874 let entries = vec![
875 SnapshotEntry {
876 cf: CF_STATE.to_string(),
877 key: b"balance:0xabc".to_vec(),
878 value: 1234u128.to_le_bytes().to_vec(),
879 },
880 SnapshotEntry {
881 cf: CF_ACCOUNTS.to_string(),
882 key: b"account:0xabc".to_vec(),
883 value: b"account_data".to_vec(),
884 },
885 ];
886
887 let state_data = serialize_snapshot_entries(&entries).unwrap();
888 let restore_store = Arc::new(MemoryStore::new());
889 let manager = SnapshotManager::new(restore_store.clone(), 5);
890
891 let snapshot = manager
892 .create_snapshot(
893 BlockHeight::new(42),
894 Hash::zero(),
895 Hash::zero(),
896 state_data,
897 )
898 .await
899 .unwrap();
900
901 let target_store = Arc::new(MemoryStore::new());
903 let restored = SnapshotRestorer::restore_from_snapshot(&snapshot, target_store.as_ref())
904 .await
905 .unwrap();
906
907 assert_eq!(restored.height, BlockHeight::new(42));
908 assert_eq!(restored.entries_restored, 2);
909
910 let balance_bytes = target_store
912 .get(CF_STATE, b"balance:0xabc")
913 .unwrap()
914 .expect("balance entry should be present");
915 assert_eq!(u128::from_le_bytes(balance_bytes.try_into().unwrap()), 1234u128);
916
917 let account_bytes = target_store
918 .get(CF_ACCOUNTS, b"account:0xabc")
919 .unwrap()
920 .expect("account entry should be present");
921 assert_eq!(account_bytes, b"account_data");
922 }
923
924 #[tokio::test]
925 async fn test_restore_from_snapshot_empty_data_returns_error() {
926 let kv_store = Arc::new(MemoryStore::new());
927 let target_store = Arc::new(MemoryStore::new());
928
929 let snapshot = Snapshot {
931 height: BlockHeight::new(1),
932 state_root: Hash::zero(),
933 block_hash: Hash::zero(),
934 timestamp: Timestamp::now(),
935 metadata: SnapshotMetadata {
936 account_count: 0,
937 state_size: 0,
938 compressed_size: 0,
939 compression: CompressionType::None,
940 },
941 state_data: vec![],
942 };
943
944 let result = SnapshotRestorer::restore_from_snapshot(&snapshot, target_store.as_ref()).await;
945 assert!(result.is_err(), "empty state_data must return an error");
946
947 let _ = kv_store;
949 }
950
951 #[tokio::test]
952 async fn test_restore_roundtrip_via_snapshot_manager() {
953 use crate::kv::{MemoryStore, CF_STATE};
954
955 let source_entries = (0u8..5).map(|i| SnapshotEntry {
957 cf: CF_STATE.to_string(),
958 key: format!("key:{}", i).into_bytes(),
959 value: vec![i * 10],
960 }).collect::<Vec<_>>();
961
962 let state_data = serialize_snapshot_entries(&source_entries).unwrap();
963
964 let snap_store = Arc::new(MemoryStore::new());
965 let manager = SnapshotManager::new(snap_store, 10);
966
967 let snapshot = manager
968 .create_snapshot(BlockHeight::new(77), Hash::zero(), Hash::zero(), state_data)
969 .await
970 .unwrap();
971
972 assert!(SnapshotRestorer::validate_snapshot(&snapshot).unwrap());
974
975 let target = Arc::new(MemoryStore::new());
977 let result = SnapshotRestorer::restore_from_snapshot(&snapshot, target.as_ref())
978 .await
979 .unwrap();
980 assert_eq!(result.entries_restored, 5);
981
982 for i in 0u8..5 {
984 let key = format!("key:{}", i).into_bytes();
985 let val = target.get(CF_STATE, &key).unwrap().expect("entry missing");
986 assert_eq!(val, vec![i * 10]);
987 }
988 }
989
990 #[test]
991 fn compute_state_root_is_deterministic() {
992 use crate::kv::CF_STATE;
993 let entries_a = vec![
994 SnapshotEntry {
995 cf: CF_STATE.to_string(),
996 key: b"k1".to_vec(),
997 value: b"v1".to_vec(),
998 },
999 SnapshotEntry {
1000 cf: CF_STATE.to_string(),
1001 key: b"k2".to_vec(),
1002 value: b"v2".to_vec(),
1003 },
1004 ];
1005 let entries_b = vec![
1006 SnapshotEntry {
1007 cf: CF_STATE.to_string(),
1008 key: b"k2".to_vec(),
1009 value: b"v2".to_vec(),
1010 },
1011 SnapshotEntry {
1012 cf: CF_STATE.to_string(),
1013 key: b"k1".to_vec(),
1014 value: b"v1".to_vec(),
1015 },
1016 ];
1017 let root_a = compute_state_root(&entries_a);
1018 let root_b = compute_state_root(&entries_b);
1019 assert_eq!(root_a, root_b, "root must be order-independent");
1020 assert_ne!(root_a, Hash::zero(), "root must be non-zero for non-empty payload");
1021 }
1022
1023 #[test]
1024 fn compute_state_root_changes_with_value() {
1025 use crate::kv::CF_STATE;
1026 let entries_a = vec![SnapshotEntry {
1027 cf: CF_STATE.to_string(),
1028 key: b"k".to_vec(),
1029 value: b"v1".to_vec(),
1030 }];
1031 let entries_b = vec![SnapshotEntry {
1032 cf: CF_STATE.to_string(),
1033 key: b"k".to_vec(),
1034 value: b"v2".to_vec(),
1035 }];
1036 assert_ne!(
1037 compute_state_root(&entries_a),
1038 compute_state_root(&entries_b)
1039 );
1040 }
1041
1042 #[tokio::test]
1043 async fn validate_snapshot_accepts_correct_root() {
1044 use crate::kv::CF_STATE;
1045 let kv_store = Arc::new(MemoryStore::new());
1046 let manager = SnapshotManager::new(kv_store, 5);
1047 let entries = vec![
1048 SnapshotEntry {
1049 cf: CF_STATE.to_string(),
1050 key: b"alpha".to_vec(),
1051 value: b"1".to_vec(),
1052 },
1053 SnapshotEntry {
1054 cf: CF_STATE.to_string(),
1055 key: b"beta".to_vec(),
1056 value: b"2".to_vec(),
1057 },
1058 ];
1059 let real_root = compute_state_root(&entries);
1060 let payload = serialize_snapshot_entries(&entries).unwrap();
1061 let snapshot = manager
1062 .create_snapshot(BlockHeight::new(42), real_root, Hash::zero(), payload)
1063 .await
1064 .unwrap();
1065 assert!(SnapshotRestorer::validate_snapshot(&snapshot).unwrap());
1066 }
1067
1068 #[tokio::test]
1069 async fn validate_snapshot_rejects_wrong_root() {
1070 use crate::kv::CF_STATE;
1071 let kv_store = Arc::new(MemoryStore::new());
1072 let manager = SnapshotManager::new(kv_store, 5);
1073 let entries = vec![SnapshotEntry {
1074 cf: CF_STATE.to_string(),
1075 key: b"alpha".to_vec(),
1076 value: b"1".to_vec(),
1077 }];
1078 let mut wrong_root = [0u8; 32];
1079 wrong_root[0] = 0x42; let payload = serialize_snapshot_entries(&entries).unwrap();
1081 let snapshot = manager
1082 .create_snapshot(
1083 BlockHeight::new(43),
1084 Hash::new(wrong_root),
1085 Hash::zero(),
1086 payload,
1087 )
1088 .await
1089 .unwrap();
1090 assert!(!SnapshotRestorer::validate_snapshot(&snapshot).unwrap());
1091 }
1092}