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