Skip to main content

eth_state_diff/
eth1_data_votes.rs

1//! Delta encoding for the Eth1 data vote list.
2//!
3//! Eth1 data votes accumulate during an Eth1 voting period and are reset when
4//! the voting period changes. This module represents those transitions using
5//! either an append-only update or a complete replacement.
6//!
7//! The delta operates directly on the serialized SSZ representation and does
8//! not deserialize individual Eth1 data values.
9
10use crate::types::{ArchivedEth1DataVotesDiff, Eth1DataVotesDiff};
11
12/// Computes a delta between two serialized Eth1 data vote lists.
13///
14/// If the target preserves the complete serialized base list, only the bytes
15/// appended to the base are stored in [`Eth1DataVotesDiff::Append`].
16/// Otherwise, the complete target list is stored in
17/// [`Eth1DataVotesDiff::ResetAndAppend`].
18///
19/// This covers both accumulation of votes during an Eth1 voting period and
20/// replacement of the vote list when the voting period changes.
21///
22/// This function operates directly on serialized SSZ bytes and does not
23/// deserialize individual votes.
24///
25/// # Arguments
26///
27/// * `base` - Serialized SSZ representation of the current vote list.
28/// * `target` - Serialized SSZ representation of the target vote list.
29///
30/// # Returns
31///
32/// A delta representing the transition from `base` to `target`.
33///
34/// # Complexity
35///
36/// O(n), where *n* is the length of `base`, for the prefix comparison.
37/// Additional space is proportional to the selected delta payload.
38///
39/// # Example
40///
41/// ```
42/// use eth_state_diff::eth1_data_votes::diff_eth1_votes;
43/// use eth_state_diff::types::Eth1DataVotesDiff;
44///
45/// let base = b"AAAA";
46/// let target = b"AAAABBBB";
47///
48/// let delta = diff_eth1_votes(base, target);
49///
50/// assert_eq!(delta, Eth1DataVotesDiff::Append(b"BBBB".to_vec()));
51/// ```
52pub fn diff_eth1_votes(base: &[u8], target: &[u8]) -> Eth1DataVotesDiff {
53    if target.starts_with(base) {
54        let appended_votes = &target[base.len()..];
55        Eth1DataVotesDiff::Append(appended_votes.to_vec())
56    } else {
57        Eth1DataVotesDiff::ResetAndAppend(target.to_vec())
58    }
59}
60
61/// Applies an Eth1 data vote delta to a serialized vote list in place.
62///
63/// [`Eth1DataVotesDiff::Append`] preserves the existing bytes and appends the
64/// delta payload.
65///
66/// [`Eth1DataVotesDiff::ResetAndAppend`] clears the existing vote list before
67/// writing the replacement payload.
68///
69/// The delta is assumed to have been produced for the current base state.
70/// This function does not validate that an append delta's base bytes match
71/// the state being modified.
72///
73/// # Arguments
74///
75/// * `base` - Serialized SSZ vote list to modify.
76/// * `delta` - Archived delta to apply.
77///
78/// # Complexity
79///
80/// - [`Eth1DataVotesDiff::Append`]: O(k), where *k* is the number of appended
81///   bytes.
82/// - [`Eth1DataVotesDiff::ResetAndAppend`]: O(k), where *k* is the size of the
83///   replacement payload.
84///
85/// # Example
86///
87/// ```
88/// use eth_state_diff::eth1_data_votes::{apply_eth1_votes, diff_eth1_votes};
89/// use eth_state_diff::types::Eth1DataVotesDiff;
90///
91/// let mut base = b"AAAA".to_vec();
92/// let delta = diff_eth1_votes(&base, b"AAAABBBB");
93///
94/// // The delta is generated locally, so its contents can be applied directly.
95/// match delta {
96///     Eth1DataVotesDiff::Append(appended_votes) => {
97///         base.extend_from_slice(&appended_votes);
98///     }
99///     Eth1DataVotesDiff::ResetAndAppend(replacement) => {
100///         base.clear();
101///         base.extend_from_slice(&replacement);
102///     }
103/// }
104///
105/// assert_eq!(base, b"AAAABBBB");
106/// ```
107///
108/// [`Eth1DataVotesDiff`]: crate::types::Eth1DataVotesDiff
109pub fn apply_eth1_votes(base: &mut Vec<u8>, delta: &ArchivedEth1DataVotesDiff) {
110    match delta {
111        ArchivedEth1DataVotesDiff::Append(appended_votes) => {
112            base.extend_from_slice(appended_votes.as_slice());
113        }
114        ArchivedEth1DataVotesDiff::ResetAndAppend(replacement) => {
115            base.clear();
116            base.extend_from_slice(replacement.as_slice());
117        }
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124    use crate::types::ArchivedEth1DataVotesDiff;
125
126    /// Helper to serialize, archive, and apply a delta.
127    fn assert_roundtrip(base: &[u8], target: &[u8]) {
128        let delta = diff_eth1_votes(base, target);
129
130        let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&delta).expect("test setup: serialize");
131        let archived = rkyv::access::<ArchivedEth1DataVotesDiff, rkyv::rancor::Error>(&bytes)
132            .expect("test setup: failed to access archived eth1 votes delta");
133
134        let mut reconstructed = base.to_vec();
135        apply_eth1_votes(&mut reconstructed, archived);
136
137        assert_eq!(reconstructed, target);
138    }
139
140    #[test]
141    fn test_diff_apply_pure_append() {
142        let base = b"AAAA";
143        let target = b"AAAABBBB";
144
145        let delta = diff_eth1_votes(base, target);
146        assert!(matches!(delta, Eth1DataVotesDiff::Append(ref v) if v == b"BBBB"));
147
148        assert_roundtrip(base, target);
149    }
150
151    #[test]
152    fn test_diff_apply_identical_lists() {
153        let base = b"AAAA";
154        let target = b"AAAA";
155
156        let delta = diff_eth1_votes(base, target);
157        // Starts with is true, slice is empty
158        assert!(matches!(delta, Eth1DataVotesDiff::Append(ref v) if v.is_empty()));
159
160        assert_roundtrip(base, target);
161    }
162
163    #[test]
164    fn test_diff_apply_empty_base() {
165        let base = b"";
166        let target = b"BBBB";
167
168        let delta = diff_eth1_votes(base, target);
169        // Empty string is a prefix of everything
170        assert!(matches!(delta, Eth1DataVotesDiff::Append(ref v) if v == b"BBBB"));
171
172        assert_roundtrip(base, target);
173    }
174
175    #[test]
176    fn test_diff_apply_complete_replacement() {
177        let base = b"AAAA";
178        let target = b"CCCC";
179
180        let delta = diff_eth1_votes(base, target);
181        assert!(matches!(delta, Eth1DataVotesDiff::ResetAndAppend(ref v) if v == b"CCCC"));
182
183        assert_roundtrip(base, target);
184    }
185
186    #[test]
187    fn test_diff_apply_target_shorter_than_base() {
188        let base = b"AAAABBBB";
189        let target = b"AA";
190
191        let delta = diff_eth1_votes(base, target);
192        assert!(matches!(delta, Eth1DataVotesDiff::ResetAndAppend(ref v) if v == b"AA"));
193
194        assert_roundtrip(base, target);
195    }
196
197    #[test]
198    fn test_diff_apply_historical_vote_modified() {
199        // CRITICAL EDGE CASE: The prefix matches up to a point, but an older
200        // vote in the middle of the base was modified. `starts_with` returns false,
201        // forcing a full replacement to prevent corruption.
202        let base = b"AAAA-XXXX";
203        let target = b"AAAA-YYYY";
204
205        let delta = diff_eth1_votes(base, target);
206        assert!(matches!(delta, Eth1DataVotesDiff::ResetAndAppend(ref v) if v == b"AAAA-YYYY"));
207
208        assert_roundtrip(base, target);
209    }
210}