|
| 1 | +// Copyright 2019-2025 ChainSafe Systems |
| 2 | +// SPDX-License-Identifier: Apache-2.0, MIT |
| 3 | +use crate::blocks::TipsetKey; |
| 4 | +use crate::lotus_json::lotus_json_with_self; |
| 5 | +use crate::networks::calculate_expected_epoch; |
| 6 | +use crate::shim::clock::ChainEpoch; |
| 7 | +use crate::state_manager::StateManager; |
| 8 | +use chrono::{DateTime, Utc}; |
| 9 | +use fvm_ipld_blockstore::Blockstore; |
| 10 | +use schemars::JsonSchema; |
| 11 | +use serde::{Deserialize, Serialize}; |
| 12 | +use std::fmt::Formatter; |
| 13 | +use std::sync::Arc; |
| 14 | + |
| 15 | +/// Represents the overall synchronization status of the Forest node. |
| 16 | +#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] |
| 17 | +pub enum NodeSyncStatus { |
| 18 | + /// Node is initializing, status not yet determined. |
| 19 | + #[default] |
| 20 | + Initializing, |
| 21 | + /// Node is significantly behind the network head and actively downloading/validating. |
| 22 | + Syncing, |
| 23 | + /// Node is close to the network head (e.g., within a configurable threshold like ~5 epochs). |
| 24 | + Synced, |
| 25 | + /// An error occurred during the sync process. |
| 26 | + Error(String), |
| 27 | + /// Node is configured to not sync (offline mode). |
| 28 | + Offline, |
| 29 | +} |
| 30 | + |
| 31 | +/// Represents the stage of processing for a specific chain fork being tracked. |
| 32 | +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)] |
| 33 | +pub enum ForkSyncStage { |
| 34 | + /// Fetching necessary block headers for this fork. |
| 35 | + FetchingHeaders, |
| 36 | + /// Validating tipsets and messages for this fork. |
| 37 | + ValidatingTipsets, |
| 38 | + /// This fork sync process is complete (e.g., reached target, merged, or deemed invalid). |
| 39 | + Complete, |
| 40 | + /// Progress is stalled, potentially waiting for dependencies. |
| 41 | + Stalled, |
| 42 | + /// An error occurred processing this specific fork. |
| 43 | + Error(String), |
| 44 | +} |
| 45 | + |
| 46 | +impl std::fmt::Display for ForkSyncStage { |
| 47 | + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { |
| 48 | + match self { |
| 49 | + ForkSyncStage::FetchingHeaders => write!(f, "Fetching Headers"), |
| 50 | + ForkSyncStage::ValidatingTipsets => write!(f, "Validating Tipsets"), |
| 51 | + ForkSyncStage::Complete => write!(f, "Complete"), |
| 52 | + ForkSyncStage::Stalled => write!(f, "Stalled"), |
| 53 | + ForkSyncStage::Error(e) => write!(f, "{}", format!("Error: {}", e)), |
| 54 | + } |
| 55 | + } |
| 56 | +} |
| 57 | + |
| 58 | +/// Contains information about a specific chain/fork the node is actively tracking or syncing. |
| 59 | +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)] |
| 60 | +pub struct ForkSyncInfo { |
| 61 | + /// The target tipset key for this synchronization task. |
| 62 | + #[schemars(with = "crate::lotus_json::LotusJson<TipsetKey>")] // Keep LotusJson for TipsetKey if needed |
| 63 | + #[serde(with = "crate::lotus_json")] |
| 64 | + pub(crate) target_tipset_key: TipsetKey, |
| 65 | + /// The target epoch for this synchronization task. |
| 66 | + pub(crate) target_epoch: ChainEpoch, |
| 67 | + /// The lowest epoch that still needs processing (fetching or validating) for this target. |
| 68 | + /// This helps indicate the start of the current sync range. |
| 69 | + pub(crate) target_sync_epoch_start: ChainEpoch, |
| 70 | + /// The current stage of processing for this fork. |
| 71 | + pub(crate) stage: ForkSyncStage, |
| 72 | + /// The epoch of the heaviest fully validated tipset on the node's main chain. |
| 73 | + /// This shows overall node progress, distinct from fork-specific progress. |
| 74 | + pub(crate) validated_chain_head_epoch: ChainEpoch, |
| 75 | + /// When processing for this fork started. |
| 76 | + pub(crate) start_time: Option<DateTime<Utc>>, |
| 77 | + /// Last time status for this fork was updated. |
| 78 | + pub(crate) last_updated: Option<DateTime<Utc>>, |
| 79 | +} |
| 80 | + |
| 81 | +#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, JsonSchema)] |
| 82 | +pub struct ForestSyncStatusReport { |
| 83 | + /// Overall status of the node's synchronization. |
| 84 | + pub status: NodeSyncStatus, |
| 85 | + /// The epoch of the heaviest validated tipset on the node's main chain. |
| 86 | + pub current_head_epoch: ChainEpoch, |
| 87 | + /// The tipset key of the current heaviest validated tipset. |
| 88 | + #[schemars(with = "crate::lotus_json::LotusJson<TipsetKey>")] |
| 89 | + #[serde(with = "crate::lotus_json")] |
| 90 | + pub current_head_key: Option<TipsetKey>, |
| 91 | + /// An estimation of the current highest epoch on the network. |
| 92 | + pub network_head_epoch: ChainEpoch, |
| 93 | + /// Estimated number of epochs the node is behind the network head. |
| 94 | + /// Can be negative if the node is slightly ahead, due to estimation variance. |
| 95 | + pub epochs_behind: i64, |
| 96 | + /// List of active fork synchronization tasks the node is currently handling. |
| 97 | + pub active_forks: Vec<ForkSyncInfo>, |
| 98 | + /// When the node process started. |
| 99 | + pub node_start_time: DateTime<Utc>, |
| 100 | + /// Last time this status report was generated. |
| 101 | + pub last_updated: DateTime<Utc>, |
| 102 | +} |
| 103 | + |
| 104 | +lotus_json_with_self!(ForestSyncStatusReport); |
| 105 | + |
| 106 | +impl ForestSyncStatusReport { |
| 107 | + pub(crate) fn new() -> Self { |
| 108 | + Self { |
| 109 | + node_start_time: Utc::now(), |
| 110 | + ..Default::default() |
| 111 | + } |
| 112 | + } |
| 113 | + |
| 114 | + pub(crate) fn set_current_chain_head(&mut self, tipset_key: TipsetKey, epoch: ChainEpoch) { |
| 115 | + self.current_head_key = Some(tipset_key); |
| 116 | + self.current_head_epoch = epoch; |
| 117 | + } |
| 118 | + |
| 119 | + pub(crate) fn set_network_head(&mut self, epoch: ChainEpoch) { |
| 120 | + self.network_head_epoch = epoch; |
| 121 | + } |
| 122 | + |
| 123 | + pub(crate) fn set_epochs_behind(&mut self, epochs_behind: i64) { |
| 124 | + self.epochs_behind = epochs_behind; |
| 125 | + } |
| 126 | + |
| 127 | + pub(crate) fn set_status(&mut self, status: NodeSyncStatus) { |
| 128 | + self.status = status; |
| 129 | + } |
| 130 | + |
| 131 | + pub(crate) fn set_active_forks(&mut self, active_forks: Vec<ForkSyncInfo>) { |
| 132 | + self.active_forks = active_forks; |
| 133 | + } |
| 134 | + |
| 135 | + pub(crate) fn update<DB: Blockstore + Sync + Send + 'static>( |
| 136 | + &mut self, |
| 137 | + state_manager: &Arc<StateManager<DB>>, |
| 138 | + current_active_forks: Vec<ForkSyncInfo>, |
| 139 | + stateless_mode: bool, |
| 140 | + ) { |
| 141 | + let heaviest = state_manager.chain_store().heaviest_tipset(); |
| 142 | + let current_chain_head_epoch = heaviest.epoch(); |
| 143 | + self.set_current_chain_head(heaviest.key().clone(), current_chain_head_epoch); |
| 144 | + let network_head_epoch = calculate_expected_epoch( |
| 145 | + Utc::now().timestamp() as u64, |
| 146 | + state_manager.chain_store().genesis_block_header().timestamp, |
| 147 | + state_manager.chain_config().block_delay_secs, |
| 148 | + ); |
| 149 | + |
| 150 | + self.set_network_head(network_head_epoch.clone() as ChainEpoch); |
| 151 | + self.set_epochs_behind(network_head_epoch as i64 - current_chain_head_epoch as i64); |
| 152 | + let seconds_per_epoch = state_manager.chain_config().block_delay_secs; |
| 153 | + let time_diff = (Utc::now().timestamp() as u64).saturating_sub(heaviest.min_timestamp()); |
| 154 | + |
| 155 | + match stateless_mode { |
| 156 | + true => self.set_status(NodeSyncStatus::Offline), |
| 157 | + false => { |
| 158 | + if time_diff < seconds_per_epoch as u64 * 5 { |
| 159 | + self.set_status(NodeSyncStatus::Synced) |
| 160 | + } else { |
| 161 | + self.set_status(NodeSyncStatus::Syncing) |
| 162 | + } |
| 163 | + } |
| 164 | + } |
| 165 | + self.set_active_forks(current_active_forks); |
| 166 | + self.last_updated = Utc::now(); |
| 167 | + } |
| 168 | +} |
0 commit comments