# SNP-002: Import Snapshot

## Summary

The blockstore can import a snapshot file produced by `export_snapshot`. The import reads and validates the `SnapshotManifest`, then ingests blocks sequentially while validating height contiguity and parent-child hash links. Blocks are stored via the write pipeline for batched ingestion. The trailing SHA-256 checksum is verified against an incrementally computed hash to detect corruption.

## Specification

```rust
impl BlockStore {
    /// Import a snapshot from a reader. Validates manifest, block contiguity,
    /// parent-child links, and trailing SHA-256 checksum.
    pub fn import_snapshot(
        &self,
        reader: &mut impl Read,
    ) -> Result<SnapshotManifest> {
        use chia_sha2::Sha256;

        let mut hasher = Sha256::new();

        // Read and validate manifest
        let manifest: SnapshotManifest = {
            let mut manifest_buf = Vec::new();
            // Read manifest (bincode length-aware deserialization)
            let m: SnapshotManifest = bincode::deserialize_from(&mut *reader)
                .map_err(|e| BlockStoreError::Serialization(
                    format!("invalid snapshot manifest: {e}")
                ))?;
            // Re-serialize to hash the exact bytes read
            let manifest_bytes = bincode::serialize(&m)?;
            hasher.update(&manifest_bytes);

            // Schema version check
            if m.version != 1 {
                return Err(BlockStoreError::Serialization(
                    format!("unsupported snapshot version: {}", m.version)
                ));
            }
            m
        };

        let mut prev_hash: Option<Bytes32> = None;

        // Read and validate blocks sequentially
        for expected_height in manifest.start_height..=manifest.end_height {
            // Read u32 LE length prefix
            let mut len_bytes = [0u8; 4];
            reader.read_exact(&mut len_bytes)?;
            hasher.update(&len_bytes);
            let block_len = u32::from_le_bytes(len_bytes) as usize;

            // Read compressed block bytes
            let mut compressed = vec![0u8; block_len];
            reader.read_exact(&mut compressed)?;
            hasher.update(&compressed);

            // Decompress and deserialize for validation
            let block = self.deserialize_block(&compressed)?;

            // Validate height contiguity
            if block.header.height != expected_height {
                return Err(BlockStoreError::Serialization(
                    format!(
                        "non-contiguous height: expected {}, got {}",
                        expected_height, block.header.height
                    )
                ));
            }

            // Validate parent-child link
            if let Some(prev) = &prev_hash {
                if block.header.parent_hash != *prev {
                    return Err(BlockStoreError::Serialization(
                        format!(
                            "broken parent link at height {}",
                            expected_height
                        )
                    ));
                }
            }

            prev_hash = Some(block.header.hash());

            // Store via write pipeline for batched ingestion
            self.put_pipelined(block, /* canonical */ true)?;
        }

        // Read and verify trailing checksum
        let mut checksum_bytes = [0u8; 32];
        reader.read_exact(&mut checksum_bytes)?;
        let expected_checksum = Bytes32::from(checksum_bytes);

        let computed: [u8; 32] = hasher.finalize();
        let computed_checksum = Bytes32::from(computed);

        if expected_checksum != computed_checksum {
            return Err(BlockStoreError::Serialization(
                "snapshot checksum mismatch".to_string()
            ));
        }

        Ok(manifest)
    }
}
```

## Acceptance Criteria

1. The manifest MUST be read and deserialized as the first operation.
2. The manifest schema version MUST be validated; unsupported versions MUST be rejected with `BlockStoreError::Serialization`.
3. Block heights MUST be contiguous (sequential from `start_height` to `end_height`); non-contiguous heights MUST be rejected.
4. Each block's `parent_hash` MUST equal the hash of the previous block; broken links MUST be rejected.
5. Blocks MUST be ingested via `put_pipelined` for batched storage.
6. Blocks MUST be stored as canonical (`canonical=true`).
7. The trailing SHA-256 checksum MUST be verified against the incrementally computed hash of all preceding bytes.
8. Checksum mismatch MUST produce `BlockStoreError::Serialization`.
9. After successful import, the returned `SnapshotManifest` MUST match the manifest read from the stream.

## Implementation Notes

- The import decompresses each block to validate height and parent links, but stores the original compressed bytes. This adds CPU cost but is necessary for integrity.
- Using `put_pipelined` leverages the batched write path (BLK-008) for efficient ingestion during large snapshot imports.
- The first block in the range has no predecessor to validate against; `prev_hash` starts as `None` and the parent-child check is skipped for it.
- For very large snapshots, memory usage is bounded to one block at a time (streaming import).
- If import fails partway through, blocks already written via the pipeline are persisted; callers may need to handle partial imports (e.g., by tracking import progress).

## Test Plan

- **Integration: round-trip export/import** -- Export a block range, import into a fresh store, verify all blocks present and canonical.
- **Integration: manifest validation** -- Construct a snapshot with `version: 99`, attempt import, verify rejection with `BlockStoreError::Serialization`.
- **Integration: non-contiguous heights** -- Construct a snapshot with a gap in block heights, attempt import, verify rejection.
- **Integration: broken parent-child link** -- Construct a snapshot where one block's `parent_hash` does not match the previous block's hash, attempt import, verify rejection.
- **Integration: checksum mismatch** -- Export a valid snapshot, flip one byte in the block data, attempt import, verify rejection with checksum mismatch error.
- **Integration: empty range** -- Export/import a snapshot with `start_height == end_height` (single block), verify success.
- **Unit: batched ingestion** -- Verify that `put_pipelined` is called for each imported block (mock or spy verification).

## Expected Test Files

- `tests/snp_002_tests.rs`
