Skip to main content

forest/ipld/
util.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4use 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    // Don't include identity CIDs.
24    // We only include raw and dagcbor, for now.
25    // Raw for "code" CIDs.
26    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
36/// Depth-first-search iterator for `ipld` leaf nodes.
37///
38/// This iterator consumes the given `ipld` structure and returns leaf nodes (i.e.,
39/// no list or map) in depth-first order. The iterator can be extended at any
40/// point by the caller.
41///
42/// Consider walking this `ipld` graph:
43/// ```text
44/// List
45///  ├ Integer(5)
46///  ├ Link(Y)
47///  └ String("string")
48///
49/// Link(Y):
50/// Map
51///  ├ "key1" => Bool(true)
52///  └ "key2" => Float(3.14)
53/// ```
54///
55/// If we walk the above `ipld` graph (replacing `Link(Y)` when it is encountered), the leaf nodes will be seen in this order:
56/// 1. `Integer(5)`
57/// 2. `Bool(true)`
58/// 3. `Float(3.14)`
59/// 4. `String("string")`
60pub 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    // Yield the block, don't visit it.
100    Emit(Cid, Option<Bytes>),
101    // Visit all the elements, recursively.
102    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>, // Depth-first work queue.
110        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    /// Whether to enable traversal of message receipt roots during chain export.
133    pub fn with_message_receipts(mut self, message_receipts: bool) -> Self {
134        self.message_receipts = message_receipts;
135        self
136    }
137
138    /// Whether to enable traversal of events roots during chain export.
139    /// Requires message receipts to be enabled as well.
140    pub fn with_events(mut self, events: bool) -> Self {
141        self.events = events;
142        self
143    }
144
145    /// Whether to export tipset keys.
146    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
156/// Stream all blocks that are reachable before the `stateroot_limit` epoch in a depth-first
157/// fashion.
158/// After this limit, only block headers are streamed. Any dead links are reported as errors.
159///
160/// # Arguments
161///
162/// * `db` - A database that implements [`Blockstore`] interface.
163/// * `tipset_iter` - An iterator of [`Tipset`], descending order `$child -> $parent`.
164/// * `stateroot_limit` - An epoch that signifies how far back (exclusive) we need to inspect tipsets,
165///   in-depth. This has to be pre-calculated using this formula: `$cur_epoch - $depth`, where `$depth`
166///   is the number of `[`Tipset`]` that needs inspection.
167pub 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
193// Stream available graph in a depth-first search. All reachable nodes are touched and dead-links
194// are ignored.
195pub 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        // Yield to the runtime every 128 polls to allow cancellation
223        {
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                            // The link traversal implementation assumes there are three types of encoding:
256                            // 1. DAG_CBOR: needs to be reachable, so we add it to the queue and load.
257                            // 2. IPLD_RAW: WASM blocks, for example. Need to be loaded, but not traversed.
258                            // 3. _: ignore all other links
259                            // Don't revisit what's already been visited.
260                            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                                            // Forgive message receipts
280                                            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                                            // Forgive events
287                                            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            // This consumes a [`Tipset`] from the iterator one at a time. The next iteration of the
305            // enclosing loop is processing the queue. Once the desired depth has been reached -
306            // yield the block without walking the graph it represents.
307            if let Some(tipset) = this.tipset_iter.next() {
308                // Tipset key cid can be convert from and to eth hash, which is useful for Eth APIs
309                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                        // Make sure we always yield a block otherwise.
322                        this.dfs.push_back(Emit(cid, Some(data.into())));
323
324                        if block.epoch == 0 {
325                            // The genesis block has some kind of dummy parent that needs to be emitted.
326                            for p in &block.parents {
327                                this.dfs.push_back(Emit(p, None));
328                            }
329                        }
330
331                        // Process block messages.
332                        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                            // ignore failure as receipts are not required by a lite snapshot
352                            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                        // Visit the block if it's within required depth. And a special case for `0`
372                        // epoch to match Lotus' implementation.
373                        if block.epoch == 0 || block.epoch > stateroot_limit_exclusive {
374                            // NOTE: In the original `walk_snapshot` implementation we walk the dag
375                            // immediately. Which is what we do here as well, but using a queue.
376                            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                // That's it, nothing else to do. End of stream.
389                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        // That's it, nothing else to do. End of stream.
435        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        // A fetchable dag-cbor block, reached only if the walk follows links inside `Return`.
464        let embedded = db.put_cbor_default(&Ipld::String("embedded".into()))?;
465
466        // A link in the receipt (positive control): the walk does reach tag-42 links
467        // structurally embedded in the receipts AMT.
468        let events_root = db.put_cbor_default(&Ipld::String("events-root".into()))?;
469
470        // Build the receipt: `Return` is itself valid dag-cbor encoding a tag-42 link to the
471        // `embedded` block.
472        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        // One-block tipset whose `message_receipts` points at the AMT. Epoch 1 (> the stateroot
482        // limit below) so the receipts branch is reached.
483        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            // The embedded target is present; other roots (e.g. default state roots) are not, and a
493            // missing root must not stall the walk.
494            .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        // Receipts walking is active and reaches the AMT root...
502        assert!(
503            seen.contains(&receipts_root),
504            "receipts AMT root must be reachable with receipts enabled"
505        );
506        // ...and follows the EventsRoot link inside it.
507        assert!(
508            seen.contains(&events_root),
509            "EventsRoot link inside the receipt must be followed"
510        );
511        // But the link inside the opaque `Return` byte string is never followed.
512        assert!(
513            !seen.contains(&embedded),
514            "link embedded in Return must not be followed by the walk"
515        );
516
517        Ok(())
518    }
519}