Skip to content

Commit

Permalink
UIP-6: App Version Safeguard
Browse files Browse the repository at this point in the history
  • Loading branch information
cronokirby committed Nov 13, 2024
1 parent 7533cc6 commit b65e3d9
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 3 deletions.
2 changes: 2 additions & 0 deletions crates/bin/pd/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
11 changes: 11 additions & 0 deletions crates/bin/pd/src/migrate/mainnet1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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?;

Expand Down
10 changes: 10 additions & 0 deletions crates/core/app/src/app_version.rs
Original file line number Diff line number Diff line change
@@ -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};
}
}
133 changes: 133 additions & 0 deletions crates/core/app/src/app_version/component.rs
Original file line number Diff line number Diff line change
@@ -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<u64>) -> 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<u8> {
b"penumbra_app_version_safeguard".to_vec()
}

async fn read_app_version_safeguard<S: StateRead>(s: &S) -> anyhow::Result<Option<u64>> {
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<u8>| {
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: StateWrite>(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: StateWrite>(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(())
}
5 changes: 2 additions & 3 deletions crates/core/app/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,8 @@ pub static SUBSTORE_PREFIXES: Lazy<Vec<String>> = 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;
Expand Down

0 comments on commit b65e3d9

Please sign in to comment.