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}