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).unwrap();
208/// let archived = unsafe {
209/// rkyv::access_unchecked::<ArchivedSyncCommitteeDiff>(&bytes)
210/// };
211///
212/// apply_sync_committee(&mut base, archived);
213///
214/// assert_eq!(base, target);
215/// ```
216pub fn apply_sync_committee(base: &mut Vec<u8>, delta: &ArchivedSyncCommitteeDiff) {
217 match delta {
218 ArchivedSyncCommitteeDiff::Unchanged => {}
219 ArchivedSyncCommitteeDiff::FullReplacement(replacement) => {
220 base.clear();
221 base.extend_from_slice(replacement.as_slice());
222 }
223 }
224}