diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 1a51985a4b20..7acd4dc489f3 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -39,6 +39,7 @@ env: FOREST_F3_SIDECAR_FFI_BUILD_OPT_OUT: 1 FIL_PROOFS_PARAMETER_CACHE: /var/tmp/filecoin-proof-parameters RUST_LOG: error + FOREST_ACTOR_BUNDLE_PATH: /var/tmp/forest_actor_bundle.car.zst jobs: codecov: @@ -53,7 +54,7 @@ jobs: - uses: taiki-e/install-action@nextest - name: Fetch proof params and RPC test snapshots run: | - cargo run --bin forest-dev --no-default-features --profile quick -- fetch-rpc-tests + cargo run --bin forest-dev --no-default-features --profile quick -- fetch-test-snapshots --actor-bundle $FOREST_ACTOR_BUNDLE_PATH ls -ahl $FIL_PROOFS_PARAMETER_CACHE - name: Generate code coverage run: mise codecov diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index fa3bf5d3fdc7..596c1ae68592 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -77,7 +77,7 @@ jobs: uses: taiki-e/install-action@nextest - name: Fetch proof params and RPC test snapshots run: | - cargo run --bin forest-dev --no-default-features --profile quick -- fetch-rpc-tests + cargo run --bin forest-dev --no-default-features --profile quick -- fetch-test-snapshots ls -ahl $FIL_PROOFS_PARAMETER_CACHE - uses: jdx/mise-action@v3 - run: | diff --git a/docs/docs/users/reference/cli.sh b/docs/docs/users/reference/cli.sh index 74d9d2f08683..e791c35b804d 100755 --- a/docs/docs/users/reference/cli.sh +++ b/docs/docs/users/reference/cli.sh @@ -157,7 +157,7 @@ generate_markdown_section "forest-tool" "index backfill" generate_markdown_section "forest-dev" "" -generate_markdown_section "forest-dev" "fetch-rpc-tests" +generate_markdown_section "forest-dev" "fetch-test-snapshots" generate_markdown_section "forest-dev" "state" generate_markdown_section "forest-dev" "state compute" diff --git a/src/benchmark_private/tipset_validation.rs b/src/benchmark_private/tipset_validation.rs index 9a1fba9d5c3a..0eeede4507dd 100644 --- a/src/benchmark_private/tipset_validation.rs +++ b/src/benchmark_private/tipset_validation.rs @@ -19,29 +19,39 @@ pub fn bench_tipset_validation(c: &mut Criterion) { let mut group = c.benchmark_group("tipset_validation"); group - .bench_function("calibnet@3111900", |b| { + .bench_function("calibnet@3408952", |b| { let chain = NetworkChain::Calibnet; - let epoch = 3111900; - let (state_manager, ts) = rt + let epoch = 3408952; + let (state_manager, ts, ts_next) = rt .block_on(async { let snapshot = get_state_compute_snapshot(&chain, epoch).await?; - prepare_state_compute(&chain, &snapshot, true).await + prepare_state_compute(&chain, &snapshot).await }) .unwrap(); - b.to_async(&rt) - .iter(|| state_compute(black_box(state_manager.clone()), black_box(ts.clone()))) + b.to_async(&rt).iter(|| { + state_compute( + black_box(&state_manager), + black_box(ts.clone()), + black_box(&ts_next), + ) + }) }) - .bench_function("mainnet@5427431", |b| { + .bench_function("mainnet@5709604", |b| { let chain = NetworkChain::Mainnet; - let epoch = 5427431; - let (state_manager, ts) = rt + let epoch = 5709604; + let (state_manager, ts, ts_next) = rt .block_on(async { let snapshot = get_state_compute_snapshot(&chain, epoch).await?; - prepare_state_compute(&chain, &snapshot, true).await + prepare_state_compute(&chain, &snapshot).await }) .unwrap(); - b.to_async(&rt) - .iter(|| state_compute(black_box(state_manager.clone()), black_box(ts.clone()))) + b.to_async(&rt).iter(|| { + state_compute( + black_box(&state_manager), + black_box(ts.clone()), + black_box(&ts_next), + ) + }) }); group.finish(); diff --git a/src/blocks/tipset.rs b/src/blocks/tipset.rs index 169351bc26c8..7c87a753422b 100644 --- a/src/blocks/tipset.rs +++ b/src/blocks/tipset.rs @@ -317,6 +317,10 @@ impl Tipset { pub fn parent_state(&self) -> &Cid { &self.min_ticket_block().state_root } + /// Returns the message receipt root for the tipset parent. + pub fn parent_message_receipts(&self) -> &Cid { + &self.min_ticket_block().message_receipts + } /// Returns the tipset's calculated weight pub fn weight(&self) -> &BigInt { &self.min_ticket_block().weight diff --git a/src/chain_sync/tipset_syncer.rs b/src/chain_sync/tipset_syncer.rs index 76a2d7160506..1a1d67ffd9cd 100644 --- a/src/chain_sync/tipset_syncer.rs +++ b/src/chain_sync/tipset_syncer.rs @@ -11,6 +11,7 @@ use crate::shim::{ address::Address, crypto::verify_bls_aggregate, econ::BLOCK_GAS_LIMIT, gas::price_list_by_network_version, message::Message, state_tree::StateTree, }; +use crate::state_manager::StateLookupPolicy; use crate::state_manager::{Error as StateManagerError, StateManager, utils::is_valid_for_sending}; use crate::{ blocks::{Block, CachingBlockHeader, Error as ForestBlockError, FullTipset, Tipset}, @@ -278,7 +279,7 @@ async fn validate_block( async move { let header = block.header(); let (state_root, receipt_root) = state_manager - .tipset_state(&base_tipset) + .tipset_state(&base_tipset, StateLookupPolicy::Disabled) .await .map_err(|e| { TipsetSyncerError::Calculation(format!("Failed to calculate state: {e}")) @@ -439,7 +440,7 @@ async fn check_block_messages( let mut account_sequences: HashMap = HashMap::default(); let (state_root, _) = state_manager - .tipset_state(&base_tipset) + .tipset_state(&base_tipset, StateLookupPolicy::Disabled) .await .map_err(|e| TipsetSyncerError::Calculation(format!("Could not update state: {e}")))?; let tree = diff --git a/src/dev/subcommands/mod.rs b/src/dev/subcommands/mod.rs index bb9dfa34854f..2d9719872e75 100644 --- a/src/dev/subcommands/mod.rs +++ b/src/dev/subcommands/mod.rs @@ -4,6 +4,7 @@ mod state_cmd; use crate::cli_shared::cli::HELP_MESSAGE; +use crate::networks::generate_actor_bundle; use crate::rpc::Client; use crate::utils::net::{DownloadFileOption, download_file_with_cache}; use crate::utils::proofs_api::ensure_proof_params_downloaded; @@ -30,8 +31,12 @@ pub struct Cli { /// forest-dev sub-commands #[derive(clap::Subcommand)] pub enum Subcommand { - /// Fetch RPC test snapshots to the local cache - FetchRpcTests, + /// Fetch test snapshots to the local cache + FetchTestSnapshots { + // Save actor bundle to + #[arg(long)] + actor_bundle: Option, + }, #[command(subcommand)] State(state_cmd::StateCommand), } @@ -39,17 +44,30 @@ pub enum Subcommand { impl Subcommand { pub async fn run(self, _client: Client) -> anyhow::Result<()> { match self { - Self::FetchRpcTests => fetch_rpc_tests().await, + Self::FetchTestSnapshots { actor_bundle } => fetch_test_snapshots(actor_bundle).await, Self::State(cmd) => cmd.run().await, } } } -async fn fetch_rpc_tests() -> anyhow::Result<()> { +async fn fetch_test_snapshots(actor_bundle: Option) -> anyhow::Result<()> { + // Prepare proof parameter files crate::utils::proofs_api::maybe_set_proofs_parameter_cache_dir_env( &crate::Config::default().client.data_dir, ); ensure_proof_params_downloaded().await?; + + // Prepare actor bundles + if let Some(actor_bundle) = actor_bundle { + generate_actor_bundle(&actor_bundle).await?; + println!("Wrote the actors bundle to {}", actor_bundle.display()); + } + + // Prepare RPC test snapshots + fetch_rpc_tests().await +} + +async fn fetch_rpc_tests() -> anyhow::Result<()> { let tests = include_str!("../../tool/subcommands/api_cmd/test_snapshots.txt") .lines() .map(|i| { diff --git a/src/dev/subcommands/state_cmd.rs b/src/dev/subcommands/state_cmd.rs index f68c375a9c35..344c68a9202a 100644 --- a/src/dev/subcommands/state_cmd.rs +++ b/src/dev/subcommands/state_cmd.rs @@ -6,11 +6,7 @@ use crate::{ chain::{ChainStore, index::ResolveNullTipset}, chain_sync::{load_full_tipset, tipset_syncer::validate_tipset}, cli_shared::{chain_path, read_config}, - db::{ - MemoryDB, SettingsStoreExt, - car::{AnyCar, ManyCar}, - db_engine::db_root, - }, + db::{SettingsStoreExt, db_engine::db_root}, genesis::read_genesis_header, interpreter::VMTrace, networks::{ChainConfig, NetworkChain}, @@ -73,7 +69,7 @@ impl ComputeCommand { let (_, config) = read_config(None, Some(chain.clone()))?; db_root(&chain_path(&config))? }; - let db = generate_test_snapshot::load_db(&db_root_path)?; + let db = generate_test_snapshot::load_db(&db_root_path, Some(&chain)).await?; let chain_config = Arc::new(ChainConfig::from_chain(&chain)); let genesis_header = read_genesis_header(None, chain_config.genesis_bytes(&db).await?.as_deref(), &db) @@ -85,7 +81,7 @@ impl ComputeCommand { chain_config, genesis_header, )?); - let ts = { + let (ts, ts_next) = { // We don't want to track all entries that are visited by `tipset_by_height` db.pause_tracking(); let ts = chain_store.chain_index().tipset_by_height( @@ -93,10 +89,22 @@ impl ComputeCommand { chain_store.heaviest_tipset(), ResolveNullTipset::TakeOlder, )?; + let ts_next = chain_store.chain_index().tipset_by_height( + epoch + 1, + chain_store.heaviest_tipset(), + ResolveNullTipset::TakeNewer, + )?; db.resume_tracking(); - SettingsStoreExt::write_obj(&db.tracker, crate::db::setting_keys::HEAD_KEY, ts.key())?; - // Only track the desired tipset - Tipset::load_required(&db, ts.key())? + SettingsStoreExt::write_obj( + &db.tracker, + crate::db::setting_keys::HEAD_KEY, + ts_next.key(), + )?; + // Only track the desired tipsets + ( + Tipset::load_required(&db, ts.key())?, + Tipset::load_required(&db, ts_next.key())?, + ) }; let epoch = ts.epoch(); let state_manager = Arc::new(StateManager::new(chain_store)?); @@ -114,6 +122,16 @@ impl ComputeCommand { "epoch: {epoch}, state_root: {state_root}, receipt_root: {receipt_root}, db_snapshot_size: {}", human_bytes::human_bytes(db_snapshot.len() as f64) ); + let expected_state_root = *ts_next.parent_state(); + let expected_receipt_root = *ts_next.parent_message_receipts(); + anyhow::ensure!( + state_root == expected_state_root, + "state root mismatch, state_root: {state_root}, expected_state_root: {expected_state_root}" + ); + anyhow::ensure!( + receipt_root == expected_receipt_root, + "receipt root mismatch, receipt_root: {receipt_root}, expected_receipt_root: {expected_receipt_root}" + ); if let Some(export_db_to) = export_db_to { std::fs::write(export_db_to, db_snapshot)?; } @@ -138,39 +156,12 @@ pub struct ReplayComputeCommand { impl ReplayComputeCommand { pub async fn run(self) -> anyhow::Result<()> { let Self { snapshot, chain, n } = self; - let snap_car = AnyCar::try_from(&snapshot)?; - let ts = snap_car.heaviest_tipset()?; - let epoch = ts.epoch(); - let db = Arc::new(ManyCar::new(MemoryDB::default()).with_read_only(snap_car)?); - let chain_config = Arc::new(ChainConfig::from_chain(&chain)); - let genesis_header = - read_genesis_header(None, chain_config.genesis_bytes(&db).await?.as_deref(), &db) + let (sm, ts, ts_next) = + crate::state_manager::utils::state_compute::prepare_state_compute(&chain, &snapshot) .await?; - let chain_store = Arc::new(ChainStore::new( - db.clone(), - db.clone(), - db.clone(), - chain_config, - genesis_header, - )?); - let state_manager = Arc::new(StateManager::new(chain_store)?); for _ in 0..n.get() { - let start = Instant::now(); - let StateOutput { - state_root, - receipt_root, - .. - } = state_manager - .compute_tipset_state( - ts.clone(), - crate::state_manager::NO_CALLBACK, - VMTrace::NotTraced, - ) + crate::state_manager::utils::state_compute::state_compute(&sm, ts.clone(), &ts_next) .await?; - println!( - "epoch: {epoch}, state_root: {state_root}, receipt_root: {receipt_root}, took {}.", - humantime::format_duration(start.elapsed()) - ); } Ok(()) } @@ -208,7 +199,7 @@ impl ValidateCommand { let (_, config) = read_config(None, Some(chain.clone()))?; db_root(&chain_path(&config))? }; - let db = generate_test_snapshot::load_db(&db_root_path)?; + let db = generate_test_snapshot::load_db(&db_root_path, Some(&chain)).await?; let chain_config = Arc::new(ChainConfig::from_chain(&chain)); let genesis_header = read_genesis_header(None, chain_config.genesis_bytes(&db).await?.as_deref(), &db) @@ -267,27 +258,14 @@ pub struct ReplayValidateCommand { impl ReplayValidateCommand { pub async fn run(self) -> anyhow::Result<()> { let Self { snapshot, chain, n } = self; - let snap_car = AnyCar::try_from(&snapshot)?; - let ts = snap_car.heaviest_tipset()?; - let epoch = ts.epoch(); - let db = Arc::new(ManyCar::new(MemoryDB::default()).with_read_only(snap_car)?); - let chain_config = Arc::new(ChainConfig::from_chain(&chain)); - let genesis_header = - read_genesis_header(None, chain_config.genesis_bytes(&db).await?.as_deref(), &db) + let (sm, fts) = + crate::state_manager::utils::state_compute::prepare_state_validate(&chain, &snapshot) .await?; - let chain_store = Arc::new(ChainStore::new( - db.clone(), - db.clone(), - db.clone(), - chain_config, - genesis_header, - )?); - let state_manager = Arc::new(StateManager::new(chain_store)?); - let fts = load_full_tipset(state_manager.chain_store(), ts.key())?; + let epoch = fts.epoch(); for _ in 0..n.get() { let fts = fts.clone(); let start = Instant::now(); - validate_tipset(&state_manager, fts, None).await?; + validate_tipset(&sm, fts, None).await?; println!( "epoch: {epoch}, took {}.", humantime::format_duration(start.elapsed()) @@ -298,5 +276,7 @@ impl ReplayValidateCommand { } fn disable_tipset_cache() { - unsafe { std::env::set_var("FOREST_TIPSET_CACHE_DISABLED", "1") }; + unsafe { + std::env::set_var("FOREST_TIPSET_CACHE_DISABLED", "1"); + } } diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index 01ab8ba1909b..e2b73c490851 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -54,6 +54,7 @@ use crate::shim::gas::GasOutputs; use crate::shim::message::Message; use crate::shim::trace::{CallReturn, ExecutionEvent}; use crate::shim::{clock::ChainEpoch, state_tree::StateTree}; +use crate::state_manager::StateLookupPolicy; use crate::utils::cache::SizeTrackingLruCache; use crate::utils::db::BlockstoreExt as _; use crate::utils::encoding::from_slice_with_fallback; @@ -1200,7 +1201,10 @@ async fn execute_tipset( ) -> Result<(Cid, Vec<(ChainMessage, Receipt)>)> { let msgs = data.chain_store().messages_for_tipset(tipset)?; - let (state_root, _) = data.state_manager.tipset_state(tipset).await?; + let (state_root, _) = data + .state_manager + .tipset_state(tipset, StateLookupPolicy::Enabled) + .await?; let receipts = data.state_manager.tipset_message_receipts(tipset).await?; if msgs.len() != receipts.len() { @@ -2054,7 +2058,7 @@ where { let invoc_res = ctx .state_manager - .apply_on_state_with_gas(tipset, msg) + .apply_on_state_with_gas(tipset, msg, StateLookupPolicy::Enabled) .await .map_err(|e| anyhow::anyhow!("failed to apply on state with gas: {e}"))?; @@ -2150,6 +2154,7 @@ where prior_messages, Some(ts), VMTrace::NotTraced, + StateLookupPolicy::Enabled, ) .await?; Ok(apply_ret.msg_receipt().exit_code().is_success()) @@ -2387,7 +2392,10 @@ impl RpcMethod<2> for EthGetCode { ..Default::default() }; - let (state, _) = ctx.state_manager.tipset_state(&ts).await?; + let (state, _) = ctx + .state_manager + .tipset_state(&ts, StateLookupPolicy::Enabled) + .await?; let api_invoc_result = 'invoc: { for ts in ts.chain(ctx.store()) { match ctx.state_manager.call_on_state(state, &message, Some(ts)) { @@ -2439,7 +2447,10 @@ impl RpcMethod<3> for EthGetStorageAt { ResolveNullTipset::TakeOlder, )?; let to_address = FilecoinAddress::try_from(ð_address)?; - let (state, _) = ctx.state_manager.tipset_state(&ts).await?; + let (state, _) = ctx + .state_manager + .tipset_state(&ts, StateLookupPolicy::Enabled) + .await?; let Some(actor) = ctx.state_manager.get_actor(&to_address, state)? else { return Ok(make_empty_result()); }; @@ -2565,7 +2576,10 @@ async fn eth_get_transaction_count( where B: Blockstore + Send + Sync + 'static, { - let (state_cid, _) = ctx.state_manager.tipset_state(ts).await?; + let (state_cid, _) = ctx + .state_manager + .tipset_state(ts, StateLookupPolicy::Enabled) + .await?; let state = StateTree::new_from_root(ctx.store_owned(), &state_cid)?; let actor = state.get_required_actor(&addr)?; diff --git a/src/rpc/methods/gas.rs b/src/rpc/methods/gas.rs index 6df3ef342c05..f0a06fd40a93 100644 --- a/src/rpc/methods/gas.rs +++ b/src/rpc/methods/gas.rs @@ -15,6 +15,7 @@ use crate::shim::{ econ::{BLOCK_GAS_LIMIT, TokenAmount}, message::Message, }; +use crate::state_manager::StateLookupPolicy; use anyhow::{Context, Result}; use enumflags2::BitFlags; use fvm_ipld_blockstore::Blockstore; @@ -260,6 +261,7 @@ impl GasEstimateGasLimit { &prior_messages, Some(ts.clone()), trace_config, + StateLookupPolicy::Enabled, ) .await?; Ok((invoc_res, apply_ret, prior_messages, ts)) diff --git a/src/rpc/methods/miner.rs b/src/rpc/methods/miner.rs index aff26e15ad4f..f1a9b91cc7da 100644 --- a/src/rpc/methods/miner.rs +++ b/src/rpc/methods/miner.rs @@ -21,6 +21,7 @@ use crate::rpc::{ApiPaths, Ctx, RpcMethod, ServerError}; use crate::shim::address::Address; use crate::shim::clock::ChainEpoch; use crate::shim::crypto::{Signature, SignatureType}; +use crate::state_manager::StateLookupPolicy; use enumflags2::BitFlags; use crate::shim::sector::PoStProof; @@ -140,7 +141,10 @@ impl RpcMethod<1> for MinerCreateBlock { .context("Missing Smoke height")? .epoch, )?; - let (state, receipts) = ctx.state_manager.tipset_state(&parent_tipset).await?; + let (state, receipts) = ctx + .state_manager + .tipset_state(&parent_tipset, StateLookupPolicy::Disabled) + .await?; let network_version = ctx.state_manager.get_network_version(block_template.epoch); diff --git a/src/state_manager/mod.rs b/src/state_manager/mod.rs index e2463c688235..7256d5b04de4 100644 --- a/src/state_manager/mod.rs +++ b/src/state_manager/mod.rs @@ -458,18 +458,23 @@ where /// Returns the pair of (state root, message receipt root). This will /// either be cached or will be calculated and fill the cache. Tipset /// state for a given tipset is guaranteed not to be computed twice. - pub async fn tipset_state(self: &Arc, tipset: &Tipset) -> anyhow::Result { + pub async fn tipset_state( + self: &Arc, + tipset: &Tipset, + state_lookup: StateLookupPolicy, + ) -> anyhow::Result { let StateOutput { state_root, receipt_root, .. - } = self.tipset_state_output(tipset).await?; + } = self.tipset_state_output(tipset, state_lookup).await?; Ok((state_root, receipt_root)) } pub async fn tipset_state_output( self: &Arc, tipset: &Tipset, + state_lookup: StateLookupPolicy, ) -> anyhow::Result { let key = tipset.key(); self.cache @@ -483,7 +488,9 @@ where // First, try to look up the state and receipt if not found in the blockstore // compute it - if let Some(state_from_child) = self.try_lookup_state_from_next_tipset(tipset) { + if matches!(state_lookup, StateLookupPolicy::Enabled) + && let Some(state_from_child) = self.try_lookup_state_from_next_tipset(tipset) + { return Ok(state_from_child); } @@ -676,6 +683,7 @@ where self: &Arc, tipset: Option, msg: Message, + state_lookup: StateLookupPolicy, ) -> anyhow::Result { let ts = tipset.unwrap_or_else(|| self.heaviest_tipset()); @@ -699,7 +707,7 @@ where }; let (_invoc_res, apply_ret, duration) = self - .call_with_gas(&mut chain_msg, &[], Some(ts), VMTrace::Traced) + .call_with_gas(&mut chain_msg, &[], Some(ts), VMTrace::Traced, state_lookup) .await?; Ok(ApiInvocResult { msg_cid: msg.cid(), @@ -720,10 +728,11 @@ where prior_messages: &[ChainMessage], tipset: Option, trace_config: VMTrace, + state_lookup: StateLookupPolicy, ) -> Result<(InvocResult, ApplyRet, Duration), Error> { let ts = tipset.unwrap_or_else(|| self.heaviest_tipset()); let (st, _) = self - .tipset_state(&ts) + .tipset_state(&ts, state_lookup) .await .map_err(|e| Error::Other(format!("Could not load tipset state: {e}")))?; let chain_rand = self.chain_rand(ts.clone()); @@ -1432,7 +1441,7 @@ where } // If that fails, compute the tip-set and try again. - let (st, _) = self.tipset_state(ts).await?; + let (st, _) = self.tipset_state(ts, StateLookupPolicy::Enabled).await?; let state = StateTree::new_from_root(self.blockstore_owned(), &st)?; resolve_to_key_addr(&state, self.blockstore(), addr) @@ -1690,7 +1699,7 @@ where } // If that fails, compute the tip-set and try again. - let (state_root, _) = self.tipset_state(ts).await?; + let (state_root, _) = self.tipset_state(ts, StateLookupPolicy::Enabled).await?; let state = StateTree::new_from_root(self.blockstore_owned(), &state_root)?; state.resolve_to_deterministic_addr(self.chain_store().blockstore(), address) } @@ -2075,3 +2084,11 @@ where Ok(output) } + +/// Whether or not to lookup the state output from the next tipset before computing a state +#[derive(Debug, Copy, Clone, Default)] +pub enum StateLookupPolicy { + #[default] + Enabled, + Disabled, +} diff --git a/src/state_manager/utils.rs b/src/state_manager/utils.rs index 0e5f83d38c66..a4b2d59c8338 100644 --- a/src/state_manager/utils.rs +++ b/src/state_manager/utils.rs @@ -178,11 +178,11 @@ fn generate_winning_post_sector_challenge( ) } -#[cfg(any(test, feature = "benchmark-private"))] pub mod state_compute { use crate::{ - blocks::Tipset, + blocks::{FullTipset, Tipset}, chain::store::ChainStore, + chain_sync::load_full_tipset, db::{ MemoryDB, car::{AnyCar, ManyCar}, @@ -190,25 +190,18 @@ pub mod state_compute { genesis::read_genesis_header, interpreter::VMTrace, networks::{ChainConfig, NetworkChain}, - state_manager::StateManager, + state_manager::{StateManager, StateOutput}, utils::net::{DownloadFileOption, download_file_with_cache}, }; use directories::ProjectDirs; use std::{ path::{Path, PathBuf}, sync::{Arc, LazyLock}, - time::Duration, + time::{Duration, Instant}, }; use url::Url; - static SNAPSHOT_CACHE_DIR: LazyLock = LazyLock::new(|| { - let project_dir = ProjectDirs::from("com", "ChainSafe", "Forest"); - project_dir - .map(|d| d.cache_dir().to_path_buf()) - .unwrap_or_else(std::env::temp_dir) - .join("state_compute_snapshots") - }); - + #[allow(dead_code)] pub async fn get_state_compute_snapshot( chain: &NetworkChain, epoch: i64, @@ -216,11 +209,20 @@ pub mod state_compute { get_state_snapshot(chain, "state_compute", epoch).await } + #[allow(dead_code)] pub async fn get_state_snapshot( chain: &NetworkChain, bucket: &str, epoch: i64, ) -> anyhow::Result { + static SNAPSHOT_CACHE_DIR: LazyLock = LazyLock::new(|| { + let project_dir = ProjectDirs::from("com", "ChainSafe", "Forest"); + project_dir + .map(|d| d.cache_dir().to_path_buf()) + .unwrap_or_else(std::env::temp_dir) + .join("state_compute_snapshots") + }); + let url = Url::parse(&format!( "https://forest-snapshots.fra1.cdn.digitaloceanspaces.com/{bucket}/{chain}_{epoch}.forest.car.zst" ))?; @@ -245,11 +247,11 @@ pub mod state_compute { pub async fn prepare_state_compute( chain: &NetworkChain, snapshot: &Path, - warmup: bool, - ) -> anyhow::Result<(Arc>, Tipset)> { + ) -> anyhow::Result<(Arc>, Tipset, Tipset)> { let snap_car = AnyCar::try_from(snapshot)?; - let ts = snap_car.heaviest_tipset()?; + let ts_next = snap_car.heaviest_tipset()?; let db = Arc::new(ManyCar::new(MemoryDB::default()).with_read_only(snap_car)?); + let ts = Tipset::load_required(&db, ts_next.parents())?; let chain_config = Arc::new(ChainConfig::from_chain(chain)); let genesis_header = read_genesis_header(None, chain_config.genesis_bytes(&db).await?.as_deref(), &db) @@ -262,84 +264,162 @@ pub mod state_compute { genesis_header, )?); let state_manager = Arc::new(StateManager::new(chain_store.clone())?); - if warmup { - state_compute(state_manager.clone(), ts.clone()).await; - } - Ok((state_manager, ts)) + Ok((state_manager, ts, ts_next)) + } + + pub async fn prepare_state_validate( + chain: &NetworkChain, + snapshot: &Path, + ) -> anyhow::Result<(Arc>, FullTipset)> { + let (sm, _, ts) = prepare_state_compute(chain, snapshot).await?; + let fts = load_full_tipset(sm.chain_store(), ts.key())?; + Ok((sm, fts)) } - pub async fn state_compute(state_manager: Arc>, ts: Tipset) { - state_manager + pub async fn state_compute( + state_manager: &Arc>, + ts: Tipset, + ts_next: &Tipset, + ) -> anyhow::Result<()> { + let epoch = ts.epoch(); + let expected_state_root = *ts_next.parent_state(); + let expected_receipt_root = *ts_next.parent_message_receipts(); + let start = Instant::now(); + let StateOutput { + state_root, + receipt_root, + .. + } = state_manager .compute_tipset_state(ts, crate::state_manager::NO_CALLBACK, VMTrace::NotTraced) - .await - .unwrap(); + .await?; + println!( + "epoch: {epoch}, state_root: {state_root}, receipt_root: {receipt_root}, took {}.", + humantime::format_duration(start.elapsed()) + ); + anyhow::ensure!( + state_root == expected_state_root, + "state root mismatch, state_root: {state_root}, expected_state_root: {expected_state_root}" + ); + anyhow::ensure!( + receipt_root == expected_receipt_root, + "receipt root mismatch, receipt_root: {receipt_root}, expected_receipt_root: {expected_receipt_root}" + ); + Ok(()) } #[cfg(test)] mod tests { + //! + //! Test snapshots are generate by `forest-dev state` tool + //! + use super::*; - use crate::{ - blocks::FullTipset, - chain_sync::{load_full_tipset, tipset_syncer::validate_tipset}, - }; + use crate::chain_sync::tipset_syncer::validate_tipset; - pub async fn get_state_validate_snapshot( + async fn get_state_validate_snapshot( chain: &NetworkChain, epoch: i64, ) -> anyhow::Result { get_state_snapshot(chain, "state_validate", epoch).await } - pub async fn prepare_state_validate( - chain: &NetworkChain, - snapshot: &Path, - ) -> anyhow::Result<(Arc>, FullTipset)> { - let (sm, ts) = prepare_state_compute(chain, snapshot, false).await?; - let fts = load_full_tipset(sm.chain_store(), ts.key())?; - Ok((sm, fts)) + // FVM@4 + #[tokio::test(flavor = "multi_thread")] + async fn state_compute_mainnet_5709604() { + let chain = NetworkChain::Mainnet; + let snapshot = get_state_compute_snapshot(&chain, 5709604).await.unwrap(); + let (sm, ts, ts_next) = prepare_state_compute(&chain, &snapshot).await.unwrap(); + state_compute(&sm, ts, &ts_next).await.unwrap(); } + // FVM@4 #[tokio::test(flavor = "multi_thread")] - async fn state_compute_calibnet_3111900() { + async fn state_compute_calibnet_3408952() { let chain = NetworkChain::Calibnet; - // FVM@4 - let snapshot = get_state_compute_snapshot(&chain, 3111900).await.unwrap(); - let (sm, ts) = prepare_state_compute(&chain, &snapshot, false) - .await - .unwrap(); - state_compute(sm, ts).await; + let snapshot = get_state_compute_snapshot(&chain, 3408952).await.unwrap(); + let (sm, ts, ts_next) = prepare_state_compute(&chain, &snapshot).await.unwrap(); + state_compute(&sm, ts, &ts_next).await.unwrap(); } + // Shark state migration with FVM@2 #[tokio::test(flavor = "multi_thread")] - async fn state_compute_calibnet_480000() { + async fn state_compute_calibnet_16801() { let chain = NetworkChain::Calibnet; - // FVM@3 - let snapshot = get_state_compute_snapshot(&chain, 480000).await.unwrap(); - let (sm, ts) = prepare_state_compute(&chain, &snapshot, false) - .await - .unwrap(); - state_compute(sm, ts).await; + let snapshot = get_state_compute_snapshot(&chain, 16801).await.unwrap(); + let (sm, ts, ts_next) = prepare_state_compute(&chain, &snapshot).await.unwrap(); + state_compute(&sm, ts, &ts_next).await.unwrap(); } + // Hygge state migration with FVM@2 #[tokio::test(flavor = "multi_thread")] - async fn state_compute_calibnet_300000() { + async fn state_compute_calibnet_322355() { let chain = NetworkChain::Calibnet; - // FVM@2 - let snapshot = get_state_compute_snapshot(&chain, 300000).await.unwrap(); - let (sm, ts) = prepare_state_compute(&chain, &snapshot, false) - .await - .unwrap(); - state_compute(sm, ts).await; + let snapshot = get_state_compute_snapshot(&chain, 322355).await.unwrap(); + let (sm, ts, ts_next) = prepare_state_compute(&chain, &snapshot).await.unwrap(); + state_compute(&sm, ts, &ts_next).await.unwrap(); } + // Lightning state migration with FVM@3 #[tokio::test(flavor = "multi_thread")] - async fn state_compute_mainnet_5688000() { - let chain = NetworkChain::Mainnet; - let snapshot = get_state_compute_snapshot(&chain, 5688000).await.unwrap(); - let (sm, ts) = prepare_state_compute(&chain, &snapshot, false) - .await - .unwrap(); - state_compute(sm, ts).await; + async fn state_compute_calibnet_489095() { + let chain = NetworkChain::Calibnet; + let snapshot = get_state_compute_snapshot(&chain, 489095).await.unwrap(); + let (sm, ts, ts_next) = prepare_state_compute(&chain, &snapshot).await.unwrap(); + state_compute(&sm, ts, &ts_next).await.unwrap(); + } + + // Watermelon state migration with FVM@3 + #[tokio::test(flavor = "multi_thread")] + async fn state_compute_calibnet_1013135() { + let chain = NetworkChain::Calibnet; + let snapshot = get_state_compute_snapshot(&chain, 1013135).await.unwrap(); + let (sm, ts, ts_next) = prepare_state_compute(&chain, &snapshot).await.unwrap(); + state_compute(&sm, ts, &ts_next).await.unwrap(); + } + + // Dragon state migration with FVM@4 + #[tokio::test(flavor = "multi_thread")] + async fn state_compute_calibnet_1427975() { + let chain = NetworkChain::Calibnet; + let snapshot = get_state_compute_snapshot(&chain, 1427975).await.unwrap(); + let (sm, ts, ts_next) = prepare_state_compute(&chain, &snapshot).await.unwrap(); + state_compute(&sm, ts, &ts_next).await.unwrap(); + } + + // Waffle state migration with FVM@4 + #[tokio::test(flavor = "multi_thread")] + async fn state_compute_calibnet_1779095() { + let chain = NetworkChain::Calibnet; + let snapshot = get_state_compute_snapshot(&chain, 1779095).await.unwrap(); + let (sm, ts, ts_next) = prepare_state_compute(&chain, &snapshot).await.unwrap(); + state_compute(&sm, ts, &ts_next).await.unwrap(); + } + + // TukTuk state migration with FVM@4 + #[tokio::test(flavor = "multi_thread")] + async fn state_compute_calibnet_2078795() { + let chain = NetworkChain::Calibnet; + let snapshot = get_state_compute_snapshot(&chain, 2078795).await.unwrap(); + let (sm, ts, ts_next) = prepare_state_compute(&chain, &snapshot).await.unwrap(); + state_compute(&sm, ts, &ts_next).await.unwrap(); + } + + // Teep state migration with FVM@4 + #[tokio::test(flavor = "multi_thread")] + async fn state_compute_calibnet_2523455() { + let chain = NetworkChain::Calibnet; + let snapshot = get_state_compute_snapshot(&chain, 2523455).await.unwrap(); + let (sm, ts, ts_next) = prepare_state_compute(&chain, &snapshot).await.unwrap(); + state_compute(&sm, ts, &ts_next).await.unwrap(); + } + + // GoldenWeek state migration with FVM@4 + #[tokio::test(flavor = "multi_thread")] + async fn state_compute_calibnet_3007295() { + let chain = NetworkChain::Calibnet; + let snapshot = get_state_compute_snapshot(&chain, 3007295).await.unwrap(); + let (sm, ts, ts_next) = prepare_state_compute(&chain, &snapshot).await.unwrap(); + state_compute(&sm, ts, &ts_next).await.unwrap(); } #[tokio::test(flavor = "multi_thread")] @@ -349,6 +429,24 @@ pub mod state_compute { let (sm, fts) = prepare_state_validate(&chain, &snapshot).await.unwrap(); validate_tipset(&sm, fts, None).await.unwrap(); } + + // Shark state migration + #[tokio::test(flavor = "multi_thread")] + async fn state_validate_calibnet_16802() { + let chain = NetworkChain::Calibnet; + let snapshot = get_state_validate_snapshot(&chain, 16802).await.unwrap(); + let (sm, fts) = prepare_state_validate(&chain, &snapshot).await.unwrap(); + validate_tipset(&sm, fts, None).await.unwrap(); + } + + // Hygge state migration + #[tokio::test(flavor = "multi_thread")] + async fn state_validate_calibnet_322356() { + let chain = NetworkChain::Calibnet; + let snapshot = get_state_validate_snapshot(&chain, 322356).await.unwrap(); + let (sm, fts) = prepare_state_validate(&chain, &snapshot).await.unwrap(); + validate_tipset(&sm, fts, None).await.unwrap(); + } } } diff --git a/src/tool/subcommands/api_cmd.rs b/src/tool/subcommands/api_cmd.rs index 065d2ebc0273..c837355e91d6 100644 --- a/src/tool/subcommands/api_cmd.rs +++ b/src/tool/subcommands/api_cmd.rs @@ -335,7 +335,7 @@ impl ApiCommands { let (_, config) = read_config(None, Some(chain.clone()))?; db_root(&chain_path(&config))? }; - let tracking_db = generate_test_snapshot::load_db(&db)?; + let tracking_db = generate_test_snapshot::load_db(&db, None).await?; for test_dump_file in test_dump_files { let out_path = out_dir .join(test_dump_file.file_name().context("Infallible")?) diff --git a/src/tool/subcommands/api_cmd/generate_test_snapshot.rs b/src/tool/subcommands/api_cmd/generate_test_snapshot.rs index 46eb1baa09af..020b1d00f7f6 100644 --- a/src/tool/subcommands/api_cmd/generate_test_snapshot.rs +++ b/src/tool/subcommands/api_cmd/generate_test_snapshot.rs @@ -3,6 +3,7 @@ use super::*; use crate::chain_sync::SyncStatusReport; +use crate::daemon::bundle::load_actor_bundles; use crate::{ KeyStore, KeyStoreConfig, blocks::TipsetKey, @@ -73,11 +74,17 @@ pub async fn run_test_with_dump( Ok(()) } -pub fn load_db(db_root: &Path) -> anyhow::Result>>> { +pub async fn load_db( + db_root: &Path, + chain: Option<&NetworkChain>, +) -> anyhow::Result>>> { 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)?; + if let Some(chain) = chain { + load_actor_bundles(&db, chain).await?; + } Ok(Arc::new(ReadOpsTrackingStore::new(db))) }