1use super::network_context::SyncNetworkContext;
20use crate::{
21 blocks::{Block, FullTipset, Tipset, TipsetKey},
22 chain::{ChainStore, index::ResolveNullTipset},
23 chain_sync::{
24 ForkSyncInfo, ForkSyncStage, SyncStatus, SyncStatusReport, TipsetValidator,
25 bad_block_cache::{BadBlockCache, SeenBlockCache},
26 metrics,
27 tipset_syncer::{TipsetSyncerError, validate_tipset},
28 validation::GossipBlockValidator,
29 },
30 libp2p::{NetworkEvent, PubsubMessage, hello::HelloRequest},
31 message_pool::MessagePool,
32 networks::calculate_expected_epoch,
33 prelude::*,
34 shim::clock::ChainEpoch,
35 state_manager::StateManager,
36 utils::misc::env::env_or_default_logged,
37};
38use arc_swap::ArcSwap;
39use chrono::Utc;
40use hashbrown::{HashMap, HashSet};
41use libp2p::PeerId;
42use nonzero_ext::nonzero;
43use parking_lot::Mutex;
44use std::{
45 borrow::Cow,
46 sync::LazyLock,
47 time::{Duration, Instant},
48};
49use tokio::{
50 sync::{Notify, OwnedSemaphorePermit, Semaphore},
51 task::JoinSet,
52};
53use tokio_util::sync::CancellationToken;
54
55pub struct ChainFollower {
56 tasks: Arc<Mutex<HashSet<SyncTask>>>,
58
59 state_machine: Arc<Mutex<SyncStateMachine>>,
61
62 pub sync_status: SyncStatus,
64
65 pub state_manager: StateManager,
67
68 pub network: SyncNetworkContext,
70
71 genesis: Tipset,
73
74 pub bad_blocks: Option<BadBlockCache>,
78
79 net_handler: Arc<flume::Receiver<NetworkEvent>>,
81
82 pub tipset_sender: flume::Sender<FullTipset>,
84
85 tipset_receiver: Arc<flume::Receiver<FullTipset>>,
87
88 stateless_mode: bool,
93
94 mem_pool: MessagePool<ChainStore>,
96}
97
98impl ShallowClone for ChainFollower {
99 fn shallow_clone(&self) -> Self {
100 Self {
101 tasks: self.tasks.shallow_clone(),
102 state_machine: self.state_machine.shallow_clone(),
103 sync_status: self.sync_status.shallow_clone(),
104 state_manager: self.state_manager.shallow_clone(),
105 network: self.network.shallow_clone(),
106 genesis: self.genesis.shallow_clone(),
107 bad_blocks: self.bad_blocks.shallow_clone(),
108 net_handler: self.net_handler.shallow_clone(),
109 tipset_sender: self.tipset_sender.clone(),
110 tipset_receiver: self.tipset_receiver.shallow_clone(),
111 stateless_mode: self.stateless_mode,
112 mem_pool: self.mem_pool.shallow_clone(),
113 }
114 }
115}
116
117impl ChainFollower {
118 pub fn new(
119 state_manager: StateManager,
120 network: SyncNetworkContext,
121 genesis: Tipset,
122 net_handler: flume::Receiver<NetworkEvent>,
123 stateless_mode: bool,
124 mem_pool: MessagePool<ChainStore>,
125 ) -> Self {
126 crate::def_is_env_truthy!(cache_disabled, "FOREST_DISABLE_BAD_BLOCK_CACHE");
127 let (tipset_sender, tipset_receiver) = flume::bounded(20);
128 let tasks: Arc<Mutex<HashSet<SyncTask>>> = Arc::new(Mutex::new(HashSet::default()));
129 let bad_blocks = if cache_disabled() {
130 tracing::warn!("bad block cache is disabled by `FOREST_DISABLE_BAD_BLOCK_CACHE`");
131 None
132 } else {
133 Some(Default::default())
134 };
135 let state_machine = Arc::new(Mutex::new(SyncStateMachine::new(
136 state_manager.chain_store().shallow_clone(),
137 bad_blocks.shallow_clone(),
138 stateless_mode,
139 )));
140
141 crate::metrics::register_collector(Box::new(SyncTasks(tasks.shallow_clone())));
142 crate::metrics::register_collector(Box::new(SyncStateMachineWrapper(
143 state_machine.shallow_clone(),
144 )));
145
146 Self {
147 tasks,
148 state_machine,
149 sync_status: Arc::new(ArcSwap::from_pointee(SyncStatusReport::init())),
150 state_manager,
151 network,
152 genesis,
153 bad_blocks,
154 net_handler: net_handler.into(),
155 tipset_sender,
156 tipset_receiver: tipset_receiver.into(),
157 stateless_mode,
158 mem_pool,
159 }
160 }
161
162 pub fn reset(&self) {
164 let start = Instant::now();
165 self.tasks.lock().clear();
166 self.state_manager.chain_store().validated_blocks.clear();
167 self.state_machine.lock().tipsets.clear();
168 if let Some(bad_blocks) = &self.bad_blocks {
169 bad_blocks.clear();
170 }
171 tracing::info!(
172 "chain follower reset, took {}",
173 humantime::format_duration(start.elapsed())
174 );
175 }
176
177 pub async fn run(&self) -> anyhow::Result<()> {
178 chain_follower(
179 &self.tasks,
180 &self.state_machine,
181 &self.state_manager,
182 self.bad_blocks.shallow_clone(),
183 self.net_handler.shallow_clone(),
184 self.tipset_sender.clone(),
185 self.tipset_receiver.shallow_clone(),
186 &self.network,
187 &self.mem_pool,
188 &self.sync_status,
189 &self.genesis,
190 self.stateless_mode,
191 )
192 .await
193 }
194
195 pub fn subscribe_validated_tipset(&self) -> tokio::sync::broadcast::Receiver<TipsetKey> {
197 self.state_machine
198 .lock()
199 .validated_tipset_broadcast_tx
200 .subscribe()
201 }
202
203 pub fn block_validation_subscriber(&self) -> BlockValidationSubscriber {
205 BlockValidationSubscriber(self.state_machine.lock().block_validation_tx.clone())
206 }
207}
208
209#[allow(clippy::too_many_arguments)]
210async fn chain_follower(
212 tasks: &Arc<Mutex<HashSet<SyncTask>>>,
213 state_machine: &Arc<Mutex<SyncStateMachine>>,
214 state_manager: &StateManager,
215 bad_block_cache: Option<BadBlockCache>,
216 network_rx: Arc<flume::Receiver<NetworkEvent>>,
217 tipset_sender: flume::Sender<FullTipset>,
218 tipset_receiver: Arc<flume::Receiver<FullTipset>>,
219 network: &SyncNetworkContext,
220 mem_pool: &MessagePool<ChainStore>,
221 sync_status: &SyncStatus,
222 genesis: &Tipset,
223 stateless_mode: bool,
224) -> anyhow::Result<()> {
225 let state_changed = Arc::new(Notify::new());
226
227 let seen_block_cache = SeenBlockCache::default();
228
229 let hello_fetch_limiter = Arc::new(Semaphore::new(*MAX_CONCURRENT_HELLO_TRIGGERED_FETCHES));
230
231 let mut set = JoinSet::new();
232 let cancellation_token = CancellationToken::new();
233 let _cancellation_token_drop_guard = cancellation_token.drop_guard_ref();
234
235 set.spawn({
237 let state_manager = state_manager.shallow_clone();
238 let network = network.shallow_clone();
239 let mem_pool = mem_pool.shallow_clone();
240 let genesis = genesis.shallow_clone();
241 let bad_block_cache = bad_block_cache.shallow_clone();
242 let seen_block_cache = seen_block_cache.shallow_clone();
243 let cancellation_token = cancellation_token.clone();
244 let hello_fetch_limiter = hello_fetch_limiter.shallow_clone();
245 let tipset_sender = tipset_sender.clone();
246 async move {
247 while let Ok(event) = network_rx.recv_async().await {
248 inc_gossipsub_event_metrics(&event);
249
250 update_peer_info(
251 &event,
252 &network,
253 state_manager.chain_store().shallow_clone(),
254 &genesis,
255 cancellation_token.clone(),
256 );
257
258 match event {
261 NetworkEvent::HelloResponseOutbound { request, source } => {
262 let Ok(permit) = hello_fetch_limiter.shallow_clone().try_acquire_owned() else {
263 debug!(%source, "dropping hello-triggered tipset fetch: too many in flight");
264 continue;
265 };
266 spawn_tipset_fetch(
267 network.shallow_clone(),
268 state_manager.chain_store().shallow_clone(),
269 Some(source),
270 TipsetKey::from(request.heaviest_tip_set),
271 tipset_sender.clone(),
272 cancellation_token.clone(),
273 Some(permit),
274 );
275 }
276 NetworkEvent::PubsubMessage { message } => match message {
277 PubsubMessage::Block(b) => {
278 let cs = state_manager.chain_store();
279 let cfg = cs.chain_config();
280 if let Err(reason) = GossipBlockValidator::new(&b).validate_pre_fetch(
281 &genesis,
282 cfg.block_delay_secs,
283 cs.ec_calculator_finalized_epoch(), bad_block_cache.as_ref(),
285 &seen_block_cache,
286 ) {
287 metrics::GOSSIP_BLOCK_REJECTED_TOTAL
288 .get_or_create(&metrics::GossipRejectReasonLabel {
289 reason: reason.label(),
290 })
291 .inc();
292 debug!("Rejected gossip block {}: {reason}", b.header.cid());
293 continue;
294 }
295 let key = TipsetKey::from(nunny::vec![*b.header.cid()]);
296 spawn_tipset_fetch(
298 network.shallow_clone(),
299 cs.shallow_clone(),
300 None,
301 key,
302 tipset_sender.clone(),
303 cancellation_token.clone(),
304 None,
305 );
306 }
307 PubsubMessage::Message(m) => {
308 if let Err(why) = mem_pool.add(m).await {
309 debug!("Received invalid GossipSub message: {}", why);
310 }
311 }
312 },
313 _ => {}
314 }
315 }
316 }
317 });
318
319 set.spawn({
321 let state_changed = state_changed.clone();
322 let state_machine = state_machine.clone();
323
324 async move {
325 while let Ok(tipset) = tipset_receiver.recv_async().await {
326 state_machine
327 .lock()
328 .update(SyncEvent::NewFullTipsets(vec![tipset]));
329 state_changed.notify_one();
330 }
331 }
332 });
333
334 set.spawn({
336 let state_manager = state_manager.shallow_clone();
337 let state_machine = state_machine.shallow_clone();
338 let network = network.shallow_clone();
339 let sync_status = sync_status.shallow_clone();
340 let state_changed = state_changed.shallow_clone();
341 let tasks = tasks.shallow_clone();
342 let bad_block_cache = bad_block_cache.shallow_clone();
343 let cancellation_token = cancellation_token.clone();
344 async move {
345 const FORK_CLEANUP_INTERVAL: Duration = Duration::from_mins(1);
346 let mut last_fork_cleanup = Instant::now();
347 loop {
348 state_changed.notified().await;
349
350 let mut tasks_set = tasks.lock();
351 if last_fork_cleanup + FORK_CLEANUP_INTERVAL < Instant::now() {
352 state_machine.lock().cleanup_dangling_forks();
353 last_fork_cleanup = Instant::now();
354 }
355 let (task_vec, current_active_forks) = state_machine.lock().tasks();
356
357 {
359 let old_status_report = sync_status.load().shallow_clone();
360 let new_status_report = old_status_report.update(
361 &state_manager,
362 current_active_forks,
363 stateless_mode,
364 );
365 sync_status.store(new_status_report.into());
366 }
367
368 for task in task_vec {
369 let new = tasks_set.insert(task.clone());
371 if new {
372 let action = task.clone().execute(
373 network.shallow_clone(),
374 state_manager.shallow_clone(),
375 stateless_mode,
376 bad_block_cache.shallow_clone(),
377 );
378 tokio::spawn({
379 let tasks = tasks.shallow_clone();
380 let state_machine = state_machine.shallow_clone();
381 let state_changed = state_changed.shallow_clone();
382 let cancellation_token = cancellation_token.clone();
383 async move {
384 cancellation_token
385 .run_until_cancelled(async move {
386 if let Some(event) = action.await {
387 state_machine.lock().update(event);
388 state_changed.notify_one();
389 }
390 let mut tasks = tasks.lock();
391 tasks.remove(&task);
392 tasks.shrink_to_fit();
393 })
394 .await
395 }
396 });
397 }
398 }
399 }
400 }
401 });
402
403 set.spawn({
407 let state_manager = state_manager.shallow_clone();
408 let sync_status = sync_status.shallow_clone();
409 async move {
410 loop {
411 tokio::time::sleep(tokio::time::Duration::from_secs(10)).await;
412 let heaviest_tipset = state_manager.chain_store().heaviest_tipset();
413 let heaviest_epoch = heaviest_tipset.epoch();
414 let expected_head = calculate_expected_epoch(
415 Utc::now().timestamp() as u64,
416 state_manager.chain_store().genesis_block_header().timestamp,
417 state_manager.chain_config().block_delay_secs,
418 );
419 let diff = expected_head - heaviest_epoch;
420
421 if diff <= 10 {
424 continue;
425 }
426
427 let report = sync_status.load();
430 let forks = &report.active_forks;
431 let status: Cow<'static, str> = if forks
435 .iter()
436 .any(|fork| fork.stage == ForkSyncStage::ValidatingTipsets)
437 {
438 ", validating tipsets".into()
439 } else if forks
440 .iter()
441 .any(|fork| fork.stage == ForkSyncStage::FetchingHeaders)
442 {
443 let remaining = forks
447 .iter()
448 .filter(|fork| fork.stage == ForkSyncStage::FetchingHeaders)
449 .map(|fork| fork.target_sync_epoch_start - fork.validated_chain_head_epoch)
450 .filter(|gap| *gap > 0)
451 .min();
452 match remaining {
453 Some(remaining) => format!(", fetching headers (~{remaining})").into(),
454 None => ", fetching headers".into(),
458 }
459 } else {
460 ", waiting for tipsets".into()
463 };
464 info!(
465 "Catching up to HEAD: {heaviest_epoch}{} -> {expected_head} (diff: {diff}){status}",
466 heaviest_tipset.key()
467 );
468 }
469 }
470 });
471
472 set.join_all().await;
473 Ok(())
474}
475
476fn inc_gossipsub_event_metrics(event: &NetworkEvent) {
478 let label = match event {
479 NetworkEvent::HelloRequestInbound => metrics::values::HELLO_REQUEST_INBOUND,
480 NetworkEvent::HelloResponseOutbound { .. } => metrics::values::HELLO_RESPONSE_OUTBOUND,
481 NetworkEvent::HelloRequestOutbound => metrics::values::HELLO_REQUEST_OUTBOUND,
482 NetworkEvent::HelloResponseInbound => metrics::values::HELLO_RESPONSE_INBOUND,
483 NetworkEvent::PeerConnected(_) => metrics::values::PEER_CONNECTED,
484 NetworkEvent::PeerDisconnected(_) => metrics::values::PEER_DISCONNECTED,
485 NetworkEvent::PubsubMessage { message } => match message {
486 PubsubMessage::Block(_) => metrics::values::PUBSUB_BLOCK,
487 PubsubMessage::Message(_) => metrics::values::PUBSUB_MESSAGE,
488 },
489 NetworkEvent::ChainExchangeRequestOutbound => {
490 metrics::values::CHAIN_EXCHANGE_REQUEST_OUTBOUND
491 }
492 NetworkEvent::ChainExchangeResponseInbound => {
493 metrics::values::CHAIN_EXCHANGE_RESPONSE_INBOUND
494 }
495 NetworkEvent::ChainExchangeRequestInbound => {
496 metrics::values::CHAIN_EXCHANGE_REQUEST_INBOUND
497 }
498 NetworkEvent::ChainExchangeResponseOutbound => {
499 metrics::values::CHAIN_EXCHANGE_RESPONSE_OUTBOUND
500 }
501 };
502
503 metrics::LIBP2P_MESSAGE_TOTAL.get_or_create(&label).inc();
504}
505
506fn update_peer_info(
508 event: &NetworkEvent,
509 network: &SyncNetworkContext,
510 chain_store: ChainStore,
511 genesis: &Tipset,
512 cancellation_token: CancellationToken,
513) {
514 match event {
515 NetworkEvent::PeerConnected(peer_id) => {
516 let peer_id = *peer_id;
517 let genesis_cid = *genesis.block_headers().first().cid();
518 let network = network.shallow_clone();
519 tokio::task::spawn(async move {
521 cancellation_token
522 .run_until_cancelled(handle_peer_connected_event(
523 network,
524 chain_store,
525 peer_id,
526 genesis_cid,
527 ))
528 .await
529 });
530 }
531 NetworkEvent::PeerDisconnected(peer_id) => {
532 handle_peer_disconnected_event(network, *peer_id);
533 }
534 _ => {}
535 }
536}
537
538async fn handle_peer_connected_event(
539 network: SyncNetworkContext,
540 chain_store: ChainStore,
541 peer_id: PeerId,
542 genesis_block_cid: Cid,
543) {
544 if network.peer_manager().is_peer_new(&peer_id) {
546 let heaviest = chain_store.heaviest_tipset();
549 let request = HelloRequest {
550 heaviest_tip_set: heaviest.cids(),
551 heaviest_tipset_height: heaviest.epoch(),
552 heaviest_tipset_weight: heaviest.weight().clone().into(),
553 genesis_cid: genesis_block_cid,
554 };
555 let (peer_id, moment_sent, response) = match network.hello_request(peer_id, request).await {
556 Ok(response) => response,
557 Err(e) => {
558 debug!("Hello request failed: {}", e);
559 return;
560 }
561 };
562 let dur = Instant::now().duration_since(moment_sent);
563
564 match response {
566 Some(_) => {
567 network.peer_manager().log_success(&peer_id, dur);
568 }
569 None => {
570 network.peer_manager().log_failure(&peer_id, dur);
571 }
572 }
573 }
574}
575
576fn handle_peer_disconnected_event(network: &SyncNetworkContext, peer_id: PeerId) {
577 network.peer_manager().remove_peer(&peer_id);
578 network.peer_manager().unmark_peer_bad(&peer_id);
579}
580
581static MAX_CONCURRENT_HELLO_TRIGGERED_FETCHES: LazyLock<usize> = LazyLock::new(|| {
588 env_or_default_logged(
590 "FOREST_MAX_CONCURRENT_HELLO_TRIGGERED_FETCHES",
591 nonzero!(16_usize),
592 )
593 .get()
594 .min(Semaphore::MAX_PERMITS)
595});
596
597fn spawn_tipset_fetch(
600 network: SyncNetworkContext,
601 chain_store: ChainStore,
602 peer_id: Option<PeerId>,
603 tipset_keys: TipsetKey,
604 tipset_sender: flume::Sender<FullTipset>,
605 cancellation_token: CancellationToken,
606 permit: Option<OwnedSemaphorePermit>,
607) {
608 tokio::spawn(async move {
609 let _permit = permit;
610 cancellation_token
613 .run_until_cancelled(async {
614 match get_full_tipset(&network, &chain_store, peer_id, &tipset_keys).await {
615 Ok(tipset) => {
616 let _ = tipset_sender.send_async(tipset).await;
617 }
618 Err(e) => debug!("Querying full tipset failed: {e}"),
619 }
620 })
621 .await;
622 });
623}
624
625pub async fn get_full_tipset(
626 network: &SyncNetworkContext,
627 chain_store: &ChainStore,
628 peer_id: Option<PeerId>,
629 tipset_keys: &TipsetKey,
630) -> anyhow::Result<FullTipset> {
631 if let Ok(full_tipset) = load_full_tipset(chain_store, tipset_keys) {
633 return Ok(full_tipset);
634 }
635 let tipset = network
637 .chain_exchange_full_tipset(peer_id, tipset_keys)
638 .await
639 .map_err(|e| anyhow::anyhow!(e))?;
640 tipset.persist(chain_store.db())?;
641
642 Ok(tipset)
643}
644
645async fn get_full_tipset_batch(
646 network: &SyncNetworkContext,
647 chain_store: &ChainStore,
648 peer_id: Option<PeerId>,
649 tipset_keys: &TipsetKey,
650) -> anyhow::Result<Vec<FullTipset>> {
651 if let Ok(full_tipset) = load_full_tipset(chain_store, tipset_keys) {
653 return Ok(vec![full_tipset]);
654 }
655 let tipsets = network
657 .chain_exchange_full_tipsets(peer_id, tipset_keys)
658 .await
659 .map_err(|e| anyhow::anyhow!(e))?;
660
661 for tipset in tipsets.iter() {
662 tipset.persist(chain_store.db())?;
663 }
664
665 Ok(tipsets)
666}
667
668pub fn load_full_tipset(
669 chain_store: &ChainStore,
670 tipset_keys: &TipsetKey,
671) -> anyhow::Result<FullTipset> {
672 let ts = chain_store
674 .chain_index()
675 .load_required_tipset(tipset_keys)?;
676 let blocks: Vec<_> = ts
677 .block_headers()
678 .iter()
679 .map(|header| -> anyhow::Result<Block> {
680 let (bls_msgs, secp_msgs) = crate::chain::block_messages(chain_store.db(), header)?;
681 Ok(Block {
682 header: header.clone(),
683 bls_messages: bls_msgs,
684 secp_messages: secp_msgs,
685 })
686 })
687 .try_collect()?;
688 let fts = FullTipset::new(blocks)?;
690 Ok(fts)
691}
692
693#[derive(Clone, Copy, Debug, PartialEq, Eq)]
696pub enum BlockValidationOutcome {
697 Applied,
698 Rejected,
699}
700
701#[derive(Clone)]
705pub struct BlockValidationSubscriber(tokio::sync::broadcast::Sender<(Cid, BlockValidationOutcome)>);
706
707impl Default for BlockValidationSubscriber {
708 fn default() -> Self {
709 Self(tokio::sync::broadcast::Sender::new(1))
710 }
711}
712
713impl BlockValidationSubscriber {
714 pub fn subscribe(&self) -> tokio::sync::broadcast::Receiver<(Cid, BlockValidationOutcome)> {
718 self.0.subscribe()
719 }
720}
721
722enum SyncEvent {
723 NewFullTipsets(Vec<FullTipset>),
724 BadTipset(FullTipset),
725 ValidatedTipset {
726 tipset: FullTipset,
727 is_proposed_head: bool,
728 },
729}
730
731impl std::fmt::Display for SyncEvent {
732 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
733 fn tss_to_string(tss: &[FullTipset]) -> String {
734 format!(
735 "epoch: {}-{}",
736 tss.first().map(|ts| ts.epoch()).unwrap_or_default(),
737 tss.last().map(|ts| ts.epoch()).unwrap_or_default()
738 )
739 }
740
741 match self {
742 Self::NewFullTipsets(tss) => write!(f, "NewFullTipsets({})", tss_to_string(tss)),
743 Self::BadTipset(ts) => {
744 write!(f, "BadTipset(epoch: {}, key: {})", ts.epoch(), ts.key())
745 }
746 Self::ValidatedTipset {
747 tipset,
748 is_proposed_head,
749 } => write!(
750 f,
751 "ValidatedTipset(epoch: {}, key: {}, is_proposed_head: {is_proposed_head})",
752 tipset.epoch(),
753 tipset.key()
754 ),
755 }
756 }
757}
758
759#[derive(derive_more::Debug)]
760struct SyncStateMachine {
761 #[debug(skip)]
762 cs: ChainStore,
763 bad_block_cache: Option<BadBlockCache>,
764 tipsets: HashMap<TipsetKey, FullTipset>,
766 stateless_mode: bool,
767 validated_tipset_broadcast_tx: tokio::sync::broadcast::Sender<TipsetKey>,
769 block_validation_tx: tokio::sync::broadcast::Sender<(Cid, BlockValidationOutcome)>,
771}
772
773impl SyncStateMachine {
774 pub fn new(
775 cs: ChainStore,
776 bad_block_cache: Option<BadBlockCache>,
777 stateless_mode: bool,
778 ) -> Self {
779 Self {
780 cs,
781 bad_block_cache,
782 tipsets: HashMap::default(),
783 stateless_mode,
784 validated_tipset_broadcast_tx: tokio::sync::broadcast::Sender::new(1024),
785 block_validation_tx: tokio::sync::broadcast::Sender::new(1024),
786 }
787 }
788
789 fn notify_rejected(&self, tipset: &FullTipset) {
794 if crate::utils::broadcast::has_subscribers(&self.block_validation_tx) {
795 for cid in tipset.key().to_cids() {
796 if self
797 .bad_block_cache
798 .as_ref()
799 .is_some_and(|c| c.get(&cid).is_some())
800 {
801 let _ = self
802 .block_validation_tx
803 .send((cid, BlockValidationOutcome::Rejected));
804 }
805 }
806 }
807 }
808
809 fn chains(&self) -> Vec<Vec<FullTipset>> {
811 let mut chains = Vec::new();
812 let mut remaining_tipsets = self.tipsets.clone();
813
814 while let Some(heaviest) = remaining_tipsets
815 .values()
816 .max_by_key(|ts| ts.weight())
817 .cloned()
818 {
819 let mut chain = Vec::new();
821 let mut current = Some(heaviest);
822
823 while let Some(tipset) = current.take() {
824 remaining_tipsets.remove(tipset.key());
825
826 current = self.tipsets.get(tipset.parents()).cloned();
828
829 chain.push(tipset);
830 }
831 chain.reverse();
832 chains.push(chain);
833 }
834
835 chains
836 }
837
838 fn is_parent_validated(&self, tipset: &FullTipset) -> bool {
839 let db = self.cs.db();
840 self.stateless_mode || db.has(tipset.parent_state()).unwrap_or(false)
841 }
842
843 fn is_ready_for_validation(&self, tipset: &FullTipset) -> bool {
844 if self.stateless_mode || tipset.key() == self.cs.genesis_tipset().key() {
845 true
847 } else if let Ok(parent_ts) = load_full_tipset(&self.cs, tipset.parents()) {
848 let head_ts = self.cs.heaviest_tipset();
849 if parent_ts.key() == head_ts.key() {
855 true
856 } else if parent_ts.epoch() >= head_ts.epoch() {
857 false
858 } else {
859 self.is_parent_validated(tipset)
860 }
861 } else {
862 false
863 }
864 }
865
866 fn add_full_tipset(&mut self, tipset: FullTipset) {
867 if let Err(why) = TipsetValidator(&tipset).validate(
868 &self.cs,
869 self.bad_block_cache.as_ref(),
870 &self.cs.genesis_tipset(),
871 self.cs.chain_config().block_delay_secs,
872 ) {
873 metrics::INVALID_TIPSET_TOTAL.inc();
874 trace!("Skipping invalid tipset: {}", why);
875 self.mark_bad_tipset(tipset);
876 return;
877 }
878
879 if tipset.epoch() < self.cs.ec_calculator_finalized_epoch() {
882 self.mark_bad_tipset(tipset);
883 return;
884 }
885
886 if self.tipsets.contains_key(tipset.key()) {
888 return;
889 }
890
891 if let Ok(Some(ts)) = self.cs.chain_index().tipset_by_height_blocking(
893 tipset.epoch(),
894 self.cs.heaviest_tipset(),
895 ResolveNullTipset::TakeOlder,
896 ) && ts.key() == tipset.key()
897 {
898 return;
899 }
900
901 let mut to_remove = Vec::new();
903 #[allow(clippy::mutable_key_type)]
904 let mut merged_blocks: HashSet<_> = tipset.blocks().iter().cloned().collect();
905
906 let parent_refs: HashSet<_> = self
908 .tipsets
909 .values()
910 .map(|ts| ts.parents().clone())
911 .collect();
912
913 for (key, existing_ts) in self.tipsets.iter() {
914 if existing_ts.epoch() == tipset.epoch() && existing_ts.parents() == tipset.parents() {
915 if !parent_refs.contains(key) {
917 to_remove.push(key.clone());
918 }
919 merged_blocks.extend(existing_ts.blocks().iter().cloned());
921 }
922 }
923
924 for key in to_remove {
926 self.tipsets.remove(&key);
927 }
928
929 if let Ok(merged_tipset) = FullTipset::new(merged_blocks) {
931 self.tipsets
932 .insert(merged_tipset.key().clone(), merged_tipset);
933 }
934
935 self.tipsets.shrink_to_fit();
936 }
937
938 fn mark_bad_tipset(&mut self, tipset: FullTipset) {
942 self.notify_rejected(&tipset);
945 let mut stack = vec![tipset];
946 while let Some(tipset) = stack.pop() {
947 self.tipsets.remove(tipset.key());
948 let mut to_remove = Vec::new();
950 let mut descendants = Vec::new();
951
952 for (key, ts) in self.tipsets.iter() {
953 if ts.parents() == tipset.key() {
954 to_remove.push(key.clone());
955 descendants.push(ts.clone());
956 }
957 }
958
959 for key in to_remove {
961 self.tipsets.remove(&key);
962 }
963
964 stack.extend(descendants);
966 }
967 }
968
969 fn try_mark_tipset_as_validated(&mut self, tipset: FullTipset, is_proposed_head: bool) -> bool {
970 if !self.is_parent_validated(&tipset) {
971 tracing::error!(epoch = %tipset.epoch(), tsk = %tipset.key(), parent_state = %tipset.parent_state(), "Parent tipset must be validated");
972 return false;
973 }
974
975 self.tipsets.remove(tipset.key());
976 let tipset = tipset.into_tipset();
977 if self.stateless_mode {
979 let epoch = tipset.epoch();
980 let terse_key = tipset.key().terse();
981 if self.cs.heaviest_tipset().weight() < tipset.weight() {
982 if let Err(e) = self.cs.set_heaviest_tipset(tipset) {
983 error!("Error setting heaviest tipset: {}", e);
984 return false;
985 } else {
986 info!("Heaviest tipset: {} ({})", epoch, terse_key);
987 }
988 }
989 } else if is_proposed_head {
990 if let Err(e) = self.cs.maybe_update_pending_head(&tipset) {
991 error!("Error putting tipset: {e}");
992 return false;
993 }
994 } else if let Err(e) = self.cs.set_heaviest_tipset(tipset) {
995 error!("Error setting heaviest tipset: {e}");
996 return false;
997 }
998 true
999 }
1000
1001 pub fn update(&mut self, event: SyncEvent) {
1002 tracing::trace!("update: {event}");
1003 match event {
1004 SyncEvent::NewFullTipsets(tipsets) => {
1005 for tipset in tipsets {
1006 self.add_full_tipset(tipset);
1007 }
1008 }
1009 SyncEvent::BadTipset(tipset) => self.mark_bad_tipset(tipset),
1010 SyncEvent::ValidatedTipset {
1011 tipset,
1012 is_proposed_head,
1013 } => {
1014 let applied_cids =
1017 crate::utils::broadcast::has_subscribers(&self.block_validation_tx)
1018 .then(|| tipset.key().to_cids());
1019 if self.try_mark_tipset_as_validated(tipset, is_proposed_head) {
1020 if let Some(cids) = applied_cids {
1021 for cid in cids {
1022 let _ = self
1023 .block_validation_tx
1024 .send((cid, BlockValidationOutcome::Applied));
1025 }
1026 }
1027 if crate::utils::broadcast::has_subscribers(&self.validated_tipset_broadcast_tx)
1028 && let Err(e) = self.validated_tipset_broadcast_tx.send(self.cs.heaviest_tipset().key().clone())
1030 {
1031 warn!("Failed to broadcast validated tipset: {e}");
1032 }
1033 }
1034 }
1035 }
1036 }
1037
1038 pub fn tasks(&self) -> (Vec<SyncTask>, Vec<ForkSyncInfo>) {
1039 let current_validated_epoch = self.cs.heaviest_tipset().epoch();
1041 let now = Utc::now();
1042
1043 let mut active_sync_info = Vec::new();
1044 let mut tasks = Vec::new();
1045 for chain in self.chains() {
1046 if let Some(first_ts) = chain.first() {
1047 let last_ts = chain.last().expect("Infallible");
1048 let stage: ForkSyncStage;
1049 let start_time = Some(now);
1050
1051 if !self.is_ready_for_validation(first_ts) {
1052 stage = ForkSyncStage::FetchingHeaders;
1053 tasks.push(SyncTask::FetchTipset(
1054 first_ts.parents().clone(),
1055 first_ts.epoch(),
1056 ));
1057 } else {
1058 stage = ForkSyncStage::ValidatingTipsets;
1059 tasks.push(SyncTask::ValidateTipset {
1060 tipset: first_ts.clone(),
1061 is_proposed_head: chain.len() == 1,
1062 });
1063 }
1064
1065 let fork_info = ForkSyncInfo {
1066 target_tipset_key: last_ts.key().clone(),
1067 target_epoch: last_ts.epoch(),
1068 target_sync_epoch_start: first_ts.epoch(),
1069 stage,
1070 validated_chain_head_epoch: current_validated_epoch,
1071 start_time,
1072 last_updated: Some(now),
1073 };
1074
1075 active_sync_info.push(fork_info);
1076 }
1077 }
1078 (tasks, active_sync_info)
1079 }
1080
1081 pub fn cleanup_dangling_forks(&mut self) {
1082 let finalized_epoch = self.cs.ec_calculator_finalized_epoch();
1083 for chain in self.chains() {
1084 if let Some(target) = chain.last()
1086 && target.epoch() < finalized_epoch
1087 {
1088 chain.iter().for_each(|ts| {
1089 self.tipsets.remove(ts.key());
1090 });
1091 tracing::info!(
1092 "Cleaned up dangling fork from epoch {} to {}",
1093 chain.first().map(|ts| ts.epoch()).unwrap_or_default(),
1094 chain.last().map(|ts| ts.epoch()).unwrap_or_default(),
1095 );
1096 }
1097 }
1098 }
1099}
1100
1101#[derive(PartialEq, Eq, Hash, Clone, Debug)]
1102enum SyncTask {
1103 ValidateTipset {
1104 tipset: FullTipset,
1105 is_proposed_head: bool,
1106 },
1107 FetchTipset(TipsetKey, ChainEpoch),
1108}
1109
1110impl std::fmt::Display for SyncTask {
1111 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1112 match self {
1113 SyncTask::ValidateTipset {
1114 tipset,
1115 is_proposed_head,
1116 } => write!(
1117 f,
1118 "ValidateTipset(epoch: {}, is_proposed_head: {is_proposed_head})",
1119 tipset.epoch()
1120 ),
1121 SyncTask::FetchTipset(key, epoch) => {
1122 let s = key.to_string();
1123 write!(
1124 f,
1125 "FetchTipset({}, epoch: {})",
1126 &s[s.len().saturating_sub(8)..],
1127 epoch
1128 )
1129 }
1130 }
1131 }
1132}
1133
1134impl SyncTask {
1135 async fn execute(
1136 self,
1137 network: SyncNetworkContext,
1138 state_manager: StateManager,
1139 stateless_mode: bool,
1140 bad_block_cache: Option<BadBlockCache>,
1141 ) -> Option<SyncEvent> {
1142 tracing::trace!("SyncTask::execute {self}");
1143 match self {
1144 SyncTask::ValidateTipset {
1145 tipset,
1146 is_proposed_head,
1147 } if stateless_mode => Some(SyncEvent::ValidatedTipset {
1148 tipset,
1149 is_proposed_head,
1150 }),
1151 SyncTask::ValidateTipset {
1152 tipset,
1153 is_proposed_head,
1154 } => match validate_tipset(&state_manager, tipset.clone(), bad_block_cache).await {
1155 Ok(()) => Some(SyncEvent::ValidatedTipset {
1156 tipset,
1157 is_proposed_head,
1158 }),
1159 Err(e) if matches!(e, TipsetSyncerError::TimeTravellingBlock { .. }) => {
1164 warn!("Time travelling block detected, skipping tipset for now: {e}");
1165 None
1166 }
1167 Err(e) if matches!(e, TipsetSyncerError::ParentChainStateMismatch(_)) => {
1170 warn!("Error validating tipset: {e}");
1171 let sm = state_manager.shallow_clone();
1172 match tokio::task::spawn_blocking(move || sm.repair_tipset_lookup())
1173 .await
1174 .unwrap_or_else(|e| Err(e.into()))
1175 {
1176 Ok(0) => Some(SyncEvent::BadTipset(tipset)),
1178 Ok(repaired) => {
1179 warn!(
1180 "Repaired {repaired} tipset lookup entries; retrying tipset validation at epoch {}",
1181 tipset.epoch()
1182 );
1183 None
1184 }
1185 Err(e) => {
1188 warn!("Failed to repair tipset lookup table: {e:#}");
1189 None
1190 }
1191 }
1192 }
1193 Err(e) => {
1194 warn!("Error validating tipset: {e}");
1195 Some(SyncEvent::BadTipset(tipset))
1196 }
1197 },
1198 SyncTask::FetchTipset(key, epoch) => {
1199 match get_full_tipset_batch(&network, state_manager.chain_store(), None, &key).await
1200 {
1201 Ok(parents) => Some(SyncEvent::NewFullTipsets(parents)),
1202 Err(e) => {
1203 tracing::debug!(%key, %epoch, "failed to fetch tipset: {e:#}");
1205 None
1206 }
1207 }
1208 }
1209 }
1210 }
1211}
1212
1213#[derive(Debug)]
1214struct SyncTasks(Arc<Mutex<HashSet<SyncTask>>>);
1215
1216#[derive(Debug)]
1217struct SyncStateMachineWrapper(Arc<Mutex<SyncStateMachine>>);
1218
1219mod metrics_collection {
1220 use super::*;
1221 use prometheus_client::{
1222 collector::Collector,
1223 encoding::{DescriptorEncoder, EncodeMetric},
1224 metrics::gauge::Gauge,
1225 registry::Unit,
1226 };
1227
1228 impl Collector for SyncTasks {
1229 fn encode(&self, mut encoder: DescriptorEncoder) -> Result<(), std::fmt::Error> {
1230 {
1231 let size_in_bytes = {
1232 let g: Gauge = Default::default();
1233 g.set(self.0.lock().allocation_size() as i64);
1234 g
1235 };
1236 let size_metric_encoder = encoder.encode_descriptor(
1237 "chain_follower_tasks_size",
1238 "Size of the chain follower tasks in bytes",
1239 Some(&Unit::Bytes),
1240 size_in_bytes.metric_type(),
1241 )?;
1242 size_in_bytes.encode(size_metric_encoder)?;
1243 }
1244 {
1245 let len = {
1246 let g: Gauge = Default::default();
1247 g.set(self.0.lock().len() as i64);
1248 g
1249 };
1250 let size_metric_encoder = encoder.encode_descriptor(
1251 "chain_follower_tasks_len",
1252 "Length of the chain follower tasks",
1253 None,
1254 len.metric_type(),
1255 )?;
1256 len.encode(size_metric_encoder)?;
1257 }
1258 {
1259 let cap = {
1260 let g: Gauge = Default::default();
1261 g.set(self.0.lock().capacity() as i64);
1262 g
1263 };
1264 let size_metric_encoder = encoder.encode_descriptor(
1265 "chain_follower_tasks_cap",
1266 "Capacity of the chain follower tasks",
1267 None,
1268 cap.metric_type(),
1269 )?;
1270 cap.encode(size_metric_encoder)?;
1271 }
1272
1273 Ok(())
1274 }
1275 }
1276
1277 impl Collector for SyncStateMachineWrapper {
1278 fn encode(&self, mut encoder: DescriptorEncoder) -> Result<(), std::fmt::Error> {
1279 {
1280 let size_in_bytes = {
1281 let g: Gauge = Default::default();
1282 g.set(self.0.lock().tipsets.allocation_size() as i64);
1283 g
1284 };
1285 let size_metric_encoder = encoder.encode_descriptor(
1286 "chain_follower_tipsets_size",
1287 "Size of the chain follower tipsets in bytes",
1288 Some(&Unit::Bytes),
1289 size_in_bytes.metric_type(),
1290 )?;
1291 size_in_bytes.encode(size_metric_encoder)?;
1292 }
1293 {
1294 let len = {
1295 let g: Gauge = Default::default();
1296 g.set(self.0.lock().tipsets.len() as i64);
1297 g
1298 };
1299 let size_metric_encoder = encoder.encode_descriptor(
1300 "chain_follower_tipsets_len",
1301 "Length of the chain follower tipsets",
1302 None,
1303 len.metric_type(),
1304 )?;
1305 len.encode(size_metric_encoder)?;
1306 }
1307 {
1308 let cap = {
1309 let g: Gauge = Default::default();
1310 g.set(self.0.lock().tipsets.capacity() as i64);
1311 g
1312 };
1313 let size_metric_encoder = encoder.encode_descriptor(
1314 "chain_follower_tipsets_cap",
1315 "Capacity of the chain follower tipsets",
1316 None,
1317 cap.metric_type(),
1318 )?;
1319 cap.encode(size_metric_encoder)?;
1320 }
1321
1322 Ok(())
1323 }
1324 }
1325}
1326
1327#[cfg(test)]
1328mod tests {
1329 use super::*;
1330 use crate::blocks::{CachingBlockHeader, Chain4U, HeaderBuilder, chain4u};
1331 use crate::db::MemoryDB;
1332 use crate::utils::db::CborStoreExt as _;
1333 use num_bigint::BigInt;
1334 use num_traits::ToPrimitive;
1335 use std::sync::Arc;
1336 use tracing::level_filters::LevelFilter;
1337 use tracing_subscriber::EnvFilter;
1338
1339 fn setup() -> (ChainStore, Chain4U<Arc<MemoryDB>>) {
1340 let _ = tracing_subscriber::fmt()
1342 .without_time()
1343 .with_env_filter(
1344 EnvFilter::builder()
1345 .with_default_directive(LevelFilter::DEBUG.into())
1346 .from_env()
1347 .unwrap(),
1348 )
1349 .try_init();
1350
1351 let db = Arc::new(MemoryDB::default());
1352
1353 let c4u = Chain4U::with_blockstore(db.clone());
1355 chain4u! {
1356 in c4u;
1357 [genesis_header = dummy_node(&db, 0)]
1358 };
1359
1360 let cs = ChainStore::new(db, Default::default(), genesis_header).unwrap();
1361
1362 cs.set_heaviest_tipset(cs.genesis_tipset()).unwrap();
1363
1364 (cs, c4u)
1365 }
1366
1367 fn dummy_state(db: impl Blockstore, i: ChainEpoch) -> Cid {
1368 db.put_cbor_default(&i).unwrap()
1369 }
1370
1371 fn dummy_node(db: impl Blockstore, i: ChainEpoch) -> HeaderBuilder {
1372 HeaderBuilder {
1373 state_root: dummy_state(db, i).into(),
1374 weight: BigInt::from(i).into(),
1375 epoch: i.into(),
1376 ..Default::default()
1377 }
1378 }
1379
1380 #[test]
1381 fn test_state_machine_validation_order() {
1382 let (cs, c4u) = setup();
1383 let db = cs.db_owned();
1384
1385 chain4u! {
1386 from [genesis_header] in c4u;
1387 [a = dummy_node(&db, 1)] -> [b = dummy_node(&db, 2)] -> [c = dummy_node(&db, 3)] -> [d = dummy_node(&db, 4)] -> [e = dummy_node(&db, 5)]
1388 };
1389
1390 let mut state_machine = SyncStateMachine::new(cs.shallow_clone(), Default::default(), true);
1392
1393 let tipsets = vec![e, b, d, c, a];
1395
1396 for block in tipsets {
1398 let full_tipset = FullTipset::new(vec![Block {
1399 header: block.clone().into(),
1400 bls_messages: vec![],
1401 secp_messages: vec![],
1402 }])
1403 .unwrap();
1404 state_machine.update(SyncEvent::NewFullTipsets(vec![full_tipset]));
1405 }
1406
1407 let mut validation_tasks = Vec::new();
1409 loop {
1410 let (tasks, _) = state_machine.tasks();
1411
1412 let validation_tipsets: Vec<_> = tasks
1414 .into_iter()
1415 .filter_map(|task| {
1416 if let SyncTask::ValidateTipset {
1417 tipset,
1418 is_proposed_head,
1419 } = task
1420 {
1421 Some((tipset, is_proposed_head))
1422 } else {
1423 None
1424 }
1425 })
1426 .collect();
1427
1428 if validation_tipsets.is_empty() {
1429 break;
1430 }
1431
1432 for (ts, is_proposed_head) in validation_tipsets {
1434 validation_tasks.push(ts.epoch());
1435 db.put_cbor_default(&ts.epoch()).unwrap();
1436 state_machine.try_mark_tipset_as_validated(ts, is_proposed_head);
1437 }
1438 }
1439
1440 assert_eq!(validation_tasks, vec![1, 2, 3, 4, 5]);
1442 }
1443
1444 #[test]
1445 fn test_sync_state_machine_chain_fragments() {
1446 let (cs, c4u) = setup();
1447 let db = cs.db();
1448
1449 chain4u! {
1453 in c4u;
1454 [a = dummy_node(db, 1)] -> [b = dummy_node(db, 2)]
1455 };
1456 chain4u! {
1457 from [a] in c4u;
1458 [c = dummy_node(db, 3)]
1459 };
1460
1461 let mut state_machine = SyncStateMachine::new(cs, Default::default(), false);
1463
1464 for block in [a, b, c] {
1466 let full_tipset = FullTipset::new(vec![Block {
1467 header: block.clone().into(),
1468 bls_messages: vec![],
1469 secp_messages: vec![],
1470 }])
1471 .unwrap();
1472 state_machine.update(SyncEvent::NewFullTipsets(vec![full_tipset]));
1473 }
1474
1475 let chains = state_machine
1476 .chains()
1477 .into_iter()
1478 .map(|v| {
1479 v.into_iter()
1480 .map(|ts| ts.weight().to_i64().unwrap_or(0))
1481 .collect_vec()
1482 })
1483 .collect_vec();
1484
1485 assert_eq!(chains, vec![vec![1, 3], vec![1, 2]]);
1487 }
1488
1489 fn single_block_tipset(header: CachingBlockHeader) -> FullTipset {
1490 FullTipset::new(vec![Block {
1491 header,
1492 bls_messages: vec![],
1493 secp_messages: vec![],
1494 }])
1495 .unwrap()
1496 }
1497
1498 #[test]
1499 fn notify_rejected_reports_only_bad_block_cache_members() {
1500 let (cs, c4u) = setup();
1501 let db = cs.db_owned();
1502 chain4u! { from [genesis_header] in c4u; [a = dummy_node(&db, 1)] };
1503
1504 let bad_blocks = BadBlockCache::default();
1505 let state_machine =
1506 SyncStateMachine::new(cs.shallow_clone(), Some(bad_blocks.shallow_clone()), true);
1507 let mut rx = state_machine.block_validation_tx.subscribe();
1508
1509 let tipset = single_block_tipset(a.clone().into());
1510 let block_cid = *tipset.blocks().first().cid();
1511
1512 state_machine.notify_rejected(&tipset);
1514 assert!(rx.try_recv().is_err());
1515
1516 bad_blocks.push(block_cid);
1518 state_machine.notify_rejected(&tipset);
1519 assert_eq!(
1520 rx.try_recv().unwrap(),
1521 (block_cid, BlockValidationOutcome::Rejected)
1522 );
1523 }
1524
1525 #[test]
1526 fn validated_tipset_reports_applied() {
1527 let (cs, c4u) = setup();
1528 let db = cs.db_owned();
1529 chain4u! { from [genesis_header] in c4u; [a = dummy_node(&db, 1)] };
1530
1531 let mut state_machine = SyncStateMachine::new(cs.shallow_clone(), Default::default(), true);
1532 let mut rx = state_machine.block_validation_tx.subscribe();
1533
1534 let tipset = single_block_tipset(a.clone().into());
1535 let block_cid = *tipset.blocks().first().cid();
1536
1537 state_machine.update(SyncEvent::ValidatedTipset {
1538 tipset,
1539 is_proposed_head: false,
1540 });
1541 assert_eq!(
1542 rx.try_recv().unwrap(),
1543 (block_cid, BlockValidationOutcome::Applied)
1544 );
1545 }
1546}