Skip to content

Commit 19a5e2a

Browse files
authored
Update known blocks subcommand (#6604)
1 parent ca1ae7b commit 19a5e2a

File tree

3 files changed

+285
-1
lines changed

3 files changed

+285
-1
lines changed

.config/forest.dic

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
73
1+
75
22
Algorand/M
33
API/M
44
APIs
@@ -58,6 +58,7 @@ F3
5858
FFI
5959
FIL
6060
Filecoin/M
61+
Filfox
6162
Filops
6263
FIP
6364
FVM
@@ -147,4 +148,5 @@ VRF
147148
WebAssembly
148149
WebSocket
149150
WPoStProvingPeriodDeadlines
151+
YAML
150152
zstd

src/dev/subcommands/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// SPDX-License-Identifier: Apache-2.0, MIT
33

44
mod state_cmd;
5+
mod update_checkpoints_cmd;
56

67
use crate::cli_shared::cli::HELP_MESSAGE;
78
use crate::networks::generate_actor_bundle;
@@ -42,13 +43,17 @@ pub enum Subcommand {
4243
},
4344
#[command(subcommand)]
4445
State(state_cmd::StateCommand),
46+
/// Update known blocks (checkpoints), normally in `build/known_blocks.yaml`, by querying RPC
47+
/// endpoints
48+
UpdateCheckpoints(update_checkpoints_cmd::UpdateCheckpointsCommand),
4549
}
4650

4751
impl Subcommand {
4852
pub async fn run(self, _client: Client) -> anyhow::Result<()> {
4953
match self {
5054
Self::FetchTestSnapshots { actor_bundle } => fetch_test_snapshots(actor_bundle).await,
5155
Self::State(cmd) => cmd.run().await,
56+
Self::UpdateCheckpoints(cmd) => cmd.run().await,
5257
}
5358
}
5459
}
Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
// Copyright 2019-2026 ChainSafe Systems
2+
// SPDX-License-Identifier: Apache-2.0, MIT
3+
4+
use anyhow::Context as _;
5+
use cid::Cid;
6+
use clap::Parser;
7+
use indexmap::IndexMap;
8+
use serde::{Deserialize, Serialize};
9+
use std::path::PathBuf;
10+
use url::Url;
11+
12+
use crate::rpc::Client;
13+
use crate::rpc::prelude::*;
14+
use crate::rpc::types::ApiTipsetKey;
15+
use crate::shim::clock::ChainEpoch;
16+
17+
/// The interval between checkpoints (86400 epochs = 30 days)
18+
const CHECKPOINT_INTERVAL: ChainEpoch = 86400;
19+
20+
/// YAML structure for `known_blocks.yaml`
21+
/// Using `IndexMap` to preserve insertion order
22+
#[derive(Debug, Clone, Serialize, Deserialize)]
23+
struct 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+
/// Network selection for checkpoint updates
31+
#[derive(Debug, Clone, clap::ValueEnum)]
32+
pub enum Network {
33+
/// Update both calibnet and mainnet
34+
All,
35+
/// Update calibnet only
36+
Calibnet,
37+
/// Update mainnet only
38+
Mainnet,
39+
}
40+
41+
/// Update known blocks in `build/known_blocks.yaml` by querying RPC endpoints
42+
///
43+
/// This command finds and adds missing checkpoint entries at constant intervals
44+
/// by querying Filfox or other full-archive RPC nodes that support historical queries.
45+
#[derive(Debug, Parser)]
46+
pub struct UpdateCheckpointsCommand {
47+
/// Path to `known_blocks.yaml` file
48+
#[arg(long, default_value = "build/known_blocks.yaml")]
49+
known_blocks_file: PathBuf,
50+
51+
/// Mainnet RPC endpoint (Filfox recommended for full historical data)
52+
#[arg(long, default_value = "https://filfox.info")]
53+
mainnet_rpc: Url,
54+
55+
/// Calibnet RPC endpoint (Filfox recommended for full historical data)
56+
#[arg(long, default_value = "https://calibration.filfox.info")]
57+
calibnet_rpc: Url,
58+
59+
/// Which network(s) to update
60+
#[arg(long, default_value = "all")]
61+
network: Network,
62+
63+
/// Dry run - don't write changes to file
64+
#[arg(long)]
65+
dry_run: bool,
66+
}
67+
68+
impl 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+
110+
async 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+
// Map chain name for Beryx URL (calibnet -> calibration)
167+
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+
203+
/// Fetch a checkpoint at a specific height via RPC.
204+
///
205+
/// Returns `(actual_epoch, cid)` where `actual_epoch` might be slightly earlier than requested
206+
/// if there were no blocks at the exact requested height.
207+
async 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+
220+
fn 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+
244+
// Custom serde module for serializing/deserializing IndexMap<ChainEpoch, Cid> as strings
245+
mod 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+
}

0 commit comments

Comments
 (0)