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