Skip to main content

eth_state_diff/
randao_mixes.rs

1//! Delta encoding for Ethereum RANDAO mix buffers.
2//!
3//! Ethereum consensus maintains historical RANDAO mixes in a fixed-capacity
4//! circular buffer. Each epoch writes one mix, with the buffer index derived
5//! from the epoch number modulo the buffer capacity.
6//!
7//! Rather than storing the complete RANDAO buffer, this module stores only the
8//! sequence of mixes needed to reconstruct the target buffer over the epochs
9//! covered by a slot transition. Applying the delta replays those epoch writes
10//! using the same circular-buffer indexing rule.
11//!
12//! The delta therefore contains no buffer indices. Indices are reconstructed
13//! deterministically from the starting slot and the destination buffer's
14//! capacity.
15//!
16//! # Representation
17//!
18//! [`RandaoDiff`] stores one 32-byte RANDAO mix for each epoch in the inclusive
19//! range from the epoch containing `base_slot` through the epoch containing
20//! `target_slot`.
21//!
22//! For an epoch `e` and buffer capacity `N`, the mix is stored at:
23//!
24//! ```text
25//! buffer_index = e % N
26//! ```
27//!
28//! During application, the same indexing rule is used starting from the epoch
29//! containing `base_slot`.
30//!
31//! # Correctness
32//!
33//! The source and destination buffers must have the same capacity. The delta
34//! does not store explicit buffer indices; instead, indices are reconstructed
35//! from the starting epoch and the destination buffer capacity.
36//!
37//! Consequently, applying a delta to a buffer with a different capacity can
38//! write mixes to different positions and will not reconstruct the original
39//! target state.
40//!
41//! The caller must also provide the same `base_slot` used when generating the
42//! delta. Because the delta stores only the sequence of mixes, changing the
43//! starting slot changes the epochs and therefore the destination indices at
44//! which those mixes are written.
45//!
46//! # Workflow
47//!
48//! The typical workflow is:
49//!
50//! 1. Call [`diff_randao`] with the base slot, target slot, and target RANDAO
51//!    buffer.
52//! 2. Serialize the resulting [`RandaoDiff`] using `rkyv`.
53//! 3. Store or compress the serialized delta.
54//! 4. Deserialize/access the archived delta and apply it with
55//!    [`apply_randao`] using the same `base_slot`.
56//!
57//! # Complexity
58//!
59//! If `E` is the number of epochs covered by the transition:
60//!
61//! - [`diff_randao`] runs in O(E) time and uses O(E) additional space.
62//! - [`apply_randao`] runs in O(E) time and uses O(1) additional space.
63
64use crate::types::{ArchivedRandaoDiff, RandaoDiff, SLOTS_PER_EPOCH};
65
66/// Computes a RANDAO delta between two consensus slots.
67///
68/// The returned delta contains one RANDAO mix for every epoch in the inclusive
69/// range from the epoch containing `base_slot` through the epoch containing
70/// `target_slot`.
71///
72/// Each mix is read from `target_buffer` using the circular-buffer indexing
73/// rule:
74///
75/// ```text
76/// buffer_index = epoch % target_buffer.len()
77/// ```
78///
79/// Only the mixes corresponding to the covered epochs are stored. The complete
80/// target buffer is not copied.
81///
82/// # Arguments
83///
84/// * `base_slot` - Starting consensus slot. The epoch containing this slot is
85///   the first epoch represented in the delta.
86/// * `target_slot` - Ending consensus slot. The epoch containing this slot is
87///   the final epoch represented in the delta.
88/// * `target_buffer` - RANDAO mix buffer belonging to the target state.
89///
90/// # Returns
91///
92/// A [`RandaoDiff`] containing one mix for each epoch from the base epoch
93/// through the target epoch, inclusive.
94///
95/// # Panics
96///
97/// Panics if `target_slot < base_slot`.
98///
99/// Panics if `target_buffer` is empty because circular-buffer indexing requires
100/// a non-zero capacity.
101///
102/// # Correctness
103///
104/// The delta stores mixes in chronological epoch order rather than storing
105/// their circular-buffer indices. During application, indices are reconstructed
106/// using modulo arithmetic and the capacity of the destination buffer.
107///
108/// The buffer used with [`apply_randao`] must therefore have the same capacity
109/// as `target_buffer`, and `apply_randao` must be given the same `base_slot`.
110///
111/// # Example
112///
113/// ```
114/// use eth_state_diff::randao_mixes::diff_randao;
115///
116/// let base_slot = 0;
117/// let target_slot = 64;
118///
119/// let target_buffer = vec![[0u8; 32]; 4];
120///
121/// let delta = diff_randao(base_slot, target_slot, &target_buffer);
122///
123/// // With 32 slots per epoch, slots 0 and 64 belong to epochs 0 and 2.
124/// // The inclusive epoch range is therefore 0..=2.
125/// assert_eq!(delta.mixes.len(), 3);
126/// ```
127///
128/// # Complexity
129///
130/// If `E` is the number of epochs from the base epoch through the target
131/// epoch, inclusive:
132///
133/// - Time: O(E)
134/// - Additional space: O(E)
135pub fn diff_randao(base_slot: u64, target_slot: u64, target_buffer: &[[u8; 32]]) -> RandaoDiff {
136    assert!(
137        target_slot >= base_slot,
138        "target_slot must be greater than or equal to base_slot"
139    );
140
141    assert!(!target_buffer.is_empty(), "RANDAO buffer must not be empty");
142
143    let base_epoch = base_slot / SLOTS_PER_EPOCH;
144    let target_epoch = target_slot / SLOTS_PER_EPOCH;
145    let capacity = target_buffer.len() as u64;
146
147    let mut mixes = Vec::with_capacity((target_epoch - base_epoch + 1) as usize);
148
149    for epoch in base_epoch..=target_epoch {
150        let idx = (epoch % capacity) as usize;
151        mixes.push(
152            *target_buffer
153                .get(idx)
154                .expect("modulo arithmetic guarantees index is within bounds"),
155        );
156    }
157
158    RandaoDiff { mixes }
159}
160
161/// Applies a RANDAO delta to a circular mix buffer in place.
162///
163/// Each mix stored in `delta` is written to the destination buffer at the
164/// position corresponding to its epoch. The first mix is written to the epoch
165/// containing `base_slot`; each subsequent mix advances by one epoch.
166///
167/// The destination index is reconstructed using:
168///
169/// ```text
170/// buffer_index = epoch % base_buffer.len()
171/// ```
172///
173/// No allocation is performed while applying the delta.
174///
175/// # Arguments
176///
177/// * `base_slot` - Starting consensus slot corresponding to the first mix in
178///   `delta`. This must be the same starting slot used by [`diff_randao`].
179/// * `base_buffer` - Destination RANDAO circular buffer. It is modified in
180///   place and must have the same capacity as the buffer used to generate
181///   `delta`.
182/// * `delta` - Archived [`RandaoDiff`] containing the mixes to replay.
183///
184/// # Correctness
185///
186/// This function is the application counterpart to [`diff_randao`].
187///
188/// For correct reconstruction, `base_buffer` must have the same capacity as
189/// the target buffer supplied to [`diff_randao`], and `base_slot` must be the
190/// same starting slot used when generating the delta.
191///
192/// The delta does not contain explicit buffer indices. The indices are derived
193/// from the starting epoch and the buffer capacity. Changing either value
194/// changes where the recorded mixes are written.
195///
196/// # Panics
197///
198/// Panics if `base_buffer` is empty because circular-buffer indexing requires
199/// a non-zero capacity.
200///
201/// # Example
202///
203/// ```
204/// use eth_state_diff::randao_mixes::{apply_randao, diff_randao};
205/// use eth_state_diff::types::ArchivedRandaoDiff;
206///
207/// let base_slot = 0;
208/// let target_slot = 64;
209///
210/// // With 32 slots per epoch, epochs 0, 1, and 2 are covered.
211/// let mut target_buffer = vec![[0u8; 32]; 4];
212/// target_buffer[0] = [1u8; 32];
213/// target_buffer[1] = [2u8; 32];
214/// target_buffer[2] = [3u8; 32];
215///
216/// let delta = diff_randao(base_slot, target_slot, &target_buffer);
217///
218/// let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&delta).unwrap();
219/// let archived = unsafe {
220///     rkyv::access_unchecked::<ArchivedRandaoDiff>(&bytes)
221/// };
222///
223/// let mut reconstructed = vec![[0u8; 32]; 4];
224/// apply_randao(base_slot, &mut reconstructed, archived);
225///
226/// assert_eq!(reconstructed, target_buffer);
227/// ```
228///
229/// # Complexity
230///
231/// If `E` is the number of mixes stored in `delta`:
232///
233/// - Time: O(E)
234/// - Additional space: O(1)
235pub fn apply_randao(base_slot: u64, base_buffer: &mut [[u8; 32]], delta: &ArchivedRandaoDiff) {
236    assert!(!base_buffer.is_empty(), "RANDAO buffer must not be empty");
237
238    let capacity = base_buffer.len() as u64;
239    let mut current_epoch = base_slot / SLOTS_PER_EPOCH;
240
241    for mix in delta.mixes.iter() {
242        let idx = (current_epoch % capacity) as usize;
243        *base_buffer
244            .get_mut(idx)
245            .expect("modulo arithmetic guarantees index is within bounds") = *mix;
246        current_epoch += 1;
247    }
248}