Skip to content
13 changes: 13 additions & 0 deletions rs/nns/governance/api/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,16 @@ pub struct Motion {
/// The text of the motion. Maximum 100kib.
pub motion_text: String,
}
/// Take a snapshot of the state of a canister.
#[derive(
candid::CandidType, candid::Deserialize, serde::Serialize, Clone, PartialEq, Debug, Default,
)]
pub struct TakeCanisterSnapshot {
/// The canister being snapshotted.
pub canister_id: Option<PrincipalId>,
/// If set, the existing snapshot with this content will be replaced.
pub replace_snapshot: Option<Vec<u8>>,
}
/// For all Neurons controlled by the given principals, set their
/// KYC status to `kyc_verified=true`.
#[derive(
Expand Down Expand Up @@ -641,6 +651,8 @@ pub mod proposal {
/// back to a healthy state, to recover from some kind of disaster, like
/// a boot loop, or something like that.
BlessAlternativeGuestOsVersion(super::BlessAlternativeGuestOsVersion),
/// Take a canister snapshot.
TakeCanisterSnapshot(super::TakeCanisterSnapshot),
}
}
/// Empty message to use in oneof fields that represent empty
Expand Down Expand Up @@ -1399,6 +1411,7 @@ pub enum ProposalActionRequest {
UpdateCanisterSettings(UpdateCanisterSettings),
FulfillSubnetRentalRequest(FulfillSubnetRentalRequest),
BlessAlternativeGuestOsVersion(BlessAlternativeGuestOsVersion),
TakeCanisterSnapshot(TakeCanisterSnapshot),
}

#[derive(
Expand Down
5 changes: 5 additions & 0 deletions rs/nns/governance/canister/governance.did
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type Action = variant {
Motion : Motion;
FulfillSubnetRentalRequest : FulfillSubnetRentalRequest;
BlessAlternativeGuestOsVersion : BlessAlternativeGuestOsVersion;
TakeCanisterSnapshot : TakeCanisterSnapshot
};

type AddHotKey = record {
Expand Down Expand Up @@ -1099,6 +1100,10 @@ type GuestLaunchMeasurementMetadata = record {
kernel_cmdline : opt text;
};

type TakeCanisterSnapshot = record {
canister_id : opt principal;
replace_snapshot : opt blob;
};

type ProposalData = record {
id : opt ProposalId;
Expand Down
10 changes: 10 additions & 0 deletions rs/nns/governance/proto/ic_nns_governance/pb/v1/governance.proto
Original file line number Diff line number Diff line change
Expand Up @@ -707,9 +707,19 @@ message Proposal {
// Allow node operators to manually intervene in case of disaster to run
// (NNS-approved) new software.
BlessAlternativeGuestOsVersion bless_alternative_guest_os_version = 31;
// Take a canister snapshot.
TakeCanisterSnapshot take_canister_snapshot = 32;
}
}

// Take a canister snapshot.
message TakeCanisterSnapshot {
// The canister being snapshotted.
ic_base_types.pb.v1.PrincipalId canister_id = 1;
// If set, the existing snapshot with this content will be replaced.
optional bytes replace_snapshot = 2;
}

// Empty message to use in oneof fields that represent empty
// enums.
message Empty {}
Expand Down
23 changes: 22 additions & 1 deletion rs/nns/governance/src/gen/ic_nns_governance.pb.v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,7 @@ pub struct Proposal {
/// take.
#[prost(
oneof = "proposal::Action",
tags = "10, 12, 13, 14, 15, 16, 17, 18, 19, 21, 29, 22, 23, 24, 25, 26, 27, 28, 31"
tags = "10, 12, 13, 14, 15, 16, 17, 18, 19, 21, 29, 22, 23, 24, 25, 26, 27, 28, 31, 32"
)]
pub action: ::core::option::Option<proposal::Action>,
}
Expand Down Expand Up @@ -542,8 +542,29 @@ pub mod proposal {
/// (NNS-approved) new software.
#[prost(message, tag = "31")]
BlessAlternativeGuestOsVersion(super::BlessAlternativeGuestOsVersion),
/// Take a canister snapshot.
#[prost(message, tag = "32")]
TakeCanisterSnapshot(super::TakeCanisterSnapshot),
}
}
/// Take a canister snapshot.
#[derive(
candid::CandidType,
candid::Deserialize,
serde::Serialize,
comparable::Comparable,
Clone,
PartialEq,
::prost::Message,
)]
pub struct TakeCanisterSnapshot {
/// The canister being snapshotted.
#[prost(message, optional, tag = "1")]
pub canister_id: ::core::option::Option<::ic_base_types::PrincipalId>,
/// If set, the existing snapshot with this content will be replaced.
#[prost(bytes = "vec", optional, tag = "2")]
pub replace_snapshot: ::core::option::Option<::prost::alloc::vec::Vec<u8>>,
}
/// Empty message to use in oneof fields that represent empty
/// enums.
#[derive(
Expand Down
23 changes: 21 additions & 2 deletions rs/nns/governance/src/governance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ use crate::{
NeuronsFundSnapshot as NeuronsFundSnapshotPb, NnsFunction, NodeProvider, Proposal,
ProposalData, ProposalRewardStatus, ProposalStatus, RestoreAgingSummary, RewardEvent,
RewardNodeProvider, RewardNodeProviders, SettleNeuronsFundParticipationRequest,
SettleNeuronsFundParticipationResponse, StopOrStartCanister, Tally, Topic,
UpdateCanisterSettings, UpdateNodeProvider, Vote, VotingPowerEconomics,
SettleNeuronsFundParticipationResponse, StopOrStartCanister, TakeCanisterSnapshot,
Tally, Topic, UpdateCanisterSettings, UpdateNodeProvider, Vote, VotingPowerEconomics,
WaitForQuietState, archived_monthly_node_provider_rewards,
create_service_nervous_system::LedgerParameters,
get_neurons_fund_audit_info_response,
Expand Down Expand Up @@ -512,6 +512,7 @@ impl Action {
Action::BlessAlternativeGuestOsVersion(_) => {
"ACTION_BLESS_ALTERNATIVE_GUEST_OS_VERSION"
}
Action::TakeCanisterSnapshot(_) => "ACTION_TAKE_CANISTER_SNAPSHOT",
}
}
}
Expand Down Expand Up @@ -4174,6 +4175,10 @@ impl Governance {
pid,
bless_alternative_guest_os_version,
),
ValidProposalAction::TakeCanisterSnapshot(take_canister_snapshot) => {
self.perform_take_canister_snapshot(pid, take_canister_snapshot)
.await;
}
}
}

