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}, downloading {to_download} tipsets"
412 , heaviest_tipset.key()
413 ),
414 (true, false) => info!(
415 "Catching up to HEAD: {heaviest_epoch}{} -> {expected_head}"
416 , heaviest_tipset.key()
417 ),
418 (false, true) => {
419 info!("Downloading {to_download} tipsets")
420 }
421 (false, false) => {}
422 }
423 }
424 }
425 });
426
427 set.join_all().await;
428 Ok(())
429}
430
431fn inc_gossipsub_event_metrics(event: &NetworkEvent) {
433 let label = match event {
434 NetworkEvent::HelloRequestInbound => metrics::values::HELLO_REQUEST_INBOUND,
435 NetworkEvent::HelloResponseOutbound { .. } => metrics::values::HELLO_RESPONSE_OUTBOUND,
436 NetworkEvent::HelloRequestOutbound => metrics::values::HELLO_REQUEST_OUTBOUND,
437 NetworkEvent::HelloResponseInbound => metrics::values::HELLO_RESPONSE_INBOUND,
438 NetworkEvent::PeerConnected(_) => metrics::values::PEER_CONNECTED,
439 NetworkEvent::PeerDisconnected(_) => metrics::values::PEER_DISCONNECTED,
440 NetworkEvent::PubsubMessage { message } => match message {
441 PubsubMessage::Block(_) => metrics::values::PUBSUB_BLOCK,
442 PubsubMessage::Message(_) => metrics::values::PUBSUB_MESSAGE,
443 },
444 NetworkEvent::ChainExchangeRequestOutbound => {
445 metrics::values::CHAIN_EXCHANGE_REQUEST_OUTBOUND
446 }
447 NetworkEvent::ChainExchangeResponseInbound => {
448 metrics::values::CHAIN_EXCHANGE_RESPONSE_INBOUND
449 }
450 NetworkEvent::ChainExchangeRequestInbound => {
451 metrics::values::CHAIN_EXCHANGE_REQUEST_INBOUND
452 }
453 NetworkEvent::ChainExchangeResponseOutbound => {
454 metrics::values::CHAIN_EXCHANGE_RESPONSE_OUTBOUND
455 }
456 };
457
458 metrics::LIBP2P_MESSAGE_TOTAL.get_or_create(&label).inc();
459}
460
461fn update_peer_info(
463 event: &NetworkEvent,
464 network: &SyncNetworkContext,
465 chain_store: ChainStore,
466 genesis: &Tipset,
467 cancellation_token: CancellationToken,
468) {
469 match event {
470 NetworkEvent::PeerConnected(peer_id) => {
471 let peer_id = *peer_id;
472 let genesis_cid = *genesis.block_headers().first().cid();
473 let network = network.shallow_clone();
474 tokio::task::spawn(async move {
476 cancellation_token
477 .run_until_cancelled(handle_peer_connected_event(
478 network,
479 chain_store,
480 peer_id,
481 genesis_cid,
482 ))
483 .await
484 });
485 }
486 NetworkEvent::PeerDisconnected(peer_id) => {
487 handle_peer_disconnected_event(network, *peer_id);
488 }
489 _ => {}
490 }
491}
492
493async fn handle_peer_connected_event(
494 network: SyncNetworkContext,
495 chain_store: ChainStore,
496 peer_id: PeerId,
497 genesis_block_cid: Cid,
498) {
499 if network.peer_manager().is_peer_new(&peer_id) {
501 let heaviest = chain_store.heaviest_tipset();
504 let request = HelloRequest {
505 heaviest_tip_set: heaviest.cids(),
506 heaviest_tipset_height: heaviest.epoch(),
507 heaviest_tipset_weight: heaviest.weight().clone().into(),
508 genesis_cid: genesis_block_cid,
509 };
510 let (peer_id, moment_sent, response) = match network.hello_request(peer_id, request).await {
511 Ok(response) => response,
512 Err(e) => {
513 debug!("Hello request failed: {}", e);
514 return;
515 }
516 };
517 let dur = Instant::now().duration_since(moment_sent);
518
519 match response {
521 Some(_) => {
522 network.peer_manager().log_success(&peer_id, dur);
523 }
524 None => {
525 network.peer_manager().log_failure(&peer_id, dur);
526 }
527 }
528 }
529}
530
531fn handle_peer_disconnected_event(network: &SyncNetworkContext, peer_id: PeerId) {
532 network.peer_manager().remove_peer(&peer_id);
533 network.peer_manager().unmark_peer_bad(&peer_id);
534}
535
536pub async fn get_full_tipset(
537 network: &SyncNetworkContext,
538 chain_store: &ChainStore,
539 peer_id: Option<PeerId>,
540 tipset_keys: &TipsetKey,
541) -> anyhow::Result<FullTipset> {
542 if let Ok(full_tipset) = load_full_tipset(chain_store, tipset_keys) {
544 return Ok(full_tipset);
545 }
546 let tipset = network
548 .chain_exchange_full_tipset(peer_id, tipset_keys)
549 .await
550 .map_err(|e| anyhow::anyhow!(e))?;
551 tipset.persist(chain_store.db())?;
552
553 Ok(tipset)
554}
555
556async fn get_full_tipset_batch(
557 network: &SyncNetworkContext,
558 chain_store: &ChainStore,
559 peer_id: Option<PeerId>,
560 tipset_keys: &TipsetKey,
561) -> anyhow::Result<Vec<FullTipset>> {
562 if let Ok(full_tipset) = load_full_tipset(chain_store, tipset_keys) {
564 return Ok(vec![full_tipset]);
565 }
566 let tipsets = network
568 .chain_exchange_full_tipsets(peer_id, tipset_keys)
569 .await
570 .map_err(|e| anyhow::anyhow!(e))?;
571
572 for tipset in tipsets.iter() {
573 tipset.persist(chain_store.db())?;
574 }
575
576 Ok(tipsets)
577}
578
579pub fn load_full_tipset(
580 chain_store: &ChainStore,
581 tipset_keys: &TipsetKey,
582) -> anyhow::Result<FullTipset> {
583 let ts = chain_store
585 .chain_index()
586 .load_required_tipset(tipset_keys)?;
587 let blocks: Vec<_> = ts
588 .block_headers()
589 .iter()
590 .map(|header| -> anyhow::Result<Block> {
591 let (bls_msgs, secp_msgs) = crate::chain::block_messages(chain_store.db(), header)?;
592 Ok(Block {
593 header: header.clone(),
594 bls_messages: bls_msgs,
595 secp_messages: secp_msgs,
596 })
597 })
598 .try_collect()?;
599 let fts = FullTipset::new(blocks)?;
601 Ok(fts)
602}
603
604enum SyncEvent {
605 NewFullTipsets(Vec<FullTipset>),
606 BadTipset(FullTipset),
607 ValidatedTipset {
608 tipset: FullTipset,
609 is_proposed_head: bool,
610 },
611}
612
613impl std::fmt::Display for SyncEvent {
614 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
615 fn tss_to_string(tss: &[FullTipset]) -> String {
616 format!(
617 "epoch: {}-{}",
618 tss.first().map(|ts| ts.epoch()).unwrap_or_default(),
619 tss.last().map(|ts| ts.epoch()).unwrap_or_default()
620 )
621 }
622
623 match self {
624 Self::NewFullTipsets(tss) => write!(f, "NewFullTipsets({})", tss_to_string(tss)),
625 Self::BadTipset(ts) => {
626 write!(f, "BadTipset(epoch: {}, key: {})", ts.epoch(), ts.key())
627 }
628 Self::ValidatedTipset {
629 tipset,
630 is_proposed_head,
631 } => write!(
632 f,
633 "ValidatedTipset(epoch: {}, key: {}, is_proposed_head: {is_proposed_head})",
634 tipset.epoch(),
635 tipset.key()
636 ),
637 }
638 }
639}
640
641#[derive(derive_more::Debug)]
642struct SyncStateMachine {
643 #[debug(skip)]
644 cs: ChainStore,
645 bad_block_cache: Option<BadBlockCache>,
646 tipsets: HashMap<TipsetKey, FullTipset>,
648 stateless_mode: bool,
649 validated_tipset_broadcast_tx: tokio::sync::broadcast::Sender<TipsetKey>,
651}
652
653impl SyncStateMachine {
654 pub fn new(
655 cs: ChainStore,
656 bad_block_cache: Option<BadBlockCache>,
657 stateless_mode: bool,
658 ) -> Self {
659 Self {
660 cs,
661 bad_block_cache,
662 tipsets: HashMap::default(),
663 stateless_mode,
664 validated_tipset_broadcast_tx: tokio::sync::broadcast::Sender::new(1024),
665 }
666 }
667
668 fn chains(&self) -> Vec<Vec<FullTipset>> {
670 let mut chains = Vec::new();
671 let mut remaining_tipsets = self.tipsets.clone();
672
673 while let Some(heaviest) = remaining_tipsets
674 .values()
675 .max_by_key(|ts| ts.weight())
676 .cloned()
677 {
678 let mut chain = Vec::new();
680 let mut current = Some(heaviest);
681
682 while let Some(tipset) = current.take() {
683 remaining_tipsets.remove(tipset.key());
684
685 current = self.tipsets.get(tipset.parents()).cloned();
687
688 chain.push(tipset);
689 }
690 chain.reverse();
691 chains.push(chain);
692 }
693
694 chains
695 }
696
697 fn is_parent_validated(&self, tipset: &FullTipset) -> bool {
698 let db = self.cs.db();
699 self.stateless_mode || db.has(tipset.parent_state()).unwrap_or(false)
700 }
701
702 fn is_ready_for_validation(&self, tipset: &FullTipset) -> bool {
703 if self.stateless_mode || tipset.key() == self.cs.genesis_tipset().key() {
704 true
706 } else if let Ok(parent_ts) = load_full_tipset(&self.cs, tipset.parents()) {
707 let head_ts = self.cs.heaviest_tipset();
708 if parent_ts.key() == head_ts.key() {
714 true
715 } else if parent_ts.epoch() >= head_ts.epoch() {
716 false
717 } else {
718 self.is_parent_validated(tipset)
719 }
720 } else {
721 false
722 }
723 }
724
725 fn add_full_tipset(&mut self, tipset: FullTipset) {
726 if let Err(why) = TipsetValidator(&tipset).validate(
727 &self.cs,
728 self.bad_block_cache.as_ref(),
729 &self.cs.genesis_tipset(),
730 self.cs.chain_config().block_delay_secs,
731 ) {
732 metrics::INVALID_TIPSET_TOTAL.inc();
733 trace!("Skipping invalid tipset: {}", why);
734 self.mark_bad_tipset(tipset);
735 return;
736 }
737
738 if tipset.epoch() < self.cs.ec_calculator_finalized_epoch() {
741 self.mark_bad_tipset(tipset);
742 return;
743 }
744
745 if self.tipsets.contains_key(tipset.key()) {
747 return;
748 }
749
750 if let Ok(Some(ts)) = self.cs.chain_index().tipset_by_height_blocking(
752 tipset.epoch(),
753 self.cs.heaviest_tipset(),
754 ResolveNullTipset::TakeOlder,
755 ) && ts.key() == tipset.key()
756 {
757 return;
758 }
759
760 let mut to_remove = Vec::new();
762 #[allow(clippy::mutable_key_type)]
763 let mut merged_blocks: HashSet<_> = tipset.blocks().iter().cloned().collect();
764
765 let parent_refs: HashSet<_> = self
767 .tipsets
768 .values()
769 .map(|ts| ts.parents().clone())
770 .collect();
771
772 for (key, existing_ts) in self.tipsets.iter() {
773 if existing_ts.epoch() == tipset.epoch() && existing_ts.parents() == tipset.parents() {
774 if !parent_refs.contains(key) {
776 to_remove.push(key.clone());
777 }
778 merged_blocks.extend(existing_ts.blocks().iter().cloned());
780 }
781 }
782
783 for key in to_remove {
785 self.tipsets.remove(&key);
786 }
787
788 if let Ok(merged_tipset) = FullTipset::new(merged_blocks) {
790 self.tipsets
791 .insert(merged_tipset.key().clone(), merged_tipset);
792 }
793
794 self.tipsets.shrink_to_fit();
795 }
796
797 fn mark_bad_tipset(&mut self, tipset: FullTipset) {
801 let mut stack = vec![tipset];
802 while let Some(tipset) = stack.pop() {
803 self.tipsets.remove(tipset.key());
804 let mut to_remove = Vec::new();
806 let mut descendants = Vec::new();
807
808 for (key, ts) in self.tipsets.iter() {
809 if ts.parents() == tipset.key() {
810 to_remove.push(key.clone());
811 descendants.push(ts.clone());
812 }
813 }
814
815 for key in to_remove {
817 self.tipsets.remove(&key);
818 }
819
820 stack.extend(descendants);
822 }
823 }
824
825 fn try_mark_tipset_as_validated(&mut self, tipset: FullTipset, is_proposed_head: bool) -> bool {
826 if !self.is_parent_validated(&tipset) {
827 tracing::error!(epoch = %tipset.epoch(), tsk = %tipset.key(), parent_state = %tipset.parent_state(), "Parent tipset must be validated");
828 return false;
829 }
830
831 self.tipsets.remove(tipset.key());
832 let tipset = tipset.into_tipset();
833 if self.stateless_mode {
835 let epoch = tipset.epoch();
836 let terse_key = tipset.key().terse();
837 if self.cs.heaviest_tipset().weight() < tipset.weight() {
838 if let Err(e) = self.cs.set_heaviest_tipset(tipset) {
839 error!("Error setting heaviest tipset: {}", e);
840 return false;
841 } else {
842 info!("Heaviest tipset: {} ({})", epoch, terse_key);
843 }
844 }
845 } else if is_proposed_head {
846 if let Err(e) = self.cs.maybe_update_pending_head(&tipset) {
847 error!("Error putting tipset: {e}");
848 return false;
849 }
850 } else if let Err(e) = self.cs.set_heaviest_tipset(tipset) {
851 error!("Error setting heaviest tipset: {e}");
852 return false;
853 }
854 true
855 }
856
857 pub fn update(&mut self, event: SyncEvent) {
858 tracing::trace!("update: {event}");
859 match event {
860 SyncEvent::NewFullTipsets(tipsets) => {
861 for tipset in tipsets {
862 self.add_full_tipset(tipset);
863 }
864 }
865 SyncEvent::BadTipset(tipset) => self.mark_bad_tipset(tipset),
866 SyncEvent::ValidatedTipset {
867 tipset,
868 is_proposed_head,
869 } => {
870 if self.try_mark_tipset_as_validated(tipset, is_proposed_head)
871 && crate::utils::broadcast::has_subscribers(&self.validated_tipset_broadcast_tx)
872 && let Err(e) = self.validated_tipset_broadcast_tx.send(self.cs.heaviest_tipset().key().clone())
874 {
875 warn!("Failed to broadcast validated tipset: {e}");
876 }
877 }
878 }
879 }
880
881 pub fn tasks(&self) -> (Vec<SyncTask>, Vec<ForkSyncInfo>) {
882 let current_validated_epoch = self.cs.heaviest_tipset().epoch();
884 let now = Utc::now();
885
886 let mut active_sync_info = Vec::new();
887 let mut tasks = Vec::new();
888 for chain in self.chains() {
889 if let Some(first_ts) = chain.first() {
890 let last_ts = chain.last().expect("Infallible");
891 let stage: ForkSyncStage;
892 let start_time = Some(now);
893
894 if !self.is_ready_for_validation(first_ts) {
895 stage = ForkSyncStage::FetchingHeaders;
896 tasks.push(SyncTask::FetchTipset(
897 first_ts.parents().clone(),
898 first_ts.epoch(),
899 ));
900 } else {
901 stage = ForkSyncStage::ValidatingTipsets;
902 tasks.push(SyncTask::ValidateTipset {
903 tipset: first_ts.clone(),
904 is_proposed_head: chain.len() == 1,
905 });
906 }
907
908 let fork_info = ForkSyncInfo {
909 target_tipset_key: last_ts.key().clone(),
910 target_epoch: last_ts.epoch(),
911 target_sync_epoch_start: first_ts.epoch(),
912 stage,
913 validated_chain_head_epoch: current_validated_epoch,
914 start_time,
915 last_updated: Some(now),
916 };
917
918 active_sync_info.push(fork_info);
919 }
920 }
921 (tasks, active_sync_info)
922 }
923
924 pub fn cleanup_dangling_forks(&mut self) {
925 let finalized_epoch = self.cs.ec_calculator_finalized_epoch();
926 for chain in self.chains() {
927 if let Some(target) = chain.last()
929 && target.epoch() < finalized_epoch
930 {
931 chain.iter().for_each(|ts| {
932 self.tipsets.remove(ts.key());
933 });
934 tracing::info!(
935 "Cleaned up dangling fork from epoch {} to {}",
936 chain.first().map(|ts| ts.epoch()).unwrap_or_default(),
937 chain.last().map(|ts| ts.epoch()).unwrap_or_default(),
938 );
939 }
940 }
941 }
942}
943
944#[derive(PartialEq, Eq, Hash, Clone, Debug)]
945enum SyncTask {
946 ValidateTipset {
947 tipset: FullTipset,
948 is_proposed_head: bool,
949 },
950 FetchTipset(TipsetKey, ChainEpoch),
951}
952
953impl std::fmt::Display for SyncTask {
954 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
955 match self {
956 SyncTask::ValidateTipset {
957 tipset,
958 is_proposed_head,
959 } => write!(
960 f,
961 "ValidateTipset(epoch: {}, is_proposed_head: {is_proposed_head})",
962 tipset.epoch()
963 ),
964 SyncTask::FetchTipset(key, epoch) => {
965 let s = key.to_string();
966 write!(
967 f,
968 "FetchTipset({}, epoch: {})",
969 &s[s.len().saturating_sub(8)..],
970 epoch
971 )
972 }
973 }
974 }
975}
976
977impl SyncTask {
978 async fn execute(
979 self,
980 network: SyncNetworkContext,
981 state_manager: StateManager,
982 stateless_mode: bool,
983 bad_block_cache: Option<BadBlockCache>,
984 ) -> Option<SyncEvent> {
985 tracing::trace!("SyncTask::execute {self}");
986 match self {
987 SyncTask::ValidateTipset {
988 tipset,
989 is_proposed_head,
990 } if stateless_mode => Some(SyncEvent::ValidatedTipset {
991 tipset,
992 is_proposed_head,
993 }),
994 SyncTask::ValidateTipset {
995 tipset,
996 is_proposed_head,
997 } => match validate_tipset(&state_manager, tipset.clone(), bad_block_cache).await {
998 Ok(()) => Some(SyncEvent::ValidatedTipset {
999 tipset,
1000 is_proposed_head,
1001 }),
1002 Err(e) if matches!(e, TipsetSyncerError::TimeTravellingBlock { .. }) => {
1007 warn!("Time travelling block detected, skipping tipset for now: {e}");
1008 None
1009 }
1010 Err(e) => {
1011 warn!("Error validating tipset: {e}");
1012 Some(SyncEvent::BadTipset(tipset))
1013 }
1014 },
1015 SyncTask::FetchTipset(key, epoch) => {
1016 match get_full_tipset_batch(&network, state_manager.chain_store(), None, &key).await
1017 {
1018 Ok(parents) => Some(SyncEvent::NewFullTipsets(parents)),
1019 Err(e) => {
1020 tracing::debug!(%key, %epoch, "failed to fetch tipset: {e:#}");
1022 None
1023 }
1024 }
1025 }
1026 }
1027 }
1028}
1029
1030#[derive(Debug)]
1031struct SyncTasks(Arc<Mutex<HashSet<SyncTask>>>);
1032
1033#[derive(Debug)]
1034struct SyncStateMachineWrapper(Arc<Mutex<SyncStateMachine>>);
1035
1036mod metrics_collection {
1037 use super::*;
1038 use prometheus_client::{
1039 collector::Collector,
1040 encoding::{DescriptorEncoder, EncodeMetric},
1041 metrics::gauge::Gauge,
1042 registry::Unit,
1043 };
1044
1045 impl Collector for SyncTasks {
1046 fn encode(&self, mut encoder: DescriptorEncoder) -> Result<(), std::fmt::Error> {
1047 {
1048 let size_in_bytes = {
1049 let g: Gauge = Default::default();
1050 g.set(self.0.lock().allocation_size() as i64);
1051 g
1052 };
1053 let size_metric_encoder = encoder.encode_descriptor(
1054 "chain_follower_tasks_size",
1055 "Size of the chain follower tasks in bytes",
1056 Some(&Unit::Bytes),
1057 size_in_bytes.metric_type(),
1058 )?;
1059 size_in_bytes.encode(size_metric_encoder)?;
1060 }
1061 {
1062 let len = {
1063 let g: Gauge = Default::default();
1064 g.set(self.0.lock().len() as i64);
1065 g
1066 };
1067 let size_metric_encoder = encoder.encode_descriptor(
1068 "chain_follower_tasks_len",
1069 "Length of the chain follower tasks",
1070 None,
1071 len.metric_type(),
1072 )?;
1073 len.encode(size_metric_encoder)?;
1074 }
1075 {
1076 let cap = {
1077 let g: Gauge = Default::default();
1078 g.set(self.0.lock().capacity() as i64);
1079 g
1080 };
1081 let size_metric_encoder = encoder.encode_descriptor(
1082 "chain_follower_tasks_cap",
1083 "Capacity of the chain follower tasks",
1084 None,
1085 cap.metric_type(),
1086 )?;
1087 cap.encode(size_metric_encoder)?;
1088 }
1089
1090 Ok(())
1091 }
1092 }
1093
1094 impl Collector for SyncStateMachineWrapper {
1095 fn encode(&self, mut encoder: DescriptorEncoder) -> Result<(), std::fmt::Error> {
1096 {
1097 let size_in_bytes = {
1098 let g: Gauge = Default::default();
1099 g.set(self.0.lock().tipsets.allocation_size() as i64);
1100 g
1101 };
1102 let size_metric_encoder = encoder.encode_descriptor(
1103 "chain_follower_tipsets_size",
1104 "Size of the chain follower tipsets in bytes",
1105 Some(&Unit::Bytes),
1106 size_in_bytes.metric_type(),
1107 )?;
1108 size_in_bytes.encode(size_metric_encoder)?;
1109 }
1110 {
1111 let len = {
1112 let g: Gauge = Default::default();
1113 g.set(self.0.lock().tipsets.len() as i64);
1114 g
1115 };
1116 let size_metric_encoder = encoder.encode_descriptor(
1117 "chain_follower_tipsets_len",
1118 "Length of the chain follower tipsets",
1119 None,
1120 len.metric_type(),
1121 )?;
1122 len.encode(size_metric_encoder)?;
1123 }
1124 {
1125 let cap = {
1126 let g: Gauge = Default::default();
1127 g.set(self.0.lock().tipsets.capacity() as i64);
1128 g
1129 };
1130 let size_metric_encoder = encoder.encode_descriptor(
1131 "chain_follower_tipsets_cap",
1132 "Capacity of the chain follower tipsets",
1133 None,
1134 cap.metric_type(),
1135 )?;
1136 cap.encode(size_metric_encoder)?;
1137 }
1138
1139 Ok(())
1140 }
1141 }
1142}
1143
1144#[cfg(test)]
1145mod tests {
1146 use super::*;
1147 use crate::blocks::{Chain4U, HeaderBuilder, chain4u};
1148 use crate::db::MemoryDB;
1149 use crate::utils::db::CborStoreExt as _;
1150 use num_bigint::BigInt;
1151 use num_traits::ToPrimitive;
1152 use std::sync::Arc;
1153 use tracing::level_filters::LevelFilter;
1154 use tracing_subscriber::EnvFilter;
1155
1156 fn setup() -> (ChainStore, Chain4U<Arc<MemoryDB>>) {
1157 let _ = tracing_subscriber::fmt()
1159 .without_time()
1160 .with_env_filter(
1161 EnvFilter::builder()
1162 .with_default_directive(LevelFilter::DEBUG.into())
1163 .from_env()
1164 .unwrap(),
1165 )
1166 .try_init();
1167
1168 let db = Arc::new(MemoryDB::default());
1169
1170 let c4u = Chain4U::with_blockstore(db.clone());
1172 chain4u! {
1173 in c4u;
1174 [genesis_header = dummy_node(&db, 0)]
1175 };
1176
1177 let cs = ChainStore::new(db, Default::default(), genesis_header).unwrap();
1178
1179 cs.set_heaviest_tipset(cs.genesis_tipset()).unwrap();
1180
1181 (cs, c4u)
1182 }
1183
1184 fn dummy_state(db: impl Blockstore, i: ChainEpoch) -> Cid {
1185 db.put_cbor_default(&i).unwrap()
1186 }
1187
1188 fn dummy_node(db: impl Blockstore, i: ChainEpoch) -> HeaderBuilder {
1189 HeaderBuilder {
1190 state_root: dummy_state(db, i).into(),
1191 weight: BigInt::from(i).into(),
1192 epoch: i.into(),
1193 ..Default::default()
1194 }
1195 }
1196
1197 #[test]
1198 fn test_state_machine_validation_order() {
1199 let (cs, c4u) = setup();
1200 let db = cs.db_owned();
1201
1202 chain4u! {
1203 from [genesis_header] in c4u;
1204 [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)]
1205 };
1206
1207 let mut state_machine = SyncStateMachine::new(cs.shallow_clone(), Default::default(), true);
1209
1210 let tipsets = vec![e, b, d, c, a];
1212
1213 for block in tipsets {
1215 let full_tipset = FullTipset::new(vec![Block {
1216 header: block.clone().into(),
1217 bls_messages: vec![],
1218 secp_messages: vec![],
1219 }])
1220 .unwrap();
1221 state_machine.update(SyncEvent::NewFullTipsets(vec![full_tipset]));
1222 }
1223
1224 let mut validation_tasks = Vec::new();
1226 loop {
1227 let (tasks, _) = state_machine.tasks();
1228
1229 let validation_tipsets: Vec<_> = tasks
1231 .into_iter()
1232 .filter_map(|task| {
1233 if let SyncTask::ValidateTipset {
1234 tipset,
1235 is_proposed_head,
1236 } = task
1237 {
1238 Some((tipset, is_proposed_head))
1239 } else {
1240 None
1241 }
1242 })
1243 .collect();
1244
1245 if validation_tipsets.is_empty() {
1246 break;
1247 }
1248
1249 for (ts, is_proposed_head) in validation_tipsets {
1251 validation_tasks.push(ts.epoch());
1252 db.put_cbor_default(&ts.epoch()).unwrap();
1253 state_machine.try_mark_tipset_as_validated(ts, is_proposed_head);
1254 }
1255 }
1256
1257 assert_eq!(validation_tasks, vec![1, 2, 3, 4, 5]);
1259 }
1260
1261 #[test]
1262 fn test_sync_state_machine_chain_fragments() {
1263 let (cs, c4u) = setup();
1264 let db = cs.db();
1265
1266 chain4u! {
1270 in c4u;
1271 [a = dummy_node(db, 1)] -> [b = dummy_node(db, 2)]
1272 };
1273 chain4u! {
1274 from [a] in c4u;
1275 [c = dummy_node(db, 3)]
1276 };
1277
1278 let mut state_machine = SyncStateMachine::new(cs, Default::default(), false);
1280
1281 for block in [a, b, c] {
1283 let full_tipset = FullTipset::new(vec![Block {
1284 header: block.clone().into(),
1285 bls_messages: vec![],
1286 secp_messages: vec![],
1287 }])
1288 .unwrap();
1289 state_machine.update(SyncEvent::NewFullTipsets(vec![full_tipset]));
1290 }
1291
1292 let chains = state_machine
1293 .chains()
1294 .into_iter()
1295 .map(|v| {
1296 v.into_iter()
1297 .map(|ts| ts.weight().to_i64().unwrap_or(0))
1298 .collect_vec()
1299 })
1300 .collect_vec();
1301
1302 assert_eq!(chains, vec![vec![1, 3], vec![1, 2]]);
1304 }
1305}