eth_state_diff/attestations.rs
1//! Delta encoding for Phase 0 pending attestation lists.
2//!
3//! Phase 0 maintains two attestation lists with different update semantics:
4//!
5//! - `current_epoch_attestations` grows by appending new attestations during an
6//! epoch.
7//! - `previous_epoch_attestations` is replaced when the epoch transitions.
8//!
9//! This module provides a single diff function that selects the most compact
10//! supported representation for both cases. If the target list starts with the
11//! exact byte sequence of the base list, only the appended bytes are stored.
12//! Otherwise, the complete target list is stored as a full replacement.
13//!
14//! Both representations use [`AttestationsDiff`] and can be applied with
15//! [`apply_attestations`] without deserializing individual attestations.
16
17use crate::types::{ArchivedAttestationsDiff, AttestationsDiff};
18
19/// Computes a compact delta between two serialized SSZ attestation lists.
20///
21/// The function automatically selects between the supported delta
22/// representations:
23///
24/// - If the lists are identical, [`AttestationsDiff::Unchanged`] is returned.
25/// - If the target list starts with the exact byte sequence of the base list,
26/// only the trailing bytes are stored in [`AttestationsDiff::Append`].
27/// - Otherwise, the complete target list is stored in
28/// [`AttestationsDiff::FullReplacement`].
29///
30/// This covers both append-only updates, such as growth of
31/// `current_epoch_attestations`, and epoch-boundary replacement of
32/// `previous_epoch_attestations`.
33///
34/// An empty base list is naturally represented as an append of the complete
35/// target list.
36///
37/// # Arguments
38///
39/// * `base_ssz` - Serialized SSZ representation of the base attestation list.
40/// * `target_ssz` - Serialized SSZ representation of the target attestation list.
41///
42/// # Returns
43///
44/// A compact [`AttestationsDiff`] representing the transition from `base_ssz`
45/// to `target_ssz`.
46///
47/// # Complexity
48///
49/// O(n) time, where *n* is the length of `base_ssz`, for the prefix comparison.
50/// Additional space is proportional to the selected delta payload.
51///
52/// # Example
53///
54/// ```
55/// # use eth_state_diff::attestations::diff_attestations;
56/// # use eth_state_diff::types::AttestationsDiff;
57///
58/// let base = b"AAAA";
59///
60/// // Append case.
61/// let target = b"AAAABBBB";
62/// assert_eq!(
63/// diff_attestations(base, target),
64/// AttestationsDiff::Append(b"BBBB".to_vec())
65/// );
66///
67/// // Replacement case.
68/// let target = b"CCCC";
69/// assert_eq!(
70/// diff_attestations(base, target),
71/// AttestationsDiff::FullReplacement(b"CCCC".to_vec())
72/// );
73/// ```
74pub fn diff_attestations(base_ssz: &[u8], target_ssz: &[u8]) -> AttestationsDiff {
75 if base_ssz == target_ssz {
76 return AttestationsDiff::Unchanged;
77 }
78
79 if target_ssz.starts_with(base_ssz) {
80 AttestationsDiff::Append(target_ssz[base_ssz.len()..].to_vec())
81 } else {
82 AttestationsDiff::FullReplacement(target_ssz.to_vec())
83 }
84}
85
86/// Applies an attestation delta to a serialized SSZ list in place.
87///
88/// [`AttestationsDiff::Unchanged`] leaves the base buffer untouched.
89///
90/// [`AttestationsDiff::Append`] appends the serialized bytes stored in the
91/// delta to the existing buffer.
92///
93/// [`AttestationsDiff::FullReplacement`] clears the existing buffer and
94/// replaces it with the serialized target bytes.
95///
96/// # Complexity
97///
98/// - [`AttestationsDiff::Unchanged`][]: O(1).
99/// - [`AttestationsDiff::Append`]: O(k), where *k* is the number of appended bytes.
100/// - [`AttestationsDiff::FullReplacement`]: O(n), where *n* is the size of the replacement list.
101///
102/// # Example
103///
104/// ```
105/// # use eth_state_diff::attestations::{apply_attestations, diff_attestations};
106/// # use eth_state_diff::types::{ArchivedAttestationsDiff, AttestationsDiff};
107///
108/// let mut base = b"AAAA".to_vec();
109/// let delta = diff_attestations(&base, b"AAAABBBB");
110///
111/// let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&delta).unwrap();
112/// let archived = unsafe { rkyv::access_unchecked::<ArchivedAttestationsDiff>(&bytes) };
113///
114/// apply_attestations(&mut base, archived);
115///
116/// assert_eq!(base, b"AAAABBBB");
117/// ```
118pub fn apply_attestations(base: &mut Vec<u8>, delta: &ArchivedAttestationsDiff) {
119 match delta {
120 ArchivedAttestationsDiff::Unchanged => {}
121 ArchivedAttestationsDiff::Append(bytes) => {
122 base.extend_from_slice(bytes.as_slice());
123 }
124 ArchivedAttestationsDiff::FullReplacement(bytes) => {
125 base.clear();
126 base.extend_from_slice(bytes.as_slice());
127 }
128 }
129}