Skip to main content

forest/cli/subcommands/
snapshot_cmd.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4use crate::chain::FilecoinSnapshotVersion;
5use crate::chain_sync::chain_muxer::DEFAULT_RECENT_STATE_ROOTS;
6use crate::cli_shared::snapshot::{self, TrustedVendor};
7use crate::db::car::forest::tmp_exporting_forest_car_path;
8use crate::networks::calibnet;
9use crate::prelude::*;
10use crate::rpc::chain::ForestChainExportDiffParams;
11use crate::rpc::types::ApiExportResult;
12use crate::rpc::{self, chain::ForestChainExportParams, prelude::*};
13use crate::shim::policy::policy_constants::CHAIN_FINALITY;
14use chrono::DateTime;
15use clap::Subcommand;
16use indicatif::{ProgressBar, ProgressStyle};
17use std::{path::PathBuf, time::Duration};
18use tokio_util::sync::CancellationToken;
19
20#[derive(Debug, Clone, clap::ValueEnum)]
21pub enum Format {
22    Json,
23    Text,
24}
25
26#[derive(Debug, Subcommand)]
27pub enum SnapshotCommands {
28    /// Export a snapshot of the chain to `<output_path>`
29    Export {
30        /// `./forest_snapshot_{chain}_{year}-{month}-{day}_height_{epoch}.car.zst`.
31        #[arg(short, long, default_value = ".", verbatim_doc_comment)]
32        output_path: PathBuf,
33        /// Skip creating the checksum file.
34        #[arg(long)]
35        skip_checksum: bool,
36        /// Don't write the archive.
37        #[arg(long)]
38        dry_run: bool,
39        /// Tipset to start the export from, default is the chain head
40        #[arg(short, long)]
41        tipset: Option<i64>,
42        /// How many state trees to include. 0 for chain spine with no state trees.
43        #[arg(short, long, default_value_t = DEFAULT_RECENT_STATE_ROOTS)]
44        depth: crate::chain::ChainEpochDelta,
45        /// Snapshot format to export.
46        #[arg(long, value_enum, default_value_t = FilecoinSnapshotVersion::V2)]
47        format: FilecoinSnapshotVersion,
48    },
49    /// Show status of the current export.
50    ExportStatus {
51        /// Wait until it completes and print progress.
52        #[arg(long)]
53        wait: bool,
54        /// Format of the output. `json` or `text`.
55        #[arg(long, value_enum, default_value_t = Format::Text)]
56        format: Format,
57    },
58    /// Cancel the current export.
59    ExportCancel {},
60    /// Export a diff snapshot between `from` and `to` epochs to `<output_path>`
61    ExportDiff {
62        /// `./forest_snapshot_diff_{chain}_{from}_{to}+{depth}.car.zst`.
63        #[arg(short, long, default_value = ".", verbatim_doc_comment)]
64        output_path: PathBuf,
65        /// Epoch to export from
66        #[arg(long)]
67        from: i64,
68        /// Epoch to diff against
69        #[arg(long)]
70        to: i64,
71        /// How many state-roots to include. Lower limit is 900 for `calibnet` and `mainnet`.
72        #[arg(short, long)]
73        depth: Option<crate::chain::ChainEpochDelta>,
74    },
75}
76
77impl SnapshotCommands {
78    pub async fn run(self, client: rpc::Client) -> anyhow::Result<()> {
79        match self {
80            Self::Export {
81                output_path,
82                skip_checksum,
83                dry_run,
84                tipset,
85                depth,
86                format,
87            } => {
88                anyhow::ensure!(
89                    depth >= 0,
90                    "--depth must be non-negative; use 0 for spine-only snapshots"
91                );
92
93                if depth < CHAIN_FINALITY {
94                    tracing::warn!(
95                        "Depth {depth} should be no less than CHAIN_FINALITY {CHAIN_FINALITY} to export a valid lite snapshot"
96                    );
97                }
98
99                let raw_network_name = StateNetworkName::call(&client, ()).await?;
100                // For historical reasons and backwards compatibility if snapshot services or their
101                // consumers relied on the `calibnet`, we use `calibnet` as the chain name.
102                let chain_name = if raw_network_name == calibnet::NETWORK_GENESIS_NAME {
103                    calibnet::NETWORK_COMMON_NAME
104                } else {
105                    raw_network_name.as_str()
106                };
107
108                let tipset = if let Some(epoch) = tipset {
109                    // This could take a while when the requested epoch is far behind the chain head
110                    client
111                        .call(
112                            ChainGetTipSetByHeight::request((epoch, Default::default()))?
113                                .with_timeout(Duration::from_secs(60 * 15)),
114                        )
115                        .await?
116                } else {
117                    ChainHead::call(&client, ()).await?
118                };
119
120                let output_path = std::path::absolute(match output_path.is_dir() {
121                    true => output_path.join(snapshot::filename(
122                        TrustedVendor::Forest,
123                        chain_name,
124                        DateTime::from_timestamp(tipset.min_ticket_block().timestamp as i64, 0)
125                            .unwrap_or_default()
126                            .naive_utc()
127                            .date(),
128                        tipset.epoch(),
129                        true,
130                    )),
131                    false => output_path.clone(),
132                })
133                .context("failed to make output path absolute")?;
134
135                let params = ForestChainExportParams {
136                    version: format,
137                    epoch: tipset.epoch(),
138                    recent_roots: depth,
139                    output_path: output_path.clone(),
140                    tipset_keys: tipset.key().clone().into(),
141                    include_receipts: false,
142                    include_events: false,
143                    include_tipset_keys: false,
144                    include_tipset_lookup: false,
145                    skip_checksum,
146                    dry_run,
147                };
148
149                let pb = ProgressBar::new_spinner().with_style(
150                    ProgressStyle::with_template(
151                        "{spinner} {msg} {binary_total_bytes} written in {elapsed} ({binary_bytes_per_sec})",
152                    )
153                    .expect("indicatif template must be valid"),
154                ).with_message(format!("Exporting v{} snapshot to {} ...", format as u64, output_path.display()));
155                pb.enable_steady_tick(std::time::Duration::from_millis(80));
156                let handle = tokio::spawn({
157                    let path = tmp_exporting_forest_car_path(&output_path);
158                    let pb = pb.clone();
159                    let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(1));
160                    async move {
161                        loop {
162                            interval.tick().await;
163                            if let Ok(meta) = std::fs::metadata(&path) {
164                                pb.set_position(meta.len());
165                            }
166                        }
167                    }
168                });
169
170                // Manually construct RpcRequest because snapshot export could
171                // take a few hours on mainnet
172                let export_result = client
173                    .call(ForestChainExport::request((params,))?.with_timeout(Duration::MAX))
174                    .await?;
175
176                handle.abort();
177                pb.finish();
178                _ = handle.await;
179
180                match export_result {
181                    ApiExportResult::Done => {
182                        println!("Export completed.");
183                    }
184                    ApiExportResult::Cancelled => {
185                        println!("Export cancelled.");
186                    }
187                }
188                Ok(())
189            }
190            Self::ExportStatus { wait, format } => {
191                let result = client
192                    .call(
193                        ForestChainExportStatus::request(())?.with_timeout(Duration::from_secs(30)),
194                    )
195                    .await?;
196                if !result.exporting
197                    && let Format::Text = format
198                {
199                    if result.cancelled {
200                        println!("No export in progress (last export was cancelled)");
201                    } else {
202                        println!("No export in progress");
203                    }
204                    return Ok(());
205                }
206                if wait {
207                    let elapsed = chrono::Utc::now()
208                        .signed_duration_since(result.start_time.unwrap_or_default())
209                        .to_std()
210                        .unwrap_or(Duration::ZERO);
211                    let pb = ProgressBar::new(10000)
212                        .with_elapsed(elapsed)
213                        .with_message("Exporting");
214                    pb.set_style(
215                        ProgressStyle::with_template(
216                            "[{elapsed_precise}] [{wide_bar}] {percent}% {msg} ",
217                        )
218                        .expect("indicatif template must be valid")
219                        .progress_chars("#>-"),
220                    );
221                    loop {
222                        let result = client
223                            .call(
224                                ForestChainExportStatus::request(())?
225                                    .with_timeout(Duration::from_secs(30)),
226                            )
227                            .await?;
228                        if result.cancelled {
229                            pb.set_message("Export cancelled");
230                            pb.abandon();
231                            return Ok(());
232                        }
233                        let position = (result.progress.clamp(0.0, 1.0) * 10000.0).trunc() as u64;
234                        pb.set_position(position);
235
236                        if !result.exporting {
237                            break;
238                        }
239                        tokio::time::sleep(Duration::from_millis(500)).await;
240                    }
241
242                    pb.finish_with_message(if result.succeeded {
243                        "Export completed"
244                    } else {
245                        "Export failed"
246                    });
247
248                    return Ok(());
249                }
250                match format {
251                    Format::Text => {
252                        println!("Exporting: {:.1}%", result.progress.clamp(0.0, 1.0) * 100.0);
253                    }
254                    Format::Json => {
255                        println!("{}", serde_json::to_string_pretty(&result)?);
256                    }
257                }
258
259                Ok(())
260            }
261            Self::ExportCancel {} => {
262                let result = client
263                    .call(
264                        ForestChainExportCancel::request(())?.with_timeout(Duration::from_secs(30)),
265                    )
266                    .await?;
267                if result {
268                    println!("Export cancelled.");
269                } else {
270                    println!("No export in progress to cancel.");
271                }
272                Ok(())
273            }
274            Self::ExportDiff {
275                output_path,
276                from,
277                to,
278                depth,
279            } => {
280                let raw_network_name = StateNetworkName::call(&client, ()).await?;
281
282                // For historical reasons and backwards compatibility if snapshot services or their
283                // consumers relied on the `calibnet`, we use `calibnet` as the chain name.
284                let chain_name = if raw_network_name == calibnet::NETWORK_GENESIS_NAME {
285                    calibnet::NETWORK_COMMON_NAME
286                } else {
287                    raw_network_name.as_str()
288                };
289
290                let depth = depth.unwrap_or_else(|| from - to);
291                anyhow::ensure!(depth > 0, "depth must be positive");
292
293                let output_path = std::path::absolute(match output_path.is_dir() {
294                    true => output_path.join(format!(
295                        "forest_snapshot_diff_{chain_name}_{from}_{to}+{depth}.car.zst"
296                    )),
297                    false => output_path.clone(),
298                })
299                .context("failed to make output path absolute")?;
300
301                let params = ForestChainExportDiffParams {
302                    output_path: output_path.clone(),
303                    from,
304                    to,
305                    depth,
306                };
307
308                let pb = ProgressBar::new_spinner().with_style(
309                    ProgressStyle::with_template(
310                        "{spinner} {msg} {binary_total_bytes} written in {elapsed} ({binary_bytes_per_sec})",
311                    )
312                    .expect("indicatif template must be valid"),
313                ).with_message(format!("Exporting {} ...", output_path.display()));
314                pb.enable_steady_tick(std::time::Duration::from_millis(80));
315                let cancellation_token = CancellationToken::new();
316                // Make sure token is cancelled on error path
317                let _cancellation_token_drop_guard = cancellation_token.drop_guard_ref();
318                let handle = tokio::spawn({
319                    let cancellation_token = cancellation_token.clone();
320                    let path = tmp_exporting_forest_car_path(&output_path);
321                    let pb = pb.clone();
322                    let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(1));
323                    async move {
324                        while !cancellation_token.is_cancelled() {
325                            interval.tick().await;
326                            if let Ok(meta) = std::fs::metadata(&path) {
327                                pb.set_position(meta.len());
328                            }
329                        }
330                    }
331                });
332                // Manually construct RpcRequest because snapshot export could
333                // take a few hours on mainnet
334                let export_result = client
335                    .call(ForestChainExportDiff::request((params,))?.with_timeout(Duration::MAX))
336                    .await?;
337                // cancel before `handle.await` to avoid deadlock
338                cancellation_token.cancel();
339                pb.finish();
340                _ = handle.await;
341
342                match export_result {
343                    ApiExportResult::Done => {
344                        println!("Export completed.");
345                    }
346                    ApiExportResult::Cancelled => {
347                        println!("Export cancelled.");
348                    }
349                }
350                Ok(())
351            }
352        }
353    }
354}