Expand Down Expand Up @@ -4254,6 +4259,17 @@ impl Governance {
self.set_proposal_execution_status(proposal_id, result);
}

async fn perform_take_canister_snapshot(
&mut self,
proposal_id: u64,
take_canister_snapshot: TakeCanisterSnapshot,
) {
let result = self
.perform_call_canister(proposal_id, take_canister_snapshot)
.await;
self.set_proposal_execution_status(proposal_id, result);
}

async fn perform_call_canister(
&mut self,
proposal_id: u64,
Expand Down Expand Up @@ -4771,6 +4787,9 @@ impl Governance {
ValidProposalAction::BlessAlternativeGuestOsVersion(
bless_alternative_guest_os_version,
) => bless_alternative_guest_os_version.validate(),
ValidProposalAction::TakeCanisterSnapshot(take_canister_snapshot) => {
take_canister_snapshot.validate()
}
}
}

Expand Down
24 changes: 24 additions & 0 deletions rs/nns/governance/src/pb/conversions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,9 @@ impl From<pb_api::proposal::Action> for pb::proposal::Action {
pb_api::proposal::Action::BlessAlternativeGuestOsVersion(v) => {
pb::proposal::Action::BlessAlternativeGuestOsVersion(v.into())
}
pb_api::proposal::Action::TakeCanisterSnapshot(v) => {
pb::proposal::Action::TakeCanisterSnapshot(v.into())
}
}
}
}
Expand Down Expand Up @@ -515,6 +518,9 @@ impl From<pb_api::ProposalActionRequest> for pb::proposal::Action {
pb_api::ProposalActionRequest::BlessAlternativeGuestOsVersion(v) => {
pb::proposal::Action::BlessAlternativeGuestOsVersion(v.into())
}
pb_api::ProposalActionRequest::TakeCanisterSnapshot(v) => {
pb::proposal::Action::TakeCanisterSnapshot(v.into())
}
}
}
}
Expand Down Expand Up @@ -4249,3 +4255,21 @@ impl From<pb::SelfDescribingProposalAction> for pb_api::SelfDescribingProposalAc
}
}
}

impl From<pb::TakeCanisterSnapshot> for pb_api::TakeCanisterSnapshot {
fn from(item: pb::TakeCanisterSnapshot) -> Self {
Self {
canister_id: item.canister_id,
replace_snapshot: item.replace_snapshot,
}
}
}

