Skip to main content

forest/daemon/
db_util.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4use crate::blocks::Tipset;
5use crate::db::SettingsStoreExt;
6use crate::db::car::forest::{
7    FOREST_CAR_FILE_EXTENSION, TEMP_FOREST_CAR_FILE_EXTENSION, new_forest_car_temp_path_in,
8};
9use crate::db::car::{ForestCar, ManyCar};
10use crate::ipld::ChainExportState;
11use crate::message::SignedMessage;
12use crate::networks::ChainConfig;
13use crate::prelude::*;
14use crate::rpc::sync::SnapshotProgressTracker;
15use crate::shim::clock::ChainEpoch;
16use crate::state_manager::StateManager;
17use crate::utils::db::car_stream::CarStream;
18use crate::utils::io::EitherMmapOrRandomAccessFile;
19use crate::utils::net::{DownloadFileOption, download_to};
20use anyhow::{Context, bail};
21use futures::TryStreamExt;
22use serde::{Deserialize, Serialize};
23use std::sync::LazyLock;
24use std::sync::atomic::{AtomicI64, Ordering};
25use std::{
26    ffi::OsStr,
27    fs,
28    path::{Path, PathBuf},
29    time,
30};
31use tokio::io::AsyncWriteExt;
32use tokio_util::sync::CancellationToken;
33use tracing::{debug, info, warn};
34use url::Url;
35use walkdir::WalkDir;
36
37#[cfg(doc)]
38use crate::rpc::eth::types::EthHash;
39
40#[cfg(doc)]
41use crate::blocks::TipsetKey;
42
43#[cfg(doc)]
44use cid::Cid;
45
46/// Loads all `.forest.car.zst` snapshots and cleanup stale `.forest.car.zst.tmp` files.
47pub fn load_all_forest_cars_with_cleanup<T>(
48    store: &ManyCar<T>,
49    forest_car_db_dir: &Path,
50) -> anyhow::Result<()> {
51    load_all_forest_cars_internal(store, forest_car_db_dir, true)
52}
53
54/// Loads all `.forest.car.zst` snapshots
55pub fn load_all_forest_cars<T>(store: &ManyCar<T>, forest_car_db_dir: &Path) -> anyhow::Result<()> {
56    load_all_forest_cars_internal(store, forest_car_db_dir, false)
57}
58
59fn load_all_forest_cars_internal<T>(
60    store: &ManyCar<T>,
61    forest_car_db_dir: &Path,
62    cleanup: bool,
63) -> anyhow::Result<()> {
64    if !forest_car_db_dir.is_dir() {
65        fs::create_dir_all(forest_car_db_dir)?;
66    }
67    for file in WalkDir::new(forest_car_db_dir)
68        .max_depth(1)
69        .into_iter()
70        .filter_map(|e| {
71            e.ok().and_then(|e| {
72                if !e.file_type().is_dir() {
73                    Some(e.into_path())
74                } else {
75                    None
76                }
77            })
78        })
79    {
80        if let Some(filename) = file.file_name().and_then(OsStr::to_str) {
81            if filename.ends_with(FOREST_CAR_FILE_EXTENSION) {
82                let car = ForestCar::try_from(file.as_path())
83                    .with_context(|| format!("Error loading car DB at {}", file.display()))?;
84                store.read_only(car.into())?;
85                debug!("Loaded car DB at {}", file.display());
86            } else if cleanup && filename.ends_with(TEMP_FOREST_CAR_FILE_EXTENSION) {
87                // Only delete files that appear to be incomplete car DB files
88                match std::fs::remove_file(&file) {
89                    Ok(_) => {
90                        info!("Deleted temp car DB at {}", file.display());
91                    }
92                    Err(e) => {
93                        warn!("Failed to delete temp car DB at {}: {e}", file.display());
94                    }
95                }
96            }
97        }
98    }
99
100    tracing::info!("Loaded {} CARs", store.len());
101
102    Ok(())
103}
104
105#[derive(
106    Default,
107    PartialEq,
108    Eq,
109    Debug,
110    Clone,
111    Copy,
112    strum::Display,
113    strum::EnumString,
114    Serialize,
115    Deserialize,
116)]
117#[strum(serialize_all = "lowercase")]
118#[cfg_attr(test, derive(derive_quickcheck_arbitrary::Arbitrary))]
119pub enum ImportMode {
120    #[default]
121    /// Hard link the snapshot and fallback to `Copy` if not applicable
122    Auto,
123    /// Copies the snapshot to the database directory.
124    Copy,
125    /// Moves the snapshot to the database directory (or copies and deletes the original).
126    Move,
127    /// Creates a symbolic link to the snapshot in the database directory.
128    Symlink,
129    /// Creates a symbolic link to the snapshot in the database directory.
130    Hardlink,
131}
132
133/// This function validates and stores the CAR binary from `from_path`(either local path or URL) into the `{DB_ROOT}/car_db/`
134/// (automatically trans-code into `.forest.car.zst` format when needed), and returns its final file path and the heaviest tipset.
135pub async fn import_chain_as_forest_car(
136    from_path: &Path,
137    forest_car_db_dir: &Path,
138    import_mode: ImportMode,
139    rpc_endpoint: Url,
140    f3_root: &Path,
141    chain_config: &ChainConfig,
142    snapshot_progress_tracker: &SnapshotProgressTracker,
143) -> anyhow::Result<(PathBuf, Tipset)> {
144    info!("Importing chain from snapshot at: {}", from_path.display());
145
146    let stopwatch = time::Instant::now();
147
148    let forest_car_db_path = forest_car_db_dir.join(format!(
149        "{}{FOREST_CAR_FILE_EXTENSION}",
150        chrono::Utc::now().timestamp_millis()
151    ));
152
153    let move_or_copy = |mode: ImportMode| {
154        let forest_car_db_path = forest_car_db_path.clone();
155        async move {
156            let downloaded_car_temp_path = new_forest_car_temp_path_in(forest_car_db_dir)?;
157            if let Ok(url) = Url::parse(&from_path.display().to_string()) {
158                download_to(
159                    &url,
160                    &downloaded_car_temp_path,
161                    DownloadFileOption::Resumable,
162                    snapshot_progress_tracker.create_callback(),
163                )
164                .await?;
165
166                snapshot_progress_tracker.completed();
167            } else {
168                snapshot_progress_tracker.not_required();
169                if ForestCar::is_valid(&EitherMmapOrRandomAccessFile::open(from_path)?) {
170                    move_or_copy_file(from_path, &downloaded_car_temp_path, mode)?;
171                } else {
172                    // For a local snapshot, we transcode directly instead of copying & transcoding.
173                    transcode_into_forest_car(from_path, &downloaded_car_temp_path).await?;
174                    if mode == ImportMode::Move {
175                        std::fs::remove_file(from_path).context("Error removing original file")?;
176                    }
177                }
178            }
179
180            if ForestCar::is_valid(&EitherMmapOrRandomAccessFile::open(
181                &downloaded_car_temp_path,
182            )?) {
183                downloaded_car_temp_path.persist(&forest_car_db_path)?;
184            } else {
185                // Use another temp file to make sure all final `.forest.car.zst` files are complete and valid.
186                let forest_car_db_temp_path = new_forest_car_temp_path_in(forest_car_db_dir)?;
187                transcode_into_forest_car(&downloaded_car_temp_path, &forest_car_db_temp_path)
188                    .await?;
189                forest_car_db_temp_path.persist(&forest_car_db_path)?;
190            }
191            anyhow::Ok(())
192        }
193    };
194
195    match import_mode {
196        ImportMode::Auto => {
197            if Url::parse(&from_path.display().to_string()).is_ok() {
198                // Fallback to move if from_path is url
199                move_or_copy(ImportMode::Move).await?;
200            } else if ForestCar::is_valid(&EitherMmapOrRandomAccessFile::open(from_path)?) {
201                tracing::info!(
202                    "Hardlinking {} to {}",
203                    from_path.display(),
204                    forest_car_db_path.display()
205                );
206                if std::fs::hard_link(from_path, &forest_car_db_path).is_err() {
207                    tracing::warn!("Error creating hardlink, fallback to copy");
208                    move_or_copy(ImportMode::Copy).await?;
209                }
210            } else {
211                tracing::warn!(
212                    "Snapshot file is not a valid forest.car.zst file, fallback to copy"
213                );
214                move_or_copy(ImportMode::Copy).await?;
215            }
216        }
217        ImportMode::Copy | ImportMode::Move => {
218            move_or_copy(import_mode).await?;
219        }
220        ImportMode::Symlink => {
221            let from_path = std::path::absolute(from_path)?;
222            if ForestCar::is_valid(&EitherMmapOrRandomAccessFile::open(&from_path)?) {
223                tracing::info!(
224                    "Symlinking {} to {}",
225                    from_path.display(),
226                    forest_car_db_path.display()
227                );
228                std::os::unix::fs::symlink(from_path, &forest_car_db_path)
229                    .context("Error creating symlink")?;
230            } else {
231                bail!("Snapshot file must be a valid forest.car.zst file");
232            }
233        }
234        ImportMode::Hardlink => {
235            if ForestCar::is_valid(&EitherMmapOrRandomAccessFile::open(from_path)?) {
236                tracing::info!(
237                    "Hardlinking {} to {}",
238                    from_path.display(),
239                    forest_car_db_path.display()
240                );
241                std::fs::hard_link(from_path, &forest_car_db_path)
242                    .context("Error creating hardlink")?;
243            } else {
244                bail!("Snapshot file must be a valid forest.car.zst file");
245            }
246        }
247    };
248
249    let forest_car = ForestCar::try_from(forest_car_db_path.as_path())?;
250
251    if let Some(f3_cid) = forest_car.metadata().and_then(|m| m.f3_data) {
252        if crate::f3::get_f3_sidecar_params(chain_config)
253            .initial_power_table
254            .is_none()
255        {
256            // To avoid importing old/wrong F3 data without initial power table check
257            tracing::warn!(
258                "skipped importing F3 data as the initial power table CID is not set in the current manifest"
259            );
260        } else {
261            let mut f3_data = forest_car
262                .get_reader(f3_cid)?
263                .with_context(|| format!("f3 data not found, cid: {f3_cid}"))?;
264            let mut temp_f3_snap = tempfile::Builder::new()
265                .suffix(".f3snap.bin")
266                .tempfile_in(forest_car_db_dir)?;
267            {
268                let f = temp_f3_snap.as_file_mut();
269                std::io::copy(&mut f3_data, f)?;
270                f.sync_all()?;
271            }
272            if let Err(e) = crate::f3::import_f3_snapshot(
273                chain_config,
274                rpc_endpoint.to_string(),
275                f3_root.display().to_string(),
276                temp_f3_snap.path().display().to_string(),
277            ) {
278                // Do not make it a hard error if anything is wrong with F3 snapshot
279                tracing::error!("Failed to import F3 snapshot: {e:#}");
280            }
281        }
282    }
283
284    let ts = forest_car.heaviest_tipset()?;
285    info!(
286        "Imported snapshot in: {}s, heaviest tipset epoch: {}, key: {}",
287        stopwatch.elapsed().as_secs(),
288        ts.epoch(),
289        ts.key()
290    );
291
292    Ok((forest_car_db_path, ts))
293}
294
295fn move_or_copy_file(from: &Path, to: &Path, import_mode: ImportMode) -> anyhow::Result<()> {
296    match import_mode {
297        ImportMode::Move => {
298            tracing::info!("Moving {} to {}", from.display(), to.display());
299            if fs::rename(from, to).is_ok() {
300                Ok(())
301            } else {
302                fs::copy(from, to).context("Error copying file")?;
303                fs::remove_file(from).context("Error removing original file")?;
304                Ok(())
305            }
306        }
307        ImportMode::Copy => {
308            tracing::info!("Copying {} to {}", from.display(), to.display());
309            fs::copy(from, to).map(|_| ()).context("Error copying file")
310        }
311        m => {
312            bail!("{m} must be handled elsewhere");
313        }
314    }
315}
316
317async fn transcode_into_forest_car(from: &Path, to: &Path) -> anyhow::Result<()> {
318    tracing::info!(
319        from = %from.display(),
320        to = %to.display(),
321        "transcoding into forest car"
322    );
323    let car_stream = CarStream::new_from_path(from).await?;
324    let roots = car_stream.header_v1.roots.clone();
325
326    let mut writer = tokio::io::BufWriter::new(tokio::fs::File::create(to).await?);
327    let frames = crate::db::car::forest::Encoder::compress_stream_default(
328        car_stream.map_err(anyhow::Error::from),
329    );
330    crate::db::car::forest::Encoder::write(&mut writer, roots, frames).await?;
331    writer.shutdown().await?;
332
333    Ok(())
334}
335
336/// Settings-store key under which index backfill persists the epoch of the last committed
337/// batch, so an interrupted backfill can be resumed from where it left off.
338pub const BACKFILL_CHECKPOINT_KEY: &str = "/index/backfill/checkpoint";
339
340/// Outcome of indexing a single tipset during backfill.
341enum ProcessOutcome {
342    /// The tipset was indexed.
343    Indexed,
344    /// The tipset was skipped because its state output was unavailable and recomputation was
345    /// disabled (see [`BackfillOptions::allow_recompute`]).
346    Skipped,
347}
348
349/// Options controlling a backfill run. [`Default`] matches the historical offline behavior:
350/// recompute missing state, allow indexing right up to the head, and commit in modest batches.
351#[derive(Debug, Clone, Copy)]
352pub struct BackfillOptions {
353    /// When `true`, missing tipset state is recomputed (expensive); when `false`, such tipsets
354    /// are skipped and reported. Online backfill default this to `false` to avoid starving sync.
355    pub allow_recompute: bool,
356    /// When `false`, the walk start is clamped to the EC-finalized epoch so that revert-prone
357    /// near-head tipsets are not indexed.
358    pub allow_near_head: bool,
359    /// Number of tipsets to process between commits/checkpoints.
360    pub batch_size: usize,
361}
362
363impl Default for BackfillOptions {
364    fn default() -> Self {
365        Self {
366            allow_recompute: true,
367            allow_near_head: true,
368            batch_size: 1000,
369        }
370    }
371}
372
373/// Report returned by [`run_backfill`].
374#[derive(Debug, Clone, Copy, Default)]
375pub struct BackfillReport {
376    pub indexed: u64,
377    pub skipped: u64,
378    pub cancelled: bool,
379}
380
381/// Lock-free counters for backfill progress; the epoch counters are hot on the walk.
382#[derive(Default)]
383struct BackfillCounters {
384    start_epoch: AtomicI64,
385    current_epoch: AtomicI64,
386    target_epoch: AtomicI64,
387    indexed: AtomicI64,
388    skipped: AtomicI64,
389}
390
391#[derive(Default)]
392struct BackfillStatusInner {
393    /// `None` while running and before the first run.
394    outcome: Option<ChainExportState>,
395    error: Option<String>,
396    start_time: Option<chrono::DateTime<chrono::Utc>>,
397    cancellation_token: Option<CancellationToken>,
398    counters: Arc<BackfillCounters>,
399}
400
401impl BackfillStatusInner {
402    fn is_running(&self) -> bool {
403        self.cancellation_token.is_some()
404    }
405}
406
407/// Status of the in-daemon index backfill, surfaced by the `Forest.IndexBackfillStatus` RPC and
408/// driven only through [`BackfillGuard`]. Mirrors the life-cycle of [`ChainExportState`].
409#[derive(Default)]
410pub struct BackfillStatus {
411    inner: parking_lot::Mutex<BackfillStatusInner>,
412}
413
414/// A consistent snapshot of [`BackfillStatus`], read under a single lock.
415#[derive(Debug, Clone)]
416pub struct BackfillStatusSnapshot {
417    pub state: ChainExportState,
418    pub error: Option<String>,
419    pub start_time: Option<chrono::DateTime<chrono::Utc>>,
420    pub start_epoch: ChainEpoch,
421    pub current_epoch: ChainEpoch,
422    pub target_epoch: ChainEpoch,
423    pub indexed: u64,
424    pub skipped: u64,
425}
426
427impl BackfillStatus {
428    pub fn snapshot(&self) -> BackfillStatusSnapshot {
429        let inner = self.inner.lock();
430        let c = &inner.counters;
431        BackfillStatusSnapshot {
432            state: if inner.is_running() {
433                ChainExportState::Running
434            } else {
435                inner.outcome.unwrap_or(ChainExportState::Idle)
436            },
437            error: inner.error.clone(),
438            start_time: inner.start_time,
439            start_epoch: c.start_epoch.load(Ordering::Relaxed),
440            current_epoch: c.current_epoch.load(Ordering::Relaxed),
441            target_epoch: c.target_epoch.load(Ordering::Relaxed),
442            indexed: c.indexed.load(Ordering::Relaxed).max(0) as u64,
443            skipped: c.skipped.load(Ordering::Relaxed).max(0) as u64,
444        }
445    }
446
447    /// Cancels the running backfill, if any, returning whether one was running.
448    pub fn cancel_running(&self) -> bool {
449        if let Some(token) = &self.inner.lock().cancellation_token {
450            token.cancel();
451            true
452        } else {
453            false
454        }
455    }
456
457    fn try_begin(
458        &self,
459        cancellation_token: CancellationToken,
460    ) -> anyhow::Result<Arc<BackfillCounters>> {
461        let mut inner = self.inner.lock();
462        anyhow::ensure!(
463            !inner.is_running(),
464            "an index backfill is already running; check `forest-cli index backfill-status`",
465        );
466        let counters = Arc::new(BackfillCounters::default());
467        *inner = BackfillStatusInner {
468            outcome: None,
469            error: None,
470            start_time: Some(chrono::Utc::now()),
471            cancellation_token: Some(cancellation_token),
472            counters: counters.clone(),
473        };
474        Ok(counters)
475    }
476
477    fn record_outcome(&self, outcome: ChainExportState, error: Option<String>) {
478        let mut inner = self.inner.lock();
479        if inner.outcome.is_none() {
480            inner.outcome = Some(outcome);
481            inner.error = error;
482        }
483    }
484
485    fn end(&self) {
486        let mut inner = self.inner.lock();
487        if inner.outcome.is_none() {
488            inner.outcome = Some(ChainExportState::Failed);
489        }
490        inner.cancellation_token = None;
491    }
492}
493
494/// Global status of the in-daemon index backfill.
495pub static BACKFILL_STATUS: LazyLock<BackfillStatus> = LazyLock::new(BackfillStatus::default);
496
497/// Single-flight guard for an index backfill. Holds the [`BACKFILL_STATUS`] slot for progress and
498/// cancellation, and nests a [`ChainExportGuard`] so backfill never overlaps snapshot exports or
499/// the snapshot GC (all three share the chain-export slot).
500pub struct BackfillGuard {
501    cancellation_token: CancellationToken,
502    counters: Arc<BackfillCounters>,
503    // Held for the lifetime of the backfill to exclude exports and snapshot GC.
504    export_guard: crate::ipld::ChainExportGuard,
505}
506
507impl BackfillGuard {
508    pub fn try_start() -> anyhow::Result<Self> {
509        // Acquire the shared chain-export slot first so backfill excludes GC/exports.
510        let export_guard = crate::ipld::ChainExportGuard::try_start_export(
511            crate::ipld::ChainExportKind::IndexBackfill,
512        )?;
513        let cancellation_token = CancellationToken::new();
514        let counters = match BACKFILL_STATUS.try_begin(cancellation_token.clone()) {
515            Ok(counters) => counters,
516            Err(e) => {
517                // Roll back the export slot if another backfill is somehow already tracked.
518                drop(export_guard);
519                return Err(e);
520            }
521        };
522        Ok(Self {
523            cancellation_token,
524            counters,
525            export_guard,
526        })
527    }
528
529    pub fn cancellation_token(&self) -> CancellationToken {
530        self.cancellation_token.clone()
531    }
532
533    /// Records the backfill outcome; the export guard is only held for mutual exclusion, so it is
534    /// reset to `Idle`.
535    pub fn finish<T>(self, result: anyhow::Result<T>) -> anyhow::Result<T> {
536        match &result {
537            Ok(_) => {
538                BACKFILL_STATUS.record_outcome(ChainExportState::Succeeded, None);
539            }
540            Err(e) => {
541                BACKFILL_STATUS.record_outcome(ChainExportState::Failed, Some(format!("{e:#}")));
542            }
543        }
544        self.export_guard
545            .record_outcome(ChainExportState::Idle, None);
546        result
547    }
548
549    fn reset_counters(&self, start_epoch: ChainEpoch, target_epoch: ChainEpoch) {
550        self.counters
551            .start_epoch
552            .store(start_epoch, Ordering::Relaxed);
553        self.counters
554            .current_epoch
555            .store(start_epoch, Ordering::Relaxed);
556        self.counters
557            .target_epoch
558            .store(target_epoch, Ordering::Relaxed);
559        self.counters.indexed.store(0, Ordering::Relaxed);
560        self.counters.skipped.store(0, Ordering::Relaxed);
561    }
562
563    fn set_current(&self, epoch: ChainEpoch) {
564        self.counters.current_epoch.store(epoch, Ordering::Relaxed);
565    }
566
567    fn inc_indexed(&self) {
568        self.counters.indexed.fetch_add(1, Ordering::Relaxed);
569    }
570
571    fn inc_skipped(&self) {
572        self.counters.skipped.fetch_add(1, Ordering::Relaxed);
573    }
574
575    fn record_cancelled(&self) {
576        BACKFILL_STATUS.record_outcome(ChainExportState::Cancelled, None);
577        self.export_guard
578            .record_outcome(ChainExportState::Idle, None);
579    }
580}
581
582impl Drop for BackfillGuard {
583    fn drop(&mut self) {
584        self.cancellation_token.cancel();
585        BACKFILL_STATUS.end();
586    }
587}
588
589/// Reads the persisted backfill checkpoint epoch, if any. A cleared checkpoint (see
590/// [`clear_backfill_checkpoint`]) is reported as `None`.
591pub fn read_backfill_checkpoint(
592    state_manager: &StateManager,
593) -> anyhow::Result<Option<ChainEpoch>> {
594    Ok(state_manager
595        .db()
596        .read_obj::<ChainEpoch>(BACKFILL_CHECKPOINT_KEY)?
597        .filter(|epoch| *epoch >= 0))
598}
599
600fn write_backfill_checkpoint(
601    state_manager: &StateManager,
602    epoch: ChainEpoch,
603) -> anyhow::Result<()> {
604    state_manager
605        .db()
606        .write_obj(BACKFILL_CHECKPOINT_KEY, &epoch)
607}
608
609fn clear_backfill_checkpoint(state_manager: &StateManager) -> anyhow::Result<()> {
610    // Write `-1`, which `read_backfill_checkpoint` treats as "no checkpoint".
611    write_backfill_checkpoint(state_manager, -1)
612}
613
614async fn process_ts(
615    ts: &Tipset,
616    state_manager: &StateManager,
617    delegated_messages: &mut Vec<(SignedMessage, u64)>,
618    allow_recompute: bool,
619) -> anyhow::Result<ProcessOutcome> {
620    let epoch = ts.epoch();
621    let tsk = ts.key().clone();
622
623    let executed = match state_manager
624        .load_executed_tipset_uncached(ts, allow_recompute)
625        .await
626    {
627        Ok(executed) => executed,
628        // Skip a tipset we can't load: its state may be GC'd, and even with recompute
629        // the lookback state can be gone. Progress beats failing a long backfill.
630        Err(e) => {
631            tracing::warn!(
632                "skipping tipset @{epoch} during backfill (state unavailable, recompute={allow_recompute}): {e:#}"
633            );
634            return Ok(ProcessOutcome::Skipped);
635        }
636    };
637    // Store the block-logs bloom; idempotent if the loader already recomputed and stored it.
638    crate::rpc::eth::store_block_logs_bloom(
639        state_manager,
640        ts,
641        &executed.state_root,
642        &executed.executed_messages,
643    )?;
644
645    delegated_messages.append(
646        &mut state_manager
647            .chain_store()
648            .headers_delegated_messages(ts.block_headers().iter())?,
649    );
650    tracing::trace!("Indexing tipset @{}: {}", epoch, &tsk);
651    tsk.save(state_manager.db())?;
652
653    Ok(ProcessOutcome::Indexed)
654}
655
656#[derive(Debug, Clone, Copy, PartialEq, Eq)]
657pub enum RangeSpec {
658    To(ChainEpoch),
659    NumTipsets(usize),
660}
661
662impl RangeSpec {
663    /// Both ingresses (`Filecoin.IndexBackfill` and `forest-tool index backfill`) parse the same
664    /// mutually exclusive pair.
665    pub fn new(to: Option<ChainEpoch>, n_tipsets: Option<usize>) -> anyhow::Result<Self> {
666        match (to, n_tipsets) {
667            (Some(to), None) => {
668                anyhow::ensure!(to >= 0, "'to' must not be negative, got {to}.");
669                Ok(Self::To(to))
670            }
671            (None, Some(n)) => Ok(Self::NumTipsets(n)),
672            (None, None) => anyhow::bail!("You must provide either 'to' or 'n_tipsets'."),
673            (Some(_), Some(_)) => anyhow::bail!("'to' and 'n_tipsets' are mutually exclusive."),
674        }
675    }
676}
677
678impl std::fmt::Display for RangeSpec {
679    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
680        match self {
681            RangeSpec::To(epoch) => write!(f, "To epoch:      {epoch}"),
682            RangeSpec::NumTipsets(n) => write!(f, "Tipsets:       {n}"),
683        }
684    }
685}
686
687/// To support the Event RPC API, a new column has been added to parity-db to handle the mapping:
688/// - Events root [`Cid`] -> [`TipsetKey`].
689///
690/// Similarly, to support the Ethereum RPC API, another column has been introduced to map:
691/// - [`struct@EthHash`] -> [`TipsetKey`],
692/// - [`struct@EthHash`] -> Delegated message [`Cid`].
693///
694/// This function traverses the chain store and populates these columns accordingly. It is a thin
695/// wrapper over [`run_backfill`] with the historical (offline) options and no cancellation.
696pub async fn backfill_db(
697    state_manager: &StateManager,
698    head_ts: &Tipset,
699    spec: RangeSpec,
700) -> anyhow::Result<()> {
701    let guard = BackfillGuard::try_start()?;
702    let result = run_backfill(
703        state_manager,
704        head_ts,
705        spec,
706        BackfillOptions::default(),
707        &guard,
708    )
709    .await;
710    let report = guard.finish(result)?;
711    tracing::info!(
712        "Total successful backfills: {} (skipped: {})",
713        report.indexed,
714        report.skipped
715    );
716    Ok(())
717}
718
719/// Hardened index backfill core shared by the offline `forest-tool index backfill` command and the
720/// online `Forest.IndexBackfill` RPC method.
721///
722/// Beyond the plain chain walk it:
723/// - clamps the start to the EC-finalized epoch unless [`BackfillOptions::allow_near_head`] is set,
724/// - commits and checkpoints in batches of [`BackfillOptions::batch_size`] so a large range is not
725///   a single transaction and can be resumed,
726/// - honors `cancel` between tipsets,
727/// - writes Ethereum mappings with newest-wins semantics so it does not clobber the live head
728///   indexer, and
729/// - re-indexes tipsets applied while the walk was running (revert-awareness).
730///
731/// Progress is published to [`BACKFILL_STATUS`] via `guard`.
732pub async fn run_backfill(
733    state_manager: &StateManager,
734    from_ts: &Tipset,
735    spec: RangeSpec,
736    options: BackfillOptions,
737    guard: &BackfillGuard,
738) -> anyhow::Result<BackfillReport> {
739    tracing::info!("Starting index backfill...");
740
741    let cancel = guard.cancellation_token();
742
743    // Subscribe before the walk so applies/reverts that happen during it are observed.
744    let head_rx = state_manager.chain_store().subscribe_head_changes();
745
746    // Optionally clamp the start below finality to avoid indexing revert-prone near-head tipsets.
747    let start_ts = if options.allow_near_head {
748        from_ts.shallow_clone()
749    } else {
750        let safe_epoch = state_manager.chain_store().ec_calculator_finalized_epoch();
751        if from_ts.epoch() > safe_epoch {
752            state_manager
753                .chain_index()
754                .load_required_tipset_by_height(
755                    safe_epoch,
756                    from_ts.shallow_clone(),
757                    crate::chain::index::ResolveNullTipset::TakeOlder,
758                )
759                .await?
760        } else {
761            from_ts.shallow_clone()
762        }
763    };
764
765    let target_epoch = match spec {
766        RangeSpec::To(to_epoch) => to_epoch,
767        // Not known exactly ahead of time; approximate for progress reporting.
768        RangeSpec::NumTipsets(n) => start_ts.epoch().saturating_sub(n as ChainEpoch),
769    };
770    guard.reset_counters(start_ts.epoch(), target_epoch);
771
772    let mut batch: Vec<(SignedMessage, u64)> = vec![];
773    let mut report = BackfillReport::default();
774    let mut processed_since_flush = 0usize;
775    let mut lowest_epoch = start_ts.epoch();
776
777    for (count, ts) in start_ts
778        .shallow_clone()
779        .chain(&state_manager.chain_store().db())
780        .enumerate()
781    {
782        match spec {
783            RangeSpec::To(to_epoch) if ts.epoch() < to_epoch => break,
784            RangeSpec::NumTipsets(n) if count >= n => break,
785            _ => {}
786        }
787
788        if cancel.is_cancelled() {
789            report.cancelled = true;
790            break;
791        }
792
793        guard.set_current(ts.epoch());
794        lowest_epoch = ts.epoch();
795        match process_ts(&ts, state_manager, &mut batch, options.allow_recompute).await? {
796            ProcessOutcome::Indexed => {
797                report.indexed += 1;
798                guard.inc_indexed();
799            }
800            ProcessOutcome::Skipped => {
801                report.skipped += 1;
802                guard.inc_skipped();
803            }
804        }
805        processed_since_flush += 1;
806
807        if processed_since_flush >= options.batch_size {
808            state_manager
809                .chain_store()
810                .process_signed_messages(&batch, true)?;
811            batch.clear();
812            write_backfill_checkpoint(state_manager, ts.epoch())?;
813            processed_since_flush = 0;
814        }
815    }
816
817    // Final commit of the trailing batch.
818    state_manager
819        .chain_store()
820        .process_signed_messages(&batch, true)?;
821    batch.clear();
822
823    // Re-index tipsets applied during the walk so the canonical mapping wins.
824    if !report.cancelled {
825        let mut extra: Vec<(SignedMessage, u64)> = vec![];
826        for changes in head_rx.try_iter() {
827            for ts in changes.applies {
828                if ts.epoch() >= lowest_epoch && ts.epoch() <= start_ts.epoch() {
829                    tracing::debug!("re-indexing tipset @{} applied during backfill", ts.epoch());
830                    if let Err(e) =
831                        process_ts(&ts, state_manager, &mut extra, options.allow_recompute).await
832                    {
833                        tracing::warn!("failed to re-index applied tipset @{}: {e:#}", ts.epoch());
834                    }
835                }
836            }
837        }
838        if !extra.is_empty() {
839            state_manager
840                .chain_store()
841                .process_signed_messages(&extra, true)?;
842        }
843    }
844
845    if report.cancelled {
846        // Persist where we stopped so the run can be resumed, and reflect the cancellation.
847        write_backfill_checkpoint(state_manager, lowest_epoch)?;
848        guard.record_cancelled();
849        tracing::info!(
850            "Index backfill cancelled after {} tipsets (skipped: {})",
851            report.indexed,
852            report.skipped
853        );
854    } else {
855        // Successful completion: clear the resume checkpoint.
856        clear_backfill_checkpoint(state_manager)?;
857    }
858
859    Ok(report)
860}
861
862#[cfg(test)]
863mod test {
864    use super::*;
865
866    // The backfill guard shares the chain-export single-flight slot, so serialize with the export
867    // tests that also touch it.
868    #[test]
869    #[serial_test::serial(chain_export)]
870    fn backfill_guard_is_single_flight_and_records_outcomes() {
871        let g = BackfillGuard::try_start().unwrap();
872        assert_eq!(BACKFILL_STATUS.snapshot().state, ChainExportState::Running);
873
874        // A second concurrent backfill is rejected while the first is running.
875        assert!(BackfillGuard::try_start().is_err());
876
877        // Succeeded is recorded via `finish`.
878        g.finish(anyhow::Ok(())).unwrap();
879        assert_eq!(
880            BACKFILL_STATUS.snapshot().state,
881            ChainExportState::Succeeded
882        );
883
884        // A new run can start once the previous one finished, and failures are recorded.
885        let g = BackfillGuard::try_start().unwrap();
886        g.finish(anyhow::Result::<()>::Err(anyhow::anyhow!("boom")))
887            .unwrap_err();
888        let snapshot = BACKFILL_STATUS.snapshot();
889        assert_eq!(snapshot.state, ChainExportState::Failed);
890        assert_eq!(snapshot.error.as_deref(), Some("boom"));
891
892        // A guard dropped without `finish` lands in `Failed`.
893        let g = BackfillGuard::try_start().unwrap();
894        drop(g);
895        assert_eq!(BACKFILL_STATUS.snapshot().state, ChainExportState::Failed);
896    }
897
898    #[test]
899    #[serial_test::serial(chain_export)]
900    fn backfill_cancellation_is_observable_and_wins() {
901        let g = BackfillGuard::try_start().unwrap();
902        // The cancel handler cancels the running backfill.
903        assert!(BACKFILL_STATUS.cancel_running());
904        assert!(g.cancellation_token().is_cancelled());
905
906        // The cooperative-cancel path records `Cancelled`, and that terminal state wins over a
907        // subsequent `finish(Ok(..))` (as happens when the walk returns a cancelled report).
908        g.record_cancelled();
909        g.finish(anyhow::Ok(())).unwrap();
910        assert_eq!(
911            BACKFILL_STATUS.snapshot().state,
912            ChainExportState::Cancelled
913        );
914
915        // With no backfill running, cancel is a no-op.
916        assert!(!BACKFILL_STATUS.cancel_running());
917    }
918
919    #[test]
920    #[serial_test::serial(chain_export)]
921    fn backfill_and_snapshot_gc_are_mutually_exclusive() {
922        use crate::ipld::{ChainExportGuard, ChainExportKind};
923
924        // A held snapshot-GC slot (which the GC now keeps across its whole export+cleanup) blocks
925        // a backfill from starting.
926        let gc = ChainExportGuard::try_start_export(ChainExportKind::SnapshotGc).unwrap();
927        assert!(BackfillGuard::try_start().is_err());
928        drop(gc);
929
930        // And a running backfill blocks a snapshot-GC export from starting.
931        let bf = BackfillGuard::try_start().unwrap();
932        assert!(ChainExportGuard::try_start_export(ChainExportKind::SnapshotGc).is_err());
933        drop(bf);
934    }
935
936    fn test_state_manager() -> StateManager {
937        use crate::blocks::{CachingBlockHeader, RawBlockHeader};
938        use crate::chain::ChainStore;
939        use crate::networks::ChainConfig;
940        use crate::shim::address::Address;
941
942        let db = Arc::new(crate::db::MemoryDB::default());
943        let genesis = CachingBlockHeader::new(RawBlockHeader {
944            miner_address: Address::new_id(0),
945            timestamp: 7777,
946            ..Default::default()
947        });
948        let cs = ChainStore::new(db, Arc::new(ChainConfig::default()), genesis).unwrap();
949        StateManager::new(cs).unwrap()
950    }
951
952    #[test]
953    fn backfill_checkpoint_roundtrip_and_clear() {
954        let sm = test_state_manager();
955
956        // No checkpoint initially: a fresh run starts at the head.
957        assert_eq!(read_backfill_checkpoint(&sm).unwrap(), None);
958
959        write_backfill_checkpoint(&sm, 4321).unwrap();
960        assert_eq!(read_backfill_checkpoint(&sm).unwrap(), Some(4321));
961
962        // Clearing writes a sentinel that reads back as "no checkpoint", so a completed run is not
963        // later mistaken for a resumable one.
964        clear_backfill_checkpoint(&sm).unwrap();
965        assert_eq!(read_backfill_checkpoint(&sm).unwrap(), None);
966    }
967
968    #[tokio::test]
969    async fn import_snapshot_from_file_valid() {
970        for import_mode in [ImportMode::Auto, ImportMode::Copy, ImportMode::Move] {
971            import_snapshot_from_file("test-snapshots/chain4.car", import_mode)
972                .await
973                .unwrap();
974        }
975
976        // Linking is not supported for raw CAR files.
977        for import_mode in [ImportMode::Symlink, ImportMode::Hardlink] {
978            import_snapshot_from_file("test-snapshots/chain4.car", import_mode)
979                .await
980                .unwrap_err();
981        }
982    }
983
984    #[tokio::test]
985    async fn import_snapshot_from_compressed_file_valid() {
986        for import_mode in [ImportMode::Auto, ImportMode::Copy, ImportMode::Move] {
987            import_snapshot_from_file("test-snapshots/chain4.car.zst", import_mode)
988                .await
989                .unwrap();
990        }
991
992        // Linking is not supported for raw CAR files.
993        for import_mode in [ImportMode::Symlink, ImportMode::Hardlink] {
994            import_snapshot_from_file("test-snapshots/chain4.car", import_mode)
995                .await
996                .unwrap_err();
997        }
998    }
999
1000    #[tokio::test]
1001    async fn import_snapshot_from_forest_car_valid() {
1002        for import_mode in [
1003            ImportMode::Auto,
1004            ImportMode::Copy,
1005            ImportMode::Move,
1006            ImportMode::Symlink,
1007            ImportMode::Hardlink,
1008        ] {
1009            import_snapshot_from_file("test-snapshots/chain4.forest.car.zst", import_mode)
1010                .await
1011                .unwrap();
1012        }
1013    }
1014
1015    #[tokio::test]
1016    async fn import_snapshot_from_file_invalid() {
1017        for import_mode in &[
1018            ImportMode::Auto,
1019            ImportMode::Copy,
1020            ImportMode::Move,
1021            ImportMode::Symlink,
1022            ImportMode::Hardlink,
1023        ] {
1024            import_snapshot_from_file("Cargo.toml", *import_mode)
1025                .await
1026                .unwrap_err();
1027        }
1028    }
1029
1030    #[tokio::test]
1031    async fn import_snapshot_from_file_not_found() {
1032        for import_mode in &[
1033            ImportMode::Auto,
1034            ImportMode::Copy,
1035            ImportMode::Move,
1036            ImportMode::Symlink,
1037            ImportMode::Hardlink,
1038        ] {
1039            import_snapshot_from_file("dummy.car", *import_mode)
1040                .await
1041                .unwrap_err();
1042        }
1043    }
1044
1045    #[tokio::test]
1046    async fn import_snapshot_from_url_not_found() {
1047        for import_mode in &[
1048            ImportMode::Auto,
1049            ImportMode::Copy,
1050            ImportMode::Move,
1051            ImportMode::Symlink,
1052            ImportMode::Hardlink,
1053        ] {
1054            import_snapshot_from_file("https://forest.chainsafe.io/dummy.car", *import_mode)
1055                .await
1056                .unwrap_err();
1057        }
1058    }
1059
1060    async fn import_snapshot_from_file(
1061        file_path: &str,
1062        import_mode: ImportMode,
1063    ) -> anyhow::Result<()> {
1064        // Prevent modifications on the original file, e.g., deletion via `ImportMode::Move`.
1065        let temp_file = tempfile::Builder::new().tempfile()?;
1066        fs::copy(Path::new(file_path), temp_file.path())?;
1067        let file_path = temp_file.path();
1068
1069        let temp_db_dir = tempfile::Builder::new().tempdir()?;
1070
1071        let (path, ts) = import_chain_as_forest_car(
1072            file_path,
1073            temp_db_dir.path(),
1074            import_mode,
1075            "http://127.0.0.1:2345/rpc/v1".parse().unwrap(),
1076            Path::new("test"),
1077            &ChainConfig::devnet(),
1078            &SnapshotProgressTracker::default(),
1079        )
1080        .await?;
1081        match import_mode {
1082            ImportMode::Symlink => {
1083                assert_eq!(
1084                    std::path::absolute(path.read_link()?)?,
1085                    std::path::absolute(file_path)?
1086                );
1087            }
1088            ImportMode::Move => {
1089                assert!(!file_path.exists());
1090                assert!(path.is_file());
1091            }
1092            _ => {
1093                assert!(file_path.is_file());
1094                assert!(path.is_file());
1095            }
1096        }
1097        assert!(ts.epoch() > 0);
1098        Ok(())
1099    }
1100}