Skip to main content

ic_query/sns/report/model/reports/
upgrade.rs

1//! Module: sns::report::model::reports::upgrade
2//!
3//! Responsibility: bounded native SNS upgrade report DTOs.
4//! Does not own: Governance or SNS-W calls, target admission, lookup, or rendering.
5//! Boundary: preserves native versions and validates their shared hash representation.
6
7use super::invocation::SnsCanisterMethod;
8use crate::hex::is_canonical_lowercase_hex;
9use crate::sns::report::SnsReportContext;
10use serde::{Deserialize as SerdeDeserialize, Serialize};
11
12///
13/// SnsVersion
14///
15/// Native six-role SNS Wasm version represented as lowercase hexadecimal hashes.
16///
17
18#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
19#[serde(deny_unknown_fields)]
20pub struct SnsVersion {
21    /// Archive Wasm hash.
22    pub archive_wasm_hash_hex: String,
23    /// Root Wasm hash.
24    pub root_wasm_hash_hex: String,
25    /// Swap Wasm hash.
26    pub swap_wasm_hash_hex: String,
27    /// Ledger Wasm hash.
28    pub ledger_wasm_hash_hex: String,
29    /// Governance Wasm hash.
30    pub governance_wasm_hash_hex: String,
31    /// Ledger index Wasm hash.
32    pub index_wasm_hash_hex: String,
33}
34
35impl SnsVersion {
36    pub(in crate::sns::report) fn validate_hashes(
37        &self,
38        field: &str,
39        expected_bytes: Option<usize>,
40    ) -> Result<(), String> {
41        for (role, hash) in [
42            ("archive", self.archive_wasm_hash_hex.as_str()),
43            ("root", self.root_wasm_hash_hex.as_str()),
44            ("swap", self.swap_wasm_hash_hex.as_str()),
45            ("ledger", self.ledger_wasm_hash_hex.as_str()),
46            ("governance", self.governance_wasm_hash_hex.as_str()),
47            ("index", self.index_wasm_hash_hex.as_str()),
48        ] {
49            if !is_canonical_lowercase_hex(hash)
50                || expected_bytes.is_some_and(|bytes| hash.len() / 2 != bytes)
51            {
52                let expectation = expected_bytes.map_or_else(
53                    || "lowercase even-length hexadecimal text".to_string(),
54                    |bytes| format!("a {bytes}-byte lowercase hexadecimal hash"),
55                );
56                return Err(format!("{field}.{role}_wasm_hash_hex is not {expectation}"));
57            }
58        }
59        Ok(())
60    }
61}
62
63///
64/// SnsPendingUpgrade
65///
66/// Native pending-upgrade state reported by SNS Governance.
67///
68
69#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
70#[serde(deny_unknown_fields)]
71pub struct SnsPendingUpgrade {
72    /// Native deadline after which Governance may mark the upgrade failed.
73    pub mark_failed_at_seconds: u64,
74    /// Native Governance upgrade-lock checking value.
75    pub checking_upgrade_lock: u64,
76    /// NNS proposal that initiated the pending upgrade.
77    pub proposal_id: u64,
78    /// Pending target version, when Governance returned it.
79    pub target_version: Option<SnsVersion>,
80}
81
82///
83/// SnsRunningVersionResponse
84///
85/// Complete native Governance running-version response retained for bracketing.
86///
87
88#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
89#[serde(deny_unknown_fields)]
90pub struct SnsRunningVersionResponse {
91    /// Currently deployed six-role version when Governance supplied it.
92    pub deployed_version: Option<SnsVersion>,
93    /// Pending Governance upgrade state when present.
94    pub pending_version: Option<SnsPendingUpgrade>,
95}
96
97///
98/// SnsUpgradeQueryGap
99///
100/// Failed next-version query retained after deployed-version collection succeeded.
101///
102
103#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
104pub struct SnsUpgradeQueryGap {
105    /// Native SNS-W method that failed.
106    pub method: SnsCanisterMethod,
107    /// Transport, encoding, or decoding failure retained for diagnostics.
108    pub reason: String,
109}
110
111///
112/// SnsUpgradeReport
113///
114/// Bounded live report of one SNS deployed, pending, and next blessed version.
115///
116
117#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
118pub struct SnsUpgradeReport {
119    /// Report schema version.
120    pub schema_version: u32,
121    /// SNS identity and provenance, serialized as flat report fields.
122    #[serde(flatten)]
123    pub context: SnsReportContext,
124    /// Native Governance running-version method.
125    pub running_version_method: SnsCanisterMethod,
126    /// Native SNS-W next-version method.
127    pub next_version_method: SnsCanisterMethod,
128    /// Whether both component responses represent one authoritative point in time.
129    pub point_in_time_guaranteed: bool,
130    /// Fixed number of bounded upgrade component queries attempted.
131    pub component_query_count: usize,
132    /// Number of component queries that returned successfully.
133    pub successful_component_query_count: usize,
134    /// Number of retained next-version query gaps.
135    pub component_gap_count: usize,
136    /// Governance-reported deployed SNS version.
137    pub deployed_version: SnsVersion,
138    /// Governance-reported pending upgrade, when present.
139    pub pending_upgrade: Option<SnsPendingUpgrade>,
140    /// Next blessed SNS-W version, or `None` when no successor exists.
141    pub next_version: Option<SnsVersion>,
142    /// Failed next-version query, distinct from a successful response with no successor.
143    pub next_version_gap: Option<SnsUpgradeQueryGap>,
144}
145
146#[cfg(test)]
147mod tests {
148    use super::SnsVersion;
149
150    #[test]
151    fn version_hash_admission_preserves_upgrade_and_checkpoint_policies() {
152        for (hash, upgrade_valid, checkpoint_valid) in [
153            ("0a".repeat(32), true, true),
154            ("0a".to_string(), true, false),
155            ("0a".repeat(33), true, false),
156            (String::new(), false, false),
157            ("a".to_string(), false, false),
158            ("AB".repeat(32), false, false),
159            ("zz".repeat(32), false, false),
160        ] {
161            let version = SnsVersion {
162                archive_wasm_hash_hex: hash.clone(),
163                root_wasm_hash_hex: hash.clone(),
164                swap_wasm_hash_hex: hash.clone(),
165                ledger_wasm_hash_hex: hash.clone(),
166                governance_wasm_hash_hex: hash.clone(),
167                index_wasm_hash_hex: hash,
168            };
169            assert_eq!(
170                version.validate_hashes("version", None).is_ok(),
171                upgrade_valid
172            );
173            assert_eq!(
174                version.validate_hashes("version", Some(32)).is_ok(),
175                checkpoint_valid
176            );
177        }
178    }
179}