Skip to main content

forest/rpc/methods/
node.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4use std::time::{Duration, SystemTime, UNIX_EPOCH};
5
6use crate::{
7    lotus_json::lotus_json_with_self,
8    networks::calculate_expected_epoch,
9    rpc::{ApiPaths, Ctx, Permission, RpcMethod, ServerError},
10    shim::clock::EPOCH_DURATION_SECONDS,
11};
12use anyhow::ensure;
13use enumflags2::BitFlags;
14use schemars::JsonSchema;
15use serde::{Deserialize, Serialize};
16
17/// Epochs the node is behind the head tipset, matching Lotus' `NodeStatus`
18/// (`Behind` is in epochs, not seconds). Clock skew of up to one epoch (head
19/// ahead of the local clock) clamps to `0`; beyond that the value is
20/// meaningless, so an error is returned instead.
21fn epochs_behind_head(head_timestamp: u64, now_secs: u64) -> anyhow::Result<u64> {
22    ensure!(
23        head_timestamp <= now_secs + EPOCH_DURATION_SECONDS as u64,
24        "Head tipset timestamp is more than one epoch ahead of system time, please sync the system clock."
25    );
26    Ok(calculate_expected_epoch(now_secs, head_timestamp, EPOCH_DURATION_SECONDS as u32) as u64)
27}
28
29pub enum NodeStatus {}
30impl RpcMethod<1> for NodeStatus {
31    const NAME: &'static str = "Filecoin.NodeStatus";
32    const PARAM_NAMES: [&'static str; 1] = ["inclChainStatus"];
33    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
34    const PERMISSION: Permission = Permission::Read;
35    const DESCRIPTION: &'static str =
36        "Returns the node's status, including sync and chain health information.";
37
38    type Params = (bool,);
39    type Ok = NodeStatusResult;
40
41    async fn handle(
42        ctx: Ctx,
43        (incl_chain_status,): Self::Params,
44        _: &http::Extensions,
45    ) -> Result<Self::Ok, ServerError> {
46        let mut node_status = NodeStatusResult::default();
47
48        let head = ctx.chain_store().heaviest_tipset();
49        let cur_duration: Duration = SystemTime::now().duration_since(UNIX_EPOCH)?;
50
51        let chain_finality = ctx.chain_config().policy.chain_finality;
52
53        node_status.sync_status.epoch = head.epoch() as u64;
54        node_status.sync_status.behind =
55            epochs_behind_head(head.min_timestamp(), cur_duration.as_secs())?;
56
57        if incl_chain_status && head.epoch() > chain_finality {
58            let mut block_count = 0;
59            let mut ts = head;
60
61            for _ in 0..100 {
62                block_count += ts.block_headers().len();
63                let tsk = ts.parents();
64                ts = ctx.chain_index().load_required_tipset(tsk)?;
65            }
66
67            node_status.chain_status.blocks_per_tipset_last_100 = block_count as f64 / 100.;
68
69            for _ in 100..chain_finality {
70                block_count += ts.block_headers().len();
71                let tsk = ts.parents();
72                ts = ctx.chain_index().load_required_tipset(tsk)?;
73            }
74
75            node_status.chain_status.blocks_per_tipset_last_finality =
76                block_count as f64 / chain_finality as f64;
77        }
78
79        Ok(node_status)
80    }
81}
82
83#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default, Clone, JsonSchema)]
84#[serde(rename_all = "PascalCase")]
85pub struct NodeSyncStatus {
86    pub epoch: u64,
87    pub behind: u64,
88}
89lotus_json_with_self!(NodeSyncStatus);
90
91#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default, Clone, JsonSchema)]
92#[serde(rename_all = "PascalCase")]
93pub struct NodePeerStatus {
94    pub peers_to_publish_msgs: u32,
95    pub peers_to_publish_blocks: u32,
96}
97lotus_json_with_self!(NodePeerStatus);
98
99#[derive(Debug, PartialEq, Serialize, Deserialize, Default, Clone, JsonSchema)]
100#[serde(rename_all = "PascalCase")]
101pub struct NodeChainStatus {
102    pub blocks_per_tipset_last_100: f64,
103    pub blocks_per_tipset_last_finality: f64,
104}
105lotus_json_with_self!(NodeChainStatus);
106
107#[derive(Debug, Deserialize, Default, Serialize, Clone, JsonSchema, PartialEq)]
108#[serde(rename_all = "PascalCase")]
109pub struct NodeStatusResult {
110    pub sync_status: NodeSyncStatus,
111    pub peer_status: NodePeerStatus,
112    pub chain_status: NodeChainStatus,
113}
114lotus_json_with_self!(NodeStatusResult);
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn epochs_behind_head_is_reported_in_epochs() {
122        let epoch = EPOCH_DURATION_SECONDS as u64;
123        assert_eq!(epochs_behind_head(1_000, 1_000).unwrap(), 0);
124        assert_eq!(epochs_behind_head(1_000, 1_000 + epoch).unwrap(), 1);
125        assert_eq!(epochs_behind_head(1_000, 1_000 + 10 * epoch).unwrap(), 10);
126        assert_eq!(epochs_behind_head(1_000, 1_000 + epoch - 1).unwrap(), 0);
127        assert_eq!(epochs_behind_head(1_000, 1_000 + 2 * epoch - 1).unwrap(), 1);
128    }
129
130    #[test]
131    fn epochs_behind_head_tolerates_skew_up_to_one_epoch() {
132        let epoch = EPOCH_DURATION_SECONDS as u64;
133        assert_eq!(epochs_behind_head(1_001, 1_000).unwrap(), 0);
134        assert_eq!(epochs_behind_head(1_000 + epoch, 1_000).unwrap(), 0);
135    }
136
137    #[test]
138    fn epochs_behind_head_errors_on_skew_above_one_epoch() {
139        let epoch = EPOCH_DURATION_SECONDS as u64;
140        assert!(epochs_behind_head(1_000 + epoch + 1, 1_000).is_err());
141    }
142}