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
204#[allow(clippy::too_many_arguments)]
205async fn chain_follower(
207 tasks: &Arc<Mutex<HashSet<SyncTask>>>,
208 state_machine: &Arc<Mutex<SyncStateMachine>>,
209 state_manager: &StateManager,
210 bad_block_cache: Option<BadBlockCache>,
211 network_rx: Arc<flume::Receiver<NetworkEvent>>,
212 tipset_sender: flume::Sender<FullTipset>,
213 tipset_receiver: Arc<flume::Receiver<FullTipset>>,
214 network: &SyncNetworkContext,
215 mem_pool: &MessagePool<ChainStore>,
216 sync_status: &SyncStatus,
217 genesis: &Tipset,
218 stateless_mode: bool,
219) -> anyhow::Result<()> {
220 let state_changed = Arc::new(Notify::new());
221
222 let seen_block_cache = SeenBlockCache::default();
223
224 let hello_fetch_limiter = Arc::new(Semaphore::new(*MAX_CONCURRENT_HELLO_TRIGGERED_FETCHES));
225
226 let mut set = JoinSet::new();
227 let cancellation_token = CancellationToken::new();
228 let _cancellation_token_drop_guard = cancellation_token.drop_guard_ref();
229
230 set.spawn({
232 let state_manager = state_manager.shallow_clone();
233 let network = network.shallow_clone();
234 let mem_pool = mem_pool.shallow_clone();
235 let genesis = genesis.shallow_clone();
236 let bad_block_cache = bad_block_cache.shallow_clone();
237 let seen_block_cache = seen_block_cache.shallow_clone();
238 let cancellation_token = cancellation_token.clone();
239 let hello_fetch_limiter = hello_fetch_limiter.shallow_clone();
240 let tipset_sender = tipset_sender.clone();
241 async move {
242 while let Ok(event) = network_rx.recv_async().await {
243 inc_gossipsub_event_metrics(&event);
244
245 update_peer_info(
246 &event,
247 &network,
248 state_manager.chain_store().shallow_clone(),
249 &genesis,
250 cancellation_token.clone(),
251 );
252
253 match event {
256 NetworkEvent::HelloResponseOutbound { request, source } => {
257 let Ok(permit) = hello_fetch_limiter.shallow_clone().try_acquire_owned() else {
258 debug!(%source, "dropping hello-triggered tipset fetch: too many in flight");
259 continue;
260 };
261 spawn_tipset_fetch(
262 network.shallow_clone(),
263 state_manager.chain_store().shallow_clone(),
264 Some(source),
265 TipsetKey::from(request.heaviest_tip_set),
266 tipset_sender.clone(),
267 cancellation_token.clone(),
268 Some(permit),
269 );
270 }
271 NetworkEvent::PubsubMessage { message } => match message {
272 PubsubMessage::Block(b) => {
273 let cs = state_manager.chain_store();
274 let cfg = cs.chain_config();
275 if let Err(reason) = GossipBlockValidator::new(&b).validate_pre_fetch(
276 &genesis,
277 cfg.block_delay_secs,
278 cs.ec_calculator_finalized_epoch(), bad_block_cache.as_ref(),
280 &seen_block_cache,
281 ) {
282 metrics::GOSSIP_BLOCK_REJECTED_TOTAL
283 .get_or_create(&metrics::GossipRejectReasonLabel {
284 reason: reason.label(),
285 })
286 .inc();
287 debug!("Rejected gossip block {}: {reason}", b.header.cid());
288 continue;
289 }
290 let key = TipsetKey::from(nunny::vec![*b.header.cid()]);
291 spawn_tipset_fetch(
293 network.shallow_clone(),
294 cs.shallow_clone(),
295 None,
296 key,
297 tipset_sender.clone(),
298 cancellation_token.clone(),
299 None,
300 );
301 }
302 PubsubMessage::Message(m) => {
303 if let Err(why) = mem_pool.add(m).await {
304 debug!("Received invalid GossipSub message: {}", why);
305 }
306 }
307 },
308 _ => {}
309 }
310 }
311 }
312 });
313
314 set.spawn({
316 let state_changed = state_changed.clone();
317 let state_machine = state_machine.clone();
318
319 async move {
320 while let Ok(tipset) = tipset_receiver.recv_async().await {
321 state_machine
322 .lock()
323 .update(SyncEvent::NewFullTipsets(vec![tipset]));
324 state_changed.notify_one();
325 }
326 }
327 });
328
329 set.spawn({
331 let state_manager = state_manager.shallow_clone();
332 let state_machine = state_machine.shallow_clone();
333 let network = network.shallow_clone();
334 let sync_status = sync_status.shallow_clone();
335 let state_changed = state_changed.shallow_clone();
336 let tasks = tasks.shallow_clone();
337 let bad_block_cache = bad_block_cache.shallow_clone();
338 let cancellation_token = cancellation_token.clone();
339 async move {
340 const FORK_CLEANUP_INTERVAL: Duration = Duration::from_mins(1);
341 let mut last_fork_cleanup = Instant::now();
342 loop {
343 state_changed.notified().await;
344
345 let mut tasks_set = tasks.lock();
346 if last_fork_cleanup + FORK_CLEANUP_INTERVAL < Instant::now() {
347 state_machine.lock().cleanup_dangling_forks();
348 last_fork_cleanup = Instant::now();
349 }
350 let (task_vec, current_active_forks) = state_machine.lock().tasks();
351
352 {
354 let old_status_report = sync_status.load().shallow_clone();
355 let new_status_report = old_status_report.update(
356 &state_manager,
357 current_active_forks,
358 stateless_mode,
359 );
360 sync_status.store(new_status_report.into());
361 }
362
363 for task in task_vec {
364 let new = tasks_set.insert(task.clone());
366 if new {
367 let action = task.clone().execute(
368 network.shallow_clone(),
369 state_manager.shallow_clone(),
370 stateless_mode,
371 bad_block_cache.shallow_clone(),
372 );
373 tokio::spawn({
374 let tasks = tasks.shallow_clone();
375 let state_machine = state_machine.shallow_clone();
376 let state_changed = state_changed.shallow_clone();
377 let cancellation_token = cancellation_token.clone();
378 async move {
379 cancellation_token
380 .run_until_cancelled(async move {
381 if let Some(event) = action.await {
382 state_machine.lock().update(event);
383 state_changed.notify_one();
384 }
385 let mut tasks = tasks.lock();
386 tasks.remove(&task);
387 tasks.shrink_to_fit();
388 })
389 .await
390 }
391 });
392 }
393 }
394 }
395 }
396 });
397
398 set.spawn({
402 let state_manager = state_manager.shallow_clone();
403 let sync_status = sync_status.shallow_clone();
404 async move {
405 loop {
406 tokio::time::sleep(tokio::time::Duration::from_secs(10)).await;
407 let heaviest_tipset = state_manager.chain_store().heaviest_tipset();
408 let heaviest_epoch = heaviest_tipset.epoch();
409 let expected_head = calculate_expected_epoch(
410 Utc::now().timestamp() as u64,
411 state_manager.chain_store().genesis_block_header().timestamp,
412 state_manager.chain_config().block_delay_secs,
413 );
414 let diff = expected_head - heaviest_epoch;
415
416 if diff <= 10 {
419 continue;
420 }
421
422 let report = sync_status.load();
425 let forks = &report.active_forks;
426 let status: Cow<'static, str> = if forks
430 .iter()
431 .any(|fork| fork.stage == ForkSyncStage::ValidatingTipsets)
432 {
433 ", validating tipsets".into()
434 } else if forks
435 .iter()
436 .any(|fork| fork.stage == ForkSyncStage::FetchingHeaders)
437 {
438 let remaining = forks
442 .iter()
443 .filter(|fork| fork.stage == ForkSyncStage::FetchingHeaders)
444 .map(|fork| fork.target_sync_epoch_start - fork.validated_chain_head_epoch)
445 .filter(|gap| *gap > 0)
446 .min();
447 match remaining {
448 Some(remaining) => format!(", fetching headers (~{remaining})").into(),
449 None => ", fetching headers".into(),
453 }
454 } else {
455 ", waiting for tipsets".into()
458 };
459 info!(
460 "Catching up to HEAD: {heaviest_epoch}{} -> {expected_head} (diff: {diff}){status}",
461 heaviest_tipset.key()
462 );
463 }
464 }
465 });
466
467 set.join_all().await;
468 Ok(())
469}
470
471fn inc_gossipsub_event_metrics(event: &NetworkEvent) {
473 let label = match event {
474 NetworkEvent::HelloRequestInbound => metrics::values::HELLO_REQUEST_INBOUND,
475 NetworkEvent::HelloResponseOutbound { .. } => metrics::values::HELLO_RESPONSE_OUTBOUND,
476 NetworkEvent::HelloRequestOutbound => metrics::values::HELLO_REQUEST_OUTBOUND,
477 NetworkEvent::HelloResponseInbound => metrics::values::HELLO_RESPONSE_INBOUND,
478 NetworkEvent::PeerConnected(_) => metrics::values::PEER_CONNECTED,
479 NetworkEvent::PeerDisconnected(_) => metrics::values::PEER_DISCONNECTED,
480 NetworkEvent::PubsubMessage { message } => match message {
481 PubsubMessage::Block(_) => metrics::values::PUBSUB_BLOCK,
482 PubsubMessage::Message(_) => metrics::values::PUBSUB_MESSAGE,
483 },
484 NetworkEvent::ChainExchangeRequestOutbound => {
485 metrics::values::CHAIN_EXCHANGE_REQUEST_OUTBOUND
486 }
487 NetworkEvent::ChainExchangeResponseInbound => {
488 metrics::values::CHAIN_EXCHANGE_RESPONSE_INBOUND
489 }
490 NetworkEvent::ChainExchangeRequestInbound => {
491 metrics::values::CHAIN_EXCHANGE_REQUEST_INBOUND
492 }
493 NetworkEvent::ChainExchangeResponseOutbound => {
494 metrics::values::CHAIN_EXCHANGE_RESPONSE_OUTBOUND
495 }
496 };
497
498 metrics::LIBP2P_MESSAGE_TOTAL.get_or_create(&label).inc();
499}
500
501fn update_peer_info(
503 event: &NetworkEvent,
504 network: &SyncNetworkContext,
505 chain_store: ChainStore,
506 genesis: &Tipset,
507 cancellation_token: CancellationToken,
508) {
509 match event {
510 NetworkEvent::PeerConnected(peer_id) => {
511 let peer_id = *peer_id;
512 let genesis_cid = *genesis.block_headers().first().cid();
513 let network = network.shallow_clone();
514 tokio::task::spawn(async move {
516 cancellation_token
517 .run_until_cancelled(handle_peer_connected_event(
518 network,
519 chain_store,
520 peer_id,
521 genesis_cid,
522 ))
523 .await
524 });
525 }
526 NetworkEvent::PeerDisconnected(peer_id) => {
527 handle_peer_disconnected_event(network, *peer_id);
528 }
529 _ => {}
530 }
531}
532
533async fn handle_peer_connected_event(
534 network: SyncNetworkContext,
535 chain_store: ChainStore,
536 peer_id: PeerId,
537 genesis_block_cid: Cid,
538) {
539 if network.peer_manager().is_peer_new(&peer_id) {
541 let heaviest = chain_store.heaviest_tipset();
544 let request = HelloRequest {
545 heaviest_tip_set: heaviest.cids(),
546 heaviest_tipset_height: heaviest.epoch(),
547 heaviest_tipset_weight: heaviest.weight().clone().into(),
548 genesis_cid: genesis_block_cid,
549 };
550 let (peer_id, moment_sent, response) = match network.hello_request(peer_id, request).await {
551 Ok(response) => response,
552 Err(e) => {
553 debug!("Hello request failed: {}", e);
554 return;
555 }
556 };
557 let dur = Instant::now().duration_since(moment_sent);
558
559 match response {
561 Some(_) => {
562 network.peer_manager().log_success(&peer_id, dur);
563 }
564 None => {
565 network.peer_manager().log_failure(&peer_id, dur);
566 }
567 }
568 }
569}
570
571fn handle_peer_disconnected_event(network: &SyncNetworkContext, peer_id: PeerId) {
572 network.peer_manager().remove_peer(&peer_id);
573 network.peer_manager().unmark_peer_bad(&peer_id);
574}
575
576static MAX_CONCURRENT_HELLO_TRIGGERED_FETCHES: LazyLock<usize> = LazyLock::new(|| {
583 env_or_default_logged(
585 "FOREST_MAX_CONCURRENT_HELLO_TRIGGERED_FETCHES",
586 nonzero!(16_usize),
587 )
588 .get()
589 .min(Semaphore::MAX_PERMITS)
590});
591
592fn spawn_tipset_fetch(
595 network: SyncNetworkContext,
596 chain_store: ChainStore,
597 peer_id: Option<PeerId>,
598 tipset_keys: TipsetKey,
599 tipset_sender: flume::Sender<FullTipset>,
600 cancellation_token: CancellationToken,
601 permit: Option<OwnedSemaphorePermit>,
602) {
603 tokio::spawn(async move {
604 let _permit = permit;
605 cancellation_token
608 .run_until_cancelled(async {
609 match get_full_tipset(&network, &chain_store, peer_id, &tipset_keys).await {
610 Ok(tipset) => {
611 let _ = tipset_sender.send_async(tipset).await;
612 }
613 Err(e) => debug!("Querying full tipset failed: {e}"),
614 }
615 })
616 .await;
617 });
618}
619
620pub async fn get_full_tipset(
621 network: &SyncNetworkContext,
622 chain_store: &ChainStore,
623 peer_id: Option<PeerId>,
624 tipset_keys: &TipsetKey,
625) -> anyhow::Result<FullTipset> {
626 if let Ok(full_tipset) = load_full_tipset(chain_store, tipset_keys) {
628 return Ok(full_tipset);
629 }
630 let tipset = network
632 .chain_exchange_full_tipset(peer_id, tipset_keys)
633 .await
634 .map_err(|e| anyhow::anyhow!(e))?;
635 tipset.persist(chain_store.db())?;
636
637 Ok(tipset)
638}
639
640async fn get_full_tipset_batch(
641 network: &SyncNetworkContext,
642 chain_store: &ChainStore,
643 peer_id: Option<PeerId>,
644 tipset_keys: &TipsetKey,
645) -> anyhow::Result<Vec<FullTipset>> {
646 if let Ok(full_tipset) = load_full_tipset(chain_store, tipset_keys) {
648 return Ok(vec![full_tipset]);
649 }
650 let tipsets = network
652 .chain_exchange_full_tipsets(peer_id, tipset_keys)
653 .await
654 .map_err(|e| anyhow::anyhow!(e))?;
655
656 for tipset in tipsets.iter() {
657 tipset.persist(chain_store.db())?;
658 }
659
660 Ok(tipsets)
661}
662
663pub fn load_full_tipset(
664 chain_store: &ChainStore,
665 tipset_keys: &TipsetKey,
666) -> anyhow::Result<FullTipset> {
667 let ts = chain_store
669 .chain_index()
670 .load_required_tipset(tipset_keys)?;
671 let blocks: Vec<_> = ts
672 .block_headers()
673 .iter()
674 .map(|header| -> anyhow::Result<Block> {
675 let (bls_msgs, secp_msgs) = crate::chain::block_messages(chain_store.db(), header)?;
676 Ok(Block {
677 header: header.clone(),
678 bls_messages: bls_msgs,
679 secp_messages: secp_msgs,
680 })
681 })
682 .try_collect()?;
683 let fts = FullTipset::new(blocks)?;
685 Ok(fts)
686}
687
688enum SyncEvent {
689 NewFullTipsets(Vec<FullTipset>),
690 BadTipset(FullTipset),
691 ValidatedTipset {
692 tipset: FullTipset,
693 is_proposed_head: bool,
694 },
695}
696
697impl std::fmt::Display for SyncEvent {
698 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
699 fn tss_to_string(tss: &[FullTipset]) -> String {
700 format!(
701 "epoch: {}-{}",
702 tss.first().map(|ts| ts.epoch()).unwrap_or_default(),
703 tss.last().map(|ts| ts.epoch()).unwrap_or_default()
704 )
705 }
706
707 match self {
708 Self::NewFullTipsets(tss) => write!(f, "NewFullTipsets({})", tss_to_string(tss)),
709 Self::BadTipset(ts) => {
710 write!(f, "BadTipset(epoch: {}, key: {})", ts.epoch(), ts.key())
711 }
712 Self::ValidatedTipset {
713 tipset,
714 is_proposed_head,
715 } => write!(
716 f,
717 "ValidatedTipset(epoch: {}, key: {}, is_proposed_head: {is_proposed_head})",
718 tipset.epoch(),
719 tipset.key()
720 ),
721 }
722 }
723}
724
725#[derive(derive_more::Debug)]
726struct SyncStateMachine {
727 #[debug(skip)]
728 cs: ChainStore,
729 bad_block_cache: Option<BadBlockCache>,
730 tipsets: HashMap<TipsetKey, FullTipset>,
732 stateless_mode: bool,
733 validated_tipset_broadcast_tx: tokio::sync::broadcast::Sender<TipsetKey>,
735}
736
737impl SyncStateMachine {
738 pub fn new(
739 cs: ChainStore,
740 bad_block_cache: Option<BadBlockCache>,
741 stateless_mode: bool,
742 ) -> Self {
743 Self {
744 cs,
745 bad_block_cache,
746 tipsets: HashMap::default(),
747 stateless_mode,
748 validated_tipset_broadcast_tx: tokio::sync::broadcast::Sender::new(1024),
749 }
750 }
751
752 fn chains(&self) -> Vec<Vec<FullTipset>> {
754 let mut chains = Vec::new();
755 let mut remaining_tipsets = self.tipsets.clone();
756
757 while let Some(heaviest) = remaining_tipsets
758 .values()
759 .max_by_key(|ts| ts.weight())
760 .cloned()
761 {
762 let mut chain = Vec::new();
764 let mut current = Some(heaviest);
765
766 while let Some(tipset) = current.take() {
767 remaining_tipsets.remove(tipset.key());
768
769 current = self.tipsets.get(tipset.parents()).cloned();
771
772 chain.push(tipset);
773 }
774 chain.reverse();
775 chains.push(chain);
776 }
777
778 chains
779 }
780
781 fn is_parent_validated(&self, tipset: &FullTipset) -> bool {
782 let db = self.cs.db();
783 self.stateless_mode || db.has(tipset.parent_state()).unwrap_or(false)
784 }
785
786 fn is_ready_for_validation(&self, tipset: &FullTipset) -> bool {
787 if self.stateless_mode || tipset.key() == self.cs.genesis_tipset().key() {
788 true
790 } else if let Ok(parent_ts) = load_full_tipset(&self.cs, tipset.parents()) {
791 let head_ts = self.cs.heaviest_tipset();
792 if parent_ts.key() == head_ts.key() {
798 true
799 } else if parent_ts.epoch() >= head_ts.epoch() {
800 false
801 } else {
802 self.is_parent_validated(tipset)
803 }
804 } else {
805 false
806 }
807 }
808
809 fn add_full_tipset(&mut self, tipset: FullTipset) {
810 if let Err(why) = TipsetValidator(&tipset).validate(
811 &self.cs,
812 self.bad_block_cache.as_ref(),
813 &self.cs.genesis_tipset(),
814 self.cs.chain_config().block_delay_secs,
815 ) {
816 metrics::INVALID_TIPSET_TOTAL.inc();
817 trace!("Skipping invalid tipset: {}", why);
818 self.mark_bad_tipset(tipset);
819 return;
820 }
821
822 if tipset.epoch() < self.cs.ec_calculator_finalized_epoch() {
825 self.mark_bad_tipset(tipset);
826 return;
827 }
828
829 if self.tipsets.contains_key(tipset.key()) {
831 return;
832 }
833
834 if let Ok(Some(ts)) = self.cs.chain_index().tipset_by_height_blocking(
836 tipset.epoch(),
837 self.cs.heaviest_tipset(),
838 ResolveNullTipset::TakeOlder,
839 ) && ts.key() == tipset.key()
840 {
841 return;
842 }
843
844 let mut to_remove = Vec::new();
846 #[allow(clippy::mutable_key_type)]
847 let mut merged_blocks: HashSet<_> = tipset.blocks().iter().cloned().collect();
848
849 let parent_refs: HashSet<_> = self
851 .tipsets
852 .values()
853 .map(|ts| ts.parents().clone())
854 .collect();
855
856 for (key, existing_ts) in self.tipsets.iter() {
857 if existing_ts.epoch() == tipset.epoch() && existing_ts.parents() == tipset.parents() {
858 if !parent_refs.contains(key) {
860 to_remove.push(key.clone());
861 }
862 merged_blocks.extend(existing_ts.blocks().iter().cloned());
864 }
865 }
866
867 for key in to_remove {
869 self.tipsets.remove(&key);
870 }
871
872 if let Ok(merged_tipset) = FullTipset::new(merged_blocks) {
874 self.tipsets
875 .insert(merged_tipset.key().clone(), merged_tipset);
876 }
877
878 self.tipsets.shrink_to_fit();
879 }
880
881 fn mark_bad_tipset(&mut self, tipset: FullTipset) {
885 let mut stack = vec![tipset];
886 while let Some(tipset) = stack.pop() {
887 self.tipsets.remove(tipset.key());
888 let mut to_remove = Vec::new();
890 let mut descendants = Vec::new();
891
892 for (key, ts) in self.tipsets.iter() {
893 if ts.parents() == tipset.key() {
894 to_remove.push(key.clone());
895 descendants.push(ts.clone());
896 }
897 }
898
899 for key in to_remove {
901 self.tipsets.remove(&key);
902 }
903
904 stack.extend(descendants);
906 }
907 }
908
909 fn try_mark_tipset_as_validated(&mut self, tipset: FullTipset, is_proposed_head: bool) -> bool {
910 if !self.is_parent_validated(&tipset) {
911 tracing::error!(epoch = %tipset.epoch(), tsk = %tipset.key(), parent_state = %tipset.parent_state(), "Parent tipset must be validated");
912 return false;
913 }
914
915 self.tipsets.remove(tipset.key());
916 let tipset = tipset.into_tipset();
917 if self.stateless_mode {
919 let epoch = tipset.epoch();
920 let terse_key = tipset.key().terse();
921 if self.cs.heaviest_tipset().weight() < tipset.weight() {
922 if let Err(e) = self.cs.set_heaviest_tipset(tipset) {
923 error!("Error setting heaviest tipset: {}", e);
924 return false;
925 } else {
926 info!("Heaviest tipset: {} ({})", epoch, terse_key);
927 }
928 }
929 } else if is_proposed_head {
930 if let Err(e) = self.cs.maybe_update_pending_head(&tipset) {
931 error!("Error putting tipset: {e}");
932 return false;
933 }
934 } else if let Err(e) = self.cs.set_heaviest_tipset(tipset) {
935 error!("Error setting heaviest tipset: {e}");
936 return false;
937 }
938 true
939 }
940
941 pub fn update(&mut self, event: SyncEvent) {
942 tracing::trace!("update: {event}");
943 match event {
944 SyncEvent::NewFullTipsets(tipsets) => {
945 for tipset in tipsets {
946 self.add_full_tipset(tipset);
947 }
948 }
949 SyncEvent::BadTipset(tipset) => self.mark_bad_tipset(tipset),
950 SyncEvent::ValidatedTipset {
951 tipset,
952 is_proposed_head,
953 } => {
954 if self.try_mark_tipset_as_validated(tipset, is_proposed_head)
955 && crate::utils::broadcast::has_subscribers(&self.validated_tipset_broadcast_tx)
956 && let Err(e) = self.validated_tipset_broadcast_tx.send(self.cs.heaviest_tipset().key().clone())
958 {
959 warn!("Failed to broadcast validated tipset: {e}");
960 }
961 }
962 }
963 }
964
965 pub fn tasks(&self) -> (Vec<SyncTask>, Vec<ForkSyncInfo>) {
966 let current_validated_epoch = self.cs.heaviest_tipset().epoch();
968 let now = Utc::now();
969
970 let mut active_sync_info = Vec::new();
971 let mut tasks = Vec::new();
972 for chain in self.chains() {
973 if let Some(first_ts) = chain.first() {
974 let last_ts = chain.last().expect("Infallible");
975 let stage: ForkSyncStage;
976 let start_time = Some(now);
977
978 if !self.is_ready_for_validation(first_ts) {
979 stage = ForkSyncStage::FetchingHeaders;
980 tasks.push(SyncTask::FetchTipset(
981 first_ts.parents().clone(),
982 first_ts.epoch(),
983 ));
984 } else {
985 stage = ForkSyncStage::ValidatingTipsets;
986 tasks.push(SyncTask::ValidateTipset {
987 tipset: first_ts.clone(),
988 is_proposed_head: chain.len() == 1,
989 });
990 }
991
992 let fork_info = ForkSyncInfo {
993 target_tipset_key: last_ts.key().clone(),
994 target_epoch: last_ts.epoch(),
995 target_sync_epoch_start: first_ts.epoch(),
996 stage,
997 validated_chain_head_epoch: current_validated_epoch,
998 start_time,
999 last_updated: Some(now),
1000 };
1001
1002 active_sync_info.push(fork_info);
1003 }
1004 }
1005 (tasks, active_sync_info)
1006 }
1007
1008 pub fn cleanup_dangling_forks(&mut self) {
1009 let finalized_epoch = self.cs.ec_calculator_finalized_epoch();
1010 for chain in self.chains() {
1011 if let Some(target) = chain.last()
1013 && target.epoch() < finalized_epoch
1014 {
1015 chain.iter().for_each(|ts| {
1016 self.tipsets.remove(ts.key());
1017 });
1018 tracing::info!(
1019 "Cleaned up dangling fork from epoch {} to {}",
1020 chain.first().map(|ts| ts.epoch()).unwrap_or_default(),
1021 chain.last().map(|ts| ts.epoch()).unwrap_or_default(),
1022 );
1023 }
1024 }
1025 }
1026}
1027
1028#[derive(PartialEq, Eq, Hash, Clone, Debug)]
1029enum SyncTask {
1030 ValidateTipset {
1031 tipset: FullTipset,
1032 is_proposed_head: bool,
1033 },
1034 FetchTipset(TipsetKey, ChainEpoch),
1035}
1036
1037impl std::fmt::Display for SyncTask {
1038 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1039 match self {
1040 SyncTask::ValidateTipset {
1041 tipset,
1042 is_proposed_head,
1043 } => write!(
1044 f,
1045 "ValidateTipset(epoch: {}, is_proposed_head: {is_proposed_head})",
1046 tipset.epoch()
1047 ),
1048 SyncTask::FetchTipset(key, epoch) => {
1049 let s = key.to_string();
1050 write!(
1051 f,
1052 "FetchTipset({}, epoch: {})",
1053 &s[s.len().saturating_sub(8)..],
1054 epoch
1055 )
1056 }
1057 }
1058 }
1059}
1060
1061impl SyncTask {
1062 async fn execute(
1063 self,
1064 network: SyncNetworkContext,
1065 state_manager: StateManager,
1066 stateless_mode: bool,
1067 bad_block_cache: Option<BadBlockCache>,
1068 ) -> Option<SyncEvent> {
1069 tracing::trace!("SyncTask::execute {self}");
1070 match self {
1071 SyncTask::ValidateTipset {
1072 tipset,
1073 is_proposed_head,
1074 } if stateless_mode => Some(SyncEvent::ValidatedTipset {
1075 tipset,
1076 is_proposed_head,
1077 }),
1078 SyncTask::ValidateTipset {
1079 tipset,
1080 is_proposed_head,
1081 } => match validate_tipset(&state_manager, tipset.clone(), bad_block_cache).await {
1082 Ok(()) => Some(SyncEvent::ValidatedTipset {
1083 tipset,
1084 is_proposed_head,
1085 }),
1086 Err(e) if matches!(e, TipsetSyncerError::TimeTravellingBlock { .. }) => {
1091 warn!("Time travelling block detected, skipping tipset for now: {e}");
1092 None
1093 }
1094 Err(e) if matches!(e, TipsetSyncerError::ParentChainStateMismatch(_)) => {
1097 warn!("Error validating tipset: {e}");
1098 let sm = state_manager.shallow_clone();
1099 match tokio::task::spawn_blocking(move || sm.repair_tipset_lookup())
1100 .await
1101 .unwrap_or_else(|e| Err(e.into()))
1102 {
1103 Ok(0) => Some(SyncEvent::BadTipset(tipset)),
1105 Ok(repaired) => {
1106 warn!(
1107 "Repaired {repaired} tipset lookup entries; retrying tipset validation at epoch {}",
1108 tipset.epoch()
1109 );
1110 None
1111 }
1112 Err(e) => {
1115 warn!("Failed to repair tipset lookup table: {e:#}");
1116 None
1117 }
1118 }
1119 }
1120 Err(e) => {
1121 warn!("Error validating tipset: {e}");
1122 Some(SyncEvent::BadTipset(tipset))
1123 }
1124 },
1125 SyncTask::FetchTipset(key, epoch) => {
1126 match get_full_tipset_batch(&network, state_manager.chain_store(), None, &key).await
1127 {
1128 Ok(parents) => Some(SyncEvent::NewFullTipsets(parents)),
1129 Err(e) => {
1130 tracing::debug!(%key, %epoch, "failed to fetch tipset: {e:#}");
1132 None
1133 }
1134 }
1135 }
1136 }
1137 }
1138}
1139
1140#[derive(Debug)]
1141struct SyncTasks(Arc<Mutex<HashSet<SyncTask>>>);
1142
1143#[derive(Debug)]
1144struct SyncStateMachineWrapper(Arc<Mutex<SyncStateMachine>>);
1145
1146mod metrics_collection {
1147 use super::*;
1148 use prometheus_client::{
1149 collector::Collector,
1150 encoding::{DescriptorEncoder, EncodeMetric},
1151 metrics::gauge::Gauge,
1152 registry::Unit,
1153 };
1154
1155 impl Collector for SyncTasks {
1156 fn encode(&self, mut encoder: DescriptorEncoder) -> Result<(), std::fmt::Error> {
1157 {
1158 let size_in_bytes = {
1159 let g: Gauge = Default::default();
1160 g.set(self.0.lock().allocation_size() as i64);
1161 g
1162 };
1163 let size_metric_encoder = encoder.encode_descriptor(
1164 "chain_follower_tasks_size",
1165 "Size of the chain follower tasks in bytes",
1166 Some(&Unit::Bytes),
1167 size_in_bytes.metric_type(),
1168 )?;
1169 size_in_bytes.encode(size_metric_encoder)?;
1170 }
1171 {
1172 let len = {
1173 let g: Gauge = Default::default();
1174 g.set(self.0.lock().len() as i64);
1175 g
1176 };
1177 let size_metric_encoder = encoder.encode_descriptor(
1178 "chain_follower_tasks_len",
1179 "Length of the chain follower tasks",
1180 None,
1181 len.metric_type(),
1182 )?;
1183 len.encode(size_metric_encoder)?;
1184 }
1185 {
1186 let cap = {
1187 let g: Gauge = Default::default();
1188 g.set(self.0.lock().capacity() as i64);
1189 g
1190 };
1191 let size_metric_encoder = encoder.encode_descriptor(
1192 "chain_follower_tasks_cap",
1193 "Capacity of the chain follower tasks",
1194 None,
1195 cap.metric_type(),
1196 )?;
1197 cap.encode(size_metric_encoder)?;
1198 }
1199
1200 Ok(())
1201 }
1202 }
1203
1204 impl Collector for SyncStateMachineWrapper {
1205 fn encode(&self, mut encoder: DescriptorEncoder) -> Result<(), std::fmt::Error> {
1206 {
1207 let size_in_bytes = {
1208 let g: Gauge = Default::default();
1209 g.set(self.0.lock().tipsets.allocation_size() as i64);
1210 g
1211 };
1212 let size_metric_encoder = encoder.encode_descriptor(
1213 "chain_follower_tipsets_size",
1214 "Size of the chain follower tipsets in bytes",
1215 Some(&Unit::Bytes),
1216 size_in_bytes.metric_type(),
1217 )?;
1218 size_in_bytes.encode(size_metric_encoder)?;
1219 }
1220 {
1221 let len = {
1222 let g: Gauge = Default::default();
1223 g.set(self.0.lock().tipsets.len() as i64);
1224 g
1225 };
1226 let size_metric_encoder = encoder.encode_descriptor(
1227 "chain_follower_tipsets_len",
1228 "Length of the chain follower tipsets",
1229 None,
1230 len.metric_type(),
1231 )?;
1232 len.encode(size_metric_encoder)?;
1233 }
1234 {
1235 let cap = {
1236 let g: Gauge = Default::default();
1237 g.set(self.0.lock().tipsets.capacity() as i64);
1238 g
1239 };
1240 let size_metric_encoder = encoder.encode_descriptor(
1241 "chain_follower_tipsets_cap",
1242 "Capacity of the chain follower tipsets",
1243 None,
1244 cap.metric_type(),
1245 )?;
1246 cap.encode(size_metric_encoder)?;
1247 }
1248
1249 Ok(())
1250 }
1251 }
1252}
1253
1254#[cfg(test)]
1255mod tests {
1256 use super::*;
1257 use crate::blocks::{Chain4U, HeaderBuilder, chain4u};
1258 use crate::db::MemoryDB;
1259 use crate::utils::db::CborStoreExt as _;
1260 use num_bigint::BigInt;
1261 use num_traits::ToPrimitive;
1262 use std::sync::Arc;
1263 use tracing::level_filters::LevelFilter;
1264 use tracing_subscriber::EnvFilter;
1265
1266 fn setup() -> (ChainStore, Chain4U<Arc<MemoryDB>>) {
1267 let _ = tracing_subscriber::fmt()
1269 .without_time()
1270 .with_env_filter(
1271 EnvFilter::builder()
1272 .with_default_directive(LevelFilter::DEBUG.into())
1273 .from_env()
1274 .unwrap(),
1275 )
1276 .try_init();
1277
1278 let db = Arc::new(MemoryDB::default());
1279
1280 let c4u = Chain4U::with_blockstore(db.clone());
1282 chain4u! {
1283 in c4u;
1284 [genesis_header = dummy_node(&db, 0)]
1285 };
1286
1287 let cs = ChainStore::new(db, Default::default(), genesis_header).unwrap();
1288
1289 cs.set_heaviest_tipset(cs.genesis_tipset()).unwrap();
1290
1291 (cs, c4u)
1292 }
1293
1294 fn dummy_state(db: impl Blockstore, i: ChainEpoch) -> Cid {
1295 db.put_cbor_default(&i).unwrap()
1296 }
1297
1298 fn dummy_node(db: impl Blockstore, i: ChainEpoch) -> HeaderBuilder {
1299 HeaderBuilder {
1300 state_root: dummy_state(db, i).into(),
1301 weight: BigInt::from(i).into(),
1302 epoch: i.into(),
1303 ..Default::default()
1304 }
1305 }
1306
1307 #[test]
1308 fn test_state_machine_validation_order() {
1309 let (cs, c4u) = setup();
1310 let db = cs.db_owned();
1311
1312 chain4u! {
1313 from [genesis_header] in c4u;
1314 [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)]
1315 };
1316
1317 let mut state_machine = SyncStateMachine::new(cs.shallow_clone(), Default::default(), true);
1319
1320 let tipsets = vec![e, b, d, c, a];
1322
1323 for block in tipsets {
1325 let full_tipset = FullTipset::new(vec![Block {
1326 header: block.clone().into(),
1327 bls_messages: vec![],
1328 secp_messages: vec![],
1329 }])
1330 .unwrap();
1331 state_machine.update(SyncEvent::NewFullTipsets(vec![full_tipset]));
1332 }
1333
1334 let mut validation_tasks = Vec::new();
1336 loop {
1337 let (tasks, _) = state_machine.tasks();
1338
1339 let validation_tipsets: Vec<_> = tasks
1341 .into_iter()
1342 .filter_map(|task| {
1343 if let SyncTask::ValidateTipset {
1344 tipset,
1345 is_proposed_head,
1346 } = task
1347 {
1348 Some((tipset, is_proposed_head))
1349 } else {
1350 None
1351 }
1352 })
1353 .collect();
1354
1355 if validation_tipsets.is_empty() {
1356 break;
1357 }
1358
1359 for (ts, is_proposed_head) in validation_tipsets {
1361 validation_tasks.push(ts.epoch());
1362 db.put_cbor_default(&ts.epoch()).unwrap();
1363 state_machine.try_mark_tipset_as_validated(ts, is_proposed_head);
1364 }
1365 }
1366
1367 assert_eq!(validation_tasks, vec![1, 2, 3, 4, 5]);
1369 }
1370
1371 #[test]
1372 fn test_sync_state_machine_chain_fragments() {
1373 let (cs, c4u) = setup();
1374 let db = cs.db();
1375
1376 chain4u! {
1380 in c4u;
1381 [a = dummy_node(db, 1)] -> [b = dummy_node(db, 2)]
1382 };
1383 chain4u! {
1384 from [a] in c4u;
1385 [c = dummy_node(db, 3)]
1386 };
1387
1388 let mut state_machine = SyncStateMachine::new(cs, Default::default(), false);
1390
1391 for block in [a, b, c] {
1393 let full_tipset = FullTipset::new(vec![Block {
1394 header: block.clone().into(),
1395 bls_messages: vec![],
1396 secp_messages: vec![],
1397 }])
1398 .unwrap();
1399 state_machine.update(SyncEvent::NewFullTipsets(vec![full_tipset]));
1400 }
1401
1402 let chains = state_machine
1403 .chains()
1404 .into_iter()
1405 .map(|v| {
1406 v.into_iter()
1407 .map(|ts| ts.weight().to_i64().unwrap_or(0))
1408 .collect_vec()
1409 })
1410 .collect_vec();
1411
1412 assert_eq!(chains, vec![vec![1, 3], vec![1, 2]]);
1414 }
1415}