# SER-004: Round-Trip Guarantees

## Summary

All serialization paths in the blockstore must preserve data identity through round-trip cycles. Bincode serialization/deserialization must produce values equal to the original for all stored types. Zstd compression/decompression (both plain and dictionary-based) must preserve byte-level identity. Block hashes must be invariant across serialize/deserialize cycles, ensuring that no data corruption or drift occurs through storage operations.

## Specification

```rust
#[cfg(test)]
mod round_trip_guarantees {
    use super::*;

    /// Bincode round-trip: deserialize(serialize(x)) == x
    /// Must hold for L2Block, L2BlockHeader, BlockRecord, SnapshotManifest,
    /// and AttestedBlock.
    fn assert_bincode_round_trip<T>(value: &T)
    where
        T: Serialize + DeserializeOwned + PartialEq + std::fmt::Debug,
    {
        let bytes = bincode::serialize(value).expect("serialize");
        let restored: T = bincode::deserialize(&bytes).expect("deserialize");
        assert_eq!(value, &restored);
    }

    /// Zstd round-trip: decode_all(encode_all(data)) == data
    fn assert_zstd_round_trip(data: &[u8]) {
        let compressed = zstd::encode_all(data, 3).expect("compress");
        let decompressed = zstd::decode_all(&compressed[..]).expect("decompress");
        assert_eq!(data, &decompressed[..]);
    }

    /// Dictionary-compressed round-trip:
    /// decompress_with_dict(compress_with_dict(data)) == data
    fn assert_dict_round_trip(data: &[u8], dict: &[u8]) {
        let mut compressor =
            zstd::bulk::Compressor::with_dictionary(3, dict).expect("compressor");
        let compressed = compressor.compress(data).expect("compress");
        let mut decompressor =
            zstd::bulk::Decompressor::with_dictionary(dict).expect("decompressor");
        let decompressed = decompressor
            .decompress(&compressed, data.len() * 2)
            .expect("decompress");
        assert_eq!(data, &decompressed[..]);
    }

    /// Hash invariance: block.hash() must be the same before and after
    /// a full serialize/deserialize cycle through the storage pipeline.
    fn assert_hash_invariance(store: &BlockStore, block: &L2Block) {
        let hash_before = block.header.hash();
        let compressed = store.serialize_block(block).expect("serialize");
        let restored = store.deserialize_block(&compressed).expect("deserialize");
        let hash_after = restored.header.hash();
        assert_eq!(hash_before, hash_after);
    }
}
```

## Acceptance Criteria

1. `bincode::deserialize(bincode::serialize(block))` equals the original `L2Block`.
2. `bincode::deserialize(bincode::serialize(header))` equals the original `L2BlockHeader`.
3. `zstd::decode_all(zstd::encode_all(data, level))` equals the original data for any input.
4. Dictionary-compressed data decompresses to the exact original bytes.
5. `block.header.hash()` is identical before serialization and after deserialization.
6. These guarantees hold for all stored types: `L2Block`, `L2BlockHeader`, `BlockRecord`, `SnapshotManifest`, `AttestedBlock`.

## Implementation Notes

- Round-trip guarantees depend on `bincode` using a deterministic encoding. The default bincode configuration is deterministic for fixed-layout structs; avoid `bincode::Options` that enable variable-length encoding unless carefully tested.
- Hash invariance is critical for consensus: if a block's hash changes through a store/retrieve cycle, chain validation will break.
- Property-based testing (e.g., with `proptest` or `quickcheck`) is strongly recommended to exercise these guarantees across a wide range of inputs.
- Zstd dictionary round-trip must be tested with the same dictionary on both sides; mismatched dictionaries will produce garbage.

## Test Plan

- **Unit: bincode round-trip L2Block** -- Construct an L2Block, serialize/deserialize with bincode, assert equality.
- **Unit: bincode round-trip L2BlockHeader** -- Same for L2BlockHeader.
- **Unit: bincode round-trip BlockRecord** -- Same for BlockRecord.
- **Unit: zstd plain round-trip** -- Compress/decompress random data, assert byte equality.
- **Unit: zstd dictionary round-trip** -- Train a dictionary, compress/decompress with it, assert byte equality.
- **Unit: hash invariance** -- Full storage pipeline round-trip, assert `block.hash()` unchanged.
- **Property: arbitrary blocks** -- Use proptest to generate arbitrary L2Blocks and verify all round-trip guarantees.

## Expected Test Files

- `tests/ser_004_tests.rs`
