penumbra_app/app_version/
component.rsuse std::fmt::Write as _;
use anyhow::{anyhow, Context};
use cnidarium::{StateDelta, Storage};
use penumbra_proto::{StateReadProto, StateWriteProto};
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",
9 => "v0.81.x",
_ => "unknown",
}
}
#[derive(Debug, Clone, Copy)]
enum CheckContext {
Running,
Migration,
}
fn check_version(ctx: CheckContext, expected: u64, found: Option<u64>) -> anyhow::Result<()> {
let found = found.unwrap_or(expected);
if found == expected {
return Ok(());
}
match ctx {
CheckContext::Running => {
let expected_name = version_to_software_version(expected);
let found_name = version_to_software_version(found);
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(found);
let mut error = String::new();
if found == APP_VERSION {
write!(
&mut error,
"state already migrated to version {}",
APP_VERSION
)?;
anyhow::bail!(error);
}
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))
}
}
}
async fn read_app_version_safeguard<S: StateReadProto>(s: &S) -> anyhow::Result<Option<u64>> {
let out = s
.nonverifiable_get_proto(crate::app::state_key::app_version::safeguard().as_bytes())
.await
.context("while reading app version safeguard")?;
Ok(out)
}
fn write_app_version_safeguard<S: StateWriteProto>(s: &mut S, x: u64) {
s.nonverifiable_put_proto(
crate::app::state_key::app_version::safeguard()
.as_bytes()
.to_vec(),
x,
)
}
pub async fn check_and_update_app_version(s: Storage) -> anyhow::Result<()> {
if s.latest_version() == u64::MAX {
return Ok(());
}
let mut delta = StateDelta::new(s.latest_snapshot());
match read_app_version_safeguard(&delta).await? {
None => {
tracing::debug!(?APP_VERSION, "version safeguard not found, initializing");
write_app_version_safeguard(&mut delta, APP_VERSION);
s.commit_in_place(delta).await?;
}
Some(found) => check_version(CheckContext::Running, APP_VERSION, Some(found))?,
}
Ok(())
}
pub async fn migrate_app_version<S: StateWriteProto>(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);
Ok(())
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn ensure_app_version_is_current_in_checks() -> anyhow::Result<()> {
let result = version_to_software_version(APP_VERSION);
assert!(
result != "unknown",
"APP_VERSION lacks a corresponding software version"
);
Ok(())
}
}