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