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