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};
37use arc_swap::ArcSwap;
38use chrono::Utc;
39use hashbrown::{HashMap, HashSet};
40use libp2p::PeerId;
41use parking_lot::Mutex;
42use std::time::{Duration, Instant};
43use tokio::{sync::Notify, task::JoinSet};
44use tokio_util::sync::CancellationToken;
45
46pub struct ChainFollower {
47 tasks: Arc<Mutex<HashSet<SyncTask>>>,
49
50 state_machine: Arc<Mutex<SyncStateMachine>>,
52
53 pub sync_status: SyncStatus,
55
56 pub state_manager: StateManager,
58
59 pub network: SyncNetworkContext,
61
62 genesis: Tipset,
64
65 pub bad_blocks: Option<BadBlockCache>,
69
70 net_handler: Arc<flume::Receiver<NetworkEvent>>,
72
73 pub tipset_sender: flume::Sender<FullTipset>,
75
76 tipset_receiver: Arc<flume::Receiver<FullTipset>>,
78
79 stateless_mode: bool,
84
85 mem_pool: MessagePool<ChainStore>,
87}
88
89impl ShallowClone for ChainFollower {
90 fn shallow_clone(&self) -> Self {
91 Self {
92 tasks: self.tasks.shallow_clone(),
93 state_machine: self.state_machine.shallow_clone(),
94 sync_status: self.sync_status.shallow_clone(),
95 state_manager: self.state_manager.shallow_clone(),
96 network: self.network.shallow_clone(),
97 genesis: self.genesis.shallow_clone(),
98 bad_blocks: self.bad_blocks.shallow_clone(),
99 net_handler: self.net_handler.shallow_clone(),
100 tipset_sender: self.tipset_sender.clone(),
101 tipset_receiver: self.tipset_receiver.shallow_clone(),
102 stateless_mode: self.stateless_mode,
103 mem_pool: self.mem_pool.shallow_clone(),
104 }
105 }
106}
107
108impl ChainFollower {
109 pub fn new(
110 state_manager: StateManager,
111 network: SyncNetworkContext,
112 genesis: Tipset,
113 net_handler: flume::Receiver<NetworkEvent>,
114 stateless_mode: bool,
115 mem_pool: MessagePool<ChainStore>,
116 ) -> Self {
117 crate::def_is_env_truthy!(cache_disabled, "FOREST_DISABLE_BAD_BLOCK_CACHE");
118 let (tipset_sender, tipset_receiver) = flume::bounded(20);
119 let tasks: Arc<Mutex<HashSet<SyncTask>>> = Arc::new(Mutex::new(HashSet::default()));
120 let bad_blocks = if cache_disabled() {
121 tracing::warn!("bad block cache is disabled by `FOREST_DISABLE_BAD_BLOCK_CACHE`");
122 None
123 } else {
124 Some(Default::default())
125 };
126 let state_machine = Arc::new(Mutex::new(SyncStateMachine::new(
127 state_manager.chain_store().shallow_clone(),
128 bad_blocks.shallow_clone(),
129 stateless_mode,
130 )));
131
132 crate::metrics::register_collector(Box::new(SyncTasks(tasks.shallow_clone())));
133 crate::metrics::register_collector(Box::new(SyncStateMachineWrapper(
134 state_machine.shallow_clone(),
135 )));
136
137 Self {
138 tasks,
139 state_machine,
140 sync_status: Arc::new(ArcSwap::from_pointee(SyncStatusReport::init())),
141 state_manager,
142 network,
143 genesis,
144 bad_blocks,
145 net_handler: net_handler.into(),
146 tipset_sender,
147 tipset_receiver: tipset_receiver.into(),
148 stateless_mode,
149 mem_pool,
150 }
151 }
152
153 pub fn reset(&self) {
155 let start = Instant::now();
156 self.tasks.lock().clear();
157 self.state_manager.chain_store().validated_blocks.clear();
158 self.state_machine.lock().tipsets.clear();
159 if let Some(bad_blocks) = &self.bad_blocks {
160 bad_blocks.clear();
161 }
162 tracing::info!(
163 "chain follower reset, took {}",
164 humantime::format_duration(start.elapsed())
165 );
166 }
167
168 pub async fn run(&self) -> anyhow::Result<()> {
169 chain_follower(
170 &self.tasks,
171 &self.state_machine,
172 &self.state_manager,
173 self.bad_blocks.shallow_clone(),
174 self.net_handler.shallow_clone(),
175 self.tipset_receiver.shallow_clone(),
176 &self.network,
177 &self.mem_pool,
178 &self.sync_status,
179 &self.genesis,
180 self.stateless_mode,
181 )
182 .await
183 }
184
185 pub fn subscribe_validated_tipset(&self) -> tokio::sync::broadcast::Receiver<TipsetKey> {
187 self.state_machine
188 .lock()
189 .validated_tipset_broadcast_tx
190 .subscribe()
191 }
192}
193
194#[allow(clippy::too_many_arguments)]
195async fn chain_follower(
197 tasks: &Arc<Mutex<HashSet<SyncTask>>>,
198 state_machine: &Arc<Mutex<SyncStateMachine>>,
199 state_manager: &StateManager,
200 bad_block_cache: Option<BadBlockCache>,
201 network_rx: Arc<flume::Receiver<NetworkEvent>>,
202 tipset_receiver: Arc<flume::Receiver<FullTipset>>,
203 network: &SyncNetworkContext,
204 mem_pool: &MessagePool<ChainStore>,
205 sync_status: &SyncStatus,
206 genesis: &Tipset,
207 stateless_mode: bool,
208) -> anyhow::Result<()> {
209 let state_changed = Arc::new(Notify::new());
210
211 let seen_block_cache = SeenBlockCache::default();
212
213 let mut set = JoinSet::new();
214 let cancellation_token = CancellationToken::new();
215 let _cancellation_token_drop_guard = cancellation_token.drop_guard_ref();
216
217 set.spawn({
219 let state_manager = state_manager.shallow_clone();
220 let state_changed = state_changed.shallow_clone();
221 let state_machine = state_machine.shallow_clone();
222 let network = network.shallow_clone();
223 let mem_pool = mem_pool.shallow_clone();
224 let genesis = genesis.shallow_clone();
225 let bad_block_cache = bad_block_cache.shallow_clone();
226 let seen_block_cache = seen_block_cache.shallow_clone();
227 let cancellation_token = cancellation_token.clone();
228 async move {
229 while let Ok(event) = network_rx.recv_async().await {
230 inc_gossipsub_event_metrics(&event);
231
232 update_peer_info(
233 &event,
234 &network,
235 state_manager.chain_store().shallow_clone(),
236 &genesis,
237 cancellation_token.clone(),
238 );
239
240 let Ok(tipset) = (match event {
241 NetworkEvent::HelloResponseOutbound { request, source } => {
242 let tipset_keys = TipsetKey::from(request.heaviest_tip_set.clone());
243 get_full_tipset(
244 &network,
245 state_manager.chain_store(),
246 Some(source),
247 &tipset_keys,
248 )
249 .await
250 .inspect_err(|e| debug!("Querying full tipset failed: {e}"))
251 }
252 NetworkEvent::PubsubMessage { message } => match message {
253 PubsubMessage::Block(b) => {
254 let cs = state_manager.chain_store();
255 let cfg = cs.chain_config();
256 if let Err(reason) = GossipBlockValidator::new(&b).validate_pre_fetch(
257 &genesis,
258 cfg.block_delay_secs,
259 cs.ec_calculator_finalized_epoch(), bad_block_cache.as_ref(),
261 &seen_block_cache,
262 ) {
263 metrics::GOSSIP_BLOCK_REJECTED_TOTAL
264 .get_or_create(&metrics::GossipRejectReasonLabel {
265 reason: reason.label(),
266 })
267 .inc();
268 debug!("Rejected gossip block {}: {reason}", b.header.cid());
269 continue;
270 }
271 let key = TipsetKey::from(nunny::vec![*b.header.cid()]);
272 get_full_tipset(&network, cs, None, &key).await
273 }
274 PubsubMessage::Message(m) => {
275 if let Err(why) = mem_pool.add(m).await {
276 debug!("Received invalid GossipSub message: {}", why);
277 }
278 continue;
279 }
280 },
281 _ => continue,
282 }) else {
283 continue;
284 };
285 {
286 state_machine
287 .lock()
288 .update(SyncEvent::NewFullTipsets(vec![tipset]));
289 state_changed.notify_one();
290 }
291 }
292 }
293 });
294
295 set.spawn({
297 let state_changed = state_changed.clone();
298 let state_machine = state_machine.clone();
299
300 async move {
301 while let Ok(tipset) = tipset_receiver.recv_async().await {
302 state_machine
303 .lock()
304 .update(SyncEvent::NewFullTipsets(vec![tipset]));
305 state_changed.notify_one();
306 }
307 }
308 });
309
310 set.spawn({
312 let state_manager = state_manager.shallow_clone();
313 let state_machine = state_machine.shallow_clone();
314 let network = network.shallow_clone();
315 let sync_status = sync_status.shallow_clone();
316 let state_changed = state_changed.shallow_clone();
317 let tasks = tasks.shallow_clone();
318 let bad_block_cache = bad_block_cache.shallow_clone();
319 let cancellation_token = cancellation_token.clone();
320 async move {
321 const FORK_CLEANUP_INTERVAL: Duration = Duration::from_mins(1);
322 let mut last_fork_cleanup = Instant::now();
323 loop {
324 state_changed.notified().await;
325
326 let mut tasks_set = tasks.lock();
327 if last_fork_cleanup + FORK_CLEANUP_INTERVAL < Instant::now() {
328 state_machine.lock().cleanup_dangling_forks();
329 last_fork_cleanup = Instant::now();
330 }
331 let (task_vec, current_active_forks) = state_machine.lock().tasks();
332
333 {
335 let old_status_report = sync_status.load().shallow_clone();
336 let new_status_report = old_status_report.update(
337 &state_manager,
338 current_active_forks,
339 stateless_mode,
340 );
341 sync_status.store(new_status_report.into());
342 }
343
344 for task in task_vec {
345 let new = tasks_set.insert(task.clone());
347 if new {
348 let action = task.clone().execute(
349 network.shallow_clone(),
350 state_manager.shallow_clone(),
351 stateless_mode,
352 bad_block_cache.shallow_clone(),
353 );
354 tokio::spawn({
355 let tasks = tasks.shallow_clone();
356 let state_machine = state_machine.shallow_clone();
357 let state_changed = state_changed.shallow_clone();
358 let cancellation_token = cancellation_token.clone();
359 async move {
360 cancellation_token
361 .run_until_cancelled(async move {
362 if let Some(event) = action.await {
363 state_machine.lock().update(event);
364 state_changed.notify_one();
365 }
366 let mut tasks = tasks.lock();
367 tasks.remove(&task);
368 tasks.shrink_to_fit();
369 })
370 .await
371 }
372 });
373 }
374 }
375 }
376 }
377 });
378
379 set.spawn({
383 let state_manager = state_manager.shallow_clone();
384 let state_machine = state_machine.clone();
385 async move {
386 loop {
387 tokio::time::sleep(tokio::time::Duration::from_secs(10)).await;
388 let (tasks_set, _) = state_machine.lock().tasks();
389 let heaviest_tipset = state_manager.chain_store().heaviest_tipset();
390 let heaviest_epoch = heaviest_tipset.epoch();
391
392 let to_download = tasks_set
393 .iter()
394 .filter_map(|task| match task {
395 SyncTask::FetchTipset(_, epoch) => Some(epoch - heaviest_epoch),
396 _ => None,
397 })
398 .max()
399 .unwrap_or(0);
400
401 let expected_head = calculate_expected_epoch(
402 Utc::now().timestamp() as u64,
403 state_manager.chain_store().genesis_block_header().timestamp,
404 state_manager.chain_config().block_delay_secs,
405 );
406
407 match (expected_head - heaviest_epoch > 10, to_download > 0) {
410 (true, true) => info!(
411 "Catching up to HEAD: {heaviest_epoch}{} -> {expected_head} (diff: {}), downloading {to_download} tipsets"
412 , heaviest_tipset.key(),
413 expected_head - heaviest_epoch
414 ),
415 (true, false) => info!(
416 "Catching up to HEAD: {heaviest_epoch}{} -> {expected_head} (diff: {})"
417 ,heaviest_tipset.key(),
418 expected_head - heaviest_epoch
419 ),
420 (false, true) => {
421 info!("Downloading {to_download} tipsets")
422 }
423 (false, false) => {}
424 }
425 }
426 }
427 });
428
429 set.join_all().await;
430 Ok(())
431}
432
433fn inc_gossipsub_event_metrics(event: &NetworkEvent) {
435 let label = match event {
436 NetworkEvent::HelloRequestInbound => metrics::values::HELLO_REQUEST_INBOUND,
437 NetworkEvent::HelloResponseOutbound { .. } => metrics::values::HELLO_RESPONSE_OUTBOUND,
438 NetworkEvent::HelloRequestOutbound => metrics::values::HELLO_REQUEST_OUTBOUND,
439 NetworkEvent::HelloResponseInbound => metrics::values::HELLO_RESPONSE_INBOUND,
440 NetworkEvent::PeerConnected(_) => metrics::values::PEER_CONNECTED,
441 NetworkEvent::PeerDisconnected(_) => metrics::values::PEER_DISCONNECTED,
442 NetworkEvent::PubsubMessage { message } => match message {
443 PubsubMessage::Block(_) => metrics::values::PUBSUB_BLOCK,
444 PubsubMessage::Message(_) => metrics::values::PUBSUB_MESSAGE,
445 },
446 NetworkEvent::ChainExchangeRequestOutbound => {
447 metrics::values::CHAIN_EXCHANGE_REQUEST_OUTBOUND
448 }
449 NetworkEvent::ChainExchangeResponseInbound => {
450 metrics::values::CHAIN_EXCHANGE_RESPONSE_INBOUND
451 }
452 NetworkEvent::ChainExchangeRequestInbound => {
453 metrics::values::CHAIN_EXCHANGE_REQUEST_INBOUND
454 }
455 NetworkEvent::ChainExchangeResponseOutbound => {
456 metrics::values::CHAIN_EXCHANGE_RESPONSE_OUTBOUND
457 }
458 };
459
460 metrics::LIBP2P_MESSAGE_TOTAL.get_or_create(&label).inc();
461}
462
463fn update_peer_info(
465 event: &NetworkEvent,
466 network: &SyncNetworkContext,
467 chain_store: ChainStore,
468 genesis: &Tipset,
469 cancellation_token: CancellationToken,
470) {
471 match event {
472 NetworkEvent::PeerConnected(peer_id) => {
473 let peer_id = *peer_id;
474 let genesis_cid = *genesis.block_headers().first().cid();
475 let network = network.shallow_clone();
476 tokio::task::spawn(async move {
478 cancellation_token
479 .run_until_cancelled(handle_peer_connected_event(
480 network,
481 chain_store,
482 peer_id,
483 genesis_cid,
484 ))
485 .await
486 });
487 }
488 NetworkEvent::PeerDisconnected(peer_id) => {
489 handle_peer_disconnected_event(network, *peer_id);
490 }
491 _ => {}
492 }
493}
494
495async fn handle_peer_connected_event(
496 network: SyncNetworkContext,
497 chain_store: ChainStore,
498 peer_id: PeerId,
499 genesis_block_cid: Cid,
500) {
501 if network.peer_manager().is_peer_new(&peer_id) {
503 let heaviest = chain_store.heaviest_tipset();
506 let request = HelloRequest {
507 heaviest_tip_set: heaviest.cids(),
508 heaviest_tipset_height: heaviest.epoch(),
509 heaviest_tipset_weight: heaviest.weight().clone().into(),
510 genesis_cid: genesis_block_cid,
511 };
512 let (peer_id, moment_sent, response) = match network.hello_request(peer_id, request).await {
513 Ok(response) => response,
514 Err(e) => {
515 debug!("Hello request failed: {}", e);
516 return;
517 }
518 };
519 let dur = Instant::now().duration_since(moment_sent);
520
521 match response {
523 Some(_) => {
524 network.peer_manager().log_success(&peer_id, dur);
525 }
526 None => {
527 network.peer_manager().log_failure(&peer_id, dur);
528 }
529 }
530 }
531}
532
533fn handle_peer_disconnected_event(network: &SyncNetworkContext, peer_id: PeerId) {
534 network.peer_manager().remove_peer(&peer_id);
535 network.peer_manager().unmark_peer_bad(&peer_id);
536}
537
538pub async fn get_full_tipset(
539 network: &SyncNetworkContext,
540 chain_store: &ChainStore,
541 peer_id: Option<PeerId>,
542 tipset_keys: &TipsetKey,
543) -> anyhow::Result<FullTipset> {
544 if let Ok(full_tipset) = load_full_tipset(chain_store, tipset_keys) {
546 return Ok(full_tipset);
547 }
548 let tipset = network
550 .chain_exchange_full_tipset(peer_id, tipset_keys)
551 .await
552 .map_err(|e| anyhow::anyhow!(e))?;
553 tipset.persist(chain_store.db())?;
554
555 Ok(tipset)
556}
557
558async fn get_full_tipset_batch(
559 network: &SyncNetworkContext,
560 chain_store: &ChainStore,
561 peer_id: Option<PeerId>,
562 tipset_keys: &TipsetKey,
563) -> anyhow::Result<Vec<FullTipset>> {
564 if let Ok(full_tipset) = load_full_tipset(chain_store, tipset_keys) {
566 return Ok(vec![full_tipset]);
567 }
568 let tipsets = network
570 .chain_exchange_full_tipsets(peer_id, tipset_keys)
571 .await
572 .map_err(|e| anyhow::anyhow!(e))?;
573
574 for tipset in tipsets.iter() {
575 tipset.persist(chain_store.db())?;
576 }
577
578 Ok(tipsets)
579}
580
581pub fn load_full_tipset(
582 chain_store: &ChainStore,
583 tipset_keys: &TipsetKey,
584) -> anyhow::Result<FullTipset> {
585 let ts = chain_store
587 .chain_index()
588 .load_required_tipset(tipset_keys)?;
589 let blocks: Vec<_> = ts
590 .block_headers()
591 .iter()
592 .map(|header| -> anyhow::Result<Block> {
593 let (bls_msgs, secp_msgs) = crate::chain::block_messages(chain_store.db(), header)?;
594 Ok(Block {
595 header: header.clone(),
596 bls_messages: bls_msgs,
597 secp_messages: secp_msgs,
598 })
599 })
600 .try_collect()?;
601 let fts = FullTipset::new(blocks)?;
603 Ok(fts)
604}
605
606enum SyncEvent {
607 NewFullTipsets(Vec<FullTipset>),
608 BadTipset(FullTipset),
609 ValidatedTipset {
610 tipset: FullTipset,
611 is_proposed_head: bool,
612 },
613}
614
615impl std::fmt::Display for SyncEvent {
616 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
617 fn tss_to_string(tss: &[FullTipset]) -> String {
618 format!(
619 "epoch: {}-{}",
620 tss.first().map(|ts| ts.epoch()).unwrap_or_default(),
621 tss.last().map(|ts| ts.epoch()).unwrap_or_default()
622 )
623 }
624
625 match self {
626 Self::NewFullTipsets(tss) => write!(f, "NewFullTipsets({})", tss_to_string(tss)),
627 Self::BadTipset(ts) => {
628 write!(f, "BadTipset(epoch: {}, key: {})", ts.epoch(), ts.key())
629 }
630 Self::ValidatedTipset {
631 tipset,
632 is_proposed_head,
633 } => write!(
634 f,
635 "ValidatedTipset(epoch: {}, key: {}, is_proposed_head: {is_proposed_head})",
636 tipset.epoch(),
637 tipset.key()
638 ),
639 }
640 }
641}
642
643#[derive(derive_more::Debug)]
644struct SyncStateMachine {
645 #[debug(skip)]
646 cs: ChainStore,
647 bad_block_cache: Option<BadBlockCache>,
648 tipsets: HashMap<TipsetKey, FullTipset>,
650 stateless_mode: bool,
651 validated_tipset_broadcast_tx: tokio::sync::broadcast::Sender<TipsetKey>,
653}
654
655impl SyncStateMachine {
656 pub fn new(
657 cs: ChainStore,
658 bad_block_cache: Option<BadBlockCache>,
659 stateless_mode: bool,
660 ) -> Self {
661 Self {
662 cs,
663 bad_block_cache,
664 tipsets: HashMap::default(),
665 stateless_mode,
666 validated_tipset_broadcast_tx: tokio::sync::broadcast::Sender::new(1024),
667 }
668 }
669
670 fn chains(&self) -> Vec<Vec<FullTipset>> {
672 let mut chains = Vec::new();
673 let mut remaining_tipsets = self.tipsets.clone();
674
675 while let Some(heaviest) = remaining_tipsets
676 .values()
677 .max_by_key(|ts| ts.weight())
678 .cloned()
679 {
680 let mut chain = Vec::new();
682 let mut current = Some(heaviest);
683
684 while let Some(tipset) = current.take() {
685 remaining_tipsets.remove(tipset.key());
686
687 current = self.tipsets.get(tipset.parents()).cloned();
689
690 chain.push(tipset);
691 }
692 chain.reverse();
693 chains.push(chain);
694 }
695
696 chains
697 }
698
699 fn is_parent_validated(&self, tipset: &FullTipset) -> bool {
700 let db = self.cs.db();
701 self.stateless_mode || db.has(tipset.parent_state()).unwrap_or(false)
702 }
703
704 fn is_ready_for_validation(&self, tipset: &FullTipset) -> bool {
705 if self.stateless_mode || tipset.key() == self.cs.genesis_tipset().key() {
706 true
708 } else if let Ok(parent_ts) = load_full_tipset(&self.cs, tipset.parents()) {
709 let head_ts = self.cs.heaviest_tipset();
710 if parent_ts.key() == head_ts.key() {
716 true
717 } else if parent_ts.epoch() >= head_ts.epoch() {
718 false
719 } else {
720 self.is_parent_validated(tipset)
721 }
722 } else {
723 false
724 }
725 }
726
727 fn add_full_tipset(&mut self, tipset: FullTipset) {
728 if let Err(why) = TipsetValidator(&tipset).validate(
729 &self.cs,
730 self.bad_block_cache.as_ref(),
731 &self.cs.genesis_tipset(),
732 self.cs.chain_config().block_delay_secs,
733 ) {
734 metrics::INVALID_TIPSET_TOTAL.inc();
735 trace!("Skipping invalid tipset: {}", why);
736 self.mark_bad_tipset(tipset);
737 return;
738 }
739
740 if tipset.epoch() < self.cs.ec_calculator_finalized_epoch() {
743 self.mark_bad_tipset(tipset);
744 return;
745 }
746
747 if self.tipsets.contains_key(tipset.key()) {
749 return;
750 }
751
752 if let Ok(Some(ts)) = self.cs.chain_index().tipset_by_height_blocking(
754 tipset.epoch(),
755 self.cs.heaviest_tipset(),
756 ResolveNullTipset::TakeOlder,
757 ) && ts.key() == tipset.key()
758 {
759 return;
760 }
761
762 let mut to_remove = Vec::new();
764 #[allow(clippy::mutable_key_type)]
765 let mut merged_blocks: HashSet<_> = tipset.blocks().iter().cloned().collect();
766
767 let parent_refs: HashSet<_> = self
769 .tipsets
770 .values()
771 .map(|ts| ts.parents().clone())
772 .collect();
773
774 for (key, existing_ts) in self.tipsets.iter() {
775 if existing_ts.epoch() == tipset.epoch() && existing_ts.parents() == tipset.parents() {
776 if !parent_refs.contains(key) {
778 to_remove.push(key.clone());
779 }
780 merged_blocks.extend(existing_ts.blocks().iter().cloned());
782 }
783 }
784
785 for key in to_remove {
787 self.tipsets.remove(&key);
788 }
789
790 if let Ok(merged_tipset) = FullTipset::new(merged_blocks) {
792 self.tipsets
793 .insert(merged_tipset.key().clone(), merged_tipset);
794 }
795
796 self.tipsets.shrink_to_fit();
797 }
798
799 fn mark_bad_tipset(&mut self, tipset: FullTipset) {
803 let mut stack = vec![tipset];
804 while let Some(tipset) = stack.pop() {
805 self.tipsets.remove(tipset.key());
806 let mut to_remove = Vec::new();
808 let mut descendants = Vec::new();
809
810 for (key, ts) in self.tipsets.iter() {
811 if ts.parents() == tipset.key() {
812 to_remove.push(key.clone());
813 descendants.push(ts.clone());
814 }
815 }
816
817 for key in to_remove {
819 self.tipsets.remove(&key);
820 }
821
822 stack.extend(descendants);
824 }
825 }
826
827 fn try_mark_tipset_as_validated(&mut self, tipset: FullTipset, is_proposed_head: bool) -> bool {
828 if !self.is_parent_validated(&tipset) {
829 tracing::error!(epoch = %tipset.epoch(), tsk = %tipset.key(), parent_state = %tipset.parent_state(), "Parent tipset must be validated");
830 return false;
831 }
832
833 self.tipsets.remove(tipset.key());
834 let tipset = tipset.into_tipset();
835 if self.stateless_mode {
837 let epoch = tipset.epoch();
838 let terse_key = tipset.key().terse();
839 if self.cs.heaviest_tipset().weight() < tipset.weight() {
840 if let Err(e) = self.cs.set_heaviest_tipset(tipset) {
841 error!("Error setting heaviest tipset: {}", e);
842 return false;
843 } else {
844 info!("Heaviest tipset: {} ({})", epoch, terse_key);
845 }
846 }
847 } else if is_proposed_head {
848 if let Err(e) = self.cs.maybe_update_pending_head(&tipset) {
849 error!("Error putting tipset: {e}");
850 return false;
851 }
852 } else if let Err(e) = self.cs.set_heaviest_tipset(tipset) {
853 error!("Error setting heaviest tipset: {e}");
854 return false;
855 }
856 true
857 }
858
859 pub fn update(&mut self, event: SyncEvent) {
860 tracing::trace!("update: {event}");
861 match event {
862 SyncEvent::NewFullTipsets(tipsets) => {
863 for tipset in tipsets {
864 self.add_full_tipset(tipset);
865 }
866 }
867 SyncEvent::BadTipset(tipset) => self.mark_bad_tipset(tipset),
868 SyncEvent::ValidatedTipset {
869 tipset,
870 is_proposed_head,
871 } => {
872 if self.try_mark_tipset_as_validated(tipset, is_proposed_head)
873 && crate::utils::broadcast::has_subscribers(&self.validated_tipset_broadcast_tx)
874 && let Err(e) = self.validated_tipset_broadcast_tx.send(self.cs.heaviest_tipset().key().clone())
876 {
877 warn!("Failed to broadcast validated tipset: {e}");
878 }
879 }
880 }
881 }
882
883 pub fn tasks(&self) -> (Vec<SyncTask>, Vec<ForkSyncInfo>) {
884 let current_validated_epoch = self.cs.heaviest_tipset().epoch();
886 let now = Utc::now();
887
888 let mut active_sync_info = Vec::new();
889 let mut tasks = Vec::new();
890 for chain in self.chains() {
891 if let Some(first_ts) = chain.first() {
892 let last_ts = chain.last().expect("Infallible");
893 let stage: ForkSyncStage;
894 let start_time = Some(now);
895
896 if !self.is_ready_for_validation(first_ts) {
897 stage = ForkSyncStage::FetchingHeaders;
898 tasks.push(SyncTask::FetchTipset(
899 first_ts.parents().clone(),
900 first_ts.epoch(),
901 ));
902 } else {
903 stage = ForkSyncStage::ValidatingTipsets;
904 tasks.push(SyncTask::ValidateTipset {
905 tipset: first_ts.clone(),
906 is_proposed_head: chain.len() == 1,
907 });
908 }
909
910 let fork_info = ForkSyncInfo {
911 target_tipset_key: last_ts.key().clone(),
912 target_epoch: last_ts.epoch(),
913 target_sync_epoch_start: first_ts.epoch(),
914 stage,
915 validated_chain_head_epoch: current_validated_epoch,
916 start_time,
917 last_updated: Some(now),
918 };
919
920 active_sync_info.push(fork_info);
921 }
922 }
923 (tasks, active_sync_info)
924 }
925
926 pub fn cleanup_dangling_forks(&mut self) {
927 let finalized_epoch = self.cs.ec_calculator_finalized_epoch();
928 for chain in self.chains() {
929 if let Some(target) = chain.last()
931 && target.epoch() < finalized_epoch
932 {
933 chain.iter().for_each(|ts| {
934 self.tipsets.remove(ts.key());
935 });
936 tracing::info!(
937 "Cleaned up dangling fork from epoch {} to {}",
938 chain.first().map(|ts| ts.epoch()).unwrap_or_default(),
939 chain.last().map(|ts| ts.epoch()).unwrap_or_default(),
940 );
941 }
942 }
943 }
944}
945
946#[derive(PartialEq, Eq, Hash, Clone, Debug)]
947enum SyncTask {
948 ValidateTipset {
949 tipset: FullTipset,
950 is_proposed_head: bool,
951 },
952 FetchTipset(TipsetKey, ChainEpoch),
953}
954
955impl std::fmt::Display for SyncTask {
956 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
957 match self {
958 SyncTask::ValidateTipset {
959 tipset,
960 is_proposed_head,
961 } => write!(
962 f,
963 "ValidateTipset(epoch: {}, is_proposed_head: {is_proposed_head})",
964 tipset.epoch()
965 ),
966 SyncTask::FetchTipset(key, epoch) => {
967 let s = key.to_string();
968 write!(
969 f,
970 "FetchTipset({}, epoch: {})",
971 &s[s.len().saturating_sub(8)..],
972 epoch
973 )
974 }
975 }
976 }
977}
978
979impl SyncTask {
980 async fn execute(
981 self,
982 network: SyncNetworkContext,
983 state_manager: StateManager,
984 stateless_mode: bool,
985 bad_block_cache: Option<BadBlockCache>,
986 ) -> Option<SyncEvent> {
987 tracing::trace!("SyncTask::execute {self}");
988 match self {
989 SyncTask::ValidateTipset {
990 tipset,
991 is_proposed_head,
992 } if stateless_mode => Some(SyncEvent::ValidatedTipset {
993 tipset,
994 is_proposed_head,
995 }),
996 SyncTask::ValidateTipset {
997 tipset,
998 is_proposed_head,
999 } => match validate_tipset(&state_manager, tipset.clone(), bad_block_cache).await {
1000 Ok(()) => Some(SyncEvent::ValidatedTipset {
1001 tipset,
1002 is_proposed_head,
1003 }),
1004 Err(e) if matches!(e, TipsetSyncerError::TimeTravellingBlock { .. }) => {
1009 warn!("Time travelling block detected, skipping tipset for now: {e}");
1010 None
1011 }
1012 Err(e) if matches!(e, TipsetSyncerError::ParentChainStateMismatch(_)) => {
1015 warn!("Error validating tipset: {e}");
1016 let sm = state_manager.shallow_clone();
1017 match tokio::task::spawn_blocking(move || sm.repair_tipset_lookup())
1018 .await
1019 .unwrap_or_else(|e| Err(e.into()))
1020 {
1021 Ok(0) => Some(SyncEvent::BadTipset(tipset)),
1023 Ok(repaired) => {
1024 warn!(
1025 "Repaired {repaired} tipset lookup entries; retrying tipset validation at epoch {}",
1026 tipset.epoch()
1027 );
1028 None
1029 }
1030 Err(e) => {
1033 warn!("Failed to repair tipset lookup table: {e:#}");
1034 None
1035 }
1036 }
1037 }
1038 Err(e) => {
1039 warn!("Error validating tipset: {e}");
1040 Some(SyncEvent::BadTipset(tipset))
1041 }
1042 },
1043 SyncTask::FetchTipset(key, epoch) => {
1044 match get_full_tipset_batch(&network, state_manager.chain_store(), None, &key).await
1045 {
1046 Ok(parents) => Some(SyncEvent::NewFullTipsets(parents)),
1047 Err(e) => {
1048 tracing::debug!(%key, %epoch, "failed to fetch tipset: {e:#}");
1050 None
1051 }
1052 }
1053 }
1054 }
1055 }
1056}
1057
1058#[derive(Debug)]
1059struct SyncTasks(Arc<Mutex<HashSet<SyncTask>>>);
1060
1061#[derive(Debug)]
1062struct SyncStateMachineWrapper(Arc<Mutex<SyncStateMachine>>);
1063
1064mod metrics_collection {
1065 use super::*;
1066 use prometheus_client::{
1067 collector::Collector,
1068 encoding::{DescriptorEncoder, EncodeMetric},
1069 metrics::gauge::Gauge,
1070 registry::Unit,
1071 };
1072
1073 impl Collector for SyncTasks {
1074 fn encode(&self, mut encoder: DescriptorEncoder) -> Result<(), std::fmt::Error> {
1075 {
1076 let size_in_bytes = {
1077 let g: Gauge = Default::default();
1078 g.set(self.0.lock().allocation_size() as i64);
1079 g
1080 };
1081 let size_metric_encoder = encoder.encode_descriptor(
1082 "chain_follower_tasks_size",
1083 "Size of the chain follower tasks in bytes",
1084 Some(&Unit::Bytes),
1085 size_in_bytes.metric_type(),
1086 )?;
1087 size_in_bytes.encode(size_metric_encoder)?;
1088 }
1089 {
1090 let len = {
1091 let g: Gauge = Default::default();
1092 g.set(self.0.lock().len() as i64);
1093 g
1094 };
1095 let size_metric_encoder = encoder.encode_descriptor(
1096 "chain_follower_tasks_len",
1097 "Length of the chain follower tasks",
1098 None,
1099 len.metric_type(),
1100 )?;
1101 len.encode(size_metric_encoder)?;
1102 }
1103 {
1104 let cap = {
1105 let g: Gauge = Default::default();
1106 g.set(self.0.lock().capacity() as i64);
1107 g
1108 };
1109 let size_metric_encoder = encoder.encode_descriptor(
1110 "chain_follower_tasks_cap",
1111 "Capacity of the chain follower tasks",
1112 None,
1113 cap.metric_type(),
1114 )?;
1115 cap.encode(size_metric_encoder)?;
1116 }
1117
1118 Ok(())
1119 }
1120 }
1121
1122 impl Collector for SyncStateMachineWrapper {
1123 fn encode(&self, mut encoder: DescriptorEncoder) -> Result<(), std::fmt::Error> {
1124 {
1125 let size_in_bytes = {
1126 let g: Gauge = Default::default();
1127 g.set(self.0.lock().tipsets.allocation_size() as i64);
1128 g
1129 };
1130 let size_metric_encoder = encoder.encode_descriptor(
1131 "chain_follower_tipsets_size",
1132 "Size of the chain follower tipsets in bytes",
1133 Some(&Unit::Bytes),
1134 size_in_bytes.metric_type(),
1135 )?;
1136 size_in_bytes.encode(size_metric_encoder)?;
1137 }
1138 {
1139 let len = {
1140 let g: Gauge = Default::default();
1141 g.set(self.0.lock().tipsets.len() as i64);
1142 g
1143 };
1144 let size_metric_encoder = encoder.encode_descriptor(
1145 "chain_follower_tipsets_len",
1146 "Length of the chain follower tipsets",
1147 None,
1148 len.metric_type(),
1149 )?;
1150 len.encode(size_metric_encoder)?;
1151 }
1152 {
1153 let cap = {
1154 let g: Gauge = Default::default();
1155 g.set(self.0.lock().tipsets.capacity() as i64);
1156 g
1157 };
1158 let size_metric_encoder = encoder.encode_descriptor(
1159 "chain_follower_tipsets_cap",
1160 "Capacity of the chain follower tipsets",
1161 None,
1162 cap.metric_type(),
1163 )?;
1164 cap.encode(size_metric_encoder)?;
1165 }
1166
1167 Ok(())
1168 }
1169 }
1170}
1171
1172#[cfg(test)]
1173mod tests {
1174 use super::*;
1175 use crate::blocks::{Chain4U, HeaderBuilder, chain4u};
1176 use crate::db::MemoryDB;
1177 use crate::utils::db::CborStoreExt as _;
1178 use num_bigint::BigInt;
1179 use num_traits::ToPrimitive;
1180 use std::sync::Arc;
1181 use tracing::level_filters::LevelFilter;
1182 use tracing_subscriber::EnvFilter;
1183
1184 fn setup() -> (ChainStore, Chain4U<Arc<MemoryDB>>) {
1185 let _ = tracing_subscriber::fmt()
1187 .without_time()
1188 .with_env_filter(
1189 EnvFilter::builder()
1190 .with_default_directive(LevelFilter::DEBUG.into())
1191 .from_env()
1192 .unwrap(),
1193 )
1194 .try_init();
1195
1196 let db = Arc::new(MemoryDB::default());
1197
1198 let c4u = Chain4U::with_blockstore(db.clone());
1200 chain4u! {
1201 in c4u;
1202 [genesis_header = dummy_node(&db, 0)]
1203 };
1204
1205 let cs = ChainStore::new(db, Default::default(), genesis_header).unwrap();
1206
1207 cs.set_heaviest_tipset(cs.genesis_tipset()).unwrap();
1208
1209 (cs, c4u)
1210 }
1211
1212 fn dummy_state(db: impl Blockstore, i: ChainEpoch) -> Cid {
1213 db.put_cbor_default(&i).unwrap()
1214 }
1215
1216 fn dummy_node(db: impl Blockstore, i: ChainEpoch) -> HeaderBuilder {
1217 HeaderBuilder {
1218 state_root: dummy_state(db, i).into(),
1219 weight: BigInt::from(i).into(),
1220 epoch: i.into(),
1221 ..Default::default()
1222 }
1223 }
1224
1225 #[test]
1226 fn test_state_machine_validation_order() {
1227 let (cs, c4u) = setup();
1228 let db = cs.db_owned();
1229
1230 chain4u! {
1231 from [genesis_header] in c4u;
1232 [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)]
1233 };
1234
1235 let mut state_machine = SyncStateMachine::new(cs.shallow_clone(), Default::default(), true);
1237
1238 let tipsets = vec![e, b, d, c, a];
1240
1241 for block in tipsets {
1243 let full_tipset = FullTipset::new(vec![Block {
1244 header: block.clone().into(),
1245 bls_messages: vec![],
1246 secp_messages: vec![],
1247 }])
1248 .unwrap();
1249 state_machine.update(SyncEvent::NewFullTipsets(vec![full_tipset]));
1250 }
1251
1252 let mut validation_tasks = Vec::new();
1254 loop {
1255 let (tasks, _) = state_machine.tasks();
1256
1257 let validation_tipsets: Vec<_> = tasks
1259 .into_iter()
1260 .filter_map(|task| {
1261 if let SyncTask::ValidateTipset {
1262 tipset,
1263 is_proposed_head,
1264 } = task
1265 {
1266 Some((tipset, is_proposed_head))
1267 } else {
1268 None
1269 }
1270 })
1271 .collect();
1272
1273 if validation_tipsets.is_empty() {
1274 break;
1275 }
1276
1277 for (ts, is_proposed_head) in validation_tipsets {
1279 validation_tasks.push(ts.epoch());
1280 db.put_cbor_default(&ts.epoch()).unwrap();
1281 state_machine.try_mark_tipset_as_validated(ts, is_proposed_head);
1282 }
1283 }
1284
1285 assert_eq!(validation_tasks, vec![1, 2, 3, 4, 5]);
1287 }
1288
1289 #[test]
1290 fn test_sync_state_machine_chain_fragments() {
1291 let (cs, c4u) = setup();
1292 let db = cs.db();
1293
1294 chain4u! {
1298 in c4u;
1299 [a = dummy_node(db, 1)] -> [b = dummy_node(db, 2)]
1300 };
1301 chain4u! {
1302 from [a] in c4u;
1303 [c = dummy_node(db, 3)]
1304 };
1305
1306 let mut state_machine = SyncStateMachine::new(cs, Default::default(), false);
1308
1309 for block in [a, b, c] {
1311 let full_tipset = FullTipset::new(vec![Block {
1312 header: block.clone().into(),
1313 bls_messages: vec![],
1314 secp_messages: vec![],
1315 }])
1316 .unwrap();
1317 state_machine.update(SyncEvent::NewFullTipsets(vec![full_tipset]));
1318 }
1319
1320 let chains = state_machine
1321 .chains()
1322 .into_iter()
1323 .map(|v| {
1324 v.into_iter()
1325 .map(|ts| ts.weight().to_i64().unwrap_or(0))
1326 .collect_vec()
1327 })
1328 .collect_vec();
1329
1330 assert_eq!(chains, vec![vec![1, 3], vec![1, 2]]);
1332 }
1333}