eth_state_diff/types.rs
1//! Core data structures used by [`eth_state_diff`].
2//!
3//! This module defines the serialized delta representations produced by the
4//! crate's diff algorithms.
5//!
6//! The structures in this module are deliberately independent of any specific
7//! Ethereum consensus-client implementation. They contain only primitive Rust
8//! values, byte buffers, and `rkyv`-archivable types. This allows a delta to be:
9//!
10//! 1. computed from one consensus state and another;
11//! 2. serialized with [`rkyv`];
12//! 3. optionally compressed with a general-purpose compressor such as zstd;
13//! 4. stored as an archival record; and
14//! 5. later deserialized and applied to another compatible state.
15//!
16//! # Delta model
17//!
18//! Most types in this module represent the transformation:
19//!
20//! ```text
21//! base state + delta -> target state
22//! ```
23//!
24//! Different consensus-state fields have different update patterns, so the
25//! crate uses specialized representations rather than one universal encoding.
26//! For example:
27//!
28//! - validator registries use field-level patches;
29//! - balances use packed tags and varint-encoded differences;
30//! - participation and inactivity scores use sparse updates;
31//! - FIFO-like lists use consumed-item counts and appended bytes;
32//! - circular buffers store only values written during the transition;
33//! - append-only historical logs use protocol-defined append counts; and
34//! - fields that change infrequently use unchanged/full-replacement variants.
35//!
36//! # Serialization
37//!
38//! All public delta structures derive [`rkyv::Archive`], [`rkyv::Serialize`],
39//! and [`rkyv::Deserialize`]. They are therefore suitable for use as the
40//! serialized representation of an archival state delta.
41//!
42//! The structures themselves do not perform compression. Applications can
43//! serialize a delta with `rkyv` and subsequently compress the resulting bytes
44//! using a compressor such as zstd.
45//!
46//! # Compatibility
47//!
48//! The delta types describe the encoding used by this crate. They should be
49//! treated as part of the crate's serialization format: changing field types,
50//! enum variants, or encoding invariants may affect the compatibility of
51//! previously stored deltas.
52//!
53//! [`eth_state_diff`]: crate
54
55use rkyv::{Archive, Deserialize, Serialize};
56
57/// Number of slots in an Ethereum consensus epoch.
58///
59/// Ethereum mainnet uses 32 slots per epoch.
60pub const SLOTS_PER_EPOCH: u64 = 32;
61
62/// Size, in bytes, of an SSZ-serialized `Validator` record.
63///
64/// The size corresponds to the Phase0 validator container represented by this
65/// crate. Validator records are treated as fixed-width byte sequences by the
66/// byte-oriented validator diff implementation.
67pub const VALIDATOR_SSZ_SIZE: usize = 121;
68
69/// Size, in bytes, of an SSZ-serialized historical root.
70///
71/// A root is a 32-byte hash.
72pub const HISTORICAL_ROOTS_SSZ_SIZE: usize = 32;
73
74/// Size, in bytes, of an SSZ-serialized historical summary.
75///
76/// Historical summaries represented by this crate occupy 64 bytes.
77pub const HISTORICAL_SUMMARIES_SSZ_SIZE: usize = 64;
78
79/// Minimum validator withdrawability delay used when reconstructing the
80/// `withdrawable_epoch` of a non-slashed validator.
81///
82/// When a validator is not slashed, its withdrawable epoch can be derived from
83/// its exit epoch and this protocol-defined delay rather than being stored as
84/// an independent delta field.
85pub const MIN_VALIDATOR_WITHDRAWABILITY_DELAY: u64 = 256;
86
87/// Identifies a validator field modified by a [`ValidatorPatch`].
88///
89/// Validator fields are encoded independently so that changing one field does
90/// not require storing the complete 121-byte validator SSZ record.
91///
92/// The `WithdrawableEpochSlashed` variant is used specifically for slashed
93/// validators. For non-slashed validators, `withdrawable_epoch` is derived
94/// deterministically from `exit_epoch` and
95/// [`MIN_VALIDATOR_WITHDRAWABILITY_DELAY`].
96#[derive(Archive, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
97pub enum ValidatorField {
98 /// Validator withdrawal credentials.
99 WithdrawalCredentials,
100
101 /// Validator effective balance.
102 EffectiveBalance,
103
104 /// Whether the validator has been slashed.
105 Slashed,
106
107 /// Epoch at which the validator became eligible for activation.
108 ActivationEligibilityEpoch,
109
110 /// Epoch at which the validator was activated.
111 ActivationEpoch,
112
113 /// Epoch at which the validator exited.
114 ExitEpoch,
115
116 /// Explicit withdrawable epoch for a slashed validator.
117 ///
118 /// Non-slashed validators derive this value from `exit_epoch` instead of
119 /// storing a separate patch.
120 WithdrawableEpochSlashed,
121}
122
123/// A modification to a single validator field.
124///
125/// Each patch identifies the validator by its registry index and contains the
126/// replacement value for exactly one [`ValidatorField`].
127///
128/// The `value` field contains the encoded representation expected by the
129/// corresponding field. The interpretation depends on [`ValidatorField`].
130///
131/// For example:
132///
133/// - `WithdrawalCredentials` contains 32 bytes;
134/// - integer epoch and balance fields contain their little-endian `u64`
135/// representation; and
136/// - `Slashed` contains a single byte.
137///
138/// This structure is intended to be produced by the validator diff algorithm
139/// rather than constructed manually.
140#[derive(Archive, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
141pub struct ValidatorPatch {
142 /// Zero-based validator registry index.
143 pub index: u32,
144
145 /// Validator field modified by this patch.
146 pub field: ValidatorField,
147
148 /// Replacement bytes for the selected field.
149 pub value: Vec<u8>,
150}
151
152/// Compact representation of the difference between two validator registries.
153///
154/// Existing validators are represented using field-level [`ValidatorPatch`]es.
155/// Validators present only in the target registry are stored as consecutive
156/// raw SSZ validator records in [`Self::appended_validators`].
157///
158/// This avoids rewriting complete validator records when only a small number
159/// of fields changed.
160///
161/// # Reconstruction
162///
163/// Applying all patches to the common validator range and then appending the
164/// records in `appended_validators` reconstructs the target registry.
165#[derive(Archive, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
166pub struct ValidatorsDiff {
167 /// Field-level modifications to existing validators.
168 pub patches: Vec<ValidatorPatch>,
169
170 /// Raw SSZ bytes of validators appended to the target registry.
171 ///
172 /// The bytes are stored consecutively, with each validator occupying
173 /// [`VALIDATOR_SSZ_SIZE`] bytes.
174 pub appended_validators: Vec<u8>,
175}
176
177/// Compact representation of the difference between two validator balance
178/// snapshots.
179///
180/// The representation combines four mechanisms:
181///
182/// - [`BitTagVec`] stores a two-bit operation for each balance;
183/// - `mode` stores the most common representable balance difference;
184/// - `varint_payload` stores mode-adjusted signed differences using
185/// zig-zag encoding; and
186/// - `target_values` stores balances that are more efficiently represented as
187/// absolute values.
188///
189/// Validators that exist only in the target snapshot are stored in
190/// `appended_balances`.
191///
192/// This representation is designed to serialize efficiently with `rkyv` and
193/// compress well with general-purpose compressors such as zstd.
194///
195/// # Reconstruction
196///
197/// Each tag determines how the corresponding target balance is reconstructed:
198///
199/// - unchanged balances require no payload;
200/// - zero balances are set directly to zero;
201/// - difference-encoded balances are reconstructed from `mode` and the
202/// corresponding varint; and
203/// - absolute values are read from `target_values`.
204#[derive(Archive, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
205pub struct BalancesDiff {
206 /// Packed two-bit operation tags for the common balance range.
207 pub tags: BitTagVec,
208
209 /// Most frequently occurring representable balance difference.
210 ///
211 /// Difference-encoded balances store their difference relative to this
212 /// value before zig-zag and varint encoding.
213 pub mode: i64,
214
215 /// Zig-zag encoded, varint-serialized balance differences.
216 pub varint_payload: Vec<u8>,
217
218 /// Absolute target balances for changes represented without a difference.
219 pub target_values: Vec<u64>,
220
221 /// Balances belonging to validators appended to the target vector.
222 pub appended_balances: Vec<u64>,
223}
224
225/// Compact delta representation for Ethereum participation flags.
226///
227/// The encoder selects between a dense all-zero representation and a sparse
228/// representation depending on the target vector.
229///
230/// This type is intended to be serialized using `rkyv`.
231#[derive(Archive, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
232pub enum ParticipationDiff {
233 /// Represents a target participation vector containing only zero flags.
234 ///
235 /// During application, the destination vector is cleared and resized to
236 /// `len`, with every entry initialized to zero.
237 AllZeros(u32),
238
239 /// Sparse representation containing only changed participation flags.
240 ///
241 /// The changed indices are stored as delta-varint encoded gaps in
242 /// `sparse_indices`. Each decoded index corresponds positionally to one
243 /// value in `new_values`.
244 Sparse {
245 /// Delta-varint encoded gaps between successive changed indices.
246 ///
247 /// Starting from index zero, each decoded gap advances the current
248 /// index to the next changed entry.
249 sparse_indices: Vec<u8>,
250
251 /// Replacement participation flag for each changed index.
252 ///
253 /// This vector has the same number of logical entries as the decoded
254 /// index sequence.
255 new_values: Vec<u8>,
256
257 /// Participation flags belonging to validators appended to the target
258 /// vector.
259 extension: Vec<u8>,
260 },
261}
262
263/// Compact delta representation for validator inactivity scores.
264///
265/// The encoder uses a dedicated all-zero representation when the target vector
266/// contains only zero scores. Otherwise, only changed scores are stored.
267///
268/// Scores belonging to validators appended to the target vector are stored
269/// separately in `extensions`.
270#[derive(Archive, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
271pub enum InactivityDiff {
272 /// Represents a target vector containing only zero inactivity scores.
273 ///
274 /// The destination vector is resized to `len` and initialized with zeroes
275 /// during application.
276 AllZeros(u32),
277
278 /// Sparse inactivity-score updates.
279 Sparse {
280 /// Zero-based indices of modified inactivity scores.
281 indices: Vec<u32>,
282
283 /// Replacement score corresponding positionally to each entry in
284 /// `indices`.
285 new_values: Vec<u64>,
286
287 /// Scores belonging to validators appended to the target vector.
288 extensions: Vec<u64>,
289 },
290}
291
292/// Sequence of roots written while advancing through a circular root buffer.
293///
294/// Roots are stored in chronological slot order. The slot used to reconstruct
295/// the first entry is supplied separately to the apply function.
296///
297/// The buffer capacity is intentionally not serialized into the delta because
298/// it is already known by the destination state.
299#[derive(Archive, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
300pub struct RootsDiff {
301 /// Roots written for the represented slot range, in chronological order.
302 ///
303 /// Each entry is a 32-byte consensus root.
304 pub roots: Vec<[u8; 32]>,
305}
306
307/// Sparse updates for an Ethereum slashing ring buffer.
308///
309/// Each update identifies a ring-buffer index and the replacement slashing
310/// total for that index.
311///
312/// Entries that do not appear in this vector are left unchanged during
313/// application.
314#[derive(Archive, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
315pub struct SlashingsDiff {
316 /// Pairs of `(ring_index, new_slashing_amount)`.
317 ///
318 /// The index is stored as `u16`, which is sufficient for the consensus
319 /// slashing-vector capacity represented by this crate.
320 pub updates: Vec<(u16, u64)>,
321}
322
323/// Sequence of RANDAO mixes written while advancing through epochs.
324///
325/// Mixes are stored in chronological epoch order. The destination ring-buffer
326/// capacity is supplied separately during application.
327///
328/// The capacity is intentionally omitted from the serialized representation
329/// because it is a property of the destination consensus state.
330#[derive(Archive, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
331pub struct RandaoDiff {
332 /// RANDAO mixes written during the represented epoch range.
333 ///
334 /// Each mix is a 32-byte value.
335 pub mixes: Vec<[u8; 32]>,
336}
337
338/// Delta representation for an Ethereum Eth1 data vote list.
339///
340/// The list normally grows by appending votes within an Eth1 voting period.
341/// When the voting period resets, the representation switches to
342/// [`Eth1DataVotesDiff::ResetAndAppend`].
343#[derive(Archive, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
344pub enum Eth1DataVotesDiff {
345 /// Additional serialized vote bytes appended to the existing list.
346 Append(Vec<u8>),
347
348 /// The vote list was reset and replaced with the supplied serialized bytes.
349 ///
350 /// Application clears the existing list before appending these bytes.
351 ResetAndAppend(Vec<u8>),
352}
353
354/// Universal delta representation for SSZ-serialized queue-like lists.
355///
356/// The representation supports both:
357///
358/// - FIFO transitions, where items are consumed from the front and appended at
359/// the back; and
360/// - safe fallback to complete replacement when the FIFO assumptions cannot be
361/// established.
362///
363/// The FIFO representation stores raw SSZ bytes rather than deserializing
364/// individual queue items.
365#[derive(Archive, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
366pub enum QueueDiff {
367 /// Validated FIFO transition.
368 ///
369 /// The base queue loses `consumed_count` items from its front and receives
370 /// `appended_items` at its back.
371 Fifo {
372 /// Number of complete queue items consumed from the front.
373 consumed_count: u32,
374
375 /// Raw SSZ bytes of items appended to the target queue.
376 appended_items: Vec<u8>,
377 },
378
379 /// Complete replacement of the serialized target queue.
380 ///
381 /// This is used when the queue cannot safely be represented as a FIFO
382 /// transition.
383 FullReplacement(Vec<u8>),
384}
385
386/// Delta representation for an Ethereum sync committee.
387///
388/// Sync committees are stable for a sync committee period. Consequently, most
389/// state-diff windows can represent the committee using
390/// [`SyncCommitteeDiff::Unchanged`].
391///
392/// When the committee changes, the complete serialized target committee is
393/// stored as a replacement rather than attempting to encode individual member
394/// changes.
395#[derive(Archive, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
396pub enum SyncCommitteeDiff {
397 /// The serialized sync committee did not change.
398 Unchanged,
399
400 /// Complete serialized SSZ representation of the target sync committee.
401 FullReplacement(Vec<u8>),
402}
403
404/// Delta representation for an append-only historical consensus log.
405///
406/// Historical roots and historical summaries are appended according to
407/// protocol-defined slot intervals. The diff algorithm can therefore determine
408/// how many entries should have been appended from the slot transition rather
409/// than comparing the complete base and target buffers.
410#[derive(Archive, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
411pub enum HistoricalLogDiff {
412 /// No historical-log boundary was crossed.
413 Unchanged,
414
415 /// Raw SSZ bytes of the historical items appended during the transition.
416 ///
417 /// The item width depends on the historical log being represented.
418 Append(Vec<u8>),
419}
420
421/// Delta representation for Phase0 pending attestation lists.
422///
423/// The representation supports both append-only transitions and complete
424/// replacement. This matches the two update patterns used by
425/// `current_epoch_attestations` and `previous_epoch_attestations`.
426#[derive(Archive, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
427pub enum AttestationsDiff {
428 /// The serialized attestation list is unchanged.
429 Unchanged,
430
431 /// Serialized attestations appended to the existing list.
432 Append(Vec<u8>),
433
434 /// Complete serialized replacement of the target attestation list.
435 FullReplacement(Vec<u8>),
436}
437
438/// Packed two-bit operation tags used by [`BalancesDiff`].
439///
440/// Four tags are stored in each byte, with the tag for index `i` occupying
441/// bits `((i % 4) * 2)..((i % 4) * 2 + 2)`.
442///
443/// The four tag values are:
444///
445/// | Tag | Meaning |
446/// | --- | --- |
447/// | `00` | Balance is unchanged |
448/// | `01` | Replace with an absolute target value |
449/// | `10` | Set balance to zero |
450/// | `11` | Apply an encoded balance difference |
451///
452/// A newly created [`BitTagVec`] is initialized entirely with
453/// [`SET_NO_CHANGE`] tags.
454///
455/// # Storage
456///
457/// A vector containing *n* logical tags requires:
458///
459/// ```text
460/// ceil(n / 4)
461/// ```
462///
463/// bytes of backing storage.
464///
465/// # Example
466///
467/// ```
468/// use eth_state_diff::types::{BitTagVec, SET_TO_DIFF, SET_TO_ZERO};
469///
470/// let mut tags = BitTagVec::new(6);
471///
472/// tags.set(0, SET_TO_DIFF);
473/// tags.set(4, SET_TO_ZERO);
474///
475/// assert_eq!(tags.get(0), SET_TO_DIFF);
476/// assert_eq!(tags.get(1), 0);
477/// assert_eq!(tags.get(4), SET_TO_ZERO);
478/// ```
479#[derive(Eq, PartialEq, Debug, Clone, Default, Archive, Deserialize, Serialize)]
480pub struct BitTagVec {
481 /// Packed storage containing four two-bit tags per byte.
482 pub data: Vec<u8>,
483
484 /// Number of logical tags represented by `data`.
485 ///
486 /// This may be smaller than `data.len() * 4` because the final byte can
487 /// contain unused tag positions.
488 pub len: u32,
489}
490
491/// Tag indicating that the corresponding balance is unchanged.
492pub const SET_NO_CHANGE: u8 = 0b00;
493
494/// Tag indicating that the corresponding balance is replaced with zero.
495pub const SET_TO_ZERO: u8 = 0b10;
496
497/// Tag indicating that the corresponding balance is reconstructed by applying
498/// an encoded signed difference.
499pub const SET_TO_DIFF: u8 = 0b11;
500
501/// Tag indicating that the corresponding balance is replaced with an absolute
502/// target value.
503pub const SET_TO_TARGET_VALUE: u8 = 0b01;
504
505impl BitTagVec {
506 /// Creates a zero-initialized tag vector containing `len` logical entries.
507 ///
508 /// Every entry initially has the [`SET_NO_CHANGE`] tag.
509 ///
510 /// # Complexity
511 ///
512 /// O(len / 4) time and memory.
513 pub fn new(len: usize) -> Self {
514 let bytes = len.div_ceil(4);
515
516 Self {
517 data: vec![0; bytes],
518 len: u32::try_from(len).expect("BitTagVec length exceeds u32::MAX"),
519 }
520 }
521
522 /// Sets the two-bit tag at `idx`.
523 ///
524 /// Only the lowest two bits of `tag` are used.
525 ///
526 /// # Panics
527 ///
528 /// Panics if `idx >= self.len`.
529 ///
530 /// # Complexity
531 ///
532 /// O(1).
533 #[inline]
534 pub fn set(&mut self, idx: usize, tag: u8) {
535 assert!(
536 idx < self.len as usize,
537 "tag index {idx} out of bounds for length {}",
538 self.len
539 );
540
541 let byte = idx / 4;
542 let shift = (idx % 4) * 2;
543
544 // `idx` is validated above, so `byte` is mathematically guaranteed to be in bounds.
545 *self
546 .data
547 .get_mut(byte)
548 .expect("idx < self.len implies byte < self.data.len()") |= (tag & 0b11) << shift;
549 }
550
551 /// Returns the two-bit tag stored at `idx`.
552 ///
553 /// # Panics
554 ///
555 /// Panics if `idx >= self.len`.
556 ///
557 /// # Complexity
558 ///
559 /// O(1).
560 #[inline]
561 pub fn get(&self, idx: usize) -> u8 {
562 assert!(
563 idx < self.len as usize,
564 "tag index {idx} out of bounds for length {}",
565 self.len
566 );
567
568 let byte = idx / 4;
569 let shift = (idx % 4) * 2;
570
571 let value = *self
572 .data
573 .get(byte)
574 .expect("idx < self.len implies byte < self.data.len()");
575
576 (value >> shift) & 0b11
577 }
578}