eth-state-diff 0.2.3

Fork-aware, domain-specific delta encoding for Ethereum consensus-layer state.
Documentation
//! Delta encoding for Ethereum sync committees.
//!
//! A sync committee remains unchanged for an entire sync committee period and
//! is replaced when a new period begins. This makes equality-based encoding
//! sufficient: unchanged committees require no payload, while a committee
//! transition stores the complete serialized target committee.
//!
//! The module operates on the serialized SSZ representation of a
//! `SyncCommittee`. It does not inspect individual committee members or SSZ
//! fields; the serialized bytes are treated as an opaque payload.
//!
//! # Encoding strategy
//!
//! Two representations are supported:
//!
//! - [`SyncCommitteeDiff::Unchanged`] when the serialized base and target
//!   committees are identical.
//! - [`SyncCommitteeDiff::FullReplacement`] when the serialized committees
//!   differ. The complete serialized target committee is stored in the
//!   replacement payload.
//!
//! A sparse representation is intentionally not provided. Unlike some
//! consensus-state fields, a sync committee is replaced as a whole when it
//! changes, so storing the complete target representation is straightforward
//! and deterministic.
//!
//! # State transition
//!
//! Applying a delta reconstructs the target bytes:
//!
//! ```text
//! base committee + delta -> target committee
//! ```
//!
//! [`SyncCommitteeDiff::Unchanged`] leaves the base bytes untouched, so the
//! base and target must already be identical for the delta to represent the
//! target state.
//!
//! [`SyncCommitteeDiff::FullReplacement`] replaces the base bytes entirely,
//! so the original base contents do not affect the resulting state.
//!
//! The delta does not depend on the sync committee period number. The caller
//! is responsible for determining which consensus-state field is being
//! diffed.
//!
//! # SSZ representation
//!
//! The functions accept the raw serialized SSZ representation of the
//! `SyncCommittee` container. The bytes are treated as opaque data and are
//! copied without decoding or re-encoding individual committee members.
//!
//! `SyncCommittee` is a fixed-size consensus container. Callers should
//! therefore provide the serialized container bytes directly rather than
//! adding a list-length prefix.
//!
//! The examples below use simple byte strings to represent opaque serialized
//! data; they are illustrative and are not intended to be valid
//! `SyncCommittee` SSZ encodings.
//!
//! # Complexity
//!
//! Let `n` be the serialized size of the committee.
//!
//! - [`diff_sync_committee`] performs an O(n) byte comparison.
//! - [`apply_sync_committee`] is O(1) for
//!   [`SyncCommitteeDiff::Unchanged`].
//! - [`apply_sync_committee`] is O(n) for
//!   [`SyncCommitteeDiff::FullReplacement`].
//!
//! Additional memory is O(n) when a changed committee is encoded because the
//! target bytes are copied into the replacement payload.
//!
//! # Example
//!
//! ```
//! use eth_state_diff::sync_committee::diff_sync_committee;
//! use eth_state_diff::types::SyncCommitteeDiff;
//!
//! let base = b"committee-a";
//! let target = b"committee-a";
//!
//! let delta = diff_sync_committee(base, target);
//!
//! assert_eq!(delta, SyncCommitteeDiff::Unchanged);
//! ```
//!
//! A committee transition produces a full replacement:
//!
//! ```
//! use eth_state_diff::sync_committee::diff_sync_committee;
//! use eth_state_diff::types::SyncCommitteeDiff;
//!
//! let base = b"committee-a";
//! let target = b"committee-b";
//!
//! let delta = diff_sync_committee(base, target);
//!
//! assert_eq!(
//!     delta,
//!     SyncCommitteeDiff::FullReplacement(target.to_vec())
//! );
//! ```

use crate::types::{ArchivedSyncCommitteeDiff, SyncCommitteeDiff};

/// Computes a delta between two serialized Ethereum sync committees.
///
/// The serialized committee bytes are compared as opaque byte sequences.
///
/// If `base_ssz` and `target_ssz` are identical, this function returns
/// [`SyncCommitteeDiff::Unchanged`] and stores no committee bytes.
///
/// If they differ, this function returns
/// [`SyncCommitteeDiff::FullReplacement`] containing a copy of `target_ssz`.
/// The target committee is therefore stored in its entirety rather than
/// attempting to encode individual member changes.
///
/// # Arguments
///
/// * `base_ssz` - Serialized SSZ bytes of the committee in the base state.
/// * `target_ssz` - Serialized SSZ bytes of the committee in the target state.
///
/// Both arguments must represent the same consensus-state field and use the
/// same serialization format.
///
/// # Returns
///
/// A [`SyncCommitteeDiff`] representing the transition from `base_ssz` to
/// `target_ssz`.
///
/// # Complexity
///
/// O(n) time, where `n` is the length of the serialized committee.
///
/// If the committees differ, O(n) additional space is required for the copied
/// replacement payload.
///
/// # Example
///
/// ```
/// use eth_state_diff::sync_committee::diff_sync_committee;
/// use eth_state_diff::types::SyncCommitteeDiff;
///
/// let base = b"committee-a";
/// let target = b"committee-b";
///
/// let delta = diff_sync_committee(base, target);
///
/// assert_eq!(
///     delta,
///     SyncCommitteeDiff::FullReplacement(target.to_vec())
/// );
/// ```
pub fn diff_sync_committee(base_ssz: &[u8], target_ssz: &[u8]) -> SyncCommitteeDiff {
    if base_ssz == target_ssz {
        SyncCommitteeDiff::Unchanged
    } else {
        SyncCommitteeDiff::FullReplacement(target_ssz.to_vec())
    }
}

