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.historical_roots.is_some(),
638        delta.fork < ForkName::Capella,
639        "DiffSource bug: historical_roots must exist iff fork < Capella (got {:?})",
640        delta.fork
641    );
642
643    debug_assert_eq!(
644        delta.pending_deposits.is_some(),
645        delta.fork >= ForkName::Electra,
646        "DiffSource bug: pending_deposits must exist iff fork >= Electra (got {:?})",
647        delta.fork
648    );
649
650    debug_assert_eq!(
651        delta.pending_partial_withdrawals.is_some(),
652        delta.fork >= ForkName::Electra,
653        "DiffSource bug: pending_partial_withdrawals must exist iff fork >= Electra (got {:?})",
654        delta.fork
655    );
656
657    debug_assert_eq!(
658        delta.pending_consolidations.is_some(),
659        delta.fork >= ForkName::Electra,
660        "DiffSource bug: pending_consolidations must exist iff fork >= Electra (got {:?})",
661        delta.fork
662    );
663
664    delta
665}
666
667/// Applies an archived [`BeaconStateDelta`] to a mutable beacon state.
668///
669/// The destination state is modified in place and returned after all delta
670/// components have been applied.
671///
672/// Before mutation begins, the function validates that:
673///
674/// - the destination state's fork matches the delta's fork;
675/// - fork-specific fields are valid for that fork; and
676/// - the fork value can be successfully decoded from the archived delta.
677///
678/// # Errors
679///
680/// Returns [`Error::ForkMismatch`] when the delta and destination state belong
681/// to different forks.
682///
683/// Returns [`Error::InvalidFieldForFork`] when a fork-specific field is present
684/// in a delta where that field is not valid.
685///
686/// Returns [`Error::MalformedDelta`] when the archived fork cannot be decoded.
687///
688/// # Mutation
689///
690/// Fork and field validation occurs before state components are modified.
691/// Component application itself operates in place.
692///
693/// The destination state must correspond to the base state from which the
694/// delta was created. Applying a valid delta to an unrelated state is not
695/// expected to reconstruct the original target state.
696///
697/// # Complexity
698///
699/// Linear in the amount of data represented by the delta, with the exact cost
700/// determined by the individual component encodings.
701pub fn apply<M: DiffTarget>(mut state: M, delta: &ArchivedBeaconStateDelta) -> Result<M, Error> {
702    use rkyv::deserialize;
703
704    let delta_fork: ForkName = deserialize::<ForkName, rkyv::rancor::Error>(&delta.fork)
705        .map_err(|e| Error::MalformedDelta(format!("failed to deserialize fork: {e}")))?;
706
707    let state_fork = state.get_fork();
708    if state_fork != delta_fork {
709        return Err(Error::ForkMismatch {
710            state_fork,
711            delta_fork,
712        });
713    }
714
715    macro_rules! validate_removed_field {
716        ($field:ident, $removed_in:expr) => {
717            if delta.$field.is_some() && delta_fork >= $removed_in {
718                return Err(Error::InvalidFieldForFork {
719                    field: stringify!($field),
720                    fork: delta_fork,
721                });
722            }
723        };
724    }
725
726    macro_rules! validate_field {
727        ($field:ident, $fork:expr) => {
728            if delta.$field.is_some() && delta_fork < $fork {
729                return Err(Error::InvalidFieldForFork {
730                    field: stringify!($field),
731                    fork: delta_fork,
732                });
733            }
734        };
735    }
736
737    validate_field!(previous_participation, ForkName::Altair);
738    validate_field!(current_participation, ForkName::Altair);
739    validate_field!(inactivity_scores, ForkName::Altair);
740    validate_field!(current_sync_committee, ForkName::Altair);
741    validate_field!(next_sync_committee, ForkName::Altair);
742
743    validate_field!(historical_summaries, ForkName::Capella);
744
745    validate_field!(pending_deposits, ForkName::Electra);
746    validate_field!(pending_partial_withdrawals, ForkName::Electra);
747    validate_field!(pending_consolidations, ForkName::Electra);
748
749    validate_removed_field!(previous_epoch_attestations, ForkName::Altair);
750    validate_removed_field!(current_epoch_attestations, ForkName::Altair);
751
752    validate_removed_field!(historical_roots, ForkName::Capella);
753
754    let base_slot = delta.base_slot.to_native();
755
756    *state.scalar_header_mut() = delta.scalar_header.as_slice().to_vec();
757
758    // Universal
759    balances::apply_balances_iter(state.balances_mut(), &delta.balances)?;
760    validators::apply_validators_iter(state.validators_mut(), &delta.validators)?;
761    recent_roots::apply_roots(base_slot, state.block_roots_mut(), &delta.block_roots)?;
762    recent_roots::apply_roots(base_slot, state.state_roots_mut(), &delta.state_roots)?;
763    randao_mixes::apply_randao(base_slot, state.randao_mixes_mut(), &delta.randao_mixes)?;
764    slashings::apply_slashings(state.slashings_mut(), &delta.slashings)?;
765    eth1_data_votes::apply_eth1_votes(state.eth1_data_votes_mut(), &delta.eth1_data_votes);
766
767    if let (Some(s), Some(d)) = (
768        state.historical_roots_mut(),
769        delta.historical_roots.as_ref(),
770    ) {
771        historical_log::apply_historical_log(s, d);
772    }
773
774    if let (Some(s), Some(d)) = (
775        state.previous_epoch_attestations_mut(),
776        delta.previous_epoch_attestations.as_ref(),
777    ) {
778        attestations::apply_attestations(s, d);
779    }
780
781    if let (Some(s), Some(d)) = (
782        state.current_epoch_attestations_mut(),
783        delta.current_epoch_attestations.as_ref(),
784    ) {
785        attestations::apply_attestations(s, d);
786    }
787
788    if let (Some(s), Some(d)) = (
789        state.previous_participation_mut(),
790        delta.previous_participation.as_ref(),
791    ) {
792        participation::apply_participation_iter(s, d)?;
793    }
794
795    if let (Some(s), Some(d)) = (
796        state.current_participation_mut(),
797        delta.current_participation.as_ref(),
798    ) {
799        participation::apply_participation_iter(s, d)?;
800    }
801
802    if let (Some(s), Some(d)) = (
803        state.inactivity_scores_mut(),
804        delta.inactivity_scores.as_ref(),
805    ) {
806        inactivity_scores::apply_inactivity(s, d)?;
807    }
808
809    if let (Some(s), Some(d)) = (
810        state.current_sync_committee_mut(),
811        delta.current_sync_committee.as_ref(),
812    ) {
813        sync_committee::apply_sync_committee(s, d);
814    }
815
816    if let (Some(s), Some(d)) = (
817        state.next_sync_committee_mut(),
818        delta.next_sync_committee.as_ref(),
819    ) {
820        sync_committee::apply_sync_committee(s, d);
821    }
822
823    if let (Some(s), Some(d)) = (
824        state.historical_summaries_mut(),
825        delta.historical_summaries.as_ref(),
826    ) {
827        historical_log::apply_historical_log(s, d);
828    }
829
830    if let (Some(s), Some(d)) = (
831        state.pending_deposits_mut(),
832        delta.pending_deposits.as_ref(),
833    ) {
834        pending_queue::apply_queue(s, d, PENDING_DEPOSIT_SSZ_SIZE)?;
835    }
836
837    if let (Some(s), Some(d)) = (
838        state.pending_partial_withdrawals_mut(),
839        delta.pending_partial_withdrawals.as_ref(),
840    ) {
841        pending_queue::apply_queue(s, d, PARTIAL_WITHDRAWAL_SSZ_SIZE)?;
842    }
843
844    if let (Some(s), Some(d)) = (
845        state.pending_consolidations_mut(),
846        delta.pending_consolidations.as_ref(),
847    ) {
848        pending_queue::apply_queue(s, d, PENDING_CONSOLIDATION_SSZ_SIZE)?;
849    }
850
851    Ok(state)
852}
853
854/// A mutable target for list-like collections of copyable values.
855///
856/// [`ListMutTarget`] provides the minimal interface required by the generic
857/// delta-application routines in this crate. It allows those routines to
858/// update consensus-state collections without requiring the collection to be
859/// backed by a contiguous `Vec`.
860///
861/// Implementations may use any underlying storage strategy, including
862/// contiguous buffers, persistent trees, or other client-specific data
863/// structures.
864///
865/// # Type parameter
866///
867/// `T` is the element type stored by the collection. It must implement
868/// [`Copy`] because delta application reads values from the encoded delta and
869/// writes them directly into the target collection.
870///
871/// # Required operations
872///
873/// An implementation must provide:
874///
875/// - [`len`](Self::len) to report the current number of elements.
876/// - [`get_mut`](Self::get_mut) to obtain mutable access to an existing
877///   element by index.
878/// - [`push`](Self::push) to append a newly decoded element.
879///
880/// # Example
881///
882/// The crate provides an implementation for `Vec<u64>` and `Vec<u8>`.
883///
884/// ```
885/// use eth_state_diff::ListMutTarget;
886///
887/// let mut values = vec![100u64, 200, 300];
888/// let target: &mut dyn ListMutTarget<u64> = &mut values;
889///
890/// *target.get_mut(1).unwrap() = 250;
891/// target.push(400);
892///
893/// assert_eq!(values, [100, 250, 300, 400]);
894/// ```
895///
896/// # Implementing for client-specific collections
897///
898/// Consensus clients with non-contiguous or tree-backed state can implement
899/// this trait to allow the generic delta algorithms to operate directly on
900/// their native collections, without first materializing the collection as a
901/// flat buffer.
902///
903/// Implementations should return `None` from [`get_mut`](Self::get_mut) when
904/// the requested index is outside the current collection bounds.
905pub trait ListMutTarget<T: Copy> {
906    /// Returns the current number of elements in the collection.
907    fn len(&self) -> usize;
908
909    /// Returns `true` if the collection contains no elements.
910    fn is_empty(&self) -> bool {
911        self.len() == 0
912    }
913
914    /// Returns mutable access to the element at `index`.
915    ///
916    /// Returns `None` if `index` is outside the current collection bounds.
917    fn get_mut(&mut self, index: usize) -> Option<&mut T>;
918
919    /// Appends `value` to the end of the collection.
920    fn push(&mut self, value: T);
921}
922
923impl ListMutTarget<u64> for Vec<u64> {
924    #[inline]
925    fn len(&self) -> usize {
926        self.len()
927    }
928
929    #[inline]
930    fn get_mut(&mut self, index: usize) -> Option<&mut u64> {
931        self.as_mut_slice().get_mut(index)
932    }
933
934    #[inline]
935    fn push(&mut self, value: u64) {
936        self.push(value);
937    }
938}
939
940impl ListMutTarget<u8> for Vec<u8> {
941    #[inline]
942    fn len(&self) -> usize {
943        self.len()
944    }
945
946    #[inline]
947    fn get_mut(&mut self, index: usize) -> Option<&mut u8> {
948        self.as_mut_slice().get_mut(index)
949    }
950
951    #[inline]
952    fn push(&mut self, value: u8) {
953        self.push(value);
954    }
955}
956
957const PENDING_DEPOSIT_SSZ_SIZE: usize = 192;
958const PARTIAL_WITHDRAWAL_SSZ_SIZE: usize = 24;
959const PENDING_CONSOLIDATION_SSZ_SIZE: usize = 16;
960
961#[cfg(test)]
962mod tests {
963    use super::*;
964
965    #[test]
966    fn fork_name_ordering_is_correct() {
967        // Ensure the protocol progression holds.
968        assert!(ForkName::Phase0 < ForkName::Altair);
969        assert!(ForkName::Altair < ForkName::Bellatrix);
970        assert!(ForkName::Bellatrix < ForkName::Capella);
971        assert!(ForkName::Capella < ForkName::Deneb);
972        assert!(ForkName::Deneb < ForkName::Electra);
973
974        // Sanity check equality.
975        assert_eq!(ForkName::Capella, ForkName::Capella);
976    }
977
978    #[test]
979    fn list_mut_target_vec_u64_works() {
980        let mut v = vec![100u64, 200, 300];
981
982        {
983            let target: &mut dyn ListMutTarget<u64> = &mut v;
984
985            assert_eq!(target.len(), 3);
986            assert!(!target.is_empty());
987
988            *target.get_mut(1).expect("index 1 exists") = 250;
989
990            assert!(target.get_mut(10).is_none());
991
992            target.push(400);
993
994            assert_eq!(target.len(), 4);
995        }
996
997        assert_eq!(v[1], 250);
998        assert_eq!(v[3], 400);
999    }
1000
1001    #[test]
1002    fn list_mut_target_vec_u8_works() {
1003        let mut v = vec![1u8, 2, 3];
1004
1005        {
1006            let target: &mut dyn ListMutTarget<u8> = &mut v;
1007
1008            assert_eq!(target.len(), 3);
1009
1010            *target.get_mut(0).expect("index 0 exists") = 9;
1011
1012            target.push(4);
1013        }
1014
1015        assert_eq!(v[0], 9);
1016        assert_eq!(v[3], 4);
1017    }
1018}
1019
1020#[cfg(test)]
1021mod integration_tests {
1022    use super::*;
1023    use crate::types::{MIN_VALIDATOR_WITHDRAWABILITY_DELAY, VALIDATOR_SSZ_SIZE};
1024    use crate::validators::{ValidatorMut, ValidatorMutTarget, ValidatorSnapshot};
1025
1026    const SLOTS_PER_EPOCH: u64 = 32;
1027    const SLOTS_PER_HISTORICAL_ROOT: usize = 8192;
1028    const EPOCHS_PER_HISTORICAL_ROOT: usize = 256;
1029    const EPOCHS_PER_SLASHINGS_VECTOR: usize = 8192;
1030
1031    #[derive(Clone, Debug, PartialEq, Eq)]
1032    struct MockValidator {
1033        withdrawal_credentials: [u8; 32],
1034        effective_balance: u64,
1035        slashed: bool,
1036        activation_eligibility_epoch: u64,
1037        activation_epoch: u64,
1038        exit_epoch: u64,
1039        withdrawable_epoch: u64,
1040    }
1041
1042    impl MockValidator {
1043        fn new(id: u8) -> Self {
1044            Self {
1045                withdrawal_credentials: [id; 32],
1046                effective_balance: 32_000_000_000,
1047                slashed: false,
1048                activation_eligibility_epoch: 0,
1049                activation_epoch: 0,
1050                exit_epoch: u64::MAX,
1051                withdrawable_epoch: u64::MAX,
1052            }
1053        }
1054
1055        fn from_ssz_bytes(b: &[u8]) -> Self {
1056            assert!(b.len() >= VALIDATOR_SSZ_SIZE, "ssz too short");
1057            Self {
1058                withdrawal_credentials: b[48..80].try_into().unwrap(),
1059                effective_balance: u64::from_le_bytes(b[80..88].try_into().unwrap()),
1060                slashed: b[88] != 0,
1061                activation_eligibility_epoch: u64::from_le_bytes(b[89..97].try_into().unwrap()),
1062                activation_epoch: u64::from_le_bytes(b[97..105].try_into().unwrap()),
1063                exit_epoch: u64::from_le_bytes(b[105..113].try_into().unwrap()),
1064                withdrawable_epoch: u64::from_le_bytes(b[113..121].try_into().unwrap()),
1065            }
1066        }
1067    }
1068
1069    impl ValidatorSnapshot for MockValidator {
1070        fn withdrawal_credentials(&self) -> &[u8; 32] {
1071            &self.withdrawal_credentials
1072        }
1073
1074        fn effective_balance(&self) -> u64 {
1075            self.effective_balance
1076        }
1077
1078        fn is_slashed(&self) -> bool {
1079            self.slashed
1080        }
1081
1082        fn activation_eligibility_epoch(&self) -> u64 {
1083            self.activation_eligibility_epoch
1084        }
1085
1086        fn activation_epoch(&self) -> u64 {
1087            self.activation_epoch
1088        }
1089
1090        fn exit_epoch(&self) -> u64 {
1091            self.exit_epoch
1092        }
1093
1094        fn withdrawable_epoch(&self) -> u64 {
1095            self.withdrawable_epoch
1096        }
1097
1098        fn to_ssz_bytes(&self) -> Vec<u8> {
1099            let mut b = vec![0u8; VALIDATOR_SSZ_SIZE];
1100            b[48..80].copy_from_slice(&self.withdrawal_credentials);
1101            b[80..88].copy_from_slice(&self.effective_balance.to_le_bytes());
1102            b[88] = self.slashed as u8;
1103            b[89..97].copy_from_slice(&self.activation_eligibility_epoch.to_le_bytes());
1104            b[97..105].copy_from_slice(&self.activation_epoch.to_le_bytes());
1105            b[105..113].copy_from_slice(&self.exit_epoch.to_le_bytes());
1106            b[113..121].copy_from_slice(&self.withdrawable_epoch.to_le_bytes());
1107            b
1108        }
1109    }
1110
1111    struct MockMutVal<'a>(&'a mut MockValidator);
1112
1113    impl ValidatorMut for MockMutVal<'_> {
1114        fn is_slashed(&self) -> bool {
1115            self.0.slashed
1116        }
1117
1118        fn set_withdrawal_credentials(&mut self, v: &[u8; 32]) {
1119            self.0.withdrawal_credentials = *v;
1120        }
1121
1122        fn set_effective_balance(&mut self, v: u64) {
1123            self.0.effective_balance = v;
1124        }
1125
1126        fn set_slashed(&mut self, v: bool) {
1127            self.0.slashed = v;
1128        }
1129
1130        fn set_activation_eligibility_epoch(&mut self, v: u64) {
1131            self.0.activation_eligibility_epoch = v;
1132        }
1133
1134        fn set_activation_epoch(&mut self, v: u64) {
1135            self.0.activation_epoch = v;
1136        }
1137
1138        fn set_exit_epoch(&mut self, v: u64) {
1139            self.0.exit_epoch = v;
1140        }
1141
1142        fn set_withdrawable_epoch(&mut self, v: u64) {
1143            self.0.withdrawable_epoch = v;
1144        }
1145    }
1146
1147    impl ValidatorMutTarget for Vec<MockValidator> {
1148        type Validator<'a>
1149            = MockMutVal<'a>
1150        where
1151            Self: 'a;
1152
1153        fn get_mut(&mut self, i: usize) -> Option<Self::Validator<'_>> {
1154            self.as_mut_slice().get_mut(i).map(MockMutVal)
1155        }
1156
1157        fn push_from_ssz(&mut self, b: &[u8]) {
1158            self.push(MockValidator::from_ssz_bytes(b));
1159        }
1160    }
1161
1162    #[derive(Clone, Debug, PartialEq, Eq)]
1163    struct MockState {
1164        fork: ForkName,
1165        slot: u64,
1166        capella_fork_slot: u64,
1167        scalar_header: Vec<u8>,
1168
1169        balances: Vec<u64>,
1170        validators: Vec<MockValidator>,
1171        block_roots: Vec<[u8; 32]>,
1172        state_roots: Vec<[u8; 32]>,
1173        randao_mixes: Vec<[u8; 32]>,
1174        slashings: Vec<u64>,
1175        eth1_data_votes: Vec<u8>,
1176
1177        historical_roots: Option<Vec<u8>>,
1178
1179        previous_epoch_attestations: Option<Vec<u8>>,
1180        current_epoch_attestations: Option<Vec<u8>>,
1181
1182        previous_participation: Option<Vec<u8>>,
1183        current_participation: Option<Vec<u8>>,
1184        inactivity_scores: Option<Vec<u64>>,
1185        current_sync_committee: Option<Vec<u8>>,
1186        next_sync_committee: Option<Vec<u8>>,
1187
1188        historical_summaries: Option<Vec<u8>>,
1189
1190        pending_deposits: Option<Vec<u8>>,
1191        pending_partial_withdrawals: Option<Vec<u8>>,
1192        pending_consolidations: Option<Vec<u8>>,
1193    }
1194
1195    impl MockState {
1196        fn at_fork(fork: ForkName, slot: u64, capella_fork_slot: u64) -> Self {
1197            let mut state = Self {
1198                fork: fork.clone(),
1199                slot,
1200                capella_fork_slot,
1201                scalar_header: vec![],
1202
1203                balances: vec![],
1204                validators: vec![],
1205
1206                block_roots: vec![[0; 32]; SLOTS_PER_HISTORICAL_ROOT],
1207                state_roots: vec![[0; 32]; SLOTS_PER_HISTORICAL_ROOT],
1208                randao_mixes: vec![[0; 32]; EPOCHS_PER_HISTORICAL_ROOT],
1209                slashings: vec![0; EPOCHS_PER_SLASHINGS_VECTOR],
1210                eth1_data_votes: vec![],
1211
1212                historical_roots: None,
1213
1214                previous_epoch_attestations: None,
1215                current_epoch_attestations: None,
1216
1217                previous_participation: None,
1218                current_participation: None,
1219                inactivity_scores: None,
1220                current_sync_committee: None,
1221                next_sync_committee: None,
1222
1223                historical_summaries: None,
1224
1225                pending_deposits: None,
1226                pending_partial_withdrawals: None,
1227                pending_consolidations: None,
1228            };
1229
1230            if fork == ForkName::Phase0 {
1231                state.previous_epoch_attestations = Some(vec![]);
1232                state.current_epoch_attestations = Some(vec![]);
1233            }
1234
1235            if fork < ForkName::Capella {
1236                state.historical_roots = Some(vec![]);
1237            }
1238
1239            if fork >= ForkName::Altair {
1240                state.previous_participation = Some(vec![]);
1241                state.current_participation = Some(vec![]);
1242                state.inactivity_scores = Some(vec![]);
1243                state.current_sync_committee = Some(vec![0; 48 * 512]);
1244                state.next_sync_committee = Some(vec![0; 48 * 512]);
1245            }
1246
1247            if fork >= ForkName::Capella {
1248                state.historical_summaries = Some(vec![]);
1249            }
1250
1251            if fork >= ForkName::Electra {
1252                state.pending_deposits = Some(vec![]);
1253                state.pending_partial_withdrawals = Some(vec![]);
1254                state.pending_consolidations = Some(vec![]);
1255            }
1256
1257            state
1258        }
1259    }
1260
1261    struct MockSource<'a> {
1262        base: &'a MockState,
1263        target: &'a MockState,
1264    }
1265
1266    fn pair_ref<'a, T>(base: &'a Option<T>, target: &'a Option<T>) -> Option<(&'a T, &'a T)> {
1267        match (base, target) {
1268            (Some(base), Some(target)) => Some((base, target)),
1269            _ => None,
1270        }
1271    }
1272
1273    impl DiffSource for MockSource<'_> {
1274        fn fork(&self) -> ForkName {
1275            self.target.fork.clone()
1276        }
1277
1278        fn slot(&self) -> (u64, u64) {
1279            (self.base.slot, self.target.slot)
1280        }
1281
1282        fn capella_fork_slot(&self) -> u64 {
1283            self.target.capella_fork_slot
1284        }
1285
1286        fn scalar_header(&self) -> Vec<u8> {
1287            self.target.scalar_header.clone()
1288        }
1289
1290        fn balances(
1291            &self,
1292        ) -> (
1293            impl ExactSizeIterator<Item = u64>,
1294            impl ExactSizeIterator<Item = u64>,
1295        ) {
1296            (
1297                self.base.balances.clone().into_iter(),
1298                self.target.balances.clone().into_iter(),
1299            )
1300        }
1301
1302        fn validators(
1303            &self,
1304        ) -> (
1305            impl ExactSizeIterator<Item = impl ValidatorSnapshot>,
1306            impl ExactSizeIterator<Item = impl ValidatorSnapshot>,
1307        ) {
1308            (
1309                self.base.validators.clone().into_iter(),
1310                self.target.validators.clone().into_iter(),
1311            )
1312        }
1313
1314        fn block_roots(&self) -> &[[u8; 32]] {
1315            &self.target.block_roots
1316        }
1317
1318        fn state_roots(&self) -> &[[u8; 32]] {
1319            &self.target.state_roots
1320        }
1321
1322        fn randao_mixes(&self) -> &[[u8; 32]] {
1323            &self.target.randao_mixes
1324        }
1325
1326        fn slashings(&self) -> (&[u64], &[u64]) {
1327            (&self.base.slashings, &self.target.slashings)
1328        }
1329
1330        fn eth1_data_votes(&self) -> (&[u8], &[u8]) {
1331            (&self.base.eth1_data_votes, &self.target.eth1_data_votes)
1332        }
1333
1334        fn historical_roots(&self) -> Option<&[u8]> {
1335            self.target.historical_roots.as_deref()
1336        }
1337
1338        fn previous_epoch_attestations(&self) -> Option<(&[u8], &[u8])> {
1339            pair_ref(
1340                &self.base.previous_epoch_attestations,
1341                &self.target.previous_epoch_attestations,
1342            )
1343            .map(|(base, target)| (base.as_slice(), target.as_slice()))
1344        }
1345
1346        fn current_epoch_attestations(&self) -> Option<(&[u8], &[u8])> {
1347            pair_ref(
1348                &self.base.current_epoch_attestations,
1349                &self.target.current_epoch_attestations,
1350            )
1351            .map(|(base, target)| (base.as_slice(), target.as_slice()))
1352        }
1353
1354        fn previous_participation(
1355            &self,
1356        ) -> Option<(
1357            impl ExactSizeIterator<Item = u8>,
1358            impl ExactSizeIterator<Item = u8>,
1359        )> {
1360            pair_ref(
1361                &self.base.previous_participation,
1362                &self.target.previous_participation,
1363            )
1364            .map(|(base, target)| (base.clone().into_iter(), target.clone().into_iter()))
1365        }
1366
1367        fn current_participation(
1368            &self,
1369        ) -> Option<(
1370            impl ExactSizeIterator<Item = u8>,
1371            impl ExactSizeIterator<Item = u8>,
1372        )> {
1373            pair_ref(
1374                &self.base.current_participation,
1375                &self.target.current_participation,
1376            )
1377            .map(|(base, target)| (base.clone().into_iter(), target.clone().into_iter()))
1378        }
1379
1380        fn inactivity_scores(&self) -> Option<(&[u64], &[u64])> {
1381            pair_ref(&self.base.inactivity_scores, &self.target.inactivity_scores)
1382                .map(|(base, target)| (base.as_slice(), target.as_slice()))
1383        }
1384
1385        fn current_sync_committee(&self) -> Option<(&[u8], &[u8])> {
1386            pair_ref(
1387                &self.base.current_sync_committee,
1388                &self.target.current_sync_committee,
1389            )
1390            .map(|(base, target)| (base.as_slice(), target.as_slice()))
1391        }
1392
1393        fn next_sync_committee(&self) -> Option<(&[u8], &[u8])> {
1394            pair_ref(
1395                &self.base.next_sync_committee,
1396                &self.target.next_sync_committee,
1397            )
1398            .map(|(base, target)| (base.as_slice(), target.as_slice()))
1399        }
1400
1401        fn historical_summaries(&self) -> Option<&[u8]> {
1402            self.target.historical_summaries.as_deref()
1403        }
1404
1405        fn pending_deposits(&self) -> Option<(&[u8], &[u8])> {
1406            pair_ref(&self.base.pending_deposits, &self.target.pending_deposits)
1407                .map(|(base, target)| (base.as_slice(), target.as_slice()))
1408        }
1409
1410        fn pending_partial_withdrawals(&self) -> Option<(&[u8], &[u8])> {
1411            pair_ref(
1412                &self.base.pending_partial_withdrawals,
1413                &self.target.pending_partial_withdrawals,
1414            )
1415            .map(|(base, target)| (base.as_slice(), target.as_slice()))
1416        }
1417
1418        fn pending_consolidations(&self) -> Option<(&[u8], &[u8])> {
1419            pair_ref(
1420                &self.base.pending_consolidations,
1421                &self.target.pending_consolidations,
1422            )
1423            .map(|(base, target)| (base.as_slice(), target.as_slice()))
1424        }
1425    }
1426
1427    impl DiffTarget for MockState {
1428        fn get_fork(&self) -> ForkName {
1429            self.fork.clone()
1430        }
1431
1432        fn scalar_header_mut(&mut self) -> &mut Vec<u8> {
1433            &mut self.scalar_header
1434        }
1435
1436        fn balances_mut(&mut self) -> &mut impl ListMutTarget<u64> {
1437            &mut self.balances
1438        }
1439
1440        fn validators_mut(&mut self) -> &mut impl ValidatorMutTarget {
1441            &mut self.validators
1442        }
1443
1444        fn block_roots_mut(&mut self) -> &mut [[u8; 32]] {
1445            self.block_roots.as_mut_slice()
1446        }
1447
1448        fn state_roots_mut(&mut self) -> &mut [[u8; 32]] {
1449            self.state_roots.as_mut_slice()
1450        }
1451
1452        fn randao_mixes_mut(&mut self) -> &mut [[u8; 32]] {
1453            self.randao_mixes.as_mut_slice()
1454        }
1455
1456        fn slashings_mut(&mut self) -> &mut [u64] {
1457            self.slashings.as_mut_slice()
1458        }
1459
1460        fn eth1_data_votes_mut(&mut self) -> &mut Vec<u8> {
1461            &mut self.eth1_data_votes
1462        }
1463
1464        fn historical_roots_mut(&mut self) -> Option<&mut Vec<u8>> {
1465            self.historical_roots.as_mut()
1466        }
1467
1468        fn previous_epoch_attestations_mut(&mut self) -> Option<&mut Vec<u8>> {
1469            self.previous_epoch_attestations.as_mut()
1470        }
1471
1472        fn current_epoch_attestations_mut(&mut self) -> Option<&mut Vec<u8>> {
1473            self.current_epoch_attestations.as_mut()
1474        }
1475
1476        fn previous_participation_mut(&mut self) -> Option<&mut impl ListMutTarget<u8>> {
1477            self.previous_participation.as_mut()
1478        }
1479
1480        fn current_participation_mut(&mut self) -> Option<&mut impl ListMutTarget<u8>> {
1481            self.current_participation.as_mut()
1482        }
1483
1484        fn inactivity_scores_mut(&mut self) -> Option<&mut Vec<u64>> {
1485            self.inactivity_scores.as_mut()
1486        }
1487
1488        fn current_sync_committee_mut(&mut self) -> Option<&mut Vec<u8>> {
1489            self.current_sync_committee.as_mut()
1490        }
1491
1492        fn next_sync_committee_mut(&mut self) -> Option<&mut Vec<u8>> {
1493            self.next_sync_committee.as_mut()
1494        }
1495
1496        fn historical_summaries_mut(&mut self) -> Option<&mut Vec<u8>> {
1497            self.historical_summaries.as_mut()
1498        }
1499
1500        fn pending_deposits_mut(&mut self) -> Option<&mut Vec<u8>> {
1501            self.pending_deposits.as_mut()
1502        }
1503
1504        fn pending_partial_withdrawals_mut(&mut self) -> Option<&mut Vec<u8>> {
1505            self.pending_partial_withdrawals.as_mut()
1506        }
1507
1508        fn pending_consolidations_mut(&mut self) -> Option<&mut Vec<u8>> {
1509            self.pending_consolidations.as_mut()
1510        }
1511    }
1512
1513    fn archive_delta(delta: &BeaconStateDelta) -> Vec<u8> {
1514        rkyv::to_bytes::<rkyv::rancor::Error>(delta)
1515            .expect("serialize delta")
1516            .to_vec()
1517    }
1518
1519    fn access_archived(bytes: &[u8]) -> &ArchivedBeaconStateDelta {
1520        rkyv::access::<ArchivedBeaconStateDelta, rkyv::rancor::Error>(bytes)
1521            .expect("access archived delta")
1522    }
1523
1524    fn roundtrip(base: MockState, target: MockState) {
1525        assert_eq!(
1526            base.fork, target.fork,
1527            "roundtrip requires base and target to use the same fork"
1528        );
1529
1530        let source = MockSource {
1531            base: &base,
1532            target: &target,
1533        };
1534
1535        let delta = create(&source);
1536        let bytes = archive_delta(&delta);
1537        let archived = access_archived(&bytes);
1538
1539        let mut reconstructed = apply(base.clone(), archived).expect("apply");
1540
1541        // In a real client, slot is part of scalar_header.
1542        // The mock stores it separately because DiffSource::slot()
1543        // needs independent base/target slot values.
1544        reconstructed.slot = target.slot;
1545
1546        assert_eq!(reconstructed.fork, target.fork, "fork mismatch");
1547        assert_eq!(
1548            reconstructed.scalar_header, target.scalar_header,
1549            "scalar_header mismatch"
1550        );
1551        assert_eq!(reconstructed.balances, target.balances, "balances mismatch");
1552        assert_eq!(
1553            reconstructed.validators, target.validators,
1554            "validators mismatch"
1555        );
1556        assert_eq!(
1557            reconstructed.block_roots, target.block_roots,
1558            "block_roots mismatch"
1559        );
1560        assert_eq!(
1561            reconstructed.state_roots, target.state_roots,
1562            "state_roots mismatch"
1563        );
1564        assert_eq!(
1565            reconstructed.randao_mixes, target.randao_mixes,
1566            "randao_mixes mismatch"
1567        );
1568        assert_eq!(
1569            reconstructed.slashings, target.slashings,
1570            "slashings mismatch"
1571        );
1572        assert_eq!(
1573            reconstructed.eth1_data_votes, target.eth1_data_votes,
1574            "eth1_data_votes mismatch"
1575        );
1576        assert_eq!(
1577            reconstructed.historical_roots, target.historical_roots,
1578            "historical_roots mismatch"
1579        );
1580        assert_eq!(
1581            reconstructed.previous_epoch_attestations, target.previous_epoch_attestations,
1582            "previous_epoch_attestations mismatch"
1583        );
1584        assert_eq!(
1585            reconstructed.current_epoch_attestations, target.current_epoch_attestations,
1586            "current_epoch_attestations mismatch"
1587        );
1588        assert_eq!(
1589            reconstructed.previous_participation, target.previous_participation,
1590            "previous_participation mismatch"
1591        );
1592        assert_eq!(
1593            reconstructed.current_participation, target.current_participation,
1594            "current_participation mismatch"
1595        );
1596        assert_eq!(
1597            reconstructed.inactivity_scores, target.inactivity_scores,
1598            "inactivity_scores mismatch"
1599        );
1600        assert_eq!(
1601            reconstructed.current_sync_committee, target.current_sync_committee,
1602            "current_sync_committee mismatch"
1603        );
1604        assert_eq!(
1605            reconstructed.next_sync_committee, target.next_sync_committee,
1606            "next_sync_committee mismatch"
1607        );
1608        assert_eq!(
1609            reconstructed.historical_summaries, target.historical_summaries,
1610            "historical_summaries mismatch"
1611        );
1612        assert_eq!(
1613            reconstructed.pending_deposits, target.pending_deposits,
1614            "pending_deposits mismatch"
1615        );
1616        assert_eq!(
1617            reconstructed.pending_partial_withdrawals, target.pending_partial_withdrawals,
1618            "pending_partial_withdrawals mismatch"
1619        );
1620        assert_eq!(
1621            reconstructed.pending_consolidations, target.pending_consolidations,
1622            "pending_consolidations mismatch"
1623        );
1624    }
1625
1626    #[test]
1627    fn phase0_empty_state_no_changes() {
1628        let base = MockState::at_fork(ForkName::Phase0, 100, 32_000);
1629        let target = MockState::at_fork(ForkName::Phase0, 105, 32_000);
1630
1631        roundtrip(base, target);
1632    }
1633
1634    #[test]
1635    fn phase0_balances_validators_roots_slashings_votes() {
1636        let mut base = MockState::at_fork(ForkName::Phase0, 96, 32_000);
1637        let mut target = MockState::at_fork(ForkName::Phase0, 128, 32_000);
1638
1639        base.validators = vec![MockValidator::new(1), MockValidator::new(2)];
1640        base.balances = vec![32_000_000_000, 32_000_000_000];
1641
1642        target.validators = base.validators.clone();
1643        target.balances = base.balances.clone();
1644
1645        // Balance change.
1646        target.balances[1] = 31_000_000_000;
1647
1648        // Validator field patch.
1649        target.validators[0].effective_balance = 31_000_000_000;
1650
1651        // Appended validator + balance.
1652        target.validators.push(MockValidator::new(3));
1653        target.balances.push(32_000_000_000);
1654
1655        // Circular-buffer roots.
1656        for slot in 96u64..128 {
1657            let index = (slot as usize) % SLOTS_PER_HISTORICAL_ROOT;
1658
1659            target.block_roots[index] = [slot as u8; 32];
1660            target.state_roots[index] = [(slot + 1) as u8; 32];
1661        }
1662
1663        // Slashing at target epoch index.
1664        let slashing_index = (target.slot / SLOTS_PER_EPOCH) as usize % EPOCHS_PER_SLASHINGS_VECTOR;
1665        target.slashings[slashing_index] = 1_000_000_000;
1666
1667        // Eth1 data votes.
1668        target.eth1_data_votes = vec![0xAA, 0xBB, 0xCC];
1669
1670        // Scalar header.
1671        base.scalar_header = vec![0x11; 16];
1672        target.scalar_header = vec![0x22; 16];
1673
1674        roundtrip(base, target);
1675    }
1676
1677    #[test]
1678    fn capella_with_altair_fields_and_slashed_validator() {
1679        let mut base = MockState::at_fork(ForkName::Capella, 32, 32);
1680        let mut target = MockState::at_fork(ForkName::Capella, 8224, 32);
1681
1682        base.validators.clone_from(&vec![
1683            MockValidator::new(1),
1684            MockValidator::new(2),
1685            MockValidator::new(3),
1686        ]);
1687
1688        target.validators.clone_from(&base.validators);
1689
1690        base.balances
1691            .clone_from(&vec![32_000_000_000, 32_000_000_000, 32_000_000_000]);
1692
1693        target.balances.clone_from(&base.balances);
1694
1695        target.validators[1].slashed = true;
1696        target.validators[1].exit_epoch = 200;
1697        target.validators[1].withdrawable_epoch = 200 + MIN_VALIDATOR_WITHDRAWABILITY_DELAY;
1698
1699        target.balances[2] -= 1_000_000_000;
1700
1701        for i in 0..5 {
1702            target.block_roots[100 + i] = [i as u8; 32];
1703            target.state_roots[100 + i] = [(i + 10) as u8; 32];
1704        }
1705
1706        target.previous_participation = Some(vec![1, 3, 7]);
1707        target.current_participation = Some(vec![2, 4, 8]);
1708        target.inactivity_scores = Some(vec![0, 1, 2]);
1709        target.current_sync_committee = Some(vec![0xFF; 48 * 512]);
1710        target.next_sync_committee = Some(vec![0xEE; 48 * 512]);
1711
1712        // One historical summary becomes available at:
1713        // capella_fork_slot + SLOTS_PER_HISTORICAL_PERIOD
1714        // = 32 + 8192 = 8224.
1715        target.historical_summaries = Some(vec![0xAB; 64]);
1716
1717        target.scalar_header = vec![0x44];
1718        // base.scalar_header = vec![0x33];
1719
1720        roundtrip(base, target);
1721    }
1722
1723    #[test]
1724    fn electra_with_pending_queues() {
1725        let mut base = MockState::at_fork(ForkName::Electra, 100, 32);
1726        let mut target = MockState::at_fork(ForkName::Electra, 105, 32);
1727
1728        base.validators = vec![MockValidator::new(1)];
1729        base.balances = vec![32_000_000_000];
1730
1731        target.validators = base.validators.clone();
1732        target.balances = base.balances.clone();
1733
1734        // Pending deposit: 192 bytes.
1735        target.pending_deposits = Some(vec![0xAB; PENDING_DEPOSIT_SSZ_SIZE]);
1736
1737        // Pending partial withdrawal: 24 bytes.
1738        target.pending_partial_withdrawals = Some(vec![0xCD; PARTIAL_WITHDRAWAL_SSZ_SIZE]);
1739
1740        // Pending consolidation: 16 bytes.
1741        target.pending_consolidations = Some(vec![0xEF; PENDING_CONSOLIDATION_SSZ_SIZE]);
1742
1743        base.scalar_header = vec![0x55; 16];
1744        target.scalar_header = vec![0x66; 16];
1745
1746        roundtrip(base, target);
1747    }
1748
1749    // there is probably a way to fix the below
1750    // TODO:
1751    // the four commented out tests pass locally, but fail in CI because it is debug_assert!
1752    //
1753    // #[test]
1754    // #[should_panic(
1755    //     expected = "DiffSource bug: previous_participation must exist iff fork >= Altair"
1756    // )]
1757    // fn create_rejects_altair_missing_participation() {
1758    //     let base = MockState::at_fork(ForkName::Capella, 100, 32);
1759    //     let mut target = MockState::at_fork(ForkName::Capella, 105, 32);
1760    //
1761    //     target.previous_participation = None;
1762    //
1763    //     let source = MockSource {
1764    //         base: &base,
1765    //         target: &target,
1766    //     };
1767    //
1768    //     let _ = create(&source);
1769    // }
1770    //
1771    // #[test]
1772    // #[should_panic(
1773    //     expected = "DiffSource bug: current_participation must exist iff fork >= Altair"
1774    // )]
1775    // fn create_rejects_altair_missing_current_participation() {
1776    //     let base = MockState::at_fork(ForkName::Capella, 100, 32);
1777    //     let mut target = MockState::at_fork(ForkName::Capella, 105, 32);
1778    //
1779    //     target.current_participation = None;
1780    //
1781    //     let source = MockSource {
1782    //         base: &base,
1783    //         target: &target,
1784    //     };
1785    //
1786    //     let _ = create(&source);
1787    // }
1788    //
1789    // #[test]
1790    // #[should_panic(
1791    //     expected = "DiffSource bug: historical_summaries must exist iff fork >= Capella"
1792    // )]
1793    // fn create_rejects_capella_missing_historical_summaries() {
1794    //     let base = MockState::at_fork(ForkName::Capella, 100, 32);
1795    //     let mut target = MockState::at_fork(ForkName::Capella, 105, 32);
1796    //
1797    //     target.historical_summaries = None;
1798    //
1799    //     let source = MockSource {
1800    //         base: &base,
1801    //         target: &target,
1802    //     };
1803    //
1804    //     let _ = create(&source);
1805    // }
1806    //
1807    // #[test]
1808    // #[should_panic(expected = "DiffSource bug: pending_deposits must exist iff fork >= Electra")]
1809    // fn create_rejects_electra_missing_pending_deposits() {
1810    //     let base = MockState::at_fork(ForkName::Electra, 100, 32);
1811    //     let mut target = MockState::at_fork(ForkName::Electra, 105, 32);
1812    //
1813    //     target.pending_deposits = None;
1814    //
1815    //     let source = MockSource {
1816    //         base: &base,
1817    //         target: &target,
1818    //     };
1819    //
1820    //     let _ = create(&source);
1821    // }
1822    //
1823    #[test]
1824    fn fork_mismatch_rejected() {
1825        let base = MockState::at_fork(ForkName::Capella, 100, 32);
1826        let target = MockState::at_fork(ForkName::Capella, 105, 32);
1827
1828        let source = MockSource {
1829            base: &base,
1830            target: &target,
1831        };
1832
1833        let delta = create(&source);
1834        assert_eq!(delta.fork, ForkName::Capella);
1835
1836        let bytes = archive_delta(&delta);
1837        let archived = access_archived(&bytes);
1838
1839        let state = MockState::at_fork(ForkName::Phase0, 100, 32_000);
1840
1841        let result = apply(state, archived);
1842
1843        assert!(
1844            matches!(
1845                result,
1846                Err(Error::ForkMismatch {
1847                    state_fork: ForkName::Phase0,
1848                    delta_fork: ForkName::Capella,
1849                })
1850            ),
1851            "expected ForkMismatch, got {result:?}"
1852        );
1853    }
1854
1855    #[test]
1856    fn historical_roots_on_capella_rejected_by_apply() {
1857        let base = MockState::at_fork(ForkName::Phase0, 100, 32_000);
1858        let target = MockState::at_fork(ForkName::Phase0, 105, 32_000);
1859
1860        let source = MockSource {
1861            base: &base,
1862            target: &target,
1863        };
1864
1865        let phase0_delta = create(&source);
1866        assert!(phase0_delta.historical_roots.is_some());
1867
1868        let capella_base = MockState::at_fork(ForkName::Capella, 100, 32);
1869        let capella_target = MockState::at_fork(ForkName::Capella, 105, 32);
1870
1871        let capella_source = MockSource {
1872            base: &capella_base,
1873            target: &capella_target,
1874        };
1875
1876        let mut delta = create(&capella_source);
1877        assert!(delta.historical_roots.is_none());
1878
1879        // Corrupt the delta after create().
1880        delta.historical_roots = phase0_delta.historical_roots;
1881        assert!(delta.historical_roots.is_some());
1882
1883        let bytes = archive_delta(&delta);
1884        let archived = access_archived(&bytes);
1885
1886        let state = MockState::at_fork(ForkName::Capella, 100, 32);
1887
1888        let result = apply(state, archived);
1889
1890        assert!(
1891            matches!(
1892                result,
1893                Err(Error::InvalidFieldForFork {
1894                    field: "historical_roots",
1895                    fork: ForkName::Capella,
1896                })
1897            ),
1898            "expected InvalidFieldForFork, got {result:?}"
1899        );
1900    }
1901
1902    #[test]
1903    fn altair_field_on_phase0_rejected_by_apply() {
1904        let altair_base = MockState::at_fork(ForkName::Altair, 100, 32_000);
1905        let mut altair_target = MockState::at_fork(ForkName::Altair, 105, 32_000);
1906
1907        altair_target.previous_participation = Some(vec![1, 2, 3]);
1908
1909        let altair_source = MockSource {
1910            base: &altair_base,
1911            target: &altair_target,
1912        };
1913
1914        let altair_delta = create(&altair_source);
1915        assert!(altair_delta.previous_participation.is_some());
1916
1917        let phase0_base = MockState::at_fork(ForkName::Phase0, 100, 32_000);
1918        let phase0_target = MockState::at_fork(ForkName::Phase0, 105, 32_000);
1919
1920        let phase0_source = MockSource {
1921            base: &phase0_base,
1922            target: &phase0_target,
1923        };
1924
1925        let mut delta = create(&phase0_source);
1926        assert!(delta.previous_participation.is_none());
1927
1928        // Corrupt the delta after create().
1929        delta.previous_participation = altair_delta.previous_participation;
1930        assert!(delta.previous_participation.is_some());
1931
1932        let bytes = archive_delta(&delta);
1933        let archived = access_archived(&bytes);
1934
1935        let state = MockState::at_fork(ForkName::Phase0, 100, 32_000);
1936
1937        let result = apply(state, archived);
1938
1939        assert!(
1940            matches!(
1941                result,
1942                Err(Error::InvalidFieldForFork {
1943                    field: "previous_participation",
1944                    fork: ForkName::Phase0,
1945                })
1946            ),
1947            "expected InvalidFieldForFork, got {result:?}"
1948        );
1949    }
1950
1951    #[test]
1952    fn phase0_attestations_on_altair_rejected_by_apply() {
1953        let phase0_base = MockState::at_fork(ForkName::Phase0, 100, 32_000);
1954        let mut phase0_target = MockState::at_fork(ForkName::Phase0, 105, 32_000);
1955
1956        phase0_target.previous_epoch_attestations = Some(vec![0xAA]);
1957        phase0_target.current_epoch_attestations = Some(vec![0xBB]);
1958
1959        let phase0_source = MockSource {
1960            base: &phase0_base,
1961            target: &phase0_target,
1962        };
1963
1964        let phase0_delta = create(&phase0_source);
1965        assert!(phase0_delta.previous_epoch_attestations.is_some());
1966        assert!(phase0_delta.current_epoch_attestations.is_some());
1967
1968        let altair_base = MockState::at_fork(ForkName::Altair, 100, 32_000);
1969        let altair_target = MockState::at_fork(ForkName::Altair, 105, 32_000);
1970
1971        let altair_source = MockSource {
1972            base: &altair_base,
1973            target: &altair_target,
1974        };
1975
1976        let mut delta = create(&altair_source);
1977        assert!(delta.previous_epoch_attestations.is_none());
1978        assert!(delta.current_epoch_attestations.is_none());
1979
1980        // Corrupt the delta after create().
1981        delta.previous_epoch_attestations = phase0_delta.previous_epoch_attestations;
1982        delta.current_epoch_attestations = phase0_delta.current_epoch_attestations;
1983
1984        let bytes = archive_delta(&delta);
1985        let archived = access_archived(&bytes);
1986
1987        let state = MockState::at_fork(ForkName::Altair, 100, 32_000);
1988
1989        let result = apply(state, archived);
1990
1991        assert!(
1992            matches!(
1993                result,
1994                Err(Error::InvalidFieldForFork {
1995                    field: "previous_epoch_attestations",
1996                    fork: ForkName::Altair,
1997                })
1998            ),
1999            "expected InvalidFieldForFork, got {result:?}"
2000        );
2001    }
2002}