impl From<pb_api::TakeCanisterSnapshot> for pb::TakeCanisterSnapshot {
fn from(item: pb_api::TakeCanisterSnapshot) -> Self {
Self {
canister_id: item.canister_id,
replace_snapshot: item.replace_snapshot,
}
}
}
3 changes: 3 additions & 0 deletions rs/nns/governance/src/pb/proposal_conversions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,9 @@ fn convert_action(
pb::proposal::Action::BlessAlternativeGuestOsVersion(v) => {
pb_api::proposal::Action::BlessAlternativeGuestOsVersion(v.clone().into())
}
pb::proposal::Action::TakeCanisterSnapshot(v) => {
pb_api::proposal::Action::TakeCanisterSnapshot(v.clone().into())
}

// The action types with potentially large fields need to be converted in a way that avoids
// cloning the action first.
Expand Down
12 changes: 10 additions & 2 deletions rs/nns/governance/src/proposals/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ use crate::{
ApproveGenesisKyc, BlessAlternativeGuestOsVersion, CreateServiceNervousSystem,
DeregisterKnownNeuron, GovernanceError, InstallCode, KnownNeuron, ManageNeuron, Motion,
NetworkEconomics, ProposalData, RewardNodeProvider, RewardNodeProviders,
SelfDescribingProposalAction, StopOrStartCanister, Topic, UpdateCanisterSettings, Vote,
governance_error::ErrorType, proposal::Action,
SelfDescribingProposalAction, StopOrStartCanister, TakeCanisterSnapshot, Topic,
UpdateCanisterSettings, Vote, governance_error::ErrorType, proposal::Action,
},
proposals::{
add_or_remove_node_provider::ValidAddOrRemoveNodeProvider,
Expand All @@ -32,6 +32,7 @@ pub mod manage_neuron;
pub mod register_known_neuron;
pub mod self_describing;
pub mod stop_or_start_canister;
pub mod take_canister_snapshot;
pub mod update_canister_settings;

mod decode_candid_args_to_self_describing_value;
Expand All @@ -57,6 +58,7 @@ pub(crate) enum ValidProposalAction {
UpdateCanisterSettings(UpdateCanisterSettings),
FulfillSubnetRentalRequest(ValidFulfillSubnetRentalRequest),
BlessAlternativeGuestOsVersion(BlessAlternativeGuestOsVersion),
TakeCanisterSnapshot(TakeCanisterSnapshot),
}

impl TryFrom<Option<Action>> for ValidProposalAction {
Expand Down Expand Up @@ -117,6 +119,9 @@ impl TryFrom<Option<Action>> for ValidProposalAction {
bless_alternative_guest_os_version,
))
}
Action::TakeCanisterSnapshot(take_canister_snapshot) => Ok(
ValidProposalAction::TakeCanisterSnapshot(take_canister_snapshot),
),

// Obsolete actions
Action::SetDefaultFollowees(_) => Err(GovernanceError::new_with_message(
Expand Down Expand Up @@ -161,6 +166,9 @@ impl ValidProposalAction {
}
ValidProposalAction::FulfillSubnetRentalRequest(_) => Topic::SubnetRental,
ValidProposalAction::BlessAlternativeGuestOsVersion(_) => Topic::NodeAdmin,
ValidProposalAction::TakeCanisterSnapshot(take_canister_snapshot) => {
take_canister_snapshot.valid_topic()?
}
};
Ok(topic)
}
Expand Down
60 changes: 60 additions & 0 deletions rs/nns/governance/src/proposals/take_canister_snapshot.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
use crate::{
pb::v1::{GovernanceError, TakeCanisterSnapshot, Topic},
proposals::{call_canister::CallCanister, invalid_proposal_error, topic_to_manage_canister},
};
use candid::Encode;
use ic_base_types::CanisterId;
use ic_nns_constants::ROOT_CANISTER_ID;
use ic_nns_handler_root_interface::TakeCanisterSnapshotRequest;

impl TakeCanisterSnapshot {
pub fn validate(&self) -> Result<(), GovernanceError> {
self.valid_canister_id()?;
Ok(())
}

pub fn valid_topic(&self) -> Result<Topic, GovernanceError> {
let canister_id = self.valid_canister_id()?;
Ok(topic_to_manage_canister(&canister_id))
}

fn valid_canister_id(&self) -> Result<CanisterId, GovernanceError> {
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)
}
}

impl CallCanister for TakeCanisterSnapshot {
fn canister_and_function(&self) -> Result<(CanisterId, &str), GovernanceError> {
Ok((ROOT_CANISTER_ID, "take_canister_snapshot"))
}

fn payload(&self) -> Result<Vec<u8>, GovernanceError> {
let args = convert_take_canister_snapshot_from_proposal_to_root_request(self)?;
Encode!(&args)
.map_err(|e| invalid_proposal_error(&format!("Failed to encode payload: {e}")))
}
}

pub fn convert_take_canister_snapshot_from_proposal_to_root_request(
original: &TakeCanisterSnapshot,
) -> Result<TakeCanisterSnapshotRequest, GovernanceError> {
let TakeCanisterSnapshot {
canister_id,
replace_snapshot,
} = original.clone();

let canister_id = canister_id.ok_or_else(|| {
invalid_proposal_error("canister_id is required for TakeCanisterSnapshot")
})?;

Ok(TakeCanisterSnapshotRequest {
canister_id,
replace_snapshot,
})
}
Loading
Loading