diff --git a/crates/bin/pd/src/main.rs b/crates/bin/pd/src/main.rs index b4bd928966..e8dd635958 100644 --- a/crates/bin/pd/src/main.rs +++ b/crates/bin/pd/src/main.rs @@ -13,7 +13,7 @@ use cnidarium::{StateDelta, Storage}; use metrics_exporter_prometheus::PrometheusBuilder; use pd::{ cli::{Opt, RootCommand, TestnetCommand}, - migrate::Migration::SimpleMigration, + migrate::Migration::Testnet70, testnet::{ config::{get_testnet_dir, parse_tm_address, url_has_necessary_parts}, generate::TestnetConfig, @@ -442,7 +442,7 @@ async fn main() -> anyhow::Result<()> { migrate_archive, } => { tracing::info!("migrating state in {}", target_directory.display()); - SimpleMigration + Testnet70 .migrate(target_directory.clone(), genesis_start) .await .context("failed to upgrade state")?; diff --git a/crates/bin/pd/src/migrate.rs b/crates/bin/pd/src/migrate.rs index 04feef53f4..d68575f5e6 100644 --- a/crates/bin/pd/src/migrate.rs +++ b/crates/bin/pd/src/migrate.rs @@ -5,15 +5,13 @@ //! This module declares how local `pd` state should be altered, if at all, //! in order to be compatible with the network post-chain-upgrade. use anyhow::Context; +use futures::StreamExt as _; use std::path::PathBuf; -use cnidarium::{StateDelta, StateWrite, Storage}; +use cnidarium::{StateDelta, StateRead, StateWrite, Storage}; use jmt::RootHash; use penumbra_app::{app::StateReadExt, SUBSTORE_PREFIXES}; use penumbra_sct::component::clock::{EpochManager, EpochRead}; -use penumbra_stake::{ - component::validator_handler::ValidatorDataRead, genesis::Content as StakeContent, -}; use crate::testnet::generate::TestnetConfig; @@ -28,8 +26,8 @@ pub enum Migration { /// A simple migration: adds a key to the consensus state. /// This is useful for testing upgrade mechanisms, including in production. SimpleMigration, - /// Migrates from testnet-64 to testnet-65. - Testnet65, + /// Testnet-70 migration: move swap executions from the jmt to nv-storage. + Testnet70, } impl Migration { @@ -73,14 +71,8 @@ impl Migration { /* ---------- generate genesis ------------ */ let chain_id = migrated_state.get_chain_id().await?; - let validators = migrated_state.validator_definitions().await?; let app_state = penumbra_genesis::Content { chain_id, - stake_content: StakeContent { - // TODO(erwan): See https://github.com/penumbra-zone/penumbra/issues/3846 - validators: validators.into_iter().map(Into::into).collect(), - ..Default::default() - }, ..Default::default() }; let mut genesis = @@ -110,7 +102,94 @@ impl Migration { std::fs::write(validator_state_path, fresh_validator_state) .expect("can write validator state"); } - Migration::Testnet65 => { /* currently a no-op. */ } + Migration::Testnet70 => { + // Our goal is to fetch all swap executions from the jmt and store them in nv-storage. + // In particular, we want to make sure that client lookups for (height, trading pair) + // resolve to the same value as before. + + // Setup: + let start_time = std::time::SystemTime::now(); + let rocksdb_dir = path_to_export.join("rocksdb"); + let storage = + Storage::load(rocksdb_dir.clone(), SUBSTORE_PREFIXES.to_vec()).await?; + let export_state = storage.latest_snapshot(); + let root_hash = export_state.root_hash().await.expect("can get root hash"); + let pre_upgrade_root_hash: RootHash = root_hash.into(); + let pre_upgrade_height = export_state + .get_block_height() + .await + .expect("can get block height"); + let post_upgrade_height = pre_upgrade_height.wrapping_add(1); + + // We initialize a `StateDelta` and start by reaching into the JMT for all entries matching the + // swap execution prefix. Then, we write each entry to the nv-storage. + let mut delta = StateDelta::new(export_state); + + let prefix_key = "dex/swap_execution/"; + let mut swap_execution_stream = delta.prefix_raw(prefix_key); + + while let Some(r) = swap_execution_stream.next().await { + let (key, swap_execution) = r?; + tracing::info!("migrating swap execution: {}", key); + delta.nonverifiable_put_raw(key.into_bytes(), swap_execution); + } + + delta.put_block_height(0u64); + + let post_upgrade_root_hash = storage.commit_in_place(delta).await?; + tracing::info!(?post_upgrade_root_hash, "post-upgrade root hash"); + + let migration_duration = start_time.elapsed().unwrap(); + + // Reload storage so we can make reads against its migrated state: + storage.release().await; + let storage = Storage::load(rocksdb_dir, SUBSTORE_PREFIXES.to_vec()).await?; + let migrated_state = storage.latest_snapshot(); + + // The migration is complete, now we need to generate a genesis file. To do this, we need + // to lookup a validator view from the chain, and specify the post-upgrade app hash and + // initial height. + let chain_id = migrated_state.get_chain_id().await?; + let app_state = penumbra_genesis::Content { + chain_id, + ..Default::default() + }; + let mut genesis = + TestnetConfig::make_genesis(app_state.clone()).expect("can make genesis"); + genesis.app_hash = post_upgrade_root_hash + .0 + .to_vec() + .try_into() + .expect("infaillible conversion"); + genesis.initial_height = post_upgrade_height as i64; + genesis.genesis_time = genesis_start.unwrap_or_else(|| { + let now = tendermint::time::Time::now(); + tracing::info!(%now, "no genesis time provided, detecting a testing setup"); + now + }); + let checkpoint = post_upgrade_root_hash.0.to_vec(); + let genesis = TestnetConfig::make_checkpoint(genesis, Some(checkpoint)); + + let genesis_json = serde_json::to_string(&genesis).expect("can serialize genesis"); + tracing::info!("genesis: {}", genesis_json); + let genesis_path = path_to_export.join("genesis.json"); + std::fs::write(genesis_path, genesis_json).expect("can write genesis"); + + let validator_state_path = path_to_export.join("priv_validator_state.json"); + let fresh_validator_state = + crate::testnet::generate::TestnetValidator::initial_state(); + std::fs::write(validator_state_path, fresh_validator_state) + .expect("can write validator state"); + + tracing::info!( + pre_upgrade_height, + post_upgrade_height, + ?pre_upgrade_root_hash, + ?post_upgrade_root_hash, + duration = migration_duration.as_secs(), + "successful migration!" + ); + } } Ok(()) } diff --git a/crates/core/app/src/app/mod.rs b/crates/core/app/src/app/mod.rs index ac3493a75c..e08991de43 100644 --- a/crates/core/app/src/app/mod.rs +++ b/crates/core/app/src/app/mod.rs @@ -619,7 +619,7 @@ impl App { /// /// Increment this manually after fixing the root cause for a chain halt: updated nodes will then be /// able to proceed past the block height of the halt. -const TOTAL_HALT_COUNT: u64 = 0; +const TOTAL_HALT_COUNT: u64 = 1; #[async_trait] pub trait StateReadExt: StateRead { diff --git a/crates/core/component/dex/src/component/dex.rs b/crates/core/component/dex/src/component/dex.rs index 38456735d6..a8727a30f5 100644 --- a/crates/core/component/dex/src/component/dex.rs +++ b/crates/core/component/dex/src/component/dex.rs @@ -152,7 +152,7 @@ pub trait StateReadExt: StateRead { height: u64, trading_pair: DirectedTradingPair, ) -> Result> { - self.get(&state_key::swap_execution(height, trading_pair)) + self.nonverifiable_get(state_key::swap_execution(height, trading_pair).as_bytes()) .await } @@ -240,17 +240,11 @@ pub trait StateWriteExt: StateWrite + StateReadExt { // Store the swap executions for both directions in the state as well. if let Some(swap_execution) = swap_execution_1_for_2.clone() { let tp_1_for_2 = DirectedTradingPair::new(trading_pair.asset_1, trading_pair.asset_2); - self.put( - state_key::swap_execution(height, tp_1_for_2), - swap_execution, - ); + self.put_swap_execution_at_height(height, tp_1_for_2, swap_execution); } if let Some(swap_execution) = swap_execution_2_for_1.clone() { let tp_2_for_1 = DirectedTradingPair::new(trading_pair.asset_2, trading_pair.asset_1); - self.put( - state_key::swap_execution(height, tp_2_for_1), - swap_execution, - ); + self.put_swap_execution_at_height(height, tp_2_for_1, swap_execution); } // ... and also add it to the set in the compact block to be pushed out to clients. @@ -299,6 +293,16 @@ pub trait StateWriteExt: StateWrite + StateReadExt { Ok(()) } + + fn put_swap_execution_at_height( + &mut self, + height: u64, + pair: DirectedTradingPair, + swap_execution: SwapExecution, + ) { + let path = state_key::swap_execution(height, pair); + self.nonverifiable_put(path.as_bytes().to_vec(), swap_execution); + } } impl StateWriteExt for T {} diff --git a/crates/proto/src/state/write.rs b/crates/proto/src/state/write.rs index d3367ebf01..57b374cefe 100644 --- a/crates/proto/src/state/write.rs +++ b/crates/proto/src/state/write.rs @@ -22,6 +22,15 @@ pub trait StateWriteProto: StateWrite + Send + Sync { self.put_raw(key, value.encode_to_vec()); } + /// Puts a domain type into the nonverifiable key-value store with the given key + fn nonverifiable_put(&mut self, key: Vec, value: D) + where + D: DomainType, + anyhow::Error: From<>::Error>, + { + self.nonverifiable_put_raw(key, value.encode_to_vec()); + } + /// Records a Protobuf message as a typed ABCI event. fn record_proto(&mut self, proto_event: E) where