From f6ef2ecce6c9f4f71e146bfc705010dfb2d30cd0 Mon Sep 17 00:00:00 2001 From: Daniel Wong Date: Tue, 6 Jan 2026 11:26:07 +0100 Subject: [PATCH 1/6] TakeCanisterSnapshot proposal type. Execution is not implemented. All this commit does is introduces the (canister) API and storage types. Disabled via flag; hence, this can be merged into master. --- rs/nns/governance/api/src/types.rs | 8 ++++++++ .../proto/ic_nns_governance/pb/v1/governance.proto | 3 +++ rs/nns/governance/src/canister_state.rs | 3 ++- rs/nns/governance/src/gen/ic_nns_governance.pb.v1.rs | 4 ++++ rs/nns/governance/src/governance.rs | 10 +++++++++- rs/nns/governance/src/pb/conversions/mod.rs | 2 ++ .../governance/src/proposals/execute_nns_function.rs | 11 +++++++++++ 7 files changed, 39 insertions(+), 2 deletions(-) diff --git a/rs/nns/governance/api/src/types.rs b/rs/nns/governance/api/src/types.rs index b15f9b960aef..68ce89a42bc8 100644 --- a/rs/nns/governance/api/src/types.rs +++ b/rs/nns/governance/api/src/types.rs @@ -4200,6 +4200,12 @@ pub enum NnsFunction { /// UpdateConfigOfSubnet can be used instead. But otherwise, this is the /// state of the art (as of Oct 2025) way of doing subnet recovery. SetSubnetOperationalLevel = 55, + /// Does what the name says: takes a canister snapshot. + /// + /// The canister being snapshotted (the "target" canister) must be + /// controlled by the NNS Root canister. This restriction could be relaxed + /// later. See nns/.../root.did for the payload type. + TakeCanisterSnapshot = 56, } impl NnsFunction { /// String value of the enum field names used in the ProtoBuf definition. @@ -4283,6 +4289,7 @@ impl NnsFunction { NnsFunction::PauseCanisterMigrations => "NNS_FUNCTION_PAUSE_CANISTER_MIGRATIONS", NnsFunction::UnpauseCanisterMigrations => "NNS_FUNCTION_UNPAUSE_CANISTER_MIGRATIONS", NnsFunction::SetSubnetOperationalLevel => "NNS_FUNCTION_SET_SUBNET_OPERATIONAL_LEVEL", + NnsFunction::TakeCanisterSnapshot => "NNS_FUNCTION_TAKE_CANISTER_SNAPSHOT", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -4363,6 +4370,7 @@ impl NnsFunction { "NNS_FUNCTION_PAUSE_CANISTER_MIGRATIONS" => Some(Self::PauseCanisterMigrations), "NNS_FUNCTION_UNPAUSE_CANISTER_MIGRATIONS" => Some(Self::UnpauseCanisterMigrations), "NNS_FUNCTION_SET_SUBNET_OPERATIONAL_LEVEL" => Some(Self::SetSubnetOperationalLevel), + "NNS_FUNCTION_TAKE_CANISTER_SNAPSHOT" => Some(Self::TakeCanisterSnapshot), _ => None, } } 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 5ef8a6938c36..aa48018e3b61 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 @@ -495,6 +495,9 @@ enum NnsFunction { // Take subnet offline or bring back online. Used as part of subnet recovery. NNS_FUNCTION_SET_SUBNET_OPERATIONAL_LEVEL = 55; + + // Take a canister snapshot. + NNS_FUNCTION_TAKE_CANISTER_SNAPSHOT = 56; } // Payload of a proposal that calls a function on another NNS diff --git a/rs/nns/governance/src/canister_state.rs b/rs/nns/governance/src/canister_state.rs index 1a7e3877cf6b..71c7727d3ed9 100644 --- a/rs/nns/governance/src/canister_state.rs +++ b/rs/nns/governance/src/canister_state.rs @@ -394,7 +394,8 @@ fn get_effective_payload( | ValidNnsFunction::DeployHostosToSomeNodes | ValidNnsFunction::PauseCanisterMigrations | ValidNnsFunction::UnpauseCanisterMigrations - | ValidNnsFunction::SetSubnetOperationalLevel => Ok(payload.clone()), + | ValidNnsFunction::SetSubnetOperationalLevel + | ValidNnsFunction::TakeCanisterSnapshot => Ok(payload.clone()), } } 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 b7795d5807a1..600a8e8dbd8a 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 @@ -4820,6 +4820,8 @@ pub enum NnsFunction { UnpauseCanisterMigrations = 54, /// Take subnet offline or bring back online. Used as part of subnet recovery. SetSubnetOperationalLevel = 55, + /// Take a canister snapshot. + TakeCanisterSnapshot = 56, } impl NnsFunction { /// String value of the enum field names used in the ProtoBuf definition. @@ -4893,6 +4895,7 @@ impl NnsFunction { Self::PauseCanisterMigrations => "NNS_FUNCTION_PAUSE_CANISTER_MIGRATIONS", Self::UnpauseCanisterMigrations => "NNS_FUNCTION_UNPAUSE_CANISTER_MIGRATIONS", Self::SetSubnetOperationalLevel => "NNS_FUNCTION_SET_SUBNET_OPERATIONAL_LEVEL", + Self::TakeCanisterSnapshot => "NNS_FUNCTION_TAKE_CANISTER_SNAPSHOT", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -4973,6 +4976,7 @@ impl NnsFunction { "NNS_FUNCTION_PAUSE_CANISTER_MIGRATIONS" => Some(Self::PauseCanisterMigrations), "NNS_FUNCTION_UNPAUSE_CANISTER_MIGRATIONS" => Some(Self::UnpauseCanisterMigrations), "NNS_FUNCTION_SET_SUBNET_OPERATIONAL_LEVEL" => Some(Self::SetSubnetOperationalLevel), + "NNS_FUNCTION_TAKE_CANISTER_SNAPSHOT" => Some(Self::TakeCanisterSnapshot), _ => None, } } diff --git a/rs/nns/governance/src/governance.rs b/rs/nns/governance/src/governance.rs index 97f1f1d2db20..9f1fc968a336 100644 --- a/rs/nns/governance/src/governance.rs +++ b/rs/nns/governance/src/governance.rs @@ -1,6 +1,7 @@ #![allow(deprecated)] use crate::{ - are_nf_fund_proposals_disabled, are_performance_based_rewards_enabled, decoder_config, + are_canister_snapshot_proposals_enabled, are_nf_fund_proposals_disabled, + are_performance_based_rewards_enabled, decoder_config, governance::{ merge_neurons::{ build_merge_neurons_response, calculate_merge_neurons_effect, @@ -4807,6 +4808,13 @@ impl Governance { Self::validate_add_or_remove_data_centers_payload(&update.payload) .map_err(invalid_proposal_error)?; } + ValidNnsFunction::TakeCanisterSnapshot => { + if !are_canister_snapshot_proposals_enabled() { + return Err(invalid_proposal_error( + "TakeCanisterSnapshot proposals are not yet enabled.".to_string(), + )); + } + } _ => {} }; diff --git a/rs/nns/governance/src/pb/conversions/mod.rs b/rs/nns/governance/src/pb/conversions/mod.rs index 61c21285f491..86b2d4a31461 100644 --- a/rs/nns/governance/src/pb/conversions/mod.rs +++ b/rs/nns/governance/src/pb/conversions/mod.rs @@ -3987,6 +3987,7 @@ impl From for pb_api::NnsFunction { pb::NnsFunction::SetSubnetOperationalLevel => { pb_api::NnsFunction::SetSubnetOperationalLevel } + pb::NnsFunction::TakeCanisterSnapshot => pb_api::NnsFunction::TakeCanisterSnapshot, } } } @@ -4096,6 +4097,7 @@ impl From for pb::NnsFunction { pb_api::NnsFunction::SetSubnetOperationalLevel => { pb::NnsFunction::SetSubnetOperationalLevel } + pb_api::NnsFunction::TakeCanisterSnapshot => pb::NnsFunction::TakeCanisterSnapshot, } } } diff --git a/rs/nns/governance/src/proposals/execute_nns_function.rs b/rs/nns/governance/src/proposals/execute_nns_function.rs index 79f74f654e1f..0d8a47cac333 100644 --- a/rs/nns/governance/src/proposals/execute_nns_function.rs +++ b/rs/nns/governance/src/proposals/execute_nns_function.rs @@ -153,6 +153,7 @@ pub enum ValidNnsFunction { PauseCanisterMigrations, UnpauseCanisterMigrations, SetSubnetOperationalLevel, + TakeCanisterSnapshot, } impl ValidNnsFunction { @@ -281,6 +282,7 @@ impl ValidNnsFunction { ValidNnsFunction::SetSubnetOperationalLevel => { (REGISTRY_CANISTER_ID, "set_subnet_operational_level") } + ValidNnsFunction::TakeCanisterSnapshot => (ROOT_CANISTER_ID, "take_canister_snapshot"), } } @@ -341,6 +343,8 @@ impl ValidNnsFunction { ValidNnsFunction::AddSnsWasm | ValidNnsFunction::InsertSnsWasmUpgradePathEntries => { Topic::ServiceNervousSystemManagement } + + ValidNnsFunction::TakeCanisterSnapshot => Topic::ProtocolCanisterManagement, } } @@ -397,6 +401,7 @@ impl ValidNnsFunction { ValidNnsFunction::PauseCanisterMigrations => "Pause Canister Migrations", ValidNnsFunction::UnpauseCanisterMigrations => "Unpause Canister Migrations", ValidNnsFunction::SetSubnetOperationalLevel => "Set Subnet Operational Level", + ValidNnsFunction::TakeCanisterSnapshot => "Take Canister Snapshot", } } @@ -623,6 +628,11 @@ impl ValidNnsFunction { used to take a subnet offline or bring it back online as part of \ subnet recovery." } + ValidNnsFunction::TakeCanisterSnapshot => { + "A proposal to take a snapshot of a canister controlled the NNS. \ + For an introduction to canister snapshots in general, see \ + https://docs.internetcomputer.org/building-apps/canister-management/snapshots ." + } } } } @@ -711,6 +721,7 @@ impl TryFrom for ValidNnsFunction { NnsFunction::SetSubnetOperationalLevel => { Ok(ValidNnsFunction::SetSubnetOperationalLevel) } + NnsFunction::TakeCanisterSnapshot => Ok(ValidNnsFunction::TakeCanisterSnapshot), // Obsolete functions - based on check_obsolete NnsFunction::BlessReplicaVersion | NnsFunction::RetireReplicaVersion => { From ad368ee0a95d08ee310041e06cd8de0306d65fc3 Mon Sep 17 00:00:00 2001 From: Daniel Wong Date: Tue, 6 Jan 2026 13:17:37 +0100 Subject: [PATCH 2/6] Validation for TakeCanisterSnapshot. This just consists of decoding it into a TakeCanisterSnapshotRequest. --- rs/nns/governance/src/governance.rs | 32 ++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/rs/nns/governance/src/governance.rs b/rs/nns/governance/src/governance.rs index 9f1fc968a336..eaab31db9c63 100644 --- a/rs/nns/governance/src/governance.rs +++ b/rs/nns/governance/src/governance.rs @@ -92,6 +92,7 @@ use ic_cdk::println; use ic_cdk::spawn; use ic_nervous_system_canisters::cmc::CMC; use ic_nervous_system_canisters::ledger::IcpLedger; +use ic_nns_handler_root_interface::TakeCanisterSnapshotRequest; use ic_nervous_system_common::{ NervousSystemError, ONE_DAY_SECONDS, ONE_MONTH_SECONDS, ONE_YEAR_SECONDS, ledger, }; @@ -4809,11 +4810,8 @@ impl Governance { .map_err(invalid_proposal_error)?; } ValidNnsFunction::TakeCanisterSnapshot => { - if !are_canister_snapshot_proposals_enabled() { - return Err(invalid_proposal_error( - "TakeCanisterSnapshot proposals are not yet enabled.".to_string(), - )); - } + Self::validate_take_canister_snapshot_payload(&update.payload) + .map_err(invalid_proposal_error)?; } _ => {} }; @@ -4887,6 +4885,30 @@ impl Governance { .map_err(|e| format!("The given AddOrRemoveDataCentersProposalPayload is invalid: {e}")) } + fn validate_take_canister_snapshot_payload(payload: &[u8]) -> Result<(), String> { + if !are_canister_snapshot_proposals_enabled() { + return Err("TakeCanisterSnapshot proposals are not yet enabled.".to_string()); + } + + let _request = Decode!([decoder_config()]; payload, TakeCanisterSnapshotRequest) + .map_err(|err| format!("Invalid TakeCanisterSnapshotRequest: {err}"))?; + + // Ideally, we would verify that the Root canister (or maybe the + // Governance canister?) is a controller of _request.canister_id, but + // that would require async (or we have to hard-code a list of known + // controllees); whereas, currently, proposal validation is sync, and + // changing it to async is fraught with peril... + // + // Ditto for _request.replace_snapshot. It can be None, but if it is + // Some, it must be some snapshot belonging to _request.canister_id. + // + // Not performaing these checks is not catastrophic; it just means that + // when the proposal is executed, no snapshot will be generated. In that + // case, all they need to do is make a another proposal. + + Ok(()) + } + fn validate_create_service_nervous_system( &self, create_service_nervous_system: &CreateServiceNervousSystem, From 87da120c068b70f839fc4fde532ea97f4e895979 Mon Sep 17 00:00:00 2001 From: IDX GitHub Automation Date: Tue, 6 Jan 2026 12:22:49 +0000 Subject: [PATCH 3/6] Automatically fixing code for linting and formatting issues --- rs/nns/governance/src/governance.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rs/nns/governance/src/governance.rs b/rs/nns/governance/src/governance.rs index eaab31db9c63..70c0c836e6d7 100644 --- a/rs/nns/governance/src/governance.rs +++ b/rs/nns/governance/src/governance.rs @@ -92,7 +92,6 @@ use ic_cdk::println; use ic_cdk::spawn; use ic_nervous_system_canisters::cmc::CMC; use ic_nervous_system_canisters::ledger::IcpLedger; -use ic_nns_handler_root_interface::TakeCanisterSnapshotRequest; use ic_nervous_system_common::{ NervousSystemError, ONE_DAY_SECONDS, ONE_MONTH_SECONDS, ONE_YEAR_SECONDS, ledger, }; @@ -117,6 +116,7 @@ use ic_nns_governance_api::{ }, subnet_rental::SubnetRentalRequest, }; +use ic_nns_handler_root_interface::TakeCanisterSnapshotRequest; use ic_node_rewards_canister_api::monthly_rewards::{ GetNodeProvidersMonthlyXdrRewardsRequest, GetNodeProvidersMonthlyXdrRewardsResponse, }; From 29bd7e62ab6786fb4faad215910aa84db9aa5c48 Mon Sep 17 00:00:00 2001 From: Daniel Wong Date: Tue, 6 Jan 2026 13:27:24 +0100 Subject: [PATCH 4/6] changelog --- rs/nns/governance/unreleased_changelog.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rs/nns/governance/unreleased_changelog.md b/rs/nns/governance/unreleased_changelog.md index 94126a0ff421..bee492f2761d 100644 --- a/rs/nns/governance/unreleased_changelog.md +++ b/rs/nns/governance/unreleased_changelog.md @@ -9,6 +9,10 @@ on the process that this file is part of, see ## Added +* New TakeCanisterSnapshot proposal type. Disabled for now, but will be enabled + in the not too distant future. For now, the declarations are in place (in + governance.did), which helps clients to get ready for this new proposal type. + ## Changed ## Deprecated From 6177d991209f784b9b34ecfba72ccffc7e4a3ef0 Mon Sep 17 00:00:00 2001 From: Daniel Wong Date: Thu, 8 Jan 2026 10:56:21 +0100 Subject: [PATCH 5/6] For consistency, remove validation of TakeCanisterSnapshot proposals. --- rs/nns/governance/src/governance.rs | 31 +++++------------------------ 1 file changed, 5 insertions(+), 26 deletions(-) diff --git a/rs/nns/governance/src/governance.rs b/rs/nns/governance/src/governance.rs index 70c0c836e6d7..23611a667f8f 100644 --- a/rs/nns/governance/src/governance.rs +++ b/rs/nns/governance/src/governance.rs @@ -4810,8 +4810,11 @@ impl Governance { .map_err(invalid_proposal_error)?; } ValidNnsFunction::TakeCanisterSnapshot => { - Self::validate_take_canister_snapshot_payload(&update.payload) - .map_err(invalid_proposal_error)?; + if !are_canister_snapshot_proposals_enabled() { + return Err(invalid_proposal_error( + "TakeCanisterSnapshot proposals are not yet enabled.".to_string(), + )); + } } _ => {} }; @@ -4885,30 +4888,6 @@ impl Governance { .map_err(|e| format!("The given AddOrRemoveDataCentersProposalPayload is invalid: {e}")) } - fn validate_take_canister_snapshot_payload(payload: &[u8]) -> Result<(), String> { - if !are_canister_snapshot_proposals_enabled() { - return Err("TakeCanisterSnapshot proposals are not yet enabled.".to_string()); - } - - let _request = Decode!([decoder_config()]; payload, TakeCanisterSnapshotRequest) - .map_err(|err| format!("Invalid TakeCanisterSnapshotRequest: {err}"))?; - - // Ideally, we would verify that the Root canister (or maybe the - // Governance canister?) is a controller of _request.canister_id, but - // that would require async (or we have to hard-code a list of known - // controllees); whereas, currently, proposal validation is sync, and - // changing it to async is fraught with peril... - // - // Ditto for _request.replace_snapshot. It can be None, but if it is - // Some, it must be some snapshot belonging to _request.canister_id. - // - // Not performaing these checks is not catastrophic; it just means that - // when the proposal is executed, no snapshot will be generated. In that - // case, all they need to do is make a another proposal. - - Ok(()) - } - fn validate_create_service_nervous_system( &self, create_service_nervous_system: &CreateServiceNervousSystem, From 029cd14e60c21787fc1f42c4832b51d9e717d842 Mon Sep 17 00:00:00 2001 From: Daniel Wong Date: Thu, 8 Jan 2026 11:23:18 +0100 Subject: [PATCH 6/6] lint --- rs/nns/governance/src/governance.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/rs/nns/governance/src/governance.rs b/rs/nns/governance/src/governance.rs index 23611a667f8f..9f1fc968a336 100644 --- a/rs/nns/governance/src/governance.rs +++ b/rs/nns/governance/src/governance.rs @@ -116,7 +116,6 @@ use ic_nns_governance_api::{ }, subnet_rental::SubnetRentalRequest, }; -use ic_nns_handler_root_interface::TakeCanisterSnapshotRequest; use ic_node_rewards_canister_api::monthly_rewards::{ GetNodeProvidersMonthlyXdrRewardsRequest, GetNodeProvidersMonthlyXdrRewardsResponse, };