Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
94 changes: 94 additions & 0 deletions src/dev/subcommands/migrate_cmd.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Copyright 2019-2026 ChainSafe Systems
// SPDX-License-Identifier: Apache-2.0, MIT

use crate::daemon::bundle::load_actor_bundles;
use crate::db::{
car::ManyCar,
db_engine::{Db, DbConfig},
};
use crate::networks::{ChainConfig, Height, NetworkChain};
use crate::state_migration::run_state_migrations;
use anyhow::Context as _;
use clap::Args;
use std::{path::PathBuf, sync::Arc};

/// Runs a single state migration against the head of a snapshot, using a
/// throwaway on-disk ParityDb as the writable backing store so that timings
/// reflect the real production I/O path.
///
/// The snapshot CAR file is attached as a read-only layer; any state tree
/// blocks produced by the migration are written to the temporary ParityDb,
/// which is removed when the command exits.
#[derive(Debug, Args)]
pub struct MigrateCommand {
/// Path to the snapshot CAR file (plain `.car` or zstd-compressed `.car.zst`).
#[arg(long, required = true)]
snapshot: PathBuf,
/// Migration height to run (e.g. `GoldenWeek`, `Xxx`). The migration will
/// be invoked as if the chain had reached that height's configured epoch
/// for the network detected from the snapshot's genesis.
#[arg(long, required = true)]
height: Height,
}

impl MigrateCommand {
pub async fn run(self) -> anyhow::Result<()> {
let Self { snapshot, height } = self;

// On-disk ParityDb so the benchmark reflects production I/O rather than
// the in-memory fast path.
let temp_dir = tempfile::Builder::new()
.prefix("forest-migrate-")
.tempdir()?;
let paritydb_path = temp_dir.path().join("paritydb");
let paritydb = Db::open(&paritydb_path, &DbConfig::default())?;
tracing::info!("Using temporary ParityDb at {}", paritydb_path.display());

let store = Arc::new(ManyCar::new(paritydb));
store
.read_only_file(&snapshot)
.with_context(|| format!("failed to attach snapshot {}", snapshot.display()))?;

let head = store.heaviest_tipset()?;
let genesis = head.genesis(&store)?;
let network = NetworkChain::from_genesis(genesis.cid()).context(
"snapshot genesis does not match any known mainnet/calibnet/butterflynet genesis; custom devnets are not supported",
)?;
let chain_config = ChainConfig::from_chain(&network);

// The migration reads the target-height actor bundle from the
// blockstore; load it into the writable layer so it's visible through
// the ManyCar.
load_actor_bundles(store.writer(), &network).await?;

let epoch = chain_config.epoch(height);
anyhow::ensure!(
epoch > 0,
"no epoch configured for height {height} on {network}"
);

let parent_state = *head.parent_state();
tracing::info!(
"Running {height} migration on {network} (epoch {epoch}); head epoch {head_epoch}, parent state {parent_state}",
head_epoch = head.epoch(),
);

let start = std::time::Instant::now();
let new_state = run_state_migrations(epoch, &chain_config, &store, &parent_state)?;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate that the snapshot head matches the requested migration boundary.

run_state_migrations() uses the supplied parent_state as-is. This command always passes head.parent_state(), so if the snapshot head is later than the requested upgrade, the migration runs against the wrong state tree and the benchmark result is meaningless. Reject mismatched heads or resolve the tipset at the requested epoch before extracting parent_state.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/dev/subcommands/migrate_cmd.rs` around lines 52 - 77, Compare the
requested migration epoch/height to the snapshot head epoch returned by
store.heaviest_tipset() and reject or resolve appropriately before calling
run_state_migrations: if head.epoch() > epoch (i.e., the snapshot is later than
the requested migration), resolve the tipset corresponding to the requested
epoch and extract its parent state (instead of using head.parent_state()), or
return an error; if head.epoch() < epoch, return an error indicating the
snapshot is too old; finally pass that resolved parent_state into
run_state_migrations(epoch, &chain_config, &store, &parent_state).

let elapsed = start.elapsed();

match new_state {
Some(new_state) => {
tracing::info!(
"Migration completed: {parent_state} -> {new_state} in {elapsed}",
elapsed = humantime::format_duration(elapsed),
);
}
None => anyhow::bail!(
"No migration ran. Check that the mapping for height {height} is registered for {network} in `get_migrations` and that the snapshot's head is compatible."
),
}

Ok(())
}
}
5 changes: 5 additions & 0 deletions src/dev/subcommands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
mod archive_missing_cmd;
mod export_state_tree_cmd;
mod export_tipset_lookup_cmd;
mod migrate_cmd;
mod state_cmd;
mod update_checkpoints_cmd;

Expand Down Expand Up @@ -53,6 +54,9 @@ pub enum Subcommand {
ArchiveMissing(archive_missing_cmd::ArchiveMissingCommand),
ExportTipsetLookup(export_tipset_lookup_cmd::ExportTipsetLookupCommand),
ExportStateTree(export_state_tree_cmd::ExportStateTreeCommand),
/// Run a single state migration on the head of a snapshot, backed by a
/// throwaway on-disk ParityDb. Primarily intended for benchmarking.
Migrate(migrate_cmd::MigrateCommand),
}

impl Subcommand {
Expand All @@ -64,6 +68,7 @@ impl Subcommand {
Self::ArchiveMissing(cmd) => cmd.run().await,
Self::ExportTipsetLookup(cmd) => cmd.run().await,
Self::ExportStateTree(cmd) => cmd.run().await,
Self::Migrate(cmd) => cmd.run().await,
}
}
}
Expand Down
16 changes: 14 additions & 2 deletions src/networks/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use itertools::Itertools;
use libp2p::Multiaddr;
use num_traits::Zero;
use serde::{Deserialize, Serialize};
use strum::{Display, EnumIter, IntoEnumIterator};
use strum::{Display, EnumIter, EnumString, IntoEnumIterator};
use tracing::warn;

use crate::beacon::{BeaconPoint, BeaconSchedule, DrandBeacon, DrandConfig};
Expand Down Expand Up @@ -136,8 +136,20 @@ impl NetworkChain {

/// Defines the meaningful heights of the protocol.
#[derive(
Debug, Default, Display, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash, EnumIter,
Debug,
Default,
Display,
Clone,
Copy,
Serialize,
Deserialize,
PartialEq,
Eq,
Hash,
EnumIter,
EnumString,
)]
#[strum(ascii_case_insensitive)]
#[cfg_attr(test, derive(derive_quickcheck_arbitrary::Arbitrary))]
pub enum Height {
#[default]
Expand Down
Loading