diff --git a/rs/nns/governance/api/src/ic_nns_governance.pb.v1.rs b/rs/nns/governance/api/src/ic_nns_governance.pb.v1.rs index 59938bfb04a..44815c070d9 100644 --- a/rs/nns/governance/api/src/ic_nns_governance.pb.v1.rs +++ b/rs/nns/governance/api/src/ic_nns_governance.pb.v1.rs @@ -560,7 +560,7 @@ pub struct Proposal { /// take. #[prost( oneof = "proposal::Action", - tags = "10, 12, 13, 14, 15, 16, 17, 18, 19, 21, 22, 23, 24, 25, 26" + tags = "10, 12, 13, 14, 15, 16, 17, 18, 19, 21, 22, 23, 24, 25, 26, 27" )] pub action: ::core::option::Option, } @@ -651,6 +651,9 @@ pub mod proposal { /// Stops or starts a canister controlled by Root. #[prost(message, tag = "26")] StopOrStartCanister(super::StopOrStartCanister), + /// Updates canister settings for those controlled by NNS Root. + #[prost(message, tag = "27")] + UpdateCanisterSettings(super::UpdateCanisterSettings), } } /// Empty message to use in oneof fields that represent empty @@ -2432,6 +2435,95 @@ pub mod stop_or_start_canister { } } } +#[derive(candid::CandidType, candid::Deserialize, serde::Serialize, comparable::Comparable)] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct UpdateCanisterSettings { + /// The target canister ID to call update_settings on. Required. + #[prost(message, optional, tag = "1")] + pub canister_id: ::core::option::Option<::ic_base_types::PrincipalId>, + /// The settings to update. Required. + #[prost(message, optional, tag = "2")] + pub settings: ::core::option::Option, +} +/// Nested message and enum types in `UpdateCanisterSettings`. +pub mod update_canister_settings { + /// The controllers of the canister. We use a message to wrap the repeated field because prost does + /// not generate `Option>` for repeated fields. + #[derive(candid::CandidType, candid::Deserialize, serde::Serialize, comparable::Comparable)] + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct Controllers { + /// The controllers of the canister. + #[prost(message, repeated, tag = "1")] + pub controllers: ::prost::alloc::vec::Vec<::ic_base_types::PrincipalId>, + } + /// The CanisterSettings struct as defined in the ic-interface-spec + /// + #[derive(candid::CandidType, candid::Deserialize, serde::Serialize, comparable::Comparable)] + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct CanisterSettings { + #[prost(message, optional, tag = "1")] + pub controllers: ::core::option::Option, + #[prost(uint64, optional, tag = "2")] + pub compute_allocation: ::core::option::Option, + #[prost(uint64, optional, tag = "3")] + pub memory_allocation: ::core::option::Option, + #[prost(uint64, optional, tag = "4")] + pub freezing_threshold: ::core::option::Option, + #[prost(enumeration = "LogVisibility", optional, tag = "5")] + pub log_visibility: ::core::option::Option, + #[prost(uint64, optional, tag = "6")] + pub wasm_memory_limit: ::core::option::Option, + } + /// Log visibility of a canister. + #[derive( + candid::CandidType, + candid::Deserialize, + serde::Serialize, + comparable::Comparable, + Clone, + Copy, + Debug, + PartialEq, + Eq, + Hash, + PartialOrd, + Ord, + ::prost::Enumeration, + )] + #[repr(i32)] + pub enum LogVisibility { + Unspecified = 0, + /// The log is visible to the controllers of the dapp canister. + Controllers = 1, + /// The log is visible to the public. + Public = 2, + } + impl LogVisibility { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + LogVisibility::Unspecified => "LOG_VISIBILITY_UNSPECIFIED", + LogVisibility::Controllers => "LOG_VISIBILITY_CONTROLLERS", + LogVisibility::Public => "LOG_VISIBILITY_PUBLIC", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "LOG_VISIBILITY_UNSPECIFIED" => Some(Self::Unspecified), + "LOG_VISIBILITY_CONTROLLERS" => Some(Self::Controllers), + "LOG_VISIBILITY_PUBLIC" => Some(Self::Public), + _ => None, + } + } + } +} /// This represents the whole NNS governance system. It contains all /// information about the NNS governance system that must be kept /// across upgrades of the NNS governance system. diff --git a/rs/nns/governance/canister/governance.did b/rs/nns/governance/canister/governance.did index e2f89459677..87c72560cb8 100644 --- a/rs/nns/governance/canister/governance.did +++ b/rs/nns/governance/canister/governance.did @@ -2,6 +2,7 @@ type AccountIdentifier = record { hash : blob }; type Action = variant { RegisterKnownNeuron : KnownNeuron; ManageNeuron : ManageNeuron; + UpdateCanisterSettings : UpdateCanisterSettings; InstallCode : InstallCode; StopOrStartCanister : StopOrStartCanister; CreateServiceNervousSystem : CreateServiceNervousSystem; @@ -28,6 +29,14 @@ type By = variant { Memo : nat64; }; type Canister = record { id : opt principal }; +type CanisterSettings = record { + freezing_threshold : opt nat64; + controllers : opt Controllers; + log_visibility : opt int32; + wasm_memory_limit : opt nat64; + memory_allocation : opt nat64; + compute_allocation : opt nat64; +}; type CanisterStatusResultV2 = record { status : opt int32; freezing_threshold : opt nat64; @@ -114,6 +123,7 @@ type Committed_1 = record { sns_governance_canister_id : opt principal; }; type Configure = record { operation : opt Operation }; +type Controllers = record { controllers : vec principal }; type Countries = record { iso_codes : vec text }; type CreateServiceNervousSystem = record { url : opt text; @@ -689,6 +699,10 @@ type TimeWindow = record { end_timestamp_seconds : nat64; }; type Tokens = record { e8s : opt nat64 }; +type UpdateCanisterSettings = record { + canister_id : opt principal; + settings : opt CanisterSettings; +}; type UpdateNodeProvider = record { reward_account : opt AccountIdentifier }; type VotingRewardParameters = record { reward_rate_transition_duration : opt Duration; diff --git a/rs/nns/governance/canister/governance_test.did b/rs/nns/governance/canister/governance_test.did index a3017162f27..6086e212f1f 100644 --- a/rs/nns/governance/canister/governance_test.did +++ b/rs/nns/governance/canister/governance_test.did @@ -2,6 +2,7 @@ type AccountIdentifier = record { hash : blob }; type Action = variant { RegisterKnownNeuron : KnownNeuron; ManageNeuron : ManageNeuron; + UpdateCanisterSettings : UpdateCanisterSettings; InstallCode : InstallCode; StopOrStartCanister : StopOrStartCanister; CreateServiceNervousSystem : CreateServiceNervousSystem; @@ -28,6 +29,14 @@ type By = variant { Memo : nat64; }; type Canister = record { id : opt principal }; +type CanisterSettings = record { + freezing_threshold : opt nat64; + controllers : opt Controllers; + log_visibility : opt int32; + wasm_memory_limit : opt nat64; + memory_allocation : opt nat64; + compute_allocation : opt nat64; +}; type CanisterStatusResultV2 = record { status : opt int32; freezing_threshold : opt nat64; @@ -114,6 +123,7 @@ type Committed_1 = record { sns_governance_canister_id : opt principal; }; type Configure = record { operation : opt Operation }; +type Controllers = record { controllers : vec principal }; type Countries = record { iso_codes : vec text }; type CreateServiceNervousSystem = record { url : opt text; @@ -689,6 +699,10 @@ type TimeWindow = record { end_timestamp_seconds : nat64; }; type Tokens = record { e8s : opt nat64 }; +type UpdateCanisterSettings = record { + canister_id : opt principal; + settings : opt CanisterSettings; +}; type UpdateNodeProvider = record { reward_account : opt AccountIdentifier }; type VotingRewardParameters = record { reward_rate_transition_duration : opt Duration; diff --git a/rs/nns/governance/proto/ic_nns_governance/pb/v1/governance.proto b/rs/nns/governance/proto/ic_nns_governance/pb/v1/governance.proto index 19abf480186..e4d866bbcc6 100644 --- a/rs/nns/governance/proto/ic_nns_governance/pb/v1/governance.proto +++ b/rs/nns/governance/proto/ic_nns_governance/pb/v1/governance.proto @@ -877,6 +877,8 @@ message Proposal { InstallCode install_code = 25; // Stops or starts a canister controlled by Root. StopOrStartCanister stop_or_start_canister = 26; + // Updates canister settings for those controlled by NNS Root. + UpdateCanisterSettings update_canister_settings = 27; } } @@ -2086,6 +2088,41 @@ message StopOrStartCanister { optional CanisterAction action = 2; } +message UpdateCanisterSettings { + // The target canister ID to call update_settings on. Required. + optional ic_base_types.pb.v1.PrincipalId canister_id = 1; + + // Log visibility of a canister. + enum LogVisibility { + LOG_VISIBILITY_UNSPECIFIED = 0; + // The log is visible to the controllers of the dapp canister. + LOG_VISIBILITY_CONTROLLERS = 1; + // The log is visible to the public. + LOG_VISIBILITY_PUBLIC = 2; + } + + // The controllers of the canister. We use a message to wrap the repeated field because prost does + // not generate `Option>` for repeated fields. + message Controllers { + // The controllers of the canister. + repeated ic_base_types.pb.v1.PrincipalId controllers = 1; + } + + // The CanisterSettings struct as defined in the ic-interface-spec + // https://internetcomputer.org/docs/current/references/ic-interface-spec/#ic-candid. + message CanisterSettings { + optional Controllers controllers = 1; + optional uint64 compute_allocation = 2; + optional uint64 memory_allocation = 3; + optional uint64 freezing_threshold = 4; + optional LogVisibility log_visibility = 5; + optional uint64 wasm_memory_limit = 6; + } + + // The settings to update. Required. + optional CanisterSettings settings = 2; +} + // This represents the whole NNS governance system. It contains all // information about the NNS governance system that must be kept // across upgrades of the NNS governance system. diff --git a/rs/nns/governance/src/gen/ic_nns_governance.pb.v1.rs b/rs/nns/governance/src/gen/ic_nns_governance.pb.v1.rs index 59938bfb04a..44815c070d9 100644 --- a/rs/nns/governance/src/gen/ic_nns_governance.pb.v1.rs +++ b/rs/nns/governance/src/gen/ic_nns_governance.pb.v1.rs @@ -560,7 +560,7 @@ pub struct Proposal { /// take. #[prost( oneof = "proposal::Action", - tags = "10, 12, 13, 14, 15, 16, 17, 18, 19, 21, 22, 23, 24, 25, 26" + tags = "10, 12, 13, 14, 15, 16, 17, 18, 19, 21, 22, 23, 24, 25, 26, 27" )] pub action: ::core::option::Option, } @@ -651,6 +651,9 @@ pub mod proposal { /// Stops or starts a canister controlled by Root. #[prost(message, tag = "26")] StopOrStartCanister(super::StopOrStartCanister), + /// Updates canister settings for those controlled by NNS Root. + #[prost(message, tag = "27")] + UpdateCanisterSettings(super::UpdateCanisterSettings), } } /// Empty message to use in oneof fields that represent empty @@ -2432,6 +2435,95 @@ pub mod stop_or_start_canister { } } } +#[derive(candid::CandidType, candid::Deserialize, serde::Serialize, comparable::Comparable)] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct UpdateCanisterSettings { + /// The target canister ID to call update_settings on. Required. + #[prost(message, optional, tag = "1")] + pub canister_id: ::core::option::Option<::ic_base_types::PrincipalId>, + /// The settings to update. Required. + #[prost(message, optional, tag = "2")] + pub settings: ::core::option::Option, +} +/// Nested message and enum types in `UpdateCanisterSettings`. +pub mod update_canister_settings { + /// The controllers of the canister. We use a message to wrap the repeated field because prost does + /// not generate `Option>` for repeated fields. + #[derive(candid::CandidType, candid::Deserialize, serde::Serialize, comparable::Comparable)] + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct Controllers { + /// The controllers of the canister. + #[prost(message, repeated, tag = "1")] + pub controllers: ::prost::alloc::vec::Vec<::ic_base_types::PrincipalId>, + } + /// The CanisterSettings struct as defined in the ic-interface-spec + /// + #[derive(candid::CandidType, candid::Deserialize, serde::Serialize, comparable::Comparable)] + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct CanisterSettings { + #[prost(message, optional, tag = "1")] + pub controllers: ::core::option::Option, + #[prost(uint64, optional, tag = "2")] + pub compute_allocation: ::core::option::Option, + #[prost(uint64, optional, tag = "3")] + pub memory_allocation: ::core::option::Option, + #[prost(uint64, optional, tag = "4")] + pub freezing_threshold: ::core::option::Option, + #[prost(enumeration = "LogVisibility", optional, tag = "5")] + pub log_visibility: ::core::option::Option, + #[prost(uint64, optional, tag = "6")] + pub wasm_memory_limit: ::core::option::Option, + } + /// Log visibility of a canister. + #[derive( + candid::CandidType, + candid::Deserialize, + serde::Serialize, + comparable::Comparable, + Clone, + Copy, + Debug, + PartialEq, + Eq, + Hash, + PartialOrd, + Ord, + ::prost::Enumeration, + )] + #[repr(i32)] + pub enum LogVisibility { + Unspecified = 0, + /// The log is visible to the controllers of the dapp canister. + Controllers = 1, + /// The log is visible to the public. + Public = 2, + } + impl LogVisibility { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + LogVisibility::Unspecified => "LOG_VISIBILITY_UNSPECIFIED", + LogVisibility::Controllers => "LOG_VISIBILITY_CONTROLLERS", + LogVisibility::Public => "LOG_VISIBILITY_PUBLIC", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "LOG_VISIBILITY_UNSPECIFIED" => Some(Self::Unspecified), + "LOG_VISIBILITY_CONTROLLERS" => Some(Self::Controllers), + "LOG_VISIBILITY_PUBLIC" => Some(Self::Public), + _ => None, + } + } + } +} /// This represents the whole NNS governance system. It contains all /// information about the NNS governance system that must be kept /// across upgrades of the NNS governance system. diff --git a/rs/nns/governance/src/governance.rs b/rs/nns/governance/src/governance.rs index 57a78d7ed79..f2abcc56cba 100644 --- a/rs/nns/governance/src/governance.rs +++ b/rs/nns/governance/src/governance.rs @@ -55,8 +55,8 @@ use crate::{ ProposalData, ProposalInfo, ProposalRewardStatus, ProposalStatus, RestoreAgingSummary, RewardEvent, RewardNodeProvider, RewardNodeProviders, SettleNeuronsFundParticipationRequest, SettleNeuronsFundParticipationResponse, - StopOrStartCanister, Tally, Topic, UpdateNodeProvider, Vote, WaitForQuietState, - XdrConversionRate as XdrConversionRatePb, + StopOrStartCanister, Tally, Topic, UpdateCanisterSettings, UpdateNodeProvider, Vote, + WaitForQuietState, XdrConversionRate as XdrConversionRatePb, }, proposals::{ call_canister::CallCanister, @@ -828,6 +828,14 @@ impl Proposal { // lot of places. stop_or_start.valid_topic().unwrap_or(Topic::Unspecified) } + Action::UpdateCanisterSettings(update_canister_settings) => { + // There should be a valid topic since the validation should be done when the + // proposal is created. We avoid panicking here since `topic()` is called in a + // lot of places. + update_canister_settings + .valid_topic() + .unwrap_or(Topic::Unspecified) + } } } else { println!("{}ERROR: No action -> no topic.", LOG_PREFIX); @@ -900,6 +908,7 @@ impl Action { Action::ExecuteNnsFunction(_) => "ACTION_EXECUTE_NNS_FUNCTION", Action::InstallCode(_) => "ACTION_CHANGE_CANISTER", Action::StopOrStartCanister(_) => "ACTION_STOP_OR_START_CANISTER", + Action::UpdateCanisterSettings(_) => "ACTION_UPDATE_CANISTER_SETTINGS", } } @@ -4425,6 +4434,10 @@ impl Governance { self.perform_stop_or_start_canister(pid, stop_or_start) .await; } + Action::UpdateCanisterSettings(update_settings) => { + self.perform_update_canister_settings(pid, update_settings) + .await; + } } } @@ -4456,6 +4469,17 @@ impl Governance { self.set_proposal_execution_status(proposal_id, result); } + async fn perform_update_canister_settings( + &mut self, + proposal_id: u64, + update_settings: UpdateCanisterSettings, + ) { + let result = self + .perform_call_canister(proposal_id, update_settings) + .await; + self.set_proposal_execution_status(proposal_id, result); + } + async fn perform_call_canister( &mut self, proposal_id: u64, @@ -4823,6 +4847,7 @@ impl Governance { } Action::InstallCode(install_code) => install_code.validate(), Action::StopOrStartCanister(stop_or_start) => stop_or_start.validate(), + Action::UpdateCanisterSettings(update_settings) => update_settings.validate(), }?; Ok(action.clone()) diff --git a/rs/nns/governance/src/pb/conversions.rs b/rs/nns/governance/src/pb/conversions.rs index fa8689442f6..9140d0c0738 100644 --- a/rs/nns/governance/src/pb/conversions.rs +++ b/rs/nns/governance/src/pb/conversions.rs @@ -600,6 +600,9 @@ impl From for pb_api::proposal::Action { pb::proposal::Action::StopOrStartCanister(v) => { pb_api::proposal::Action::StopOrStartCanister(v.into()) } + pb::proposal::Action::UpdateCanisterSettings(v) => { + pb_api::proposal::Action::UpdateCanisterSettings(v.into()) + } } } } @@ -647,6 +650,9 @@ impl From for pb::proposal::Action { pb_api::proposal::Action::StopOrStartCanister(v) => { pb::proposal::Action::StopOrStartCanister(v.into()) } + pb_api::proposal::Action::UpdateCanisterSettings(v) => { + pb::proposal::Action::UpdateCanisterSettings(v.into()) + } } } } @@ -2869,6 +2875,110 @@ impl From } } +impl From for pb_api::UpdateCanisterSettings { + fn from(item: pb::UpdateCanisterSettings) -> Self { + Self { + canister_id: item.canister_id, + settings: item.settings.map(|x| x.into()), + } + } +} + +impl From for pb::UpdateCanisterSettings { + fn from(item: pb_api::UpdateCanisterSettings) -> Self { + Self { + canister_id: item.canister_id, + settings: item.settings.map(|x| x.into()), + } + } +} + +impl From + for pb_api::update_canister_settings::CanisterSettings +{ + fn from(item: pb::update_canister_settings::CanisterSettings) -> Self { + Self { + controllers: item.controllers.map(|x| x.into()), + compute_allocation: item.compute_allocation, + memory_allocation: item.memory_allocation, + freezing_threshold: item.freezing_threshold, + log_visibility: item.log_visibility, + wasm_memory_limit: item.wasm_memory_limit, + } + } +} + +impl From + for pb::update_canister_settings::CanisterSettings +{ + fn from(item: pb_api::update_canister_settings::CanisterSettings) -> Self { + Self { + controllers: item.controllers.map(|x| x.into()), + compute_allocation: item.compute_allocation, + memory_allocation: item.memory_allocation, + freezing_threshold: item.freezing_threshold, + log_visibility: item.log_visibility, + wasm_memory_limit: item.wasm_memory_limit, + } + } +} + +impl From + for pb_api::update_canister_settings::Controllers +{ + fn from(item: pb::update_canister_settings::Controllers) -> Self { + Self { + controllers: item.controllers.into_iter().map(|x| x.into()).collect(), + } + } +} + +impl From + for pb::update_canister_settings::Controllers +{ + fn from(item: pb_api::update_canister_settings::Controllers) -> Self { + Self { + controllers: item.controllers.into_iter().map(|x| x.into()).collect(), + } + } +} + +impl From + for pb_api::update_canister_settings::LogVisibility +{ + fn from(item: pb::update_canister_settings::LogVisibility) -> Self { + match item { + pb::update_canister_settings::LogVisibility::Unspecified => { + pb_api::update_canister_settings::LogVisibility::Unspecified + } + pb::update_canister_settings::LogVisibility::Controllers => { + pb_api::update_canister_settings::LogVisibility::Controllers + } + pb::update_canister_settings::LogVisibility::Public => { + pb_api::update_canister_settings::LogVisibility::Public + } + } + } +} + +impl From + for pb::update_canister_settings::LogVisibility +{ + fn from(item: pb_api::update_canister_settings::LogVisibility) -> Self { + match item { + pb_api::update_canister_settings::LogVisibility::Unspecified => { + pb::update_canister_settings::LogVisibility::Unspecified + } + pb_api::update_canister_settings::LogVisibility::Controllers => { + pb::update_canister_settings::LogVisibility::Controllers + } + pb_api::update_canister_settings::LogVisibility::Public => { + pb::update_canister_settings::LogVisibility::Public + } + } + } +} + impl From for pb_api::Governance { fn from(item: pb::Governance) -> Self { Self { diff --git a/rs/nns/governance/src/proposals/mod.rs b/rs/nns/governance/src/proposals/mod.rs index b51d6018e23..522a78bfd05 100644 --- a/rs/nns/governance/src/proposals/mod.rs +++ b/rs/nns/governance/src/proposals/mod.rs @@ -13,6 +13,7 @@ pub mod create_service_nervous_system; pub mod install_code; pub mod proposal_submission; pub mod stop_or_start_canister; +pub mod update_canister_settings; const PROTOCOL_CANISTER_IDS: [&CanisterId; 16] = [ ®ISTRY_CANISTER_ID, diff --git a/rs/nns/governance/src/proposals/update_canister_settings.rs b/rs/nns/governance/src/proposals/update_canister_settings.rs new file mode 100644 index 00000000000..7fad9887743 --- /dev/null +++ b/rs/nns/governance/src/proposals/update_canister_settings.rs @@ -0,0 +1,182 @@ +use super::{invalid_proposal_error, topic_to_manage_canister}; +use crate::{ + pb::v1::{ + update_canister_settings::CanisterSettings, GovernanceError, Topic, UpdateCanisterSettings, + }, + proposals::call_canister::CallCanister, +}; + +use ic_base_types::CanisterId; +use ic_nns_constants::ROOT_CANISTER_ID; + +impl UpdateCanisterSettings { + pub fn validate(&self) -> Result<(), GovernanceError> { + if !cfg!(feature = "test") { + return Err(invalid_proposal_error( + "UpdateCanisterSettings proposal is not yet supported", + )); + } + + let _ = self.valid_canister_id()?; + let _ = self.valid_topic()?; + let _ = self.canister_and_function()?; + let _ = self.valid_canister_settings()?; + + Ok(()) + } + + fn valid_canister_id(&self) -> Result { + let canister_principal_id = self + .canister_id + .ok_or(invalid_proposal_error("Canister ID is required"))?; + let canister_id = CanisterId::try_from(canister_principal_id) + .map_err(|_| invalid_proposal_error("Invalid canister ID"))?; + Ok(canister_id) + } + + pub fn valid_topic(&self) -> Result { + let canister_id = self.valid_canister_id()?; + topic_to_manage_canister(&canister_id) + } + + pub fn valid_canister_settings(&self) -> Result { + let settings = self + .settings + .as_ref() + .ok_or(invalid_proposal_error("Settings are required"))?; + + if settings.controllers.is_none() + && settings.compute_allocation.is_none() + && settings.memory_allocation.is_none() + && settings.freezing_threshold.is_none() + && settings.log_visibility.is_none() + && settings.wasm_memory_limit.is_none() + { + return Err(invalid_proposal_error( + "At least one setting must be provided", + )); + } + + Ok(settings.clone()) + } +} + +impl CallCanister for UpdateCanisterSettings { + fn canister_and_function(&self) -> Result<(CanisterId, &str), GovernanceError> { + Ok((ROOT_CANISTER_ID, "update_canister_settings")) + } + + fn payload(&self) -> Result, GovernanceError> { + // TODO(NNS1-2522): convert to payload to be sent to Root. + Err(invalid_proposal_error( + "UpdateCanisterSettings not yet supported", + )) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::pb::v1::governance_error::ErrorType; + use ic_nns_constants::LEDGER_CANISTER_ID; + + #[cfg(not(feature = "test"))] + #[test] + fn update_canister_settings_disabled() { + let update_canister_settings = UpdateCanisterSettings { + canister_id: Some(LEDGER_CANISTER_ID.get()), + settings: Some(Default::default()), + }; + + assert_eq!( + update_canister_settings.validate(), + Err(GovernanceError::new_with_message( + ErrorType::InvalidProposal, + "Proposal invalid because of UpdateCanisterSettings proposal is not yet supported" + .to_string(), + )) + ); + } + + #[cfg(feature = "test")] + #[test] + fn test_invalid_update_canister_settings() { + let valid_update_canister_settings = UpdateCanisterSettings { + canister_id: Some(LEDGER_CANISTER_ID.get()), + settings: Some(CanisterSettings { + memory_allocation: Some(1 >> 30), + ..Default::default() + }), + }; + + let is_invalid_proposal_with_keywords = + |update_canister_settings: UpdateCanisterSettings, keywords: Vec<&str>| { + let error = match update_canister_settings.validate() { + Err(error) => error, + Ok(_) => panic!( + "Expected an error for invalid proposal {:?} but it's valid", + update_canister_settings + ), + }; + assert_eq!(error.error_type, ErrorType::InvalidProposal as i32); + for keyword in keywords { + let error_message = error.error_message.to_lowercase(); + assert!( + error_message.contains(keyword), + "{} not found in {:#?}", + keyword, + error_message + ); + } + }; + + is_invalid_proposal_with_keywords( + UpdateCanisterSettings { + canister_id: None, + ..valid_update_canister_settings.clone() + }, + vec!["canister id", "required"], + ); + + is_invalid_proposal_with_keywords( + UpdateCanisterSettings { + settings: None, + ..valid_update_canister_settings.clone() + }, + vec!["settings", "required"], + ); + + is_invalid_proposal_with_keywords( + UpdateCanisterSettings { + settings: Some(Default::default()), + ..valid_update_canister_settings.clone() + }, + vec!["at least one setting", "provided"], + ); + } + + #[cfg(feature = "test")] + #[test] + fn test_update_ledger_canister_settings() { + let update_ledger_canister_settings = UpdateCanisterSettings { + canister_id: Some(LEDGER_CANISTER_ID.get()), + settings: Some(CanisterSettings { + memory_allocation: Some(1 << 30), + ..Default::default() + }), + }; + + assert_eq!(update_ledger_canister_settings.validate(), Ok(())); + assert_eq!( + update_ledger_canister_settings.valid_topic(), + Ok(Topic::ProtocolCanisterManagement) + ); + assert_eq!( + update_ledger_canister_settings.canister_and_function(), + Ok((ROOT_CANISTER_ID, "update_canister_settings")) + ); + + // TODO(NNS1-2522): test payload after it's implemented. + } +} diff --git a/rs/nns/integration_tests/src/update_canister_settings.rs b/rs/nns/integration_tests/src/update_canister_settings.rs new file mode 100644 index 00000000000..cb7de035256 --- /dev/null +++ b/rs/nns/integration_tests/src/update_canister_settings.rs @@ -0,0 +1,49 @@ +use ic_nns_constants::REGISTRY_CANISTER_ID; +use ic_nns_governance::pb::v1::{ + manage_neuron_response::Command, proposal::Action, update_canister_settings::CanisterSettings, + Proposal, UpdateCanisterSettings, +}; +use ic_nns_test_utils::{ + common::NnsInitPayloadsBuilder, + neuron_helpers::get_neuron_1, + state_test_helpers::{ + nns_governance_make_proposal, nns_wait_for_proposal_failure, setup_nns_canisters, + state_machine_builder_for_nns_tests, + }, +}; + +#[test] +fn test_update_canister_settings() { + // Step 1: Set up the NNS canisters and get the neuron. + let state_machine = state_machine_builder_for_nns_tests().build(); + let nns_init_payloads = NnsInitPayloadsBuilder::new().with_test_neurons().build(); + setup_nns_canisters(&state_machine, nns_init_payloads); + let n1 = get_neuron_1(); + + // Step 2: Make a proposal to update settings of the registry canister. + let propose_response = nns_governance_make_proposal( + &state_machine, + n1.principal_id, + n1.neuron_id, + &Proposal { + title: Some("Update canister settings".to_string()), + action: Some(Action::UpdateCanisterSettings(UpdateCanisterSettings { + canister_id: Some(REGISTRY_CANISTER_ID.get()), + settings: Some(CanisterSettings { + memory_allocation: Some(1 << 32), + ..Default::default() + }), + })), + ..Default::default() + }, + ); + let proposal_id = match propose_response.command.unwrap() { + Command::MakeProposal(response) => response.proposal_id.unwrap(), + _ => panic!("Propose didn't return MakeProposal"), + }; + + // Step 3: make sure it fails to execute since it's fully implemented yet. + // TODO(NNS1-2522): test that the proposal is executed successfully after it's fully + // implemented. + nns_wait_for_proposal_failure(&state_machine, proposal_id.id); +}