eth_state_diff/sync_committee.rs
1//! Delta encoding for Ethereum sync committees.
2//!
3//! Sync committees remain unchanged for an entire sync committee period and
4//! are replaced when a new period begins. This makes equality-based encoding
5//! sufficient for the normal state-diff window: unchanged committees require
6//! no payload, while a committee transition stores the complete target
7//! committee.
8//!
9//! The module operates on the serialized SSZ representation of a
10//! `SyncCommittee`. It does not inspect individual committee members or SSZ
11//! fields; the serialized bytes are treated as an opaque payload.
12//!
13//! # Encoding strategy
14//!
15//! Two representations are possible:
16//!
17//! - [`SyncCommitteeDiff::Unchanged`] when the serialized base and target
18//! committees are identical.
19//! - [`SyncCommitteeDiff::FullReplacement`] when the committee has changed.
20//!
21//! The replacement representation stores the complete serialized target
22//! committee. This is intentional: unlike validator balances or participation
23//! flags, there is no useful sparse update representation for a committee in
24//! this module.
25//!
26//! # State transition
27//!
28//! Applying a delta to the same base committee used during diff generation
29//! reconstructs the target bytes:
30//!
31//! ```text
32//! base committee + delta -> target committee
33//! ```
34//!
35//! The delta does not depend on the committee period number. The caller is
36//! responsible for determining which consensus-state field is being diffed.
37//!
38//! # SSZ representation
39//!
40//! The functions accept the raw serialized bytes of the `SyncCommittee`
41//! container. The bytes are treated as opaque data and are copied without
42//! decoding or re-encoding individual committee members.
43//!
44//! `SyncCommittee` is a fixed-size consensus container. Callers should
45//! therefore normally provide the serialized container bytes directly rather
46//! than adding a list-length prefix.
47//!
48//! # Complexity
49//!
50//! Let *n* be the serialized size of the committee:
51//!
52//! - [`diff_sync_committee`] performs an O(n) byte comparison.
53//! - [`apply_sync_committee`] is O(1) for [`SyncCommitteeDiff::Unchanged`].
54//! - [`apply_sync_committee`] is O(n) for
55//! [`SyncCommitteeDiff::FullReplacement`].
56//!
57//! Additional memory is O(n) when a changed committee is encoded, because the
58//! target bytes are copied into the replacement payload.
59//!
60//! # Example
61//!
62//! ```
63//! use eth_state_diff::sync_committee::diff_sync_committee;
64//! use eth_state_diff::types::SyncCommitteeDiff;
65//!
66//! let base = b"committee-a";
67//! let target = b"committee-a";
68//!
69//! let delta = diff_sync_committee(base, target);
70//!
71//! assert_eq!(delta, SyncCommitteeDiff::Unchanged);
72//! ```
73//!
74//! A committee transition produces a full replacement:
75//!
76//! ```
77//! use eth_state_diff::sync_committee::diff_sync_committee;
78//! use eth_state_diff::types::SyncCommitteeDiff;
79//!
80//! let base = b"committee-a";
81//! let target = b"committee-b";
82//!
83//! let delta = diff_sync_committee(base, target);
84//!
85//! assert_eq!(
86//! delta,
87//! SyncCommitteeDiff::FullReplacement(target.to_vec())
88//! );
89//! ```
90
91use crate::types::{ArchivedSyncCommitteeDiff, SyncCommitteeDiff};
92
93/// Computes a delta between two serialized Ethereum sync committees.
94///
95/// The serialized committee bytes are compared as opaque byte sequences.
96///
97/// If `base_ssz` and `target_ssz` are identical, this function returns
98/// [`SyncCommitteeDiff::Unchanged`] and stores no committee bytes.
99///
100/// If they differ, this function returns
101/// [`SyncCommitteeDiff::FullReplacement`] containing a copy of `target_ssz`.
102/// The target committee is therefore stored in its entirety rather than
103/// attempting to encode individual member changes.
104///
105/// # Arguments
106///
107/// * `base_ssz` - Serialized SSZ bytes of the committee in the base state.
108/// * `target_ssz` - Serialized SSZ bytes of the committee in the target state.
109///
110/// Both arguments must represent the same consensus-state field and use the
111/// same serialization format.
112///
113/// # Returns
114///
115/// A [`SyncCommitteeDiff`] that can be applied to `base_ssz` to reconstruct
116/// `target_ssz`.
117///
118/// # Complexity
119///
120/// O(n) time, where *n* is the length of the serialized committee.
121///
122/// If the committees differ, O(n) additional space is required for the copied
123/// replacement payload.
124///
125/// # Example
126///
127/// ```
128/// use eth_state_diff::sync_committee::diff_sync_committee;
129/// use eth_state_diff::types::SyncCommitteeDiff;
130///
131/// let base = b"committee-a";
132/// let target = b"committee-b";
133///
134/// let delta = diff_sync_committee(base, target);
135///
136/// assert_eq!(
137/// delta,
138/// SyncCommitteeDiff::FullReplacement(target.to_vec())
139/// );
140/// ```
141pub fn diff_sync_committee(base_ssz: &[u8], target_ssz: &[u8]) -> SyncCommitteeDiff {
142 if base_ssz == target_ssz {
143 SyncCommitteeDiff::Unchanged
144 } else {
145 SyncCommitteeDiff::FullReplacement(target_ssz.to_vec())
146 }
147}
148
149/// Applies a sync committee delta to a serialized SSZ committee in place.
150///
151/// [`SyncCommitteeDiff::Unchanged`] leaves `base` untouched.
152///
153/// [`SyncCommitteeDiff::FullReplacement`] clears `base` and replaces it with
154/// the serialized committee stored in the delta.
155///
156/// After successful execution, `base` contains the serialized target committee
157/// represented by `delta`.
158///
159/// # Arguments
160///
161/// * `base` - Serialized SSZ bytes of the base committee. This buffer is
162/// modified in place.
163/// * `delta` - Archived sync committee delta to apply.
164///
165/// # Complexity
166///
167/// - [`SyncCommitteeDiff::Unchanged`]: O(1) time and O(1) additional space.
168/// - [`SyncCommitteeDiff::FullReplacement`]: O(n) time, where *n* is the
169/// replacement size.
170///
171/// The replacement case may allocate when `base` does not have sufficient
172/// capacity for the target committee.
173///
174/// # Example
175///
176/// ```
177/// use eth_state_diff::sync_committee::diff_sync_committee;
178/// use eth_state_diff::sync_committee::apply_sync_committee;
179/// use eth_state_diff::types::ArchivedSyncCommitteeDiff;
180///
181/// let mut base = b"committee-a".to_vec();
182/// let target = b"committee-b";
183///
184/// let delta = diff_sync_committee(&base, target);
185///
186/// let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&delta).unwrap();
187/// let archived = unsafe {
188/// rkyv::access_unchecked::<ArchivedSyncCommitteeDiff>(&bytes)
189/// };
190///
191/// apply_sync_committee(&mut base, archived);
192///
193/// assert_eq!(base, target);
194/// ```
195pub fn apply_sync_committee(base: &mut Vec<u8>, delta: &ArchivedSyncCommitteeDiff) {
196 match delta {
197 ArchivedSyncCommitteeDiff::Unchanged => {}
198 ArchivedSyncCommitteeDiff::FullReplacement(replacement) => {
199 base.clear();
200 base.extend_from_slice(replacement.as_slice());
201 }
202 }
203}