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//! The module provides both slice-based and iterator-based APIs.
32//!
33//! [`diff_participation`] and [`apply_participation`] operate on contiguous
34//! vectors and provide the specialized [`ParticipationDiff::AllZeros`] fast
35//! path.
36//!
37//! [`diff_participation_iter`] and [`apply_participation_iter`] operate through
38//! iterators and [`crate::ListMutTarget`], allowing consensus clients with
39//! tree-backed or otherwise non-contiguous state representations to compute
40//! and apply participation deltas without first materializing the complete
41//! vector.
42//!
43//! The iterator-based diff API always produces the sparse representation.
44//! The slice-based API can additionally detect an all-zero target and use the
45//! more compact [`ParticipationDiff::AllZeros`] representation.
46//!
47//! ## Reconstruction
48//!
49//! Applying a sparse delta updates only the modified indices of the existing
50//! collection and then appends any values in `extension`.
51//!
52//! Applying an [`ParticipationDiff::AllZeros`] delta replaces the destination
53//! with a vector of the specified length containing only zero values.
54//!
55//! ## Complexity
56//!
57//! Diff generation is `O(n)` time and `O(m)` additional space, where `n` is
58//! the number of participation flags examined and `m` is the number of
59//! modified entries.
60//!
61//! Sparse delta application is `O(m + e)` time, where `m` is the number of
62//! modified entries and `e` is the number of appended participation flags.
63//!
64//! The contiguous all-zero fast path performs `O(n)` work to initialize the
65//! resulting vector.
66//!
67//! ## Serialization
68//!
69//! [`ParticipationDiff`] is designed to be serialized with `rkyv` and can be
70//! subsequently compressed with a general-purpose compressor such as `zstd`.
71
72use crate::{
73    balances::{read_varint, write_varint},
74    types::{ArchivedParticipationDiff, ParticipationDiff},
75};
76
77/// Computes a compact delta between two participation flag slices.
78///
79/// The returned [`ParticipationDiff`] contains the information required to
80/// reconstruct `target` from `base`.
81///
82/// If every flag in `target` is zero, the function uses the specialized
83/// [`ParticipationDiff::AllZeros`] representation. Otherwise it produces a
84/// sparse delta containing only modified flags.
85///
86/// This is the contiguous-slice convenience API. Clients whose participation
87/// flags are stored in a non-contiguous representation can use
88/// [`diff_participation_iter`] instead.
89///
90/// # Complexity
91///
92/// `O(n)` time and `O(m)` additional space, where `n` is the number of flags
93/// examined and `m` is the number of modified flags.
94pub fn diff_participation(base: &[u8], target: &[u8]) -> ParticipationDiff {
95    // Fast path for skip slots
96    if target.iter().all(|&v| v == 0) {
97        return ParticipationDiff::AllZeros(target.len());
98    }
99
100    diff_participation_iter(base.iter().copied(), target.iter().copied())
101}
102
103/// Applies a participation delta to a contiguous vector in place.
104///
105/// This is the contiguous-vector convenience API. Sparse deltas are delegated
106/// to [`apply_participation_iter`], while [`ParticipationDiff::AllZeros`] is
107/// handled directly by replacing the destination with a zero-filled vector of
108/// the encoded length.
109///
110/// After successful application, `base` contains the target participation
111/// vector from which `delta` was produced.
112///
113/// # Complexity
114///
115/// Sparse deltas require `O(m + e)` work, where `m` is the number of modified
116/// entries and `e` is the number of appended flags.
117///
118/// An [`ParticipationDiff::AllZeros`] delta requires `O(n)` work to construct
119/// the resulting zero-filled vector of length `n`.
120///
121/// # Panics
122///
123/// Panics if a sparse delta contains an invalid or internally inconsistent
124/// payload.
125pub fn apply_participation(base: &mut Vec<u8>, delta: &ArchivedParticipationDiff) {
126    match delta {
127        ArchivedParticipationDiff::AllZeros(len) => {
128            base.clear();
129            base.resize(len.to_native() as usize, 0);
130        }
131        // Delegate sparse application to the generic iterator implementation
132        sparse => apply_participation_iter(base, sparse),
133    }
134}
135
136/// Computes a compact sparse delta between two participation flag iterators.
137///
138/// This API is intended for consensus clients whose participation flags are
139/// stored in tree-backed or otherwise non-contiguous structures. The caller
140/// can expose the values through [`ExactSizeIterator`]s without first
141/// materializing the complete vectors as contiguous buffers.
142///
143/// The iterators are consumed during diff generation.
144///
145/// Unlike [`diff_participation`], this function always returns
146/// [`ParticipationDiff::Sparse`]. It does not perform the all-zero
147/// specialization because the iterator is consumed while determining the
148/// changed entries.
149///
150/// Values remaining in `target` after the common portion are treated as
151/// newly appended participation flags and are stored in the delta's
152/// `extension` field.
153///
154/// # Complexity
155///
156/// `O(n)` time and `O(m + e)` additional space, where `n` is the size of the
157/// common portion, `m` is the number of modified entries, and `e` is the
158/// number of appended target entries.
159pub fn diff_participation_iter<I1, I2>(mut base: I1, mut target: I2) -> ParticipationDiff
160where
161    I1: ExactSizeIterator<Item = u8>,
162    I2: ExactSizeIterator<Item = u8>,
163{
164    let common_len = base.len().min(target.len());
165
166    // Reserve space for a typical sparse update while allowing the vectors
167    // to grow normally for larger participation changes.
168    let mut sparse_indices = Vec::with_capacity(50_000);
169    let mut new_values = Vec::with_capacity(50_000);
170
171    let mut last_idx = 0u64;
172
173    for i in 0..common_len {
174        let v1 = base.next().unwrap();
175        let v2 = target.next().unwrap();
176
177        if v1 != v2 {
178            // Calculate and write the gap from the last changed index
179            let gap = (i - last_idx as usize) as u64;
180            write_varint(gap, &mut sparse_indices);
181
182            new_values.push(v2);
183            last_idx = i as u64;
184        }
185    }
186
187    // Any remaining items in the target iterator are newly appended flags
188    let extension = target.collect();
189
190    ParticipationDiff::Sparse {
191        sparse_indices,
192        new_values,
193        extension,
194    }
195}
196
197/// Applies a participation delta to a mutable collection in place.
198///
199/// This API is intended for consensus clients whose participation flags are
200/// stored in tree-backed or otherwise non-contiguous structures.
201///
202/// The destination collection is updated according to the sparse entries in
203/// `delta`. Each encoded index gap identifies the next modified entry, whose
204/// value is replaced with the corresponding entry from `new_values`. Values
205/// in `extension` are then appended to the destination.
206///
207/// [`ParticipationDiff::AllZeros`] is intentionally not handled by this
208/// function. Callers using the generic API must handle that representation
209/// separately if they need to support it.
210///
211/// # Panics
212///
213/// Panics if the sparse delta contains an invalid or inconsistent payload,
214/// including malformed varint data or fewer encoded indices than replacement
215/// values.
216///
217/// The implementation may also extend the destination if an encoded index
218/// falls outside its current length.
219///
220/// # Complexity
221///
222/// `O(m + e)` time and `O(1)` additional working space, excluding allocations
223/// performed by the destination collection when it grows.
224///
225/// Here `m` is the number of modified entries and `e` is the number of
226/// appended entries.
227pub fn apply_participation_iter<T: crate::ListMutTarget<u8>>(
228    target: &mut T,
229    delta: &ArchivedParticipationDiff,
230) {
231    if let ArchivedParticipationDiff::Sparse {
232        sparse_indices,
233        new_values,
234        extension,
235    } = delta
236    {
237        let indices_raw = sparse_indices.as_slice();
238        let values_iter = new_values.iter();
239
240        let mut cursor = 0usize;
241        let mut current_idx = 0usize;
242
243        for val in values_iter {
244            // Decode the gap to the next changed index
245            let gap = read_varint(indices_raw, &mut cursor) as usize;
246            current_idx += gap;
247
248            // Defensively grow the target if the delta implies an index
249            // beyond the current length (should rarely happen in practice).
250            while target.len() <= current_idx {
251                target.push(0);
252            }
253
254            // Apply the change
255            *target.get_mut(current_idx).unwrap() = *val;
256        }
257
258        // Append any new flags from the extension
259        for byte in extension.iter() {
260            target.push(*byte);
261        }
262    }
263}