diff --git a/Cargo.lock b/Cargo.lock index 3c7769964dd0..420a85941bec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12429,6 +12429,7 @@ dependencies = [ "ic-nns-governance-init", "ic-nns-gtc", "ic-nns-handler-root", + "ic-nns-handler-root-interface", "ic-nns-test-utils", "ic-nns-test-utils-golden-nns-state", "ic-nns-test-utils-macros", diff --git a/rs/nervous_system/clients/src/lib.rs b/rs/nervous_system/clients/src/lib.rs index 6434cd2cc570..26f38a4d82b8 100644 --- a/rs/nervous_system/clients/src/lib.rs +++ b/rs/nervous_system/clients/src/lib.rs @@ -3,6 +3,7 @@ pub mod canister_metadata; pub mod canister_status; pub mod delete_canister; pub mod ledger_client; +pub mod load_canister_snapshot; pub mod management_canister_client; pub mod stop_canister; pub mod take_canister_snapshot; diff --git a/rs/nervous_system/clients/src/load_canister_snapshot.rs b/rs/nervous_system/clients/src/load_canister_snapshot.rs new file mode 100644 index 000000000000..e29b27240a68 --- /dev/null +++ b/rs/nervous_system/clients/src/load_canister_snapshot.rs @@ -0,0 +1,9 @@ +use ic_management_canister_types_private::{IC_00, LoadCanisterSnapshotArgs}; +use ic_nervous_system_runtime::Runtime; + +pub async fn load_canister_snapshot(args: LoadCanisterSnapshotArgs) -> Result<(), (i32, String)> +where + Rt: Runtime, +{ + Rt::call_with_cleanup(IC_00, "load_canister_snapshot", (args,)).await +} diff --git a/rs/nervous_system/clients/src/management_canister_client.rs b/rs/nervous_system/clients/src/management_canister_client.rs index 1e1ff6780a93..36f9a59a4567 100644 --- a/rs/nervous_system/clients/src/management_canister_client.rs +++ b/rs/nervous_system/clients/src/management_canister_client.rs @@ -3,6 +3,7 @@ use crate::{ canister_metadata::canister_metadata, canister_status::{CanisterStatusResultFromManagementCanister, canister_status}, delete_canister::delete_canister, + load_canister_snapshot::load_canister_snapshot, stop_canister::stop_canister, take_canister_snapshot::take_canister_snapshot, update_settings::{UpdateSettings, update_settings}, @@ -12,7 +13,7 @@ use candid::Encode; use ic_base_types::PrincipalId; use ic_error_types::RejectCode; use ic_management_canister_types_private::{ - CanisterSnapshotResponse, IC_00, TakeCanisterSnapshotArgs, + CanisterSnapshotResponse, IC_00, LoadCanisterSnapshotArgs, TakeCanisterSnapshotArgs, }; use ic_nervous_system_proxied_canister_calls_tracker::ProxiedCanisterCallsTracker; use ic_nervous_system_runtime::Runtime; @@ -62,6 +63,11 @@ pub trait ManagementCanisterClient { &self, args: TakeCanisterSnapshotArgs, ) -> Result; + + async fn load_canister_snapshot( + &self, + args: LoadCanisterSnapshotArgs, + ) -> Result<(), (i32, String)>; } /// An example implementation of the ManagementCanisterClient trait. @@ -197,6 +203,24 @@ impl ManagementCanisterClient for ManagementCanisterClientIm take_canister_snapshot::(args).await } + + async fn load_canister_snapshot( + &self, + args: LoadCanisterSnapshotArgs, + ) -> Result<(), (i32, String)> { + let _tracker = self.proxied_canister_calls_tracker.map(|tracker| { + let encoded_args = Encode!(&args).unwrap_or_default(); + ProxiedCanisterCallsTracker::start_tracking( + tracker, + dfn_core::api::caller(), + IC_00, + "load_canister_snapshot", + &encoded_args, + ) + }); + + load_canister_snapshot::(args).await + } } /// A ManagementCanisterClient that wraps another ManagementCanisterClient. @@ -322,6 +346,14 @@ where let _loan = self.try_borrow_slot()?; self.inner.take_canister_snapshot(args).await } + + async fn load_canister_snapshot( + &self, + args: LoadCanisterSnapshotArgs, + ) -> Result<(), (i32, String)> { + let _loan = self.try_borrow_slot()?; + self.inner.load_canister_snapshot(args).await + } } /// Increments available_slot_count by used_slot_count when dropped. @@ -376,6 +408,7 @@ pub enum MockManagementCanisterClientCall { StopCanister(CanisterIdRecord), DeleteCanister(CanisterIdRecord), TakeCanisterSnapshot(TakeCanisterSnapshotArgs), + LoadCanisterSnapshot(LoadCanisterSnapshotArgs), } #[derive(Clone, Eq, PartialEq, Debug)] @@ -387,6 +420,7 @@ pub enum MockManagementCanisterClientReply { StopCanister(Result<(), (i32, String)>), DeleteCanister(Result<(), (i32, String)>), TakeCanisterSnapshot(Result), + LoadCanisterSnapshot(Result<(), (i32, String)>), } #[async_trait] @@ -555,6 +589,32 @@ impl ManagementCanisterClient for MockManagementCanisterClient { ), } } + + async fn load_canister_snapshot( + &self, + args: LoadCanisterSnapshotArgs, + ) -> Result<(), (i32, String)> { + self.calls + .lock() + .unwrap() + .push_back(MockManagementCanisterClientCall::LoadCanisterSnapshot(args)); + + let reply = self + .replies + .lock() + .unwrap() + .pop_front() + .expect("Expected a MockManagementCanisterClientCall to be on the queue."); + + match reply { + MockManagementCanisterClientReply::LoadCanisterSnapshot(result) => result, + err => panic!( + "Expected MockManagementCanisterClientReply::LoadCanisterSnapshot to be at \ + the front of the queue. Had {:?}", + err + ), + } + } } impl Drop for MockManagementCanisterClient { diff --git a/rs/nervous_system/clients/src/management_canister_client/tests.rs b/rs/nervous_system/clients/src/management_canister_client/tests.rs index fbefab470b1f..517b656f3a94 100644 --- a/rs/nervous_system/clients/src/management_canister_client/tests.rs +++ b/rs/nervous_system/clients/src/management_canister_client/tests.rs @@ -5,6 +5,9 @@ use crate::canister_status::{ }; use candid::Nat; use ic_base_types::{CanisterId, PrincipalId}; +use ic_management_canister_types_private::{ + CanisterSnapshotResponse, LoadCanisterSnapshotArgs, TakeCanisterSnapshotArgs, +}; use rand::{Rng, thread_rng}; use std::time::Duration; @@ -91,6 +94,13 @@ async fn test_limit_outstanding_calls() { ) -> Result { unimplemented!(); } + + async fn load_canister_snapshot( + &self, + _args: LoadCanisterSnapshotArgs, + ) -> Result<(), (i32, String)> { + unimplemented!(); + } } impl Drop for MockManagementCanisterClient { diff --git a/rs/nns/governance/api/src/types.rs b/rs/nns/governance/api/src/types.rs index 68ce89a42bc8..1db02c92cca2 100644 --- a/rs/nns/governance/api/src/types.rs +++ b/rs/nns/governance/api/src/types.rs @@ -4206,6 +4206,8 @@ pub enum NnsFunction { /// controlled by the NNS Root canister. This restriction could be relaxed /// later. See nns/.../root.did for the payload type. TakeCanisterSnapshot = 56, + /// Loads a canister snapshot. + LoadCanisterSnapshot = 57, } impl NnsFunction { /// String value of the enum field names used in the ProtoBuf definition. @@ -4290,6 +4292,7 @@ impl NnsFunction { NnsFunction::UnpauseCanisterMigrations => "NNS_FUNCTION_UNPAUSE_CANISTER_MIGRATIONS", NnsFunction::SetSubnetOperationalLevel => "NNS_FUNCTION_SET_SUBNET_OPERATIONAL_LEVEL", NnsFunction::TakeCanisterSnapshot => "NNS_FUNCTION_TAKE_CANISTER_SNAPSHOT", + NnsFunction::LoadCanisterSnapshot => "NNS_FUNCTION_LOAD_CANISTER_SNAPSHOT", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -4371,6 +4374,7 @@ impl NnsFunction { "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), + "NNS_FUNCTION_LOAD_CANISTER_SNAPSHOT" => Some(Self::LoadCanisterSnapshot), _ => 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 aa48018e3b61..40e6f055ea86 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 @@ -498,6 +498,9 @@ enum NnsFunction { // Take a canister snapshot. NNS_FUNCTION_TAKE_CANISTER_SNAPSHOT = 56; + + // Load a canister snapshot. + NNS_FUNCTION_LOAD_CANISTER_SNAPSHOT = 57; } // 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 71c7727d3ed9..e4d92ccd7151 100644 --- a/rs/nns/governance/src/canister_state.rs +++ b/rs/nns/governance/src/canister_state.rs @@ -395,7 +395,8 @@ fn get_effective_payload( | ValidNnsFunction::PauseCanisterMigrations | ValidNnsFunction::UnpauseCanisterMigrations | ValidNnsFunction::SetSubnetOperationalLevel - | ValidNnsFunction::TakeCanisterSnapshot => Ok(payload.clone()), + | ValidNnsFunction::TakeCanisterSnapshot + | ValidNnsFunction::LoadCanisterSnapshot => 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 600a8e8dbd8a..15d3f96d79ce 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 @@ -4822,6 +4822,8 @@ pub enum NnsFunction { SetSubnetOperationalLevel = 55, /// Take a canister snapshot. TakeCanisterSnapshot = 56, + /// Load a canister snapshot. + LoadCanisterSnapshot = 57, } impl NnsFunction { /// String value of the enum field names used in the ProtoBuf definition. @@ -4896,6 +4898,7 @@ impl NnsFunction { Self::UnpauseCanisterMigrations => "NNS_FUNCTION_UNPAUSE_CANISTER_MIGRATIONS", Self::SetSubnetOperationalLevel => "NNS_FUNCTION_SET_SUBNET_OPERATIONAL_LEVEL", Self::TakeCanisterSnapshot => "NNS_FUNCTION_TAKE_CANISTER_SNAPSHOT", + Self::LoadCanisterSnapshot => "NNS_FUNCTION_LOAD_CANISTER_SNAPSHOT", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -4977,6 +4980,7 @@ impl NnsFunction { "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), + "NNS_FUNCTION_LOAD_CANISTER_SNAPSHOT" => Some(Self::LoadCanisterSnapshot), _ => None, } } diff --git a/rs/nns/governance/src/governance.rs b/rs/nns/governance/src/governance.rs index 9f1fc968a336..e08a6c22e3ac 100644 --- a/rs/nns/governance/src/governance.rs +++ b/rs/nns/governance/src/governance.rs @@ -4815,6 +4815,13 @@ impl Governance { )); } } + ValidNnsFunction::LoadCanisterSnapshot => { + if !are_canister_snapshot_proposals_enabled() { + return Err(invalid_proposal_error( + "LoadCanisterSnapshot 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 86b2d4a31461..2c2d461311c3 100644 --- a/rs/nns/governance/src/pb/conversions/mod.rs +++ b/rs/nns/governance/src/pb/conversions/mod.rs @@ -3988,6 +3988,7 @@ impl From for pb_api::NnsFunction { pb_api::NnsFunction::SetSubnetOperationalLevel } pb::NnsFunction::TakeCanisterSnapshot => pb_api::NnsFunction::TakeCanisterSnapshot, + pb::NnsFunction::LoadCanisterSnapshot => pb_api::NnsFunction::LoadCanisterSnapshot, } } } @@ -4098,6 +4099,7 @@ impl From for pb::NnsFunction { pb::NnsFunction::SetSubnetOperationalLevel } pb_api::NnsFunction::TakeCanisterSnapshot => pb::NnsFunction::TakeCanisterSnapshot, + pb_api::NnsFunction::LoadCanisterSnapshot => pb::NnsFunction::LoadCanisterSnapshot, } } } diff --git a/rs/nns/governance/src/proposals/execute_nns_function.rs b/rs/nns/governance/src/proposals/execute_nns_function.rs index 0d8a47cac333..1a8ecaa6bd22 100644 --- a/rs/nns/governance/src/proposals/execute_nns_function.rs +++ b/rs/nns/governance/src/proposals/execute_nns_function.rs @@ -154,6 +154,7 @@ pub enum ValidNnsFunction { UnpauseCanisterMigrations, SetSubnetOperationalLevel, TakeCanisterSnapshot, + LoadCanisterSnapshot, } impl ValidNnsFunction { @@ -283,6 +284,7 @@ impl ValidNnsFunction { (REGISTRY_CANISTER_ID, "set_subnet_operational_level") } ValidNnsFunction::TakeCanisterSnapshot => (ROOT_CANISTER_ID, "take_canister_snapshot"), + ValidNnsFunction::LoadCanisterSnapshot => (ROOT_CANISTER_ID, "load_canister_snapshot"), } } @@ -338,13 +340,13 @@ impl ValidNnsFunction { | ValidNnsFunction::HardResetNnsRootToVersion | ValidNnsFunction::BitcoinSetConfig | ValidNnsFunction::PauseCanisterMigrations - | ValidNnsFunction::UnpauseCanisterMigrations => Topic::ProtocolCanisterManagement, + | ValidNnsFunction::UnpauseCanisterMigrations + | ValidNnsFunction::TakeCanisterSnapshot + | ValidNnsFunction::LoadCanisterSnapshot => Topic::ProtocolCanisterManagement, ValidNnsFunction::AddSnsWasm | ValidNnsFunction::InsertSnsWasmUpgradePathEntries => { Topic::ServiceNervousSystemManagement } - - ValidNnsFunction::TakeCanisterSnapshot => Topic::ProtocolCanisterManagement, } } @@ -402,6 +404,7 @@ impl ValidNnsFunction { ValidNnsFunction::UnpauseCanisterMigrations => "Unpause Canister Migrations", ValidNnsFunction::SetSubnetOperationalLevel => "Set Subnet Operational Level", ValidNnsFunction::TakeCanisterSnapshot => "Take Canister Snapshot", + ValidNnsFunction::LoadCanisterSnapshot => "Load Canister Snapshot", } } @@ -633,6 +636,13 @@ impl ValidNnsFunction { For an introduction to canister snapshots in general, see \ https://docs.internetcomputer.org/building-apps/canister-management/snapshots ." } + ValidNnsFunction::LoadCanisterSnapshot => { + "A proposal to load a canister snapshot into a canister controlled the NNS. \ + In other words, to restore the canister to an earlier recorded state, \ + which including the code and memory (including stable memory). \ + For an introduction to canister snapshots in general, see \ + https://docs.internetcomputer.org/building-apps/canister-management/snapshots ." + } } } } @@ -722,6 +732,7 @@ impl TryFrom for ValidNnsFunction { Ok(ValidNnsFunction::SetSubnetOperationalLevel) } NnsFunction::TakeCanisterSnapshot => Ok(ValidNnsFunction::TakeCanisterSnapshot), + NnsFunction::LoadCanisterSnapshot => Ok(ValidNnsFunction::LoadCanisterSnapshot), // Obsolete functions - based on check_obsolete NnsFunction::BlessReplicaVersion | NnsFunction::RetireReplicaVersion => { diff --git a/rs/nns/handlers/root/impl/canister/canister.rs b/rs/nns/handlers/root/impl/canister/canister.rs index 632e42b3a131..2f2874c50749 100644 --- a/rs/nns/handlers/root/impl/canister/canister.rs +++ b/rs/nns/handlers/root/impl/canister/canister.rs @@ -30,8 +30,8 @@ use ic_nns_handler_root::{ }; use ic_nns_handler_root_interface::{ ChangeCanisterControllersRequest, ChangeCanisterControllersResponse, - TakeCanisterSnapshotRequest, TakeCanisterSnapshotResponse, UpdateCanisterSettingsRequest, - UpdateCanisterSettingsResponse, + LoadCanisterSnapshotRequest, LoadCanisterSnapshotResponse, TakeCanisterSnapshotRequest, + TakeCanisterSnapshotResponse, UpdateCanisterSettingsRequest, UpdateCanisterSettingsResponse, }; use std::cell::RefCell; @@ -242,6 +242,20 @@ async fn take_canister_snapshot( .await } +/// Loads a snapshot of a canister controlled by NNS Root. Only callable by NNS +/// Governance. +#[update] +async fn load_canister_snapshot( + load_canister_snapshot_request: LoadCanisterSnapshotRequest, +) -> LoadCanisterSnapshotResponse { + check_caller_is_governance(); + canister_management::load_canister_snapshot( + load_canister_snapshot_request, + &mut new_management_canister_client(), + ) + .await +} + /// Resources to serve for a given http_request /// Serve an HttpRequest made to this canister #[query( diff --git a/rs/nns/handlers/root/impl/canister/root.did b/rs/nns/handlers/root/impl/canister/root.did index 8ef061a7b0be..bc64fac0d44d 100644 --- a/rs/nns/handlers/root/impl/canister/root.did +++ b/rs/nns/handlers/root/impl/canister/root.did @@ -172,6 +172,26 @@ type TakeCanisterSnapshotError = record { description : text; }; +type LoadCanisterSnapshotRequest = record { + canister_id : principal; + snapshot_id : blob; +}; + +type LoadCanisterSnapshotResponse = variant { + Ok : LoadCanisterSnapshotOk; + Err : LoadCanisterSnapshotError; +}; + +type LoadCanisterSnapshotOk = record { + // This intentionally left empty. It is nevertheless here for + // a) consistency, and b) in case of future expansion. +}; + +type LoadCanisterSnapshotError = record { + code : opt int32; + description : text; +}; + service : () -> { canister_status : (CanisterIdRecord) -> (CanisterStatusResult); get_build_metadata : () -> (text) query; @@ -190,4 +210,7 @@ service : () -> { take_canister_snapshot : (TakeCanisterSnapshotArgs) -> ( TakeCanisterSnapshotResponse, ); + load_canister_snapshot : (LoadCanisterSnapshotRequest) -> ( + LoadCanisterSnapshotResponse, + ); } diff --git a/rs/nns/handlers/root/impl/src/canister_management.rs b/rs/nns/handlers/root/impl/src/canister_management.rs index 1be377c1b8a7..8d26dfe2811e 100644 --- a/rs/nns/handlers/root/impl/src/canister_management.rs +++ b/rs/nns/handlers/root/impl/src/canister_management.rs @@ -7,7 +7,7 @@ use ic_cdk::{ }; use ic_management_canister_types_private::{ CanisterInstallMode::Install, CanisterSettingsArgsBuilder, CanisterSnapshotResponse, - CreateCanisterArgs, InstallCodeArgs, TakeCanisterSnapshotArgs, + CreateCanisterArgs, InstallCodeArgs, LoadCanisterSnapshotArgs, TakeCanisterSnapshotArgs, }; use ic_nervous_system_clients::{ canister_id_record::CanisterIdRecord, @@ -24,9 +24,11 @@ use ic_nns_common::{ types::CallCanisterRequest, }; use ic_nns_handler_root_interface::{ - ChangeCanisterControllersRequest, ChangeCanisterControllersResponse, TakeCanisterSnapshotError, - TakeCanisterSnapshotOk, TakeCanisterSnapshotRequest, TakeCanisterSnapshotResponse, - UpdateCanisterSettingsError, UpdateCanisterSettingsRequest, UpdateCanisterSettingsResponse, + ChangeCanisterControllersRequest, ChangeCanisterControllersResponse, LoadCanisterSnapshotError, + LoadCanisterSnapshotOk, LoadCanisterSnapshotRequest, LoadCanisterSnapshotResponse, + TakeCanisterSnapshotError, TakeCanisterSnapshotOk, TakeCanisterSnapshotRequest, + TakeCanisterSnapshotResponse, UpdateCanisterSettingsError, UpdateCanisterSettingsRequest, + UpdateCanisterSettingsResponse, }; use ic_protobuf::{ registry::nns::v1::{NnsCanisterRecord, NnsCanisterRecords}, @@ -321,3 +323,50 @@ fn convert_from_canister_snapshot_response_to_take_canister_snapshot_ok( total_size, } } + +pub async fn load_canister_snapshot( + load_canister_snapshot_request: LoadCanisterSnapshotRequest, + management_canister_client: &mut impl ManagementCanisterClient, +) -> LoadCanisterSnapshotResponse { + let LoadCanisterSnapshotRequest { + canister_id, + snapshot_id, + } = load_canister_snapshot_request; + + let snapshot_id = match SnapshotId::try_from(snapshot_id) { + Ok(ok) => ok, + Err(err) => { + return LoadCanisterSnapshotResponse::Err(LoadCanisterSnapshotError { + code: None, + description: format!("Invalid snapshot ID: {err}"), + }); + } + }; + + let canister_id = match CanisterId::try_from(canister_id) { + Ok(ok) => ok, + Err(err) => { + return LoadCanisterSnapshotResponse::Err(LoadCanisterSnapshotError { + code: None, + description: format!("Invalid canister ID: {err}"), + }); + } + }; + + let load_canister_snapshot_args = LoadCanisterSnapshotArgs::new( + canister_id, + snapshot_id, + management_canister_client.canister_version(), + ); + + match management_canister_client + .load_canister_snapshot(load_canister_snapshot_args) + .await + { + Ok(()) => LoadCanisterSnapshotResponse::Ok(LoadCanisterSnapshotOk {}), + Err((code, description)) => LoadCanisterSnapshotResponse::Err(LoadCanisterSnapshotError { + code: Some(code), + description, + }), + } +} diff --git a/rs/nns/handlers/root/interface/src/lib.rs b/rs/nns/handlers/root/interface/src/lib.rs index cd7e72c2654c..3f2f0a03ac69 100644 --- a/rs/nns/handlers/root/interface/src/lib.rs +++ b/rs/nns/handlers/root/interface/src/lib.rs @@ -107,3 +107,24 @@ pub struct TakeCanisterSnapshotError { pub code: Option, pub description: String, } + +#[derive(Clone, Eq, PartialEq, Hash, Debug, CandidType, Deserialize)] +pub struct LoadCanisterSnapshotRequest { + pub canister_id: PrincipalId, + pub snapshot_id: Vec, +} + +#[derive(Clone, Eq, PartialEq, Hash, Debug, CandidType, Deserialize)] +pub enum LoadCanisterSnapshotResponse { + Ok(LoadCanisterSnapshotOk), + Err(LoadCanisterSnapshotError), +} + +#[derive(Clone, Eq, PartialEq, Hash, Debug, CandidType, Deserialize)] +pub struct LoadCanisterSnapshotOk {} + +#[derive(Clone, Eq, PartialEq, Hash, Debug, CandidType, Deserialize)] +pub struct LoadCanisterSnapshotError { + pub code: Option, + pub description: String, +} diff --git a/rs/nns/handlers/root/unreleased_changelog.md b/rs/nns/handlers/root/unreleased_changelog.md index 2b7ee204e396..6e5853c94834 100644 --- a/rs/nns/handlers/root/unreleased_changelog.md +++ b/rs/nns/handlers/root/unreleased_changelog.md @@ -9,7 +9,10 @@ on the process that this file is part of, see ## Added -* Added `take_canister_snapshot` method. It is only callable by the Governance canister though. +* Added `take_canister_snapshot`, and `load_canister_snapshot` methods. These + are only callable by the Governance canister though. What these do is + proxy/immediately forward to methods of the same name in the Management + (pseudo-)canister. ## Changed diff --git a/rs/nns/integration_tests/BUILD.bazel b/rs/nns/integration_tests/BUILD.bazel index cbffb5786c13..71f64ae8f6a0 100644 --- a/rs/nns/integration_tests/BUILD.bazel +++ b/rs/nns/integration_tests/BUILD.bazel @@ -18,6 +18,7 @@ BASE_DEPENDENCIES = [ "//rs/nns/governance/api", "//rs/nns/governance/init", "//rs/nns/handlers/lifeline/impl:lifeline", + "//rs/nns/handlers/root/interface", "//rs/nns/sns-wasm", "//rs/node_rewards/canister/api", "//rs/registry/canister/api", diff --git a/rs/nns/integration_tests/Cargo.toml b/rs/nns/integration_tests/Cargo.toml index 81e104c5eb4b..e07b29bcacb2 100644 --- a/rs/nns/integration_tests/Cargo.toml +++ b/rs/nns/integration_tests/Cargo.toml @@ -50,6 +50,7 @@ ic-nns-common = { path = "../common" } ic-nns-governance = { path = "../governance" } ic-nns-governance-api = { path = "../governance/api" } ic-nns-governance-init = { path = "../governance/init" } +ic-nns-handler-root-interface = { path = "../handlers/root/interface" } ic-node-rewards-canister-api = { path = "../../node_rewards/canister/api" } ic-sns-root = { path = "../../sns/root" } ic-sns-swap = { path = "../../sns/swap" } diff --git a/rs/nns/integration_tests/src/take_canister_snapshot.rs b/rs/nns/integration_tests/src/take_canister_snapshot.rs index 62abe73af7e6..0572b9920444 100644 --- a/rs/nns/integration_tests/src/take_canister_snapshot.rs +++ b/rs/nns/integration_tests/src/take_canister_snapshot.rs @@ -1,12 +1,13 @@ -use candid::{CandidType, Encode}; +use candid::{Decode, Encode}; use ic_base_types::{CanisterId, PrincipalId}; use ic_management_canister_types_private::{CanisterSnapshotResponse, ListCanisterSnapshotArgs}; use ic_nns_constants::{GOVERNANCE_CANISTER_ID, ROOT_CANISTER_ID}; use ic_nns_governance::pb::v1::ProposalStatus; use ic_nns_governance_api::{ - ExecuteNnsFunction, MakeProposalRequest, NnsFunction, ProposalActionRequest, - manage_neuron_response::Command, + ExecuteNnsFunction, MakeProposalRequest, Motion, NnsFunction, ProposalActionRequest, + ProposalInfo, manage_neuron_response::Command, }; +use ic_nns_handler_root_interface::{LoadCanisterSnapshotRequest, TakeCanisterSnapshotRequest}; use ic_nns_test_utils::{ common::NnsInitPayloadsBuilder, neuron_helpers::get_neuron_1, @@ -16,19 +17,10 @@ use ic_nns_test_utils::{ update_with_sender, }, }; -use serde::Deserialize; use std::time::{Duration, SystemTime}; -// Defined in ic_nns_handler_root_interface, but redefined here to avoid extra dependencies -// for the test target if not already present. -#[derive(Clone, Eq, PartialEq, Hash, Debug, CandidType, Deserialize)] -pub struct TakeCanisterSnapshotRequest { - pub canister_id: PrincipalId, - pub replace_snapshot: Option>, -} - #[test] -fn test_take_canister_snapshot() { +fn test_take_and_load_canister_snapshot() { // Step 1: Prepare the world: Set up the NNS canisters and get the neuron. let state_machine = state_machine_builder_for_nns_tests().build(); @@ -196,4 +188,159 @@ fn test_take_canister_snapshot() { first_snapshot.snapshot_id(), "{second_snapshot:#?}\n\nvs.\n\n{first_snapshot:#?}" ); + + // Step 1C: Prepare the world for LoadCanisterSnapshot. This consists of + // submitting a "marker" (Motion) proposal. It will get blown away by the + // LoadCanisterSnapshot proposal, because it is being created after the + // snapshot loaded by the LoadCanisterSnapshot proposal. + let make_marker_response = nns_governance_make_proposal( + &state_machine, + neuron.principal_id, + neuron.neuron_id, + &MakeProposalRequest { + title: Some("Marker Proposal".to_string()), + summary: "This is a marker proposal.".to_string(), + url: "https://forum.dfinity.org/marker-proposal".to_string(), + action: Some(ProposalActionRequest::Motion(Motion { + motion_text: "This proposal should disappear after snapshot load".to_string(), + })), + }, + ); + let marker_proposal_id = match make_marker_response.command.as_ref().unwrap() { + Command::MakeProposal(response) => response.proposal_id.unwrap(), + _ => panic!("{make_marker_response:#?}"), + }; + nns_wait_for_proposal_execution(&state_machine, marker_proposal_id.id); + + // Verify marker exists. After loading the snapshot, this won't work anymore. + let _marker_info: ProposalInfo = + nns_governance_get_proposal_info_as_anonymous(&state_machine, marker_proposal_id.id); + + // Step 2C: Run the code under test by passing a LoadCanisterSnapshot + // proposal. (As is often the case in tests, the proposal passes right away + // due to the proposal being made by a neuron with overwhelming voting + // power.) + + // Step 2C.1: Assemble MakeProposalRequest. + let payload = Encode!(&LoadCanisterSnapshotRequest { + canister_id: target_canister_id.get(), + // Remember, this snapshot (second_snapshot) was taken BEFORE the marker + // proposal. + snapshot_id: second_snapshot.snapshot_id().to_vec(), + }) + .unwrap(); + let action = ProposalActionRequest::ExecuteNnsFunction(ExecuteNnsFunction { + nns_function: NnsFunction::LoadCanisterSnapshot as i32, + payload, + }); + let make_proposal_request = MakeProposalRequest { + title: Some("Restore Governance Canister to Snapshot 2".to_string()), + summary: r#"This will clobber the "marker" motion proposal."#.to_string(), + url: "https://forum.dfinity.org/restore-governance-canister-to-snapshot-2".to_string(), + action: Some(action), + }; + + // Step 2C.2: Submit the proposal. + let make_proposal_response = nns_governance_make_proposal( + &state_machine, + neuron.principal_id, + neuron.neuron_id, + &make_proposal_request, + ); + let load_proposal_id = match make_proposal_response.command.as_ref().unwrap() { + Command::MakeProposal(response) => response.proposal_id.unwrap(), + _ => panic!("{make_proposal_response:#?}"), + }; + + // Step 3C: Verify LoadCanisterSnapshot execution. + + // Step 3C.1: Poll until the LoadCanisterSnapshot proposal vanishes (or it + // is marked as fail). If LoadCanisterSnapshot proposals work correctly, + // then the LoadCanisterSnapshot proposal itself would disappear, because + // that proposal itself is not in the (Governance canister) snapshot. + let mut done = false; + for _ in 0..50 { + // Fetch the LoadCanisterSnapshot proposal. + let response_bytes = state_machine + .execute_ingress_as( + PrincipalId::new_anonymous(), + GOVERNANCE_CANISTER_ID, + "get_proposal_info", + Encode!(&load_proposal_id.id).unwrap(), + ) + .unwrap(); + let result = match response_bytes { + ic_types::ingress::WasmResult::Reply(bytes) => bytes, + ic_types::ingress::WasmResult::Reject(reason) => { + panic!("get_proposal_info rejected: {reason}") + } + }; + let proposal_info: Option = + candid::Decode!(&result, Option).unwrap(); + + // If the proposal is suddenly missing, that's actually a sign that it + // worked. In any case, it means we can now proceed with the rest of + // verification. + if proposal_info.is_none() { + println!( + "As expected, the LoadCanisterSnapshot proposal vanished \ + (as a result of its own execution!).", + ); + done = true; + break; + } + + // Exit early if proposal execution failed, since this is a terminal + // state. This is "just" an optimization in that this whole test would + // fail even if we deleted this chunk. + let status = ProposalStatus::try_from(proposal_info.unwrap().status); + if status == Ok(ProposalStatus::Failed) { + panic!("Load Snapshot Proposal failed execution!"); + } + + // Sleep before polling again. + state_machine.advance_time(Duration::from_secs(10)); + state_machine.tick(); + } + assert!( + done, + "Timeout waiting for Load Snapshot Proposal to vanish \ + (as a result of correct execution).", + ); + + // Step 3C.2: Verify that the MARKER (motion) proposal has (also) been blown + // away (not just the LoadCanisterSnapshot proposal). + let response_bytes = state_machine + .execute_ingress_as( + PrincipalId::new_anonymous(), + GOVERNANCE_CANISTER_ID, + "get_proposal_info", + Encode!(&marker_proposal_id.id).unwrap(), + ) + .unwrap(); + let result = match response_bytes { + ic_types::ingress::WasmResult::Reply(bytes) => bytes, + ic_types::ingress::WasmResult::Reject(reason) => { + panic!("get_proposal_info rejected: {reason}") + } + }; + let final_marker_proposal_status: Option = + candid::Decode!(&result, Option).unwrap(); + assert_eq!( + final_marker_proposal_status, None, + "Marker proposal {} should have been wiped out by snapshot load, \ + but it still exists: {:#?}", + marker_proposal_id.id, final_marker_proposal_status + ); + + // Step 3C.3: Verify that the first proposal is still there (albeit moot, + // since the second proposal clobbered the snapshot created by the first + // proposal.) + let first_proposal_info = + nns_governance_get_proposal_info_as_anonymous(&state_machine, first_proposal_id.id); + assert_eq!( + ProposalStatus::try_from(first_proposal_info.status), + Ok(ProposalStatus::Executed), + "First proposal should still exist and be executed: {first_proposal_info:#?}", + ); }