penumbra_sdk_app/app_version/
component.rs

1use std::fmt::Write as _;
2
3use anyhow::{anyhow, Context};
4use cnidarium::{StateDelta, Storage};
5use penumbra_sdk_proto::{StateReadProto, StateWriteProto};
6
7use super::APP_VERSION;
8
9fn version_to_software_version(version: u64) -> &'static str {
10    match version {
11        1 => "v0.70.x",
12        2 => "v0.73.x",
13        3 => "v0.74.x",
14        4 => "v0.75.x",
15        5 => "v0.76.x",
16        6 => "v0.77.x",
17        7 => "v0.79.x",
18        8 => "v0.80.x",
19        9 => "v0.81.x",
20        _ => "unknown",
21    }
22}
23
24#[derive(Debug, Clone, Copy)]
25enum CheckContext {
26    Running,
27    Migration,
28}
29
30/// Check that the expected version matches the found version, if it set.
31/// This will return an error if the versions do not match.
32fn check_version(ctx: CheckContext, expected: u64, found: Option<u64>) -> anyhow::Result<()> {
33    let found = found.unwrap_or(expected);
34    if found == expected {
35        return Ok(());
36    }
37
38    match ctx {
39        CheckContext::Running => {
40            let expected_name = version_to_software_version(expected);
41            let found_name = version_to_software_version(found);
42            let mut error = String::new();
43            error.push_str("app version mismatch:\n");
44            write!(
45                &mut error,
46                "  expected {} (penumbra {})\n",
47                expected, expected_name
48            )?;
49            write!(&mut error, "  found {} (penumbra {})\n", found, found_name)?;
50            write!(
51                &mut error,
52                "make sure you're running penumbra {}",
53                expected_name
54            )?;
55            Err(anyhow!(error))
56        }
57        CheckContext::Migration => {
58            let expected_name = version_to_software_version(expected);
59            let found_name = version_to_software_version(found);
60            let mut error = String::new();
61            if found == APP_VERSION {
62                write!(
63                    &mut error,
64                    "state already migrated to version {}",
65                    APP_VERSION
66                )?;
67                anyhow::bail!(error);
68            }
69            error.push_str("app version mismatch:\n");
70            write!(
71                &mut error,
72                "  expected {} (penumbra {})\n",
73                expected, expected_name
74            )?;
75            write!(&mut error, "  found {} (penumbra {})\n", found, found_name)?;
76            write!(
77                &mut error,
78                "this migration should be run with penumbra {} instead",
79                version_to_software_version(expected + 1)
80            )?;
81            Err(anyhow!(error))
82        }
83    }
84}
85
86/// Read the app version safeguard from nonverifiable storage.
87async fn read_app_version_safeguard<S: StateReadProto>(s: &S) -> anyhow::Result<Option<u64>> {
88    let out = s
89        .nonverifiable_get_proto(crate::app::state_key::app_version::safeguard().as_bytes())
90        .await
91        .context("while reading app version safeguard")?;
92    Ok(out)
93}
94
95/// Write the app version safeguard to nonverifiable storage.
96fn write_app_version_safeguard<S: StateWriteProto>(s: &mut S, x: u64) {
97    s.nonverifiable_put_proto(
98        crate::app::state_key::app_version::safeguard()
99            .as_bytes()
100            .to_vec(),
101        x,
102    )
103}
104
105/// Ensure that the app version safeguard is `APP_VERSION`, or update it if it is missing.
106///
107/// # Errors
108/// This method errors if the app version safeguard is different than `APP_VERSION`.
109///
110/// # Usage
111/// This should be called on startup. This method short-circuits if the database
112/// is uninitialized (pregenesis).
113///
114/// # UIP:
115/// More context is available in the UIP-6 document: https://uips.penumbra.zone/uip-6.html
116pub async fn check_and_update_app_version(s: Storage) -> anyhow::Result<()> {
117    // If the storage is not initialized, avoid touching it at all,
118    // to avoid complaints about it already being initialized before the first genesis.
119    if s.latest_version() == u64::MAX {
120        return Ok(());
121    }
122    let mut delta = StateDelta::new(s.latest_snapshot());
123
124    // If the safeguard is not set, set it to the current version.
125    // Otherwise, ensure that it matches the current version.
126    match read_app_version_safeguard(&delta).await? {
127        None => {
128            tracing::debug!(?APP_VERSION, "version safeguard not found, initializing");
129            write_app_version_safeguard(&mut delta, APP_VERSION);
130            s.commit_in_place(delta).await?;
131        }
132        Some(found) => check_version(CheckContext::Running, APP_VERSION, Some(found))?,
133    }
134    Ok(())
135}
136
137/// Migrate the app version to a given number.
138///
139/// This will check that the app version is currently the previous version, if set at all.
140///
141/// This is the recommended way to change the app version, and should be called during a migration
142/// with breaking consensus logic.
143pub async fn migrate_app_version<S: StateWriteProto>(s: &mut S, to: u64) -> anyhow::Result<()> {
144    anyhow::ensure!(to > 1, "you can't migrate to the first penumbra version!");
145    let found = read_app_version_safeguard(s).await?;
146    check_version(CheckContext::Migration, to - 1, found)?;
147    write_app_version_safeguard(s, to);
148    Ok(())
149}
150
151#[cfg(test)]
152mod test {
153    use super::*;
154    #[test]
155    /// Confirm there's a matching branch on the APP_VERSION to crate version lookup.
156    /// It's possible to overlook that update when bumping the APP_VERSION, so this test
157    /// ensures that if the APP_VERSION was changed, so was the match arm.
158    fn ensure_app_version_is_current_in_checks() -> anyhow::Result<()> {
159        let result = version_to_software_version(APP_VERSION);
160        assert!(
161            result != "unknown",
162            "APP_VERSION lacks a corresponding software version"
163        );
164        Ok(())
165    }
166}