# SNP-001: Export Snapshot

## Summary

The blockstore can export a range of canonical blocks as a self-contained snapshot file. The export writes a `SnapshotManifest` header, followed by each block as a length-prefixed compressed blob (reusing pre-compressed bytes from `CF_BLOCKS` to avoid recompression), and concludes with a SHA-256 checksum of all preceding bytes.

## Specification

```rust
impl BlockStore {
    /// Export canonical blocks in [start_height, end_height] as a snapshot.
    /// Writes manifest, length-prefixed compressed blocks, and trailing checksum.
    pub fn export_snapshot(
        &self,
        start_height: u64,
        end_height: u64,
        writer: &mut impl Write,
    ) -> Result<SnapshotManifest> {
        use chia_sha2::Sha256;

        let block_count = end_height - start_height + 1;

        // Build manifest (checksum filled after writing blocks)
        let mut manifest = SnapshotManifest {
            version: 1,
            start_height,
            end_height,
            block_count,
            state_root: self.get_state_root_at(end_height)?,
            checksum: Bytes32::default(), // placeholder
        };

        let mut hasher = Sha256::new();

        // Write manifest
        let manifest_bytes = bincode::serialize(&manifest)
            .map_err(|e| BlockStoreError::Serialization(e.to_string()))?;
        writer.write_all(&manifest_bytes)?;
        hasher.update(&manifest_bytes);

        // Write blocks: length-prefixed compressed bytes
        for height in start_height..=end_height {
            let hash = self.get_canonical_hash(height)?
                .ok_or(BlockStoreError::BlockNotFound)?;

            // Read pre-compressed bytes directly from CF_BLOCKS
            let compressed = self.db
                .get_cf(&self.cf_blocks, hash.as_ref())?
                .ok_or(BlockStoreError::BlockNotFound)?;

            // Write u32 LE length prefix + compressed block bytes
            let len = compressed.len() as u32;
            let len_bytes = len.to_le_bytes();
            writer.write_all(&len_bytes)?;
            hasher.update(&len_bytes);

            writer.write_all(&compressed)?;
            hasher.update(&compressed);
        }

        // Compute and append SHA-256 checksum
        let checksum_bytes: [u8; 32] = hasher.finalize();
        let checksum = Bytes32::from(checksum_bytes);
        writer.write_all(checksum.as_ref())?;

        manifest.checksum = checksum;
        Ok(manifest)
    }
}
```

## Acceptance Criteria

1. The first bytes written MUST be the bincode-serialized `SnapshotManifest`.
2. Each block MUST be written as a 4-byte little-endian length prefix followed by the compressed block bytes.
3. Compressed block bytes MUST be read directly from `CF_BLOCKS` (pre-compressed, no recompression).
4. The final 32 bytes MUST be the SHA-256 checksum of all preceding bytes (manifest + all length-prefixed blocks).
5. The returned `SnapshotManifest` MUST have its `checksum` field populated with the computed checksum.
6. Blocks MUST be written in ascending height order from `start_height` to `end_height` inclusive.
7. Missing canonical blocks in the requested range MUST produce a `BlockStoreError::BlockNotFound`.

## Implementation Notes

- Reading pre-compressed bytes from `CF_BLOCKS` avoids a decompress/recompress cycle, making export I/O-bound rather than CPU-bound.
- The manifest is written first so that importers can validate metadata before committing to reading the full block stream.
- The checksum in the manifest struct is initially a placeholder; the actual checksum is appended as trailing bytes and also set on the returned manifest.
- For very large exports, consider wrapping the writer in a `BufWriter` for performance.
- The `state_root` field captures the state at `end_height`, enabling importers to verify state consistency after ingestion.

## Test Plan

- **Integration: basic export** -- Store a range of canonical blocks, export to a buffer, verify buffer is non-empty and starts with valid bincode manifest.
- **Integration: block framing** -- Export blocks, parse the output manually, verify each block has correct u32 LE length prefix and correct compressed bytes.
- **Integration: pre-compressed bypass** -- Store a block, read raw bytes from CF_BLOCKS, export snapshot, verify exported block bytes match raw CF_BLOCKS bytes (no recompression).
- **Integration: checksum correctness** -- Export snapshot, compute SHA-256 of all bytes before the last 32, verify it matches the trailing 32 bytes.
- **Integration: missing block** -- Request export of a range with a gap in canonical chain, verify `BlockStoreError::BlockNotFound`.
- **Integration: manifest fields** -- Export, deserialize manifest from beginning of output, verify start_height, end_height, block_count, and state_root are correct.

## Expected Test Files

- `tests/snp_001_tests.rs`
