1pub mod ec_finality;
5mod snapshot_format;
6pub mod store;
7#[cfg(test)]
8mod tests;
9mod weight;
10
11pub use self::{snapshot_format::*, store::*, weight::*};
12
13use crate::blocks::{Tipset, TipsetKey};
14use crate::chain::index::ChainIndex;
15use crate::cid_collections::{CidHashSet, CidHashSetLike};
16use crate::db::IndexMapBlockstore;
17use crate::db::car::forest::{self, ForestCarFrame, finalize_frame};
18use crate::ipld::{IpldStream, stream_chain};
19use crate::prelude::*;
20use crate::shim::executor::Receipt;
21use crate::utils::db::car_stream::{CarBlock, CarBlockWrite};
22use crate::utils::io::{AsyncWriterWithChecksum, Checksum};
23use crate::utils::multihash::MultihashCode;
24use crate::utils::stream::par_buffer;
25use fil_actors_shared::fvm_ipld_hamt::Hamt;
26use futures::StreamExt as _;
27use fvm_ipld_encoding::DAG_CBOR;
28use multihash_derive::MultihashDigest as _;
29use nunny::Vec as NonEmpty;
30use sha2::digest::{self, Digest};
31use std::io::{Read, Seek, SeekFrom};
32use std::time::Instant;
33use tokio::io::{AsyncWrite, AsyncWriteExt, BufWriter};
34use tokio_util::task::AbortOnDropHandle;
35
36pub const TIPSET_LOOKUP_HAMT_BIT_WIDTH: u32 = 5;
37
38pub struct ExportOptions<S> {
39 pub skip_checksum: bool,
40 pub include_receipts: bool,
41 pub include_events: bool,
42 pub include_tipset_keys: bool,
43 pub include_tipset_lookup: bool,
44 pub seen: S,
45}
46
47impl<S: Default> Default for ExportOptions<S> {
48 fn default() -> Self {
49 Self {
50 skip_checksum: Default::default(),
51 include_receipts: Default::default(),
52 include_events: Default::default(),
53 include_tipset_keys: Default::default(),
54 include_tipset_lookup: Default::default(),
55 seen: Default::default(),
56 }
57 }
58}
59
60pub struct ExportResult<D: Digest> {
61 pub checksum: Option<digest::Output<D>>,
62 #[allow(dead_code)]
63 pub tipset_lookup: Option<anyhow::Result<Hamt<IndexMapBlockstore, TipsetKey, ChainEpoch>>>,
64}
65
66pub async fn export<D: Digest, S: CidHashSetLike + Send + Sync + 'static>(
69 db: &(impl Blockstore + ShallowClone + Unpin + Send + Sync + 'static),
70 tipset: &Tipset,
71 lookup_depth: ChainEpochDelta,
72 writer: impl AsyncWrite + Unpin,
73 options: ExportOptions<S>,
74) -> anyhow::Result<ExportResult<D>> {
75 let roots = tipset.key().to_cids();
76 export_to_forest_car::<D, S>(roots, None, db, tipset, lookup_depth, writer, options).await
77}
78
79pub async fn export_v2<D: Digest, F: Seek + Read, S: CidHashSetLike + Send + Sync + 'static>(
82 db: &(impl Blockstore + ShallowClone + Unpin + Send + Sync + 'static),
83 mut f3: Option<(Cid, F)>,
84 tipset: &Tipset,
85 lookup_depth: ChainEpochDelta,
86 writer: impl AsyncWrite + Unpin,
87 options: ExportOptions<S>,
88) -> anyhow::Result<ExportResult<D>> {
89 if let Some((f3_cid, f3_data)) = &mut f3 {
91 f3_data.seek(SeekFrom::Start(0))?;
92 let expected_cid = crate::f3::snapshot::get_f3_snapshot_cid(f3_data)?;
93 anyhow::ensure!(
94 f3_cid == &expected_cid,
95 "f3 snapshot integrity check failed, actual cid: {f3_cid}, expected cid: {expected_cid}"
96 );
97 }
98
99 let head = tipset.key().to_cids();
100 let f3_cid = f3.as_ref().map(|(cid, _)| *cid);
101 let snap_meta = FilecoinSnapshotMetadata::new_v2(head, f3_cid);
102 let snap_meta_cbor_encoded = fvm_ipld_encoding::to_vec(&snap_meta)?;
103 let snap_meta_block = CarBlock {
104 cid: Cid::new_v1(
105 DAG_CBOR,
106 MultihashCode::Blake2b256.digest(&snap_meta_cbor_encoded),
107 ),
108 data: snap_meta_cbor_encoded.into(),
109 };
110 let roots = nunny::vec![snap_meta_block.cid];
111 let mut prefix_data_frames = vec![{
112 let mut encoder = forest::new_encoder(forest::DEFAULT_FOREST_CAR_COMPRESSION_LEVEL)?;
113 snap_meta_block.write(&mut encoder)?;
114 anyhow::Ok((
115 vec![snap_meta_block.cid],
116 finalize_frame(forest::DEFAULT_FOREST_CAR_COMPRESSION_LEVEL, &mut encoder)?,
117 ))
118 }];
119
120 if let Some((f3_cid, mut f3_data)) = f3 {
121 let f3_data_len = f3_data.seek(SeekFrom::End(0))?;
122 f3_data.seek(SeekFrom::Start(0))?;
123 prefix_data_frames.push({
124 let mut encoder = forest::new_encoder(forest::DEFAULT_FOREST_CAR_COMPRESSION_LEVEL)?;
125 encoder.write_car_block(f3_cid, f3_data_len, &mut f3_data)?;
126 anyhow::Ok((
127 vec![f3_cid],
128 finalize_frame(forest::DEFAULT_FOREST_CAR_COMPRESSION_LEVEL, &mut encoder)?,
129 ))
130 });
131 }
132
133 export_to_forest_car::<D, S>(
134 roots,
135 Some(prefix_data_frames),
136 db,
137 tipset,
138 lookup_depth,
139 writer,
140 options,
141 )
142 .await
143}
144
145#[allow(clippy::too_many_arguments)]
146async fn export_to_forest_car<D: Digest, S: CidHashSetLike + Send + Sync + 'static>(
147 roots: NonEmpty<Cid>,
148 prefix_data_frames: Option<Vec<anyhow::Result<ForestCarFrame>>>,
149 db: &(impl Blockstore + ShallowClone + Unpin + Send + Sync + 'static),
150 tipset: &Tipset,
151 lookup_depth: ChainEpochDelta,
152 writer: impl AsyncWrite + Unpin,
153 ExportOptions {
154 skip_checksum,
155 include_receipts,
156 include_events,
157 include_tipset_keys,
158 include_tipset_lookup,
159 seen,
160 }: ExportOptions<S>,
161) -> anyhow::Result<ExportResult<D>> {
162 if include_events && !include_receipts {
163 anyhow::bail!("message receipts must be included when events are included");
164 }
165
166 let start = Instant::now();
167 tracing::info!(
168 "Exporting snapshot, epoch={}, depth={lookup_depth}, prefix_frames={}",
169 tipset.epoch(),
170 prefix_data_frames.as_ref().map(|v| v.len()).unwrap_or(0)
171 );
172
173 let stateroot_lookup_limit = tipset.epoch() - lookup_depth;
174
175 let mut writer = AsyncWriterWithChecksum::<D, _>::new(BufWriter::new(writer), !skip_checksum);
177
178 let (ts_lookup_tx, ts_lookup_handle) = if include_tipset_lookup {
179 let (ts_lookup_tx, ts_lookup_rx) = flume::bounded::<(ChainEpoch, TipsetKey)>(1024);
180 let handle = AbortOnDropHandle::new(tokio::spawn(async move {
181 let mut hamt = Hamt::new_with_bit_width(
182 IndexMapBlockstore::default(),
183 TIPSET_LOOKUP_HAMT_BIT_WIDTH,
184 );
185 while let Ok((epoch, tsk)) = ts_lookup_rx.recv_async().await {
186 hamt.set(epoch, tsk)?;
187 }
188 hamt.flush()?;
189 anyhow::Ok(hamt)
190 }));
191 (Some(ts_lookup_tx), Some(handle))
192 } else {
193 (None, None)
194 };
195
196 let (blocks, _drop_guard) = par_buffer(
199 1024,
203 stream_chain(
204 db.shallow_clone(),
205 tipset
206 .shallow_clone()
207 .chain_owned(db.shallow_clone())
208 .inspect(move |ts| {
209 if let Some(ts_lookup_tx) = &ts_lookup_tx
210 && ChainIndex::is_tipset_lookup_checkpoint(ts.epoch())
211 {
212 _ = ts_lookup_tx.send((ts.epoch(), ts.key().clone()));
213 }
214 }),
215 stateroot_lookup_limit,
216 seen,
217 )
218 .with_message_receipts(include_receipts)
219 .with_events(include_events)
220 .with_tipset_keys(include_tipset_keys)
221 .track_progress(true),
222 );
223
224 let block_frames = forest::Encoder::compress_stream_default(blocks);
226 let frames = futures::stream::iter(prefix_data_frames.unwrap_or_default()).chain(block_frames);
227
228 forest::Encoder::write(&mut writer, roots, frames).await?;
230
231 tokio::time::timeout(forest::ASYNC_OPS_TIMEOUT, writer.flush())
233 .await
234 .context("`writer.flush` timed out")??;
235
236 let digest = writer.finalize().map_err(|e| Error::Other(e.to_string()))?;
237
238 let tipset_lookup = if let Some(ts_lookup_handle) = ts_lookup_handle {
239 Some(
244 tokio::time::timeout(forest::ASYNC_OPS_TIMEOUT, ts_lookup_handle)
245 .await
246 .context(
247 "tipset-lookup task did not finish; is a `ts_lookup_tx` sender still alive?",
248 )??,
249 )
250 } else {
251 None
252 };
253
254 tracing::info!(
255 "Exported snapshot, took {}",
256 humantime::format_duration(start.elapsed())
257 );
258
259 Ok(ExportResult {
260 checksum: digest,
261 tipset_lookup,
262 })
263}
264
265pub async fn export_receipts_events_to_forest_car(
266 db: &(impl Blockstore + ShallowClone + Unpin + Send + Sync + 'static),
267 tipset: &Tipset,
268 lookup_depth: ChainEpochDelta,
269 writer: impl AsyncWrite + Unpin,
270) -> anyhow::Result<()> {
271 let start = Instant::now();
272 tracing::info!(
273 "Exporting message receipts and events snapshot, epoch={}, depth={lookup_depth}",
274 tipset.epoch(),
275 );
276
277 let min_lookup_epoch_exclusive = tipset.epoch() - lookup_depth;
278 let ipld_roots = tokio::task::spawn_blocking({
279 let tipset = tipset.shallow_clone();
280 let db = db.shallow_clone();
281 move || {
282 let mut ipld_roots = vec![];
284 for ts in tipset
285 .chain(&db)
286 .take_while(|ts| ts.epoch() > min_lookup_epoch_exclusive)
287 {
288 let message_receipts_root = *ts.parent_message_receipts();
289 ipld_roots.push(message_receipts_root);
290 let receipts = Receipt::get_receipts(&db, message_receipts_root).with_context(|| {
291 format!(
292 "failed to get receipts, root: {message_receipts_root}, epoch: {}, tipset key: {}",
293 ts.epoch(),
294 ts.key(),
295 )
296 })?;
297 ipld_roots.extend(receipts.into_iter().filter_map(|r| r.events_root()));
298 }
299 anyhow::Ok(ipld_roots)
300 }
301 })
302 .await??;
303
304 let stream = IpldStream::new(db.shallow_clone(), ipld_roots, CidHashSet::default());
305 let mut writer = BufWriter::new(writer);
306 let (blocks, _drop_guard) = par_buffer(
307 1024, stream,
311 );
312 let block_frames = forest::Encoder::compress_stream_default(blocks);
314
315 let roots = nunny::vec![Cid::default()];
320
321 forest::Encoder::write(&mut writer, roots, block_frames).await?;
323
324 tokio::time::timeout(forest::ASYNC_OPS_TIMEOUT, writer.flush())
326 .await
327 .context("`writer.flush` timed out")??;
328
329 tracing::info!(
330 "Exported message receipts and events snapshot, took {}",
331 humantime::format_duration(start.elapsed())
332 );
333
334 Ok(())
335}