1use crate::{SnapGeneration, SnapPhase, SnapSyncError};
14use alloy_eip7928::BAL_RETENTION_PERIOD_SLOTS;
15use reth_primitives_traits::AlloyBlockHeader;
16use reth_storage_api::HeaderProvider;
17
18const DEFAULT_HEAD_DISTANCE: u64 = 64;
20
21const SERVED_STATE_WINDOW: u64 = 128;
24
25const DEFAULT_ADVANCE_AFTER: u64 = SERVED_STATE_WINDOW - DEFAULT_HEAD_DISTANCE / 2;
28
29#[derive(Clone, Copy, Debug, Eq, PartialEq)]
31pub struct SnapPivotPolicy {
32 head_distance: u64,
34 advance_after: u64,
36 history: u64,
38}
39
40impl Default for SnapPivotPolicy {
41 fn default() -> Self {
42 Self {
43 head_distance: DEFAULT_HEAD_DISTANCE,
44 advance_after: DEFAULT_ADVANCE_AFTER,
45 history: BAL_RETENTION_PERIOD_SLOTS,
46 }
47 }
48}
49
50impl SnapPivotPolicy {
51 pub const fn with_head_distance(mut self, head_distance: u64) -> Self {
53 self.head_distance = head_distance;
54 self
55 }
56
57 pub const fn with_advance_after(mut self, advance_after: u64) -> Self {
59 self.advance_after = advance_after;
60 self
61 }
62
63 pub const fn with_history(mut self, history: u64) -> Self {
68 self.history = history;
69 self
70 }
71
72 pub const fn pivot_block(&self, head: u64, finalized: Option<u64>) -> Option<u64> {
77 if let Some(finalized) = finalized &&
78 head.saturating_sub(finalized) <= self.advance_after
79 {
80 return Some(finalized)
81 }
82 head.checked_sub(self.head_distance)
83 }
84
85 pub const fn needs_advance(&self, generation: SnapGeneration, head: u64) -> bool {
90 generation.lag(head) > self.advance_after
91 }
92
93 pub const fn is_catchable(&self, generation: SnapGeneration, head: u64) -> bool {
97 self.is_catchable_from(generation.target().number, head)
98 }
99
100 pub const fn is_catchable_from(&self, applied: u64, head: u64) -> bool {
105 head.saturating_sub(applied) <= self.history
106 }
107
108 pub fn select(
113 &self,
114 provider: &impl HeaderProvider,
115 head: u64,
116 finalized: Option<u64>,
117 ) -> Result<Option<SnapGeneration>, SnapSyncError> {
118 let preferred = self.pivot_block(head, finalized);
119 let fallback =
120 head.checked_sub(self.head_distance).filter(|block| Some(*block) != preferred);
121 for block_number in preferred.into_iter().chain(fallback) {
122 let Some(header) = provider.sealed_header(block_number)? else { continue };
123 if header.block_access_list_hash().is_some() {
124 return Ok(Some(SnapGeneration::new(header.num_hash(), header.state_root())))
125 }
126 }
127 Ok(None)
128 }
129
130 pub const fn is_finishable(&self, generation: SnapGeneration, head: u64) -> bool {
134 matches!(generation.phase(), SnapPhase::Trie) || self.is_catchable(generation, head)
135 }
136
137 pub const fn awaits_orphaned_lists(&self, ancestor: u64, head: u64) -> bool {
140 head.saturating_sub(ancestor) <= SERVED_STATE_WINDOW
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147 use crate::test_utils::{chain, policy, provider_with};
148 use alloy_eips::BlockNumHash;
149 use alloy_primitives::B256;
150
151 #[test]
152 fn selects_the_bal_capable_pivot_behind_the_head() {
153 let headers = chain(Some(0));
154 let expected = headers[2].clone();
155 let provider = provider_with(headers);
156
157 let generation = policy().select(&provider, 3, None).unwrap().unwrap();
158
159 assert_eq!(generation.target().number, 2);
160 assert_eq!(generation.target().hash, expected.hash_slow());
161 assert_eq!(generation.state_root(), expected.state_root);
162 assert_eq!(generation.phase(), SnapPhase::Accounts);
163 }
164
165 #[test]
166 fn a_recent_finalized_block_is_anchored_to_instead_of_the_head_distance() {
167 let headers = chain(Some(0));
168 let expected = headers[1].clone();
169 let provider = provider_with(headers);
170
171 let generation = policy().select(&provider, 3, Some(1)).unwrap().unwrap();
172
173 assert_eq!(generation.target().number, 1);
174 assert_eq!(generation.target().hash, expected.hash_slow());
175 }
176
177 #[test]
178 fn finality_stalled_outside_the_advance_window_falls_back_to_the_head_distance() {
179 let headers = chain(Some(0));
180 let fallback = headers[2].clone();
181 let provider = provider_with(headers);
182 let policy = policy().with_advance_after(1);
184
185 let generation = policy.select(&provider, 3, Some(1)).unwrap().unwrap();
186
187 assert_eq!(generation.target().number, 2);
189 assert_eq!(generation.target().hash, fallback.hash_slow());
190 }
191
192 #[test]
193 fn an_ineligible_finalized_pivot_falls_back_to_the_head_distance() {
194 let headers = chain(Some(2));
196 let fallback = headers[2].clone();
197 let provider = provider_with(headers);
198
199 let generation = policy().select(&provider, 3, Some(1)).unwrap().unwrap();
200
201 assert_eq!(generation.target().number, 2);
203 assert_eq!(generation.target().hash, fallback.hash_slow());
204 }
205
206 #[test]
207 fn pivot_without_a_bal_commitment_is_not_selectable() {
208 let provider = provider_with(chain(Some(3)));
209
210 assert_eq!(policy().select(&provider, 3, None).unwrap(), None);
211 assert_eq!(policy().select(&provider, 3, Some(1)).unwrap(), None);
213 }
214
215 #[test]
216 fn pivot_beyond_downloaded_headers_is_not_selectable() {
217 let provider = provider_with(chain(Some(0)));
218
219 assert_eq!(policy().select(&provider, 9, None).unwrap(), None);
220 }
221
222 #[test]
223 fn chain_shorter_than_the_head_distance_has_no_pivot() {
224 let provider = provider_with(chain(Some(0)));
225
226 assert_eq!(policy().with_head_distance(4).select(&provider, 0, None).unwrap(), None);
227 }
228
229 #[test]
230 fn a_pivot_lagging_past_the_advance_window_is_re_anchored() {
231 let policy = policy();
232 let generation = SnapGeneration::new(BlockNumHash::new(0, B256::ZERO), B256::ZERO);
233
234 assert!(!policy.needs_advance(generation, 4));
235 assert!(policy.needs_advance(generation, 5));
236 }
237
238 #[test]
239 fn generation_outside_the_bal_window_is_not_finishable() {
240 let headers = chain(Some(0));
241 let anchor = headers[1].clone();
242 let provider = provider_with(headers);
243 let generation =
244 SnapGeneration::new(BlockNumHash::new(1, anchor.hash_slow()), anchor.state_root);
245 let policy = policy();
246
247 assert!(generation.is_canonical(&provider).unwrap());
248 assert!(policy.is_finishable(generation, 9));
249 assert!(!policy.is_finishable(generation, 10));
250 }
251
252 #[test]
253 fn downloaded_state_finishes_outside_the_bal_window() {
254 let anchor = chain(Some(0))[1].clone();
255 let generation =
256 SnapGeneration::new(BlockNumHash::new(1, anchor.hash_slow()), anchor.state_root)
257 .with_phase(SnapPhase::Trie);
258
259 assert!(policy().is_finishable(generation, 1_000));
260 }
261
262 #[test]
263 fn reorged_anchor_is_not_canonical() {
264 let provider = provider_with(chain(Some(0)));
265 let generation =
266 SnapGeneration::new(BlockNumHash::new(1, B256::repeat_byte(0xff)), B256::ZERO);
267
268 assert!(!generation.is_canonical(&provider).unwrap());
269 }
270}