eth_state_diff/pending_queue.rs
1//! Universal delta encoding for SSZ lists that behave as FIFO queues.
2//!
3//! This module handles both pure FIFO queues (e.g., consolidations, withdrawals)
4//! and logically-FIFO queues with rare reordering (e.g., deposits).
5//!
6//! It guarantees correctness by strictly validating item boundaries during overlap
7//! detection and ensuring the remaining base queue matches the target prefix exactly.
8
9use crate::types::{ArchivedQueueDiff, QueueDiff};
10
11/// Finds the first occurrence of an SSZ-encoded queue item within a byte slice,
12/// strictly checking only at valid SSZ item boundaries to prevent false positives.
13fn find_chunk_aligned(haystack: &[u8], needle: &[u8], item_ssz_size: usize) -> Option<usize> {
14 if needle.len() != item_ssz_size {
15 return None;
16 }
17 haystack
18 .chunks_exact(item_ssz_size)
19 .position(|chunk| chunk == needle)
20 .map(|idx| idx * item_ssz_size)
21}
22
23/// Computes the delta between two serialized queue lists.
24///
25/// # Algorithm
26///
27/// 1. Locates the first item of the target queue within the base queue **only at
28/// valid item boundaries**.
29/// 2. **Strictly validates** that the remaining items in the base queue are
30/// exactly identical to the prefix of the target queue.
31/// 3. If valid, emits `QueueDiff::Fifo`. If invalid, emits `QueueDiff::FullReplacement`.
32pub fn diff_queue(base_ssz: &[u8], target_ssz: &[u8], item_ssz_size: usize) -> QueueDiff {
33 // Edge case: target is empty, everything was consumed.
34 if target_ssz.is_empty() {
35 return QueueDiff::Fifo {
36 consumed_count: base_ssz.len() as u32 / item_ssz_size as u32,
37 appended_items: Vec::new(),
38 };
39 }
40
41 // Edge case: base is empty, everything is an append.
42 if base_ssz.is_empty() {
43 return QueueDiff::Fifo {
44 consumed_count: 0,
45 appended_items: target_ssz.to_vec(),
46 };
47 }
48
49 let target_head = &target_ssz[..item_ssz_size];
50
51 match find_chunk_aligned(base_ssz, target_head, item_ssz_size) {
52 Some(byte_offset) => {
53 let remaining_base_bytes = &base_ssz[byte_offset..];
54 let expected_target_prefix_len = remaining_base_bytes.len();
55
56 // CRITICAL VALIDATION: Ensure the rest of the base queue exactly matches
57 // the prefix of the target queue. This catches any reordering edge cases.
58 if expected_target_prefix_len <= target_ssz.len()
59 && &target_ssz[..expected_target_prefix_len] == remaining_base_bytes
60 {
61 let consumed_count = (byte_offset / item_ssz_size) as u32;
62 let appended_items = target_ssz[expected_target_prefix_len..].to_vec();
63
64 QueueDiff::Fifo {
65 consumed_count,
66 appended_items,
67 }
68 } else {
69 // Overlap found, but the tails don't match. Fallback to safe replacement.
70 QueueDiff::FullReplacement(target_ssz.to_vec())
71 }
72 }
73 None => {
74 // The first item of the target does not exist anywhere in the base.
75 // This happens if the base was entirely consumed. Fallback to safe replacement.
76 QueueDiff::FullReplacement(target_ssz.to_vec())
77 }
78 }
79}
80
81/// Applies a universal queue delta in place.
82///
83/// # Complexity
84///
85/// - `Fifo`: O(consumed bytes + appended bytes)
86/// - `FullReplacement`: O(target queue size)
87pub fn apply_queue(base: &mut Vec<u8>, delta: &ArchivedQueueDiff, item_ssz_size: usize) {
88 let delta: QueueDiff = rkyv::deserialize::<QueueDiff, rkyv::rancor::Error>(delta)
89 .expect("Failed to deserialize PendingDepositsDiff");
90
91 match delta {
92 QueueDiff::Fifo {
93 consumed_count,
94 appended_items,
95 } => {
96 let bytes_to_drain = consumed_count as usize * item_ssz_size;
97 if bytes_to_drain > base.len() {
98 base.clear();
99 } else {
100 base.drain(..bytes_to_drain);
101 }
102
103 if !appended_items.is_empty() {
104 base.extend_from_slice(&appended_items);
105 }
106 }
107 QueueDiff::FullReplacement(replacement) => {
108 base.clear();
109 if !replacement.is_empty() {
110 base.extend_from_slice(&replacement);
111 }
112 }
113 }
114}