Skip to main content

eth_state_diff/
sync_committee.rs

1//! Delta encoding for Ethereum sync committees.
2//!
3//! A sync committee remains unchanged for an entire sync committee period and
4//! is replaced when a new period begins. This makes equality-based encoding
5//! sufficient: unchanged committees require no payload, while a committee
6//! transition stores the complete serialized target committee.
7//!
8//! The module operates on the serialized SSZ representation of a
9//! `SyncCommittee`. It does not inspect individual committee members or SSZ
10//! fields; the serialized bytes are treated as an opaque payload.
11//!
12//! # Encoding strategy
13//!
14//! Two representations are supported:
15//!
16//! - [`SyncCommitteeDiff::Unchanged`] when the serialized base and target
17//!   committees are identical.
18//! - [`SyncCommitteeDiff::FullReplacement`] when the serialized committees
19//!   differ. The complete serialized target committee is stored in the
20//!   replacement payload.
21//!
22//! A sparse representation is intentionally not provided. Unlike some
23//! consensus-state fields, a sync committee is replaced as a whole when it
24//! changes, so storing the complete target representation is straightforward
25//! and deterministic.
26//!
27//! # State transition
28//!
29//! Applying a delta reconstructs the target bytes:
30//!
31//! ```text
32//! base committee + delta -> target committee
33//! ```
34//!
35//! [`SyncCommitteeDiff::Unchanged`] leaves the base bytes untouched, so the
36//! base and target must already be identical for the delta to represent the
37//! target state.
38//!
39//! [`SyncCommitteeDiff::FullReplacement`] replaces the base bytes entirely,
40//! so the original base contents do not affect the resulting state.
41//!
42//! The delta does not depend on the sync committee period number. The caller
43//! is responsible for determining which consensus-state field is being
44//! diffed.
45//!
46//! # SSZ representation
47//!
48//! The functions accept the raw serialized SSZ representation of the
49//! `SyncCommittee` container. The bytes are treated as opaque data and are
50//! copied without decoding or re-encoding individual committee members.
51//!
52//! `SyncCommittee` is a fixed-size consensus container. Callers should
53//! therefore provide the serialized container bytes directly rather than
54//! adding a list-length prefix.
55//!
56//! The examples below use simple byte strings to represent opaque serialized
57//! data; they are illustrative and are not intended to be valid
58//! `SyncCommittee` SSZ encodings.
59//!
60//! # Complexity
61//!
62//! Let `n` be the serialized size of the committee.
63//!
64//! - [`diff_sync_committee`] performs an O(n) byte comparison.
65//! - [`apply_sync_committee`] is O(1) for
66//!   [`SyncCommitteeDiff::Unchanged`].
67//! - [`apply_sync_committee`] is O(n) for
68//!   [`SyncCommitteeDiff::FullReplacement`].
69//!
70//! Additional memory is O(n) when a changed committee is encoded because the
71//! target bytes are copied into the replacement payload.
72//!
73//! # Example
74//!
75//! ```
76//! use eth_state_diff::sync_committee::diff_sync_committee;
77//! use eth_state_diff::types::SyncCommitteeDiff;
78//!
79//! let base = b"committee-a";
80//! let target = b"committee-a";
81//!
82//! let delta = diff_sync_committee(base, target);
83//!
84//! assert_eq!(delta, SyncCommitteeDiff::Unchanged);
85//! ```
86//!
87//! A committee transition produces a full replacement:
88//!
89//! ```
90//! use eth_state_diff::sync_committee::diff_sync_committee;
91//! use eth_state_diff::types::SyncCommitteeDiff;
92//!
93//! let base = b"committee-a";
94//! let target = b"committee-b";
95//!
96//! let delta = diff_sync_committee(base, target);
97//!
98//! assert_eq!(
99//!     delta,
100//!     SyncCommitteeDiff::FullReplacement(target.to_vec())
101//! );
102//! ```
103
104use crate::types::{ArchivedSyncCommitteeDiff, SyncCommitteeDiff};
105
106/// Computes a delta between two serialized Ethereum sync committees.
107///
108/// The serialized committee bytes are compared as opaque byte sequences.
109///
110/// If `base_ssz` and `target_ssz` are identical, this function returns
111/// [`SyncCommitteeDiff::Unchanged`] and stores no committee bytes.
112///
113/// If they differ, this function returns
114/// [`SyncCommitteeDiff::FullReplacement`] containing a copy of `target_ssz`.
115/// The target committee is therefore stored in its entirety rather than
116/// attempting to encode individual member changes.
117///
118/// # Arguments
119///
120/// * `base_ssz` - Serialized SSZ bytes of the committee in the base state.
121/// * `target_ssz` - Serialized SSZ bytes of the committee in the target state.
122///
123/// Both arguments must represent the same consensus-state field and use the
124/// same serialization format.
125///
126/// # Returns
127///
128/// A [`SyncCommitteeDiff`] representing the transition from `base_ssz` to
129/// `target_ssz`.
130///
131/// # Complexity
132///
133/// O(n) time, where `n` is the length of the serialized committee.
134///
135/// If the committees differ, O(n) additional space is required for the copied
136/// replacement payload.
137///
138/// # Example
139///
140/// ```
141/// use eth_state_diff::sync_committee::diff_sync_committee;
142/// use eth_state_diff::types::SyncCommitteeDiff;
143///
144/// let base = b"committee-a";
145/// let target = b"committee-b";
146///
147/// let delta = diff_sync_committee(base, target);
148///
149/// assert_eq!(
150///     delta,
151///     SyncCommitteeDiff::FullReplacement(target.to_vec())
152/// );
153/// ```
154pub fn diff_sync_committee(base_ssz: &[u8], target_ssz: &[u8]) -> SyncCommitteeDiff {
155    if base_ssz == target_ssz {
156        SyncCommitteeDiff::Unchanged
157    } else {
158        SyncCommitteeDiff::FullReplacement(target_ssz.to_vec())
159    }
160}
161
162/// Applies a sync committee delta to a serialized SSZ committee in place.
163///
164/// [`SyncCommitteeDiff::Unchanged`] leaves `base` unchanged.
165///
166/// [`SyncCommitteeDiff::FullReplacement`] clears `base` and replaces it with
167/// the serialized target committee stored in the delta.
168///
169/// After successful execution, `base` contains the target committee
170/// represented by `delta`.
171///
172/// # Arguments
173///
174/// * `base` - Serialized SSZ bytes of the base committee. This buffer is
175///   modified in place.
176/// * `delta` - Archived sync committee delta to apply.
177///
178/// # Correctness
179///
180/// For [`SyncCommitteeDiff::Unchanged`], `base` is expected to already contain
181/// the target committee because the delta represents no state change.
182///
183/// For [`SyncCommitteeDiff::FullReplacement`], the original contents of
184/// `base` are irrelevant because the entire buffer is replaced by the target
185/// bytes stored in the delta.
186///
187/// # Complexity
188///
189/// - [`SyncCommitteeDiff::Unchanged`]: O(1) time and O(1) additional space.
190/// - [`SyncCommitteeDiff::FullReplacement`]: O(n) time, where `n` is the
191///   replacement size.
192///
193/// The replacement case may allocate if `base` does not have sufficient
194/// capacity for the target committee.
195///
196/// # Example
197///
198/// ```
199/// use eth_state_diff::sync_committee::{apply_sync_committee, diff_sync_committee};
200/// use eth_state_diff::types::ArchivedSyncCommitteeDiff;
201///
202/// let mut base = b"committee-a".to_vec();
203/// let target = b"committee-b";
204///
205/// let delta = diff_sync_committee(&base, target);
206///
207/// let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&delta).expect("failed to serialize");
208/// let archived = rkyv::access::<ArchivedSyncCommitteeDiff, rkyv::rancor::Error>(&bytes)
209///     .expect("failed to access");
210///
211/// apply_sync_committee(&mut base, archived);
212///
213/// assert_eq!(base, target);
214/// ```
215pub fn apply_sync_committee(base: &mut Vec<u8>, delta: &ArchivedSyncCommitteeDiff) {
216    match delta {
217        ArchivedSyncCommitteeDiff::Unchanged => {}
218        ArchivedSyncCommitteeDiff::FullReplacement(replacement) => {
219            base.clear();
220            base.extend_from_slice(replacement.as_slice());
221        }
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228    use crate::types::ArchivedSyncCommitteeDiff;
229
230    fn archive(diff: &SyncCommitteeDiff) -> rkyv::util::AlignedVec {
231        rkyv::to_bytes::<rkyv::rancor::Error>(diff).expect("test setup: failed to serialize delta")
232    }
233
234    fn archived(bytes: &[u8]) -> &ArchivedSyncCommitteeDiff {
235        rkyv::access::<ArchivedSyncCommitteeDiff, rkyv::rancor::Error>(bytes)
236            .expect("test setup: failed to access archived delta")
237    }
238
239    #[test]
240    fn test_diff_unchanged() {
241        let data = b"sync-committee-bytes";
242        let delta = diff_sync_committee(data, data);
243        assert!(matches!(delta, SyncCommitteeDiff::Unchanged));
244    }
245
246    #[test]
247    fn test_diff_full_replacement() {
248        let base = b"committee-a";
249        let target = b"committee-b";
250        let delta = diff_sync_committee(base, target);
251
252        match delta {
253            SyncCommitteeDiff::FullReplacement(bytes) => assert_eq!(bytes, target),
254            _ => panic!("test setup: expected FullReplacement"),
255        }
256    }
257
258    #[test]
259    fn test_apply_unchanged() {
260        let mut base = b"committee-a".to_vec();
261        let delta = SyncCommitteeDiff::Unchanged;
262
263        let bytes = archive(&delta);
264        let archived = archived(&bytes);
265
266        apply_sync_committee(&mut base, archived);
267
268        // Buffer should remain completely untouched
269        assert_eq!(base, b"committee-a");
270    }
271
272    #[test]
273    fn test_apply_full_replacement() {
274        let mut base = b"old-committee".to_vec();
275        let delta = SyncCommitteeDiff::FullReplacement(b"new-committee".to_vec());
276
277        let bytes = archive(&delta);
278        let archived = archived(&bytes);
279
280        apply_sync_committee(&mut base, archived);
281
282        // Buffer should be entirely replaced
283        assert_eq!(base, b"new-committee");
284    }
285
286    #[test]
287    fn test_full_roundtrip_unchanged() {
288        let base = b"committee-a";
289        let target = b"committee-a";
290
291        let delta = diff_sync_committee(base, target);
292
293        let bytes = archive(&delta);
294        let archived = archived(&bytes);
295
296        let mut reconstructed = base.to_vec();
297        apply_sync_committee(&mut reconstructed, archived);
298
299        assert_eq!(reconstructed, target);
300    }
301
302    #[test]
303    fn test_full_roundtrip_replacement() {
304        let base = b"committee-a";
305        let target = b"committee-b";
306
307        let delta = diff_sync_committee(base, target);
308
309        let bytes = archive(&delta);
310        let archived = archived(&bytes);
311
312        let mut reconstructed = base.to_vec();
313        apply_sync_committee(&mut reconstructed, archived);
314
315        assert_eq!(reconstructed, target);
316    }
317}