Skip to main content

forest/state_manager/
message_search.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4use super::*;
5use crate::blocks::TipsetKey;
6use crate::message::MessageRead as _;
7use ahash::HashSet;
8use parking_lot::RwLock;
9use std::sync::OnceLock;
10use std::time::Duration;
11use tokio::sync::broadcast::error::RecvError;
12use tokio_util::sync::CancellationToken;
13use tracing::warn;
14
15impl StateManager {
16    /// Check if tipset had executed the message, by loading the receipt based
17    /// on the index of the message in the block.
18    fn tipset_executed_message(
19        &self,
20        tipset: &Tipset,
21        message: &ChainMessage,
22        allow_replaced: bool,
23    ) -> Result<Option<Receipt>, Error> {
24        if tipset.epoch() == 0 {
25            return Ok(None);
26        }
27        let message_from_address = message.from();
28        let message_sequence = message.sequence();
29        // Load parent state.
30        let pts = self
31            .chain_index()
32            .load_required_tipset(tipset.parents())
33            .map_err(|err| Error::Other(format!("Failed to load tipset: {err}")))?;
34        let messages = self
35            .cs
36            .messages_for_tipset(&pts)
37            .map_err(|err| Error::Other(format!("Failed to load messages for tipset: {err}")))?;
38        messages
39            .iter()
40            .enumerate()
41            // iterate in reverse because we going backwards through the chain
42            .rev()
43            .filter(|(_, s)| {
44                s.sequence() == message_sequence
45                    && s.from() == message_from_address
46                    && s.equal_call(message)
47            })
48            .map(|(index, m)| {
49                // A replacing message is a message with a different CID,
50                // any of Gas values, and different signature, but with all
51                // other parameters matching (source/destination, nonce, params, etc.)
52                if !allow_replaced && message.cid() != m.cid(){
53                    Err(Error::Other(format!(
54                        "found message with equal nonce and call params but different CID. wanted {}, found: {}, nonce: {}, from: {}",
55                        message.cid(),
56                        m.cid(),
57                        message.sequence(),
58                        message.from(),
59                    )))
60                } else {
61                    let block_header = tipset.block_headers().first();
62                    crate::chain::get_parent_receipt(
63                        self.db(),
64                        block_header,
65                        index,
66                    )
67                        .map_err(|err| Error::Other(format!("Failed to get parent receipt (message_receipts={}, index={index}, error={err})", block_header.message_receipts)))
68                }
69            })
70            .next()
71            .unwrap_or(Ok(None))
72    }
73
74    fn check_search_blocking(
75        &self,
76        mut current: Tipset,
77        message: &ChainMessage,
78        lookback_max_epoch: ChainEpoch,
79        allow_replaced: bool,
80        cancellation_token: &CancellationToken,
81    ) -> Result<Option<(Tipset, Receipt)>, Error> {
82        let message_from_address = message.from();
83        let message_sequence = message.sequence();
84        let current_actor_state = self
85            .get_required_actor(&message_from_address, *current.parent_state())
86            .map_err(Error::state)?;
87        // The sender's nonce only grows, so once it is at or below the message
88        // nonce the message cannot have been executed yet. Walking back would
89        // only end at the sender's creation or at pruned state.
90        if current_actor_state.sequence <= message_sequence {
91            return Ok(None);
92        }
93        let message_from_id = self.lookup_required_id(&message_from_address, &current)?;
94
95        while !cancellation_token.is_cancelled() && current.epoch() >= lookback_max_epoch {
96            let parent_tipset = self
97                .chain_index()
98                .load_required_tipset(current.parents())
99                .map_err(|err| {
100                    Error::Other(format!(
101                        "failed to load tipset during msg wait searchback: {err:}"
102                    ))
103                })?;
104
105            let parent_actor_state = self
106                .get_actor(&message_from_id, *parent_tipset.parent_state())
107                .map_err(|e| Error::State(e.to_string()))?;
108
109            match parent_actor_state {
110                // The nonce is still above the message nonce at the parent, so
111                // the message executed strictly earlier; keep walking back.
112                Some(state) if state.sequence > message_sequence => current = parent_tipset,
113                // The nonce crossed the message nonce between the parent and
114                // `current` (or the sender did not exist yet), so only `current`
115                // can have executed the message. No receipt there means a
116                // replacing message was executed instead.
117                _ => {
118                    return Ok(self
119                        .tipset_executed_message(&current, message, allow_replaced)?
120                        .map(|receipt| (current, receipt)));
121                }
122            }
123        }
124
125        Ok(None)
126    }
127
128    /// Searches backwards through the chain for a message receipt.
129    fn search_back_for_message_blocking(
130        &self,
131        current: Tipset,
132        message: &ChainMessage,
133        look_back_limit: Option<ChainEpoch>,
134        allow_replaced: Option<bool>,
135        cancellation_token: &CancellationToken,
136    ) -> Result<Option<(Tipset, Receipt)>, Error> {
137        let current_epoch = current.epoch();
138        let allow_replaced = allow_replaced.unwrap_or(true);
139
140        let Some(max_lookback_epoch_inclusive) =
141            Self::max_lookback_epoch_inclusive(current_epoch, look_back_limit)
142        else {
143            return Ok(None);
144        };
145
146        self.check_search_blocking(
147            current,
148            message,
149            max_lookback_epoch_inclusive,
150            allow_replaced,
151            cancellation_token,
152        )
153    }
154
155    //. Calculates the max lookback epoch (inclusive lower bound) for the search.
156    pub fn max_lookback_epoch_inclusive(
157        current_epoch: ChainEpoch,
158        look_back_limit: Option<ChainEpoch>,
159    ) -> Option<ChainEpoch> {
160        match look_back_limit {
161            // No search: limit = 0 means search 0 epochs
162            Some(0) => None,
163            // Limited search: calculate the inclusive lower bound, clamped to genesis
164            // Example: limit=5 at epoch=1000 → min_epoch=996, searches [996,1000] = 5 epochs
165            // Example: limit=2000 at epoch=1000 → min_epoch=0, searches [0,1000] = 1001 epochs (all available)
166            Some(limit) if limit > 0 => Some((current_epoch - limit + 1).max(0)),
167            // Search all the way to genesis (epoch 0)
168            _ => Some(0),
169        }
170    }
171
172    /// Returns a message receipt from a given tipset and message CID.
173    pub fn get_receipt_blocking(
174        &self,
175        tipset: Tipset,
176        msg: Cid,
177        cancellation_token: &CancellationToken,
178    ) -> Result<Receipt, Error> {
179        let m = crate::chain::get_chain_message(self.db(), &msg)
180            .map_err(|e| Error::Other(e.to_string()))?;
181        let message_receipt = self.tipset_executed_message(&tipset, &m, true)?;
182        if let Some(receipt) = message_receipt {
183            return Ok(receipt);
184        }
185
186        let maybe_tuple =
187            self.search_back_for_message_blocking(tipset, &m, None, None, cancellation_token)?;
188        let message_receipt = maybe_tuple
189            .ok_or_else(|| {
190                Error::Other("Could not get receipt from search back message".to_string())
191            })?
192            .1;
193        Ok(message_receipt)
194    }
195
196    pub async fn wait_for_message_with_timeout(
197        &self,
198        msg_cid: Cid,
199        confidence: i64,
200        look_back_limit: Option<ChainEpoch>,
201        allow_replaced: Option<bool>,
202        timeout: Duration,
203    ) -> Result<(Tipset, Receipt), Error> {
204        let cancellation_token = CancellationToken::new();
205        let _cancellation_token_drop_guard = cancellation_token.drop_guard_ref();
206        tokio::time::timeout(
207            timeout,
208            self.wait_for_message(
209                msg_cid,
210                confidence,
211                look_back_limit,
212                allow_replaced,
213                &cancellation_token,
214            ),
215        )
216        .await
217        .map_err(|_| {
218            Error::other(format!(
219                "wait_for_message timed out after {}",
220                humantime::format_duration(timeout)
221            ))
222        })?
223    }
224
225    /// `WaitForMessage` blocks until a message appears on chain. It looks
226    /// backwards in the chain to see if this has already happened. It
227    /// guarantees that the message has been on chain for at least
228    /// confidence epochs without being reverted before returning.
229    /// Returns an error when cancelled.
230    pub async fn wait_for_message(
231        &self,
232        msg_cid: Cid,
233        confidence: i64,
234        look_back_limit: Option<ChainEpoch>,
235        allow_replaced: Option<bool>,
236        cancellation_token: &CancellationToken,
237    ) -> Result<(Tipset, Receipt), Error> {
238        let message = Arc::new(
239            crate::chain::get_chain_message(self.db(), &msg_cid)
240                .map_err(|err| Error::Other(format!("failed to load message {err:}")))?,
241        );
242        let current_ts = self.heaviest_tipset();
243        let maybe_message_receipt = self.tipset_executed_message(&current_ts, &message, true)?;
244        if let Some(receipt) = maybe_message_receipt {
245            return Ok((current_ts, receipt));
246        }
247
248        // For immediate search back response
249        let (search_back_tx, search_back_rx) = flume::bounded(1);
250        let search_back_candidate: Arc<OnceLock<(Tipset, Receipt)>> = Default::default();
251        let reverted: Arc<RwLock<HashSet<TipsetKey>>> = Arc::new(RwLock::new(HashSet::default()));
252        // Search back task
253        tokio::task::spawn_blocking({
254            let sm = self.shallow_clone();
255            let message = message.shallow_clone();
256            // Cloning tx to avoid all senders being dropped to make `search_back_rx.recv_async()` wait
257            let search_back_tx = search_back_tx.clone();
258            let search_back_candidate = search_back_candidate.shallow_clone();
259            let reverted = reverted.shallow_clone();
260            let cancellation_token = cancellation_token.clone();
261            move || {
262                if let Ok(Some((ts, receipt))) = sm
263                    .search_back_for_message_blocking(
264                        current_ts,
265                        &message,
266                        look_back_limit,
267                        allow_replaced,
268                        &cancellation_token,
269                    )
270                    .inspect_err(|e| {
271                        tracing::warn!("failed to search back for message: {e}");
272                    })
273                    && !reverted.read().contains(ts.key())
274                {
275                    if sm.heaviest_tipset().epoch() >= ts.epoch() + confidence {
276                        _ = search_back_tx.send((ts, receipt)).inspect_err(|e| {
277                            tracing::warn!("failed to send to search_back_tx: {e}");
278                        });
279                    } else {
280                        _ = search_back_candidate.set((ts, receipt)).inspect_err(|_| {
281                            tracing::warn!("failed to send to set search_back_candidate");
282                        });
283                    }
284                }
285            }
286        });
287
288        // Wait for message to be included in head change.
289        let subscriber_poll = tokio::task::spawn({
290            let cancellation_token = cancellation_token.clone();
291            let search_back_candidate = search_back_candidate.shallow_clone();
292            let reverted = reverted.shallow_clone();
293            let sm = self.shallow_clone();
294            async move {
295                let mut head_changes_rx = sm.cs.subscribe_head_changes();
296                let mut candidate: Option<(Tipset, Receipt)> = None;
297                while !cancellation_token.is_cancelled() {
298                    match head_changes_rx.recv().await {
299                        Ok(head_changes) => {
300                            for reverted_ts in head_changes.reverts {
301                                reverted.write().insert(reverted_ts.key().clone());
302
303                                if candidate
304                                    .as_ref()
305                                    .is_some_and(|(ts, _)| ts.key() == reverted_ts.key())
306                                {
307                                    candidate = None;
308                                }
309                            }
310                            for applied_ts in head_changes.applies {
311                                reverted.write().remove(applied_ts.key());
312
313                                // Return if `search_back_candidate` meets confidence requirement
314                                if let Some((candidate_ts, candidate_receipt)) =
315                                    search_back_candidate.get()
316                                    && applied_ts.epoch() >= candidate_ts.epoch() + confidence
317                                    && !reverted.read().contains(candidate_ts.key())
318                                {
319                                    return Ok((
320                                        candidate_ts.shallow_clone(),
321                                        candidate_receipt.clone(),
322                                    ));
323                                }
324
325                                // Return if the candidate meets confidence requirement
326                                if let Some((candidate_ts, _)) = &candidate
327                                    && applied_ts.epoch() >= candidate_ts.epoch() + confidence
328                                    && let Some(candidate) = candidate
329                                {
330                                    return Ok(candidate);
331                                }
332
333                                let maybe_receipt =
334                                    sm.tipset_executed_message(&applied_ts, &message, true)?;
335                                if let Some(receipt) = maybe_receipt {
336                                    if confidence == 0 {
337                                        // Return if there's no confidence requirement
338                                        return Ok((applied_ts, receipt));
339                                    } else {
340                                        // Otherwise set it as candidate
341                                        candidate = Some((applied_ts, receipt));
342                                    }
343                                }
344                            }
345                        }
346                        Err(RecvError::Lagged(i)) => {
347                            warn!(
348                                "wait for message head change subscriber lagged, skipped {} events",
349                                i
350                            );
351                        }
352                        Err(RecvError::Closed) => break,
353                    }
354                }
355                Err(Error::other("cancelled"))
356            }
357        });
358
359        // Await on first future to finish.
360        tokio::select! {
361            res = subscriber_poll => {
362                res.context("tokio join error")?
363            }
364            res = search_back_rx.recv_async()  => {
365                Ok(res.context("channel receive error")?)
366            }
367            _ = cancellation_token.cancelled() => {
368                Err(Error::other("cancelled"))
369            }
370        }
371    }
372
373    pub async fn search_for_message(
374        &self,
375        from: Option<Tipset>,
376        msg_cid: Cid,
377        look_back_limit: Option<i64>,
378        allow_replaced: Option<bool>,
379        cancellation_token: &CancellationToken,
380    ) -> Result<Option<(Tipset, Receipt)>, Error> {
381        let from = from.unwrap_or_else(|| self.heaviest_tipset());
382        let message = crate::chain::get_chain_message(self.db(), &msg_cid)
383            .map_err(|err| Error::Other(format!("failed to load message {err}")))?;
384        let maybe_message_receipt =
385            self.tipset_executed_message(&from, &message, allow_replaced.unwrap_or(true))?;
386        if let Some(r) = maybe_message_receipt {
387            Ok(Some((from, r)))
388        } else {
389            tokio::task::spawn_blocking({
390                let this = self.shallow_clone();
391                let cancellation_token = cancellation_token.clone();
392                move || {
393                    this.search_back_for_message_blocking(
394                        from,
395                        &message,
396                        look_back_limit,
397                        allow_replaced,
398                        &cancellation_token,
399                    )
400                }
401            })
402            .await?
403        }
404    }
405}
406
407#[cfg(test)]
408mod tests {
409    use super::*;
410    use crate::blocks::{
411        CachingBlockHeader, Chain4U, HeaderBuilder, RawBlockHeader, TxMeta, chain4u,
412    };
413    use crate::chain::ChainStore;
414    use crate::db::MemoryDB;
415    use crate::networks::ChainConfig;
416    use crate::shim::address::Address;
417    use crate::shim::econ::TokenAmount;
418    use crate::shim::message::Message;
419    use crate::shim::state_tree::{ActorState, StateTree, StateTreeVersion};
420    use crate::utils::db::CborStoreExt as _;
421    use fil_actors_shared::fvm_ipld_amt::Amtv0;
422    use fvm_ipld_blockstore::Blockstore;
423
424    const SENDER: Address = Address::new_id(100);
425
426    fn state_root_with_sender_nonce(db: &Arc<MemoryDB>, sequence: u64) -> Cid {
427        let mut state_tree = StateTree::new(db, StateTreeVersion::V5).unwrap();
428        state_tree
429            .set_actor(
430                &SENDER,
431                ActorState::new(
432                    Cid::default(),
433                    Cid::default(),
434                    TokenAmount::default(),
435                    sequence,
436                    None,
437                ),
438            )
439            .unwrap();
440        state_tree.flush().unwrap()
441    }
442
443    fn tx_meta(db: &impl Blockstore, message: Cid) -> Cid {
444        let bls_message_root = Amtv0::new_from_iter(db, [message]).unwrap();
445        let secp_message_root = Amtv0::new_from_iter(db, std::iter::empty::<Cid>()).unwrap();
446        db.put_cbor_default(&TxMeta {
447            bls_message_root,
448            secp_message_root,
449        })
450        .unwrap()
451    }
452
453    fn receipts_root(db: &impl Blockstore) -> Cid {
454        let receipt = fvm_shared4::receipt::Receipt {
455            exit_code: fvm_shared4::error::ExitCode::OK,
456            return_data: Default::default(),
457            gas_used: 0,
458            events_root: None,
459        };
460        Amtv0::new_from_iter(db, [receipt]).unwrap()
461    }
462
463    fn message_with_nonce(sequence: u64) -> Message {
464        Message {
465            from: SENDER,
466            to: Address::new_id(101),
467            sequence,
468            ..Default::default()
469        }
470    }
471
472    fn state_manager_with_head(
473        db: Arc<MemoryDB>,
474        genesis: &RawBlockHeader,
475        head: &Tipset,
476    ) -> StateManager {
477        let chain_store = ChainStore::new(
478            db,
479            Arc::new(ChainConfig::default()),
480            CachingBlockHeader::new(genesis.clone()),
481        )
482        .unwrap();
483        chain_store.set_heaviest_tipset(head.clone()).unwrap();
484        StateManager::new(chain_store).unwrap()
485    }
486
487    /// Chain where the sender's nonce is `actor_nonce` at every epoch and the
488    /// genesis state is unavailable, like state pruned by GC. Searching must
489    /// not walk into the missing state.
490    async fn search_pending(
491        actor_nonce: u64,
492        message_nonce: u64,
493    ) -> Result<Option<(Tipset, Receipt)>, Error> {
494        let db = Arc::new(MemoryDB::default());
495        let root = state_root_with_sender_nonce(&db, actor_nonce);
496        let c4u = Chain4U::with_blockstore(db.clone());
497        chain4u! {
498            in c4u;
499            [genesis = HeaderBuilder::new().with_timestamp(7777)]
500            -> [_e1 = HeaderBuilder::new().with_state_root(root)]
501            -> [_e2 = HeaderBuilder::new().with_state_root(root)]
502            -> head @ [_e3 = HeaderBuilder::new().with_state_root(root)]
503        };
504        let state_manager = state_manager_with_head(db.clone(), genesis, head);
505
506        let message = message_with_nonce(message_nonce);
507        let msg_cid = db.put_cbor_default(&message).unwrap();
508
509        state_manager
510            .search_for_message(None, msg_cid, None, Some(true), &CancellationToken::new())
511            .await
512    }
513
514    #[tokio::test]
515    async fn search_returns_none_for_message_with_future_nonce() {
516        let result = search_pending(5, 10).await.unwrap();
517        assert!(result.is_none());
518    }
519
520    #[tokio::test]
521    async fn search_returns_none_for_pending_message_at_current_nonce() {
522        let result = search_pending(5, 5).await.unwrap();
523        assert!(result.is_none());
524    }
525
526    #[tokio::test]
527    async fn search_returns_none_for_pending_message_from_fresh_sender() {
528        let result = search_pending(0, 0).await.unwrap();
529        assert!(result.is_none());
530    }
531
532    /// The sender's nonce crossed the message nonce, but a replacing message
533    /// with a different call executed instead: the searched message was never
534    /// executed, so the result is `None`, not an error.
535    #[tokio::test]
536    async fn search_returns_none_for_replaced_message() {
537        let db = Arc::new(MemoryDB::default());
538        let message = message_with_nonce(5);
539        let msg_cid = db.put_cbor_default(&message).unwrap();
540
541        let root_before = state_root_with_sender_nonce(&db, 5);
542        let root_after = state_root_with_sender_nonce(&db, 6);
543        let c4u = Chain4U::with_blockstore(db.clone());
544        chain4u! {
545            in c4u;
546            [genesis = HeaderBuilder::new().with_timestamp(7777)]
547            -> [_e1 = HeaderBuilder::new().with_state_root(root_before)]
548            -> [_e2 = HeaderBuilder::new().with_state_root(root_before)]
549            -> [_e3 = HeaderBuilder::new().with_state_root(root_after)]
550            -> head @ [_e4 = HeaderBuilder::new().with_state_root(root_after)]
551        };
552        let state_manager = state_manager_with_head(db.clone(), genesis, head);
553
554        let result = state_manager
555            .search_for_message(None, msg_cid, None, Some(true), &CancellationToken::new())
556            .await
557            .unwrap();
558        assert!(result.is_none());
559    }
560
561    #[tokio::test]
562    async fn search_finds_executed_message() {
563        let db = Arc::new(MemoryDB::default());
564        let message = message_with_nonce(5);
565        let msg_cid = db.put_cbor_default(&message).unwrap();
566
567        let root_before = state_root_with_sender_nonce(&db, 5);
568        let root_after = state_root_with_sender_nonce(&db, 6);
569        let messages = tx_meta(&db, msg_cid);
570        let receipts = receipts_root(&db);
571        let c4u = Chain4U::with_blockstore(db.clone());
572        chain4u! {
573            in c4u;
574            [genesis = HeaderBuilder::new().with_timestamp(7777)]
575            -> [_e1 = HeaderBuilder::new().with_state_root(root_before)]
576            -> [_e2 = HeaderBuilder::new()
577                    .with_state_root(root_before)
578                    .with_messages(messages)]
579            -> [_e3 = HeaderBuilder::new()
580                    .with_state_root(root_after)
581                    .with_message_receipts(receipts)]
582            -> head @ [_e4 = HeaderBuilder::new().with_state_root(root_after)]
583        };
584        let state_manager = state_manager_with_head(db.clone(), genesis, head);
585
586        let (tipset, receipt) = state_manager
587            .search_for_message(None, msg_cid, None, Some(true), &CancellationToken::new())
588            .await
589            .unwrap()
590            .expect("executed message should be found");
591        assert_eq!(tipset.epoch(), 3);
592        assert!(receipt.exit_code().is_success());
593    }
594
595    /// Searching from an explicit tipset only covers executions at or below
596    /// it, like in Lotus, even when the message executed later in the chain.
597    #[tokio::test]
598    async fn search_from_older_tipset_ignores_later_execution() {
599        let db = Arc::new(MemoryDB::default());
600        let message = message_with_nonce(5);
601        let msg_cid = db.put_cbor_default(&message).unwrap();
602
603        let root_before = state_root_with_sender_nonce(&db, 5);
604        let root_after = state_root_with_sender_nonce(&db, 6);
605        let messages = tx_meta(&db, msg_cid);
606        let receipts = receipts_root(&db);
607        let c4u = Chain4U::with_blockstore(db.clone());
608        chain4u! {
609            in c4u;
610            [genesis = HeaderBuilder::new().with_timestamp(7777)]
611            -> [_e1 = HeaderBuilder::new().with_state_root(root_before)]
612            -> from @ [_e2 = HeaderBuilder::new()
613                    .with_state_root(root_before)
614                    .with_messages(messages)]
615            -> [_e3 = HeaderBuilder::new()
616                    .with_state_root(root_after)
617                    .with_message_receipts(receipts)]
618            -> head @ [_e4 = HeaderBuilder::new().with_state_root(root_after)]
619        };
620        let state_manager = state_manager_with_head(db.clone(), genesis, head);
621
622        let result = state_manager
623            .search_for_message(
624                Some(from.clone()),
625                msg_cid,
626                None,
627                Some(true),
628                &CancellationToken::new(),
629            )
630            .await
631            .unwrap();
632        assert!(result.is_none());
633    }
634}