/// Applies a sync committee delta to a serialized SSZ committee in place.
///
/// [`SyncCommitteeDiff::Unchanged`] leaves `base` unchanged.
///
/// [`SyncCommitteeDiff::FullReplacement`] clears `base` and replaces it with
/// the serialized target committee stored in the delta.
///
/// After successful execution, `base` contains the target committee
/// represented by `delta`.
///
/// # Arguments
///
/// * `base` - Serialized SSZ bytes of the base committee. This buffer is
///   modified in place.
/// * `delta` - Archived sync committee delta to apply.
///
/// # Correctness
///
/// For [`SyncCommitteeDiff::Unchanged`], `base` is expected to already contain
/// the target committee because the delta represents no state change.
///
/// For [`SyncCommitteeDiff::FullReplacement`], the original contents of
/// `base` are irrelevant because the entire buffer is replaced by the target
/// bytes stored in the delta.
///
/// # Complexity
///
/// - [`SyncCommitteeDiff::Unchanged`]: O(1) time and O(1) additional space.
/// - [`SyncCommitteeDiff::FullReplacement`]: O(n) time, where `n` is the
///   replacement size.
///
/// The replacement case may allocate if `base` does not have sufficient
/// capacity for the target committee.
///
/// # Example
///
/// ```
/// use eth_state_diff::sync_committee::{apply_sync_committee, diff_sync_committee};
/// use eth_state_diff::types::ArchivedSyncCommitteeDiff;
///
/// let mut base = b"committee-a".to_vec();
/// let target = b"committee-b";
///
/// let delta = diff_sync_committee(&base, target);
///
/// let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&delta).expect("failed to serialize");
/// let archived = rkyv::access::<ArchivedSyncCommitteeDiff, rkyv::rancor::Error>(&bytes)
///     .expect("failed to access");
///
/// apply_sync_committee(&mut base, archived);
///
/// assert_eq!(base, target);
/// ```
pub fn apply_sync_committee(base: &mut Vec<u8>, delta: &ArchivedSyncCommitteeDiff) {
    match delta {
        ArchivedSyncCommitteeDiff::Unchanged => {}
        ArchivedSyncCommitteeDiff::FullReplacement(replacement) => {
            base.clear();
            base.extend_from_slice(replacement.as_slice());
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::types::ArchivedSyncCommitteeDiff;

    fn archive(diff: &SyncCommitteeDiff) -> rkyv::util::AlignedVec {
        rkyv::to_bytes::<rkyv::rancor::Error>(diff).expect("test setup: failed to serialize delta")
    }

    fn archived(bytes: &[u8]) -> &ArchivedSyncCommitteeDiff {
        rkyv::access::<ArchivedSyncCommitteeDiff, rkyv::rancor::Error>(bytes)
            .expect("test setup: failed to access archived delta")
    }

    #[test]
    fn test_diff_unchanged() {
        let data = b"sync-committee-bytes";
        let delta = diff_sync_committee(data, data);
        assert!(matches!(delta, SyncCommitteeDiff::Unchanged));
    }

    #[test]
    fn test_diff_full_replacement() {
        let base = b"committee-a";
        let target = b"committee-b";
        let delta = diff_sync_committee(base, target);

        match delta {
            SyncCommitteeDiff::FullReplacement(bytes) => assert_eq!(bytes, target),
            _ => panic!("test setup: expected FullReplacement"),
        }
    }

    #[test]
    fn test_apply_unchanged() {
        let mut base = b"committee-a".to_vec();
        let delta = SyncCommitteeDiff::Unchanged;

        let bytes = archive(&delta);
        let archived = archived(&bytes);

        apply_sync_committee(&mut base, archived);

        // Buffer should remain completely untouched
        assert_eq!(base, b"committee-a");
    }

    #[test]
    fn test_apply_full_replacement() {
        let mut base = b"old-committee".to_vec();
        let delta = SyncCommitteeDiff::FullReplacement(b"new-committee".to_vec());

        let bytes = archive(&delta);
        let archived = archived(&bytes);

        apply_sync_committee(&mut base, archived);

        // Buffer should be entirely replaced
        assert_eq!(base, b"new-committee");
    }

    #[test]
    fn test_full_roundtrip_unchanged() {
        let base = b"committee-a";
        let target = b"committee-a";

        let delta = diff_sync_committee(base, target);

        let bytes = archive(&delta);
        let archived = archived(&bytes);

        let mut reconstructed = base.to_vec();
        apply_sync_committee(&mut reconstructed, archived);

        assert_eq!(reconstructed, target);
    }

    #[test]
    fn test_full_roundtrip_replacement() {
        let base = b"committee-a";
        let target = b"committee-b";

        let delta = diff_sync_committee(base, target);

        let bytes = archive(&delta);
        let archived = archived(&bytes);

        let mut reconstructed = base.to_vec();
        apply_sync_committee(&mut reconstructed, archived);

        assert_eq!(reconstructed, target);
    }
}