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