Skip to main content

eth_state_diff/
lib.rs

1//! High-performance delta encoding for Ethereum consensus state.
2//!
3//! `eth_state_diff` computes compact deltas between two beacon states and
4//! efficiently reconstructs the target state by applying those deltas.
5//!
6//! The crate is designed for consensus clients, archival storage, state
7//! synchronization, and historical state reconstruction.
8//!
9//! Individual state components use specialized encodings chosen for their
10//! respective data structures, including sparse patches, circular buffer
11//! updates, packed bit vectors, and FIFO queue deltas.
12//!
13//! Deltas are designed to serialize efficiently with `rkyv`, although the
14//! library itself remains serialization-agnostic.
15
16pub mod attestations;
17pub mod balances;
18pub mod eth1_data_votes;
19pub mod historical_log;
20pub mod inactivity_scores;
21pub mod participation;
22pub mod pending_queue;
23pub mod randao_mixes;
24pub mod recent_roots;
25pub mod slashings;
26pub mod sync_committee;
27pub mod types;
28pub mod validators;
29
30pub mod error;
31use error::Error;
32
33use rkyv::{Archive, Deserialize, Serialize};
34
35use crate::types::{
36    AttestationsDiff, BalancesDiff, Eth1DataVotesDiff, HistoricalLogDiff, InactivityDiff,
37    ParticipationDiff, QueueDiff, RandaoDiff, RootsDiff, SlashingsDiff, SyncCommitteeDiff,
38    ValidatorsDiff, HISTORICAL_ROOTS_SSZ_SIZE, HISTORICAL_SUMMARIES_SSZ_SIZE,
39};
40
41/// Ethereum consensus fork supported by this delta.
42///
43/// Explicit integer discriminants are assigned to allow strict invariant
44/// checking during delta creation.
45#[derive(Archive, Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
46#[repr(u8)]
47pub enum ForkName {
48    Phase0 = 0,
49    Altair = 1,
50    Bellatrix = 2,
51    Capella = 3,
52    Deneb = 4,
53    Electra = 5,
54    Fulu = 6,
55    Gloas = 7,
56    Heze = 8,
57}
58
59/// Complete delta describing the transition between two beacon states.
60///
61/// Fields introduced in later forks are wrapped in `Option<T>`.
62/// A delta for Phase0 will have `None` for all Altair/Capella/Electra fields.
63/// `rkyv` serializes `None` as a single zero byte, meaning fork-incompatible
64/// fields add effectively zero size to the final compressed delta.
65#[derive(Archive, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
66pub struct BeaconStateDelta {
67    pub fork: ForkName,
68    pub base_slot: u64,
69    pub scalar_header: Vec<u8>,
70
71    // --- Universal (Phase0+) ---
72    pub balances: BalancesDiff,
73    pub validators: ValidatorsDiff,
74    pub block_roots: RootsDiff,
75    pub state_roots: RootsDiff,
76    pub randao_mixes: RandaoDiff,
77    pub slashings: SlashingsDiff,
78    pub eth1_data_votes: Eth1DataVotesDiff,
79    pub historical_roots: Option<HistoricalLogDiff>,
80
81    // --- Phase0 Specific ---
82    /// `Some` for Phase0. `None` for Altair+.
83    pub previous_epoch_attestations: Option<AttestationsDiff>,
84    pub current_epoch_attestations: Option<AttestationsDiff>,
85
86    // --- Altair+ ---
87    /// `None` for Phase0. `Some` for Altair+.
88    pub previous_participation: Option<ParticipationDiff>,
89    pub current_participation: Option<ParticipationDiff>,
90    pub inactivity_scores: Option<InactivityDiff>,
91    pub current_sync_committee: Option<SyncCommitteeDiff>,
92    pub next_sync_committee: Option<SyncCommitteeDiff>,
93
94    // --- Capella+ ---
95    /// `None` for pre-Capella. `Some` for Capella+.
96    pub historical_summaries: Option<HistoricalLogDiff>,
97
98    // --- Electra+ ---
99    /// `None` for pre-Electra. `Some` for Electra+.
100    pub pending_deposits: Option<QueueDiff>,
101    pub pending_partial_withdrawals: Option<QueueDiff>,
102    pub pending_consolidations: Option<QueueDiff>,
103}
104
105const PENDING_DEPOSIT_SSZ_SIZE: usize = 192;
106const PARTIAL_WITHDRAWAL_SSZ_SIZE: usize = 24;
107const PENDING_CONSOLIDATION_SSZ_SIZE: usize = 16;
108
109/// Mutable view of a beacon state.
110///
111/// Implement this trait for your beacon-state representation to allow
112/// [`apply`] to reconstruct a target state from a [`BeaconStateDelta`].
113///
114/// The trait intentionally operates on primitive buffers and slices rather
115/// than client-specific types, allowing integration with any consensus client.
116/// Return `None` for fields that do not exist in the state's current fork.
117pub trait DiffTarget {
118    fn get_fork(&self) -> ForkName;
119    fn scalar_header_mut(&mut self) -> &mut Vec<u8>;
120
121    // Universal
122    fn balances_mut(&mut self) -> &mut Vec<u64>;
123    fn validators_mut(&mut self) -> &mut Vec<u8>;
124    fn block_roots_mut(&mut self) -> &mut [[u8; 32]];
125    fn state_roots_mut(&mut self) -> &mut [[u8; 32]];
126    fn randao_mixes_mut(&mut self) -> &mut [[u8; 32]];
127    fn slashings_mut(&mut self) -> &mut [u64];
128    fn eth1_data_votes_mut(&mut self) -> &mut Vec<u8>;
129    fn historical_roots_mut(&mut self) -> Option<&mut Vec<u8>>;
130
131    // Phase0 specific
132    fn previous_epoch_attestations_mut(&mut self) -> Option<&mut Vec<u8>>;
133    fn current_epoch_attestations_mut(&mut self) -> Option<&mut Vec<u8>>;
134
135    // Altair+
136    fn previous_participation_mut(&mut self) -> Option<&mut Vec<u8>>;
137    fn current_participation_mut(&mut self) -> Option<&mut Vec<u8>>;
138    fn inactivity_scores_mut(&mut self) -> Option<&mut Vec<u64>>;
139    fn current_sync_committee_mut(&mut self) -> Option<&mut Vec<u8>>;
140    fn next_sync_committee_mut(&mut self) -> Option<&mut Vec<u8>>;
141
142    // Capella+
143    fn historical_summaries_mut(&mut self) -> Option<&mut Vec<u8>>;
144
145    // Electra+
146    fn pending_deposits_mut(&mut self) -> Option<&mut Vec<u8>>;
147    fn pending_partial_withdrawals_mut(&mut self) -> Option<&mut Vec<u8>>;
148    fn pending_consolidations_mut(&mut self) -> Option<&mut Vec<u8>>;
149}
150
151/// Applies a previously created beacon-state delta.
152///
153/// The supplied [`DiffTarget`] is modified in place by applying each component
154/// delta to reconstruct the target state.
155///
156/// The state's fork must match the fork recorded in the delta.
157pub fn apply<M: DiffTarget>(mut state: M, delta: &ArchivedBeaconStateDelta) -> Result<M, Error> {
158    use rkyv::deserialize;
159
160    let delta_fork: ForkName = deserialize::<ForkName, rkyv::rancor::Error>(&delta.fork)
161        .map_err(|e| Error::MalformedDelta(format!("failed to deserialize fork: {e}")))?;
162
163    let state_fork = state.get_fork();
164    if state_fork != delta_fork {
165        return Err(Error::ForkMismatch {
166            state_fork,
167            delta_fork,
168        });
169    }
170
171    macro_rules! validate_removed_field {
172        ($field:ident, $removed_in:expr) => {
173            if delta.$field.is_some() && delta_fork >= $removed_in {
174                return Err(Error::InvalidFieldForFork {
175                    field: stringify!($field),
176                    fork: delta_fork,
177                });
178            }
179        };
180    }
181
182    // Validate fork-specific fields.
183    macro_rules! validate_field {
184        ($field:ident, $fork:expr) => {
185            if delta.$field.is_some() && delta_fork < $fork {
186                return Err(Error::InvalidFieldForFork {
187                    field: stringify!($field),
188                    fork: delta_fork,
189                });
190            }
191        };
192    }
193
194    // Introduced in Altair+
195    validate_field!(previous_participation, ForkName::Altair);
196    validate_field!(current_participation, ForkName::Altair);
197    validate_field!(inactivity_scores, ForkName::Altair);
198    validate_field!(current_sync_committee, ForkName::Altair);
199    validate_field!(next_sync_committee, ForkName::Altair);
200
201    // Introduced in Capella+
202    validate_field!(historical_summaries, ForkName::Capella);
203
204    // Introduced in Electra+
205    validate_field!(pending_deposits, ForkName::Electra);
206    validate_field!(pending_partial_withdrawals, ForkName::Electra);
207    validate_field!(pending_consolidations, ForkName::Electra);
208
209    // Removed in Altair
210    validate_removed_field!(previous_epoch_attestations, ForkName::Altair);
211    validate_removed_field!(current_epoch_attestations, ForkName::Altair);
212
213    // Removed in Capella
214    validate_removed_field!(historical_roots, ForkName::Capella);
215
216    let base_slot = delta.base_slot.to_native();
217
218    *state.scalar_header_mut() = delta.scalar_header.as_slice().to_vec();
219
220    // Universal
221    balances::apply_balances(state.balances_mut(), &delta.balances);
222    validators::apply_validators(state.validators_mut(), &delta.validators);
223    recent_roots::apply_roots(base_slot, state.block_roots_mut(), &delta.block_roots);
224    recent_roots::apply_roots(base_slot, state.state_roots_mut(), &delta.state_roots);
225    randao_mixes::apply_randao(base_slot, state.randao_mixes_mut(), &delta.randao_mixes);
226    slashings::apply_slashings(state.slashings_mut(), &delta.slashings);
227    eth1_data_votes::apply_eth1_votes(state.eth1_data_votes_mut(), &delta.eth1_data_votes);
228
229    if let (Some(s), Some(d)) = (
230        state.historical_roots_mut(),
231        delta.historical_roots.as_ref(),
232    ) {
233        historical_log::apply_historical_log(s, d);
234    }
235
236    if let (Some(s), Some(d)) = (
237        state.previous_epoch_attestations_mut(),
238        delta.previous_epoch_attestations.as_ref(),
239    ) {
240        attestations::apply_attestations(s, d);
241    }
242
243    if let (Some(s), Some(d)) = (
244        state.current_epoch_attestations_mut(),
245        delta.current_epoch_attestations.as_ref(),
246    ) {
247        attestations::apply_attestations(s, d);
248    }
249
250    if let (Some(s), Some(d)) = (
251        state.previous_participation_mut(),
252        delta.previous_participation.as_ref(),
253    ) {
254        participation::apply_participation(s, d);
255    }
256
257    if let (Some(s), Some(d)) = (
258        state.current_participation_mut(),
259        delta.current_participation.as_ref(),
260    ) {
261        participation::apply_participation(s, d);
262    }
263
264    if let (Some(s), Some(d)) = (
265        state.inactivity_scores_mut(),
266        delta.inactivity_scores.as_ref(),
267    ) {
268        inactivity_scores::apply_inactivity(s, d);
269    }
270
271    if let (Some(s), Some(d)) = (
272        state.current_sync_committee_mut(),
273        delta.current_sync_committee.as_ref(),
274    ) {
275        sync_committee::apply_sync_committee(s, d);
276    }
277
278    if let (Some(s), Some(d)) = (
279        state.next_sync_committee_mut(),
280        delta.next_sync_committee.as_ref(),
281    ) {
282        sync_committee::apply_sync_committee(s, d);
283    }
284
285    if let (Some(s), Some(d)) = (
286        state.historical_summaries_mut(),
287        delta.historical_summaries.as_ref(),
288    ) {
289        historical_log::apply_historical_log(s, d);
290    }
291
292    if let (Some(s), Some(d)) = (
293        state.pending_deposits_mut(),
294        delta.pending_deposits.as_ref(),
295    ) {
296        pending_queue::apply_queue(s, d, PENDING_DEPOSIT_SSZ_SIZE);
297    }
298
299    if let (Some(s), Some(d)) = (
300        state.pending_partial_withdrawals_mut(),
301        delta.pending_partial_withdrawals.as_ref(),
302    ) {
303        pending_queue::apply_queue(s, d, PARTIAL_WITHDRAWAL_SSZ_SIZE);
304    }
305
306    if let (Some(s), Some(d)) = (
307        state.pending_consolidations_mut(),
308        delta.pending_consolidations.as_ref(),
309    ) {
310        pending_queue::apply_queue(s, d, PENDING_CONSOLIDATION_SSZ_SIZE);
311    }
312
313    Ok(state)
314}
315
316/// Read-only view of two beacon states.
317///
318/// Implement this trait to allow [`create`] to compute a
319/// [`BeaconStateDelta`] between two states.
320///
321/// Each method exposes the state component required by the corresponding delta
322/// encoder without imposing any storage layout on the implementation.
323///
324/// Return `None` for fields that do not exist in the state's current fork.
325pub trait DiffSource {
326    fn fork(&self) -> ForkName;
327    fn slot(&self) -> (u64, u64);
328    fn capella_fork_slot(&self) -> u64; // Needed for historical_summaries math
329
330    /// Returns the serialized SSZ bytes for consensus state fields that are
331    /// not covered by specialized diffing algorithms.
332    ///
333    /// # Required SSZ Layout
334    ///
335    /// To ensure deterministic reconstruction across clients, the bytes MUST
336    /// be concatenated in the exact order defined by the consensus spec for
337    /// the target state's fork. The fields generally include:
338    ///
339    /// - `genesis_time` (8 bytes)
340    /// - `genesis_validators_root` (32 bytes)
341    /// - `slot` (8 bytes)
342    /// - `fork` (Fork struct, variable bytes)
343    /// - `latest_block_header` (BeaconBlockHeader struct)
344    /// - `eth1_data` (Eth1Data struct)
345    /// - `eth1_deposit_index` (8 bytes)
346    /// - `justification_bits` (BitVector)
347    /// - Checkpoints: `previous_justified`, `current_justified`, `finalized`
348    /// - `latest_execution_payload_header` (ExecutionPayloadHeader struct)
349    /// - Electra+ scalar fields: `next_withdrawal_index`, `next_withdrawal_validator_index`,
350    ///   `deposit_requests_start_index`, `deposit_balance_to_consume`, etc.
351    ///
352    /// **Note:** Fields that have dedicated diffing algorithms (e.g., `balances`,
353    /// `historical_summaries`, `pending_deposits`) MUST NOT be included in this blob.
354    fn scalar_header(&self) -> Vec<u8>;
355
356    // Universal
357    fn balances(&self) -> (&[u64], &[u64]);
358    fn validators(&self) -> (&[u8], &[u8]);
359    fn block_roots(&self) -> &[[u8; 32]];
360    fn state_roots(&self) -> &[[u8; 32]];
361    fn randao_mixes(&self) -> &[[u8; 32]];
362    fn slashings(&self) -> (&[u64], &[u64]);
363    fn eth1_data_votes(&self) -> (&[u8], &[u8]);
364    fn historical_roots(&self) -> Option<&[u8]>;
365
366    // Phase0
367    fn previous_epoch_attestations(&self) -> Option<(&[u8], &[u8])>;
368    fn current_epoch_attestations(&self) -> Option<(&[u8], &[u8])>;
369
370    // Altair+
371    fn previous_participation(&self) -> Option<(&[u8], &[u8])>;
372    fn current_participation(&self) -> Option<(&[u8], &[u8])>;
373    fn inactivity_scores(&self) -> Option<(&[u64], &[u64])>;
374    fn current_sync_committee(&self) -> Option<(&[u8], &[u8])>;
375    fn next_sync_committee(&self) -> Option<(&[u8], &[u8])>;
376
377    // Capella+
378    fn historical_summaries(&self) -> Option<&[u8]>;
379
380    // Electra+
381    fn pending_deposits(&self) -> Option<(&[u8], &[u8])>;
382    fn pending_partial_withdrawals(&self) -> Option<(&[u8], &[u8])>;
383    fn pending_consolidations(&self) -> Option<(&[u8], &[u8])>;
384}
385
386/// Creates a delta between two beacon states.
387///
388/// The supplied [`DiffSource`] provides access to the base and target state
389/// components required by each specialized encoder.
390///
391/// The returned [`BeaconStateDelta`] contains only the information necessary
392/// to reconstruct the target state from the base state.
393///
394/// # Complexity
395///
396/// Linear in the size of the state components being compared.
397pub fn create<R: DiffSource>(state: &R) -> BeaconStateDelta {
398    let (base_slot, target_slot) = state.slot();
399
400    let delta = BeaconStateDelta {
401        fork: state.fork(),
402        base_slot,
403        scalar_header: state.scalar_header(),
404
405        // Universal
406        balances: balances::diff_balances(state.balances().0, state.balances().1),
407        validators: validators::diff_validators(state.validators().0, state.validators().1),
408        block_roots: recent_roots::diff_roots(base_slot, target_slot, state.block_roots()),
409        state_roots: recent_roots::diff_roots(base_slot, target_slot, state.state_roots()),
410        randao_mixes: randao_mixes::diff_randao(base_slot, target_slot, state.randao_mixes()),
411        slashings: slashings::diff_slashings(
412            base_slot,
413            target_slot,
414            state.slashings().0,
415            state.slashings().1,
416        ),
417        eth1_data_votes: eth1_data_votes::diff_eth1_votes(
418            state.eth1_data_votes().0,
419            state.eth1_data_votes().1,
420        ),
421        historical_roots: state.historical_roots().map(|t| {
422            historical_log::diff_historical_log(
423                base_slot,
424                target_slot,
425                t,
426                HISTORICAL_ROOTS_SSZ_SIZE,
427                None,
428            )
429        }),
430
431        // Phase0
432        previous_epoch_attestations: state
433            .previous_epoch_attestations()
434            .map(|(b, t)| attestations::diff_attestations_replacement(b, t)),
435        current_epoch_attestations: state
436            .current_epoch_attestations()
437            .map(|(b, t)| attestations::diff_attestations_append(b, t)),
438
439        // Altair+
440        previous_participation: state
441            .previous_participation()
442            .map(|(b, t)| participation::diff_participation(b, t)),
443        current_participation: state
444            .current_participation()
445            .map(|(b, t)| participation::diff_participation(b, t)),
446        inactivity_scores: state
447            .inactivity_scores()
448            .map(|(b, t)| inactivity_scores::diff_inactivity(b, t)),
449        current_sync_committee: state
450            .current_sync_committee()
451            .map(|(b, t)| sync_committee::diff_sync_committee(b, t)),
452        next_sync_committee: state
453            .next_sync_committee()
454            .map(|(b, t)| sync_committee::diff_sync_committee(b, t)),
455
456        // Capella+
457        historical_summaries: state.historical_summaries().map(|t| {
458            historical_log::diff_historical_log(
459                base_slot,
460                target_slot,
461                t,
462                HISTORICAL_SUMMARIES_SSZ_SIZE,
463                Some(state.capella_fork_slot()),
464            )
465        }),
466
467        // Electra+
468        pending_deposits: state
469            .pending_deposits()
470            .map(|(b, t)| pending_queue::diff_queue(b, t, PENDING_DEPOSIT_SSZ_SIZE)),
471        pending_partial_withdrawals: state
472            .pending_partial_withdrawals()
473            .map(|(b, t)| pending_queue::diff_queue(b, t, PARTIAL_WITHDRAWAL_SSZ_SIZE)),
474        pending_consolidations: state
475            .pending_consolidations()
476            .map(|(b, t)| pending_queue::diff_queue(b, t, PENDING_CONSOLIDATION_SSZ_SIZE)),
477    };
478
479    debug_assert_eq!(
480        delta.previous_participation.is_some(),
481        delta.fork >= ForkName::Altair,
482        "DiffSource bug: previous_participation must exist iff fork >= Altair (got {:?})",
483        delta.fork
484    );
485
486    debug_assert_eq!(
487        delta.current_participation.is_some(),
488        delta.fork >= ForkName::Altair,
489        "DiffSource bug: current_participation must exist iff fork >= Altair (got {:?})",
490        delta.fork
491    );
492
493    debug_assert_eq!(
494        delta.inactivity_scores.is_some(),
495        delta.fork >= ForkName::Altair,
496        "DiffSource bug: inactivity_scores must exist iff fork >= Altair (got {:?})",
497        delta.fork
498    );
499
500    debug_assert_eq!(
501        delta.current_sync_committee.is_some(),
502        delta.fork >= ForkName::Altair,
503        "DiffSource bug: current_sync_committee must exist iff fork >= Altair (got {:?})",
504        delta.fork
505    );
506
507    debug_assert_eq!(
508        delta.next_sync_committee.is_some(),
509        delta.fork >= ForkName::Altair,
510        "DiffSource bug: next_sync_committee must exist iff fork >= Altair (got {:?})",
511        delta.fork
512    );
513
514    debug_assert_eq!(
515        delta.historical_summaries.is_some(),
516        delta.fork >= ForkName::Capella,
517        "DiffSource bug: historical_summaries must exist iff fork >= Capella (got {:?})",
518        delta.fork
519    );
520
521    debug_assert_eq!(
522        delta.pending_deposits.is_some(),
523        delta.fork >= ForkName::Electra,
524        "DiffSource bug: pending_deposits must exist iff fork >= Electra (got {:?})",
525        delta.fork
526    );
527
528    debug_assert_eq!(
529        delta.pending_partial_withdrawals.is_some(),
530        delta.fork >= ForkName::Electra,
531        "DiffSource bug: pending_partial_withdrawals must exist iff fork >= Electra (got {:?})",
532        delta.fork
533    );
534
535    debug_assert_eq!(
536        delta.pending_consolidations.is_some(),
537        delta.fork >= ForkName::Electra,
538        "DiffSource bug: pending_consolidations must exist iff fork >= Electra (got {:?})",
539        delta.fork
540    );
541
542    delta
543}