diff --git a/crates/bin/pd/src/main.rs b/crates/bin/pd/src/main.rs index 4c2f003ae4..db02281fbe 100644 --- a/crates/bin/pd/src/main.rs +++ b/crates/bin/pd/src/main.rs @@ -19,6 +19,7 @@ use pd::{ join::network_join, }, }; +use penumbra_app::app_version::{assert_latest_app_version, migrate_app_version}; use penumbra_app::SUBSTORE_PREFIXES; use rand::Rng; use rand_core::OsRng; @@ -102,6 +103,7 @@ async fn main() -> anyhow::Result<()> { .context( "Unable to initialize RocksDB storage - is there another `pd` process running?", )?; + assert_latest_app_version(storage.clone()).await?; tracing::info!( ?abci_bind, diff --git a/crates/bin/pd/src/migrate/mainnet1.rs b/crates/bin/pd/src/migrate/mainnet1.rs index 2045c7f14c..1a51e78aeb 100644 --- a/crates/bin/pd/src/migrate/mainnet1.rs +++ b/crates/bin/pd/src/migrate/mainnet1.rs @@ -7,6 +7,7 @@ use ibc_types::core::channel::{Packet, PortId}; use ibc_types::transfer::acknowledgement::TokenTransferAcknowledgement; use jmt::RootHash; use penumbra_app::app::StateReadExt as _; +use penumbra_app::app_version::migrate_app_version; use penumbra_governance::StateWriteExt; use penumbra_ibc::{component::ChannelStateWriteExt as _, IbcRelay}; use penumbra_sct::component::clock::EpochManager; @@ -111,6 +112,16 @@ pub async fn migrate( let (migration_duration, post_upgrade_root_hash) = { let start_time = std::time::SystemTime::now(); + // Note, when this bit of code was added, the upgrade happened months ago, + // and the verison safeguard mechanism was not in place. However, + // adding this will prevent someone running version 0.80.X with the + // safeguard patch from accidentally running the migraton again, since they + // will already have version 8 written into the state. But, if someone is syncing + // up from genesis, then version 0.79 will not have written anything into the safeguard, + // and this method will not complain. So, this addition provides a safeguard + // for existing nodes, while also not impeding syncing up a node from scratch. + migrate_app_version(&mut delta, 8).await?; + // Reinsert all of the erroneously removed packets replace_lost_packets(&mut delta).await?; diff --git a/crates/core/app/src/app_version.rs b/crates/core/app/src/app_version.rs new file mode 100644 index 0000000000..93ad9afc47 --- /dev/null +++ b/crates/core/app/src/app_version.rs @@ -0,0 +1,10 @@ +/// Representation of the Penumbra application version. Notably, this is distinct +/// from the crate version(s). This number should only ever be incremented. +pub const APP_VERSION: u64 = 8; + +cfg_if::cfg_if! { + if #[cfg(feature="component")] { + mod component; + pub use component::{assert_latest_app_version, migrate_app_version}; + } +} diff --git a/crates/core/app/src/app_version/component.rs b/crates/core/app/src/app_version/component.rs new file mode 100644 index 0000000000..52af54cd6c --- /dev/null +++ b/crates/core/app/src/app_version/component.rs @@ -0,0 +1,133 @@ +use std::fmt::Write as _; + +use anyhow::{anyhow, Context}; +use cnidarium::{StateDelta, StateRead, StateWrite, Storage}; + +use super::APP_VERSION; + +fn version_to_software_version(version: u64) -> &'static str { + match version { + 1 => "v0.70.x", + 2 => "v0.73.x", + 3 => "v0.74.x", + 4 => "v0.75.x", + 5 => "v0.76.x", + 6 => "v0.77.x", + 7 => "v0.79.x", + 8 => "v0.80.x", + _ => "unknown", + } +} + +#[derive(Debug, Clone, Copy)] +enum CheckContext { + Running, + Migration, +} + +fn check_version(ctx: CheckContext, expected: u64, found: Option) -> anyhow::Result<()> { + let found = match (expected, found) { + (x, Some(y)) if x != y => y, + _ => return Ok(()), + }; + match ctx { + CheckContext::Running => { + let expected_name = version_to_software_version(expected); + let found_name = version_to_software_version(expected); + let mut error = String::new(); + error.push_str("app version mismatch:\n"); + write!( + &mut error, + " expected {} (penumbra {})\n", + expected, expected_name + )?; + write!(&mut error, " found {} (penumbra {})\n", found, found_name)?; + write!( + &mut error, + "make sure you're running penumbra {}", + expected_name + )?; + Err(anyhow!(error)) + } + CheckContext::Migration => { + let expected_name = version_to_software_version(expected); + let found_name = version_to_software_version(expected); + let mut error = String::new(); + error.push_str("app version mismatch:\n"); + write!( + &mut error, + " expected {} (penumbra {})\n", + expected, expected_name + )?; + write!(&mut error, " found {} (penumbra {})\n", found, found_name)?; + write!( + &mut error, + "this migration should be run with penumbra {} instead", + version_to_software_version(expected + 1) + )?; + Err(anyhow!(error)) + } + } +} + +fn state_key() -> Vec { + b"penumbra_app_version_safeguard".to_vec() +} + +async fn read_app_version_safeguard(s: &S) -> anyhow::Result> { + const CTX: &'static str = "while reading app_version_safeguard"; + + let res = s.nonverifiable_get_raw(&state_key()).await.context(CTX)?; + match res { + None => Ok(None), + Some(x) => { + let bytes: [u8; 8] = x + .try_into() + .map_err(|bad: Vec| { + anyhow!("expected bytes to have length 8, found: {}", bad.len()) + }) + .context(CTX)?; + Ok(Some(u64::from_le_bytes(bytes))) + } + } +} + +// Neither async nor a result are needed, but only right now, so I'm putting these here +// to reserve the right to change them later. +async fn write_app_version_safeguard(s: &mut S, x: u64) -> anyhow::Result<()> { + let bytes = u64::to_le_bytes(x).to_vec(); + s.nonverifiable_put_raw(state_key(), bytes); + Ok(()) +} + +/// Assert that the app version saved in the state is the correct one. +/// +/// You should call this before starting a node. +/// +/// This will succeed if no app version is saved, or if the app version saved matches +/// exactly. +/// +/// This will also result in the current app version being stored, so that future +/// calls to this function will be checked against this state. +pub async fn assert_latest_app_version(s: Storage) -> anyhow::Result<()> { + let mut delta = StateDelta::new(s.latest_snapshot()); + let found = read_app_version_safeguard(&delta).await?; + check_version(CheckContext::Running, APP_VERSION, found)?; + write_app_version_safeguard(&mut delta, APP_VERSION).await?; + s.commit(delta).await?; + Ok(()) +} + +/// Migrate the app version to a given number. +/// +/// This will check that the app version is currently the previous version, if set at all. +/// +/// This is the only way to change the app version, and should be called during a migration +/// with breaking consensus logic. +pub async fn migrate_app_version(s: &mut S, to: u64) -> anyhow::Result<()> { + anyhow::ensure!(to > 1, "you can't migrate to the first penumbra version!"); + let found = read_app_version_safeguard(s).await?; + check_version(CheckContext::Migration, to - 1, found)?; + write_app_version_safeguard(s, to).await?; + Ok(()) +} diff --git a/crates/core/app/src/lib.rs b/crates/core/app/src/lib.rs index 21e75bd50d..46fc1f1d84 100644 --- a/crates/core/app/src/lib.rs +++ b/crates/core/app/src/lib.rs @@ -13,9 +13,8 @@ pub static SUBSTORE_PREFIXES: Lazy> = Lazy::new(|| { /// The substore prefix used for storing historical CometBFT block data. pub static COMETBFT_SUBSTORE_PREFIX: &'static str = "cometbft-data"; -/// Representation of the Penumbra application version. Notably, this is distinct -/// from the crate version(s). This number should only ever be incremented. -pub const APP_VERSION: u64 = 8; +pub mod app_version; +pub use app_version::APP_VERSION; pub mod genesis; pub mod params;