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
66fn lookup_epoch_limit(
68 tipset_epoch: ChainEpoch,
69 lookup_depth: ChainEpoch,
70) -> anyhow::Result<ChainEpoch> {
71 tipset_epoch
72 .checked_sub(lookup_depth)
73 .with_context(|| format!("recent roots depth {lookup_depth} is out of range"))
74}
75
76pub async fn export<D: Digest, S: CidHashSetLike + Send + Sync + 'static>(
79 db: &(impl Blockstore + ShallowClone + Unpin + Send + Sync + 'static),
80 tipset: &Tipset,
81 lookup_depth: ChainEpochDelta,
82 writer: impl AsyncWrite + Unpin,
83 options: ExportOptions<S>,
84) -> anyhow::Result<ExportResult<D>> {
85 let roots = tipset.key().to_cids();
86 export_to_forest_car::<D, S>(roots, None, db, tipset, lookup_depth, writer, options).await
87}
88
89pub async fn export_v2<D: Digest, F: Seek + Read, S: CidHashSetLike + Send + Sync + 'static>(
92 db: &(impl Blockstore + ShallowClone + Unpin + Send + Sync + 'static),
93 mut f3: Option<(Cid, F)>,
94 tipset: &Tipset,
95 lookup_depth: ChainEpochDelta,
96 writer: impl AsyncWrite + Unpin,
97 options: ExportOptions<S>,
98) -> anyhow::Result<ExportResult<D>> {
99 if let Some((f3_cid, f3_data)) = &mut f3 {
101 f3_data.seek(SeekFrom::Start(0))?;
102 let expected_cid = crate::f3::snapshot::get_f3_snapshot_cid(f3_data)?;
103 anyhow::ensure!(
104 f3_cid == &expected_cid,
105 "f3 snapshot integrity check failed, actual cid: {f3_cid}, expected cid: {expected_cid}"
106 );
107 }
108
109 let head = tipset.key().to_cids();
110 let f3_cid = f3.as_ref().map(|(cid, _)| *cid);
111 let snap_meta = FilecoinSnapshotMetadata::new_v2(head, f3_cid);
112 let snap_meta_cbor_encoded = fvm_ipld_encoding::to_vec(&snap_meta)?;
113 let snap_meta_block = CarBlock {
114 cid: Cid::new_v1(
115 DAG_CBOR,
116 MultihashCode::Blake2b256.digest(&snap_meta_cbor_encoded),
117 ),
118 data: snap_meta_cbor_encoded.into(),
119 };
120 let roots = nunny::vec![snap_meta_block.cid];
121 let mut prefix_data_frames = vec![{
122 let mut encoder = forest::new_encoder(forest::DEFAULT_FOREST_CAR_COMPRESSION_LEVEL)?;
123 snap_meta_block.write(&mut encoder)?;
124 anyhow::Ok((
125 vec![snap_meta_block.cid],
126 finalize_frame(forest::DEFAULT_FOREST_CAR_COMPRESSION_LEVEL, &mut encoder)?,
127 ))
128 }];
129
130 if let Some((f3_cid, mut f3_data)) = f3 {
131 let f3_data_len = f3_data.seek(SeekFrom::End(0))?;
132 f3_data.seek(SeekFrom::Start(0))?;
133 prefix_data_frames.push({
134 let mut encoder = forest::new_encoder(forest::DEFAULT_FOREST_CAR_COMPRESSION_LEVEL)?;
135 encoder.write_car_block(f3_cid, f3_data_len, &mut f3_data)?;
136 anyhow::Ok((
137 vec![f3_cid],
138 finalize_frame(forest::DEFAULT_FOREST_CAR_COMPRESSION_LEVEL, &mut encoder)?,
139 ))
140 });
141 }
142
143 export_to_forest_car::<D, S>(
144 roots,
145 Some(prefix_data_frames),
146 db,
147 tipset,
148 lookup_depth,
149 writer,
150 options,
151 )
152 .await
153}
154
155#[allow(clippy::too_many_arguments)]
156async fn export_to_forest_car<D: Digest, S: CidHashSetLike + Send + Sync + 'static>(
157 roots: NonEmpty<Cid>,
158 prefix_data_frames: Option<Vec<anyhow::Result<ForestCarFrame>>>,
159 db: &(impl Blockstore + ShallowClone + Unpin + Send + Sync + 'static),
160 tipset: &Tipset,
161 lookup_depth: ChainEpochDelta,
162 writer: impl AsyncWrite + Unpin,
163 ExportOptions {
164 skip_checksum,
165 include_receipts,
166 include_events,
167 include_tipset_keys,
168 include_tipset_lookup,
169 seen,
170 }: ExportOptions<S>,
171) -> anyhow::Result<ExportResult<D>> {
172 if include_events && !include_receipts {
173 anyhow::bail!("message receipts must be included when events are included");
174 }
175
176 let start = Instant::now();
177 tracing::info!(
178 "Exporting snapshot, epoch={}, depth={lookup_depth}, prefix_frames={}",
179 tipset.epoch(),
180 prefix_data_frames.as_ref().map(|v| v.len()).unwrap_or(0)
181 );
182
183 let stateroot_lookup_limit = lookup_epoch_limit(tipset.epoch(), lookup_depth)?;
184
185 let mut writer = AsyncWriterWithChecksum::<D, _>::new(BufWriter::new(writer), !skip_checksum);
187
188 let (ts_lookup_tx, ts_lookup_handle) = if include_tipset_lookup {
189 let (ts_lookup_tx, ts_lookup_rx) = flume::bounded::<(ChainEpoch, TipsetKey)>(1024);
190 let handle = AbortOnDropHandle::new(tokio::spawn(async move {
191 let mut hamt = Hamt::new_with_bit_width(
192 IndexMapBlockstore::default(),
193 TIPSET_LOOKUP_HAMT_BIT_WIDTH,
194 );
195 while let Ok((epoch, tsk)) = ts_lookup_rx.recv_async().await {
196 hamt.set(epoch, tsk)?;
197 }
198 hamt.flush()?;
199 anyhow::Ok(hamt)
200 }));
201 (Some(ts_lookup_tx), Some(handle))
202 } else {
203 (None, None)
204 };
205
206 let (blocks, _drop_guard) = par_buffer(
209 1024,
213 stream_chain(
214 db.shallow_clone(),
215 tipset
216 .shallow_clone()
217 .chain_owned(db.shallow_clone())
218 .inspect(move |ts| {
219 if let Some(ts_lookup_tx) = &ts_lookup_tx
220 && ChainIndex::is_tipset_lookup_checkpoint(ts.epoch())
221 {
222 _ = ts_lookup_tx.send((ts.epoch(), ts.key().clone()));
223 }
224 }),
225 stateroot_lookup_limit,
226 seen,
227 )
228 .with_message_receipts(include_receipts)
229 .with_events(include_events)
230 .with_tipset_keys(include_tipset_keys)
231 .track_progress(true),
232 );
233
234 let block_frames = forest::Encoder::compress_stream_default(blocks);
236 let frames = futures::stream::iter(prefix_data_frames.unwrap_or_default()).chain(block_frames);
237
238 forest::Encoder::write(&mut writer, roots, frames).await?;
240
241 tokio::time::timeout(forest::ASYNC_OPS_TIMEOUT, writer.flush())
243 .await
244 .context("`writer.flush` timed out")??;
245
246 let digest = writer.finalize().map_err(|e| Error::Other(e.to_string()))?;
247
248 let tipset_lookup = if let Some(ts_lookup_handle) = ts_lookup_handle {
249 Some(
254 tokio::time::timeout(forest::ASYNC_OPS_TIMEOUT, ts_lookup_handle)
255 .await
256 .context(
257 "tipset-lookup task did not finish; is a `ts_lookup_tx` sender still alive?",
258 )??,
259 )
260 } else {
261 None
262 };
263
264 tracing::info!(
265 "Exported snapshot, took {}",
266 humantime::format_duration(start.elapsed())
267 );
268
269 Ok(ExportResult {
270 checksum: digest,
271 tipset_lookup,
272 })
273}
274
275pub async fn export_receipts_events_to_forest_car(
276 db: &(impl Blockstore + ShallowClone + Unpin + Send + Sync + 'static),
277 tipset: &Tipset,
278 lookup_depth: ChainEpochDelta,
279 writer: impl AsyncWrite + Unpin,
280) -> anyhow::Result<()> {
281 let start = Instant::now();
282 tracing::info!(
283 "Exporting message receipts and events snapshot, epoch={}, depth={lookup_depth}",
284 tipset.epoch(),
285 );
286
287 let min_lookup_epoch_exclusive = lookup_epoch_limit(tipset.epoch(), lookup_depth)?;
288 let ipld_roots = tokio::task::spawn_blocking({
289 let tipset = tipset.shallow_clone();
290 let db = db.shallow_clone();
291 move || {
292 let mut ipld_roots = vec![];
294 for ts in tipset
295 .chain(&db)
296 .take_while(|ts| ts.epoch() > min_lookup_epoch_exclusive)
297 {
298 let message_receipts_root = *ts.parent_message_receipts();
299 ipld_roots.push(message_receipts_root);
300 let receipts = Receipt::get_receipts(&db, message_receipts_root).with_context(|| {
301 format!(
302 "failed to get receipts, root: {message_receipts_root}, epoch: {}, tipset key: {}",
303 ts.epoch(),
304 ts.key(),
305 )
306 })?;
307 ipld_roots.extend(receipts.into_iter().filter_map(|r| r.events_root()));
308 }
309 anyhow::Ok(ipld_roots)
310 }
311 })
312 .await??;
313
314 let stream = IpldStream::new(db.shallow_clone(), ipld_roots, CidHashSet::default());
315 let mut writer = BufWriter::new(writer);
316 let (blocks, _drop_guard) = par_buffer(
317 1024, stream,
321 );
322 let block_frames = forest::Encoder::compress_stream_default(blocks);
324
325 let roots = nunny::vec![Cid::default()];
330
331 forest::Encoder::write(&mut writer, roots, block_frames).await?;
333
334 tokio::time::timeout(forest::ASYNC_OPS_TIMEOUT, writer.flush())
336 .await
337 .context("`writer.flush` timed out")??;
338
339 tracing::info!(
340 "Exported message receipts and events snapshot, took {}",
341 humantime::format_duration(start.elapsed())
342 );
343
344 Ok(())
345}