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