penumbra_sdk_app/app_version/
component.rs1use 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
30fn 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
86async 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
95fn 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
105pub async fn check_and_update_app_version(s: Storage) -> anyhow::Result<()> {
117 if s.latest_version() == u64::MAX {
120 return Ok(());
121 }
122 let mut delta = StateDelta::new(s.latest_snapshot());
123
124 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
137pub 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 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}