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 generation.lag(head) <= self.history
98 }
99
100 pub fn select(
105 &self,
106 provider: &impl HeaderProvider,
107 head: u64,
108 finalized: Option<u64>,
109 ) -> Result<Option<SnapGeneration>, SnapSyncError> {
110 let preferred = self.pivot_block(head, finalized);
111 let fallback =
112 head.checked_sub(self.head_distance).filter(|block| Some(*block) != preferred);
113 for block_number in preferred.into_iter().chain(fallback) {
114 let Some(header) = provider.sealed_header(block_number)? else { continue };
115 if header.block_access_list_hash().is_some() {
116 return Ok(Some(SnapGeneration::new(header.num_hash(), header.state_root())))
117 }
118 }
119 Ok(None)
120 }
121
122 pub const fn is_finishable(&self, generation: SnapGeneration, head: u64) -> bool {
126 matches!(generation.phase(), SnapPhase::Trie) || self.is_catchable(generation, head)
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133 use crate::test_utils::{chain, policy, provider_with};
134 use alloy_eips::BlockNumHash;
135 use alloy_primitives::B256;
136
137 #[test]
138 fn selects_the_bal_capable_pivot_behind_the_head() {
139 let headers = chain(Some(0));
140 let expected = headers[2].clone();
141 let provider = provider_with(headers);
142
143 let generation = policy().select(&provider, 3, None).unwrap().unwrap();
144
145 assert_eq!(generation.target().number, 2);
146 assert_eq!(generation.target().hash, expected.hash_slow());
147 assert_eq!(generation.state_root(), expected.state_root);
148 assert_eq!(generation.phase(), SnapPhase::Accounts);
149 }
150
151 #[test]
152 fn a_recent_finalized_block_is_anchored_to_instead_of_the_head_distance() {
153 let headers = chain(Some(0));
154 let expected = headers[1].clone();
155 let provider = provider_with(headers);
156
157 let generation = policy().select(&provider, 3, Some(1)).unwrap().unwrap();
158
159 assert_eq!(generation.target().number, 1);
160 assert_eq!(generation.target().hash, expected.hash_slow());
161 }
162
163 #[test]
164 fn finality_stalled_outside_the_advance_window_falls_back_to_the_head_distance() {
165 let headers = chain(Some(0));
166 let fallback = headers[2].clone();
167 let provider = provider_with(headers);
168 let policy = policy().with_advance_after(1);
170
171 let generation = policy.select(&provider, 3, Some(1)).unwrap().unwrap();
172
173 assert_eq!(generation.target().number, 2);
175 assert_eq!(generation.target().hash, fallback.hash_slow());
176 }
177
178 #[test]
179 fn an_ineligible_finalized_pivot_falls_back_to_the_head_distance() {
180 let headers = chain(Some(2));
182 let fallback = headers[2].clone();
183 let provider = provider_with(headers);
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 pivot_without_a_bal_commitment_is_not_selectable() {
194 let provider = provider_with(chain(Some(3)));
195
196 assert_eq!(policy().select(&provider, 3, None).unwrap(), None);
197 assert_eq!(policy().select(&provider, 3, Some(1)).unwrap(), None);
199 }
200
201 #[test]
202 fn pivot_beyond_downloaded_headers_is_not_selectable() {
203 let provider = provider_with(chain(Some(0)));
204
205 assert_eq!(policy().select(&provider, 9, None).unwrap(), None);
206 }
207
208 #[test]
209 fn chain_shorter_than_the_head_distance_has_no_pivot() {
210 let provider = provider_with(chain(Some(0)));
211
212 assert_eq!(policy().with_head_distance(4).select(&provider, 0, None).unwrap(), None);
213 }
214
215 #[test]
216 fn a_pivot_lagging_past_the_advance_window_is_re_anchored() {
217 let policy = policy();
218 let generation = SnapGeneration::new(BlockNumHash::new(0, B256::ZERO), B256::ZERO);
219
220 assert!(!policy.needs_advance(generation, 4));
221 assert!(policy.needs_advance(generation, 5));
222 }
223
224 #[test]
225 fn generation_outside_the_bal_window_is_not_finishable() {
226 let headers = chain(Some(0));
227 let anchor = headers[1].clone();
228 let provider = provider_with(headers);
229 let generation =
230 SnapGeneration::new(BlockNumHash::new(1, anchor.hash_slow()), anchor.state_root);
231 let policy = policy();
232
233 assert!(generation.is_canonical(&provider).unwrap());
234 assert!(policy.is_finishable(generation, 9));
235 assert!(!policy.is_finishable(generation, 10));
236 }
237
238 #[test]
239 fn downloaded_state_finishes_outside_the_bal_window() {
240 let anchor = chain(Some(0))[1].clone();
241 let generation =
242 SnapGeneration::new(BlockNumHash::new(1, anchor.hash_slow()), anchor.state_root)
243 .with_phase(SnapPhase::Trie);
244
245 assert!(policy().is_finishable(generation, 1_000));
246 }
247
248 #[test]
249 fn reorged_anchor_is_not_canonical() {
250 let provider = provider_with(chain(Some(0)));
251 let generation =
252 SnapGeneration::new(BlockNumHash::new(1, B256::repeat_byte(0xff)), B256::ZERO);
253
254 assert!(!generation.is_canonical(&provider).unwrap());
255 }
256}