penumbra_app/app_version/
component.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
use 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,
}

/// Check that the expected version matches the found version, if it set.
/// This will return an error if the versions do not match.
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))
        }
    }
}

/// Read the app version safeguard from nonverifiable storage.
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)
}

/// Write the app version safeguard to nonverifiable storage.
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,
    )
}

/// Ensure that the app version safeguard is `APP_VERSION`, or update it if it is missing.
///
/// # Errors
/// This method errors if the app version safeguard is different than `APP_VERSION`.
///
/// # Usage
/// This should be called on startup. This method short-circuits if the database
/// is uninitialized (pregenesis).
///
/// # UIP:
/// More context is available in the UIP-6 document: https://uips.penumbra.zone/uip-6.html
pub async fn check_and_update_app_version(s: Storage) -> anyhow::Result<()> {
    // If the storage is not initialized, avoid touching it at all,
    // to avoid complaints about it already being initialized before the first genesis.
    if s.latest_version() == u64::MAX {
        return Ok(());
    }
    let mut delta = StateDelta::new(s.latest_snapshot());

    // If the safeguard is not set, set it to the current version.
    // Otherwise, ensure that it matches the current version.
    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(())
}

/// Migrate the app version to a given number.
///
/// This will check that the app version is currently the previous version, if set at all.
///
/// This is the recommended way to change the app version, and should be called during a migration
/// with breaking consensus logic.
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]
    /// Confirm there's a matching branch on the APP_VERSION to crate version lookup.
    /// It's possible to overlook that update when bumping the APP_VERSION, so this test
    /// ensures that if the APP_VERSION was changed, so was the match arm.
    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(())
    }
}