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