Skip to main content

forest/rpc/methods/
sync.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4mod types;
5
6use crate::blocks::{Block, FullTipset, GossipBlock};
7use crate::chain;
8use crate::chain_sync::{BlockValidationOutcome, SyncStatusReport, TipsetValidator};
9use crate::libp2p::{IdentTopic, NetworkMessage, PUBSUB_BLOCK_STR};
10use crate::prelude::*;
11use crate::rpc::{ApiPaths, Ctx, Permission, RpcMethod, ServerError};
12use enumflags2::BitFlags;
13use fvm_ipld_encoding::to_vec;
14use std::time::Duration;
15use tokio::sync::broadcast::error::RecvError;
16pub use types::*;
17
18pub enum SyncCheckBad {}
19impl RpcMethod<1> for SyncCheckBad {
20    const NAME: &'static str = "Filecoin.SyncCheckBad";
21    const PARAM_NAMES: [&'static str; 1] = ["cid"];
22    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
23    const PERMISSION: Permission = Permission::Read;
24    const DESCRIPTION: &'static str =
25        "Returns the reason the given block is marked bad, or an empty string if it is not.";
26
27    type Params = (Cid,);
28    type Ok = String;
29
30    async fn handle(
31        ctx: Ctx,
32        (cid,): Self::Params,
33        _: &http::Extensions,
34    ) -> Result<Self::Ok, ServerError> {
35        Ok(ctx
36            .bad_blocks
37            .as_ref()
38            .context("bad block cache is disabled")?
39            .get(&cid)
40            .map(|_| "bad".to_string())
41            .unwrap_or_default())
42    }
43}
44
45pub enum SyncMarkBad {}
46impl RpcMethod<1> for SyncMarkBad {
47    const NAME: &'static str = "Filecoin.SyncMarkBad";
48    const PARAM_NAMES: [&'static str; 1] = ["cid"];
49    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
50    const PERMISSION: Permission = Permission::Admin;
51    const DESCRIPTION: &'static str = "Marks the block with the given CID as bad.";
52
53    type Params = (Cid,);
54    type Ok = ();
55
56    async fn handle(
57        ctx: Ctx,
58        (cid,): Self::Params,
59        _: &http::Extensions,
60    ) -> Result<Self::Ok, ServerError> {
61        ctx.bad_blocks
62            .as_ref()
63            .context("bad block cache is disabled")?
64            .push(cid);
65        Ok(())
66    }
67}
68
69pub enum SyncSnapshotProgress {}
70impl RpcMethod<0> for SyncSnapshotProgress {
71    const NAME: &'static str = "Forest.SyncSnapshotProgress";
72    const PARAM_NAMES: [&'static str; 0] = [];
73    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
74    const PERMISSION: Permission = Permission::Read;
75    const DESCRIPTION: &'static str =
76        "Returns the snapshot download progress. Return Null if the tracking isn't started";
77
78    type Params = ();
79    type Ok = SnapshotProgressState;
80
81    async fn handle(
82        ctx: Ctx,
83        (): Self::Params,
84        _: &http::Extensions,
85    ) -> Result<Self::Ok, ServerError> {
86        Ok(ctx.get_snapshot_progress_tracker())
87    }
88}
89
90pub enum SyncStatus {}
91impl RpcMethod<0> for SyncStatus {
92    const NAME: &'static str = "Forest.SyncStatus";
93    const PARAM_NAMES: [&'static str; 0] = [];
94    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
95    const PERMISSION: Permission = Permission::Read;
96    const DESCRIPTION: &'static str = "Returns the current sync status of the node.";
97
98    type Params = ();
99    type Ok = Arc<SyncStatusReport>;
100
101    async fn handle(
102        ctx: Ctx,
103        (): Self::Params,
104        _: &http::Extensions,
105    ) -> Result<Self::Ok, ServerError> {
106        let sync_status = ctx.sync_status.load().shallow_clone();
107        Ok(sync_status)
108    }
109}
110
111pub enum SyncSubmitBlock {}
112impl RpcMethod<1> for SyncSubmitBlock {
113    const NAME: &'static str = "Filecoin.SyncSubmitBlock";
114    const PARAM_NAMES: [&'static str; 1] = ["block"];
115    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
116    const PERMISSION: Permission = Permission::Write;
117    const DESCRIPTION: &'static str = "Submits a newly created block to the network.";
118
119    type Params = (GossipBlock,);
120    type Ok = ();
121
122    // NOTE: This currently skips all the sanity-checks and directly passes the message onto the
123    // swarm.
124    async fn handle(
125        ctx: Ctx,
126        (block_msg,): Self::Params,
127        _: &http::Extensions,
128    ) -> Result<Self::Ok, ServerError> {
129        let genesis_network_name = ctx.chain_config().network.genesis_name();
130        let encoded_message = to_vec(&block_msg)?;
131        let pubsub_block_str = format!("{PUBSUB_BLOCK_STR}/{genesis_network_name}");
132        let (bls_messages, secp_messages) =
133            chain::store::block_messages(ctx.db(), &block_msg.header)?;
134        let block_cid = *block_msg.header.cid();
135        let block = Block {
136            header: block_msg.header,
137            bls_messages,
138            secp_messages,
139        };
140        let ts = FullTipset::from(block);
141        let genesis_ts = ctx.chain_store().genesis_tipset();
142
143        TipsetValidator(&ts)
144            .validate(
145                ctx.chain_store(),
146                ctx.bad_blocks.as_ref(),
147                &genesis_ts,
148                ctx.chain_config().block_delay_secs,
149            )
150            .context("failed to validate the tipset")?;
151
152        // Subscribe before injecting the tipset so the follower's verdict cannot be missed.
153        let mut outcomes = ctx.block_validation_subscriber.subscribe();
154        ctx.tipset_send
155            .try_send(ts)
156            .context("tipset queue is full")?;
157
158        // Forest applies the tipset via the async follower, unlike Lotus whose `Syncer.Sync` is
159        // synchronous. Wait (bounded by ~one block time) for the follower's verdict; for a block
160        // that extends the head (the mining case) an applied verdict means the head has advanced,
161        // so lotus-miner does not re-select the same base.
162        let block_delay_secs = ctx.chain_config().block_delay_secs.into();
163        let verdict = tokio::time::timeout(Duration::from_secs(block_delay_secs), async {
164            loop {
165                match outcomes.recv().await {
166                    Ok((cid, outcome)) if cid == block_cid => return Some(outcome),
167                    Ok(_) | Err(RecvError::Lagged(_)) => {}
168                    Err(RecvError::Closed) => return None,
169                }
170            }
171        })
172        .await;
173
174        // Publish the block unless the follower definitively rejected it: Lotus publishes after a
175        // successful `Syncer.Sync` and errors on failure, and on no verdict within a block time we
176        // still publish best-effort (the block passed the synchronous `TipsetValidator` and is not
177        // known-bad; gossiping an unincluded block is not slashable, peers just reject it).
178        match verdict {
179            Ok(Some(BlockValidationOutcome::Rejected)) => {
180                return Err(anyhow::anyhow!(
181                    "submitted block {block_cid} was rejected during validation"
182                )
183                .into());
184            }
185            Ok(Some(BlockValidationOutcome::Applied)) | Ok(None) => {}
186            Err(_elapsed) => tracing::warn!(
187                %block_cid,
188                block_delay_secs,
189                "SyncSubmitBlock: no validation verdict within one block time; publishing best-effort"
190            ),
191        }
192        ctx.network_send().send(NetworkMessage::PubsubMessage {
193            topic: IdentTopic::new(pubsub_block_str),
194            message: encoded_message,
195        })?;
196        Ok(())
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use std::sync::Arc;
203
204    use super::*;
205    use crate::chain_sync::NodeSyncStatus;
206    use crate::libp2p::NetworkMessage;
207    use crate::rpc::RPCState;
208    use crate::rpc::test_utils::chain_store;
209
210    fn ctx() -> (Arc<RPCState>, flume::Receiver<NetworkMessage>) {
211        RPCState::for_tests(chain_store()).unwrap()
212    }
213
214    #[tokio::test]
215    async fn set_check_bad() {
216        let (ctx, _) = ctx();
217
218        let cid = "bafy2bzacea3wsdh6y3a36tb3skempjoxqpuyompjbmfeyf34fi3uy6uue42v4"
219            .parse::<Cid>()
220            .unwrap();
221
222        let reason = SyncCheckBad::handle(ctx.clone(), (cid,), &Default::default())
223            .await
224            .unwrap();
225        assert_eq!(reason, "");
226
227        // Mark that block as bad manually and check again to verify
228        SyncMarkBad::handle(ctx.clone(), (cid,), &Default::default())
229            .await
230            .unwrap();
231
232        let reason = SyncCheckBad::handle(ctx.clone(), (cid,), &Default::default())
233            .await
234            .unwrap();
235        assert_eq!(reason, "bad");
236    }
237
238    #[tokio::test]
239    async fn sync_status_test() {
240        let (ctx, _) = ctx();
241
242        let st_copy = ctx.sync_status.clone();
243
244        let sync_status = SyncStatus::handle(ctx.clone(), (), &Default::default())
245            .await
246            .unwrap();
247        assert_eq!(sync_status, st_copy.load().clone());
248
249        // update cloned state
250        st_copy.store(
251            st_copy
252                .load()
253                .as_ref()
254                .clone()
255                .with_status(NodeSyncStatus::Syncing)
256                .with_current_head_epoch(4)
257                .into(),
258        );
259
260        let sync_status = SyncStatus::handle(ctx.clone(), (), &Default::default())
261            .await
262            .unwrap();
263
264        assert_eq!(sync_status, st_copy.load().clone());
265    }
266}