Skip to main content

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}