diff --git a/.github/workflows/forest.yml b/.github/workflows/forest.yml index fb8f4a18a6e6..4cbe014f3ea3 100644 --- a/.github/workflows/forest.yml +++ b/.github/workflows/forest.yml @@ -306,6 +306,28 @@ jobs: - name: Snapshot export check v2 run: ./scripts/tests/calibnet_export_f3_check.sh timeout-minutes: ${{ fromJSON(env.SCRIPT_TIMEOUT_MINUTES) }} + calibnet-unordered-export-check: + needs: + - build-ubuntu + name: Snapshot unordered export checks + runs-on: ubuntu-24.04 + steps: + - run: lscpu + - uses: actions/cache@v4 + with: + path: "${{ env.FIL_PROOFS_PARAMETER_CACHE }}" + key: proof-params-keys + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + name: "forest-${{ runner.os }}" + path: ~/.cargo/bin + - name: Set permissions + run: | + chmod +x ~/.cargo/bin/forest* + - name: Snapshot unordered export check + run: ./scripts/tests/calibnet_export_unordered_check.sh + timeout-minutes: ${{ fromJSON(env.SCRIPT_TIMEOUT_MINUTES) }} calibnet-no-discovery-checks: needs: - build-ubuntu @@ -577,6 +599,7 @@ jobs: - calibnet-wallet-check - calibnet-export-check - calibnet-export-check-v2 + - calibnet-unordered-export-check - calibnet-no-discovery-checks - calibnet-kademlia-checks - calibnet-eth-mapping-check diff --git a/CHANGELOG.md b/CHANGELOG.md index 12500a3ebc06..344ddeb7fd4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,8 @@ - [#5859](https://github.com/ChainSafe/forest/pull/5859) Added size metrics for zstd frame cache and made max size configurable via `FOREST_ZSTD_FRAME_CACHE_DEFAULT_MAX_SIZE` environment variable. +- [#5867](https://github.com/ChainSafe/forest/pull/5867) Added `--unordered` to `forest-cli snapshot export` for exporting `CAR` blocks in non-deterministic order for better performance with more parallelization. + - [#5886](https://github.com/ChainSafe/forest/issues/5886) Add `forest-tool archive merge-f3` subcommand for merging a v1 Filecoin snapshot and an F3 snapshot into a v2 Filecoin snapshot. - [#4976](https://github.com/ChainSafe/forest/issues/4976) Add support for the `Filecoin.EthSubscribe` and `Filecoin.EthUnsubscribe` API methods to enable subscriptions to Ethereum event types: `heads` and `logs`. diff --git a/scripts/tests/calibnet_export_unordered_check.sh b/scripts/tests/calibnet_export_unordered_check.sh new file mode 100755 index 000000000000..fd2e8e42ce6f --- /dev/null +++ b/scripts/tests/calibnet_export_unordered_check.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# This script is checking the correctness of +# the snapshot export feature. +# It requires both the `forest` and `forest-cli` binaries to be in the PATH. + +set -eu + +source "$(dirname "$0")/harness.sh" + +forest_init "$@" + +echo "Cleaning up the initial snapshot" +rm --force --verbose ./*.{car,car.zst,sha256sum} + +echo "Exporting zstd compressed snapshot with unordred graph traversal" +$FOREST_CLI_PATH snapshot export --unordered -o unordered.forest.car.zst + +$FOREST_CLI_PATH shutdown --force + +for f in *.car.zst; do + echo "Inspecting archive info $f" + $FOREST_TOOL_PATH archive info "$f" + echo "Inspecting archive metadata $f" + $FOREST_TOOL_PATH archive metadata "$f" +done + +echo "Cleanup calibnet db" +$FOREST_TOOL_PATH db destroy --chain calibnet --force + +echo "Import the unordered snapshot" +$FOREST_PATH --chain calibnet --encrypt-keystore false --halt-after-import --height=-100 --import-snapshot unordered.forest.car.zst + +echo "Check if Forest is able to sync" +forest_run_node_detached +forest_wait_api +forest_wait_for_sync +forest_wait_for_healthcheck_ready diff --git a/src/chain/mod.rs b/src/chain/mod.rs index 9e5868b1e58f..d30c7cc90456 100644 --- a/src/chain/mod.rs +++ b/src/chain/mod.rs @@ -13,7 +13,7 @@ use crate::blocks::{Tipset, TipsetKey}; use crate::cid_collections::CidHashSet; use crate::db::car::forest::{self, ForestCarFrame, finalize_frame}; use crate::db::{SettingsStore, SettingsStoreExt}; -use crate::ipld::stream_chain; +use crate::ipld::{stream_chain, unordered_stream_chain}; use crate::utils::db::car_stream::{CarBlock, CarBlockWrite}; use crate::utils::io::{AsyncWriterWithChecksum, Checksum}; use crate::utils::multihash::MultihashCode; @@ -31,17 +31,23 @@ use std::io::{Seek as _, SeekFrom}; use std::sync::Arc; use tokio::io::{AsyncWrite, AsyncWriteExt, BufWriter}; +#[derive(Debug, Clone, Default)] +pub struct ExportOptions { + pub skip_checksum: bool, + pub unordered: bool, + pub seen: CidHashSet, +} + pub async fn export_from_head( db: &Arc, lookup_depth: ChainEpochDelta, writer: impl AsyncWrite + Unpin, - seen: CidHashSet, - skip_checksum: bool, + options: Option, ) -> anyhow::Result<(Tipset, Option>)> { let head_key = SettingsStoreExt::read_obj::(db, crate::db::setting_keys::HEAD_KEY)? .context("chain head key not found")?; let head_ts = Tipset::load_required(&db, &head_key)?; - let digest = export::(db, &head_ts, lookup_depth, writer, seen, skip_checksum).await?; + let digest = export::(db, &head_ts, lookup_depth, writer, options).await?; Ok((head_ts, digest)) } @@ -52,21 +58,10 @@ pub async fn export( tipset: &Tipset, lookup_depth: ChainEpochDelta, writer: impl AsyncWrite + Unpin, - seen: CidHashSet, - skip_checksum: bool, + options: Option, ) -> anyhow::Result>> { let roots = tipset.key().to_cids(); - export_to_forest_car::( - roots, - None, - db, - tipset, - lookup_depth, - writer, - seen, - skip_checksum, - ) - .await + export_to_forest_car::(roots, None, db, tipset, lookup_depth, writer, options).await } /// Exports a Filecoin snapshot in v2 format @@ -77,8 +72,7 @@ pub async fn export_v2( tipset: &Tipset, lookup_depth: ChainEpochDelta, writer: impl AsyncWrite + Unpin, - seen: CidHashSet, - skip_checksum: bool, + options: Option, ) -> anyhow::Result>> { // validate f3 data if let Some((f3_cid, f3_data)) = &mut f3 { @@ -131,8 +125,7 @@ pub async fn export_v2( tipset, lookup_depth, writer, - seen, - skip_checksum, + options, ) .await } @@ -145,9 +138,14 @@ async fn export_to_forest_car( tipset: &Tipset, lookup_depth: ChainEpochDelta, writer: impl AsyncWrite + Unpin, - seen: CidHashSet, - skip_checksum: bool, + options: Option, ) -> anyhow::Result>> { + let ExportOptions { + skip_checksum, + unordered, + seen, + } = options.unwrap_or_default(); + let stateroot_lookup_limit = tipset.epoch() - lookup_depth; // Wrap writer in optional checksum calculator @@ -160,12 +158,25 @@ async fn export_to_forest_car( // are small enough that keeping 1k in memory isn't a problem. Average // block size is between 1kb and 2kb. 1024, - stream_chain( - Arc::clone(db), - tipset.clone().chain_owned(Arc::clone(db)), - stateroot_lookup_limit, - ) - .with_seen(seen), + if unordered { + futures::future::Either::Left( + unordered_stream_chain( + Arc::clone(db), + tipset.clone().chain_owned(Arc::clone(db)), + stateroot_lookup_limit, + ) + .with_seen(seen), + ) + } else { + futures::future::Either::Right( + stream_chain( + Arc::clone(db), + tipset.clone().chain_owned(Arc::clone(db)), + stateroot_lookup_limit, + ) + .with_seen(seen), + ) + }, ); // Encode Ipld key-value pairs in zstd frames diff --git a/src/chain/tests.rs b/src/chain/tests.rs index ad680853230f..973a5c60a4af 100644 --- a/src/chain/tests.rs +++ b/src/chain/tests.rs @@ -59,19 +59,10 @@ async fn test_export_inner(version: FilecoinSnapshotVersion) -> anyhow::Result<( let checksum = match version { FilecoinSnapshotVersion::V1 => { - export::(&db, &head, 0, &mut car_bytes, Default::default(), false).await? + export::(&db, &head, 0, &mut car_bytes, None).await? } FilecoinSnapshotVersion::V2 => { - export_v2::( - &db, - None, - &head, - 0, - &mut car_bytes, - Default::default(), - false, - ) - .await? + export_v2::(&db, None, &head, 0, &mut car_bytes, None).await? } }; diff --git a/src/cli/subcommands/snapshot_cmd.rs b/src/cli/subcommands/snapshot_cmd.rs index 8bf398a9950d..e93fa4942dad 100644 --- a/src/cli/subcommands/snapshot_cmd.rs +++ b/src/cli/subcommands/snapshot_cmd.rs @@ -36,6 +36,9 @@ pub enum SnapshotCommands { /// How many state-roots to include. Lower limit is 900 for `calibnet` and `mainnet`. #[arg(short, long)] depth: Option, + /// Traverse chain in non-deterministic order for better performance with more parallelization. + #[arg(long)] + unordered: bool, /// Export snapshot in the experimental v2 format(FRC-0108). #[arg(long, value_enum, default_value_t = FilecoinSnapshotVersion::V1)] format: FilecoinSnapshotVersion, @@ -51,6 +54,7 @@ impl SnapshotCommands { dry_run, tipset, depth, + unordered, format, } => { let chain_head = ChainHead::call(&client, ()).await?; @@ -93,6 +97,7 @@ impl SnapshotCommands { recent_roots: depth.unwrap_or(SyncConfig::default().recent_state_roots), output_path: temp_path.to_path_buf(), tipset_keys: ApiTipsetKey(Some(chain_head.key().clone())), + unordered, skip_checksum, dry_run, }; @@ -128,10 +133,12 @@ impl SnapshotCommands { pb.finish(); _ = handle.await; - if let Some(hash) = hash_result { - save_checksum(&output_path, hash).await?; + if !dry_run { + if let Some(hash) = hash_result { + save_checksum(&output_path, hash).await?; + } + temp_path.persist(output_path)?; } - temp_path.persist(output_path)?; println!("Export completed."); Ok(()) diff --git a/src/db/gc/snapshot.rs b/src/db/gc/snapshot.rs index d00889b5df39..65338bdab35d 100644 --- a/src/db/gc/snapshot.rs +++ b/src/db/gc/snapshot.rs @@ -43,7 +43,7 @@ //! use crate::blocks::{Tipset, TipsetKey}; -use crate::cid_collections::CidHashSet; +use crate::chain::ExportOptions; use crate::cli_shared::chain_path; use crate::db::car::forest::new_forest_car_temp_path_in; use crate::db::{ @@ -223,8 +223,10 @@ where &db, self.recent_state_roots, file, - CidHashSet::default(), - true, + Some(ExportOptions { + skip_checksum: true, + ..Default::default() + }), ) .await?; let target_path = self.car_db_dir.join(format!( diff --git a/src/ipld/util.rs b/src/ipld/util.rs index 4127eac10deb..f530b3b25a8b 100644 --- a/src/ipld/util.rs +++ b/src/ipld/util.rs @@ -10,12 +10,13 @@ use crate::utils::encoding::extract_cids; use crate::utils::multihash::prelude::*; use anyhow::Context as _; use cid::Cid; -use flume::TryRecvError; -use futures::Stream; +use futures::stream::Fuse; +use futures::{Stream, StreamExt}; use fvm_ipld_blockstore::Blockstore; use parking_lot::Mutex; use pin_project_lite::pin_project; use std::borrow::Borrow; +use std::fmt::Display; use std::ops::DerefMut; use std::pin::Pin; use std::task::{Context, Poll}; @@ -278,26 +279,31 @@ impl, ITER: Iterator + Unpin> Stream } pin_project! { - pub struct UnorderedChainStream { + pub struct UnorderedChainStream<'a, DB, T> { tipset_iter: T, db: Arc, seen: Arc>, worker_handle: JoinHandle>, - block_receiver: flume::Receiver>, - extract_sender: flume::Sender, + block_recv_stream: Fuse>>, + extract_sender: Option>, stateroot_limit: ChainEpoch, queue: Vec<(Cid, Option>)>, fail_on_dead_links: bool, } - impl PinnedDrop for UnorderedChainStream { + impl<'a, DB, T> PinnedDrop for UnorderedChainStream<'a, DB, T> { fn drop(this: Pin<&mut Self>) { this.worker_handle.abort() } } } -impl UnorderedChainStream { +impl<'a, DB, T> UnorderedChainStream<'a, DB, T> { + pub fn with_seen(self, seen: CidHashSet) -> Self { + *self.seen.lock() = seen; + self + } + pub fn into_seen(self) -> CidHashSet { let mut set = CidHashSet::new(); let mut guard = self.seen.lock(); @@ -308,6 +314,7 @@ impl UnorderedChainStream { } fn unordered_stream_chain_inner< + 'a, DB: Blockstore + Sync + Send + 'static, T: Borrow, ITER: Iterator + Unpin + Send + 'static, @@ -316,13 +323,13 @@ fn unordered_stream_chain_inner< tipset_iter: ITER, stateroot_limit: ChainEpoch, fail_on_dead_links: bool, -) -> UnorderedChainStream { +) -> UnorderedChainStream<'a, DB, ITER> { let (sender, receiver) = flume::bounded(BLOCK_CHANNEL_LIMIT); let (extract_sender, extract_receiver) = flume::unbounded(); let seen = Arc::new(Mutex::new(CidHashSet::default())); - let handle = UnorderedChainStream::::start_workers( + let worker_handle = UnorderedChainStream::::start_workers( db.clone(), - sender.clone(), + sender, extract_receiver, seen.clone(), fail_on_dead_links, @@ -331,10 +338,10 @@ fn unordered_stream_chain_inner< UnorderedChainStream { seen, db, - worker_handle: handle, - block_receiver: receiver, + worker_handle, + block_recv_stream: receiver.into_stream().fuse(), queue: Vec::new(), - extract_sender, + extract_sender: Some(extract_sender), tipset_iter, stateroot_limit, fail_on_dead_links, @@ -351,8 +358,8 @@ fn unordered_stream_chain_inner< /// * `stateroot_limit` - An epoch that signifies how far back we need to inspect tipsets, in-depth. /// This has to be pre-calculated using this formula: `$cur_epoch - $depth`, where `$depth` is the /// number of `[`Tipset`]` that needs inspection. -#[allow(dead_code)] pub fn unordered_stream_chain< + 'a, DB: Blockstore + Sync + Send + 'static, T: Borrow, ITER: Iterator + Unpin + Send + 'static, @@ -360,13 +367,14 @@ pub fn unordered_stream_chain< db: Arc, tipset_iter: ITER, stateroot_limit: ChainEpoch, -) -> UnorderedChainStream { +) -> UnorderedChainStream<'a, DB, ITER> { unordered_stream_chain_inner(db, tipset_iter, stateroot_limit, true) } // Stream available graph in unordered search. All reachable nodes are touched and dead-links // are ignored. pub fn unordered_stream_graph< + 'a, DB: Blockstore + Sync + Send + 'static, T: Borrow, ITER: Iterator + Unpin + Send + 'static, @@ -374,12 +382,16 @@ pub fn unordered_stream_graph< db: Arc, tipset_iter: ITER, stateroot_limit: ChainEpoch, -) -> UnorderedChainStream { +) -> UnorderedChainStream<'a, DB, ITER> { unordered_stream_chain_inner(db, tipset_iter, stateroot_limit, false) } -impl, ITER: Iterator + Unpin> - UnorderedChainStream +impl< + 'a, + DB: Blockstore + Send + Sync + 'static, + T: Borrow, + ITER: Iterator + Unpin, +> UnorderedChainStream<'a, DB, ITER> { fn start_workers( db: Arc, @@ -390,8 +402,7 @@ impl, ITER: Iterator JoinHandle> { task::spawn(async move { let mut handles = JoinSet::new(); - - for _ in 0..num_cpus::get() { + for _ in 0..num_cpus::get().clamp(1, 8) { let seen = seen.clone(); let extract_receiver = extract_receiver.clone(); let db = db.clone(); @@ -407,14 +418,19 @@ impl, ITER: Iterator, ITER: Iterator + Unpin> Stream - for UnorderedChainStream +impl<'a, DB: Blockstore + Send + Sync + 'static, T: Iterator + Unpin> Stream + for UnorderedChainStream<'a, DB, T> { type Item = anyhow::Result; - fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + fn send(sender: &Option>, v: T) -> anyhow::Result<()> { + if let Some(sender) = sender { + sender + .send(v) + .map_err(|e| anyhow::anyhow!("failed to send {}", e.into_inner())) + } else { + anyhow::bail!("attempted to enqueue after shutdown (extract_sender dropped): {v}"); + } + } + + fn process_cid( + cid: Cid, + db: &DB, + extract_sender: &Option>, + queue: &mut Vec<(Cid, Option>)>, + seen: &Arc>, + fail_on_dead_links: bool, + ) -> anyhow::Result<()> { + if should_save_block_to_snapshot(cid) { + if db.has(&cid)? { + send(extract_sender, cid)?; + } else if fail_on_dead_links { + queue.push((cid, None)); + } else { + seen.lock().insert(cid); + } + } + Ok(()) + } + let stateroot_limit = self.stateroot_limit; let fail_on_dead_links = self.fail_on_dead_links; - let this = self.project(); - let receive_block = || { - if let Ok(item) = this.block_receiver.try_recv() { - return Some(item); - } - None - }; + loop { - while let Some((cid, data)) = this.queue.pop() { + while let Some((cid, data)) = self.queue.pop() { if let Some(data) = data { return Poll::Ready(Some(Ok(CarBlock { cid, data }))); - } else if let Some(data) = this.db.get(&cid)? { + } else if let Some(data) = self.db.get(&cid)? { return Poll::Ready(Some(Ok(CarBlock { cid, data }))); } else if fail_on_dead_links { return Poll::Ready(Some(Err(anyhow::anyhow!("missing key: {cid}")))); } } - if let Some(block) = receive_block() { - return Poll::Ready(Some(block)); - } - - // This consumes a [`Tipset`] from the iterator one at a time. Workers are then processing - // the extract queue. The emit queue is processed in the loop above. Once the desired depth - // has been reached yield a block without walking the graph it represents. - if let Some(tipset) = this.tipset_iter.next() { - for block in tipset.into_block_headers().into_iter() { - let (cid, data) = block.car_block()?; - if this.seen.lock().insert(cid) { - // Make sure we always yield a block, directly to the stream to avoid extra - // work. - this.queue.push((cid, Some(data))); - - if block.epoch == 0 { - // The genesis block has some kind of dummy parent that needs to be emitted. - for p in &block.parents { - this.queue.push((p, None)); - } - } + match Pin::new(&mut self.block_recv_stream).poll_next(cx) { + Poll::Ready(None) => return Poll::Ready(None), + Poll::Ready(Some(block)) => return Poll::Ready(Some(block)), + _ => { + let self_mut = self.as_mut(); + let this = self_mut.project(); + // This consumes a [`Tipset`] from the iterator one at a time. Workers are then processing + // the extract queue. The emit queue is processed in the loop above. Once the desired depth + // has been reached yield a block without walking the graph it represents. + if let Some(tipset) = this.tipset_iter.next() { + for block in tipset.into_block_headers().into_iter() { + let (cid, data) = block.car_block()?; + if this.seen.lock().insert(cid) { + // Make sure we always yield a block, directly to the stream to avoid extra + // work. + this.queue.push((cid, Some(data))); + + if block.epoch == 0 { + // The genesis block has some kind of dummy parent that needs to be emitted. + for p in &block.parents { + this.queue.push((p, None)); + } + } - // Process block messages. - if block.epoch > stateroot_limit - && should_save_block_to_snapshot(block.messages) - { - if this.db.has(&block.messages)? { - this.extract_sender.send(block.messages)?; - // This will simply return an error once we reach that item in - // the queue. - } else if fail_on_dead_links { - this.queue.push((block.messages, None)); - } else { - // Make sure we update seen here as we don't send the block for - // inspection. - this.seen.lock().insert(block.messages); - } - } + // Process block messages. + if block.epoch > stateroot_limit { + process_cid( + block.messages, + this.db, + this.extract_sender, + this.queue, + this.seen, + fail_on_dead_links, + )?; + } - // Visit the block if it's within required depth. And a special case for `0` - // epoch to match Lotus' implementation. - if (block.epoch == 0 || block.epoch > stateroot_limit) - && should_save_block_to_snapshot(block.state_root) - { - if this.db.has(&block.state_root)? { - this.extract_sender.send(block.state_root)?; - // This will simply return an error once we reach that item in - // the queue. - } else if fail_on_dead_links { - this.queue.push((block.state_root, None)); - } else { - // Make sure we update seen here as we don't send the block for - // inspection. - this.seen.lock().insert(block.state_root); + // Visit the block if it's within required depth. And a special case for `0` + // epoch to match Lotus' implementation. + if block.epoch == 0 || block.epoch > stateroot_limit { + process_cid( + block.state_root, + this.db, + this.extract_sender, + this.queue, + this.seen, + fail_on_dead_links, + )?; + } } } - } - } - } else { - match this.block_receiver.try_recv() { - Ok(item) => return Poll::Ready(Some(item)), - Err(err) => { - if this.extract_sender.is_empty() { - this.worker_handle.abort(); - return Poll::Ready(None); - // This should never happen, because both `extract_sender` and - // `block_receiver` are held by worker_handle and their counterparts - - // by the main process. So those are either both functional or both - // closed. - } else if err == TryRecvError::Disconnected { - panic!( - "block_receiver can only be closed after extract_sender is empty" - ) - } + } else if let Some(extract_sender) = this.extract_sender + && extract_sender.is_empty() + { + // drop the sender to abort the worker task + *this.extract_sender = None; } } } diff --git a/src/rpc/methods/chain.rs b/src/rpc/methods/chain.rs index 48558b345ab3..b5788c04fef3 100644 --- a/src/rpc/methods/chain.rs +++ b/src/rpc/methods/chain.rs @@ -9,7 +9,7 @@ use types::*; use crate::blocks::RawBlockHeader; use crate::blocks::{Block, CachingBlockHeader, Tipset, TipsetKey}; use crate::chain::index::ResolveNullTipset; -use crate::chain::{ChainStore, FilecoinSnapshotVersion, HeadChange}; +use crate::chain::{ChainStore, ExportOptions, FilecoinSnapshotVersion, HeadChange}; use crate::cid_collections::CidHashSet; use crate::ipld::DfsIter; use crate::lotus_json::{HasLotusJson, LotusJson, lotus_json_with_self}; @@ -321,6 +321,7 @@ impl RpcMethod<1> for ForestChainExport { recent_roots, output_path, tipset_keys: ApiTipsetKey(tsk), + unordered, skip_checksum, dry_run, } = params; @@ -345,6 +346,11 @@ impl RpcMethod<1> for ForestChainExport { ctx.chain_index() .tipset_by_height(epoch, head, ResolveNullTipset::TakeOlder)?; + let options = Some(ExportOptions { + skip_checksum, + unordered, + seen: Default::default(), + }); let writer = if dry_run { tokio_util::either::Either::Left(VoidAsyncWriter) } else { @@ -357,8 +363,7 @@ impl RpcMethod<1> for ForestChainExport { &start_ts, recent_roots, writer, - CidHashSet::default(), - skip_checksum, + options, ) .await } @@ -390,8 +395,7 @@ impl RpcMethod<1> for ForestChainExport { &start_ts, recent_roots, writer, - CidHashSet::default(), - skip_checksum, + options, ) .await } @@ -432,6 +436,7 @@ impl RpcMethod<1> for ChainExport { output_path, tipset_keys, skip_checksum, + unordered: false, dry_run, },), ) @@ -994,6 +999,7 @@ pub struct ForestChainExportParams { #[schemars(with = "LotusJson")] #[serde(with = "crate::lotus_json")] pub tipset_keys: ApiTipsetKey, + pub unordered: bool, pub skip_checksum: bool, pub dry_run: bool, } diff --git a/src/tool/subcommands/archive_cmd.rs b/src/tool/subcommands/archive_cmd.rs index fb78ecbb85a6..c6c391e67645 100644 --- a/src/tool/subcommands/archive_cmd.rs +++ b/src/tool/subcommands/archive_cmd.rs @@ -28,10 +28,9 @@ use crate::blocks::Tipset; use crate::chain::{ - ChainEpochDelta, + ChainEpochDelta, ExportOptions, FilecoinSnapshotMetadata, FilecoinSnapshotVersion, index::{ChainIndex, ResolveNullTipset}, }; -use crate::chain::{FilecoinSnapshotMetadata, FilecoinSnapshotVersion}; use crate::cid_collections::CidHashSet; use crate::cli_shared::{snapshot, snapshot::TrustedVendor}; use crate::db::car::{AnyCar, ManyCar, forest::DEFAULT_FOREST_CAR_COMPRESSION_LEVEL}; @@ -581,7 +580,18 @@ async fn do_export( pb.enable_steady_tick(std::time::Duration::from_secs_f32(0.1)); let writer = pb.wrap_async_write(writer); - crate::chain::export::(store, &ts, depth, writer, seen, true).await?; + crate::chain::export::( + store, + &ts, + depth, + writer, + Some(ExportOptions { + skip_checksum: true, + seen, + ..Default::default() + }), + ) + .await?; Ok(()) }