Skip to main content

eth_state_diff/
inactivity_scores.rs

1//! Delta encoding for validator inactivity scores.
2//!
3//! Inactivity scores are represented as a vector indexed by validator index.
4//! This module encodes changes between two score vectors without storing the
5//! complete target vector.
6//!
7//! Two representations are supported:
8//!
9//! - A sparse representation containing only changed indices and their target
10//!   values.
11//! - An all-zero representation for transitions to a completely zero-valued
12//!   target vector.
13//!
14//! Newly added validator scores are stored separately as an extension.
15//!
16//! Deltas produced by [`diff_inactivity`] can be applied in place using
17//! [`apply_inactivity`].
18
19use crate::types::{ArchivedInactivityDiff, InactivityDiff};
20
21/// Computes a compact delta between two validator inactivity-score vectors.
22///
23/// The returned delta contains sufficient information to reconstruct `target`
24/// from `base`.
25///
26/// If the target contains only zero-valued scores and the base contains at
27/// least one non-zero score, [`InactivityDiff::AllZeros`] is emitted. This
28/// avoids storing individual updates when the entire target vector has been
29/// cleared.
30///
31/// Otherwise, [`InactivityDiff::Sparse`] stores only the indices whose values
32/// changed and their corresponding target values. Scores belonging to newly
33/// appended validators are stored in the `extensions` field.
34///
35/// If `base` and `target` have different lengths, only their common prefix is
36/// compared. Any remaining target scores are treated as newly appended
37/// entries.
38///
39/// # Arguments
40///
41/// * `base` - Inactivity scores from the source state.
42/// * `target` - Inactivity scores from the target state.
43///
44/// # Returns
45///
46/// A compact [`InactivityDiff`] representing the transition from `base` to
47/// `target`.
48///
49/// # Complexity
50///
51/// O(n) time, where *n* is the length of the larger input vector.
52///
53/// Additional space is proportional to the number of changed scores plus the
54/// number of newly appended scores.
55///
56/// # Example
57///
58/// ```
59/// use eth_state_diff::inactivity_scores::diff_inactivity;
60/// use eth_state_diff::types::InactivityDiff;
61///
62/// let base = vec![10, 20, 30, 40];
63/// let target = vec![10, 25, 30, 50];
64///
65/// let delta = diff_inactivity(&base, &target);
66///
67/// assert_eq!(
68///     delta,
69///     InactivityDiff::Sparse {
70///         indices: vec![1, 3],
71///         new_values: vec![25, 50],
72///         extensions: vec![],
73///     }
74/// );
75/// ```
76///
77/// A completely cleared vector can be represented without storing individual
78/// zero values:
79///
80/// ```
81/// use eth_state_diff::inactivity_scores::diff_inactivity;
82/// use eth_state_diff::types::InactivityDiff;
83///
84/// let base = vec![10, 20, 30];
85/// let target = vec![0, 0, 0];
86///
87/// assert_eq!(
88///     diff_inactivity(&base, &target),
89///     InactivityDiff::AllZeros(3)
90/// );
91/// ```
92pub fn diff_inactivity(base: &[u64], target: &[u64]) -> InactivityDiff {
93    let target_is_zero = target.iter().all(|&v| v == 0);
94
95    if target_is_zero {
96        let base_has_non_zero = base.iter().any(|&v| v != 0);
97
98        if base_has_non_zero {
99            return InactivityDiff::AllZeros(target.len() as u32);
100        }
101    }
102
103    let common_len = base.len().min(target.len());
104    let mut indices = Vec::with_capacity(100);
105    let mut new_values = Vec::with_capacity(100);
106
107    for (i, (&v1, &v2)) in base.iter().zip(target.iter()).take(common_len).enumerate() {
108        if v1 != v2 {
109            indices.push(i as u32);
110            new_values.push(v2);
111        }
112    }
113
114    let extensions = target[common_len..].to_vec();
115
116    InactivityDiff::Sparse {
117        indices,
118        new_values,
119        extensions,
120    }
121}
122
123/// Applies an inactivity-score delta to a vector in place.
124///
125/// After successful execution, `base` contains the inactivity-score vector
126/// represented by `delta`.
127///
128/// [`ArchivedInactivityDiff::AllZeros`] clears the existing vector and
129/// recreates it with the specified number of zero-valued scores.
130///
131/// [`ArchivedInactivityDiff::Sparse`] updates the recorded indices and then
132/// appends any newly added validator scores.
133///
134/// # Panics
135///
136/// This function panics if a sparse delta contains an index that is outside
137/// the current `base` vector.
138///
139/// A sparse delta is therefore expected to have been produced for a compatible
140/// base state.
141///
142/// # Complexity
143///
144/// - [`ArchivedInactivityDiff::AllZeros`]: O(n), where *n* is the target
145///   vector length.
146/// - [`ArchivedInactivityDiff::Sparse`]: O(m + k), where *m* is the number of
147///   recorded updates and *k* is the number of appended scores.
148///
149/// # Example
150///
151/// ```
152/// use eth_state_diff::inactivity_scores::{apply_inactivity, diff_inactivity};
153/// use eth_state_diff::types::ArchivedInactivityDiff;
154///
155/// let mut base = vec![10, 20, 30];
156/// let target = vec![10, 25, 30];
157///
158/// let delta = diff_inactivity(&base, &target);
159///
160/// let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&delta).unwrap();
161/// let archived = unsafe {
162///     rkyv::access_unchecked::<ArchivedInactivityDiff>(&bytes)
163/// };
164///
165/// apply_inactivity(&mut base, archived);
166///
167/// assert_eq!(base, target);
168/// ```
169pub fn apply_inactivity(base: &mut Vec<u64>, delta: &ArchivedInactivityDiff) {
170    match delta {
171        ArchivedInactivityDiff::AllZeros(len) => {
172            base.clear();
173            base.resize(len.to_native() as usize, 0);
174        }
175
176        ArchivedInactivityDiff::Sparse {
177            indices,
178            new_values,
179            extensions,
180        } => {
181            for (idx, val) in indices.iter().zip(new_values.iter()) {
182                let i = (*idx).to_native() as usize;
183                base[i] = val.to_native();
184            }
185
186            base.reserve(extensions.len());
187            base.extend(extensions.iter().map(|v| v.to_native()));
188        }
189    }
190}