dig_blockstore/snapshot.rs
1//! Snapshot export / import ([`SNP-001`…`SNP-004`](../docs/requirements/domains/snapshot/NORMATIVE.md)).
2//!
3//! # Overview
4//!
5//! Snapshots enable fast bootstrapping: a new node downloads a snapshot file containing
6//! a contiguous range of canonical blocks, verifies the SHA-256 checksum, and bulk-inserts
7//! into its local store — skipping full block-by-block sync.
8//!
9//! # Stream format
10//!
11//! ```text
12//! [ SnapshotManifest (bincode) ]
13//! [ block_0_len: u32 LE ] [ block_0_compressed_bytes ]
14//! [ block_1_len: u32 LE ] [ block_1_compressed_bytes ]
15//! ...
16//! [ block_N_len: u32 LE ] [ block_N_compressed_bytes ]
17//! [ SHA-256 checksum: 32 bytes ]
18//! ```
19//!
20//! The checksum covers all bytes from the start of the manifest to the end of the last block
21//! (i.e., everything except the trailing 32-byte checksum itself).
22//!
23//! # Requirements
24//!
25//! - [`SNP-001`](../docs/requirements/domains/snapshot/specs/SNP-001.md) — export
26//! - [`SNP-002`](../docs/requirements/domains/snapshot/specs/SNP-002.md) — import
27//! - [`SNP-003`](../docs/requirements/domains/snapshot/specs/SNP-003.md) — SnapshotManifest struct
28//! - [`SNP-004`](../docs/requirements/domains/snapshot/specs/SNP-004.md) — checksum verification
29
30use chia_protocol::Bytes32;
31use serde::{Deserialize, Serialize};
32
33/// Metadata header for snapshot files ([`SNP-003`](../docs/requirements/domains/snapshot/specs/SNP-003.md)).
34///
35/// Written as the first bytes of every snapshot stream (bincode-encoded).
36/// The importer reads this to validate the schema version and expected block range
37/// before processing block data.
38///
39/// # Fields
40///
41/// - `version` — schema version for forward compatibility (current: 1).
42/// - `start_height` / `end_height` — inclusive block range.
43/// - `block_count` — must equal `end_height - start_height + 1`.
44/// - `state_root` — state root hash at `end_height` for post-import verification.
45/// - `checksum` — SHA-256 of all block data (placeholder during export, filled after).
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
47pub struct SnapshotManifest {
48 /// Schema version (current: 1). Importers reject unknown versions.
49 pub version: u32,
50 /// First block height included (inclusive).
51 pub start_height: u64,
52 /// Last block height included (inclusive).
53 pub end_height: u64,
54 /// Total blocks. Must equal `end_height - start_height + 1`.
55 pub block_count: u64,
56 /// State root hash at `end_height`.
57 pub state_root: Bytes32,
58 /// SHA-256 checksum of all block data bytes (after manifest, before checksum).
59 pub checksum: Bytes32,
60}
61
62/// Current snapshot schema version.
63pub const SNAPSHOT_VERSION: u32 = 1;
64
65use crate::constants::CF_BLOCKS;
66use crate::encoding::hash_key;
67use crate::error::BlockStoreError;
68use crate::store::BlockStore;
69
70impl BlockStore {
71 /// Export canonical blocks in `[start_height, end_height]` as a snapshot stream.
72 ///
73 /// # Stream format ([`SNP-001`](../docs/requirements/domains/snapshot/specs/SNP-001.md))
74 ///
75 /// 1. `SnapshotManifest` (bincode-serialized)
76 /// 2. For each height: `block_len: u32 LE` + `compressed_block_bytes`
77 /// 3. SHA-256 checksum (32 bytes) of all preceding bytes
78 ///
79 /// Block bytes are read directly from CF_BLOCKS (pre-compressed) to avoid
80 /// decompression/recompression overhead.
81 ///
82 /// # Returns
83 ///
84 /// The finalized `SnapshotManifest` with the computed checksum.
85 pub fn export_snapshot(
86 &self,
87 start_height: u64,
88 end_height: u64,
89 writer: &mut impl std::io::Write,
90 ) -> Result<crate::snapshot::SnapshotManifest, BlockStoreError> {
91 use crate::snapshot::{SnapshotManifest, SNAPSHOT_VERSION};
92 use chia_sha2::Sha256;
93
94 let block_count = end_height.saturating_sub(start_height) + 1;
95
96 // Get state root from header at end_height
97 let end_header = self.get_header_by_height(end_height)?.ok_or_else(|| {
98 BlockStoreError::Serialization(format!(
99 "export_snapshot: no canonical block at end_height {end_height}"
100 ))
101 })?;
102
103 let mut manifest = SnapshotManifest {
104 version: SNAPSHOT_VERSION,
105 start_height,
106 end_height,
107 block_count,
108 state_root: end_header.state_root,
109 checksum: Bytes32::default(), // placeholder
110 };
111
112 let mut hasher = Sha256::new();
113
114 // Write manifest
115 let manifest_bytes = bincode::serialize(&manifest)
116 .map_err(|e| BlockStoreError::Serialization(e.to_string()))?;
117 writer
118 .write_all(&manifest_bytes)
119 .map_err(|e| BlockStoreError::Serialization(format!("snapshot write: {e}")))?;
120 hasher.update(&manifest_bytes);
121
122 // Write blocks: length-prefixed pre-compressed bytes from CF_BLOCKS
123 let cf_b = self.cf(CF_BLOCKS)?;
124 for height in start_height..=end_height {
125 let hash = self.get_hash_by_height(height)?.ok_or_else(|| {
126 BlockStoreError::Serialization(format!(
127 "export_snapshot: no canonical hash at height {height}"
128 ))
129 })?;
130 let compressed = self
131 .db
132 .get_cf(cf_b, hash_key(&hash).as_slice())?
133 .ok_or(BlockStoreError::BlockNotFound(hash))?;
134
135 let len = compressed.len() as u32;
136 let len_bytes = len.to_le_bytes();
137 writer
138 .write_all(&len_bytes)
139 .map_err(|e| BlockStoreError::Serialization(format!("snapshot write: {e}")))?;
140 hasher.update(len_bytes);
141
142 writer
143 .write_all(&compressed)
144 .map_err(|e| BlockStoreError::Serialization(format!("snapshot write: {e}")))?;
145 hasher.update(&compressed);
146 }
147
148 // Compute and append SHA-256 checksum
149 let checksum_arr: [u8; 32] = hasher.finalize();
150 let checksum = Bytes32::new(checksum_arr);
151 writer
152 .write_all(checksum.as_ref())
153 .map_err(|e| BlockStoreError::Serialization(format!("snapshot write: {e}")))?;
154
155 manifest.checksum = checksum;
156 Ok(manifest)
157 }
158
159 /// Import a snapshot stream, validating manifest, contiguity, parent links, and checksum.
160 ///
161 /// # Algorithm ([`SNP-002`](../docs/requirements/domains/snapshot/specs/SNP-002.md))
162 ///
163 /// 1. Read and validate `SnapshotManifest` (schema version check).
164 /// 2. For each block: read length-prefixed compressed bytes, decompress + deserialize
165 /// for validation, verify height contiguity and parent-child links, store via `put_block`.
166 /// 3. Verify trailing SHA-256 checksum.
167 ///
168 /// # Returns
169 ///
170 /// The `SnapshotManifest` read from the stream.
171 pub fn import_snapshot(
172 &self,
173 reader: &mut impl std::io::Read,
174 ) -> Result<crate::snapshot::SnapshotManifest, BlockStoreError> {
175 use crate::snapshot::SNAPSHOT_VERSION;
176 use chia_sha2::Sha256;
177
178 let mut hasher = Sha256::new();
179
180 // Read manifest via bincode (length-aware deserialization)
181 let manifest: crate::snapshot::SnapshotManifest = bincode::deserialize_from(&mut *reader)
182 .map_err(|e| {
183 BlockStoreError::Serialization(format!("invalid snapshot manifest: {e}"))
184 })?;
185 // Re-serialize to hash the exact wire bytes
186 let manifest_bytes = bincode::serialize(&manifest)
187 .map_err(|e| BlockStoreError::Serialization(e.to_string()))?;
188 hasher.update(&manifest_bytes);
189
190 if manifest.version != SNAPSHOT_VERSION {
191 return Err(BlockStoreError::Serialization(format!(
192 "unsupported snapshot version: {}",
193 manifest.version
194 )));
195 }
196
197 let mut prev_hash: Option<Bytes32> = None;
198
199 // Read and store blocks
200 for expected_height in manifest.start_height..=manifest.end_height {
201 // Read u32 LE length prefix
202 let mut len_bytes = [0u8; 4];
203 reader
204 .read_exact(&mut len_bytes)
205 .map_err(|e| BlockStoreError::Serialization(format!("snapshot read: {e}")))?;
206 hasher.update(len_bytes);
207 let block_len = u32::from_le_bytes(len_bytes) as usize;
208
209 // Read compressed block bytes
210 let mut compressed = vec![0u8; block_len];
211 reader
212 .read_exact(&mut compressed)
213 .map_err(|e| BlockStoreError::Serialization(format!("snapshot read: {e}")))?;
214 hasher.update(&compressed);
215
216 // Decompress and deserialize for validation
217 let block = self.deserialize_block(&compressed)?;
218
219 // Validate height contiguity
220 if block.height() != expected_height {
221 return Err(BlockStoreError::Serialization(format!(
222 "non-contiguous height: expected {expected_height}, got {}",
223 block.height()
224 )));
225 }
226
227 // Validate parent-child link
228 if let Some(prev) = &prev_hash {
229 if block.header.parent_hash != *prev {
230 return Err(BlockStoreError::Serialization(format!(
231 "broken parent link at height {expected_height}"
232 )));
233 }
234 }
235
236 prev_hash = Some(block.hash());
237
238 // Store block as canonical
239 self.put_block(&block, true)?;
240 }
241
242 // Read and verify trailing checksum
243 let mut checksum_bytes = [0u8; 32];
244 reader
245 .read_exact(&mut checksum_bytes)
246 .map_err(|e| BlockStoreError::Serialization(format!("snapshot checksum read: {e}")))?;
247 let expected_checksum = Bytes32::new(checksum_bytes);
248 let computed_arr: [u8; 32] = hasher.finalize();
249 let computed_checksum = Bytes32::new(computed_arr);
250
251 if expected_checksum != computed_checksum {
252 return Err(BlockStoreError::Serialization(
253 "snapshot checksum mismatch".to_string(),
254 ));
255 }
256
257 Ok(manifest)
258 }
259}