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
15/// Maximum allowed message confidence.
16const MAX_MESSAGE_CONFIDENCE: ChainEpoch = crate::shim::policy::policy_constants::CHAIN_FINALITY;
17
18/// Checks whether `current` is at least `confidence` epochs past `candidate`.
19fn confidence_reached(current: ChainEpoch, candidate: ChainEpoch, confidence: i64) -> bool {
20    candidate >= 0 && current >= candidate && (current - candidate) >= confidence
21}
22
23impl StateManager {
24    /// Check if tipset had executed the message, by loading the receipt based
25    /// on the index of the message in the block.
26    fn tipset_executed_message(
27        &self,
28        tipset: &Tipset,
29        message: &ChainMessage,
30        allow_replaced: bool,
31    ) -> Result<Option<Receipt>, Error> {
32        if tipset.epoch() == 0 {
33            return Ok(None);
34        }
35        let message_from_address = message.from();
36        let message_sequence = message.sequence();
37        // Load parent state.
38        let pts = self
39            .chain_index()
40            .load_required_tipset(tipset.parents())
41            .map_err(|err| Error::Other(format!("Failed to load tipset: {err}")))?;
42        let messages = self
43            .cs
44            .messages_for_tipset(&pts)
45            .map_err(|err| Error::Other(format!("Failed to load messages for tipset: {err}")))?;
46        messages
47            .iter()
48            .enumerate()
49            // iterate in reverse because we going backwards through the chain
50            .rev()
51            .filter(|(_, s)| {
52                s.sequence() == message_sequence
53                    && s.from() == message_from_address
54                    && s.equal_call(message)
55            })
56            .map(|(index, m)| {
57                // A replacing message is a message with a different CID,
58                // any of Gas values, and different signature, but with all
59                // other parameters matching (source/destination, nonce, params, etc.)
60                if !allow_replaced && message.cid() != m.cid(){
61                    Err(Error::Other(format!(
62                        "found message with equal nonce and call params but different CID. wanted {}, found: {}, nonce: {}, from: {}",
63                        message.cid(),
64                        m.cid(),
65                        message.sequence(),
66                        message.from(),
67                    )))
68                } else {
69                    let block_header = tipset.block_headers().first();
70                    crate::chain::get_parent_receipt(
71                        self.db(),
72                        block_header,
73                        index,
74                    )
75                        .map_err(|err| Error::Other(format!("Failed to get parent receipt (message_receipts={}, index={index}, error={err})", block_header.message_receipts)))
76                }
77            })
78            .next()
79            .unwrap_or(Ok(None))
80    }
81
82    fn check_search_blocking(
83        &self,
84        mut current: Tipset,
85        message: &ChainMessage,
86        lookback_max_epoch: ChainEpoch,
87        allow_replaced: bool,
88        cancellation_token: &CancellationToken,
89    ) -> Result<Option<(Tipset, Receipt)>, Error> {
90        let message_from_address = message.from();
91        let message_sequence = message.sequence();
92        let current_actor_state = self
93            .get_required_actor(&message_from_address, *current.parent_state())
94            .map_err(Error::state)?;
95        // The sender's nonce only grows, so once it is at or below the message
96        // nonce the message cannot have been executed yet. Walking back would
97        // only end at the sender's creation or at pruned state.
98        if current_actor_state.sequence <= message_sequence {
99            return Ok(None);
100        }
101        let message_from_id = self.lookup_required_id(&message_from_address, &current)?;
102
103        while !cancellation_token.is_cancelled() && current.epoch() >= lookback_max_epoch {
104            let parent_tipset = self
105                .chain_index()
106                .load_required_tipset(current.parents())
107                .map_err(|err| {
108                    Error::Other(format!(
109                        "failed to load tipset during msg wait searchback: {err:}"
110                    ))
111                })?;
112
113            let parent_actor_state = self
114                .get_actor(&message_from_id, *parent_tipset.parent_state())
115                .map_err(|e| Error::State(e.to_string()))?;
116
117            match parent_actor_state {
118                // The nonce is still above the message nonce at the parent, so
119                // the message executed strictly earlier; keep walking back.
120                Some(state) if state.sequence > message_sequence => current = parent_tipset,
121                // The nonce crossed the message nonce between the parent and
122                // `current` (or the sender did not exist yet), so only `current`
123                // can have executed the message. No receipt there means a
124                // replacing message was executed instead.
125                _ => {
126                    return Ok(self
127                        .tipset_executed_message(&current, message, allow_replaced)?
128                        .map(|receipt| (current, receipt)));
129                }
130            }
131        }
132
133        Ok(None)
134    }
135
136    /// Searches backwards through the chain for a message receipt.
137    fn search_back_for_message_blocking(
138        &self,
139        current: Tipset,
140        message: &ChainMessage,
141        look_back_limit: Option<ChainEpoch>,
142        allow_replaced: Option<bool>,
143        cancellation_token: &CancellationToken,
144    ) -> Result<Option<(Tipset, Receipt)>, Error> {
145        let current_epoch = current.epoch();
146        let allow_replaced = allow_replaced.unwrap_or(true);
147
148        let Some(max_lookback_epoch_inclusive) =
149            Self::max_lookback_epoch_inclusive(current_epoch, look_back_limit)
150        else {
151            return Ok(None);
152        };
153
154        self.check_search_blocking(
155            current,
156            message,
157            max_lookback_epoch_inclusive,
158            allow_replaced,
159            cancellation_token,
160        )
161    }
162
163    //. Calculates the max lookback epoch (inclusive lower bound) for the search.
164    pub fn max_lookback_epoch_inclusive(
165        current_epoch: ChainEpoch,
166        look_back_limit: Option<ChainEpoch>,
167    ) -> Option<ChainEpoch> {
168        match look_back_limit {
169            // No search: limit = 0 means search 0 epochs
170            Some(0) => None,
171            // Limited search: calculate the inclusive lower bound, clamped to genesis
172            // Example: limit=5 at epoch=1000 → min_epoch=996, searches [996,1000] = 5 epochs
173            // Example: limit=2000 at epoch=1000 → min_epoch=0, searches [0,1000] = 1001 epochs (all available)
174            Some(limit) if limit > 0 => Some((current_epoch - limit + 1).max(0)),
175            // Search all the way to genesis (epoch 0)
176            _ => Some(0),
177        }
178    }
179
180    /// Returns a message receipt from a given tipset and message CID.
181    pub fn get_receipt_blocking(
182        &self,
183        tipset: Tipset,
184        msg: Cid,
185        cancellation_token: &CancellationToken,
186    ) -> Result<Receipt, Error> {
187        let m = crate::chain::get_chain_message(self.db(), &msg)
188            .map_err(|e| Error::Other(e.to_string()))?;
189        let message_receipt = self.tipset_executed_message(&tipset, &m, true)?;
190        if let Some(receipt) = message_receipt {
191            return Ok(receipt);
192        }
193
194        let maybe_tuple =
195            self.search_back_for_message_blocking(tipset, &m, None, None, cancellation_token)?;
196        let message_receipt = maybe_tuple
197            .ok_or_else(|| {
198                Error::Other("Could not get receipt from search back message".to_string())
199            })?
200            .1;
201        Ok(message_receipt)
202    }
203
204    pub async fn wait_for_message_with_timeout(
205        &self,
206        msg_cid: Cid,
207        confidence: i64,
208        look_back_limit: Option<ChainEpoch>,
209        allow_replaced: Option<bool>,
210        timeout: Duration,
211    ) -> Result<(Tipset, Receipt), Error> {
212        let cancellation_token = CancellationToken::new();
213        let _cancellation_token_drop_guard = cancellation_token.drop_guard_ref();
214        tokio::time::timeout(
215            timeout,
216            self.wait_for_message(
217                msg_cid,
218                confidence,
219                look_back_limit,
220                allow_replaced,
221                &cancellation_token,
222            ),
223        )
224        .await
225        .map_err(|_| {
226            Error::other(format!(
227                "wait_for_message timed out after {}",
228                humantime::format_duration(timeout)
229            ))
230        })?
231    }
232
233    /// `WaitForMessage` blocks until a message appears on chain. It looks
234    /// backwards in the chain to see if this has already happened. It
235    /// guarantees that the message has been on chain for at least
236    /// confidence epochs without being reverted before returning.
237    /// Returns an error when cancelled.
238    pub async fn wait_for_message(
239        &self,
240        msg_cid: Cid,
241        confidence: i64,
242        look_back_limit: Option<ChainEpoch>,
243        allow_replaced: Option<bool>,
244        cancellation_token: &CancellationToken,
245    ) -> Result<(Tipset, Receipt), Error> {
246        if confidence > MAX_MESSAGE_CONFIDENCE {
247            return Err(Error::other(format!(
248                "message confidence exceeds maximum: {confidence} > {MAX_MESSAGE_CONFIDENCE}"
249            )));
250        }
251        let message = Arc::new(
252            crate::chain::get_chain_message(self.db(), &msg_cid)
253                .map_err(|err| Error::Other(format!("failed to load message {err:}")))?,
254        );
255        // Subscribe to head changes before sampling the head so that a reorg
256        // between sampling and subscribing cannot be missed. Otherwise a revert
257        // of the sampled head could go unseen and a reverted receipt could be
258        // released after `confidence` epochs.
259        let mut head_changes_rx = self.cs.subscribe_head_changes();
260        let current_ts = self.heaviest_tipset();
261        let maybe_message_receipt =
262            self.tipset_executed_message(&current_ts, &message, allow_replaced.unwrap_or(true))?;
263        // If the message already executed at the current head, return right
264        // away only when no confidence is required; otherwise seed it as a
265        // candidate for the head-change loop to release after `confidence` epochs.
266        let initial_candidate = match maybe_message_receipt {
267            Some(receipt) if confidence == 0 => return Ok((current_ts, receipt)),
268            Some(receipt) => Some((current_ts.shallow_clone(), receipt)),
269            None => None,
270        };
271
272        // For immediate search back response
273        let (search_back_tx, search_back_rx) = flume::bounded(1);
274        let search_back_candidate: Arc<OnceLock<(Tipset, Receipt)>> = Default::default();
275        let reverted: Arc<RwLock<HashSet<TipsetKey>>> = Arc::new(RwLock::new(HashSet::default()));
276        // Search back task
277        tokio::task::spawn_blocking({
278            let sm = self.shallow_clone();
279            let message = message.shallow_clone();
280            // Cloning tx to avoid all senders being dropped to make `search_back_rx.recv_async()` wait
281            let search_back_tx = search_back_tx.clone();
282            let search_back_candidate = search_back_candidate.shallow_clone();
283            let reverted = reverted.shallow_clone();
284            let cancellation_token = cancellation_token.clone();
285            move || {
286                if let Ok(Some((ts, receipt))) = sm
287                    .search_back_for_message_blocking(
288                        current_ts,
289                        &message,
290                        look_back_limit,
291                        allow_replaced,
292                        &cancellation_token,
293                    )
294                    .inspect_err(|e| {
295                        tracing::warn!("failed to search back for message: {e}");
296                    })
297                    && !reverted.read().contains(ts.key())
298                {
299                    if confidence_reached(sm.heaviest_tipset().epoch(), ts.epoch(), confidence) {
300                        _ = search_back_tx.send((ts, receipt)).inspect_err(|e| {
301                            tracing::warn!("failed to send to search_back_tx: {e}");
302                        });
303                    } else {
304                        _ = search_back_candidate.set((ts, receipt)).inspect_err(|_| {
305                            tracing::warn!("failed to send to set search_back_candidate");
306                        });
307                    }
308                }
309            }
310        });
311
312        // Wait for message to be included in head change.
313        let subscriber_poll = tokio::task::spawn({
314            let cancellation_token = cancellation_token.clone();
315            let search_back_candidate = search_back_candidate.shallow_clone();
316            let reverted = reverted.shallow_clone();
317            let sm = self.shallow_clone();
318            async move {
319                let mut candidate: Option<(Tipset, Receipt)> = initial_candidate;
320                while !cancellation_token.is_cancelled() {
321                    match head_changes_rx.recv().await {
322                        Ok(head_changes) => {
323                            for reverted_ts in head_changes.reverts {
324                                reverted.write().insert(reverted_ts.key().clone());
325
326                                if candidate
327                                    .as_ref()
328                                    .is_some_and(|(ts, _)| ts.key() == reverted_ts.key())
329                                {
330                                    candidate = None;
331                                }
332                            }
333                            for applied_ts in head_changes.applies {
334                                reverted.write().remove(applied_ts.key());
335
336                                // Return if `search_back_candidate` meets confidence requirement
337                                if let Some((candidate_ts, candidate_receipt)) =
338                                    search_back_candidate.get()
339                                    && confidence_reached(
340                                        applied_ts.epoch(),
341                                        candidate_ts.epoch(),
342                                        confidence,
343                                    )
344                                    && !reverted.read().contains(candidate_ts.key())
345                                {
346                                    return Ok((
347                                        candidate_ts.shallow_clone(),
348                                        candidate_receipt.clone(),
349                                    ));
350                                }
351
352                                // Return if the candidate meets confidence requirement
353                                if let Some((candidate_ts, _)) = &candidate
354                                    && confidence_reached(
355                                        applied_ts.epoch(),
356                                        candidate_ts.epoch(),
357                                        confidence,
358                                    )
359                                    && let Some(candidate) = candidate
360                                {
361                                    return Ok(candidate);
362                                }
363
364                                let maybe_receipt = sm.tipset_executed_message(
365                                    &applied_ts,
366                                    &message,
367                                    allow_replaced.unwrap_or(true),
368                                )?;
369                                if let Some(receipt) = maybe_receipt {
370                                    if confidence == 0 {
371                                        // Return if there's no confidence requirement
372                                        return Ok((applied_ts, receipt));
373                                    } else {
374                                        // Otherwise set it as candidate
375                                        candidate = Some((applied_ts, receipt));
376                                    }
377                                }
378                            }
379                        }
380                        Err(RecvError::Lagged(i)) => {
381                            warn!(
382                                "wait for message head change subscriber lagged, skipped {} events",
383                                i
384                            );
385                        }
386                        Err(RecvError::Closed) => break,
387                    }
388                }
389                Err(Error::other("cancelled"))
390            }
391        });
392
393        // Await on first future to finish.
394        tokio::select! {
395            res = subscriber_poll => {
396                res.context("tokio join error")?
397            }
398            res = search_back_rx.recv_async()  => {
399                Ok(res.context("channel receive error")?)
400            }
401            _ = cancellation_token.cancelled() => {
402                Err(Error::other("cancelled"))
403            }
404        }
405    }
406
407    pub async fn search_for_message(
408        &self,
409        from: Option<Tipset>,
410        msg_cid: Cid,
411        look_back_limit: Option<i64>,
412        allow_replaced: Option<bool>,
413        cancellation_token: &CancellationToken,
414    ) -> Result<Option<(Tipset, Receipt)>, Error> {
415        let from = from.unwrap_or_else(|| self.heaviest_tipset());
416        let message = crate::chain::get_chain_message(self.db(), &msg_cid)
417            .map_err(|err| Error::Other(format!("failed to load message {err}")))?;
418        let maybe_message_receipt =
419            self.tipset_executed_message(&from, &message, allow_replaced.unwrap_or(true))?;
420        if let Some(r) = maybe_message_receipt {
421            Ok(Some((from, r)))
422        } else {
423            tokio::task::spawn_blocking({
424                let this = self.shallow_clone();
425                let cancellation_token = cancellation_token.clone();
426                move || {
427                    this.search_back_for_message_blocking(
428                        from,
429                        &message,
430                        look_back_limit,
431                        allow_replaced,
432                        &cancellation_token,
433                    )
434                }
435            })
436            .await?
437        }
438    }
439}
440
441#[cfg(test)]
442mod tests {
443    use super::*;
444    use crate::blocks::{
445        CachingBlockHeader, Chain4U, HeaderBuilder, RawBlockHeader, TxMeta, chain4u,
446    };
447    use crate::chain::ChainStore;
448    use crate::db::MemoryDB;
449    use crate::networks::ChainConfig;
450    use crate::shim::address::Address;
451    use crate::shim::econ::TokenAmount;
452    use crate::shim::message::Message;
453    use crate::shim::state_tree::{ActorState, StateTree, StateTreeVersion};
454    use crate::utils::db::CborStoreExt as _;
455    use fil_actors_shared::fvm_ipld_amt::Amtv0;
456    use fvm_ipld_blockstore::Blockstore;
457    use rstest::rstest;
458
459    const SENDER: Address = Address::new_id(100);
460
461    fn state_root_with_sender_nonce(db: &Arc<MemoryDB>, sequence: u64) -> Cid {
462        let mut state_tree = StateTree::new(db, StateTreeVersion::V5).unwrap();
463        state_tree
464            .set_actor(
465                &SENDER,
466                ActorState::new(
467                    Cid::default(),
468                    Cid::default(),
469                    TokenAmount::default(),
470                    sequence,
471                    None,
472                ),
473            )
474            .unwrap();
475        state_tree.flush().unwrap()
476    }
477
478    fn tx_meta(db: &impl Blockstore, message: Cid) -> Cid {
479        let bls_message_root = Amtv0::new_from_iter(db, [message]).unwrap();
480        let secp_message_root = Amtv0::new_from_iter(db, std::iter::empty::<Cid>()).unwrap();
481        db.put_cbor_default(&TxMeta {
482            bls_message_root,
483            secp_message_root,
484        })
485        .unwrap()
486    }
487
488    fn receipts_root(db: &impl Blockstore) -> Cid {
489        let receipt = fvm_shared4::receipt::Receipt {
490            exit_code: fvm_shared4::error::ExitCode::OK,
491            return_data: Default::default(),
492            gas_used: 0,
493            events_root: None,
494        };
495        Amtv0::new_from_iter(db, [receipt]).unwrap()
496    }
497
498    fn message_with_nonce(sequence: u64) -> Message {
499        Message {
500            from: SENDER,
501            to: Address::new_id(101),
502            sequence,
503            ..Default::default()
504        }
505    }
506
507    fn state_manager_with_replaced_message_at_head(db: &Arc<MemoryDB>) -> (StateManager, Cid) {
508        let message = message_with_nonce(5);
509        let msg_cid = db.put_cbor_default(&message).unwrap();
510        let replacement = Message {
511            gas_limit: 1,
512            ..message
513        };
514        let replacement_cid = db.put_cbor_default(&replacement).unwrap();
515
516        let root_before = state_root_with_sender_nonce(db, 5);
517        let root_after = state_root_with_sender_nonce(db, 6);
518        let messages = tx_meta(db, replacement_cid);
519        let receipts = receipts_root(db);
520        let c4u = Chain4U::with_blockstore(db.clone());
521        chain4u! {
522            in c4u;
523            [genesis = HeaderBuilder::new().with_timestamp(7777)]
524            -> [_e1 = HeaderBuilder::new().with_state_root(root_before)]
525            -> [_e2 = HeaderBuilder::new()
526                    .with_state_root(root_before)
527                    .with_messages(messages)]
528            -> head @ [_e3 = HeaderBuilder::new()
529                    .with_state_root(root_after)
530                    .with_message_receipts(receipts)]
531        };
532        (state_manager_with_head(db.clone(), genesis, head), msg_cid)
533    }
534
535    fn state_manager_with_head(
536        db: Arc<MemoryDB>,
537        genesis: &RawBlockHeader,
538        head: &Tipset,
539    ) -> StateManager {
540        let chain_store = ChainStore::new(
541            db,
542            Arc::new(ChainConfig::default()),
543            CachingBlockHeader::new(genesis.clone()),
544        )
545        .unwrap();
546        chain_store.set_heaviest_tipset(head.clone()).unwrap();
547        StateManager::new(chain_store).unwrap()
548    }
549
550    /// Chain where the sender's nonce is `actor_nonce` at every epoch and the
551    /// genesis state is unavailable, like state pruned by GC. Searching must
552    /// not walk into the missing state.
553    async fn search_pending(
554        actor_nonce: u64,
555        message_nonce: u64,
556    ) -> Result<Option<(Tipset, Receipt)>, Error> {
557        let db = Arc::new(MemoryDB::default());
558        let root = state_root_with_sender_nonce(&db, actor_nonce);
559        let c4u = Chain4U::with_blockstore(db.clone());
560        chain4u! {
561            in c4u;
562            [genesis = HeaderBuilder::new().with_timestamp(7777)]
563            -> [_e1 = HeaderBuilder::new().with_state_root(root)]
564            -> [_e2 = HeaderBuilder::new().with_state_root(root)]
565            -> head @ [_e3 = HeaderBuilder::new().with_state_root(root)]
566        };
567        let state_manager = state_manager_with_head(db.clone(), genesis, head);
568
569        let message = message_with_nonce(message_nonce);
570        let msg_cid = db.put_cbor_default(&message).unwrap();
571
572        state_manager
573            .search_for_message(None, msg_cid, None, Some(true), &CancellationToken::new())
574            .await
575    }
576
577    #[tokio::test]
578    async fn search_returns_none_for_message_with_future_nonce() {
579        let result = search_pending(5, 10).await.unwrap();
580        assert!(result.is_none());
581    }
582
583    #[tokio::test]
584    async fn search_returns_none_for_pending_message_at_current_nonce() {
585        let result = search_pending(5, 5).await.unwrap();
586        assert!(result.is_none());
587    }
588
589    #[tokio::test]
590    async fn search_returns_none_for_pending_message_from_fresh_sender() {
591        let result = search_pending(0, 0).await.unwrap();
592        assert!(result.is_none());
593    }
594
595    /// The sender's nonce crossed the message nonce, but a replacing message
596    /// with a different call executed instead: the searched message was never
597    /// executed, so the result is `None`, not an error.
598    #[tokio::test]
599    async fn search_returns_none_for_replaced_message() {
600        let db = Arc::new(MemoryDB::default());
601        let message = message_with_nonce(5);
602        let msg_cid = db.put_cbor_default(&message).unwrap();
603
604        let root_before = state_root_with_sender_nonce(&db, 5);
605        let root_after = state_root_with_sender_nonce(&db, 6);
606        let c4u = Chain4U::with_blockstore(db.clone());
607        chain4u! {
608            in c4u;
609            [genesis = HeaderBuilder::new().with_timestamp(7777)]
610            -> [_e1 = HeaderBuilder::new().with_state_root(root_before)]
611            -> [_e2 = HeaderBuilder::new().with_state_root(root_before)]
612            -> [_e3 = HeaderBuilder::new().with_state_root(root_after)]
613            -> head @ [_e4 = HeaderBuilder::new().with_state_root(root_after)]
614        };
615        let state_manager = state_manager_with_head(db.clone(), genesis, head);
616
617        let result = state_manager
618            .search_for_message(None, msg_cid, None, Some(true), &CancellationToken::new())
619            .await
620            .unwrap();
621        assert!(result.is_none());
622    }
623
624    #[tokio::test]
625    async fn search_finds_executed_message() {
626        let db = Arc::new(MemoryDB::default());
627        let message = message_with_nonce(5);
628        let msg_cid = db.put_cbor_default(&message).unwrap();
629
630        let root_before = state_root_with_sender_nonce(&db, 5);
631        let root_after = state_root_with_sender_nonce(&db, 6);
632        let messages = tx_meta(&db, msg_cid);
633        let receipts = receipts_root(&db);
634        let c4u = Chain4U::with_blockstore(db.clone());
635        chain4u! {
636            in c4u;
637            [genesis = HeaderBuilder::new().with_timestamp(7777)]
638            -> [_e1 = HeaderBuilder::new().with_state_root(root_before)]
639            -> [_e2 = HeaderBuilder::new()
640                    .with_state_root(root_before)
641                    .with_messages(messages)]
642            -> [_e3 = HeaderBuilder::new()
643                    .with_state_root(root_after)
644                    .with_message_receipts(receipts)]
645            -> head @ [_e4 = HeaderBuilder::new().with_state_root(root_after)]
646        };
647        let state_manager = state_manager_with_head(db.clone(), genesis, head);
648
649        let (tipset, receipt) = state_manager
650            .search_for_message(None, msg_cid, None, Some(true), &CancellationToken::new())
651            .await
652            .unwrap()
653            .expect("executed message should be found");
654        assert_eq!(tipset.epoch(), 3);
655        assert!(receipt.exit_code().is_success());
656    }
657
658    /// Searching from an explicit tipset only covers executions at or below
659    /// it, like in Lotus, even when the message executed later in the chain.
660    #[tokio::test]
661    async fn search_from_older_tipset_ignores_later_execution() {
662        let db = Arc::new(MemoryDB::default());
663        let message = message_with_nonce(5);
664        let msg_cid = db.put_cbor_default(&message).unwrap();
665
666        let root_before = state_root_with_sender_nonce(&db, 5);
667        let root_after = state_root_with_sender_nonce(&db, 6);
668        let messages = tx_meta(&db, msg_cid);
669        let receipts = receipts_root(&db);
670        let c4u = Chain4U::with_blockstore(db.clone());
671        chain4u! {
672            in c4u;
673            [genesis = HeaderBuilder::new().with_timestamp(7777)]
674            -> [_e1 = HeaderBuilder::new().with_state_root(root_before)]
675            -> from @ [_e2 = HeaderBuilder::new()
676                    .with_state_root(root_before)
677                    .with_messages(messages)]
678            -> [_e3 = HeaderBuilder::new()
679                    .with_state_root(root_after)
680                    .with_message_receipts(receipts)]
681            -> head @ [_e4 = HeaderBuilder::new().with_state_root(root_after)]
682        };
683        let state_manager = state_manager_with_head(db.clone(), genesis, head);
684
685        let result = state_manager
686            .search_for_message(
687                Some(from.clone()),
688                msg_cid,
689                None,
690                Some(true),
691                &CancellationToken::new(),
692            )
693            .await
694            .unwrap();
695        assert!(result.is_none());
696    }
697
698    /// A replacing message executed on chain. When replacements are
699    /// disallowed, `wait_for_message` must forward `allow_replaced = false`
700    /// and surface an error instead of silently returning the replacement's
701    /// receipt.
702    #[tokio::test]
703    async fn wait_for_message_rejects_replaced_when_disallowed() {
704        let db = Arc::new(MemoryDB::default());
705        let (state_manager, msg_cid) = state_manager_with_replaced_message_at_head(&db);
706
707        let result = state_manager
708            .wait_for_message(msg_cid, 0, None, Some(false), &CancellationToken::new())
709            .await;
710        let err = result.expect_err("replaced message should be rejected");
711        assert!(err.to_string().contains("different CID"), "{err}");
712    }
713
714    /// The same replacing message is accepted when replacements are allowed,
715    /// returning the receipt at the head tipset.
716    #[tokio::test]
717    async fn wait_for_message_accepts_replaced_when_allowed() {
718        let db = Arc::new(MemoryDB::default());
719        let (state_manager, msg_cid) = state_manager_with_replaced_message_at_head(&db);
720
721        let (tipset, receipt) = state_manager
722            .wait_for_message(msg_cid, 0, None, Some(true), &CancellationToken::new())
723            .await
724            .expect("replaced message should be accepted");
725        assert_eq!(tipset.epoch(), 3);
726        assert!(receipt.exit_code().is_success());
727    }
728
729    /// The message executed as of the head tipset and no confidence is
730    /// required, so `wait_for_message` returns the head hit immediately.
731    #[tokio::test]
732    async fn wait_for_message_head_hit_returns_immediately_with_zero_confidence() {
733        let db = Arc::new(MemoryDB::default());
734        let message = message_with_nonce(5);
735        let msg_cid = db.put_cbor_default(&message).unwrap();
736
737        let root_before = state_root_with_sender_nonce(&db, 5);
738        let root_after = state_root_with_sender_nonce(&db, 6);
739        let messages = tx_meta(&db, msg_cid);
740        let receipts = receipts_root(&db);
741        let c4u = Chain4U::with_blockstore(db.clone());
742        chain4u! {
743            in c4u;
744            [genesis = HeaderBuilder::new().with_timestamp(7777)]
745            -> [_e1 = HeaderBuilder::new().with_state_root(root_before)]
746            -> [_e2 = HeaderBuilder::new()
747                    .with_state_root(root_before)
748                    .with_messages(messages)]
749            -> head @ [_e3 = HeaderBuilder::new()
750                    .with_state_root(root_after)
751                    .with_message_receipts(receipts)]
752        };
753        let state_manager = state_manager_with_head(db.clone(), genesis, head);
754
755        let (tipset, receipt) = state_manager
756            .wait_for_message(msg_cid, 0, None, Some(true), &CancellationToken::new())
757            .await
758            .expect("head hit should return immediately with zero confidence");
759        assert_eq!(tipset.epoch(), 3);
760        assert!(receipt.exit_code().is_success());
761    }
762
763    /// The message executed as of the head tipset, but a positive confidence is
764    /// requested and the head never advances, so the head hit must not be
765    /// returned early; the call times out instead.
766    #[tokio::test]
767    async fn wait_for_message_head_hit_waits_for_confidence() {
768        let db = Arc::new(MemoryDB::default());
769        let message = message_with_nonce(5);
770        let msg_cid = db.put_cbor_default(&message).unwrap();
771
772        let root_before = state_root_with_sender_nonce(&db, 5);
773        let root_after = state_root_with_sender_nonce(&db, 6);
774        let messages = tx_meta(&db, msg_cid);
775        let receipts = receipts_root(&db);
776        let c4u = Chain4U::with_blockstore(db.clone());
777        chain4u! {
778            in c4u;
779            [genesis = HeaderBuilder::new().with_timestamp(7777)]
780            -> [_e1 = HeaderBuilder::new().with_state_root(root_before)]
781            -> [_e2 = HeaderBuilder::new()
782                    .with_state_root(root_before)
783                    .with_messages(messages)]
784            -> head @ [_e3 = HeaderBuilder::new()
785                    .with_state_root(root_after)
786                    .with_message_receipts(receipts)]
787        };
788        let state_manager = state_manager_with_head(db.clone(), genesis, head);
789
790        let result = state_manager
791            .wait_for_message_with_timeout(msg_cid, 2, None, Some(true), Duration::from_millis(300))
792            .await;
793        assert!(result.is_err());
794    }
795
796    /// The message executed as of the head tipset with a positive confidence.
797    /// Once the head advances by `confidence` epochs without a revert, the head
798    /// hit is returned.
799    #[tokio::test]
800    async fn wait_for_message_head_hit_returns_after_confidence_reached() {
801        let db = Arc::new(MemoryDB::default());
802        let message = message_with_nonce(5);
803        let msg_cid = db.put_cbor_default(&message).unwrap();
804
805        let root_before = state_root_with_sender_nonce(&db, 5);
806        let root_after = state_root_with_sender_nonce(&db, 6);
807        let messages = tx_meta(&db, msg_cid);
808        let receipts = receipts_root(&db);
809        let c4u = Chain4U::with_blockstore(db.clone());
810        chain4u! {
811            in c4u;
812            [genesis = HeaderBuilder::new().with_timestamp(7777)]
813            -> [_e1 = HeaderBuilder::new().with_state_root(root_before)]
814            -> [_e2 = HeaderBuilder::new()
815                    .with_state_root(root_before)
816                    .with_messages(messages)]
817            -> exec @ [_e3 = HeaderBuilder::new()
818                    .with_state_root(root_after)
819                    .with_message_receipts(receipts)]
820            -> next4 @ [_e4 = HeaderBuilder::new().with_state_root(root_after)]
821            -> next5 @ [_e5 = HeaderBuilder::new().with_state_root(root_after)]
822        };
823        let state_manager = state_manager_with_head(db.clone(), genesis, exec);
824
825        let token = CancellationToken::new();
826        let mut wait =
827            Box::pin(state_manager.wait_for_message(msg_cid, 2, None, Some(true), &token));
828
829        // Poll while the head is at epoch 3 to seed the candidate and subscribe,
830        // then advance to epoch 4. Confidence 2 is unmet in both cases, so the
831        // future must stay pending.
832        for advance_to in [None, Some(next4)] {
833            if let Some(ts) = advance_to {
834                state_manager
835                    .chain_store()
836                    .set_heaviest_tipset(ts.clone())
837                    .unwrap();
838            }
839            assert!(
840                tokio::time::timeout(Duration::from_millis(100), &mut wait)
841                    .await
842                    .is_err(),
843                "confidence 2 must not be reached before epoch 5"
844            );
845        }
846
847        // Epoch 5 reaches confidence 2; the future resolves.
848        state_manager
849            .chain_store()
850            .set_heaviest_tipset(next5.clone())
851            .unwrap();
852        let (tipset, receipt) = tokio::time::timeout(Duration::from_secs(5), &mut wait)
853            .await
854            .expect("should resolve once confidence is reached")
855            .expect("head hit should be returned after confidence reached");
856        assert_eq!(tipset.epoch(), 3);
857        assert!(receipt.exit_code().is_success());
858    }
859
860    #[rstest]
861    #[case::zero_confidence(10, 10, 0, true)]
862    #[case::exact_confidence(15, 10, 5, true)]
863    #[case::negative_candidate(0, -1, 1, false)]
864    #[case::insufficient_confidence(14, 10, 5, false)]
865    #[case::candidate_above_current(9, 10, 0, false)]
866    fn confidence_reached_cases(
867        #[case] current: ChainEpoch,
868        #[case] candidate: ChainEpoch,
869        #[case] confidence: i64,
870        #[case] expected: bool,
871    ) {
872        assert_eq!(confidence_reached(current, candidate, confidence), expected);
873    }
874
875    /// `wait_for_message` rejects a confidence above chain finality.
876    #[tokio::test]
877    async fn wait_for_message_rejects_confidence_above_maximum() {
878        let db = Arc::new(MemoryDB::default());
879        let (state_manager, msg_cid) = state_manager_with_replaced_message_at_head(&db);
880
881        let result = state_manager
882            .wait_for_message(
883                msg_cid,
884                MAX_MESSAGE_CONFIDENCE + 1,
885                None,
886                Some(true),
887                &CancellationToken::new(),
888            )
889            .await;
890        let err = result.expect_err("confidence above maximum should be rejected");
891        assert!(
892            err.to_string()
893                .contains("message confidence exceeds maximum"),
894            "{err}"
895        );
896    }
897}