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::sync::broadcast::error::RecvError;
12use tokio_util::sync::CancellationToken;
13use tracing::warn;
14
15const MAX_MESSAGE_CONFIDENCE: ChainEpoch = crate::shim::policy::policy_constants::CHAIN_FINALITY;
17
18fn confidence_reached(current: ChainEpoch, candidate: ChainEpoch, confidence: i64) -> bool {
20 candidate >= 0 && current >= candidate && (current - candidate) >= confidence
21}
22
23impl StateManager {
24 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 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 .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 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 if current_actor_state.sequence <= message_sequence {
99 return Ok(None);
100 }
101 let message_from_id = self.lookup_required_id(&message_from_address, ¤t)?;
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 Some(state) if state.sequence > message_sequence => current = parent_tipset,
121 _ => {
126 return Ok(self
127 .tipset_executed_message(¤t, message, allow_replaced)?
128 .map(|receipt| (current, receipt)));
129 }
130 }
131 }
132
133 Ok(None)
134 }
135
136 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 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 Some(0) => None,
171 Some(limit) if limit > 0 => Some((current_epoch - limit + 1).max(0)),
175 _ => Some(0),
177 }
178 }
179
180 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 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 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(¤t_ts, &message, allow_replaced.unwrap_or(true))?;
263 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 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 tokio::task::spawn_blocking({
278 let sm = self.shallow_clone();
279 let message = message.shallow_clone();
280 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 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 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 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 Ok((applied_ts, receipt));
373 } else {
374 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 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 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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 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 #[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}