Skip to main content

eth_state_diff/
lib.rs

1//! # `eth-state-diff`
2//!
3//! High-performance delta encoding and reconstruction for Ethereum consensus
4//! state.
5//!
6//! This crate computes compact deltas between two beacon states and applies
7//! those deltas to reconstruct the target state without requiring the entire
8//! state to be serialized or rewritten.
9//!
10//! The delta format is designed around the update semantics of individual
11//! Ethereum consensus-state fields. Depending on the field, the crate uses
12//! specialized representations such as sparse updates, append-only deltas,
13//! circular-buffer writes, FIFO queue deltas, and full replacements.
14//!
15//! ## Overview
16//!
17//! A state transition is represented by [`BeaconStateDelta`]. The transition
18//! has two stages:
19//!
20//! 1. [`create`] compares the base and target state through [`DiffSource`] and
21//!    constructs a [`BeaconStateDelta`].
22//! 2. [`apply`] applies the delta to a state through [`DiffTarget`], producing
23//!    the target state.
24//!
25//! ```text
26//!
27//!        base state ───────┐
28//!                           │
29//!                      [`create`]
30//!                           │
31//!                           ▼
32//!                  [`BeaconStateDelta`]
33//!                           │
34//!                      serialize
35//!                           │
36//!                           ▼
37//!                    transport / storage
38//!                           │
39//!                       deserialize
40//!                           │
41//!                           ▼
42//!                  [`ArchivedBeaconStateDelta`]
43//!                           │
44//!                      [`apply`]
45//!                           │
46//!                           ▼
47//!                       target state
48//!
49//! ```
50//!
51//! The crate does not impose a particular beacon-state storage layout.
52//! [`DiffSource`] and [`DiffTarget`] provide the integration boundary between
53//! the delta algorithms and a consensus client's state representation.
54//!
55//! ## Delta representations
56//!
57//! Each state component uses an encoding appropriate to its update pattern:
58//!
59//! - **Balances** use packed 2-bit tags and compact difference encoding.
60//! - **Validators** use field-level patches rather than rewriting complete
61//!   validator records.
62//! - **Recent roots** record only roots written to circular buffers during the
63//!   diff window.
64//! - **RANDAO mixes** record the mixes written as epochs advance.
65//! - **Slashings** use sparse ring-buffer updates.
66//! - **Eth1 data votes** use append/reset semantics.
67//! - **Historical roots and summaries** use protocol-defined append intervals.
68//! - **Attestations** use unchanged, append, or replacement representations.
69//! - **Participation flags** use packed sparse updates and an all-zero fast path.
70//! - **Inactivity scores** use sparse updates and an all-zero representation.
71//! - **Sync committees** use unchanged/full-replacement encoding.
72//! - **Pending deposits, withdrawals, and consolidations** use a validated FIFO
73//!   representation with full-replacement fallback.
74//!
75//! This specialization allows the delta to represent the *state transition*
76//! rather than treating the serialized beacon state as one opaque byte array.
77//!
78//! ## Serialization
79//!
80//! Delta structures derive [`rkyv::Archive`], [`rkyv::Serialize`], and
81//! [`rkyv::Deserialize`] and are therefore suitable for zero-copy or archived
82//! representations where appropriate.
83//!
84//! The delta algorithms themselves operate on native Rust values and serialized
85//! SSZ byte sequences where field-level SSZ representation is required.
86//! Serialization is deliberately kept separate from the diff algorithms.
87//!
88//! ## Fork handling
89//!
90//! [`ForkName`] identifies the consensus fork associated with a delta.
91//!
92//! Fork-specific fields are represented as `Option<T>` inside
93//! [`BeaconStateDelta`]. A field is populated only when it exists for the
94//! corresponding fork. [`apply`] validates these invariants before modifying
95//! the destination state and rejects fork mismatches or fields that are
96//! invalid for the delta's fork.
97//!
98//! ## Integration
99//!
100//! Consensus clients integrate with this crate by implementing two traits:
101//!
102//! - [`DiffSource`] exposes the base and target state components required to
103//!   create a delta.
104//! - [`DiffTarget`] exposes mutable access to the state components required to
105//!   apply a delta.
106//!
107//! Collection-specific integration can additionally use [`ListMutTarget`] for
108//! list-like collections and [`ValidatorMutTarget`] for validator registries.
109//!
110//! The crate intentionally does not require a particular consensus-client
111//! implementation, allocation strategy, or state storage backend.
112
113pub mod attestations;
114pub mod balances;
115pub mod eth1_data_votes;
116pub mod historical_log;
117pub mod inactivity_scores;
118pub mod participation;
119pub mod pending_queue;
120pub mod randao_mixes;
121pub mod recent_roots;
122pub mod slashings;
123pub mod sync_committee;
124pub mod types;
125pub mod validators;
126
127pub mod error;
128use error::Error;
129
130use rkyv::{Archive, Deserialize, Serialize};
131
132use crate::{
133    types::{
134        AttestationsDiff, BalancesDiff, Eth1DataVotesDiff, HistoricalLogDiff, InactivityDiff,
135        ParticipationDiff, QueueDiff, RandaoDiff, RootsDiff, SlashingsDiff, SyncCommitteeDiff,
136        ValidatorsDiff, HISTORICAL_ROOTS_SSZ_SIZE, HISTORICAL_SUMMARIES_SSZ_SIZE,
137    },
138    validators::{ValidatorMutTarget, ValidatorSnapshot},
139};
140
141/// Identifies the Ethereum consensus fork associated with a beacon state or
142/// state delta.
143///
144/// The discriminants are explicit and stable within the delta representation.
145/// They are used when validating that a delta is applied to a state belonging
146/// to the same fork.
147///
148/// Fork ordering follows the protocol progression, so the variants can also be
149/// compared to determine whether a fork-specific field has been introduced.
150///
151/// # Examples
152///
153/// ```
154/// use eth_state_diff::ForkName;
155///
156/// assert!(ForkName::Electra > ForkName::Capella);
157/// assert!(ForkName::Altair >= ForkName::Phase0);
158/// ```
159#[derive(Archive, Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
160#[repr(u8)]
161pub enum ForkName {
162    /// Phase 0 beacon state.
163    Phase0 = 0,
164    /// Altair beacon state.
165    Altair = 1,
166    /// Bellatrix beacon state.
167    Bellatrix = 2,
168    /// Capella beacon state.
169    Capella = 3,
170    /// Deneb beacon state.
171    Deneb = 4,
172    /// Electra beacon state.
173    Electra = 5,
174    /// Fulu beacon state.
175    Fulu = 6,
176    /// Gloas beacon state.
177    Gloas = 7,
178    /// Heze beacon state.
179    Heze = 8,
180}
181
182/// Read-only state interface used by [`create`].
183///
184/// Implement this trait to expose the base and target beacon states to the
185/// specialized delta algorithms without requiring a particular state-storage
186/// implementation.
187///
188/// Each accessor returns either:
189///
190/// - the state component itself when it exists for the current fork, or
191/// - `None` for fork-specific fields that do not exist.
192///
193/// The first value returned by a pair of iterators or slices represents the
194/// base state and the second represents the target state.
195///
196/// # Base and target state
197///
198/// The implementation must ensure that all returned components refer to the
199/// same pair of states. Mixing components from different state transitions
200/// produces a delta that cannot correctly reconstruct the target.
201///
202/// # Slots
203///
204/// [`slot`](Self::slot) returns `(base_slot, target_slot)`. These slots are used
205/// to reconstruct slot- and epoch-indexed circular buffers.
206///
207/// # Scalar header
208///
209/// [`scalar_header`](Self::scalar_header) must contain exactly the state bytes
210/// that are not handled by the specialized delta encoders.
211///
212/// Fields already represented by dedicated delta fields must not also appear
213/// in the scalar header.
214///
215/// # Fork-specific fields
216///
217/// Accessors for fields introduced by later forks must return `None` when the
218/// source state belongs to an earlier fork.
219pub trait DiffSource {
220    fn fork(&self) -> ForkName;
221    fn slot(&self) -> (u64, u64);
222    fn capella_fork_slot(&self) -> u64; // Needed for historical_summaries math
223
224    /// Returns the serialized SSZ bytes for consensus state fields that are
225    /// not covered by specialized diffing algorithms.
226    ///
227    /// # Required SSZ Layout
228    ///
229    /// To ensure deterministic reconstruction across clients, the bytes MUST
230    /// be concatenated in the exact order defined by the consensus spec for
231    /// the target state's fork. The fields generally include:
232    ///
233    /// - `genesis_time` (8 bytes)
234    /// - `genesis_validators_root` (32 bytes)
235    /// - `slot` (8 bytes)
236    /// - `fork` (Fork struct, variable bytes)
237    /// - `latest_block_header` (BeaconBlockHeader struct)
238    /// - `eth1_data` (Eth1Data struct)
239    /// - `eth1_deposit_index` (8 bytes)
240    /// - `justification_bits` (BitVector)
241    /// - Checkpoints: `previous_justified`, `current_justified`, `finalized`
242    /// - `latest_execution_payload_header` (ExecutionPayloadHeader struct)
243    /// - Electra+ scalar fields: `next_withdrawal_index`, `next_withdrawal_validator_index`,
244    ///   `deposit_requests_start_index`, `deposit_balance_to_consume`, etc.
245    ///
246    /// **Note:** Fields that have dedicated diffing algorithms (e.g., `balances`,
247    /// `historical_summaries`, `pending_deposits`) MUST NOT be included in this blob.
248    fn scalar_header(&self) -> Vec<u8>;
249
250    // Universal
251    fn balances(
252        &self,
253    ) -> (
254        impl ExactSizeIterator<Item = u64>,
255        impl ExactSizeIterator<Item = u64>,
256    );
257    fn validators(
258        &self,
259    ) -> (
260        impl ExactSizeIterator<Item = impl ValidatorSnapshot>,
261        impl ExactSizeIterator<Item = impl ValidatorSnapshot>,
262    );
263    fn block_roots(&self) -> &[[u8; 32]];
264    fn state_roots(&self) -> &[[u8; 32]];
265    fn randao_mixes(&self) -> &[[u8; 32]];
266    fn slashings(&self) -> (&[u64], &[u64]);
267    fn eth1_data_votes(&self) -> (&[u8], &[u8]);
268    fn historical_roots(&self) -> Option<&[u8]>;
269
270    // Phase0
271    fn previous_epoch_attestations(&self) -> Option<(&[u8], &[u8])>;
272    fn current_epoch_attestations(&self) -> Option<(&[u8], &[u8])>;
273
274    // Altair+
275    fn previous_participation(
276        &self,
277    ) -> Option<(
278        impl ExactSizeIterator<Item = u8>,
279        impl ExactSizeIterator<Item = u8>,
280    )>;
281    fn current_participation(
282        &self,
283    ) -> Option<(
284        impl ExactSizeIterator<Item = u8>,
285        impl ExactSizeIterator<Item = u8>,
286    )>;
287    fn inactivity_scores(&self) -> Option<(&[u64], &[u64])>;
288    fn current_sync_committee(&self) -> Option<(&[u8], &[u8])>;
289    fn next_sync_committee(&self) -> Option<(&[u8], &[u8])>;
290
291    // Capella+
292    fn historical_summaries(&self) -> Option<&[u8]>;
293
294    // Electra+
295    fn pending_deposits(&self) -> Option<(&[u8], &[u8])>;
296    fn pending_partial_withdrawals(&self) -> Option<(&[u8], &[u8])>;
297    fn pending_consolidations(&self) -> Option<(&[u8], &[u8])>;
298}
299
300/// Mutable state interface used by [`apply`].
301///
302/// Implement this trait for a beacon-state representation to allow an archived
303/// [`BeaconStateDelta`] to be applied directly to the client's native state.
304///
305/// The trait deliberately exposes only the mutable views required by the delta
306/// algorithms. It does not require the underlying state to use the same memory
307/// layout as Ethereum's SSZ representation.
308///
309/// # Fork requirements
310///
311/// [`get_fork`](Self::get_fork) must return the fork of the state being
312/// modified. [`apply`] rejects the operation if it differs from the fork stored
313/// in the delta.
314///
315/// Fork-specific accessors should return `None` when the corresponding field
316/// does not exist in the state representation.
317///
318/// # Mutation
319///
320/// Implementations must return references to the actual state storage.
321/// `apply` mutates these collections in place.
322///
323/// If an implementation returns a view backed by temporary storage rather than
324/// the actual state, the reconstructed values will not be persisted to the
325/// beacon state.
326pub trait DiffTarget {
327    /// Returns the fork of the state being modified.
328    ///
329    /// This value must match [`BeaconStateDelta::fork`] for [`apply`] to
330    /// proceed.
331    fn get_fork(&self) -> ForkName;
332
333    /// Returns mutable storage for the scalar state header.
334    ///
335    /// The returned buffer is replaced with the scalar bytes stored in the
336    /// delta.
337    fn scalar_header_mut(&mut self) -> &mut Vec<u8>;
338
339    // Universal
340    fn balances_mut(&mut self) -> &mut impl ListMutTarget<u64>;
341    fn validators_mut(&mut self) -> &mut impl ValidatorMutTarget;
342    fn block_roots_mut(&mut self) -> &mut [[u8; 32]];
343    fn state_roots_mut(&mut self) -> &mut [[u8; 32]];
344    fn randao_mixes_mut(&mut self) -> &mut [[u8; 32]];
345    fn slashings_mut(&mut self) -> &mut [u64];
346    fn eth1_data_votes_mut(&mut self) -> &mut Vec<u8>;
347    fn historical_roots_mut(&mut self) -> Option<&mut Vec<u8>>;
348
349    // Phase0 specific
350
351    /// Returns mutable access to `previous_epoch_attestations`.
352    ///
353    /// Returns `None` for forks where this field does not exist.
354    fn previous_epoch_attestations_mut(&mut self) -> Option<&mut Vec<u8>>;
355
356    /// Returns mutable access to `previous_epoch_attestations`.
357    ///
358    /// Returns `None` for forks where this field does not exist.
359    fn current_epoch_attestations_mut(&mut self) -> Option<&mut Vec<u8>>;
360
361    // Altair+
362    fn previous_participation_mut(&mut self) -> Option<&mut impl ListMutTarget<u8>>;
363    fn current_participation_mut(&mut self) -> Option<&mut impl ListMutTarget<u8>>;
364    fn inactivity_scores_mut(&mut self) -> Option<&mut Vec<u64>>;
365    fn current_sync_committee_mut(&mut self) -> Option<&mut Vec<u8>>;
366    fn next_sync_committee_mut(&mut self) -> Option<&mut Vec<u8>>;
367
368    // Capella+
369    fn historical_summaries_mut(&mut self) -> Option<&mut Vec<u8>>;
370
371    // Electra+
372    fn pending_deposits_mut(&mut self) -> Option<&mut Vec<u8>>;
373    fn pending_partial_withdrawals_mut(&mut self) -> Option<&mut Vec<u8>>;
374    fn pending_consolidations_mut(&mut self) -> Option<&mut Vec<u8>>;
375}
376
377/// Complete compact representation of the transition between two beacon
378/// states.
379///
380/// A [`BeaconStateDelta`] contains one specialized delta for each state
381/// component handled by this crate. Applying the delta to the corresponding
382/// base state reconstructs the target state.
383///
384/// The delta records the fork and base slot required to interpret fork-specific
385/// fields and circular-buffer updates.
386///
387/// # Fork-specific fields
388///
389/// Fields introduced by later consensus forks are represented as `Option<T>`:
390///
391/// - Phase 0 fields are present only on Phase 0.
392/// - Altair fields are present on Altair and later forks.
393/// - Capella fields are present on Capella and later forks.
394/// - Electra fields are present on Electra and later forks.
395///
396/// [`apply`] validates these invariants before modifying the destination state.
397///
398/// # Serialization
399///
400/// This type derives [`rkyv::Archive`], [`rkyv::Serialize`], and
401/// [`rkyv::Deserialize`] and can therefore be archived for storage or
402/// transmission.
403///
404/// The delta algorithms themselves do not require the delta to be serialized.
405///
406/// # Lifecycle
407///
408/// A typical workflow is:
409///
410/// ```text
411/// DiffSource
412///     │
413///     ▼
414///  create()
415///     │
416///     ▼
417/// BeaconStateDelta
418///     │
419///     ├── serialize / store / transmit
420///     │
421///     ▼
422/// ArchivedBeaconStateDelta
423///     │
424///     ▼
425///   apply()
426///     │
427///     ▼
428/// DiffTarget
429/// ```
430///
431/// # Correctness
432///
433/// The delta is intended to reconstruct the target state represented by the
434/// [`DiffSource`] used during creation. The caller is responsible for ensuring
435/// that the destination state corresponds to the base state from which the
436/// delta was created.
437#[derive(Archive, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
438pub struct BeaconStateDelta {
439    pub fork: ForkName,
440    pub base_slot: u64,
441    pub scalar_header: Vec<u8>,
442
443    // --- Universal (Phase0+) ---
444    pub balances: BalancesDiff,
445    pub validators: ValidatorsDiff,
446    pub block_roots: RootsDiff,
447    pub state_roots: RootsDiff,
448    pub randao_mixes: RandaoDiff,
449    pub slashings: SlashingsDiff,
450    pub eth1_data_votes: Eth1DataVotesDiff,
451    pub historical_roots: Option<HistoricalLogDiff>,
452
453    // --- Phase0 Specific ---
454    /// `Some` for Phase0. `None` for Altair+.
455    pub previous_epoch_attestations: Option<AttestationsDiff>,
456    pub current_epoch_attestations: Option<AttestationsDiff>,
457
458    // --- Altair+ ---
459    /// `None` for Phase0. `Some` for Altair+.
460    pub previous_participation: Option<ParticipationDiff>,
461    pub current_participation: Option<ParticipationDiff>,
462    pub inactivity_scores: Option<InactivityDiff>,
463    pub current_sync_committee: Option<SyncCommitteeDiff>,
464    pub next_sync_committee: Option<SyncCommitteeDiff>,
465
466    // --- Capella+ ---
467    /// `None` for pre-Capella. `Some` for Capella+.
468    pub historical_summaries: Option<HistoricalLogDiff>,
469
470    // --- Electra+ ---
471    /// `None` for pre-Electra. `Some` for Electra+.
472    pub pending_deposits: Option<QueueDiff>,
473    pub pending_partial_withdrawals: Option<QueueDiff>,
474    pub pending_consolidations: Option<QueueDiff>,
475}
476
477/// Creates a [`BeaconStateDelta`] describing the transition between two
478/// beacon states.
479///
480/// [`DiffSource`] supplies both the base and target components. Each component
481/// is passed to the specialized encoder best suited to its update semantics.
482///
483/// The resulting delta contains enough information to reconstruct the target
484/// state when applied to the corresponding base state with [`apply`].
485///
486/// # Fork handling
487///
488/// The fork returned by [`DiffSource::fork`] is stored in the delta. Fork-specific
489/// components are populated according to that fork.
490///
491/// The function performs debug-only invariant checks to ensure that
492/// fork-specific fields are present exactly when expected.
493///
494/// # Complexity
495///
496/// O(n) in the amount of state data examined by the individual diff algorithms.
497/// No single representation is imposed on all state components; the actual
498/// amount of work depends on the component sizes and their specialized encoding
499/// strategies.
500///
501/// # Examples
502///
503/// A consensus client typically implements [`DiffSource`] for a wrapper around
504/// its state representation and then calls:
505///
506/// ```text
507/// let delta = eth_state_diff::create(&source);
508/// ```
509///
510/// The resulting [`BeaconStateDelta`] can then be serialized or archived for
511/// later application.
512pub fn create<R: DiffSource>(state: &R) -> BeaconStateDelta {
513    let (base_slot, target_slot) = state.slot();
514
515    let delta = BeaconStateDelta {
516        fork: state.fork(),
517        base_slot,
518        scalar_header: state.scalar_header(),
519
520        // Universal
521        balances: balances::diff_balances_iter(state.balances().0, state.balances().1),
522        validators: validators::diff_validators_iter(state.validators().0, state.validators().1),
523        block_roots: recent_roots::diff_roots(base_slot, target_slot, state.block_roots()),
524        state_roots: recent_roots::diff_roots(base_slot, target_slot, state.state_roots()),
525        randao_mixes: randao_mixes::diff_randao(base_slot, target_slot, state.randao_mixes()),
526        slashings: slashings::diff_slashings(
527            base_slot,
528            target_slot,
529            state.slashings().0,
530            state.slashings().1,
531        ),
532        eth1_data_votes: eth1_data_votes::diff_eth1_votes(
533            state.eth1_data_votes().0,
534            state.eth1_data_votes().1,
535        ),
536        historical_roots: state.historical_roots().map(|t| {
537            historical_log::diff_historical_log(
538                base_slot,
539                target_slot,
540                t,
541                HISTORICAL_ROOTS_SSZ_SIZE,
542                None,
543            )
544        }),
545
546        // Phase0
547        previous_epoch_attestations: state
548            .previous_epoch_attestations()
549            .map(|(b, t)| attestations::diff_attestations(b, t)),
550        current_epoch_attestations: state
551            .current_epoch_attestations()
552            .map(|(b, t)| attestations::diff_attestations(b, t)),
553
554        // Altair+
555        previous_participation: state
556            .previous_participation()
557            .map(|(b, t)| participation::diff_participation_iter(b, t)),
558        current_participation: state
559            .current_participation()
560            .map(|(b, t)| participation::diff_participation_iter(b, t)),
561        inactivity_scores: state
562            .inactivity_scores()
563            .map(|(b, t)| inactivity_scores::diff_inactivity(b, t)),
564        current_sync_committee: state
565            .current_sync_committee()
566            .map(|(b, t)| sync_committee::diff_sync_committee(b, t)),
567        next_sync_committee: state
568            .next_sync_committee()
569            .map(|(b, t)| sync_committee::diff_sync_committee(b, t)),
570
571        // Capella+
572        historical_summaries: state.historical_summaries().map(|t| {
573            historical_log::diff_historical_log(
574                base_slot,
575                target_slot,
576                t,
577                HISTORICAL_SUMMARIES_SSZ_SIZE,
578                Some(state.capella_fork_slot()),
579            )
580        }),
581
582        // Electra+
583        pending_deposits: state
584            .pending_deposits()
585            .map(|(b, t)| pending_queue::diff_queue(b, t, PENDING_DEPOSIT_SSZ_SIZE)),
586        pending_partial_withdrawals: state
587            .pending_partial_withdrawals()
588            .map(|(b, t)| pending_queue::diff_queue(b, t, PARTIAL_WITHDRAWAL_SSZ_SIZE)),
589        pending_consolidations: state
590            .pending_consolidations()
591            .map(|(b, t)| pending_queue::diff_queue(b, t, PENDING_CONSOLIDATION_SSZ_SIZE)),
592    };
593
594    debug_assert_eq!(
595        delta.previous_participation.is_some(),
596        delta.fork >= ForkName::Altair,
597        "DiffSource bug: previous_participation must exist iff fork >= Altair (got {:?})",
598        delta.fork
599    );
600
601    debug_assert_eq!(
602        delta.current_participation.is_some(),
603        delta.fork >= ForkName::Altair,
604        "DiffSource bug: current_participation must exist iff fork >= Altair (got {:?})",
605        delta.fork
606    );
607
608    debug_assert_eq!(
609        delta.inactivity_scores.is_some(),
610        delta.fork >= ForkName::Altair,
611        "DiffSource bug: inactivity_scores must exist iff fork >= Altair (got {:?})",
612        delta.fork
613    );
614
615    debug_assert_eq!(
616        delta.current_sync_committee.is_some(),
617        delta.fork >= ForkName::Altair,
618        "DiffSource bug: current_sync_committee must exist iff fork >= Altair (got {:?})",
619        delta.fork
620    );
621
622    debug_assert_eq!(
623        delta.next_sync_committee.is_some(),
624        delta.fork >= ForkName::Altair,
625        "DiffSource bug: next_sync_committee must exist iff fork >= Altair (got {:?})",
626        delta.fork
627    );
628
629    debug_assert_eq!(
630        delta.historical_summaries.is_some(),
631        delta.fork >= ForkName::Capella,
632        "DiffSource bug: historical_summaries must exist iff fork >= Capella (got {:?})",
633        delta.fork
634    );
635
636    debug_assert_eq!(
637        delta.pending_deposits.is_some(),
638        delta.fork >= ForkName::Electra,
639        "DiffSource bug: pending_deposits must exist iff fork >= Electra (got {:?})",
640        delta.fork
641    );
642
643    debug_assert_eq!(
644        delta.pending_partial_withdrawals.is_some(),
645        delta.fork >= ForkName::Electra,
646        "DiffSource bug: pending_partial_withdrawals must exist iff fork >= Electra (got {:?})",
647        delta.fork
648    );
649
650    debug_assert_eq!(
651        delta.pending_consolidations.is_some(),
652        delta.fork >= ForkName::Electra,
653        "DiffSource bug: pending_consolidations must exist iff fork >= Electra (got {:?})",
654        delta.fork
655    );
656
657    delta
658}
659
660/// Applies an archived [`BeaconStateDelta`] to a mutable beacon state.
661///
662/// The destination state is modified in place and returned after all delta
663/// components have been applied.
664///
665/// Before mutation begins, the function validates that:
666///
667/// - the destination state's fork matches the delta's fork;
668/// - fork-specific fields are valid for that fork; and
669/// - the fork value can be successfully decoded from the archived delta.
670///
671/// # Errors
672///
673/// Returns [`Error::ForkMismatch`] when the delta and destination state belong
674/// to different forks.
675///
676/// Returns [`Error::InvalidFieldForFork`] when a fork-specific field is present
677/// in a delta where that field is not valid.
678///
679/// Returns [`Error::MalformedDelta`] when the archived fork cannot be decoded.
680///
681/// # Mutation
682///
683/// Fork and field validation occurs before state components are modified.
684/// Component application itself operates in place.
685///
686/// The destination state must correspond to the base state from which the
687/// delta was created. Applying a valid delta to an unrelated state is not
688/// expected to reconstruct the original target state.
689///
690/// # Complexity
691///
692/// Linear in the amount of data represented by the delta, with the exact cost
693/// determined by the individual component encodings.
694pub fn apply<M: DiffTarget>(mut state: M, delta: &ArchivedBeaconStateDelta) -> Result<M, Error> {
695    use rkyv::deserialize;
696
697    let delta_fork: ForkName = deserialize::<ForkName, rkyv::rancor::Error>(&delta.fork)
698        .map_err(|e| Error::MalformedDelta(format!("failed to deserialize fork: {e}")))?;
699
700    let state_fork = state.get_fork();
701    if state_fork != delta_fork {
702        return Err(Error::ForkMismatch {
703            state_fork,
704            delta_fork,
705        });
706    }
707
708    macro_rules! validate_removed_field {
709        ($field:ident, $removed_in:expr) => {
710            if delta.$field.is_some() && delta_fork >= $removed_in {
711                return Err(Error::InvalidFieldForFork {
712                    field: stringify!($field),
713                    fork: delta_fork,
714                });
715            }
716        };
717    }
718
719    macro_rules! validate_field {
720        ($field:ident, $fork:expr) => {
721            if delta.$field.is_some() && delta_fork < $fork {
722                return Err(Error::InvalidFieldForFork {
723                    field: stringify!($field),
724                    fork: delta_fork,
725                });
726            }
727        };
728    }
729
730    validate_field!(previous_participation, ForkName::Altair);
731    validate_field!(current_participation, ForkName::Altair);
732    validate_field!(inactivity_scores, ForkName::Altair);
733    validate_field!(current_sync_committee, ForkName::Altair);
734    validate_field!(next_sync_committee, ForkName::Altair);
735
736    validate_field!(historical_summaries, ForkName::Capella);
737
738    validate_field!(pending_deposits, ForkName::Electra);
739    validate_field!(pending_partial_withdrawals, ForkName::Electra);
740    validate_field!(pending_consolidations, ForkName::Electra);
741
742    validate_removed_field!(previous_epoch_attestations, ForkName::Altair);
743    validate_removed_field!(current_epoch_attestations, ForkName::Altair);
744
745    validate_removed_field!(historical_roots, ForkName::Capella);
746
747    let base_slot = delta.base_slot.to_native();
748
749    *state.scalar_header_mut() = delta.scalar_header.as_slice().to_vec();
750
751    // Universal
752    balances::apply_balances_iter(state.balances_mut(), &delta.balances)?;
753    validators::apply_validators_iter(state.validators_mut(), &delta.validators)?;
754    recent_roots::apply_roots(base_slot, state.block_roots_mut(), &delta.block_roots);
755    recent_roots::apply_roots(base_slot, state.state_roots_mut(), &delta.state_roots);
756    randao_mixes::apply_randao(base_slot, state.randao_mixes_mut(), &delta.randao_mixes);
757    slashings::apply_slashings(state.slashings_mut(), &delta.slashings);
758    eth1_data_votes::apply_eth1_votes(state.eth1_data_votes_mut(), &delta.eth1_data_votes);
759
760    if let (Some(s), Some(d)) = (
761        state.historical_roots_mut(),
762        delta.historical_roots.as_ref(),
763    ) {
764        historical_log::apply_historical_log(s, d);
765    }
766
767    if let (Some(s), Some(d)) = (
768        state.previous_epoch_attestations_mut(),
769        delta.previous_epoch_attestations.as_ref(),
770    ) {
771        attestations::apply_attestations(s, d);
772    }
773
774    if let (Some(s), Some(d)) = (
775        state.current_epoch_attestations_mut(),
776        delta.current_epoch_attestations.as_ref(),
777    ) {
778        attestations::apply_attestations(s, d);
779    }
780
781    if let (Some(s), Some(d)) = (
782        state.previous_participation_mut(),
783        delta.previous_participation.as_ref(),
784    ) {
785        participation::apply_participation_iter(s, d)?;
786    }
787
788    if let (Some(s), Some(d)) = (
789        state.current_participation_mut(),
790        delta.current_participation.as_ref(),
791    ) {
792        participation::apply_participation_iter(s, d)?;
793    }
794
795    if let (Some(s), Some(d)) = (
796        state.inactivity_scores_mut(),
797        delta.inactivity_scores.as_ref(),
798    ) {
799        inactivity_scores::apply_inactivity(s, d)?;
800    }
801
802    if let (Some(s), Some(d)) = (
803        state.current_sync_committee_mut(),
804        delta.current_sync_committee.as_ref(),
805    ) {
806        sync_committee::apply_sync_committee(s, d);
807    }
808
809    if let (Some(s), Some(d)) = (
810        state.next_sync_committee_mut(),
811        delta.next_sync_committee.as_ref(),
812    ) {
813        sync_committee::apply_sync_committee(s, d);
814    }
815
816    if let (Some(s), Some(d)) = (
817        state.historical_summaries_mut(),
818        delta.historical_summaries.as_ref(),
819    ) {
820        historical_log::apply_historical_log(s, d);
821    }
822
823    if let (Some(s), Some(d)) = (
824        state.pending_deposits_mut(),
825        delta.pending_deposits.as_ref(),
826    ) {
827        pending_queue::apply_queue(s, d, PENDING_DEPOSIT_SSZ_SIZE)?;
828    }
829
830    if let (Some(s), Some(d)) = (
831        state.pending_partial_withdrawals_mut(),
832        delta.pending_partial_withdrawals.as_ref(),
833    ) {
834        pending_queue::apply_queue(s, d, PARTIAL_WITHDRAWAL_SSZ_SIZE)?;
835    }
836
837    if let (Some(s), Some(d)) = (
838        state.pending_consolidations_mut(),
839        delta.pending_consolidations.as_ref(),
840    ) {
841        pending_queue::apply_queue(s, d, PENDING_CONSOLIDATION_SSZ_SIZE)?;
842    }
843
844    Ok(state)
845}
846
847/// A mutable target for list-like collections of copyable values.
848///
849/// [`ListMutTarget`] provides the minimal interface required by the generic
850/// delta-application routines in this crate. It allows those routines to
851/// update consensus-state collections without requiring the collection to be
852/// backed by a contiguous `Vec`.
853///
854/// Implementations may use any underlying storage strategy, including
855/// contiguous buffers, persistent trees, or other client-specific data
856/// structures.
857///
858/// # Type parameter
859///
860/// `T` is the element type stored by the collection. It must implement
861/// [`Copy`] because delta application reads values from the encoded delta and
862/// writes them directly into the target collection.
863///
864/// # Required operations
865///
866/// An implementation must provide:
867///
868/// - [`len`](Self::len) to report the current number of elements.
869/// - [`get_mut`](Self::get_mut) to obtain mutable access to an existing
870///   element by index.
871/// - [`push`](Self::push) to append a newly decoded element.
872///
873/// # Example
874///
875/// The crate provides an implementation for `Vec<u64>` and `Vec<u8>`.
876///
877/// ```
878/// use eth_state_diff::ListMutTarget;
879///
880/// let mut values = vec![100u64, 200, 300];
881/// let target: &mut dyn ListMutTarget<u64> = &mut values;
882///
883/// *target.get_mut(1).unwrap() = 250;
884/// target.push(400);
885///
886/// assert_eq!(values, [100, 250, 300, 400]);
887/// ```
888///
889/// # Implementing for client-specific collections
890///
891/// Consensus clients with non-contiguous or tree-backed state can implement
892/// this trait to allow the generic delta algorithms to operate directly on
893/// their native collections, without first materializing the collection as a
894/// flat buffer.
895///
896/// Implementations should return `None` from [`get_mut`](Self::get_mut) when
897/// the requested index is outside the current collection bounds.
898pub trait ListMutTarget<T: Copy> {
899    /// Returns the current number of elements in the collection.
900    fn len(&self) -> usize;
901
902    /// Returns `true` if the collection contains no elements.
903    fn is_empty(&self) -> bool {
904        self.len() == 0
905    }
906
907    /// Returns mutable access to the element at `index`.
908    ///
909    /// Returns `None` if `index` is outside the current collection bounds.
910    fn get_mut(&mut self, index: usize) -> Option<&mut T>;
911
912    /// Appends `value` to the end of the collection.
913    fn push(&mut self, value: T);
914}
915
916impl ListMutTarget<u64> for Vec<u64> {
917    #[inline]
918    fn len(&self) -> usize {
919        self.len()
920    }
921
922    #[inline]
923    fn get_mut(&mut self, index: usize) -> Option<&mut u64> {
924        self.as_mut_slice().get_mut(index)
925    }
926
927    #[inline]
928    fn push(&mut self, value: u64) {
929        self.push(value);
930    }
931}
932
933impl ListMutTarget<u8> for Vec<u8> {
934    #[inline]
935    fn len(&self) -> usize {
936        self.len()
937    }
938
939    #[inline]
940    fn get_mut(&mut self, index: usize) -> Option<&mut u8> {
941        self.as_mut_slice().get_mut(index)
942    }
943
944    #[inline]
945    fn push(&mut self, value: u8) {
946        self.push(value);
947    }
948}
949
950const PENDING_DEPOSIT_SSZ_SIZE: usize = 192;
951const PARTIAL_WITHDRAWAL_SSZ_SIZE: usize = 24;
952const PENDING_CONSOLIDATION_SSZ_SIZE: usize = 16;