Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@

### Added

- [#5461](https://github.com/ChainSafe/forest/pull/5461) Add `forest-tool shed migrate-state` command.

### Changed

- [#5452](https://github.com/ChainSafe/forest/pull/5452) Speed up void database migration.
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/developers/guides/network_upgrades.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ This provides the base for the state migrations and network-aware node changes.
State migrations are described in detail in the relevant FIPs, including the steps required to perform them. Note that naive state migrations might take a significant amount of time and resources. It is up to the implementation team to decide whether to optimize them.

:::note
Testing the state migration on a relevant network is crucial before the upgrade epoch. This is done by changing the upgrade epoch in both Lotus and Forest and ensuring both migrations produce the same state root. This is done locally, but it might be facilitated in the future.
Testing the state migration on a relevant network is crucial before the upgrade epoch. This could be done by either changing the upgrade epoch in both Lotus and Forest and ensuring both migrations produce the same state root, or comparing the output of `forest-tool shed migrate-state` and `lotus-shed migrate-state` commands.

This also allows for assessing the duration of the state migration and determining whether it is feasible to perform it on the mainnet.
:::
Expand Down
2 changes: 1 addition & 1 deletion src/blocks/tipset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -504,7 +504,7 @@ impl FullTipset {
pub fn parent_state(&self) -> &Cid {
&self.first_block().header().state_root
}
/// Returns the state root for the tipset parent.
/// Returns the keys of the parents of the blocks in the tipset.
pub fn parents(&self) -> &TipsetKey {
&self.first_block().header().parents
}
Expand Down
17 changes: 17 additions & 0 deletions src/shim/version.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright 2019-2025 ChainSafe Systems
// SPDX-License-Identifier: Apache-2.0, MIT
use std::fmt;
use std::ops::{Deref, DerefMut};
use std::str::FromStr;

use crate::lotus_json::lotus_json_with_self;

Expand Down Expand Up @@ -121,6 +123,21 @@ impl From<NetworkVersion> for NetworkVersion_v4 {
}
}

impl FromStr for NetworkVersion {
type Err = <u32 as FromStr>::Err;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let v: u32 = s.parse()?;
Ok(v.into())
}
}

impl fmt::Display for NetworkVersion {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.0.fmt(f)
}
}

