Skip to main content

eth_state_diff/
slashings.rs

1//! Delta encoding for Ethereum slashing vectors.
2//!
3//! Ethereum consensus stores slashing totals in a fixed-capacity circular
4//! buffer indexed by epoch.
5//!
6//! Unlike root and RANDAO buffers, where every slot or epoch transition may
7//! require recording a new value, slashing totals are comparatively sparse.
8//! This module therefore records only ring-buffer entries whose values differ
9//! between the base and target states at the ring positions touched by the
10//! epoch transition.
11//!
12//! The delta stores `(ring_index, value)` pairs. Applying the delta writes only
13//! those recorded values into the destination buffer; all other entries remain
14//! untouched.
15//!
16//! # Epoch Mapping
17//!
18//! Ring-buffer indices are derived from the epoch number:
19//!
20//! ```text
21//! ring_index = epoch % buffer_capacity
22//! ```
23//!
24//! [`diff_slashings`] examines the ring positions corresponding to the epoch
25//! boundaries crossed between `base_slot` and `target_slot`. The base epoch
26//! itself is not examined. For a transition from `base_epoch` to
27//! `target_epoch`, the epochs mapped to ring positions are:
28//!
29//! ```text
30//! base_epoch + 1, ..., target_epoch
31//! ```
32//!
33//! If both slots belong to the same epoch, no ring positions are examined and
34//! the resulting delta is empty.
35//!
36//! If the transition spans more than one complete ring cycle, the same
37//! ring-buffer index may be examined more than once. Each occurrence compares
38//! the current values at that index in the base and target buffers. Applying
39//! the resulting updates in order still reconstructs the target buffer.
40//!
41//! # Correctness
42//!
43//! The base and target buffers must have the same capacity and represent the
44//! same logical slashing ring. The delta stores ring indices rather than epoch
45//! numbers, so the buffer capacity is part of the implicit representation.
46//!
47//! Applying a delta to a buffer with a different capacity, layout, or unrelated
48//! state can write values to incorrect positions.
49//!
50//! The delta only records positions whose base and target values differ among
51//! the ring positions examined by the transition. Positions omitted from the
52//! delta are expected to already contain their target values.
53//!
54//! # Complexity
55//!
56//! If `E` is the number of epoch boundaries crossed by the transition:
57//!
58//! - [`diff_slashings`] runs in O(E) time and O(U) additional space, where `U`
59//!   is the number of recorded updates.
60//! - [`apply_slashings`] runs in O(U) time and O(1) additional space.
61
62use crate::types::{ArchivedSlashingsDiff, SlashingsDiff, SLOTS_PER_EPOCH};
63
64/// Computes a sparse delta between two slashing ring buffers.
65///
66/// The function examines the ring-buffer positions corresponding to epoch
67/// boundaries crossed between `base_slot` and `target_slot`.
68///
69/// For a base epoch `B` and target epoch `T`, the examined epochs are:
70///
71/// ```text
72/// B + 1, B + 2, ..., T
73/// ```
74///
75/// For each examined epoch `E`, its ring-buffer position is:
76///
77/// ```text
78/// index = E % buffer_capacity
79/// ```
80///
81/// The values at that position are then compared between `base_buffer` and
82/// `target_buffer`. If they differ, the target value is recorded in the
83/// returned [`SlashingsDiff`].
84///
85/// If the transition spans multiple complete ring cycles, a ring index may be
86/// examined multiple times. In that case, the same index may produce multiple
87/// updates containing the same target value. This is redundant but remains
88/// correct because applying the updates leaves the destination with the target
89/// value at that index.
90///
91/// # Arguments
92///
93/// * `base_slot` - Slot belonging to the base state.
94/// * `target_slot` - Slot belonging to the target state.
95/// * `base_buffer` - Slashing ring buffer belonging to the base state.
96/// * `target_buffer` - Slashing ring buffer belonging to the target state.
97///
98/// # Returns
99///
100/// A [`SlashingsDiff`] containing updates for ring-buffer positions whose base
101/// and target values differ at one or more epoch boundaries crossed by the
102/// transition.
103///
104/// If `base_slot` and `target_slot` belong to the same epoch, the returned
105/// delta contains no updates.
106///
107/// # Panics
108///
109/// Panics if `target_slot < base_slot`.
110///
111/// Panics if `base_buffer` or `target_buffer` is empty.
112///
113/// Panics if `base_buffer` and `target_buffer` have different lengths.
114///
115/// # Correctness
116///
117/// The two buffers must have the same capacity and correspond to the same
118/// logical slashing ring.
119///
120/// The delta stores ring indices rather than epoch numbers. Consequently, the
121/// buffer capacity and logical layout are part of the implicit encoding and
122/// must remain unchanged when applying the resulting delta.
123///
124/// # Example
125///
126/// ```
127/// use eth_state_diff::slashings::diff_slashings;
128///
129/// let base_buffer = vec![0u64; 4];
130/// let mut target_buffer = vec![0u64; 4];
131///
132/// // The transition crosses from epoch 0 to epoch 1.
133/// target_buffer[1] = 100;
134///
135/// let delta = diff_slashings(
136///     0,
137///     eth_state_diff::types::SLOTS_PER_EPOCH,
138///     &base_buffer,
139///     &target_buffer,
140/// );
141///
142/// assert_eq!(delta.updates.len(), 1);
143/// assert_eq!(delta.updates[0], (1, 100));
144/// ```
145///
146/// # Complexity
147///
148/// If `E` epoch boundaries are crossed and `U` updates are recorded:
149///
150/// - Time: O(E)
151/// - Additional space: O(U)
152pub fn diff_slashings(
153    base_slot: u64,
154    target_slot: u64,
155    base_buffer: &[u64],
156    target_buffer: &[u64],
157) -> SlashingsDiff {
158    assert!(
159        target_slot >= base_slot,
160        "target_slot must be greater than or equal to base_slot"
161    );
162
163    assert!(!base_buffer.is_empty(), "slashing buffer must not be empty");
164
165    assert!(
166        !target_buffer.is_empty(),
167        "slashing buffer must not be empty"
168    );
169
170    assert_eq!(
171        base_buffer.len(),
172        target_buffer.len(),
173        "base and target slashing buffers must have the same capacity"
174    );
175
176    let base_epoch = base_slot / SLOTS_PER_EPOCH;
177    let target_epoch = target_slot / SLOTS_PER_EPOCH;
178    let capacity = base_buffer.len() as u64;
179
180    let mut updates = Vec::new();
181    let mut current_epoch = base_epoch;
182
183    while current_epoch < target_epoch {
184        current_epoch += 1;
185
186        let idx = (current_epoch % capacity) as usize;
187
188        let base_value = base_buffer[idx];
189        let target_value = target_buffer[idx];
190
191        if base_value != target_value {
192            updates.push((idx as u16, target_value));
193        }
194    }
195
196    SlashingsDiff { updates }
197}
198
199/// Applies a sparse slashing delta to a circular slashing buffer in place.
200///
201/// Each update in `delta` contains a ring-buffer index and its replacement
202/// value. Only the recorded indices are modified; all other entries in
203/// `base_buffer` remain unchanged.
204///
205/// The delta already contains the ring-buffer indices, so no epoch calculation
206/// is required during application.
207///
208/// # Arguments
209///
210/// * `base_buffer` - Destination slashing ring buffer. It is modified in place.
211/// * `delta` - Archived [`SlashingsDiff`] containing the sparse updates.
212///
213/// # Correctness
214///
215/// The destination buffer must have the same capacity and logical ring layout
216/// as the buffer used to create the delta.
217///
218/// After successful application, every index represented by the delta contains
219/// its recorded target value. Entries omitted from the delta retain their
220/// existing values.
221///
222/// # Panics
223///
224/// Panics if an update contains an index outside `base_buffer`.
225///
226/// # Example
227///
228/// ```
229/// use eth_state_diff::slashings::{apply_slashings, diff_slashings};
230/// use eth_state_diff::types::ArchivedSlashingsDiff;
231///
232/// let base_buffer = vec![0u64; 4];
233/// let mut target_buffer = vec![0u64; 4];
234/// target_buffer[1] = 100;
235///
236/// let delta = diff_slashings(
237///     0,
238///     eth_state_diff::types::SLOTS_PER_EPOCH,
239///     &base_buffer,
240///     &target_buffer,
241/// );
242///
243/// let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&delta).unwrap();
244/// let archived = unsafe {
245///     rkyv::access_unchecked::<ArchivedSlashingsDiff>(&bytes)
246/// };
247///
248/// let mut reconstructed = base_buffer.clone();
249/// apply_slashings(&mut reconstructed, archived);
250///
251/// assert_eq!(reconstructed, target_buffer);
252/// ```
253///
254/// # Complexity
255///
256/// If `U` updates are stored in the delta:
257///
258/// - Time: O(U)
259/// - Additional space: O(1)
260pub fn apply_slashings(base_buffer: &mut [u64], delta: &ArchivedSlashingsDiff) {
261    for update in delta.updates.iter() {
262        let idx = update.0.to_native() as usize;
263        let value = update.1.to_native();
264
265        base_buffer[idx] = value;
266    }
267}