Skip to main content

forest/chain_sync/
sync_status.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3use crate::blocks::TipsetKey;
4use crate::lotus_json::lotus_json_with_self;
5use crate::networks::calculate_expected_epoch;
6use crate::shim::clock::ChainEpoch;
7use crate::state_manager::StateManager;
8use arc_swap::ArcSwap;
9use chrono::{DateTime, Utc};
10use schemars::JsonSchema;
11use serde::{Deserialize, Serialize};
12use std::sync::Arc;
13use tracing::log;
14
15// Node considered synced if the head is within this threshold.
16const SYNCED_EPOCH_THRESHOLD: u64 = 2;
17
18/// Represents the overall synchronization status of the Forest node.
19#[derive(
20    Serialize,
21    Deserialize,
22    Debug,
23    Clone,
24    Copy,
25    Default,
26    PartialEq,
27    Eq,
28    JsonSchema,
29    strum::Display,
30    strum::EnumString,
31)]
32pub enum NodeSyncStatus {
33    /// Node is initializing, status not yet determined.
34    #[default]
35    #[strum(to_string = "Intializing")]
36    Initializing,
37    /// Node is significantly behind the network head and actively downloading/validating.
38    #[strum(to_string = "Syncing")]
39    Syncing,
40    /// Node is close to the network head, within the `SYNCED_EPOCH_THRESHOLD`.
41    #[strum(to_string = "Synced")]
42    Synced,
43    /// An error occurred during the sync process.
44    #[strum(to_string = "Error")]
45    Error,
46    /// Node is configured to not sync (offline mode).
47    #[strum(to_string = "Offline")]
48    Offline,
49}
50
51/// Represents the stage of processing for a specific chain fork being tracked.
52#[derive(
53    Serialize,
54    Deserialize,
55    Debug,
56    Clone,
57    PartialEq,
58    Eq,
59    JsonSchema,
60    strum::Display,
61    strum::EnumString,
62)]
63pub enum ForkSyncStage {
64    /// Fetching necessary block headers for this fork.
65    #[strum(to_string = "Fetching Headers")]
66    FetchingHeaders,
67    /// Validating tipsets and messages for this fork.
68    #[strum(to_string = "Validating Tipsets")]
69    ValidatingTipsets,
70    /// This fork sync process is complete (e.g., reached target, merged, or deemed invalid).
71    #[strum(to_string = "Complete")]
72    Complete,
73    /// Progress is stalled, potentially waiting for dependencies.
74    #[strum(to_string = "Stalled")]
75    Stalled,
76    /// An error occurred processing this specific fork.
77    #[strum(to_string = "Error")]
78    Error,
79}
80
81/// Contains information about a specific chain/fork the node is actively tracking or syncing.
82#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)]
83pub struct ForkSyncInfo {
84    /// The target tipset key for this synchronization task.
85    #[schemars(with = "crate::lotus_json::LotusJson<TipsetKey>")]
86    #[serde(with = "crate::lotus_json")]
87    pub(crate) target_tipset_key: TipsetKey,
88    /// The target epoch for this synchronization task.
89    pub(crate) target_epoch: ChainEpoch,
90    /// The lowest epoch that still needs processing (fetching or validating) for this target.
91    /// This helps indicate the start of the current sync range.
92    pub(crate) target_sync_epoch_start: ChainEpoch,
93    /// The current stage of processing for this fork.
94    pub(crate) stage: ForkSyncStage,
95    /// The epoch of the heaviest fully validated tipset on the node's main chain.
96    /// This shows overall node progress, distinct from fork-specific progress.
97    pub(crate) validated_chain_head_epoch: ChainEpoch,
98    /// When processing for this fork started.
99    pub(crate) start_time: Option<DateTime<Utc>>,
100    /// Last time status for this fork was updated.
101    pub(crate) last_updated: Option<DateTime<Utc>>,
102}
103
104pub type SyncStatus = Arc<ArcSwap<SyncStatusReport>>;
105
106/// Contains information about the current status of the node's synchronization process.
107#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, JsonSchema)]
108pub struct SyncStatusReport {
109    /// Overall status of the node's synchronization.
110    pub(crate) status: NodeSyncStatus,
111    /// The epoch of the heaviest validated tipset on the node's main chain.
112    pub(crate) current_head_epoch: ChainEpoch,
113    /// The tipset key of the current heaviest validated tipset.
114    #[schemars(with = "crate::lotus_json::LotusJson<TipsetKey>")]
115    #[serde(with = "crate::lotus_json")]
116    pub(crate) current_head_key: Option<TipsetKey>,
117    // Current highest epoch on the network.
118    pub(crate) network_head_epoch: ChainEpoch,
119    /// Estimated number of epochs the node is behind the network head.
120    /// Can be negative if the node is slightly ahead, due to estimation variance.
121    pub(crate) epochs_behind: i64,
122    /// List of active fork synchronization tasks the node is currently handling.
123    pub(crate) active_forks: Vec<ForkSyncInfo>,
124    /// When the node process started.
125    pub(crate) node_start_time: DateTime<Utc>,
126    /// Last time this status report was generated.
127    pub(crate) last_updated: DateTime<Utc>,
128}
129
130lotus_json_with_self!(SyncStatusReport);
131
132impl SyncStatusReport {
133    pub(crate) fn init() -> Self {
134        Self {
135            node_start_time: Utc::now(),
136            ..Default::default()
137        }
138    }
139
140    /// Updates the sync status report based on the current state of the node and network.
141    /// This does not modify the existing report but returns a new one with updated information.
142    pub(crate) fn update(
143        &self,
144        state_manager: &StateManager,
145        active_forks: Vec<ForkSyncInfo>,
146        stateless_mode: bool,
147    ) -> Self {
148        let heaviest = state_manager.chain_store().heaviest_tipset();
149        let current_head_epoch = heaviest.epoch();
150        let current_head_key = Some(heaviest.key().clone());
151
152        let last_updated = Utc::now();
153        let last_updated_ts = last_updated.timestamp() as u64;
154        let seconds_per_epoch = state_manager.chain_config().block_delay_secs;
155        let network_head_epoch = calculate_expected_epoch(
156            last_updated_ts,
157            state_manager.chain_store().genesis_block_header().timestamp,
158            seconds_per_epoch,
159        );
160
161        let epochs_behind = network_head_epoch.saturating_sub(current_head_epoch);
162        log::trace!(
163            "Sync status report: current head epoch: {}, network head epoch: {}, epochs behind: {}",
164            current_head_epoch,
165            network_head_epoch,
166            epochs_behind
167        );
168
169        let time_diff = last_updated_ts.saturating_sub(heaviest.min_timestamp());
170        let status = match stateless_mode {
171            true => NodeSyncStatus::Offline,
172            false => {
173                if time_diff < u64::from(seconds_per_epoch) * SYNCED_EPOCH_THRESHOLD {
174                    NodeSyncStatus::Synced
175                } else {
176                    NodeSyncStatus::Syncing
177                }
178            }
179        };
180
181        Self {
182            node_start_time: self.node_start_time,
183            current_head_epoch,
184            current_head_key,
185            network_head_epoch,
186            epochs_behind,
187            status,
188            active_forks,
189            last_updated,
190        }
191    }
192
193    pub(crate) fn is_synced(&self) -> bool {
194        self.status == NodeSyncStatus::Synced
195    }
196
197    pub(crate) fn get_min_starting_block(&self) -> Option<ChainEpoch> {
198        self.active_forks
199            .iter()
200            .map(|fork_info| fork_info.target_sync_epoch_start)
201            .min()
202    }
203
204    #[cfg(test)]
205    pub fn with_status(mut self, status: NodeSyncStatus) -> Self {
206        self.status = status;
207        self
208    }
209
210    #[cfg(test)]
211    pub fn with_current_head_epoch(mut self, current_head_epoch: ChainEpoch) -> Self {
212        self.current_head_epoch = current_head_epoch;
213        self
214    }
215}