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 // Validate fork-specific fields.
720 macro_rules! validate_field {
721 ($field:ident, $fork:expr) => {
722 if delta.$field.is_some() && delta_fork < $fork {
723 return Err(Error::InvalidFieldForFork {
724 field: stringify!($field),
725 fork: delta_fork,
726 });
727 }
728 };
729 }
730
731 // Introduced in Altair+
732 validate_field!(previous_participation, ForkName::Altair);
733 validate_field!(current_participation, ForkName::Altair);
734 validate_field!(inactivity_scores, ForkName::Altair);
735 validate_field!(current_sync_committee, ForkName::Altair);
736 validate_field!(next_sync_committee, ForkName::Altair);
737
738 // Introduced in Capella+
739 validate_field!(historical_summaries, ForkName::Capella);
740
741 // Introduced in Electra+
742 validate_field!(pending_deposits, ForkName::Electra);
743 validate_field!(pending_partial_withdrawals, ForkName::Electra);
744 validate_field!(pending_consolidations, ForkName::Electra);
745
746 // Removed in Altair
747 validate_removed_field!(previous_epoch_attestations, ForkName::Altair);
748 validate_removed_field!(current_epoch_attestations, ForkName::Altair);
749
750 // Removed in Capella
751 validate_removed_field!(historical_roots, ForkName::Capella);
752
753 let base_slot = delta.base_slot.to_native();
754
755 *state.scalar_header_mut() = delta.scalar_header.as_slice().to_vec();
756
757 // Universal
758 balances::apply_balances_iter(state.balances_mut(), &delta.balances);
759 validators::apply_validators_iter(state.validators_mut(), &delta.validators);
760 recent_roots::apply_roots(base_slot, state.block_roots_mut(), &delta.block_roots);
761 recent_roots::apply_roots(base_slot, state.state_roots_mut(), &delta.state_roots);
762 randao_mixes::apply_randao(base_slot, state.randao_mixes_mut(), &delta.randao_mixes);
763 slashings::apply_slashings(state.slashings_mut(), &delta.slashings);
764 eth1_data_votes::apply_eth1_votes(state.eth1_data_votes_mut(), &delta.eth1_data_votes);
765
766 if let (Some(s), Some(d)) = (
767 state.historical_roots_mut(),
768 delta.historical_roots.as_ref(),
769 ) {
770 historical_log::apply_historical_log(s, d);
771 }
772
773 if let (Some(s), Some(d)) = (
774 state.previous_epoch_attestations_mut(),
775 delta.previous_epoch_attestations.as_ref(),
776 ) {
777 attestations::apply_attestations(s, d);
778 }
779
780 if let (Some(s), Some(d)) = (
781 state.current_epoch_attestations_mut(),
782 delta.current_epoch_attestations.as_ref(),
783 ) {
784 attestations::apply_attestations(s, d);
785 }
786
787 if let (Some(s), Some(d)) = (
788 state.previous_participation_mut(),
789 delta.previous_participation.as_ref(),
790 ) {
791 participation::apply_participation_iter(s, d);
792 }
793
794 if let (Some(s), Some(d)) = (
795 state.current_participation_mut(),
796 delta.current_participation.as_ref(),
797 ) {
798 participation::apply_participation_iter(s, d);
799 }
800
801 if let (Some(s), Some(d)) = (
802 state.inactivity_scores_mut(),
803 delta.inactivity_scores.as_ref(),
804 ) {
805 inactivity_scores::apply_inactivity(s, d);
806 }
807
808 if let (Some(s), Some(d)) = (
809 state.current_sync_committee_mut(),
810 delta.current_sync_committee.as_ref(),
811 ) {
812 sync_committee::apply_sync_committee(s, d);
813 }
814
815 if let (Some(s), Some(d)) = (
816 state.next_sync_committee_mut(),
817 delta.next_sync_committee.as_ref(),
818 ) {
819 sync_committee::apply_sync_committee(s, d);
820 }
821
822 if let (Some(s), Some(d)) = (
823 state.historical_summaries_mut(),
824 delta.historical_summaries.as_ref(),
825 ) {
826 historical_log::apply_historical_log(s, d);
827 }
828
829 if let (Some(s), Some(d)) = (
830 state.pending_deposits_mut(),
831 delta.pending_deposits.as_ref(),
832 ) {
833 pending_queue::apply_queue(s, d, PENDING_DEPOSIT_SSZ_SIZE);
834 }
835
836 if let (Some(s), Some(d)) = (
837 state.pending_partial_withdrawals_mut(),
838 delta.pending_partial_withdrawals.as_ref(),
839 ) {
840 pending_queue::apply_queue(s, d, PARTIAL_WITHDRAWAL_SSZ_SIZE);
841 }
842
843 if let (Some(s), Some(d)) = (
844 state.pending_consolidations_mut(),
845 delta.pending_consolidations.as_ref(),
846 ) {
847 pending_queue::apply_queue(s, d, PENDING_CONSOLIDATION_SSZ_SIZE);
848 }
849
850 Ok(state)
851}
852
853/// A mutable target for list-like collections of copyable values.
854///
855/// [`ListMutTarget`] provides the minimal interface required by the generic
856/// delta-application routines in this crate. It allows those routines to
857/// update consensus-state collections without requiring the collection to be
858/// backed by a contiguous `Vec`.
859///
860/// Implementations may use any underlying storage strategy, including
861/// contiguous buffers, persistent trees, or other client-specific data
862/// structures.
863///
864/// # Type parameter
865///
866/// `T` is the element type stored by the collection. It must implement
867/// [`Copy`] because delta application reads values from the encoded delta and
868/// writes them directly into the target collection.
869///
870/// # Required operations
871///
872/// An implementation must provide:
873///
874/// - [`len`](Self::len) to report the current number of elements.
875/// - [`get_mut`](Self::get_mut) to obtain mutable access to an existing
876/// element by index.
877/// - [`push`](Self::push) to append a newly decoded element.
878///
879/// # Example
880///
881/// The crate provides an implementation for `Vec<u64>` and `Vec<u8>`.
882///
883/// ```
884/// use eth_state_diff::ListMutTarget;
885///
886/// let mut values = vec![100u64, 200, 300];
887/// let target: &mut dyn ListMutTarget<u64> = &mut values;
888///
889/// *target.get_mut(1).unwrap() = 250;
890/// target.push(400);
891///
892/// assert_eq!(values, [100, 250, 300, 400]);
893/// ```
894///
895/// # Implementing for client-specific collections
896///
897/// Consensus clients with non-contiguous or tree-backed state can implement
898/// this trait to allow the generic delta algorithms to operate directly on
899/// their native collections, without first materializing the collection as a
900/// flat buffer.
901///
902/// Implementations should return `None` from [`get_mut`](Self::get_mut) when
903/// the requested index is outside the current collection bounds.
904pub trait ListMutTarget<T: Copy> {
905 /// Returns the current number of elements in the collection.
906 fn len(&self) -> usize;
907
908 /// Returns `true` if the collection contains no elements.
909 fn is_empty(&self) -> bool {
910 self.len() == 0
911 }
912
913 /// Returns mutable access to the element at `index`.
914 ///
915 /// Returns `None` if `index` is outside the current collection bounds.
916 fn get_mut(&mut self, index: usize) -> Option<&mut T>;
917
918 /// Appends `value` to the end of the collection.
919 fn push(&mut self, value: T);
920}
921
922impl ListMutTarget<u64> for Vec<u64> {
923 #[inline]
924 fn len(&self) -> usize {
925 self.len()
926 }
927
928 #[inline]
929 fn get_mut(&mut self, index: usize) -> Option<&mut u64> {
930 self.as_mut_slice().get_mut(index)
931 }
932
933 #[inline]
934 fn push(&mut self, value: u64) {
935 self.push(value);
936 }
937}
938
939impl ListMutTarget<u8> for Vec<u8> {
940 #[inline]
941 fn len(&self) -> usize {
942 self.len()
943 }
944
945 #[inline]
946 fn get_mut(&mut self, index: usize) -> Option<&mut u8> {
947 self.as_mut_slice().get_mut(index)
948 }
949
950 #[inline]
951 fn push(&mut self, value: u8) {
952 self.push(value);
953 }
954}
955
956const PENDING_DEPOSIT_SSZ_SIZE: usize = 192;
957const PARTIAL_WITHDRAWAL_SSZ_SIZE: usize = 24;
958const PENDING_CONSOLIDATION_SSZ_SIZE: usize = 16;