Skip to main content

eth_state_diff/
historical_log.rs

1//! Delta encoding for append-only historical logs.
2//!
3//! Both `historical_roots` (Phase0 through Bellatrix) and
4//! `historical_summaries` (Capella and later) are append-only logs that grow
5//! according to the historical-period boundary defined by the consensus
6//! protocol.
7//!
8//! This module uses the base and target slots to determine how many historical
9//! items must have been appended. It therefore avoids comparing the complete
10//! serialized log contents.
11//!
12//! The resulting delta contains only the newly appended SSZ items.
13
14use crate::types::{ArchivedHistoricalLogDiff, HistoricalLogDiff};
15
16/// Number of slots in one historical period.
17const SLOTS_PER_HISTORICAL_PERIOD: u64 = 8192;
18
19/// Calculates the expected number of historical log items at a given slot.
20///
21/// When `activation_slot` is provided, the log is considered inactive through
22/// that slot and begins accumulating items afterwards. When no activation slot
23/// is provided, the calculation starts from genesis.
24///
25/// This helper follows the historical-period calculation used by the
26/// corresponding consensus state field.
27#[inline]
28fn calculate_log_count(slot: u64, activation_slot: Option<u64>) -> u64 {
29    match activation_slot {
30        Some(act_slot) if slot <= act_slot => 0,
31        Some(act_slot) => (slot - act_slot) / SLOTS_PER_HISTORICAL_PERIOD,
32        None => (slot + 1) / SLOTS_PER_HISTORICAL_PERIOD,
33    }
34}
35
36/// Computes a delta between two historical log states.
37///
38/// The number of appended items is derived from `base_slot` and `target_slot`
39/// using the protocol-defined historical period rather than by comparing the
40/// serialized base and target logs.
41///
42/// Only the newly appended portion of `target_ssz` is stored in the returned
43/// [`HistoricalLogDiff`].
44///
45/// # Arguments
46///
47/// * `base_slot` - Slot corresponding to the base state.
48/// * `target_slot` - Slot corresponding to the target state.
49/// * `target_ssz` - Complete serialized SSZ representation of the target
50///   historical log.
51/// * `item_ssz_size` - Serialized SSZ size of one historical log item.
52/// * `activation_slot` - Optional slot at which the historical log becomes
53///   active. This is used for logs introduced by a later fork.
54///
55/// # Returns
56///
57/// - [`HistoricalLogDiff::Unchanged`] if the protocol-derived log length has
58///   not increased between the two slots.
59/// - [`HistoricalLogDiff::Append`] containing the newly appended serialized
60///   items otherwise.
61///
62/// # Panics
63///
64/// This function does not intentionally panic in release builds. In debug
65/// builds, it asserts that `target_ssz` contains enough bytes for the number
66/// of items derived from the slot calculation.
67///
68/// The caller must provide a non-zero `item_ssz_size` and a target SSZ buffer
69/// consistent with the protocol-derived log length.
70///
71/// # Complexity
72///
73/// O(k) time and O(k) additional space, where *k* is the number of newly
74/// appended serialized bytes. The number of existing log entries does not
75/// affect the computation.
76///
77/// # Example
78///
79/// ```
80/// use eth_state_diff::historical_log::diff_historical_log;
81/// use eth_state_diff::types::HistoricalLogDiff;
82///
83/// const ITEM_SIZE: usize = 32;
84///
85/// // One historical period has elapsed, so one item is appended.
86/// let target = vec![0u8; ITEM_SIZE];
87///
88/// let delta = diff_historical_log(
89///     0,
90///     8192,
91///     &target,
92///     ITEM_SIZE,
93///     None,
94/// );
95///
96/// assert_eq!(
97///     delta,
98///     HistoricalLogDiff::Append(target),
99/// );
100/// ```
101pub fn diff_historical_log(
102    base_slot: u64,
103    target_slot: u64,
104    target_ssz: &[u8],
105    item_ssz_size: usize,
106    activation_slot: Option<u64>,
107) -> HistoricalLogDiff {
108    let base_count = calculate_log_count(base_slot, activation_slot);
109    let target_count = calculate_log_count(target_slot, activation_slot);
110
111    let items_to_append = (target_count - base_count) as usize;
112
113    if items_to_append == 0 {
114        return HistoricalLogDiff::Unchanged;
115    }
116
117    let required_bytes = items_to_append * item_ssz_size;
118
119    debug_assert!(
120        target_ssz.len() >= required_bytes,
121        "Historical log math expects {required_bytes} bytes, but target_ssz is too short",
122    );
123
124    let start_byte = target_ssz.len() - required_bytes;
125
126    let appended_bytes = target_ssz.get(start_byte..).expect(
127        "Historical log SSZ buffer is too short for the items derived from slot math. \
128     Expected at least {required_bytes} bytes for the new items, but the buffer only has {target_ssz.len()} bytes.",
129    );
130
131    HistoricalLogDiff::Append(appended_bytes.to_vec())
132}
133
134/// Applies a historical log delta to a serialized historical log in place.
135///
136/// [`HistoricalLogDiff::Unchanged`] leaves the existing log untouched.
137///
138/// [`HistoricalLogDiff::Append`] appends the serialized historical items
139/// contained in the delta to the existing log.
140///
141/// The delta is assumed to have been generated for the supplied base state.
142/// This function does not validate that the base log corresponds to the
143/// state from which the delta was created.
144///
145/// # Complexity
146///
147/// - [`HistoricalLogDiff::Unchanged`][]: O(1).
148/// - [`HistoricalLogDiff::Append`]: O(k), where *k* is the number of appended
149///   bytes.
150///
151/// # Example
152///
153/// ```
154/// use eth_state_diff::historical_log::{
155///     apply_historical_log,
156///     diff_historical_log,
157/// };
158/// use eth_state_diff::types::ArchivedHistoricalLogDiff;
159///
160/// const ITEM_SIZE: usize = 4;
161///
162/// let mut base = vec![1u8, 2, 3, 4];
163/// let target = vec![1u8, 2, 3, 4, 5, 6, 7, 8];
164///
165/// let delta = diff_historical_log(
166///     8191,
167///     16384,
168///     &target,
169///     ITEM_SIZE,
170///     None,
171/// );
172///
173/// let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&delta).unwrap();
174/// let archived = unsafe {
175///     rkyv::access_unchecked::<ArchivedHistoricalLogDiff>(&bytes)
176/// };
177///
178/// apply_historical_log(&mut base, archived);
179///
180/// assert_eq!(base, target);
181/// ```
182pub fn apply_historical_log(base: &mut Vec<u8>, delta: &ArchivedHistoricalLogDiff) {
183    match delta {
184        ArchivedHistoricalLogDiff::Unchanged => {}
185
186        ArchivedHistoricalLogDiff::Append(bytes) => {
187            base.extend_from_slice(bytes.as_slice());
188        }
189    }
190}