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