use super::invocation::SnsCanisterMethod;
use crate::hex::is_canonical_lowercase_hex;
use crate::sns::report::SnsReportContext;
use serde::{Deserialize as SerdeDeserialize, Serialize};
#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct SnsVersion {
pub archive_wasm_hash_hex: String,
pub root_wasm_hash_hex: String,
pub swap_wasm_hash_hex: String,
pub ledger_wasm_hash_hex: String,
pub governance_wasm_hash_hex: String,
pub index_wasm_hash_hex: String,
}
impl SnsVersion {
pub(in crate::sns::report) fn validate_hashes(
&self,
field: &str,
expected_bytes: Option<usize>,
) -> Result<(), String> {
for (role, hash) in [
("archive", self.archive_wasm_hash_hex.as_str()),
("root", self.root_wasm_hash_hex.as_str()),
("swap", self.swap_wasm_hash_hex.as_str()),
("ledger", self.ledger_wasm_hash_hex.as_str()),
("governance", self.governance_wasm_hash_hex.as_str()),
("index", self.index_wasm_hash_hex.as_str()),
] {
if !is_canonical_lowercase_hex(hash)
|| expected_bytes.is_some_and(|bytes| hash.len() / 2 != bytes)
{
let expectation = expected_bytes.map_or_else(
|| "lowercase even-length hexadecimal text".to_string(),
|bytes| format!("a {bytes}-byte lowercase hexadecimal hash"),
);
return Err(format!("{field}.{role}_wasm_hash_hex is not {expectation}"));
}
}
Ok(())
}
}
#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct SnsPendingUpgrade {
pub mark_failed_at_seconds: u64,
pub checking_upgrade_lock: u64,
pub proposal_id: u64,
pub target_version: Option<SnsVersion>,
}
#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct SnsRunningVersionResponse {
pub deployed_version: Option<SnsVersion>,
pub pending_version: Option<SnsPendingUpgrade>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct SnsUpgradeQueryGap {
pub method: SnsCanisterMethod,
pub reason: String,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct SnsUpgradeReport {
pub schema_version: u32,
#[serde(flatten)]
pub context: SnsReportContext,
pub running_version_method: SnsCanisterMethod,
pub next_version_method: SnsCanisterMethod,
pub point_in_time_guaranteed: bool,
pub component_query_count: usize,
pub successful_component_query_count: usize,
pub component_gap_count: usize,
pub deployed_version: SnsVersion,
pub pending_upgrade: Option<SnsPendingUpgrade>,
pub next_version: Option<SnsVersion>,
pub next_version_gap: Option<SnsUpgradeQueryGap>,
}
#[cfg(test)]
mod tests {
use super::SnsVersion;
#[test]
fn version_hash_admission_preserves_upgrade_and_checkpoint_policies() {
for (hash, upgrade_valid, checkpoint_valid) in [
("0a".repeat(32), true, true),
("0a".to_string(), true, false),
("0a".repeat(33), true, false),
(String::new(), false, false),
("a".to_string(), false, false),
("AB".repeat(32), false, false),
("zz".repeat(32), false, false),
] {
let version = SnsVersion {
archive_wasm_hash_hex: hash.clone(),
root_wasm_hash_hex: hash.clone(),
swap_wasm_hash_hex: hash.clone(),
ledger_wasm_hash_hex: hash.clone(),
governance_wasm_hash_hex: hash.clone(),
index_wasm_hash_hex: hash,
};
assert_eq!(
version.validate_hashes("version", None).is_ok(),
upgrade_valid
);
assert_eq!(
version.validate_hashes("version", Some(32)).is_ok(),
checkpoint_valid
);
}
}
}