diff --git a/Cargo.lock b/Cargo.lock index 7a38f97c2572..ee9e7554f7fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9529,6 +9529,7 @@ dependencies = [ "ic-nervous-system-clients", "ic-nns-constants", "ic-sns-governance", + "ic-sns-root", "ic-sns-wasm", "pocket-ic", "serde", diff --git a/rs/nervous_system/agent/BUILD.bazel b/rs/nervous_system/agent/BUILD.bazel index 778fd6c0a06a..88fc914ac533 100644 --- a/rs/nervous_system/agent/BUILD.bazel +++ b/rs/nervous_system/agent/BUILD.bazel @@ -9,6 +9,7 @@ DEPENDENCIES = [ "//rs/nns/constants", "//rs/nns/sns-wasm", "//rs/sns/governance", + "//rs/sns/root", "//rs/types/base_types", "@crate_index//:anyhow", "@crate_index//:candid", diff --git a/rs/nervous_system/agent/Cargo.toml b/rs/nervous_system/agent/Cargo.toml index 4d24a388e03d..6fd6bf16418c 100644 --- a/rs/nervous_system/agent/Cargo.toml +++ b/rs/nervous_system/agent/Cargo.toml @@ -17,6 +17,7 @@ ic-nns-constants = { path = "../../nns/constants" } ic-sns-wasm = { path = "../../nns/sns-wasm" } ic-sns-governance = { path = "../../sns/governance" } pocket-ic = { path = "../../../packages/pocket-ic" } +ic-sns-root = { path = "../../sns/root" } serde = { workspace = true } tempfile = { workspace = true } thiserror = { workspace = true } diff --git a/rs/nervous_system/agent/src/nns/sns_wasm.rs b/rs/nervous_system/agent/src/nns/sns_wasm.rs index 9018d8359b33..c981538e5216 100644 --- a/rs/nervous_system/agent/src/nns/sns_wasm.rs +++ b/rs/nervous_system/agent/src/nns/sns_wasm.rs @@ -15,7 +15,7 @@ use tokio::process::Command; use crate::CallCanisters; -pub async fn query_sns_upgrade_steps( +pub async fn query_mainline_sns_upgrade_steps( agent: &C, ) -> Result { let request = ListUpgradeStepsRequest { diff --git a/rs/nervous_system/agent/src/sns/governance.rs b/rs/nervous_system/agent/src/sns/governance.rs index 6efb4013af46..9d5e6e6ced50 100644 --- a/rs/nervous_system/agent/src/sns/governance.rs +++ b/rs/nervous_system/agent/src/sns/governance.rs @@ -1,6 +1,9 @@ use crate::CallCanisters; use ic_base_types::PrincipalId; -use ic_sns_governance::pb::v1::{GetMetadataRequest, GetMetadataResponse}; +use ic_sns_governance::pb::v1::{ + GetMetadataRequest, GetMetadataResponse, GetRunningSnsVersionRequest, + GetRunningSnsVersionResponse, +}; use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Deserialize, Serialize)] @@ -15,4 +18,13 @@ impl GovernanceCanister { ) -> Result { agent.call(self.canister_id, GetMetadataRequest {}).await } + + pub async fn version( + &self, + agent: &C, + ) -> Result { + agent + .call(self.canister_id, GetRunningSnsVersionRequest {}) + .await + } } diff --git a/rs/nervous_system/agent/src/sns/mod.rs b/rs/nervous_system/agent/src/sns/mod.rs index 8c169fc689d4..f59d882f9245 100644 --- a/rs/nervous_system/agent/src/sns/mod.rs +++ b/rs/nervous_system/agent/src/sns/mod.rs @@ -4,7 +4,13 @@ pub mod ledger; pub mod root; pub mod swap; +use anyhow::Result; +use ic_nns_constants::SNS_WASM_CANISTER_ID; +use ic_sns_wasm::pb::v1::{ListUpgradeStepsRequest, ListUpgradeStepsResponse, SnsVersion}; use serde::{Deserialize, Serialize}; + +use crate::CallCanisters; + #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Sns { pub ledger: ledger::LedgerCanister, @@ -14,6 +20,23 @@ pub struct Sns { pub root: root::RootCanister, } +impl Sns { + pub async fn remaining_upgrade_steps( + &self, + agent: &C, + ) -> Result { + let version = self.governance.version(agent).await?; + let list_upgrade_steps_request = ListUpgradeStepsRequest { + limit: 0, + sns_governance_canister_id: Some(self.governance.canister_id), + starting_at: version.deployed_version.map(SnsVersion::from), + }; + agent + .call(SNS_WASM_CANISTER_ID, list_upgrade_steps_request) + .await + } +} + impl TryFrom for Sns { type Error = String; diff --git a/rs/nervous_system/agent/src/sns/root.rs b/rs/nervous_system/agent/src/sns/root.rs index 7ac5651cb1c8..a1aa270b9bf7 100644 --- a/rs/nervous_system/agent/src/sns/root.rs +++ b/rs/nervous_system/agent/src/sns/root.rs @@ -1,7 +1,26 @@ use ic_base_types::PrincipalId; +use ic_sns_root::{GetSnsCanistersSummaryRequest, GetSnsCanistersSummaryResponse}; use serde::{Deserialize, Serialize}; +use crate::CallCanisters; + #[derive(Clone, Debug, Deserialize, Serialize)] pub struct RootCanister { pub canister_id: PrincipalId, } + +impl RootCanister { + pub async fn sns_canisters_summary( + &self, + agent: &C, + ) -> Result { + agent + .call( + self.canister_id, + GetSnsCanistersSummaryRequest { + update_canister_list: None, + }, + ) + .await + } +} diff --git a/rs/nervous_system/tools/sync-with-released-nevous-system-wasms/src/main.rs b/rs/nervous_system/tools/sync-with-released-nevous-system-wasms/src/main.rs index 03f09aa07f1d..cda5d8bd66ac 100644 --- a/rs/nervous_system/tools/sync-with-released-nevous-system-wasms/src/main.rs +++ b/rs/nervous_system/tools/sync-with-released-nevous-system-wasms/src/main.rs @@ -83,7 +83,7 @@ async fn main() -> Result<()> { .into_iter() .collect::>>()?; - let sns_upgrade_steps = sns_wasm::query_sns_upgrade_steps(&agent).await?; + let sns_upgrade_steps = sns_wasm::query_mainline_sns_upgrade_steps(&agent).await?; let latest_sns_version = &sns_upgrade_steps .steps .last() diff --git a/rs/nns/sns-wasm/src/lib.rs b/rs/nns/sns-wasm/src/lib.rs index bde4af9b53e6..d1b8885482c9 100644 --- a/rs/nns/sns-wasm/src/lib.rs +++ b/rs/nns/sns-wasm/src/lib.rs @@ -5,4 +5,5 @@ pub mod pb; mod request_impls; pub mod sns_wasm; pub mod stable_memory; +mod types; pub mod wasm_metadata; diff --git a/rs/nns/sns-wasm/src/types.rs b/rs/nns/sns-wasm/src/types.rs new file mode 100644 index 000000000000..5dd92caa2eea --- /dev/null +++ b/rs/nns/sns-wasm/src/types.rs @@ -0,0 +1,14 @@ +use crate::pb::v1::{ListUpgradeStepsResponse, SnsVersion}; + +impl ListUpgradeStepsResponse { + pub fn version_number(&self, v: SnsVersion) -> Option { + self.steps + .iter() + .position(|x| x.version == Some(v.clone())) + .map(|x| x + 1) + } + + pub fn latest_version_number(&self) -> usize { + self.steps.len() + } +} diff --git a/rs/sns/cli/src/lib.rs b/rs/sns/cli/src/lib.rs index 5cf633484d50..9924c71b03e6 100644 --- a/rs/sns/cli/src/lib.rs +++ b/rs/sns/cli/src/lib.rs @@ -1,5 +1,5 @@ use crate::{ - deploy::DirectSnsDeployerForTests, init_config_file::InitConfigFileArgs, + deploy::DirectSnsDeployerForTests, init_config_file::InitConfigFileArgs, lint::LintArgs, neuron_id_to_candid_subaccount::NeuronIdToCandidSubaccountArgs, prepare_canisters::PrepareCanistersArgs, propose::ProposeArgs, }; @@ -31,6 +31,7 @@ use tempfile::NamedTempFile; pub mod deploy; pub mod init_config_file; +pub mod lint; pub mod list; pub mod neuron_id_to_candid_subaccount; pub mod prepare_canisters; @@ -77,6 +78,8 @@ pub enum SubCommand { NeuronIdToCandidSubaccount(NeuronIdToCandidSubaccountArgs), /// List SNSes List(list::ListArgs), + /// Check SNSes for warnings and errors. + Lint(LintArgs), } impl CliArgs { diff --git a/rs/sns/cli/src/lint.rs b/rs/sns/cli/src/lint.rs new file mode 100644 index 000000000000..6f619fe49879 --- /dev/null +++ b/rs/sns/cli/src/lint.rs @@ -0,0 +1,180 @@ +use crate::table::TableRow; +use anyhow::{anyhow, Result}; +use clap::Parser; +use futures::{stream, StreamExt}; +use ic_agent::Agent; +use ic_nervous_system_agent::nns::sns_wasm; +use itertools::Itertools; + +/// The arguments used to configure the lint command +#[derive(Debug, Parser)] +pub struct LintArgs {} + +struct SnsLintInfo { + name: String, + high_memory_consumption: Vec<(String, u64)>, + low_cycles: Vec<(String, u64)>, + num_remaining_upgrade_steps: usize, +} + +impl TableRow for SnsLintInfo { + fn column_names() -> Vec<&'static str> { + vec!["Name", "Memory", "Cycles", "Upgrades Remaining"] + } + + fn column_values(&self) -> Vec { + let memory_consumption = self + .high_memory_consumption + .iter() + .map(|(canister_type, memory_consumption)| { + format!( + "{canister_type} ({:.2} GiB)", + *memory_consumption as f64 / 1024.0 / 1024.0 / 1024.0 + ) + }) + .join(", "); + let memory_consumption = if !memory_consumption.is_empty() { + format!("❌ {memory_consumption}") + } else { + "👍".to_string() + }; + let low_cycles = self + .low_cycles + .iter() + .map(|(canister_type, cycles)| { + format!( + "{canister_type} ({:.2} TC)", + *cycles as f64 / 1000.0 / 1000.0 / 1000.0 / 1000.0 + ) + }) + .join(", "); + let low_cycles = if !low_cycles.is_empty() { + format!("❌ {low_cycles}") + } else { + "👍".to_string() + }; + vec![ + self.name.clone(), + memory_consumption, + low_cycles, + format!("{}", self.num_remaining_upgrade_steps), + ] + } +} + +pub async fn exec(_args: LintArgs, agent: &Agent) -> Result<()> { + eprintln!("Linting SNSes..."); + + let snses = sns_wasm::list_deployed_snses(agent).await?; + let num_total_snses = snses.len(); + let snses_with_metadata = stream::iter(snses) + .map(|sns| async move { + let metadata = sns.governance.metadata(agent).await?; + Ok((sns, metadata)) + }) + .buffer_unordered(10) // Do up to 10 requests at a time in parallel + .collect::>>() + .await; + let snses_with_metadata = snses_with_metadata + .into_iter() + .filter_map(Result::ok) + .collect::>(); + + let num_snses_with_metadata = snses_with_metadata.len(); + + let lint_info: Vec = stream::iter(snses_with_metadata) + .map(|(sns, metadata)| async move { + let summary = sns.root.sns_canisters_summary(agent).await?; + let name = metadata.name.ok_or(anyhow!("SNS has no name"))?; + + let governance_summary = summary.governance.ok_or(anyhow!( + "SNS {name} canister summary is missing `governance`" + ))?; + let governance_status = governance_summary + .status + .ok_or(anyhow!("SNS {name} `governance` has no status"))?; + + let root_summary = summary + .root + .ok_or(anyhow!("SNS {name} canister summary is missing `root`"))?; + let root_status = root_summary + .status + .ok_or(anyhow!("SNS {name} `root` has no status"))?; + + let swap_summary = summary + .swap + .ok_or(anyhow!("SNS {name} canister summary is missing `swap`"))?; + let swap_status = swap_summary + .status + .ok_or(anyhow!("SNS {name} `swap` has no status"))?; + + let high_memory_consumption = { + let governance_memory_consumption = + { u64::try_from(governance_status.memory_size.0).unwrap() }; + + let root_memory_consumption = { u64::try_from(root_status.memory_size.0).unwrap() }; + + let swap_memory_consumption = { u64::try_from(swap_status.memory_size.0).unwrap() }; + + [ + ("governance", governance_memory_consumption), + ("root", root_memory_consumption), + ("swap", swap_memory_consumption), + ] + .iter() + .filter(|(_, memory_consumption)| { + (*memory_consumption as f64) > 2.5 * 1024.0 * 1024.0 * 1024.0 + }) + .map(|(canister, memory_consumption)| (canister.to_string(), *memory_consumption)) + .collect::>() + }; + + let low_cycles = { + let governance_cycles = { u64::try_from(governance_status.cycles.0).unwrap() }; + + let root_cycles = { u64::try_from(root_status.cycles.0).unwrap() }; + + let swap_cycles = { u64::try_from(swap_status.cycles.0).unwrap() }; + + [ + ("governance", governance_cycles), + ("root", root_cycles), + ("swap", swap_cycles), + ] + .iter() + .filter(|(_, cycles)| (*cycles as f64) < 10.0 * 1000.0 * 1000.0 * 1000.0 * 1000.0) + .map(|(canister, cycles)| (canister.to_string(), *cycles)) + .collect::>() + }; + + let num_remaining_upgrade_steps = + sns.remaining_upgrade_steps(agent).await?.steps.len() - 1; + + Result::::Ok(SnsLintInfo { + name, + high_memory_consumption, + low_cycles, + num_remaining_upgrade_steps, + }) + }) + .buffer_unordered(10) + .collect::>>() + .await + .into_iter() + .inspect(|result| match result { + Err(e) => println!("Error: {}", e), + _ => {} + }) + .filter_map(Result::ok) + .sorted_by(|a, b| a.name.cmp(&b.name)) + .collect::>(); + + let lint_info_table = crate::table::as_table(lint_info.as_ref()); + println!("{}", lint_info_table); + eprintln!( + "Out of {num_total_snses} SNSes, {num_snses_with_metadata} had metadata and I linted {num_linted} of them.", + num_linted = lint_info.len() + ); + + Ok(()) +} diff --git a/rs/sns/cli/src/list.rs b/rs/sns/cli/src/list.rs index 3548a6c91867..477749001580 100644 --- a/rs/sns/cli/src/list.rs +++ b/rs/sns/cli/src/list.rs @@ -38,6 +38,8 @@ impl TableRow for SnsWithMetadata { } pub async fn exec(args: ListArgs, agent: &Agent) -> Result<()> { + eprintln!("Listing SNSes..."); + let snses = sns_wasm::list_deployed_snses(agent).await?; let snses_with_metadata = stream::iter(snses) .map(|sns| async move { diff --git a/rs/sns/cli/src/main.rs b/rs/sns/cli/src/main.rs index c57760132e9f..723ce84593cd 100644 --- a/rs/sns/cli/src/main.rs +++ b/rs/sns/cli/src/main.rs @@ -4,7 +4,7 @@ use anyhow::{bail, Result}; use clap::Parser; use ic_sns_cli::{ - add_sns_wasm_for_tests, deploy_testflight, init_config_file, list, + add_sns_wasm_for_tests, deploy_testflight, init_config_file, lint, list, neuron_id_to_candid_subaccount, prepare_canisters, propose, CliArgs, SubCommand, }; @@ -27,5 +27,6 @@ async fn main() -> Result<()> { SubCommand::Propose(args) => propose::exec(args), SubCommand::NeuronIdToCandidSubaccount(args) => neuron_id_to_candid_subaccount::exec(args), SubCommand::List(args) => list::exec(args, &agent).await, + SubCommand::Lint(args) => lint::exec(args, &agent).await, } } diff --git a/rs/sns/governance/src/request_impls.rs b/rs/sns/governance/src/request_impls.rs index 0fd4e810f853..db59152ce16e 100644 --- a/rs/sns/governance/src/request_impls.rs +++ b/rs/sns/governance/src/request_impls.rs @@ -4,9 +4,9 @@ use crate::pb::v1::{ ClaimSwapNeuronsRequest, ClaimSwapNeuronsResponse, FailStuckUpgradeInProgressRequest, FailStuckUpgradeInProgressResponse, GetMaturityModulationRequest, GetMaturityModulationResponse, GetMetadataRequest, GetMetadataResponse, GetMode, - GetModeResponse, GetNeuronResponse, GetProposalResponse, GetSnsInitializationParametersRequest, - GetSnsInitializationParametersResponse, ListNeuronsResponse, ListProposalsResponse, - ManageNeuronResponse, + GetModeResponse, GetNeuronResponse, GetProposalResponse, GetRunningSnsVersionResponse, + GetSnsInitializationParametersRequest, GetSnsInitializationParametersResponse, + ListNeuronsResponse, ListProposalsResponse, ManageNeuronResponse, }; impl Request for ClaimSwapNeuronsRequest { @@ -74,3 +74,9 @@ impl Request for crate::pb::v1::ManageNeuron { const METHOD: &'static str = "manage_neuron"; const UPDATE: bool = true; } + +impl Request for crate::pb::v1::GetRunningSnsVersionRequest { + type Response = GetRunningSnsVersionResponse; + const METHOD: &'static str = "get_running_sns_version"; + const UPDATE: bool = true; +} diff --git a/rs/sns/root/src/lib.rs b/rs/sns/root/src/lib.rs index 080752dd84e2..bde19ba3d422 100644 --- a/rs/sns/root/src/lib.rs +++ b/rs/sns/root/src/lib.rs @@ -33,6 +33,7 @@ pub use icrc_ledger_types::icrc3::archive::ArchiveInfo; pub mod logs; pub mod pb; pub mod types; +mod request_impls; // The number of dapp canisters that can be registered with the SNS Root const DAPP_CANISTER_REGISTRATION_LIMIT: usize = 100; diff --git a/rs/sns/root/src/request_impls.rs b/rs/sns/root/src/request_impls.rs new file mode 100644 index 000000000000..37533bc06056 --- /dev/null +++ b/rs/sns/root/src/request_impls.rs @@ -0,0 +1,8 @@ +use ic_nervous_system_clients::Request; +use crate::{GetSnsCanistersSummaryRequest, GetSnsCanistersSummaryResponse}; + +impl Request for GetSnsCanistersSummaryRequest { + type Response = GetSnsCanistersSummaryResponse; + const METHOD: &'static str = "get_sns_canisters_summary"; + const UPDATE: bool = true; +}