#[cfg(test)]
impl quickcheck::Arbitrary for NetworkVersion {
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
Expand Down
31 changes: 19 additions & 12 deletions src/state_migration/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,11 @@ mod type_migrations;

type RunMigration<DB> = fn(&ChainConfig, &Arc<DB>, &Cid, ChainEpoch) -> anyhow::Result<Cid>;

/// Run state migrations
pub fn run_state_migrations<DB>(
epoch: ChainEpoch,
chain_config: &ChainConfig,
db: &Arc<DB>,
parent_state: &Cid,
) -> anyhow::Result<Option<Cid>>
pub fn get_migrations<DB>(chain: &NetworkChain) -> Vec<(Height, RunMigration<DB>)>
where
DB: Blockstore + Send + Sync,
{
let mappings: Vec<(_, RunMigration<DB>)> = match chain_config.network {
match chain {
NetworkChain::Mainnet => {
vec![
(Height::Shark, nv17::run_migration::<DB>),
Expand Down Expand Up @@ -82,7 +76,20 @@ where
(Height::Teep, nv25::run_migration::<DB>),
]
}
};
}
}

/// Run state migrations
pub fn run_state_migrations<DB>(
epoch: ChainEpoch,
chain_config: &ChainConfig,
db: &Arc<DB>,
parent_state: &Cid,
) -> anyhow::Result<Option<Cid>>
where
DB: Blockstore + Send + Sync,
{
let mappings = get_migrations(&chain_config.network);

// Make sure bundle is defined.
static BUNDLE_CHECKED: AtomicBool = AtomicBool::new(false);
Expand All @@ -106,7 +113,7 @@ where
tracing::info!("Running {height} migration at epoch {epoch}");
let start_time = std::time::Instant::now();
let new_state = migrate(chain_config, db, parent_state, epoch)?;
let elapsed = start_time.elapsed().as_secs_f32();
let elapsed = start_time.elapsed();
// `new_state_actors` is the Go state migration output, log for comparision
let new_state_actors = db
.get_cbor::<StateRoot>(&new_state)
Expand All @@ -116,9 +123,9 @@ where
.unwrap_or_default();
if new_state != *parent_state {
crate::utils::misc::reveal_upgrade_logo(height.into());
tracing::info!("State migration at height {height}(epoch {epoch}) was successful, Previous state: {parent_state}, new state: {new_state}, new state actors: {new_state_actors}. Took: {elapsed}s.");
tracing::info!("State migration at height {height}(epoch {epoch}) was successful, Previous state: {parent_state}, new state: {new_state}, new state actors: {new_state_actors}. Took: {elapsed}.", elapsed = humantime::format_duration(elapsed));
} else {
anyhow:: bail!("State post migration at height {height} must not match. Previous state: {parent_state}, new state: {new_state}, new state actors: {new_state_actors}. Took {elapsed}s.");
anyhow:: bail!("State post migration at height {height} must not match. Previous state: {parent_state}, new state: {new_state}, new state actors: {new_state_actors}. Took {elapsed}.", elapsed = humantime::format_duration(elapsed));
}

return Ok(Some(new_state));
Expand Down
1 change: 0 additions & 1 deletion src/state_migration/nv25/migration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ impl<BS: Blockstore> StateMigration<BS> {
}

/// Runs the migration for `NV25`. Returns the new state root.
#[allow(dead_code)]
pub fn run_migration<DB>(
chain_config: &ChainConfig,
blockstore: &Arc<DB>,
Expand Down
5 changes: 5 additions & 0 deletions src/tool/subcommands/shed_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

mod f3;
use f3::*;
mod migration;
use migration::*;

use crate::{
libp2p::keypair::get_keypair,
Expand Down Expand Up @@ -67,6 +69,8 @@ pub enum ShedCommands {
/// F3 related commands.
#[command(subcommand)]
F3(F3Commands),
/// Run a network upgrade migration
MigrateState(MigrateStateCommand),
}

#[derive(Debug, Clone, ValueEnum, PartialEq)]
Expand Down Expand Up @@ -171,6 +175,7 @@ impl ShedCommands {
println!("{}", serde_json::to_string_pretty(&openrpc_doc)?);
}
ShedCommands::F3(cmd) => cmd.run(client).await?,
ShedCommands::MigrateState(cmd) => cmd.run(client).await?,
}
Ok(())
}
Expand Down
93 changes: 93 additions & 0 deletions src/tool/subcommands/shed_cmd/migration.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Copyright 2019-2025 ChainSafe Systems
// SPDX-License-Identifier: Apache-2.0, MIT

use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Instant;

use cid::Cid;
use clap::Args;
use itertools::Itertools;

use crate::utils::db::CborStoreExt;
use crate::{
blocks::CachingBlockHeader,
cli_shared::{chain_path, read_config},
daemon::db_util::load_all_forest_cars,
db::{
car::ManyCar,
db_engine::{db_root, open_db},
parity_db::ParityDb,
CAR_DB_DIR_NAME,
},
networks::{ChainConfig, NetworkChain},
shim::version::NetworkVersion,
};

#[derive(Debug, Args)]
pub struct MigrateStateCommand {
/// Target network version
network_version: NetworkVersion,
/// Block to look back from
block_to_look_back: Cid,
/// Path to the Forest database folder
#[arg(long)]
db: Option<PathBuf>,
/// Filecoin network chain
#[arg(long, required = true)]
chain: NetworkChain,
}

impl MigrateStateCommand {
pub async fn run(self, _: crate::rpc::Client) -> anyhow::Result<()> {
let Self {
network_version,
block_to_look_back,
db,
chain,
} = self;
let db = {
let db = if let Some(db) = db {
db
} else {
let (_, config) = read_config(None, Some(chain.clone()))?;
db_root(&chain_path(&config))?
};
load_db(&db)?
};
let block: CachingBlockHeader = db.get_cbor_required(&block_to_look_back)?;
let chain_config = Arc::new(ChainConfig::from_chain(&chain));
let mut state_root = block.state_root;
let epoch = block.epoch - 1;
let migrations = crate::state_migration::get_migrations(&chain)
.into_iter()
.filter(|(h, _)| {
let nv: NetworkVersion = (*h).into();
network_version == nv
})
.collect_vec();
anyhow::ensure!(
!migrations.is_empty(),
"No migration found for network version {network_version} on {chain}"
);
for (_, migrate) in migrations {
println!("Migrating... state_root: {state_root}, epoch: {epoch}");
let start = Instant::now();
let new_state = migrate(&chain_config, &db, &state_root, epoch)?;
println!(
"Done. old_state: {state_root}, new_state: {new_state}, took: {}",
humantime::format_duration(start.elapsed())
);
state_root = new_state;
}
Ok(())
}
}

pub(super) fn load_db(db_root: &Path) -> anyhow::Result<Arc<ManyCar<ParityDb>>> {
let db_writer = open_db(db_root.into(), Default::default())?;
let db = ManyCar::new(db_writer);
let forest_car_db_dir = db_root.join(CAR_DB_DIR_NAME);
load_all_forest_cars(&db, &forest_car_db_dir)?;
Ok(Arc::new(db))
}
Loading