Skip to main content

eth_state_diff/
recent_roots.rs

1//! Delta encoding for fixed-size 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 diffing 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 indices, so changing
37//! the capacity changes the modulo mapping and can cause roots to be written
38//! to different positions.
39//!
40//! This representation is independent of the *absolute* buffer contents. Only
41//! the slot range, buffer capacity, and recorded roots are required to replay
42//! the writes.
43//!
44//! # Complexity
45//!
46//! If `N = target_slot - base_slot`:
47//!
48//! - [`diff_roots`] runs in O(N) time and uses O(N) additional space.
49//! - [`apply_roots`] runs in O(N) time and uses O(1) additional space.
50
51use crate::types::{ArchivedRootsDiff, RootsDiff};
52
53/// Computes the sequence of roots written during a slot range.
54///
55/// The returned delta contains one root for every slot in the half-open range
56/// `[base_slot, target_slot)`.
57///
58/// For each slot `s`, the root is read from the circular buffer at:
59///
60/// ```text
61/// buffer_index = s % buffer.len()
62/// ```
63///
64/// The root corresponding to `target_slot` is not included.
65///
66/// # Arguments
67///
68/// * `base_slot` - The first slot whose root is included in the delta.
69/// * `target_slot` - The slot immediately following the final recorded root.
70/// * `buffer` - Circular root buffer belonging to the target state.
71///
72/// # Returns
73///
74/// A [`RootsDiff`] containing the target root for every slot in
75/// `[base_slot, target_slot)`.
76///
77/// If `base_slot == target_slot`, the returned delta contains no roots.
78///
79/// # Panics
80///
81/// Panics if `target_slot < base_slot`.
82///
83/// Panics if `buffer` is empty, because calculating a circular-buffer index
84/// requires a non-zero capacity.
85///
86/// # Correctness
87///
88/// The buffer's capacity is part of the implicit representation because root
89/// indices are reconstructed using modulo arithmetic.
90///
91/// The buffer supplied to [`apply_roots`] must therefore have the same capacity
92/// as `buffer`.
93///
94/// # Example
95///
96/// ```
97/// use eth_state_diff::recent_roots::diff_roots;
98///
99/// let mut target_buffer = vec![[0u8; 32]; 4];
100/// target_buffer[0] = [1u8; 32];
101/// target_buffer[1] = [2u8; 32];
102/// target_buffer[2] = [3u8; 32];
103///
104/// let delta = diff_roots(0, 3, &target_buffer);
105///
106/// assert_eq!(delta.roots.len(), 3);
107/// assert_eq!(delta.roots[0], [1u8; 32]);
108/// assert_eq!(delta.roots[1], [2u8; 32]);
109/// assert_eq!(delta.roots[2], [3u8; 32]);
110/// ```
111///
112/// # Complexity
113///
114/// Let `N = target_slot - base_slot`.
115///
116/// - Time: O(N)
117/// - Additional space: O(N)
118pub fn diff_roots(base_slot: u64, target_slot: u64, buffer: &[[u8; 32]]) -> RootsDiff {
119    assert!(
120        target_slot >= base_slot,
121        "target_slot must be greater than or equal to base_slot"
122    );
123
124    assert!(!buffer.is_empty(), "root buffer must not be empty");
125
126    let span = target_slot - base_slot;
127    let capacity = buffer.len() as u64;
128
129    let mut roots = Vec::with_capacity(span as usize);
130
131    for i in 0..span {
132        let slot = base_slot + i;
133        let idx = (slot % capacity) as usize;
134        roots.push(buffer[idx]);
135    }
136
137    RootsDiff { roots }
138}
139
140/// Applies a root delta to a circular root buffer in place.
141///
142/// Each root stored in `delta` is written to the destination buffer using the
143/// same slot-to-index mapping used by [`diff_roots`]:
144///
145/// ```text
146/// buffer_index = slot % buffer_capacity
147/// ```
148///
149/// The first root in the delta corresponds to `base_slot`. Each subsequent
150/// root corresponds to the next slot.
151///
152/// # Arguments
153///
154/// * `base_slot` - The slot corresponding to the first root stored in `delta`.
155/// * `base_buffer` - Destination circular root buffer. It is modified in place.
156/// * `delta` - Archived [`RootsDiff`] containing the roots to replay.
157///
158/// # Correctness
159///
160/// This function is the application counterpart to [`diff_roots`].
161///
162/// For correct reconstruction, `base_buffer` must have the same capacity as
163/// the buffer that was supplied to [`diff_roots`].
164///
165/// The delta does not contain explicit buffer indices. Indices are derived
166/// from `base_slot` and the destination buffer capacity.
167///
168/// # Panics
169///
170/// Panics if `base_buffer` is empty, because circular-buffer indexing requires
171/// a non-zero capacity.
172///
173/// # Example
174///
175/// ```
176/// use eth_state_diff::recent_roots::{apply_roots, diff_roots};
177/// use eth_state_diff::types::ArchivedRootsDiff;
178///
179/// let mut target_buffer = vec![[0u8; 32]; 4];
180/// target_buffer[0] = [1u8; 32];
181/// target_buffer[1] = [2u8; 32];
182/// target_buffer[2] = [3u8; 32];
183///
184/// let delta = diff_roots(0, 3, &target_buffer);
185///
186/// let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&delta).unwrap();
187/// let archived = unsafe {
188///     rkyv::access_unchecked::<ArchivedRootsDiff>(&bytes)
189/// };
190///
191/// let mut reconstructed = vec![[0u8; 32]; 4];
192/// apply_roots(0, &mut reconstructed, archived);
193///
194/// assert_eq!(reconstructed, target_buffer);
195/// ```
196///
197/// # Complexity
198///
199/// If `N` roots are stored in `delta`:
200///
201/// - Time: O(N)
202/// - Additional space: O(1)
203pub fn apply_roots(base_slot: u64, base_buffer: &mut [[u8; 32]], delta: &ArchivedRootsDiff) {
204    assert!(!base_buffer.is_empty(), "root buffer must not be empty");
205
206    let capacity = base_buffer.len() as u64;
207
208    for (i, root) in delta.roots.iter().enumerate() {
209        let slot = base_slot + i as u64;
210        let idx = (slot % capacity) as usize;
211        base_buffer[idx] = *root;
212    }
213}