1use 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_util::sync::CancellationToken;
12
13const MAX_MESSAGE_CONFIDENCE: ChainEpoch = crate::shim::policy::policy_constants::CHAIN_FINALITY;
15
16pub const FAILED_TO_LOAD_MESSAGE: &str = "failed to load message";
19
20fn confidence_reached(current: ChainEpoch, candidate: ChainEpoch, confidence: i64) -> bool {
22 candidate >= 0 && current >= candidate && (current - candidate) >= confidence
23}
24
25impl StateManager {
26 fn tipset_executed_message(
29 &self,
30 tipset: &Tipset,
31 message: &ChainMessage,
32 allow_replaced: bool,
33 ) -> Result<Option<Receipt>, Error> {
34 if tipset.epoch() == 0 {
35 return Ok(None);
36 }
37 let message_from_address = message.from();
38 let message_sequence = message.sequence();
39 let pts = self
41 .chain_index()
42 .load_required_tipset(tipset.parents())
43 .map_err(|err| Error::Other(format!("Failed to load tipset: {err}")))?;
44 let messages = self
45 .cs
46 .messages_for_tipset(&pts)
47 .map_err(|err| Error::Other(format!("Failed to load messages for tipset: {err}")))?;
48 messages
49 .iter()
50 .enumerate()
51 .rev()
53 .filter(|(_, s)| {
54 s.sequence() == message_sequence
55 && s.from() == message_from_address
56 && s.equal_call(message)
57 })
58 .map(|(index, m)| {
59 if !allow_replaced && message.cid() != m.cid(){
63 Err(Error::Other(format!(
64 "found message with equal nonce and call params but different CID. wanted {}, found: {}, nonce: {}, from: {}",
65 message.cid(),
66 m.cid(),
67 message.sequence(),
68 message.from(),
69 )))
70 } else {
71 let block_header = tipset.block_headers().first();
72 crate::chain::get_parent_receipt(
73 self.db(),
74 block_header,
75 index,
76 )
77 .map_err(|err| Error::Other(format!("Failed to get parent receipt (message_receipts={}, index={index}, error={err})", block_header.message_receipts)))
78 }
79 })
80 .next()
81 .unwrap_or(Ok(None))
82 }
83
84 fn check_search_blocking(
85 &self,
86 mut current: Tipset,
87 message: &ChainMessage,
88 lookback_max_epoch: ChainEpoch,
89 allow_replaced: bool,
90 cancellation_token: &CancellationToken,
91 ) -> Result<Option<(Tipset, Receipt)>, Error> {
92 let message_from_address = message.from();
93 let message_sequence = message.sequence();
94 let current_actor_state = self
95 .get_required_actor(&message_from_address, *current.parent_state())
96 .map_err(Error::state)?;
97 if current_actor_state.sequence <= message_sequence {
101 return Ok(None);
102 }
103 let message_from_id = self.lookup_required_id(&message_from_address, ¤t)?;
104
105 while !cancellation_token.is_cancelled() && current.epoch() >= lookback_max_epoch {
106 let parent_tipset = self
107 .chain_index()
108 .load_required_tipset(current.parents())
109 .map_err(|err| {
110 Error::Other(format!(
111 "failed to load tipset during msg wait searchback: {err:}"
112 ))
113 })?;
114
115 let parent_actor_state = self
116 .get_actor(&message_from_id, *parent_tipset.parent_state())
117 .map_err(|e| Error::State(e.to_string()))?;
118
119 match parent_actor_state {
120 Some(state) if state.sequence > message_sequence => current = parent_tipset,
123 _ => {
128 return Ok(self
129 .tipset_executed_message(¤t, message, allow_replaced)?
130 .map(|receipt| (current, receipt)));
131 }
132 }
133 }
134
135 Ok(None)
136 }
137
138 fn search_back_for_message_blocking(
140 &self,
141 current: Tipset,
142 message: &ChainMessage,
143 look_back_limit: Option<ChainEpoch>,
144 allow_replaced: Option<bool>,
145 cancellation_token: &CancellationToken,
146 ) -> Result<Option<(Tipset, Receipt)>, Error> {
147 let current_epoch = current.epoch();
148 let allow_replaced = allow_replaced.unwrap_or(true);
149
150 let Some(max_lookback_epoch_inclusive) =
151 Self::max_lookback_epoch_inclusive(current_epoch, look_back_limit)
152 else {
153 return Ok(None);
154 };
155
156 self.check_search_blocking(
157 current,
158 message,
159 max_lookback_epoch_inclusive,
160 allow_replaced,
161 cancellation_token,
162 )
163 }
164
165 pub fn max_lookback_epoch_inclusive(
167 current_epoch: ChainEpoch,
168 look_back_limit: Option<ChainEpoch>,
169 ) -> Option<ChainEpoch> {
170 match look_back_limit {
171 Some(0) => None,
173 Some(limit) if limit > 0 => Some((current_epoch - limit + 1).max(0)),
177 _ => Some(0),
179 }
180 }
181
182 pub fn get_receipt_blocking(
184 &self,
185 tipset: Tipset,
186 msg: Cid,
187 cancellation_token: &CancellationToken,
188 ) -> Result<Receipt, Error> {
189 let m = crate::chain::get_chain_message(self.db(), &msg)
190 .map_err(|e| Error::Other(e.to_string()))?;
191 let message_receipt = self.tipset_executed_message(&tipset, &m, true)?;
192 if let Some(receipt) = message_receipt {
193 return Ok(receipt);
194 }
195
196 let maybe_tuple =
197 self.search_back_for_message_blocking(tipset, &m, None, None, cancellation_token)?;
198 let message_receipt = maybe_tuple
199 .ok_or_else(|| {
200 Error::Other("Could not get receipt from search back message".to_string())
201 })?
202 .1;
203 Ok(message_receipt)
204 }
205
206 pub async fn wait_for_message_with_timeout(
207 &self,
208 msg_cid: Cid,
209 confidence: i64,
210 look_back_limit: Option<ChainEpoch>,
211 allow_replaced: Option<bool>,
212 timeout: Duration,
213 ) -> Result<(Tipset, Receipt), Error> {
214 let cancellation_token = CancellationToken::new();
215 let _cancellation_token_drop_guard = cancellation_token.drop_guard_ref();
216 tokio::time::timeout(
217 timeout,
218 self.wait_for_message(
219 msg_cid,
220 confidence,
221 look_back_limit,
222 allow_replaced,
223 &cancellation_token,
224 ),
225 )
226 .await
227 .map_err(|_| {
228 Error::other(format!(
229 "wait_for_message timed out after {}",
230 humantime::format_duration(timeout)
231 ))
232 })?
233 }
234
235 pub async fn wait_for_message(
241 &self,
242 msg_cid: Cid,
243 confidence: i64,
244 look_back_limit: Option<ChainEpoch>,
245 allow_replaced: Option<bool>,
246 cancellation_token: &CancellationToken,
247 ) -> Result<(Tipset, Receipt), Error> {
248 if confidence > MAX_MESSAGE_CONFIDENCE {
249 return Err(Error::other(format!(
250 "message confidence exceeds maximum: {confidence} > {MAX_MESSAGE_CONFIDENCE}"
251 )));
252 }
253 let message = Arc::new(
254 crate::chain::get_chain_message(self.db(), &msg_cid)
255 .map_err(|err| Error::Other(format!("{FAILED_TO_LOAD_MESSAGE} {err}")))?,
256 );
257 let head_changes_rx = self.cs.subscribe_head_changes();
262 let current_ts = self.heaviest_tipset();
263 let maybe_message_receipt =
264 self.tipset_executed_message(¤t_ts, &message, allow_replaced.unwrap_or(true))?;
265 let initial_candidate = match maybe_message_receipt {
269 Some(receipt) if confidence == 0 => return Ok((current_ts, receipt)),
270 Some(receipt) => Some((current_ts.shallow_clone(), receipt)),
271 None => None,
272 };
273
274 let (search_back_tx, search_back_rx) = flume::bounded(1);
276 let search_back_candidate: Arc<OnceLock<(Tipset, Receipt)>> = Default::default();
277 let reverted: Arc<RwLock<HashSet<TipsetKey>>> = Arc::new(RwLock::new(HashSet::default()));
278 tokio::task::spawn_blocking({
280 let sm = self.shallow_clone();
281 let message = message.shallow_clone();
282 let search_back_tx = search_back_tx.clone();
284 let search_back_candidate = search_back_candidate.shallow_clone();
285 let reverted = reverted.shallow_clone();
286 let cancellation_token = cancellation_token.clone();
287 move || {
288 if let Ok(Some((ts, receipt))) = sm
289 .search_back_for_message_blocking(
290 current_ts,
291 &message,
292 look_back_limit,
293 allow_replaced,
294 &cancellation_token,
295 )
296 .inspect_err(|e| {
297 tracing::warn!("failed to search back for message: {e}");
298 })
299 && !reverted.read().contains(ts.key())
300 {
301 if confidence_reached(sm.heaviest_tipset().epoch(), ts.epoch(), confidence) {
302 _ = search_back_tx.send((ts, receipt)).inspect_err(|e| {
303 tracing::warn!("failed to send to search_back_tx: {e}");
304 });
305 } else {
306 _ = search_back_candidate.set((ts, receipt)).inspect_err(|_| {
307 tracing::warn!("failed to send to set search_back_candidate");
308 });
309 }
310 }
311 }
312 });
313
314 let subscriber_poll = tokio::task::spawn({
316 let cancellation_token = cancellation_token.clone();
317 let search_back_candidate = search_back_candidate.shallow_clone();
318 let reverted = reverted.shallow_clone();
319 let sm = self.shallow_clone();
320 async move {
321 let mut candidate: Option<(Tipset, Receipt)> = initial_candidate;
322 while !cancellation_token.is_cancelled() {
323 let Ok(head_changes) = head_changes_rx.recv_async().await else {
324 break;
325 };
326 for reverted_ts in head_changes.reverts {
327 reverted.write().insert(reverted_ts.key().clone());
328
329 if candidate
330 .as_ref()
331 .is_some_and(|(ts, _)| ts.key() == reverted_ts.key())
332 {
333 candidate = None;
334 }
335 }
336 for applied_ts in head_changes.applies {
337 reverted.write().remove(applied_ts.key());
338
339 if let Some((candidate_ts, candidate_receipt)) = search_back_candidate.get()
341 && confidence_reached(
342 applied_ts.epoch(),
343 candidate_ts.epoch(),
344 confidence,
345 )
346 && !reverted.read().contains(candidate_ts.key())
347 {
348 return Ok((candidate_ts.shallow_clone(), candidate_receipt.clone()));
349 }
350
351 if let Some((candidate_ts, _)) = &candidate
353 && confidence_reached(
354 applied_ts.epoch(),
355 candidate_ts.epoch(),
356 confidence,
357 )
358 && let Some(candidate) = candidate
359 {
360 return Ok(candidate);
361 }
362
363 let maybe_receipt = sm.tipset_executed_message(
364 &applied_ts,
365 &message,
366 allow_replaced.unwrap_or(true),
367 )?;
368 if let Some(receipt) = maybe_receipt {
369 if confidence == 0 {
370 return Ok((applied_ts, receipt));
372 } else {
373 candidate = Some((applied_ts, receipt));
375 }
376 }
377 }
378 }
379 Err(Error::other("cancelled"))
380 }
381 });
382
383 tokio::select! {
385 res = subscriber_poll => {
386 res.context("tokio join error")?
387 }
388 res = search_back_rx.recv_async() => {
389 Ok(res.context("channel receive error")?)
390 }
391 _ = cancellation_token.cancelled() => {
392 Err(Error::other("cancelled"))
393 }
394 }
395 }
396
397 pub async fn search_for_message(
398 &self,
399 from: Option<Tipset>,
400 msg_cid: Cid,
401 look_back_limit: Option<i64>,
402 allow_replaced: Option<bool>,
403 cancellation_token: &CancellationToken,
404 ) -> Result<Option<(Tipset, Receipt)>, Error> {
405 let from = from.unwrap_or_else(|| self.heaviest_tipset());
406 let message = crate::chain::get_chain_message(self.db(), &msg_cid)
407 .map_err(|err| Error::Other(format!("{FAILED_TO_LOAD_MESSAGE} {err}")))?;
408 let maybe_message_receipt =
409 self.tipset_executed_message(&from, &message, allow_replaced.unwrap_or(true))?;
410 if let Some(r) = maybe_message_receipt {
411 Ok(Some((from, r)))
412 } else {
413 tokio::task::spawn_blocking({
414 let this = self.shallow_clone();
415 let cancellation_token = cancellation_token.clone();
416 move || {
417 this.search_back_for_message_blocking(
418 from,
419 &message,
420 look_back_limit,
421 allow_replaced,
422 &cancellation_token,
423 )
424 }
425 })
426 .await?
427 }
428 }
429}
430
431#[cfg(test)]
432mod tests {
433 use super::*;
434 use crate::blocks::{
435 CachingBlockHeader, Chain4U, HeaderBuilder, RawBlockHeader, TxMeta, chain4u,
436 };
437 use crate::chain::ChainStore;
438 use crate::db::MemoryDB;
439 use crate::networks::ChainConfig;
440 use crate::shim::address::Address;
441 use crate::shim::econ::TokenAmount;
442 use crate::shim::message::Message;
443 use crate::shim::state_tree::{ActorState, StateTree, StateTreeVersion};
444 use crate::utils::db::CborStoreExt as _;
445 use crate::utils::rand::random_cid;
446 use fil_actors_shared::fvm_ipld_amt::Amtv0;
447 use fvm_ipld_blockstore::Blockstore;
448 use quickcheck_macros::quickcheck;
449 use rstest::rstest;
450
451 const SENDER: Address = Address::new_id(100);
452
453 #[rstest]
454 #[case(1000, Some(0), None)]
455 #[case(1000, Some(5), Some(996))]
456 #[case(1000, Some(2000), Some(0))]
457 #[case(1000, Some(i64::MAX), Some(0))]
458 #[case(1000, Some(-1), Some(0))]
459 #[case(1000, None, Some(0))]
460 fn max_lookback_epoch_inclusive_examples(
461 #[case] current_epoch: ChainEpoch,
462 #[case] look_back_limit: Option<ChainEpoch>,
463 #[case] expected: Option<ChainEpoch>,
464 ) {
465 assert_eq!(
466 StateManager::max_lookback_epoch_inclusive(current_epoch, look_back_limit),
467 expected
468 );
469 }
470
471 #[quickcheck]
472 fn max_lookback_epoch_inclusive_no_panic(
473 current_epoch: ChainEpoch,
474 look_back_limit: Option<ChainEpoch>,
475 ) -> bool {
476 let current_epoch = current_epoch.max(0);
477 match StateManager::max_lookback_epoch_inclusive(current_epoch, look_back_limit) {
478 Some(min_epoch) => min_epoch >= 0 && min_epoch <= current_epoch.max(0),
479 None => look_back_limit == Some(0),
480 }
481 }
482
483 fn state_root_with_sender_nonce(db: &Arc<MemoryDB>, sequence: u64) -> Cid {
484 let mut state_tree = StateTree::new(db, StateTreeVersion::V5).unwrap();
485 state_tree
486 .set_actor(
487 &SENDER,
488 ActorState::new(
489 Cid::default(),
490 Cid::default(),
491 TokenAmount::default(),
492 sequence,
493 None,
494 ),
495 )
496 .unwrap();
497 state_tree.flush().unwrap()
498 }
499
500 fn tx_meta(db: &impl Blockstore, message: Cid) -> Cid {
501 let bls_message_root = Amtv0::new_from_iter(db, [message]).unwrap();
502 let secp_message_root = Amtv0::new_from_iter(db, std::iter::empty::<Cid>()).unwrap();
503 db.put_cbor_default(&TxMeta {
504 bls_message_root,
505 secp_message_root,
506 })
507 .unwrap()
508 }
509
510 fn receipts_root(db: &impl Blockstore) -> Cid {
511 let receipt = fvm_shared4::receipt::Receipt {
512 exit_code: fvm_shared4::error::ExitCode::OK,
513 return_data: Default::default(),
514 gas_used: 0,
515 events_root: None,
516 };
517 Amtv0::new_from_iter(db, [receipt]).unwrap()
518 }
519
520 fn message_with_nonce(sequence: u64) -> Message {
521 Message {
522 from: SENDER,
523 to: Address::new_id(101),
524 sequence,
525 ..Default::default()
526 }
527 }
528
529 fn state_manager_with_replaced_message_at_head(db: &Arc<MemoryDB>) -> (StateManager, Cid) {
530 let message = message_with_nonce(5);
531 let msg_cid = db.put_cbor_default(&message).unwrap();
532 let replacement = Message {
533 gas_limit: 1,
534 ..message
535 };
536 let replacement_cid = db.put_cbor_default(&replacement).unwrap();
537
538 let root_before = state_root_with_sender_nonce(db, 5);
539 let root_after = state_root_with_sender_nonce(db, 6);
540 let messages = tx_meta(db, replacement_cid);
541 let receipts = receipts_root(db);
542 let c4u = Chain4U::with_blockstore(db.clone());
543 chain4u! {
544 in c4u;
545 [genesis = HeaderBuilder::new().with_timestamp(7777)]
546 -> [_e1 = HeaderBuilder::new().with_state_root(root_before)]
547 -> [_e2 = HeaderBuilder::new()
548 .with_state_root(root_before)
549 .with_messages(messages)]
550 -> head @ [_e3 = HeaderBuilder::new()
551 .with_state_root(root_after)
552 .with_message_receipts(receipts)]
553 };
554 (state_manager_with_head(db.clone(), genesis, head), msg_cid)
555 }
556
557 fn state_manager_with_head(
558 db: Arc<MemoryDB>,
559 genesis: &RawBlockHeader,
560 head: &Tipset,
561 ) -> StateManager {
562 let chain_store = ChainStore::new(
563 db,
564 Arc::new(ChainConfig::default()),
565 CachingBlockHeader::new(genesis.clone()),
566 )
567 .unwrap();
568 chain_store.set_heaviest_tipset(head.clone()).unwrap();
569 StateManager::new(chain_store).unwrap()
570 }
571
572 async fn search_pending(
576 actor_nonce: u64,
577 message_nonce: u64,
578 ) -> Result<Option<(Tipset, Receipt)>, Error> {
579 let db = Arc::new(MemoryDB::default());
580 let root = state_root_with_sender_nonce(&db, actor_nonce);
581 let c4u = Chain4U::with_blockstore(db.clone());
582 chain4u! {
583 in c4u;
584 [genesis = HeaderBuilder::new().with_timestamp(7777)]
585 -> [_e1 = HeaderBuilder::new().with_state_root(root)]
586 -> [_e2 = HeaderBuilder::new().with_state_root(root)]
587 -> head @ [_e3 = HeaderBuilder::new().with_state_root(root)]
588 };
589 let state_manager = state_manager_with_head(db.clone(), genesis, head);
590
591 let message = message_with_nonce(message_nonce);
592 let msg_cid = db.put_cbor_default(&message).unwrap();
593
594 state_manager
595 .search_for_message(None, msg_cid, None, Some(true), &CancellationToken::new())
596 .await
597 }
598
599 #[tokio::test]
600 async fn search_returns_none_for_message_with_future_nonce() {
601 let result = search_pending(5, 10).await.unwrap();
602 assert!(result.is_none());
603 }
604
605 #[tokio::test]
606 async fn search_returns_none_for_pending_message_at_current_nonce() {
607 let result = search_pending(5, 5).await.unwrap();
608 assert!(result.is_none());
609 }
610
611 #[tokio::test]
612 async fn search_returns_none_for_pending_message_from_fresh_sender() {
613 let result = search_pending(0, 0).await.unwrap();
614 assert!(result.is_none());
615 }
616
617 #[tokio::test]
621 async fn search_returns_none_for_replaced_message() {
622 let db = Arc::new(MemoryDB::default());
623 let message = message_with_nonce(5);
624 let msg_cid = db.put_cbor_default(&message).unwrap();
625
626 let root_before = state_root_with_sender_nonce(&db, 5);
627 let root_after = state_root_with_sender_nonce(&db, 6);
628 let c4u = Chain4U::with_blockstore(db.clone());
629 chain4u! {
630 in c4u;
631 [genesis = HeaderBuilder::new().with_timestamp(7777)]
632 -> [_e1 = HeaderBuilder::new().with_state_root(root_before)]
633 -> [_e2 = HeaderBuilder::new().with_state_root(root_before)]
634 -> [_e3 = HeaderBuilder::new().with_state_root(root_after)]
635 -> head @ [_e4 = HeaderBuilder::new().with_state_root(root_after)]
636 };
637 let state_manager = state_manager_with_head(db.clone(), genesis, head);
638
639 let result = state_manager
640 .search_for_message(None, msg_cid, None, Some(true), &CancellationToken::new())
641 .await
642 .unwrap();
643 assert!(result.is_none());
644 }
645
646 #[tokio::test]
647 async fn search_reports_a_message_missing_from_the_blockstore() {
648 let db = Arc::new(MemoryDB::default());
649 let c4u = Chain4U::with_blockstore(db.clone());
650 chain4u! {
651 in c4u;
652 [genesis = HeaderBuilder::new().with_timestamp(7777)]
653 -> head @ [_e1 = HeaderBuilder::new()]
654 };
655 let state_manager = state_manager_with_head(db.clone(), genesis, head);
656 let unknown = random_cid();
657
658 let searched = state_manager
659 .search_for_message(None, unknown, None, Some(true), &CancellationToken::new())
660 .await
661 .unwrap_err();
662 assert!(
663 searched.to_string().contains(FAILED_TO_LOAD_MESSAGE),
664 "{searched}"
665 );
666
667 let waited = state_manager
668 .wait_for_message(unknown, 0, None, Some(true), &CancellationToken::new())
669 .await
670 .unwrap_err();
671 assert!(
672 waited.to_string().contains(FAILED_TO_LOAD_MESSAGE),
673 "{waited}"
674 );
675 }
676
677 #[tokio::test]
678 async fn search_finds_executed_message() {
679 let db = Arc::new(MemoryDB::default());
680 let message = message_with_nonce(5);
681 let msg_cid = db.put_cbor_default(&message).unwrap();
682
683 let root_before = state_root_with_sender_nonce(&db, 5);
684 let root_after = state_root_with_sender_nonce(&db, 6);
685 let messages = tx_meta(&db, msg_cid);
686 let receipts = receipts_root(&db);
687 let c4u = Chain4U::with_blockstore(db.clone());
688 chain4u! {
689 in c4u;
690 [genesis = HeaderBuilder::new().with_timestamp(7777)]
691 -> [_e1 = HeaderBuilder::new().with_state_root(root_before)]
692 -> [_e2 = HeaderBuilder::new()
693 .with_state_root(root_before)
694 .with_messages(messages)]
695 -> [_e3 = HeaderBuilder::new()
696 .with_state_root(root_after)
697 .with_message_receipts(receipts)]
698 -> head @ [_e4 = HeaderBuilder::new().with_state_root(root_after)]
699 };
700 let state_manager = state_manager_with_head(db.clone(), genesis, head);
701
702 let (tipset, receipt) = state_manager
703 .search_for_message(None, msg_cid, None, Some(true), &CancellationToken::new())
704 .await
705 .unwrap()
706 .expect("executed message should be found");
707 assert_eq!(tipset.epoch(), 3);
708 assert!(receipt.exit_code().is_success());
709 }
710
711 #[tokio::test]
714 async fn search_from_older_tipset_ignores_later_execution() {
715 let db = Arc::new(MemoryDB::default());
716 let message = message_with_nonce(5);
717 let msg_cid = db.put_cbor_default(&message).unwrap();
718
719 let root_before = state_root_with_sender_nonce(&db, 5);
720 let root_after = state_root_with_sender_nonce(&db, 6);
721 let messages = tx_meta(&db, msg_cid);
722 let receipts = receipts_root(&db);
723 let c4u = Chain4U::with_blockstore(db.clone());
724 chain4u! {
725 in c4u;
726 [genesis = HeaderBuilder::new().with_timestamp(7777)]
727 -> [_e1 = HeaderBuilder::new().with_state_root(root_before)]
728 -> from @ [_e2 = HeaderBuilder::new()
729 .with_state_root(root_before)
730 .with_messages(messages)]
731 -> [_e3 = HeaderBuilder::new()
732 .with_state_root(root_after)
733 .with_message_receipts(receipts)]
734 -> head @ [_e4 = HeaderBuilder::new().with_state_root(root_after)]
735 };
736 let state_manager = state_manager_with_head(db.clone(), genesis, head);
737
738 let result = state_manager
739 .search_for_message(
740 Some(from.clone()),
741 msg_cid,
742 None,
743 Some(true),
744 &CancellationToken::new(),
745 )
746 .await
747 .unwrap();
748 assert!(result.is_none());
749 }
750
751 #[tokio::test]
756 async fn wait_for_message_rejects_replaced_when_disallowed() {
757 let db = Arc::new(MemoryDB::default());
758 let (state_manager, msg_cid) = state_manager_with_replaced_message_at_head(&db);
759
760 let result = state_manager
761 .wait_for_message(msg_cid, 0, None, Some(false), &CancellationToken::new())
762 .await;
763 let err = result.expect_err("replaced message should be rejected");
764 assert!(err.to_string().contains("different CID"), "{err}");
765 }
766
767 #[tokio::test]
770 async fn wait_for_message_accepts_replaced_when_allowed() {
771 let db = Arc::new(MemoryDB::default());
772 let (state_manager, msg_cid) = state_manager_with_replaced_message_at_head(&db);
773
774 let (tipset, receipt) = state_manager
775 .wait_for_message(msg_cid, 0, None, Some(true), &CancellationToken::new())
776 .await
777 .expect("replaced message should be accepted");
778 assert_eq!(tipset.epoch(), 3);
779 assert!(receipt.exit_code().is_success());
780 }
781
782 #[tokio::test]
785 async fn wait_for_message_head_hit_returns_immediately_with_zero_confidence() {
786 let db = Arc::new(MemoryDB::default());
787 let message = message_with_nonce(5);
788 let msg_cid = db.put_cbor_default(&message).unwrap();
789
790 let root_before = state_root_with_sender_nonce(&db, 5);
791 let root_after = state_root_with_sender_nonce(&db, 6);
792 let messages = tx_meta(&db, msg_cid);
793 let receipts = receipts_root(&db);
794 let c4u = Chain4U::with_blockstore(db.clone());
795 chain4u! {
796 in c4u;
797 [genesis = HeaderBuilder::new().with_timestamp(7777)]
798 -> [_e1 = HeaderBuilder::new().with_state_root(root_before)]
799 -> [_e2 = HeaderBuilder::new()
800 .with_state_root(root_before)
801 .with_messages(messages)]
802 -> head @ [_e3 = HeaderBuilder::new()
803 .with_state_root(root_after)
804 .with_message_receipts(receipts)]
805 };
806 let state_manager = state_manager_with_head(db.clone(), genesis, head);
807
808 let (tipset, receipt) = state_manager
809 .wait_for_message(msg_cid, 0, None, Some(true), &CancellationToken::new())
810 .await
811 .expect("head hit should return immediately with zero confidence");
812 assert_eq!(tipset.epoch(), 3);
813 assert!(receipt.exit_code().is_success());
814 }
815
816 #[tokio::test]
820 async fn wait_for_message_head_hit_waits_for_confidence() {
821 let db = Arc::new(MemoryDB::default());
822 let message = message_with_nonce(5);
823 let msg_cid = db.put_cbor_default(&message).unwrap();
824
825 let root_before = state_root_with_sender_nonce(&db, 5);
826 let root_after = state_root_with_sender_nonce(&db, 6);
827 let messages = tx_meta(&db, msg_cid);
828 let receipts = receipts_root(&db);
829 let c4u = Chain4U::with_blockstore(db.clone());
830 chain4u! {
831 in c4u;
832 [genesis = HeaderBuilder::new().with_timestamp(7777)]
833 -> [_e1 = HeaderBuilder::new().with_state_root(root_before)]
834 -> [_e2 = HeaderBuilder::new()
835 .with_state_root(root_before)
836 .with_messages(messages)]
837 -> head @ [_e3 = HeaderBuilder::new()
838 .with_state_root(root_after)
839 .with_message_receipts(receipts)]
840 };
841 let state_manager = state_manager_with_head(db.clone(), genesis, head);
842
843 let result = state_manager
844 .wait_for_message_with_timeout(msg_cid, 2, None, Some(true), Duration::from_millis(300))
845 .await;
846 assert!(result.is_err());
847 }
848
849 #[tokio::test]
853 async fn wait_for_message_head_hit_returns_after_confidence_reached() {
854 let db = Arc::new(MemoryDB::default());
855 let message = message_with_nonce(5);
856 let msg_cid = db.put_cbor_default(&message).unwrap();
857
858 let root_before = state_root_with_sender_nonce(&db, 5);
859 let root_after = state_root_with_sender_nonce(&db, 6);
860 let messages = tx_meta(&db, msg_cid);
861 let receipts = receipts_root(&db);
862 let c4u = Chain4U::with_blockstore(db.clone());
863 chain4u! {
864 in c4u;
865 [genesis = HeaderBuilder::new().with_timestamp(7777)]
866 -> [_e1 = HeaderBuilder::new().with_state_root(root_before)]
867 -> [_e2 = HeaderBuilder::new()
868 .with_state_root(root_before)
869 .with_messages(messages)]
870 -> exec @ [_e3 = HeaderBuilder::new()
871 .with_state_root(root_after)
872 .with_message_receipts(receipts)]
873 -> next4 @ [_e4 = HeaderBuilder::new().with_state_root(root_after)]
874 -> next5 @ [_e5 = HeaderBuilder::new().with_state_root(root_after)]
875 };
876 let state_manager = state_manager_with_head(db.clone(), genesis, exec);
877
878 let token = CancellationToken::new();
879 let mut wait =
880 Box::pin(state_manager.wait_for_message(msg_cid, 2, None, Some(true), &token));
881
882 for advance_to in [None, Some(next4)] {
886 if let Some(ts) = advance_to {
887 state_manager
888 .chain_store()
889 .set_heaviest_tipset(ts.clone())
890 .unwrap();
891 }
892 assert!(
893 tokio::time::timeout(Duration::from_millis(100), &mut wait)
894 .await
895 .is_err(),
896 "confidence 2 must not be reached before epoch 5"
897 );
898 }
899
900 state_manager
902 .chain_store()
903 .set_heaviest_tipset(next5.clone())
904 .unwrap();
905 let (tipset, receipt) = tokio::time::timeout(Duration::from_secs(5), &mut wait)
906 .await
907 .expect("should resolve once confidence is reached")
908 .expect("head hit should be returned after confidence reached");
909 assert_eq!(tipset.epoch(), 3);
910 assert!(receipt.exit_code().is_success());
911 }
912
913 #[tokio::test]
916 async fn wait_for_message_reverted_candidate_is_not_returned() {
917 let db = Arc::new(MemoryDB::default());
918 let message = message_with_nonce(5);
919 let msg_cid = db.put_cbor_default(&message).unwrap();
920
921 let root_before = state_root_with_sender_nonce(&db, 5);
922 let root_after = state_root_with_sender_nonce(&db, 6);
923 let messages = tx_meta(&db, msg_cid);
924 let receipts = receipts_root(&db);
925 let c4u = Chain4U::with_blockstore(db.clone());
926 chain4u! {
927 in c4u;
928 [genesis = HeaderBuilder::new().with_timestamp(7777)]
929 -> [_e1 = HeaderBuilder::new().with_state_root(root_before)]
930 -> [_e2 = HeaderBuilder::new()
931 .with_state_root(root_before)
932 .with_messages(messages)]
933 -> exec @ [_e3 = HeaderBuilder::new()
934 .with_state_root(root_after)
935 .with_message_receipts(receipts)]
936 };
937 chain4u! {
939 from [_e1] in c4u;
940 [_f2 = HeaderBuilder::new().with_state_root(root_before)]
941 -> [_f3 = HeaderBuilder::new().with_state_root(root_before)]
942 -> [_f4 = HeaderBuilder::new().with_state_root(root_before)]
943 -> fork_head @ [_f5 = HeaderBuilder::new().with_state_root(root_before)]
944 };
945 let state_manager = state_manager_with_head(db.clone(), genesis, exec);
946
947 let token = CancellationToken::new();
948 let mut wait =
949 Box::pin(state_manager.wait_for_message(msg_cid, 2, None, Some(true), &token));
950
951 assert!(
953 tokio::time::timeout(Duration::from_millis(100), &mut wait)
954 .await
955 .is_err()
956 );
957
958 state_manager
960 .chain_store()
961 .set_heaviest_tipset(fork_head.clone())
962 .unwrap();
963
964 assert!(
966 tokio::time::timeout(Duration::from_millis(300), &mut wait)
967 .await
968 .is_err(),
969 "a reverted candidate must not be returned"
970 );
971 }
972
973 #[rstest]
974 #[case::zero_confidence(10, 10, 0, true)]
975 #[case::exact_confidence(15, 10, 5, true)]
976 #[case::negative_candidate(0, -1, 1, false)]
977 #[case::insufficient_confidence(14, 10, 5, false)]
978 #[case::candidate_above_current(9, 10, 0, false)]
979 fn confidence_reached_cases(
980 #[case] current: ChainEpoch,
981 #[case] candidate: ChainEpoch,
982 #[case] confidence: i64,
983 #[case] expected: bool,
984 ) {
985 assert_eq!(confidence_reached(current, candidate, confidence), expected);
986 }
987
988 #[tokio::test]
990 async fn wait_for_message_rejects_confidence_above_maximum() {
991 let db = Arc::new(MemoryDB::default());
992 let (state_manager, msg_cid) = state_manager_with_replaced_message_at_head(&db);
993
994 let result = state_manager
995 .wait_for_message(
996 msg_cid,
997 MAX_MESSAGE_CONFIDENCE + 1,
998 None,
999 Some(true),
1000 &CancellationToken::new(),
1001 )
1002 .await;
1003 let err = result.expect_err("confidence above maximum should be rejected");
1004 assert!(
1005 err.to_string()
1006 .contains("message confidence exceeds maximum"),
1007 "{err}"
1008 );
1009 }
1010}