forest/dev/subcommands/
update_checkpoints_cmd.rs1use anyhow::Context as _;
5use cid::Cid;
6use clap::Parser;
7use indexmap::IndexMap;
8use serde::{Deserialize, Serialize};
9use std::path::PathBuf;
10use url::Url;
11
12use crate::rpc::Client;
13use crate::rpc::prelude::*;
14use crate::rpc::types::ApiTipsetKey;
15use crate::shim::clock::ChainEpoch;
16
17const CHECKPOINT_INTERVAL: ChainEpoch = 86400;
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
23struct KnownBlocks {
24 #[serde(with = "cid_string_map")]
25 calibnet: IndexMap<ChainEpoch, Cid>,
26 #[serde(with = "cid_string_map")]
27 mainnet: IndexMap<ChainEpoch, Cid>,
28}
29
30#[derive(Debug, Clone, clap::ValueEnum)]
32pub enum Network {
33 All,
35 Calibnet,
37 Mainnet,
39}
40
41#[derive(Debug, Parser)]
46pub struct UpdateCheckpointsCommand {
47 #[arg(long, default_value = "build/known_blocks.yaml")]
49 known_blocks_file: PathBuf,
50
51 #[arg(long, default_value = "https://filfox.info")]
53 mainnet_rpc: Url,
54
55 #[arg(long, default_value = "https://calibration.filfox.info")]
57 calibnet_rpc: Url,
58
59 #[arg(long, default_value = "all")]
61 network: Network,
62
63 #[arg(long)]
65 dry_run: bool,
66}
67
68impl UpdateCheckpointsCommand {
69 pub async fn run(self) -> anyhow::Result<()> {
70 let Self {
71 known_blocks_file,
72 mainnet_rpc,
73 calibnet_rpc,
74 network,
75 dry_run,
76 } = self;
77
78 println!("Reading known blocks from: {}", known_blocks_file.display());
79 let yaml_content = std::fs::read_to_string(&known_blocks_file)
80 .context("Failed to read known_blocks.yaml")?;
81 let mut known_blocks: KnownBlocks =
82 serde_yaml::from_str(&yaml_content).context("Failed to parse known_blocks.yaml")?;
83
84 if matches!(network, Network::All | Network::Calibnet) {
85 println!("\n=== Updating Calibnet Checkpoints ===");
86 let calibnet_client = Client::from_url(calibnet_rpc);
87 update_chain_checkpoints(&calibnet_client, &mut known_blocks.calibnet, "calibnet")
88 .await?;
89 }
90
91 if matches!(network, Network::All | Network::Mainnet) {
92 println!("\n=== Updating Mainnet Checkpoints ===");
93 let mainnet_client = Client::from_url(mainnet_rpc);
94 update_chain_checkpoints(&mainnet_client, &mut known_blocks.mainnet, "mainnet").await?;
95 }
96
97 if dry_run {
98 println!("\n=== Dry Run - Changes Not Written ===");
99 println!("Would write to: {}", known_blocks_file.display());
100 } else {
101 println!("\n=== Writing Updated Checkpoints ===");
102 write_known_blocks(&known_blocks_file, &known_blocks)?;
103 println!("Successfully updated: {}", known_blocks_file.display());
104 }
105
106 Ok(())
107 }
108}
109
110async fn update_chain_checkpoints(
111 client: &Client,
112 checkpoints: &mut IndexMap<ChainEpoch, Cid>,
113 chain_name: &str,
114) -> anyhow::Result<()> {
115 println!("Fetching chain head for {chain_name}...");
116 let head = ChainHead::call(client, ())
117 .await
118 .context("Failed to get chain head")?;
119
120 let current_epoch = head.epoch();
121 println!("Current epoch: {}", current_epoch);
122
123 let latest_checkpoint_epoch = (current_epoch / CHECKPOINT_INTERVAL) * CHECKPOINT_INTERVAL;
124
125 let existing_max_epoch = checkpoints.keys().max().copied().unwrap_or(0);
126 println!("Existing max checkpoint epoch: {existing_max_epoch}");
127 println!("Latest checkpoint epoch should be: {latest_checkpoint_epoch}");
128
129 if latest_checkpoint_epoch <= existing_max_epoch {
130 println!("No new checkpoints needed (already up to date)");
131 return Ok(());
132 }
133
134 let mut needed_epochs = Vec::new();
135 let mut epoch = existing_max_epoch + CHECKPOINT_INTERVAL;
136 while epoch <= latest_checkpoint_epoch {
137 if !checkpoints.contains_key(&epoch) {
138 needed_epochs.push(epoch);
139 }
140 epoch += CHECKPOINT_INTERVAL;
141 }
142
143 if needed_epochs.is_empty() {
144 println!("No missing checkpoints to add");
145 return Ok(());
146 }
147
148 println!("Need to add {} checkpoint(s)", needed_epochs.len());
149
150 println!("Fetching checkpoints via RPC...");
151 let mut found_checkpoints: IndexMap<ChainEpoch, Cid> = IndexMap::new();
152
153 for &requested_epoch in &needed_epochs {
154 match fetch_checkpoint_at_height(client, requested_epoch).await {
155 Ok((actual_epoch, cid)) => {
156 found_checkpoints.insert(actual_epoch, cid);
157
158 if actual_epoch != requested_epoch {
159 println!(
160 " ✓ Epoch {actual_epoch} (requested {requested_epoch}, no blocks at exact height): {cid}"
161 );
162 } else {
163 println!(" ✓ Epoch {}: {}", actual_epoch, cid);
164 }
165
166 let beryx_network = if chain_name == "calibnet" {
168 "calibration"
169 } else {
170 chain_name
171 };
172 println!(" Verify at: https://beryx.io/fil/{beryx_network}/block-cid/{cid}",);
173 }
174 Err(e) => {
175 println!(" ✗ Epoch {requested_epoch}: {e}");
176 }
177 }
178 }
179
180 let num_found = found_checkpoints.len();
181 println!("\nAdding {num_found} new checkpoint(s) to the file...");
182
183 let mut sorted_checkpoints: Vec<_> = found_checkpoints.into_iter().collect();
184 sorted_checkpoints.sort_by_key(|(epoch, _)| std::cmp::Reverse(*epoch));
185
186 let mut new_map = IndexMap::new();
187 for (epoch, cid) in sorted_checkpoints {
188 new_map.insert(epoch, cid);
189 }
190 new_map.extend(checkpoints.drain(..));
191 *checkpoints = new_map;
192
193 if num_found < needed_epochs.len() {
194 anyhow::bail!(
195 "Only found {num_found} out of {} needed checkpoints. Consider using an RPC provider with full historical data (e.g., Filfox).",
196 needed_epochs.len()
197 );
198 }
199
200 Ok(())
201}
202
203async fn fetch_checkpoint_at_height(
208 client: &Client,
209 epoch: ChainEpoch,
210) -> anyhow::Result<(ChainEpoch, Cid)> {
211 let tipset = ChainGetTipSetByHeight::call(client, (epoch, ApiTipsetKey(None)))
212 .await
213 .context("ChainGetTipSetByHeight RPC call failed")?;
214
215 let actual_epoch = tipset.epoch();
216 let first_block_cid = tipset.block_headers().first().cid();
217 Ok((actual_epoch, *first_block_cid))
218}
219
220fn write_known_blocks(path: &PathBuf, known_blocks: &KnownBlocks) -> anyhow::Result<()> {
221 let mut output = String::new();
222
223 output.push_str("# This file is auto-generated by `forest-dev update-checkpoints` command.\n");
224 output.push_str("# Do not edit manually. Run the command to update checkpoints.\n\n");
225
226 output.push_str("calibnet:\n");
227 for (epoch, cid) in &known_blocks.calibnet {
228 output.push_str(&format!(" {epoch}: {cid}\n"));
229 }
230
231 output.push_str("mainnet:\n");
232 for (epoch, cid) in &known_blocks.mainnet {
233 output.push_str(&format!(" {epoch}: {cid}\n"));
234 }
235
236 std::fs::write(path, output).context(format!(
237 "Failed to write updated known blocks to {}",
238 path.display()
239 ))?;
240
241 Ok(())
242}
243
244mod cid_string_map {
246 use super::*;
247 use serde::de::{Deserialize, Deserializer};
248 use serde::ser::Serializer;
249 use std::str::FromStr;
250
251 pub fn serialize<S>(map: &IndexMap<ChainEpoch, Cid>, serializer: S) -> Result<S::Ok, S::Error>
252 where
253 S: Serializer,
254 {
255 use serde::ser::SerializeMap;
256 let mut ser_map = serializer.serialize_map(Some(map.len()))?;
257 for (k, v) in map {
258 ser_map.serialize_entry(k, &v.to_string())?;
259 }
260 ser_map.end()
261 }
262
263 pub fn deserialize<'de, D>(deserializer: D) -> Result<IndexMap<ChainEpoch, Cid>, D::Error>
264 where
265 D: Deserializer<'de>,
266 {
267 let string_map: IndexMap<ChainEpoch, String> = IndexMap::deserialize(deserializer)?;
268 string_map
269 .into_iter()
270 .map(|(k, v)| {
271 Cid::from_str(&v)
272 .map(|cid| (k, cid))
273 .map_err(serde::de::Error::custom)
274 })
275 .collect()
276 }
277}