Skip to main content

eth_state_diff/
participation.rs

1//! Compact delta encoding for Ethereum epoch participation flags.
2//!
3//! This module computes and applies deltas between validator participation
4//! vectors.
5//!
6//! Participation changes are typically sparse: during an epoch transition,
7//! most validators retain their existing participation flags while only a
8//! subset of validators receive new values. The delta therefore stores only
9//! modified indices and their replacement values rather than the complete
10//! target vector.
11//!
12//! ## Encoding
13//!
14//! The delta has two representations:
15//!
16//! - [`ParticipationDiff::AllZeros`] represents a target vector containing
17//!   only zero-valued participation flags.
18//! - [`ParticipationDiff::Sparse`] stores only changed entries.
19//!
20//! For the sparse representation, modified indices are encoded as
21//! delta-varint gaps between successive modified indices. The corresponding
22//! replacement values are stored separately in `new_values`. This avoids
23//! storing unchanged participation flags and makes sequences of nearby
24//! changes particularly compact.
25//!
26//! Validators present in the target vector but not in the base vector are
27//! stored separately in `extension` and appended during application.
28//!
29//! ## APIs
30//!
31//! [`diff_participation`] and [`apply_participation`] operate on contiguous
32//! vectors and provide the specialized [`ParticipationDiff::AllZeros`] fast
33//! path.
34//!
35//! [`diff_participation_iter`] and [`apply_participation_iter`] operate through
36//! iterators and [`crate::ListMutTarget`], allowing consensus clients with
37//! tree-backed or otherwise non-contiguous state representations to compute
38//! and apply participation deltas without first materializing the complete
39//! vector.
40//!
41//! The iterator-based diff API always produces the sparse representation.
42//! The slice-based API can additionally detect an all-zero target and use the
43//! more compact [`ParticipationDiff::AllZeros`] representation.
44//!
45//! ## Reconstruction
46//!
47//! Applying a sparse delta updates only the modified indices of the existing
48//! collection and then appends any values in `extension`.
49//!
50//! Applying an [`ParticipationDiff::AllZeros`] delta replaces the destination
51//! with a vector of the specified length containing only zero values.
52//!
53//! ## Complexity
54//!
55//! Diff generation is `O(n)` time and `O(m)` additional space, where `n` is
56//! the number of participation flags examined and `m` is the number of
57//! modified entries.
58//!
59//! Sparse delta application is `O(m + e)` time, where `m` is the number of
60//! modified entries and `e` is the number of appended participation flags.
61//!
62//! The contiguous all-zero fast path performs `O(n)` work to initialize the
63//! resulting vector.
64//!
65//! ## Serialization
66//!
67//! [`ParticipationDiff`] is designed to be serialized with `rkyv` and can be
68//! subsequently compressed with a general-purpose compressor such as `zstd`.
69
70use crate::{
71    balances::{read_varint, write_varint},
72    types::{ArchivedParticipationDiff, ParticipationDiff},
73    Error,
74};
75
76/// Computes a compact delta between two participation flag slices.
77///
78/// The returned [`ParticipationDiff`] contains the information required to
79/// reconstruct `target` from `base`.
80///
81/// If every flag in `target` is zero, the function uses the specialized
82/// [`ParticipationDiff::AllZeros`] representation. Otherwise it produces a
83/// sparse delta containing only modified flags.
84///
85/// This is the contiguous-slice convenience API. Clients whose participation
86/// flags are stored in a non-contiguous representation can use
87/// [`diff_participation_iter`] instead.
88///
89/// # Complexity
90///
91/// `O(n)` time and `O(m)` additional space, where `n` is the number of flags
92/// examined and `m` is the number of modified flags.
93pub fn diff_participation(base: &[u8], target: &[u8]) -> ParticipationDiff {
94    if target.iter().all(|&value| value == 0) {
95        return ParticipationDiff::AllZeros(target.len());
96    }
97
98    diff_participation_iter(base.iter().copied(), target.iter().copied())
99}
100
101/// Applies a participation delta to a contiguous vector in place.
102///
103/// This is the contiguous-vector convenience API. Sparse deltas are delegated
104/// to [`apply_participation_iter`], while [`ParticipationDiff::AllZeros`] is
105/// handled directly by replacing the destination with a zero-filled vector of
106/// the encoded length.
107///
108/// After successful application, `base` contains the target participation
109/// vector from which `delta` was produced.
110///
111/// # Errors
112///
113/// Returns [`Error::InvalidDelta`] if an encoded [`ParticipationDiff::AllZeros`]
114/// length cannot be represented by the target vector or if a sparse delta is
115/// internally inconsistent.
116///
117/// Returns [`Error::MalformedDelta`] if a sparse delta contains invalid
118/// serialized payload data, such as a truncated or overflowing varint.
119///
120/// # Complexity
121///
122/// Sparse deltas require `O(m + e)` work, where `m` is the number of modified
123/// entries and `e` is the number of appended flags.
124///
125/// An [`ParticipationDiff::AllZeros`] delta requires `O(n)` work to construct
126/// the resulting zero-filled vector of length `n`.
127pub fn apply_participation(
128    base: &mut Vec<u8>,
129    delta: &ArchivedParticipationDiff,
130) -> Result<(), Error> {
131    match delta {
132        ArchivedParticipationDiff::AllZeros(len) => {
133            let len = usize::try_from(len.to_native()).map_err(|_| {
134                Error::InvalidDelta("all-zero participation length does not fit in usize".into())
135            })?;
136
137            base.clear();
138            base.resize(len, 0);
139
140            Ok(())
141        }
142        ArchivedParticipationDiff::Sparse { .. } => apply_participation_iter(base, delta),
143    }
144}
145
146/// Computes a compact sparse delta between two participation flag iterators.
147///
148/// This API is intended for consensus clients whose participation flags are
149/// stored in tree-backed or otherwise non-contiguous structures. The caller
150/// can expose the values through [`ExactSizeIterator`]s without first
151/// materializing the complete vectors as contiguous buffers.
152///
153/// The iterators are consumed during diff generation.
154///
155/// Unlike [`diff_participation`], this function always returns
156/// [`ParticipationDiff::Sparse`]. It does not perform the all-zero
157/// specialization because the iterator is consumed while determining the
158/// changed entries.
159///
160/// Values remaining in `target` after the common portion are treated as
161/// newly appended participation flags and are stored in the delta's
162/// `extension` field.
163///
164/// # Complexity
165///
166/// `O(n)` time and `O(m + e)` additional space, where `n` is the size of the
167/// common portion, `m` is the number of modified entries, and `e` is the
168/// number of appended target entries.
169pub fn diff_participation_iter<I1, I2>(mut base: I1, mut target: I2) -> ParticipationDiff
170where
171    I1: ExactSizeIterator<Item = u8>,
172    I2: ExactSizeIterator<Item = u8>,
173{
174    let common_len = base.len().min(target.len());
175
176    let mut sparse_indices = Vec::with_capacity(50_000);
177    let mut new_values = Vec::with_capacity(50_000);
178    let mut last_idx = 0u64;
179
180    for i in 0..common_len {
181        let Some(v1) = base.next() else {
182            break;
183        };
184
185        let Some(v2) = target.next() else {
186            break;
187        };
188
189        if v1 != v2 {
190            let idx = i as u64;
191
192            // `i >= last_idx` because changed indices are processed in
193            // strictly increasing iterator order.
194            let gap = idx - last_idx;
195            write_varint(gap, &mut sparse_indices);
196
197            new_values.push(v2);
198            last_idx = idx;
199        }
200    }
201
202    let extension = target.collect();
203
204    ParticipationDiff::Sparse {
205        sparse_indices,
206        new_values,
207        extension,
208    }
209}
210
211/// Applies a sparse participation delta to a mutable collection in place.
212///
213/// This API is intended for consensus clients whose participation flags are
214/// stored in tree-backed or otherwise non-contiguous structures.
215///
216/// The destination collection is updated according to the sparse entries in
217/// `delta`. Each encoded index gap identifies the next modified entry, whose
218/// value is replaced with the corresponding entry from `new_values`. Values
219/// in `extension` are then appended to the destination.
220///
221/// [`ParticipationDiff::AllZeros`] is not supported by this generic API
222/// because [`crate::ListMutTarget`] does not provide an operation for clearing
223/// or resizing an existing collection. Callers should use
224/// [`apply_participation`] when they need to support that representation.
225///
226/// # Errors
227///
228/// Returns [`Error::InvalidDelta`] if the supplied delta is not a sparse
229/// representation, if the number of encoded indices does not match the number
230/// of replacement values, if an encoded index cannot be represented as a
231/// `usize`, if an index falls outside the destination collection, or if the
232/// destination collection cannot provide the required element.
233///
234/// Returns [`Error::MalformedDelta`] if the sparse index payload contains an
235/// invalid or truncated varint.
236///
237/// # Complexity
238///
239/// `O(m + e)` time and `O(1)` additional working space, excluding allocations
240/// performed by the destination collection when it grows.
241///
242/// Here `m` is the number of modified entries and `e` is the number of
243/// appended entries.
244pub fn apply_participation_iter<T: crate::ListMutTarget<u8>>(
245    target: &mut T,
246    delta: &ArchivedParticipationDiff,
247) -> Result<(), Error> {
248    let ArchivedParticipationDiff::Sparse {
249        sparse_indices,
250        new_values,
251        extension,
252    } = delta
253    else {
254        return Err(Error::InvalidDelta(
255            "AllZeros participation delta cannot be applied through the generic iterator API"
256                .into(),
257        ));
258    };
259
260    let indices_raw = sparse_indices.as_slice();
261    let mut cursor = 0usize;
262    let mut current_idx = 0usize;
263
264    for value in new_values.iter() {
265        let gap = read_varint(indices_raw, &mut cursor)?;
266
267        let gap = usize::try_from(gap).map_err(|_| {
268            Error::MalformedDelta("participation index gap does not fit in usize".into())
269        })?;
270
271        current_idx = current_idx.checked_add(gap).ok_or_else(|| {
272            Error::MalformedDelta(
273                "participation index overflow while decoding sparse indices".into(),
274            )
275        })?;
276
277        let Some(target_value) = target.get_mut(current_idx) else {
278            return Err(Error::InvalidDelta(format!(
279                "participation index {current_idx} is outside target collection of length {}",
280                target.len()
281            )));
282        };
283
284        *target_value = *value;
285    }
286
287    if cursor != indices_raw.len() {
288        return Err(Error::InvalidDelta(
289            "participation sparse index payload contains unused bytes".into(),
290        ));
291    }
292
293    for byte in extension.iter() {
294        target.push(*byte);
295    }
296
297    Ok(())
298}