1use crate::blocks::Tipset;
5use crate::cid_collections::{CidHashSet, CidHashSetLike};
6use crate::ipld::Ipld;
7use crate::ipld::export_status::{CHAIN_EXPORT_STATUS, ProgressReporter};
8use crate::prelude::*;
9use crate::shim::clock::ChainEpoch;
10use crate::shim::executor::Receipt;
11use crate::utils::db::car_stream::CarBlock;
12use crate::utils::encoding::extract_cids;
13use crate::utils::multihash::prelude::*;
14use bytes::Bytes;
15use futures::Stream;
16use pin_project_lite::pin_project;
17use std::borrow::Borrow;
18use std::collections::VecDeque;
19use std::pin::Pin;
20use std::task::{Context, Poll};
21
22fn should_save_block_to_snapshot(cid: Cid) -> bool {
23 if cid.hash().code() == u64::from(MultihashCode::Identity) {
27 false
28 } else {
29 matches!(
30 cid.codec(),
31 crate::shim::crypto::IPLD_RAW | fvm_ipld_encoding::DAG_CBOR
32 )
33 }
34}
35
36pub struct DfsIter {
61 dfs: Vec<Ipld>,
62}
63
64impl DfsIter {
65 pub fn new(root: Ipld) -> Self {
66 DfsIter { dfs: vec![root] }
67 }
68}
69
70impl From<Cid> for DfsIter {
71 fn from(cid: Cid) -> Self {
72 DfsIter::new(Ipld::Link(cid))
73 }
74}
75
76impl Iterator for DfsIter {
77 type Item = Ipld;
78
79 fn next(&mut self) -> Option<Self::Item> {
80 while let Some(ipld) = self.dfs.pop() {
81 match ipld {
82 Ipld::List(list) => self.dfs.extend(list.into_iter().rev()),
83 Ipld::Map(map) => self.dfs.extend(map.into_values().rev()),
84 other => return Some(other),
85 }
86 }
87 None
88 }
89}
90
91enum IterateType {
92 Message(Cid),
93 MessageReceipts(Cid),
94 StateRoot(Cid),
95 EventsRoot(Cid),
96}
97
98enum Task {
99 Emit(Cid, Option<Bytes>),
101 Iterate(ChainEpoch, Cid, IterateType, Vec<Cid>),
103}
104
105pin_project! {
106 pub struct ChainStream<DB, T, S = CidHashSet> {
107 tipset_iter: T,
108 db: DB,
109 dfs: VecDeque<Task>, seen: S,
111 stateroot_limit_exclusive: ChainEpoch,
112 fail_on_dead_links: bool,
113 message_receipts: bool,
114 events: bool,
115 tipset_keys:bool,
116 progress: Option<ProgressReporter>,
117 n_polled: usize,
118 }
119}
120
121impl<DB, T, S> ChainStream<DB, T, S> {
122 pub fn fail_on_dead_links(mut self, fail_on_dead_links: bool) -> Self {
123 self.fail_on_dead_links = fail_on_dead_links;
124 self
125 }
126
127 pub fn track_progress(mut self, track_progress: bool) -> Self {
128 self.progress = track_progress.then(|| CHAIN_EXPORT_STATUS.progress_reporter());
129 self
130 }
131
132 pub fn with_message_receipts(mut self, message_receipts: bool) -> Self {
134 self.message_receipts = message_receipts;
135 self
136 }
137
138 pub fn with_events(mut self, events: bool) -> Self {
141 self.events = events;
142 self
143 }
144
145 pub fn with_tipset_keys(mut self, tipset_keys: bool) -> Self {
147 self.tipset_keys = tipset_keys;
148 self
149 }
150
151 pub fn into_seen(self) -> S {
152 self.seen
153 }
154}
155
156pub fn stream_chain<
168 DB: Blockstore,
169 T: Borrow<Tipset>,
170 ITER: Iterator<Item = T> + Unpin,
171 S: CidHashSetLike,
172>(
173 db: DB,
174 tipset_iter: ITER,
175 stateroot_limit_exclusive: ChainEpoch,
176 seen: S,
177) -> ChainStream<DB, ITER, S> {
178 ChainStream {
179 tipset_iter,
180 db,
181 dfs: VecDeque::new(),
182 seen,
183 stateroot_limit_exclusive,
184 fail_on_dead_links: true,
185 message_receipts: false,
186 events: false,
187 tipset_keys: false,
188 progress: None,
189 n_polled: 0,
190 }
191}
192
193pub fn stream_graph<
196 DB: Blockstore,
197 T: Borrow<Tipset>,
198 ITER: Iterator<Item = T> + Unpin,
199 S: CidHashSetLike,
200>(
201 db: DB,
202 tipset_iter: ITER,
203 stateroot_limit_exclusive: ChainEpoch,
204 seen: S,
205) -> ChainStream<DB, ITER, S> {
206 stream_chain(db, tipset_iter, stateroot_limit_exclusive, seen).fail_on_dead_links(false)
207}
208
209impl<DB: Blockstore, T: Borrow<Tipset>, ITER: Iterator<Item = T> + Unpin, S: CidHashSetLike> Stream
210 for ChainStream<DB, ITER, S>
211{
212 type Item = anyhow::Result<CarBlock>;
213
214 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
215 use Task::*;
216
217 let export_tipset_keys = self.tipset_keys;
218 let fail_on_dead_links = self.fail_on_dead_links;
219 let stateroot_limit_exclusive = self.stateroot_limit_exclusive;
220 let this = self.project();
221
222 {
224 *this.n_polled += 1;
225 if this.n_polled.is_multiple_of(128) {
226 cx.waker().wake_by_ref();
227 return Poll::Pending;
228 }
229 }
230
231 loop {
232 while let Some(task) = this.dfs.front_mut() {
233 match task {
234 Emit(_, _) => {
235 if let Some(Emit(cid, data)) = this.dfs.pop_front() {
236 if let Some(data) = data {
237 return Poll::Ready(Some(Ok(CarBlock { cid, data })));
238 } else if let Some(data) = this.db.get(&cid)? {
239 return Poll::Ready(Some(Ok(CarBlock {
240 cid,
241 data: data.into(),
242 })));
243 } else if fail_on_dead_links {
244 return Poll::Ready(Some(Err(anyhow::anyhow!(
245 "[Emit] missing key: {cid}"
246 ))));
247 };
248 }
249 }
250 Iterate(epoch, block_cid, _type, cid_vec) => {
251 if let Some(progress) = this.progress {
252 progress.update_epoch(*epoch);
253 }
254 while let Some(cid) = cid_vec.pop() {
255 if should_save_block_to_snapshot(cid) && this.seen.insert(cid)? {
261 if let Some(data) = this.db.get(&cid)? {
262 if cid.codec() == fvm_ipld_encoding::DAG_CBOR {
263 let new_values = extract_cids(&data)?;
264 cid_vec.extend(new_values.into_iter().rev());
265 }
266 return Poll::Ready(Some(Ok(CarBlock {
267 cid,
268 data: data.into(),
269 })));
270 } else if fail_on_dead_links {
271 let type_display = match _type {
272 IterateType::Message(c) => {
273 format!("message {c}")
274 }
275 IterateType::StateRoot(c) => {
276 format!("state root {c}")
277 }
278 IterateType::MessageReceipts(c) => {
279 tracing::trace!(
281 "[Iterate] missing key: {cid} from message receipts {c} in block {block_cid} at epoch {epoch}"
282 );
283 continue;
284 }
285 IterateType::EventsRoot(c) => {
286 tracing::trace!(
288 "[Iterate] missing key: {cid} from events root {c} in block {block_cid} at epoch {epoch}"
289 );
290 continue;
291 }
292 };
293 return Poll::Ready(Some(Err(anyhow::anyhow!(
294 "[Iterate] missing key: {cid} from {type_display} in block {block_cid} at epoch {epoch}"
295 ))));
296 }
297 }
298 }
299 this.dfs.pop_front();
300 }
301 }
302 }
303
304 if let Some(tipset) = this.tipset_iter.next() {
308 if export_tipset_keys
310 && let Ok(CarBlock { cid, data }) = tipset.borrow().key().car_block()
311 {
312 this.dfs.push_back(Emit(cid, Some(data)));
313 }
314
315 for block in tipset.borrow().block_headers() {
316 let (cid, data) = block.car_block()?;
317 if this.seen.insert(cid)? {
318 if let Some(progress) = this.progress {
319 progress.update_epoch(block.epoch);
320 }
321 this.dfs.push_back(Emit(cid, Some(data.into())));
323
324 if block.epoch == 0 {
325 for p in &block.parents {
327 this.dfs.push_back(Emit(p, None));
328 }
329 }
330
331 if block.epoch > stateroot_limit_exclusive {
333 this.dfs.push_back(Iterate(
334 block.epoch,
335 *block.cid(),
336 IterateType::Message(block.messages),
337 DfsIter::from(block.messages)
338 .filter_map(ipld_to_cid)
339 .collect(),
340 ));
341 if *this.message_receipts {
342 this.dfs.push_back(Iterate(
343 block.epoch,
344 *block.cid(),
345 IterateType::MessageReceipts(block.message_receipts),
346 DfsIter::from(block.message_receipts)
347 .filter_map(ipld_to_cid)
348 .collect(),
349 ));
350 }
351 if *this.events
353 && let Ok(receipts) =
354 Receipt::get_receipts(this.db, block.message_receipts)
355 {
356 for receipt in receipts {
357 if let Some(events_root) = receipt.events_root() {
358 this.dfs.push_back(Iterate(
359 block.epoch,
360 *block.cid(),
361 IterateType::EventsRoot(events_root),
362 DfsIter::from(events_root)
363 .filter_map(ipld_to_cid)
364 .collect(),
365 ));
366 }
367 }
368 }
369 }
370
371 if block.epoch == 0 || block.epoch > stateroot_limit_exclusive {
374 this.dfs.push_back(Iterate(
377 block.epoch,
378 *block.cid(),
379 IterateType::StateRoot(block.state_root),
380 DfsIter::from(block.state_root)
381 .filter_map(ipld_to_cid)
382 .collect(),
383 ));
384 }
385 }
386 }
387 } else {
388 return Poll::Ready(None);
390 }
391 }
392 }
393}
394
395pin_project! {
396 pub struct IpldStream<DB, S> {
397 db: DB,
398 cid_vec: Vec<Cid>,
399 seen: S,
400 }
401}
402
403impl<DB, S> IpldStream<DB, S> {
404 pub fn new(db: DB, roots: Vec<Cid>, seen: S) -> Self {
405 Self {
406 db,
407 cid_vec: roots,
408 seen,
409 }
410 }
411}
412
413impl<DB: Blockstore, S: CidHashSetLike> Stream for IpldStream<DB, S> {
414 type Item = anyhow::Result<CarBlock>;
415
416 fn poll_next(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Option<Self::Item>> {
417 let this = self.project();
418 while let Some(cid) = this.cid_vec.pop() {
419 if should_save_block_to_snapshot(cid) && this.seen.insert(cid)? {
420 if let Some(data) = this.db.get(&cid)? {
421 if cid.codec() == fvm_ipld_encoding::DAG_CBOR {
422 let new_cids = extract_cids(&data)?;
423 this.cid_vec.extend(new_cids);
424 }
425 return Poll::Ready(Some(Ok(CarBlock {
426 cid,
427 data: data.into(),
428 })));
429 } else {
430 return Poll::Ready(Some(Err(anyhow::anyhow!("missing key: {cid}"))));
431 }
432 }
433 }
434 Poll::Ready(None)
436 }
437}
438
439fn ipld_to_cid(ipld: Ipld) -> Option<Cid> {
440 if let Ipld::Link(cid) = ipld {
441 Some(cid)
442 } else {
443 None
444 }
445}
446
447#[cfg(test)]
448mod tests {
449 use super::*;
450 use crate::blocks::{Chain4U, HeaderBuilder, chain4u};
451 use crate::db::MemoryDB;
452 use crate::utils::db::CborStoreExt as _;
453 use fil_actors_shared::fvm_ipld_amt::Amtv0;
454 use futures::TryStreamExt as _;
455 use fvm_ipld_encoding::RawBytes;
456 use ipld_core::ipld::Ipld;
457 use std::sync::Arc;
458
459 #[tokio::test]
460 async fn return_data_links_are_not_followed_by_the_walk() -> anyhow::Result<()> {
461 let db = Arc::new(MemoryDB::default());
462
463 let embedded = db.put_cbor_default(&Ipld::String("embedded".into()))?;
465
466 let events_root = db.put_cbor_default(&Ipld::String("events-root".into()))?;
469
470 let receipt = fvm_shared4::receipt::Receipt {
473 exit_code: fvm_shared4::error::ExitCode::OK,
474 return_data: RawBytes::new(serde_ipld_dagcbor::to_vec(&Ipld::Link(embedded))?),
475 gas_used: 0,
476 events_root: Some(events_root),
477 };
478
479 let receipts_root = Amtv0::new_from_iter(&db, std::iter::once(receipt))?;
480
481 let c4u = Chain4U::with_blockstore(db.clone());
484 chain4u! {
485 in c4u;
486 [_genesis]
487 -> head @ [_header = HeaderBuilder::new().with_message_receipts(receipts_root)]
488 };
489
490 let mut stream = stream_chain(&db, std::iter::once(head), 0, CidHashSet::default())
491 .with_message_receipts(true)
492 .fail_on_dead_links(false);
495
496 let mut seen = Vec::new();
497 while let Some(block) = stream.try_next().await? {
498 seen.push(block.cid);
499 }
500
501 assert!(
503 seen.contains(&receipts_root),
504 "receipts AMT root must be reachable with receipts enabled"
505 );
506 assert!(
508 seen.contains(&events_root),
509 "EventsRoot link inside the receipt must be followed"
510 );
511 assert!(
513 !seen.contains(&embedded),
514 "link embedded in Return must not be followed by the walk"
515 );
516
517 Ok(())
518 }
519}