1use crate::blocks::Tipset;
5use crate::cid_collections::{CidHashSet, CidHashSetLike};
6use crate::ipld::Ipld;
7use crate::prelude::*;
8use crate::shim::clock::ChainEpoch;
9use crate::shim::executor::Receipt;
10use crate::utils::db::car_stream::CarBlock;
11use crate::utils::encoding::extract_cids;
12use crate::utils::multihash::prelude::*;
13use arc_swap::ArcSwapOption;
14use bytes::Bytes;
15use chrono::{DateTime, Utc};
16use futures::Stream;
17use pin_project_lite::pin_project;
18use std::borrow::Borrow;
19use std::collections::VecDeque;
20use std::pin::Pin;
21use std::sync::atomic::{AtomicBool, AtomicI64};
22use std::sync::{LazyLock, atomic};
23use std::task::{Context, Poll};
24use tokio_util::sync::CancellationToken;
25
26#[derive(Default)]
27pub struct ExportStatus {
28 pub epoch: AtomicI64,
29 pub initial_epoch: AtomicI64,
30 pub exporting: AtomicBool,
31 pub cancelled: AtomicBool,
32 pub succeeded: AtomicBool,
33 pub start_time: ArcSwapOption<DateTime<Utc>>,
34 pub cancellation_token: ArcSwapOption<CancellationToken>,
35}
36
37impl ExportStatus {
38 pub fn epoch(&self) -> i64 {
39 self.epoch.load(atomic::Ordering::Relaxed)
40 }
41
42 pub fn initial_epoch(&self) -> i64 {
43 self.initial_epoch.load(atomic::Ordering::Relaxed)
44 }
45
46 pub fn exporting(&self) -> bool {
47 self.exporting.load(atomic::Ordering::Relaxed)
48 }
49
50 pub fn cancelled(&self) -> bool {
51 self.cancelled.load(atomic::Ordering::Relaxed)
52 }
53
54 pub fn succeeded(&self) -> bool {
55 self.succeeded.load(atomic::Ordering::Relaxed)
56 }
57
58 pub fn start_time(&self) -> Option<DateTime<Utc>> {
59 self.start_time.load().clone().map(Arc::unwrap_or_clone)
60 }
61
62 pub fn cancellation_token(&self) -> Option<CancellationToken> {
63 self.cancellation_token
64 .load()
65 .clone()
66 .map(Arc::unwrap_or_clone)
67 }
68}
69
70pub static CHAIN_EXPORT_STATUS: LazyLock<ExportStatus> = LazyLock::new(ExportStatus::default);
71
72#[derive(Debug)]
73pub struct ChainExportGuard {
74 cancellation_token: CancellationToken,
75}
76
77impl ChainExportGuard {
78 pub fn try_start_export() -> anyhow::Result<Self> {
79 let cancellation_token = CancellationToken::new();
80 start_export(cancellation_token.clone())?;
81 Ok(Self { cancellation_token })
82 }
83
84 pub fn cancel_export(&self) {
85 cancel_export()
86 }
87
88 pub fn mark_as_succeeded(&self) {
89 export_succeeded()
90 }
91
92 pub async fn run_cancellable<F: Future>(&self, fut: F) -> Option<F::Output> {
97 let output = self.cancellation_token.run_until_cancelled(fut).await;
98 if output.is_none() {
99 self.cancel_export();
100 }
101 output
102 }
103}
104
105impl Drop for ChainExportGuard {
106 fn drop(&mut self) {
107 self.cancellation_token.cancel();
109 end_export()
110 }
111}
112
113fn update_epoch(new_value: i64) {
114 let status = &*CHAIN_EXPORT_STATUS;
115 status.epoch.store(new_value, atomic::Ordering::Relaxed);
116 _ = status.initial_epoch.compare_exchange(
117 0,
118 new_value,
119 atomic::Ordering::Relaxed,
120 atomic::Ordering::Relaxed,
121 );
122}
123
124fn start_export(cancellation_token: CancellationToken) -> anyhow::Result<()> {
125 let status = &*CHAIN_EXPORT_STATUS;
126 let export_in_progress = status.exporting.swap(true, atomic::Ordering::Relaxed);
127 anyhow::ensure!(
128 !export_in_progress,
129 "An active chain export job has started at {}, start epoch: {}, current epoch: {}",
130 status.start_time().unwrap_or_default(),
131 status.initial_epoch(),
132 status.epoch(),
133 );
134 status.epoch.store(0, atomic::Ordering::Relaxed);
135 status.initial_epoch.store(0, atomic::Ordering::Relaxed);
136 status.cancelled.store(false, atomic::Ordering::Relaxed);
137 status.succeeded.store(false, atomic::Ordering::Relaxed);
138 status.start_time.store(Some(Utc::now().into()));
139 status
140 .cancellation_token
141 .store(Some(cancellation_token.into()));
142 Ok(())
143}
144
145fn export_succeeded() {
146 CHAIN_EXPORT_STATUS
147 .succeeded
148 .store(true, atomic::Ordering::Relaxed);
149}
150
151fn end_export() {
152 CHAIN_EXPORT_STATUS
153 .exporting
154 .store(false, atomic::Ordering::Relaxed);
155 CHAIN_EXPORT_STATUS.cancellation_token.store(None);
156}
157
158fn cancel_export() {
159 let status = &*CHAIN_EXPORT_STATUS;
160 status.cancelled.store(true, atomic::Ordering::Relaxed);
161}
162
163fn should_save_block_to_snapshot(cid: Cid) -> bool {
164 if cid.hash().code() == u64::from(MultihashCode::Identity) {
168 false
169 } else {
170 matches!(
171 cid.codec(),
172 crate::shim::crypto::IPLD_RAW | fvm_ipld_encoding::DAG_CBOR
173 )
174 }
175}
176
177pub struct DfsIter {
202 dfs: Vec<Ipld>,
203}
204
205impl DfsIter {
206 pub fn new(root: Ipld) -> Self {
207 DfsIter { dfs: vec![root] }
208 }
209}
210
211impl From<Cid> for DfsIter {
212 fn from(cid: Cid) -> Self {
213 DfsIter::new(Ipld::Link(cid))
214 }
215}
216
217impl Iterator for DfsIter {
218 type Item = Ipld;
219
220 fn next(&mut self) -> Option<Self::Item> {
221 while let Some(ipld) = self.dfs.pop() {
222 match ipld {
223 Ipld::List(list) => self.dfs.extend(list.into_iter().rev()),
224 Ipld::Map(map) => self.dfs.extend(map.into_values().rev()),
225 other => return Some(other),
226 }
227 }
228 None
229 }
230}
231
232enum IterateType {
233 Message(Cid),
234 MessageReceipts(Cid),
235 StateRoot(Cid),
236 EventsRoot(Cid),
237}
238
239enum Task {
240 Emit(Cid, Option<Bytes>),
242 Iterate(ChainEpoch, Cid, IterateType, Vec<Cid>),
244}
245
246pin_project! {
247 pub struct ChainStream<DB, T, S = CidHashSet> {
248 tipset_iter: T,
249 db: DB,
250 dfs: VecDeque<Task>, seen: S,
252 stateroot_limit_exclusive: ChainEpoch,
253 fail_on_dead_links: bool,
254 message_receipts: bool,
255 events: bool,
256 tipset_keys:bool,
257 track_progress: bool,
258 n_polled: usize,
259 }
260}
261
262impl<DB, T, S> ChainStream<DB, T, S> {
263 pub fn fail_on_dead_links(mut self, fail_on_dead_links: bool) -> Self {
264 self.fail_on_dead_links = fail_on_dead_links;
265 self
266 }
267
268 pub fn track_progress(mut self, track_progress: bool) -> Self {
269 self.track_progress = track_progress;
270 self
271 }
272
273 pub fn with_message_receipts(mut self, message_receipts: bool) -> Self {
275 self.message_receipts = message_receipts;
276 self
277 }
278
279 pub fn with_events(mut self, events: bool) -> Self {
282 self.events = events;
283 self
284 }
285
286 pub fn with_tipset_keys(mut self, tipset_keys: bool) -> Self {
288 self.tipset_keys = tipset_keys;
289 self
290 }
291
292 pub fn into_seen(self) -> S {
293 self.seen
294 }
295}
296
297pub fn stream_chain<
309 DB: Blockstore,
310 T: Borrow<Tipset>,
311 ITER: Iterator<Item = T> + Unpin,
312 S: CidHashSetLike,
313>(
314 db: DB,
315 tipset_iter: ITER,
316 stateroot_limit_exclusive: ChainEpoch,
317 seen: S,
318) -> ChainStream<DB, ITER, S> {
319 ChainStream {
320 tipset_iter,
321 db,
322 dfs: VecDeque::new(),
323 seen,
324 stateroot_limit_exclusive,
325 fail_on_dead_links: true,
326 message_receipts: false,
327 events: false,
328 tipset_keys: false,
329 track_progress: false,
330 n_polled: 0,
331 }
332}
333
334pub fn stream_graph<
337 DB: Blockstore,
338 T: Borrow<Tipset>,
339 ITER: Iterator<Item = T> + Unpin,
340 S: CidHashSetLike,
341>(
342 db: DB,
343 tipset_iter: ITER,
344 stateroot_limit_exclusive: ChainEpoch,
345 seen: S,
346) -> ChainStream<DB, ITER, S> {
347 stream_chain(db, tipset_iter, stateroot_limit_exclusive, seen).fail_on_dead_links(false)
348}
349
350impl<DB: Blockstore, T: Borrow<Tipset>, ITER: Iterator<Item = T> + Unpin, S: CidHashSetLike> Stream
351 for ChainStream<DB, ITER, S>
352{
353 type Item = anyhow::Result<CarBlock>;
354
355 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
356 use Task::*;
357
358 let export_tipset_keys = self.tipset_keys;
359 let fail_on_dead_links = self.fail_on_dead_links;
360 let stateroot_limit_exclusive = self.stateroot_limit_exclusive;
361 let this = self.project();
362
363 {
365 *this.n_polled += 1;
366 if this.n_polled.is_multiple_of(128) {
367 cx.waker().wake_by_ref();
368 return Poll::Pending;
369 }
370 }
371
372 loop {
373 while let Some(task) = this.dfs.front_mut() {
374 match task {
375 Emit(_, _) => {
376 if let Some(Emit(cid, data)) = this.dfs.pop_front() {
377 if let Some(data) = data {
378 return Poll::Ready(Some(Ok(CarBlock { cid, data })));
379 } else if let Some(data) = this.db.get(&cid)? {
380 return Poll::Ready(Some(Ok(CarBlock {
381 cid,
382 data: data.into(),
383 })));
384 } else if fail_on_dead_links {
385 return Poll::Ready(Some(Err(anyhow::anyhow!(
386 "[Emit] missing key: {cid}"
387 ))));
388 };
389 }
390 }
391 Iterate(epoch, block_cid, _type, cid_vec) => {
392 if *this.track_progress {
393 update_epoch(*epoch);
394 }
395 while let Some(cid) = cid_vec.pop() {
396 if should_save_block_to_snapshot(cid) && this.seen.insert(cid)? {
402 if let Some(data) = this.db.get(&cid)? {
403 if cid.codec() == fvm_ipld_encoding::DAG_CBOR {
404 let new_values = extract_cids(&data)?;
405 cid_vec.extend(new_values.into_iter().rev());
406 }
407 return Poll::Ready(Some(Ok(CarBlock {
408 cid,
409 data: data.into(),
410 })));
411 } else if fail_on_dead_links {
412 let type_display = match _type {
413 IterateType::Message(c) => {
414 format!("message {c}")
415 }
416 IterateType::StateRoot(c) => {
417 format!("state root {c}")
418 }
419 IterateType::MessageReceipts(c) => {
420 tracing::trace!(
422 "[Iterate] missing key: {cid} from message receipts {c} in block {block_cid} at epoch {epoch}"
423 );
424 continue;
425 }
426 IterateType::EventsRoot(c) => {
427 tracing::trace!(
429 "[Iterate] missing key: {cid} from events root {c} in block {block_cid} at epoch {epoch}"
430 );
431 continue;
432 }
433 };
434 return Poll::Ready(Some(Err(anyhow::anyhow!(
435 "[Iterate] missing key: {cid} from {type_display} in block {block_cid} at epoch {epoch}"
436 ))));
437 }
438 }
439 }
440 this.dfs.pop_front();
441 }
442 }
443 }
444
445 if let Some(tipset) = this.tipset_iter.next() {
449 if export_tipset_keys
451 && let Ok(CarBlock { cid, data }) = tipset.borrow().key().car_block()
452 {
453 this.dfs.push_back(Emit(cid, Some(data)));
454 }
455
456 for block in tipset.borrow().block_headers() {
457 let (cid, data) = block.car_block()?;
458 if this.seen.insert(cid)? {
459 if *this.track_progress {
460 update_epoch(block.epoch);
461 }
462 this.dfs.push_back(Emit(cid, Some(data.into())));
464
465 if block.epoch == 0 {
466 for p in &block.parents {
468 this.dfs.push_back(Emit(p, None));
469 }
470 }
471
472 if block.epoch > stateroot_limit_exclusive {
474 this.dfs.push_back(Iterate(
475 block.epoch,
476 *block.cid(),
477 IterateType::Message(block.messages),
478 DfsIter::from(block.messages)
479 .filter_map(ipld_to_cid)
480 .collect(),
481 ));
482 if *this.message_receipts {
483 this.dfs.push_back(Iterate(
484 block.epoch,
485 *block.cid(),
486 IterateType::MessageReceipts(block.message_receipts),
487 DfsIter::from(block.message_receipts)
488 .filter_map(ipld_to_cid)
489 .collect(),
490 ));
491 }
492 if *this.events
494 && let Ok(receipts) =
495 Receipt::get_receipts(this.db, block.message_receipts)
496 {
497 for receipt in receipts {
498 if let Some(events_root) = receipt.events_root() {
499 this.dfs.push_back(Iterate(
500 block.epoch,
501 *block.cid(),
502 IterateType::EventsRoot(events_root),
503 DfsIter::from(events_root)
504 .filter_map(ipld_to_cid)
505 .collect(),
506 ));
507 }
508 }
509 }
510 }
511
512 if block.epoch == 0 || block.epoch > stateroot_limit_exclusive {
515 this.dfs.push_back(Iterate(
518 block.epoch,
519 *block.cid(),
520 IterateType::StateRoot(block.state_root),
521 DfsIter::from(block.state_root)
522 .filter_map(ipld_to_cid)
523 .collect(),
524 ));
525 }
526 }
527 }
528 } else {
529 return Poll::Ready(None);
531 }
532 }
533 }
534}
535
536pin_project! {
537 pub struct IpldStream<DB, S> {
538 db: DB,
539 cid_vec: Vec<Cid>,
540 seen: S,
541 }
542}
543
544impl<DB, S> IpldStream<DB, S> {
545 pub fn new(db: DB, roots: Vec<Cid>, seen: S) -> Self {
546 Self {
547 db,
548 cid_vec: roots,
549 seen,
550 }
551 }
552}
553
554impl<DB: Blockstore, S: CidHashSetLike> Stream for IpldStream<DB, S> {
555 type Item = anyhow::Result<CarBlock>;
556
557 fn poll_next(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Option<Self::Item>> {
558 let this = self.project();
559 while let Some(cid) = this.cid_vec.pop() {
560 if should_save_block_to_snapshot(cid) && this.seen.insert(cid)? {
561 if let Some(data) = this.db.get(&cid)? {
562 if cid.codec() == fvm_ipld_encoding::DAG_CBOR {
563 let new_cids = extract_cids(&data)?;
564 this.cid_vec.extend(new_cids);
565 }
566 return Poll::Ready(Some(Ok(CarBlock {
567 cid,
568 data: data.into(),
569 })));
570 } else {
571 return Poll::Ready(Some(Err(anyhow::anyhow!("missing key: {cid}"))));
572 }
573 }
574 }
575 Poll::Ready(None)
577 }
578}
579
580fn ipld_to_cid(ipld: Ipld) -> Option<Cid> {
581 if let Ipld::Link(cid) = ipld {
582 Some(cid)
583 } else {
584 None
585 }
586}
587
588#[cfg(test)]
589mod tests {
590 use super::*;
591 use crate::blocks::{Chain4U, HeaderBuilder, chain4u};
592 use crate::db::MemoryDB;
593 use crate::utils::db::CborStoreExt as _;
594 use fil_actors_shared::fvm_ipld_amt::Amtv0;
595 use futures::TryStreamExt as _;
596 use fvm_ipld_encoding::RawBytes;
597 use ipld_core::ipld::Ipld;
598 use std::sync::Arc;
599
600 #[tokio::test]
601 async fn return_data_links_are_not_followed_by_the_walk() -> anyhow::Result<()> {
602 let db = Arc::new(MemoryDB::default());
603
604 let embedded = db.put_cbor_default(&Ipld::String("embedded".into()))?;
606
607 let events_root = db.put_cbor_default(&Ipld::String("events-root".into()))?;
610
611 let receipt = fvm_shared4::receipt::Receipt {
614 exit_code: fvm_shared4::error::ExitCode::OK,
615 return_data: RawBytes::new(serde_ipld_dagcbor::to_vec(&Ipld::Link(embedded))?),
616 gas_used: 0,
617 events_root: Some(events_root),
618 };
619
620 let receipts_root = Amtv0::new_from_iter(&db, std::iter::once(receipt))?;
621
622 let c4u = Chain4U::with_blockstore(db.clone());
625 chain4u! {
626 in c4u;
627 [_genesis]
628 -> head @ [_header = HeaderBuilder::new().with_message_receipts(receipts_root)]
629 };
630
631 let mut stream = stream_chain(&db, std::iter::once(head), 0, CidHashSet::default())
632 .with_message_receipts(true)
633 .fail_on_dead_links(false);
636
637 let mut seen = Vec::new();
638 while let Some(block) = stream.try_next().await? {
639 seen.push(block.cid);
640 }
641
642 assert!(
644 seen.contains(&receipts_root),
645 "receipts AMT root must be reachable with receipts enabled"
646 );
647 assert!(
649 seen.contains(&events_root),
650 "EventsRoot link inside the receipt must be followed"
651 );
652 assert!(
654 !seen.contains(&embedded),
655 "link embedded in Return must not be followed by the walk"
656 );
657
658 Ok(())
659 }
660
661 #[tokio::test]
663 #[serial_test::serial(chain_export)]
664 async fn chain_export_cancel_stops_guarded_export() {
665 let g = ChainExportGuard::try_start_export().unwrap();
666
667 let token = CHAIN_EXPORT_STATUS.cancellation_token().unwrap();
669
670 let fut = g.run_cancellable(std::future::pending::<()>());
671 token.cancel();
672 assert!(
673 fut.await.is_none(),
674 "cancellation must interrupt the export"
675 );
676
677 assert!(CHAIN_EXPORT_STATUS.cancelled());
678 assert!(CHAIN_EXPORT_STATUS.exporting());
679
680 drop(g);
681 assert!(!CHAIN_EXPORT_STATUS.exporting());
682 assert!(CHAIN_EXPORT_STATUS.cancelled());
683 }
684
685 #[test]
686 #[serial_test::serial(chain_export)]
687 fn test_chain_export_guard() {
688 let g = ChainExportGuard::try_start_export().unwrap();
690 assert!(CHAIN_EXPORT_STATUS.exporting());
691 assert!(!CHAIN_EXPORT_STATUS.succeeded());
692 assert!(!CHAIN_EXPORT_STATUS.cancelled());
693
694 ChainExportGuard::try_start_export().unwrap_err();
696
697 g.cancel_export();
699 assert!(CHAIN_EXPORT_STATUS.cancelled());
700
701 drop(g);
703 assert!(!CHAIN_EXPORT_STATUS.exporting());
704 assert!(!CHAIN_EXPORT_STATUS.succeeded());
705 assert!(CHAIN_EXPORT_STATUS.cancelled());
706
707 let g = ChainExportGuard::try_start_export().unwrap();
709 assert!(CHAIN_EXPORT_STATUS.exporting());
710 assert!(!CHAIN_EXPORT_STATUS.succeeded());
711 assert!(!CHAIN_EXPORT_STATUS.cancelled());
712
713 ChainExportGuard::try_start_export().unwrap_err();
715
716 g.mark_as_succeeded();
718 assert!(CHAIN_EXPORT_STATUS.succeeded());
719
720 drop(g);
722 assert!(!CHAIN_EXPORT_STATUS.exporting());
723 assert!(CHAIN_EXPORT_STATUS.succeeded());
724 assert!(!CHAIN_EXPORT_STATUS.cancelled());
725
726 let g = ChainExportGuard::try_start_export().unwrap();
728 assert!(CHAIN_EXPORT_STATUS.exporting());
729 assert!(!CHAIN_EXPORT_STATUS.succeeded());
730 assert!(!CHAIN_EXPORT_STATUS.cancelled());
731
732 ChainExportGuard::try_start_export().unwrap_err();
734
735 drop(g);
737 assert!(!CHAIN_EXPORT_STATUS.exporting());
738 assert!(!CHAIN_EXPORT_STATUS.succeeded());
739 assert!(!CHAIN_EXPORT_STATUS.cancelled());
740 }
741}