Skip to main content

eth_state_diff/
recent_roots.rs

1//! Delta encoding for fixed-capacity Ethereum consensus root buffers.
2//!
3//! Ethereum consensus stores historical roots, such as block roots and state
4//! roots, in fixed-capacity circular buffers. As new slots are processed,
5//! entries are written at positions derived from their slot number, eventually
6//! wrapping around and overwriting older entries.
7//!
8//! Rather than storing the complete root buffer, this module records only the
9//! roots written during a requested slot range. Applying the delta replays
10//! those writes into another root buffer using the same slot-to-index mapping.
11//!
12//! The delta contains no explicit buffer indices. Each index is reconstructed
13//! from the slot number and the buffer capacity:
14//!
15//! ```text
16//! buffer_index = slot % buffer_capacity
17//! ```
18//!
19//! # Representation
20//!
21//! [`RootsDiff`] stores one 32-byte root for every slot in the half-open range
22//! `[base_slot, target_slot)`.
23//!
24//! For example, a transition from slot `100` to slot `103` records the roots
25//! for slots:
26//!
27//! ```text
28//! 100, 101, 102
29//! ```
30//!
31//! The root for `target_slot` itself is not included.
32//!
33//! # Correctness
34//!
35//! The destination buffer must have the same capacity as the buffer supplied
36//! to [`diff_roots`]. The delta does not store explicit buffer indices, so
37//! changing the capacity changes the modulo mapping and can cause roots to be
38//! written to different positions.
39//!
40//! The `base_slot` supplied to [`apply_roots`] must also be the same starting
41//! slot used to generate the delta. Because the delta stores only the sequence
42//! of roots, changing the starting slot changes the positions at which those
43//! roots are written.
44//!
45//! The delta represents only the recorded slot writes. Contents at positions
46//! not covered by the slot range are preserved when the delta is applied.
47//!
48//! # Complexity
49//!
50//! If `N = target_slot - base_slot`:
51//!
52//! - [`diff_roots`] runs in O(N) time and uses O(N) additional space.
53//! - [`apply_roots`] runs in O(N) time and uses O(1) additional space.
54
55use crate::{
56    error::Error,
57    types::{ArchivedRootsDiff, RootsDiff},
58};
59
60/// Computes the sequence of roots written during a slot range.
61///
62/// The returned delta contains one root for every slot in the half-open range
63/// `[base_slot, target_slot)`.
64///
65/// For each slot `s`, the root is read from the circular buffer at:
66///
67/// ```text
68/// buffer_index = s % buffer.len()
69/// ```
70///
71/// The root corresponding to `target_slot` is not included.
72///
73/// # Arguments
74///
75/// * `base_slot` - The first slot whose root is included in the delta.
76/// * `target_slot` - The slot immediately following the final recorded root.
77/// * `buffer` - Circular root buffer belonging to the target state.
78///
79/// # Returns
80///
81/// A [`RootsDiff`] containing the target root for every slot in
82/// `[base_slot, target_slot)`.
83///
84/// If `base_slot == target_slot`, the returned delta contains no roots.
85///
86/// # Panics
87///
88/// Panics if `target_slot < base_slot`.
89///
90/// Panics if `buffer` is empty because circular-buffer indexing requires a
91/// non-zero capacity.
92///
93/// # Correctness
94///
95/// The buffer capacity is part of the implicit representation because root
96/// indices are reconstructed using modulo arithmetic.
97///
98/// The buffer supplied to [`apply_roots`] must therefore have the same capacity
99/// as `buffer`. The same `base_slot` must also be supplied when applying the
100/// resulting delta.
101///
102/// # Example
103///
104/// ```
105/// use eth_state_diff::recent_roots::diff_roots;
106///
107/// let mut target_buffer = vec![[0u8; 32]; 4];
108/// target_buffer[0] = [1u8; 32];
109/// target_buffer[1] = [2u8; 32];
110/// target_buffer[2] = [3u8; 32];
111///
112/// let delta = diff_roots(0, 3, &target_buffer);
113///
114/// assert_eq!(delta.roots.len(), 3);
115/// assert_eq!(delta.roots[0], [1u8; 32]);
116/// assert_eq!(delta.roots[1], [2u8; 32]);
117/// assert_eq!(delta.roots[2], [3u8; 32]);
118/// ```
119///
120/// # Complexity
121///
122/// Let `N = target_slot - base_slot`.
123///
124/// - Time: O(N)
125/// - Additional space: O(N)
126pub fn diff_roots(base_slot: u64, target_slot: u64, buffer: &[[u8; 32]]) -> RootsDiff {
127    assert!(
128        target_slot >= base_slot,
129        "target_slot must be greater than or equal to base_slot"
130    );
131
132    assert!(!buffer.is_empty(), "root buffer must not be empty");
133
134    let capacity = buffer.len() as u64;
135
136    let span = target_slot - base_slot;
137    let roots_cap = usize::try_from(span).expect("root span exceeds usize capacity");
138    let mut roots = Vec::with_capacity(roots_cap);
139
140    for i in 0..span {
141        let slot = base_slot + i;
142        let idx = (slot % capacity) as usize;
143        roots.push(
144            *buffer
145                .get(idx)
146                .expect("modulo arithmetic guarantees index is within bounds"),
147        );
148    }
149
150    RootsDiff { roots }
151}
152
153/// Applies a root delta to a circular root buffer in place.
154///
155/// Each root stored in `delta` is written to the destination buffer using the
156/// same slot-to-index mapping used by [`diff_roots`]:
157///
158/// ```text
159/// buffer_index = slot % buffer_capacity
160/// ```
161///
162/// The first root in the delta corresponds to `base_slot`. Each subsequent
163/// root corresponds to the next slot.
164///
165/// # Arguments
166///
167/// * `base_slot` - The slot corresponding to the first root stored in `delta`.
168///   This must be the same starting slot used to generate the delta.
169/// * `base_buffer` - Destination circular root buffer. It is modified in place
170///   and must have the same capacity as the buffer used to generate the delta.
171/// * `delta` - Archived [`RootsDiff`] containing the roots to replay.
172///
173/// # Correctness
174///
175/// This function is the application counterpart to [`diff_roots`].
176///
177/// For correct reconstruction, `base_buffer` must have the same capacity as
178/// the buffer supplied to [`diff_roots`], and `base_slot` must be the same
179/// starting slot used when generating the delta.
180///
181/// The delta does not contain explicit buffer indices. Indices are derived
182/// from `base_slot` and the destination buffer capacity. Contents at positions
183/// outside the recorded slot range are preserved.
184///
185/// # Errors
186///
187/// Returns [`Error::InvalidDelta`] if `base_buffer` is empty.
188///
189/// # Example
190///
191/// ```
192/// use eth_state_diff::recent_roots::{apply_roots, diff_roots};
193/// use eth_state_diff::types::ArchivedRootsDiff;
194///
195/// let mut target_buffer = vec![[0u8; 32]; 4];
196/// target_buffer[0] = [1u8; 32];
197/// target_buffer[1] = [2u8; 32];
198/// target_buffer[2] = [3u8; 32];
199///
200/// let delta = diff_roots(0, 3, &target_buffer);
201///
202/// let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&delta).expect("valid delta");
203/// let archived = rkyv::access::<ArchivedRootsDiff, rkyv::rancor::Error>(&bytes)
204///     .expect("test setup: failed to access archived delta");
205///
206/// let mut reconstructed = vec![[0u8; 32]; 4];
207/// apply_roots(0, &mut reconstructed, archived).expect("valid delta");
208///
209/// assert_eq!(reconstructed, target_buffer);
210/// ```
211///
212/// # Complexity
213///
214/// If `N` roots are stored in `delta`:
215///
216/// - Time: O(N)
217/// - Additional space: O(1)
218pub fn apply_roots(
219    base_slot: u64,
220    base_buffer: &mut [[u8; 32]],
221    delta: &ArchivedRootsDiff,
222) -> Result<(), Error> {
223    if base_buffer.is_empty() {
224        return Err(Error::InvalidDelta("root buffer must not be empty".into()));
225    }
226
227    let capacity = base_buffer.len() as u64;
228
229    for (i, root) in delta.roots.iter().enumerate() {
230        let slot = base_slot + i as u64;
231        let idx = (slot % capacity) as usize;
232
233        let Some(value) = base_buffer.get_mut(idx) else {
234            return Err(Error::InvalidDelta(format!(
235                "root index {idx} is out of bounds for buffer capacity {capacity}",
236            )));
237        };
238
239        *value = *root;
240    }
241
242    Ok(())
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248    use crate::types::ArchivedRootsDiff;
249
250    /// Helper to perform a full roundtrip: diff -> rkyv serialize -> rkyv access -> apply
251    fn assert_roundtrip(
252        base_slot: u64,
253        target_slot: u64,
254        initial_buffer: &mut [[u8; 32]],
255        target_buffer: &[[u8; 32]],
256    ) {
257        let delta = diff_roots(base_slot, target_slot, target_buffer);
258
259        let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&delta).expect("test setup: serialize");
260        let archived = rkyv::access::<ArchivedRootsDiff, rkyv::rancor::Error>(&bytes)
261            .expect("test setup: failed to access archived delta");
262
263        apply_roots(base_slot, initial_buffer, archived).expect("test setup: apply");
264
265        assert_eq!(initial_buffer, target_buffer);
266    }
267
268    #[test]
269    fn test_diff_empty_span() {
270        let buffer = vec![[0u8; 32]; 4];
271        let delta = diff_roots(5, 5, &buffer);
272        assert!(delta.roots.is_empty());
273    }
274
275    #[test]
276    fn test_diff_and_apply_single_slot() {
277        let mut target_buffer = vec![[0u8; 32]; 4];
278        target_buffer[0] = [1u8; 32];
279
280        let mut base_buffer = vec![[0u8; 32]; 4];
281        assert_roundtrip(0, 1, &mut base_buffer, &target_buffer);
282    }
283
284    #[test]
285    fn test_diff_and_apply_no_wrap() {
286        let mut target_buffer = vec![[0u8; 32]; 4];
287        target_buffer[0] = [1u8; 32];
288        target_buffer[1] = [2u8; 32];
289        target_buffer[2] = [3u8; 32];
290
291        let mut base_buffer = vec![[0u8; 32]; 4];
292        assert_roundtrip(0, 3, &mut base_buffer, &target_buffer);
293    }
294
295    #[test]
296    fn test_diff_and_apply_with_wrap() {
297        // Capacity 4. Slots 2, 3, 4.
298        // Indices: 2 % 4 = 2, 3 % 4 = 3, 4 % 4 = 0.
299        let mut target_buffer = vec![[0u8; 32]; 4];
300        target_buffer[1] = [55u8; 32]; // Untouched index
301        target_buffer[2] = [10u8; 32];
302        target_buffer[3] = [11u8; 32];
303        target_buffer[0] = [12u8; 32]; // Wrapped around
304
305        // Base buffer must match the untouched index!
306        let mut base_buffer = vec![[0u8; 32]; 4];
307        base_buffer[1] = [55u8; 32];
308
309        assert_roundtrip(2, 5, &mut base_buffer, &target_buffer);
310    }
311
312    #[test]
313    fn test_diff_and_apply_multiple_wraps() {
314        // Capacity 2. Slots 0, 1, 2, 3, 4.
315        // Indices: 0, 1, 0, 1, 0.
316        let mut target_buffer = vec![[0u8; 32]; 2];
317        target_buffer[0] = [4u8; 32]; // Overwritten by slot 4
318        target_buffer[1] = [3u8; 32]; // Overwritten by slot 3
319
320        let mut base_buffer = vec![[0u8; 32]; 2];
321        assert_roundtrip(0, 5, &mut base_buffer, &target_buffer);
322    }
323
324    #[test]
325    fn test_apply_errors_on_empty_buffer() {
326        let delta = RootsDiff {
327            roots: vec![[0u8; 32]],
328        };
329
330        let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&delta).expect("test setup: serialize");
331        let archived = rkyv::access::<ArchivedRootsDiff, rkyv::rancor::Error>(&bytes)
332            .expect("test setup: failed to access archived delta");
333
334        let mut buffer: Vec<[u8; 32]> = vec![];
335        let result = apply_roots(0, &mut buffer, archived);
336        assert!(result.is_err());
337        let err_str = format!("{}", result.expect_err("test setup"));
338        assert!(err_str.contains("root buffer must not be empty"));
339    }
340}