1use crate::backfill::FETCH_AHEAD;
5use crate::{Archive, ArchiveError, Result};
6use alloy_primitives::{Address, B256};
7use bal_source::{
8 check_requested, verify_account_proof, BalSource, SourceError, SourcedBlock, StateSource,
9};
10use futures::future::join_all;
11use std::collections::{BTreeMap, VecDeque};
12use tracing::{debug, info, warn};
13
14pub const REORG_HORIZON_FALLBACK: u64 = 4096;
19
20const PROOF_CHUNK: usize = 256;
24
25#[derive(Debug, Default, Clone)]
27pub struct SyncReport {
28 pub from: Option<u64>,
30 pub to: Option<u64>,
32 pub blocks_applied: u64,
34 pub reorged_to: Option<u64>,
36 pub slots_written: usize,
38 pub bootstrapped: usize,
40 pub bootstrap_pending: usize,
42 pub bootstrap_lost: usize,
44 pub unverified_blocks: u64,
46 pub source_head: Option<u64>,
49 pub touched: Vec<Address>,
53}
54
55pub const TOUCHED_CAP: usize = 4096;
57
58struct SyncGuard<'a>(&'a Archive);
61
62impl Drop for SyncGuard<'_> {
63 fn drop(&mut self) {
64 self.0.sync_idle();
65 }
66}
67
68impl Archive {
69 pub async fn sync<S: BalSource + ?Sized>(
77 &self,
78 source: &S,
79 state: Option<&dyn StateSource>,
80 ) -> Result<SyncReport> {
81 self.sync_step(source, state, None).await
82 }
83
84 pub async fn sync_step<S: BalSource + ?Sized>(
88 &self,
89 source: &S,
90 state: Option<&dyn StateSource>,
91 max_blocks: Option<u64>,
92 ) -> Result<SyncReport> {
93 if !self.begin_sync() {
94 return Err(ArchiveError::SyncInProgress);
95 }
96 let _guard = SyncGuard(self);
97 self.sync_inner(source, state, max_blocks).await
98 }
99
100 async fn sync_inner<S: BalSource + ?Sized>(
101 &self,
102 source: &S,
103 state: Option<&dyn StateSource>,
104 max_blocks: Option<u64>,
105 ) -> Result<SyncReport> {
106 let mut report = SyncReport::default();
107 let Some(mut next) = self.claim_start()? else {
110 return Ok(report);
111 };
112 let src_head = source.head().await?;
113 report.source_head = Some(src_head);
114 let horizon_floor = src_head.saturating_sub(REORG_HORIZON_FALLBACK);
119 let finalized = match source.finalized().await {
120 Ok(f) => f.min(src_head).max(horizon_floor),
121 Err(e) => {
122 debug!(%e, "no finalized tag; using fixed reorg horizon");
123 horizon_floor
124 }
125 };
126
127 if let Some((h, hash)) = self.head()? {
128 let cur = match source.header(h).await {
130 Ok(hdr) => hdr,
131 Err(SourceError::BlockNotFound(_)) => {
132 debug!(head = h, "head not served by upstream yet; skipping pass");
133 return Ok(report);
134 }
135 Err(e) => return Err(e.into()),
136 };
137 if cur.hash != hash {
138 let fork = self.find_fork(source, h).await?;
139 warn!(head = h, fork, "reorg detected at start of sync");
140 self.rollback_to(fork)?;
141 report.reorged_to = Some(fork);
142 next = fork + 1;
143 }
144 }
145 report.from = Some(next);
146
147 let mut last_fork: Option<u64> = None;
150 let mut touched: std::collections::BTreeSet<Address> = std::collections::BTreeSet::new();
151
152 let mut queue: VecDeque<(u64, bal_source::Result<SourcedBlock>)> = VecDeque::new();
155 while next <= src_head {
156 if max_blocks.is_some_and(|m| report.blocks_applied >= m) {
157 break;
158 }
159 if queue.front().map(|(n, _)| *n) != Some(next) {
160 queue.clear();
161 let mut n = FETCH_AHEAD.min(src_head - next + 1);
162 if let Some(m) = max_blocks {
163 n = n.min(m.saturating_sub(report.blocks_applied)).max(1);
164 }
165 let numbers: Vec<u64> = (0..n).map(|i| next + i).collect();
166 let fetched = join_all(numbers.iter().map(|&b| source.block(b))).await;
167 queue.extend(numbers.into_iter().zip(fetched));
168 }
169 let Some((_, res)) = queue.pop_front() else {
170 break;
171 };
172 let blk = match res {
173 Ok(b) => b,
174 Err(SourceError::BlockNotFound(n)) if n >= src_head.saturating_sub(2) => {
175 debug!(block = n, "not yet available upstream; stopping this pass");
176 break;
177 }
178 Err(e) => return Err(e.into()),
179 };
180 let header = blk.header.clone();
181 if header.number != next {
182 return Err(ArchiveError::Source(SourceError::Malformed(format!(
183 "asked for block {next}, source answered with block {}",
184 header.number
185 ))));
186 }
187
188 if let Some((stored_hash, _)) = self.header_at(next - 1)? {
190 if stored_hash != header.parent_hash {
191 let fork = self.find_fork(source, next - 1).await?;
192 if last_fork == Some(fork) {
193 return Err(ArchiveError::InconsistentSource(next));
194 }
195 last_fork = Some(fork);
196 warn!(block = next, fork, "reorg detected mid-sync");
197 self.rollback_to(fork)?;
198 report.reorged_to = Some(fork);
199 next = fork + 1;
200 continue;
201 }
202 }
203
204 let verified = match header.block_access_list_hash {
205 Some(expected) => {
206 blk.bal
207 .verify(expected)
208 .map_err(|err| ArchiveError::Verification { block: next, err })?;
209 true
210 }
211 None if self.config.allow_unverified => {
212 report.unverified_blocks += 1;
213 warn!(
214 block = next,
215 "applying block without BAL hash (allow_unverified)"
216 );
217 false
218 }
219 None => return Err(ArchiveError::NoBalHash(next)),
220 };
221
222 let watches = self.watchlist_for(next)?;
226 let prune_below =
227 Some(finalized.min(src_head.saturating_sub(self.config.bootstrap_window + 1)));
228 if touched.len() < TOUCHED_CAP {
229 touched.extend(blk.bal.accounts.iter().map(|a| a.address));
230 }
231 let (fresh, written) =
232 self.apply_block(&header, &blk.bal, &watches, verified, prune_below)?;
233 report.slots_written += written;
234 report.blocks_applied += 1;
235 report.to = Some(next);
236 debug!(block = next, written, fresh = fresh.len(), "applied");
237
238 if !fresh.is_empty() {
241 let prev = match state {
242 None => None,
243 Some(_) => self.header_of(source, next - 1).await,
244 };
245 for (addr, start, slots) in &fresh {
246 match (state, prev) {
247 (Some(st), Some((hash, root))) => {
248 match self
249 .bootstrap_at(st, root, hash, *addr, *start, slots, next - 1)
250 .await
251 {
252 Ok(n) => {
253 report.bootstrapped += n;
254 report.bootstrap_pending += slots.len() - n;
255 }
256 Err(e) => {
257 warn!(%addr, block = next, %e, "early bootstrap failed; left pending");
258 report.bootstrap_pending += slots.len();
259 }
260 }
261 }
262 _ => report.bootstrap_pending += slots.len(),
263 }
264 }
265 }
266
267 next += 1;
268 }
269
270 if let Some(st) = state {
272 let (ok, pending, lost) = self.retry_pending(source, st, src_head).await?;
273 report.bootstrapped += ok;
274 report.bootstrap_pending = pending;
275 report.bootstrap_lost = lost;
276 }
277
278 report.touched = touched.into_iter().take(TOUCHED_CAP).collect();
279 info!(?report, "sync done");
280 Ok(report)
281 }
282
283 async fn header_of<S: BalSource + ?Sized>(
286 &self,
287 source: &S,
288 block: u64,
289 ) -> Option<(B256, B256)> {
290 match self.header_at(block) {
291 Ok(Some(h)) => return Some(h),
292 Ok(None) => {}
293 Err(e) => {
294 warn!(block, %e, "cannot read stored header");
295 return None;
296 }
297 }
298 match source.header(block).await {
299 Ok(h) => {
300 if let Err(e) = self.remember_header(block, h.hash, h.state_root) {
301 warn!(block, %e, "cannot remember header");
302 }
303 Some((h.hash, h.state_root))
304 }
305 Err(e) => {
306 warn!(block, %e, "cannot fetch header for proof root");
307 None
308 }
309 }
310 }
311
312 async fn find_fork<S: BalSource + ?Sized>(&self, source: &S, from: u64) -> Result<u64> {
315 let floor = from.saturating_sub(REORG_HORIZON_FALLBACK);
316 let mut b = from;
317 loop {
318 let Some((stored, _)) = self.header_at(b)? else {
319 return Err(ArchiveError::ReorgBeyondHorizon(b));
320 };
321 let live = source.header(b).await?.hash;
322 if live == stored {
323 return Ok(b);
324 }
325 if b == 0 || b <= floor {
326 return Err(ArchiveError::ReorgBeyondHorizon(b));
327 }
328 b -= 1;
329 }
330 }
331
332 #[allow(clippy::too_many_arguments)]
336 async fn bootstrap_at(
337 &self,
338 state: &dyn StateSource,
339 state_root: B256,
340 block_hash: B256,
341 addr: Address,
342 start: u64,
343 slots: &[B256],
344 block: u64,
345 ) -> Result<usize> {
346 let mut stored = 0;
347 for chunk in slots.chunks(PROOF_CHUNK) {
348 let proof = state.proof(addr, chunk, block).await?;
349 check_requested(chunk, &proof)?;
350 let values = verify_account_proof(state_root, &proof)?;
351 let values: Vec<(B256, B256)> = values
352 .into_iter()
353 .map(|(k, v)| (k, B256::from(v.to_be_bytes::<32>())))
354 .collect();
355 stored += self.put_bootstrap(addr, start, block, block_hash, &values)?;
356 }
357 Ok(stored)
358 }
359
360 async fn retry_pending<S: BalSource + ?Sized>(
362 &self,
363 source: &S,
364 state: &dyn StateSource,
365 src_head: u64,
366 ) -> Result<(usize, usize, usize)> {
367 let pending = self.pending_bootstraps()?;
368 if pending.is_empty() {
369 return Ok((0, 0, 0));
370 }
371 let watches = self.watchlist()?;
372 let start_of = |a: Address| watches.iter().find(|(x, _)| *x == a).map(|(_, s)| *s);
373 let (mut ok, mut still, mut lost) = (0, 0, 0);
374 let mut groups: BTreeMap<(Address, u64), Vec<B256>> = BTreeMap::new();
376 for (addr, slot, first_seen) in pending {
377 groups.entry((addr, first_seen)).or_default().push(slot);
378 }
379 for ((addr, first_seen), slots) in groups {
380 let Some(start) = start_of(addr) else {
381 continue;
382 };
383 let Some(at) = first_seen.checked_sub(1) else {
386 return Err(ArchiveError::Corrupt("pending first_seen"));
387 };
388 if src_head.saturating_sub(at) > self.config.bootstrap_window {
389 self.mark_lost(addr, &slots, first_seen)?;
390 lost += slots.len();
391 continue;
392 }
393 let Some((hash, root)) = self.header_of(source, at).await else {
394 still += slots.len();
395 continue;
396 };
397 match self
398 .bootstrap_at(state, root, hash, addr, start, &slots, at)
399 .await
400 {
401 Ok(n) => {
402 ok += n;
403 still += slots.len() - n;
404 }
405 Err(e) => {
406 debug!(%addr, first_seen, %e, "pending bootstrap retry failed");
407 still += slots.len();
408 }
409 }
410 }
411 Ok((ok, still, lost))
412 }
413
414 pub async fn bootstrap_slot(
424 &self,
425 state: &dyn StateSource,
426 addr: Address,
427 slot: B256,
428 ) -> Result<()> {
429 let start = self.start_of(addr)?.ok_or(ArchiveError::NotWatched(addr))?;
430 let (head, _) = self
431 .head()?
432 .ok_or(ArchiveError::HeadBelowStart { head: 0, start })?;
433 if head < start {
434 return Err(ArchiveError::HeadBelowStart { head, start });
435 }
436 if self.boot_state(addr, slot)?.is_some() {
437 return Ok(()); }
439 let (hash, root) = self
440 .header_at(head)?
441 .ok_or(ArchiveError::ReorgBeyondHorizon(head))?;
442 self.bootstrap_at(state, root, hash, addr, start, &[slot], head)
443 .await?;
444 Ok(())
445 }
446}