diff --git a/core/upgrader/api/spec.did b/core/upgrader/api/spec.did index 9ba6dc764..72e37a282 100644 --- a/core/upgrader/api/spec.did +++ b/core/upgrader/api/spec.did @@ -162,9 +162,9 @@ type SetDisasterRecoveryAccountsAndAssetsInput = record { // Request to trigger disaster recovery. Requests are stored in the Upgrader // canister, and when at least `quorum` of the committee members -// agree on the exact module, args, and install mode, the request is processed. +// agree on the exact disaster recovery input, the request is processed. // Requests older than 1 week will be discarded. -type RequestDisasterRecoveryInput = record { +type RequestDisasterRecoveryInstallCodeInput = record { // The wasm module to be installed. module : blob; // Additional wasm module chunks to append to the wasm module. @@ -174,6 +174,9 @@ type RequestDisasterRecoveryInput = record { // The install mode: Install, Upgrade, or Reinstall. install_mode : InstallMode; }; +type RequestDisasterRecoveryInput = variant { + InstallCode : RequestDisasterRecoveryInstallCodeInput; +}; type InstallMode = variant { // Install the wasm module. @@ -226,16 +229,25 @@ type GetLogsResult = variant { Err : Error; }; -// Request to recover the station. -type StationRecoveryRequest = record { - // The requester user id. - user_id : text; - // The sha of the wasm module to be installed. - wasm_sha256 : blob; +type StationRecoveryRequestInstallCodeOperation = record { // The install mode: Install, Upgrade, or Reinstall. install_mode : InstallMode; + // The sha of the wasm module to be installed. + wasm_sha256 : blob; // The argument to be passed to the install function. arg : blob; +}; + +type StationRecoveryRequestOperation = variant { + InstallCode : StationRecoveryRequestInstallCodeOperation; +}; + +// Request to recover the station. +type StationRecoveryRequest = record { + // The requester user id. + user_id : text; + // The disaster recovery operation. + operation : StationRecoveryRequestOperation; // The request submission timestamp. submitted_at : text; }; diff --git a/core/upgrader/api/src/lib.rs b/core/upgrader/api/src/lib.rs index 615b38df1..a4b6bfe88 100644 --- a/core/upgrader/api/src/lib.rs +++ b/core/upgrader/api/src/lib.rs @@ -155,7 +155,7 @@ pub enum InstallMode { } #[derive(Clone, Debug, CandidType, Deserialize)] -pub struct RequestDisasterRecoveryInput { +pub struct RequestDisasterRecoveryInstallCodeInput { #[serde(with = "serde_bytes")] pub module: Vec, pub module_extra_chunks: Option, @@ -165,6 +165,11 @@ pub struct RequestDisasterRecoveryInput { pub install_mode: InstallMode, } +#[derive(Clone, Debug, CandidType, Deserialize)] +pub enum RequestDisasterRecoveryInput { + InstallCode(RequestDisasterRecoveryInstallCodeInput), +} + #[derive(CandidType, Deserialize, Debug, Clone)] pub struct PaginationInput { pub offset: Option, @@ -198,15 +203,26 @@ pub enum TriggerUpgradeResponse { } #[derive(Clone, Debug, CandidType, Deserialize, PartialEq, Eq)] -pub struct StationRecoveryRequest { - /// The user ID of the station. - pub user_id: UuidDTO, - /// The SHA-256 hash of the wasm module. - pub wasm_sha256: Vec, +pub struct StationRecoveryRequestInstallCodeOperation { /// The install mode: upgrade or reinstall. pub install_mode: InstallMode, + /// The SHA-256 hash of the wasm module. + pub wasm_sha256: Vec, /// The install arguments. pub arg: Vec, +} + +#[derive(Clone, Debug, CandidType, Deserialize, PartialEq, Eq)] +pub enum StationRecoveryRequestOperation { + InstallCode(StationRecoveryRequestInstallCodeOperation), +} + +#[derive(Clone, Debug, CandidType, Deserialize, PartialEq, Eq)] +pub struct StationRecoveryRequest { + /// The user ID of the station. + pub user_id: UuidDTO, + /// The disaster recovery operation. + pub operation: StationRecoveryRequestOperation, /// Time in nanoseconds since the UNIX epoch when the request was submitted. pub submitted_at: TimestampRfc3339, } diff --git a/core/upgrader/impl/src/lib.rs b/core/upgrader/impl/src/lib.rs index 5403e2b56..449f4f62c 100644 --- a/core/upgrader/impl/src/lib.rs +++ b/core/upgrader/impl/src/lib.rs @@ -1,4 +1,4 @@ -use crate::model::{DisasterRecovery, LogEntry}; +use crate::model::{DisasterRecovery, DisasterRecoveryV0, LogEntry}; use crate::services::insert_logs; use crate::upgrade::{ CheckController, Upgrade, Upgrader, WithAuthorization, WithBackground, WithLogs, WithStart, @@ -28,6 +28,7 @@ pub use orbit_essentials::cdk::mocks as upgrader_ic_cdk; pub mod controllers; pub mod errors; +pub mod mappers; pub mod model; pub mod services; pub mod upgrade; @@ -151,10 +152,10 @@ fn post_upgrade() { // if a principal can be parsed out of memory with OLD_MEMORY_ID_TARGET_CANISTER_ID // then we need to perform stable memory migration if let Ok(target_canister) = serde_cbor::from_slice::(&target_canister_bytes.0) { - let old_disaster_recovery: StableValue = StableValue::init( + let old_disaster_recovery: StableValue = StableValue::init( old_memory_manager.get(MemoryId::new(OLD_MEMORY_ID_DISASTER_RECOVERY)), ); - let disaster_recovery: DisasterRecovery = + let disaster_recovery: DisasterRecoveryV0 = old_disaster_recovery.get(&()).unwrap_or_default(); let old_logs: StableBTreeMap = @@ -167,7 +168,7 @@ fn post_upgrade() { let state = State { target_canister, - disaster_recovery, + disaster_recovery: disaster_recovery.into(), stable_memory_version: STABLE_MEMORY_VERSION, }; set_state(state); diff --git a/core/upgrader/impl/src/mappers/disaster_recovery.rs b/core/upgrader/impl/src/mappers/disaster_recovery.rs new file mode 100644 index 000000000..ebdcd9aa1 --- /dev/null +++ b/core/upgrader/impl/src/mappers/disaster_recovery.rs @@ -0,0 +1,120 @@ +use crate::model::{ + DisasterRecovery, DisasterRecoveryV0, RequestDisasterRecoveryInstallCodeLog, + RequestDisasterRecoveryOperationLog, StationRecoveryRequest, + StationRecoveryRequestInstallCodeOperation, + StationRecoveryRequestInstallCodeOperationFootprint, StationRecoveryRequestOperation, + StationRecoveryRequestOperationFootprint, StationRecoveryRequestV0, +}; +use orbit_essentials::utils::sha256_hash; + +impl From for StationRecoveryRequestOperation { + fn from(request: upgrader_api::RequestDisasterRecoveryInput) -> Self { + match request { + upgrader_api::RequestDisasterRecoveryInput::InstallCode(install_code) => { + let wasm_sha256 = + if let Some(ref module_extra_chunks) = install_code.module_extra_chunks { + module_extra_chunks.wasm_module_hash.clone() + } else { + sha256_hash(&install_code.module) + }; + StationRecoveryRequestOperation::InstallCode( + StationRecoveryRequestInstallCodeOperation { + install_mode: install_code.install_mode.into(), + wasm_module: install_code.module, + wasm_module_extra_chunks: install_code.module_extra_chunks, + wasm_sha256, + arg_sha256: sha256_hash(&install_code.arg), + arg: install_code.arg, + }, + ) + } + } + } +} + +impl From<&StationRecoveryRequestOperation> for StationRecoveryRequestOperationFootprint { + fn from(operation: &StationRecoveryRequestOperation) -> Self { + match operation { + StationRecoveryRequestOperation::InstallCode(ref install_code) => { + StationRecoveryRequestOperationFootprint::InstallCode( + StationRecoveryRequestInstallCodeOperationFootprint { + install_mode: install_code.install_mode, + wasm_sha256: install_code.wasm_sha256.clone(), + arg_sha256: install_code.arg_sha256.clone(), + }, + ) + } + } + } +} + +impl From<&StationRecoveryRequestOperation> for RequestDisasterRecoveryOperationLog { + fn from(operation: &StationRecoveryRequestOperation) -> Self { + match operation { + StationRecoveryRequestOperation::InstallCode(ref install_code) => { + RequestDisasterRecoveryOperationLog::InstallCode( + RequestDisasterRecoveryInstallCodeLog { + install_mode: install_code.install_mode.to_string(), + wasm_sha256: hex::encode(&install_code.wasm_sha256), + arg_sha256: hex::encode(&install_code.arg_sha256), + }, + ) + } + } + } +} + +impl From<&StationRecoveryRequestOperation> for upgrader_api::StationRecoveryRequestOperation { + fn from(operation: &StationRecoveryRequestOperation) -> Self { + match operation { + StationRecoveryRequestOperation::InstallCode(ref install_code) => { + upgrader_api::StationRecoveryRequestOperation::InstallCode( + upgrader_api::StationRecoveryRequestInstallCodeOperation { + install_mode: install_code.install_mode.into(), + wasm_sha256: install_code.wasm_sha256.clone(), + arg: install_code.arg.clone(), + }, + ) + } + } + } +} + +// legacy types + +impl From for StationRecoveryRequest { + fn from(request: StationRecoveryRequestV0) -> Self { + Self { + user_id: request.user_id, + operation: StationRecoveryRequestOperation::InstallCode( + StationRecoveryRequestInstallCodeOperation { + install_mode: request.install_mode, + wasm_module: request.wasm_module, + wasm_module_extra_chunks: request.wasm_module_extra_chunks, + wasm_sha256: request.wasm_sha256, + arg: request.arg, + arg_sha256: request.arg_sha256, + }, + ), + submitted_at: request.submitted_at, + } + } +} + +impl From for DisasterRecovery { + fn from(disaster_recovery: DisasterRecoveryV0) -> Self { + Self { + accounts: disaster_recovery.accounts, + multi_asset_accounts: disaster_recovery.multi_asset_accounts, + assets: disaster_recovery.assets, + committee: disaster_recovery.committee, + recovery_requests: disaster_recovery + .recovery_requests + .into_iter() + .map(|request| request.into()) + .collect(), + recovery_status: disaster_recovery.recovery_status, + last_recovery_result: disaster_recovery.last_recovery_result, + } + } +} diff --git a/core/upgrader/impl/src/mappers/mod.rs b/core/upgrader/impl/src/mappers/mod.rs new file mode 100644 index 000000000..aff80fbfa --- /dev/null +++ b/core/upgrader/impl/src/mappers/mod.rs @@ -0,0 +1 @@ +mod disaster_recovery; diff --git a/core/upgrader/impl/src/model/disaster_recovery.rs b/core/upgrader/impl/src/model/disaster_recovery.rs index 7725ed117..05440c3d6 100644 --- a/core/upgrader/impl/src/model/disaster_recovery.rs +++ b/core/upgrader/impl/src/model/disaster_recovery.rs @@ -12,7 +12,7 @@ use uuid::Uuid; use crate::utils::HelperMapper; #[storable] -#[derive(Clone, Copy, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] pub enum InstallMode { /// Install the wasm module. Install, @@ -63,10 +63,10 @@ impl From for CanisterInstallMode { } #[storable] -#[derive(Clone, Debug)] -pub struct StationRecoveryRequest { - /// The user ID of the station. - pub user_id: UUID, +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct StationRecoveryRequestInstallCodeOperation { + /// The install mode: upgrade or reinstall. + pub install_mode: InstallMode, /// The wasm module to be installed. #[serde(with = "serde_bytes")] pub wasm_module: Vec, @@ -74,13 +74,38 @@ pub struct StationRecoveryRequest { pub wasm_module_extra_chunks: Option, /// The SHA-256 hash of the wasm module. pub wasm_sha256: Vec, - /// The install mode: upgrade or reinstall. - pub install_mode: InstallMode, /// The install arguments. #[serde(with = "serde_bytes")] pub arg: Vec, /// The SHA-256 hash of the install arguments. pub arg_sha256: Vec, +} + +#[storable] +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum StationRecoveryRequestOperation { + InstallCode(StationRecoveryRequestInstallCodeOperation), +} + +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub struct StationRecoveryRequestInstallCodeOperationFootprint { + pub install_mode: InstallMode, + pub wasm_sha256: Vec, + pub arg_sha256: Vec, +} + +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub enum StationRecoveryRequestOperationFootprint { + InstallCode(StationRecoveryRequestInstallCodeOperationFootprint), +} + +#[storable] +#[derive(Clone, Debug)] +pub struct StationRecoveryRequest { + /// The user ID of the station. + pub user_id: UUID, + /// The disaster recovery operation. + pub operation: StationRecoveryRequestOperation, /// Time in nanoseconds since the UNIX epoch when the request was submitted. pub submitted_at: Timestamp, } @@ -89,9 +114,7 @@ impl From for upgrader_api::StationRecoveryRequest { fn from(value: StationRecoveryRequest) -> Self { upgrader_api::StationRecoveryRequest { user_id: Uuid::from_bytes(value.user_id).hyphenated().to_string(), - wasm_sha256: value.wasm_sha256, - install_mode: upgrader_api::InstallMode::from(value.install_mode), - arg: value.arg, + operation: (&value.operation).into(), submitted_at: timestamp_to_rfc3339(&value.submitted_at), } } @@ -488,6 +511,62 @@ impl From for upgrader_api::GetDisasterRecoveryStateResponse { } } +// legacy types + +#[storable] +#[derive(Clone, Debug)] +pub struct StationRecoveryRequestV0 { + /// The user ID of the station. + pub user_id: UUID, + /// The wasm module to be installed. + #[serde(with = "serde_bytes")] + pub wasm_module: Vec, + /// Optional extra chunks of the wasm module to be installed. + pub wasm_module_extra_chunks: Option, + /// The SHA-256 hash of the wasm module. + pub wasm_sha256: Vec, + /// The install mode: upgrade or reinstall. + pub install_mode: InstallMode, + /// The install arguments. + #[serde(with = "serde_bytes")] + pub arg: Vec, + /// The SHA-256 hash of the install arguments. + pub arg_sha256: Vec, + /// Time in nanoseconds since the UNIX epoch when the request was submitted. + pub submitted_at: Timestamp, +} + +#[storable] +#[derive(Clone, Debug)] +pub struct DisasterRecoveryV0 { + pub accounts: Vec, + + #[serde(default)] + pub multi_asset_accounts: Vec, + #[serde(default)] + pub assets: Vec, + + pub committee: Option, + + pub recovery_requests: Vec, + pub recovery_status: RecoveryStatus, + pub last_recovery_result: Option, +} + +impl Default for DisasterRecoveryV0 { + fn default() -> Self { + DisasterRecoveryV0 { + accounts: vec![], + multi_asset_accounts: vec![], + assets: vec![], + committee: None, + recovery_requests: vec![], + recovery_status: RecoveryStatus::Idle, + last_recovery_result: None, + } + } +} + #[cfg(test)] pub mod tests { use candid::Principal; diff --git a/core/upgrader/impl/src/model/logging.rs b/core/upgrader/impl/src/model/logging.rs index 6ce78bbb5..feb5b0006 100644 --- a/core/upgrader/impl/src/model/logging.rs +++ b/core/upgrader/impl/src/model/logging.rs @@ -29,18 +29,40 @@ pub struct SetAccountsAndAssetsLog { } #[derive(Serialize)] -pub struct RequestDisasterRecoveryLog { - pub user: AdminUser, +pub struct RequestDisasterRecoveryInstallCodeLog { + pub install_mode: String, pub wasm_sha256: String, pub arg_sha256: String, - pub install_mode: String, +} + +#[derive(Serialize)] +pub enum RequestDisasterRecoveryOperationLog { + InstallCode(RequestDisasterRecoveryInstallCodeLog), +} + +impl std::fmt::Display for RequestDisasterRecoveryOperationLog { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + RequestDisasterRecoveryOperationLog::InstallCode(install_code) => { + write!( + f, + "InstallCode with mode {}, wasm hash {}, and arg hash {}", + install_code.install_mode, install_code.wasm_sha256, install_code.arg_sha256 + ) + } + } + } +} + +#[derive(Serialize)] +pub struct RequestDisasterRecoveryLog { + pub user: AdminUser, + pub operation: RequestDisasterRecoveryOperationLog, } #[derive(Serialize)] pub struct DisasterRecoveryStartLog { - pub wasm_sha256: String, - pub arg_sha256: String, - pub install_mode: String, + pub operation: RequestDisasterRecoveryOperationLog, } #[derive(Serialize)] @@ -109,16 +131,14 @@ impl LogEntryType { format!("Set {} disaster recovery account(s)", data.accounts.len(),) } LogEntryType::RequestDisasterRecovery(data) => format!( - "{} requested disaster recovery with wasm hash {} and arg hash {}", + "{} requested disaster recovery with operation {}", data.user.to_summary(), - hex::encode(&data.wasm_sha256), - hex::encode(&data.arg_sha256) + data.operation, ), LogEntryType::DisasterRecoveryStart(data) => format!( - "Disaster recovery successfully initiated to {} station with wasm {}", - data.install_mode, - hex::encode(&data.wasm_sha256) + "Disaster recovery successfully initiated with operation {}", + data.operation, ), LogEntryType::DisasterRecoveryResult(data) => match data.result { RecoveryResult::Success => "Disaster recovery succeeded".to_owned(), diff --git a/core/upgrader/impl/src/services/disaster_recovery.rs b/core/upgrader/impl/src/services/disaster_recovery.rs index 6a97f763e..2c434689f 100644 --- a/core/upgrader/impl/src/services/disaster_recovery.rs +++ b/core/upgrader/impl/src/services/disaster_recovery.rs @@ -6,8 +6,10 @@ use crate::{ Account, AdminUser, Asset, DisasterRecovery, DisasterRecoveryCommittee, DisasterRecoveryInProgressLog, DisasterRecoveryResultLog, DisasterRecoveryStartLog, InstallMode, LogEntryType, MultiAssetAccount, RecoveryEvaluationResult, RecoveryFailure, - RecoveryResult, RecoveryStatus, RequestDisasterRecoveryLog, SetAccountsAndAssetsLog, - SetAccountsLog, SetCommitteeLog, StationRecoveryRequest, + RecoveryResult, RecoveryStatus, RequestDisasterRecoveryLog, + RequestDisasterRecoveryOperationLog, SetAccountsAndAssetsLog, SetAccountsLog, + SetCommitteeLog, StationRecoveryRequest, StationRecoveryRequestOperation, + StationRecoveryRequestOperationFootprint, }, services::LOGGER_SERVICE, set_disaster_recovery, @@ -16,7 +18,7 @@ use crate::{ use candid::Principal; use lazy_static::lazy_static; -use orbit_essentials::{api::ServiceResult, utils::sha256_hash}; +use orbit_essentials::api::ServiceResult; use std::{ collections::{HashMap, HashSet}, sync::Arc, @@ -239,11 +241,12 @@ impl DisasterRecoveryService { .recovery_requests .retain(|request| committee_set.contains(&request.user_id)); - let mut submissions: HashMap<(Vec, Vec), usize> = Default::default(); + let mut submissions: HashMap = + Default::default(); for request in storage.recovery_requests.iter() { - let key = (request.wasm_sha256.clone(), request.arg.clone()); - let entry = submissions.entry(key).or_insert(0); + let request_operation_footprint = (&request.operation).into(); + let entry = submissions.entry(request_operation_footprint).or_insert(0); *entry += 1; @@ -269,11 +272,10 @@ impl DisasterRecoveryService { ) { let mut value = storage.get(); + let operation_log: RequestDisasterRecoveryOperationLog = (&request.operation).into(); logger.log(LogEntryType::DisasterRecoveryStart( DisasterRecoveryStartLog { - wasm_sha256: hex::encode(&request.wasm_sha256), - arg_sha256: hex::encode(&request.arg_sha256), - install_mode: request.install_mode.to_string(), + operation: operation_log, }, )); @@ -292,35 +294,39 @@ impl DisasterRecoveryService { logger: logger.clone(), }; - // only stop for upgrade - if request.install_mode == InstallMode::Upgrade { - if let Err(err) = installer.stop(station_canister_id).await { - ic_cdk::print(err); - } - } - - match installer - .install( - station_canister_id, - request.wasm_module, - request.wasm_module_extra_chunks, - request.arg, - request.install_mode, - ) - .await - { - Ok(_) => { - releaser.result = Some(RecoveryResult::Success); - } - Err(reason) => { - releaser.result = Some(RecoveryResult::Failure(RecoveryFailure { reason })); - } - } - - // only start for upgrade - if request.install_mode == InstallMode::Upgrade { - if let Err(err) = installer.start(station_canister_id).await { - ic_cdk::print(err); + match request.operation { + StationRecoveryRequestOperation::InstallCode(install_code) => { + // only stop for upgrade + if install_code.install_mode == InstallMode::Upgrade { + if let Err(err) = installer.stop(station_canister_id).await { + ic_cdk::print(err); + } + } + + match installer + .install( + station_canister_id, + install_code.wasm_module, + install_code.wasm_module_extra_chunks, + install_code.arg, + install_code.install_mode, + ) + .await + { + Ok(_) => { + releaser.result = Some(RecoveryResult::Success); + } + Err(reason) => { + releaser.result = Some(RecoveryResult::Failure(RecoveryFailure { reason })); + } + } + + // only start for upgrade + if install_code.install_mode == InstallMode::Upgrade { + if let Err(err) = installer.start(station_canister_id).await { + ic_cdk::print(err); + } + } } } } @@ -333,20 +339,12 @@ impl DisasterRecoveryService { let mut value = self.storage.get(); if let Some(committee_member) = self.get_committee_member(caller) { - let wasm_sha256 = if let Some(ref module_extra_chunks) = request.module_extra_chunks { - module_extra_chunks.wasm_module_hash.clone() - } else { - sha256_hash(&request.module) - }; + let operation: StationRecoveryRequestOperation = request.into(); + let operation_log: RequestDisasterRecoveryOperationLog = (&operation).into(); let recovery_request = StationRecoveryRequest { user_id: committee_member.id, - wasm_sha256, - wasm_module: request.module, - wasm_module_extra_chunks: request.module_extra_chunks, - arg_sha256: sha256_hash(&request.arg), - arg: request.arg, + operation, submitted_at: time(), - install_mode: request.install_mode.into(), }; // check if user had previous recovery request @@ -365,9 +363,7 @@ impl DisasterRecoveryService { self.logger.log(LogEntryType::RequestDisasterRecovery( RequestDisasterRecoveryLog { user: committee_member, - wasm_sha256: hex::encode(&recovery_request.wasm_sha256), - arg_sha256: hex::encode(&recovery_request.arg_sha256), - install_mode: recovery_request.install_mode.to_string(), + operation: operation_log, }, )); } @@ -401,10 +397,11 @@ mod tests { model::{ tests::{mock_accounts, mock_assets, mock_committee, mock_multi_asset_accounts}, InstallMode, RecoveryEvaluationResult, RecoveryResult, RecoveryStatus, - StationRecoveryRequest, + StationRecoveryRequest, StationRecoveryRequestInstallCodeOperation, }, services::{ - DisasterRecoveryService, DisasterRecoveryStorage, InstallCanister, LoggerService, + disaster_recovery::StationRecoveryRequestOperation, DisasterRecoveryService, + DisasterRecoveryStorage, InstallCanister, LoggerService, }, }; @@ -498,24 +495,28 @@ mod tests { // non committee member dr.request_recovery( Principal::from_slice(&[0; 29]), - upgrader_api::RequestDisasterRecoveryInput { - arg: vec![1, 2, 3], - module: vec![4, 5, 6], - module_extra_chunks: None, - install_mode: upgrader_api::InstallMode::Upgrade, - }, + upgrader_api::RequestDisasterRecoveryInput::InstallCode( + upgrader_api::RequestDisasterRecoveryInstallCodeInput { + arg: vec![1, 2, 3], + module: vec![4, 5, 6], + module_extra_chunks: None, + install_mode: upgrader_api::InstallMode::Upgrade, + }, + ), ); assert!(dr.storage.get().recovery_requests.is_empty()); // committee member dr.request_recovery( Principal::from_slice(&[1; 29]), - upgrader_api::RequestDisasterRecoveryInput { - arg: vec![1, 2, 3], - module: vec![4, 5, 6], - module_extra_chunks: None, - install_mode: upgrader_api::InstallMode::Upgrade, - }, + upgrader_api::RequestDisasterRecoveryInput::InstallCode( + upgrader_api::RequestDisasterRecoveryInstallCodeInput { + arg: vec![1, 2, 3], + module: vec![4, 5, 6], + module_extra_chunks: None, + install_mode: upgrader_api::InstallMode::Upgrade, + }, + ), ); assert!(dr.storage.get().recovery_requests.len() == 1); @@ -526,12 +527,14 @@ mod tests { // committee member to submit different request dr.request_recovery( Principal::from_slice(&[2; 29]), - upgrader_api::RequestDisasterRecoveryInput { - arg: vec![0, 0, 0], - module: vec![4, 5, 6], - module_extra_chunks: None, - install_mode: upgrader_api::InstallMode::Upgrade, - }, + upgrader_api::RequestDisasterRecoveryInput::InstallCode( + upgrader_api::RequestDisasterRecoveryInstallCodeInput { + arg: vec![0, 0, 0], + module: vec![4, 5, 6], + module_extra_chunks: None, + install_mode: upgrader_api::InstallMode::Upgrade, + }, + ), ); assert!(dr.storage.get().recovery_requests.len() == 2); @@ -540,22 +543,26 @@ mod tests { // 3rd committee member to submit same request as first dr.request_recovery( Principal::from_slice(&[3; 29]), - upgrader_api::RequestDisasterRecoveryInput { - arg: vec![1, 2, 3], - module: vec![4, 5, 6], - module_extra_chunks: None, - install_mode: upgrader_api::InstallMode::Upgrade, - }, + upgrader_api::RequestDisasterRecoveryInput::InstallCode( + upgrader_api::RequestDisasterRecoveryInstallCodeInput { + arg: vec![1, 2, 3], + module: vec![4, 5, 6], + module_extra_chunks: None, + install_mode: upgrader_api::InstallMode::Upgrade, + }, + ), ); assert!(dr.storage.get().recovery_requests.len() == 3); // evaluation results in met DR condition match dr.evaluate_requests() { - RecoveryEvaluationResult::Met(request) => { - assert_eq!(request.arg, vec![1, 2, 3]); - assert_eq!(request.wasm_module, vec![4, 5, 6]); - } + RecoveryEvaluationResult::Met(request) => match request.operation { + StationRecoveryRequestOperation::InstallCode(install_code) => { + assert_eq!(install_code.arg, vec![1, 2, 3]); + assert_eq!(install_code.wasm_module, vec![4, 5, 6]); + } + }, _ => panic!("Unexpected result"), }; @@ -567,14 +574,19 @@ mod tests { async fn test_do_recovery() { let storage: DisasterRecoveryStorage = Default::default(); let logger = Arc::new(LoggerService::default()); + let operation = StationRecoveryRequestOperation::InstallCode( + StationRecoveryRequestInstallCodeOperation { + install_mode: InstallMode::Reinstall, + wasm_module: vec![1, 2, 3], + wasm_module_extra_chunks: None, + wasm_sha256: vec![4, 5, 6], + arg: vec![7, 8, 9], + arg_sha256: vec![10, 11, 12], + }, + ); let recovery_request = StationRecoveryRequest { user_id: [1; 16], - wasm_module: vec![1, 2, 3], - wasm_module_extra_chunks: None, - wasm_sha256: vec![4, 5, 6], - install_mode: InstallMode::Reinstall, - arg: vec![7, 8, 9], - arg_sha256: vec![10, 11, 12], + operation, submitted_at: 0, }; @@ -644,14 +656,19 @@ mod tests { async fn test_failing_do_recovery_with_panicking_install() { let storage: DisasterRecoveryStorage = Default::default(); let logger = Arc::new(LoggerService::default()); + let operation = StationRecoveryRequestOperation::InstallCode( + StationRecoveryRequestInstallCodeOperation { + install_mode: InstallMode::Reinstall, + wasm_module: vec![1, 2, 3], + wasm_module_extra_chunks: None, + wasm_sha256: vec![4, 5, 6], + arg: vec![7, 8, 9], + arg_sha256: vec![10, 11, 12], + }, + ); let recovery_request = StationRecoveryRequest { user_id: [1; 16], - wasm_module: vec![1, 2, 3], - wasm_module_extra_chunks: None, - wasm_sha256: vec![4, 5, 6], - install_mode: InstallMode::Reinstall, - arg: vec![7, 8, 9], - arg_sha256: vec![10, 11, 12], + operation, submitted_at: 0, }; diff --git a/libs/orbit-essentials/src/types.rs b/libs/orbit-essentials/src/types.rs index d1b469da7..e6e41e969 100644 --- a/libs/orbit-essentials/src/types.rs +++ b/libs/orbit-essentials/src/types.rs @@ -1,7 +1,9 @@ use candid::{CandidType, Deserialize, Principal}; use serde::Serialize; -#[derive(CandidType, Deserialize, Serialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] +#[derive( + CandidType, Deserialize, Serialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, +)] pub struct WasmModuleExtraChunks { pub store_canister: Principal, pub extra_chunks_key: String, diff --git a/tests/integration/src/disaster_recovery_tests.rs b/tests/integration/src/disaster_recovery_tests.rs index 5978f6fdf..907d21cef 100644 --- a/tests/integration/src/disaster_recovery_tests.rs +++ b/tests/integration/src/disaster_recovery_tests.rs @@ -322,22 +322,26 @@ fn test_disaster_recovery_flow() { .expect("No module hash found"); // install the upgrader wasm for the station as a test - let good_request = upgrader_api::RequestDisasterRecoveryInput { - module: base_chunk.clone(), - module_extra_chunks: Some(module_extra_chunks.clone()), - arg: Encode!(&upgrader_api::InitArg { - target_canister: canister_ids.station - }) - .unwrap(), - install_mode: upgrader_api::InstallMode::Reinstall, - }; + let good_request = upgrader_api::RequestDisasterRecoveryInput::InstallCode( + upgrader_api::RequestDisasterRecoveryInstallCodeInput { + install_mode: upgrader_api::InstallMode::Reinstall, + module: base_chunk.clone(), + module_extra_chunks: Some(module_extra_chunks.clone()), + arg: Encode!(&upgrader_api::InitArg { + target_canister: canister_ids.station + }) + .unwrap(), + }, + ); - let bad_request = upgrader_api::RequestDisasterRecoveryInput { - module: base_chunk, - module_extra_chunks: Some(module_extra_chunks), - arg: vec![1, 2, 3], - install_mode: upgrader_api::InstallMode::Reinstall, - }; + let bad_request = upgrader_api::RequestDisasterRecoveryInput::InstallCode( + upgrader_api::RequestDisasterRecoveryInstallCodeInput { + module: base_chunk, + module_extra_chunks: Some(module_extra_chunks), + arg: vec![1, 2, 3], + install_mode: upgrader_api::InstallMode::Reinstall, + }, + ); request_disaster_recovery(&env, upgrader_id, WALLET_ADMIN_USER, good_request.clone()) .expect("Failed to request disaster recovery"); @@ -493,34 +497,36 @@ fn test_disaster_recovery_flow_recreates_same_accounts() { &env, upgrader_id, WALLET_ADMIN_USER, - upgrader_api::RequestDisasterRecoveryInput { - module: base_chunk, - module_extra_chunks: Some(module_extra_chunks), - arg: Encode!(&station_api::SystemInstall::Init(station_api::SystemInit { - name: "Station".to_string(), - admins: vec![ - station_api::AdminInitInput { - identity: WALLET_ADMIN_USER, - name: "updated-admin-name".to_string(), - }, - station_api::AdminInitInput { - identity: Principal::from_slice(&[95; 29]), - name: "another-admin".to_string(), - }, - station_api::AdminInitInput { - identity: Principal::from_slice(&[97; 29]), - name: "yet-another-admin".to_string(), - } - ], - quorum: None, - fallback_controller: None, - upgrader: station_api::SystemUpgraderInput::Id(upgrader_id), - accounts: Some(init_accounts_input), - assets: Some(vec![init_assets_input]), - })) - .unwrap(), - install_mode: upgrader_api::InstallMode::Reinstall, - }, + upgrader_api::RequestDisasterRecoveryInput::InstallCode( + upgrader_api::RequestDisasterRecoveryInstallCodeInput { + module: base_chunk, + module_extra_chunks: Some(module_extra_chunks), + arg: Encode!(&station_api::SystemInstall::Init(station_api::SystemInit { + name: "Station".to_string(), + admins: vec![ + station_api::AdminInitInput { + identity: WALLET_ADMIN_USER, + name: "updated-admin-name".to_string(), + }, + station_api::AdminInitInput { + identity: Principal::from_slice(&[95; 29]), + name: "another-admin".to_string(), + }, + station_api::AdminInitInput { + identity: Principal::from_slice(&[97; 29]), + name: "yet-another-admin".to_string(), + } + ], + quorum: None, + fallback_controller: None, + upgrader: station_api::SystemUpgraderInput::Id(upgrader_id), + accounts: Some(init_accounts_input), + assets: Some(vec![init_assets_input]), + })) + .unwrap(), + install_mode: upgrader_api::InstallMode::Reinstall, + }, + ), ) .expect("Unexpected failed to request disaster recovery"); @@ -674,24 +680,26 @@ fn test_disaster_recovery_flow_reuses_same_upgrader() { &env, upgrader_id, WALLET_ADMIN_USER, - upgrader_api::RequestDisasterRecoveryInput { - module: base_chunk, - module_extra_chunks: Some(module_extra_chunks), - arg: Encode!(&station_api::SystemInstall::Init(station_api::SystemInit { - name: "Station".to_string(), - admins: vec![station_api::AdminInitInput { - identity: WALLET_ADMIN_USER, - name: "updated-admin-name".to_string(), - }], - quorum: None, - fallback_controller: Some(fallback_controller), - upgrader: station_api::SystemUpgraderInput::Id(upgrader_id), - accounts: None, - assets: None, - })) - .unwrap(), - install_mode: upgrader_api::InstallMode::Reinstall, - }, + upgrader_api::RequestDisasterRecoveryInput::InstallCode( + upgrader_api::RequestDisasterRecoveryInstallCodeInput { + module: base_chunk, + module_extra_chunks: Some(module_extra_chunks), + arg: Encode!(&station_api::SystemInstall::Init(station_api::SystemInit { + name: "Station".to_string(), + admins: vec![station_api::AdminInitInput { + identity: WALLET_ADMIN_USER, + name: "updated-admin-name".to_string(), + }], + quorum: None, + fallback_controller: Some(fallback_controller), + upgrader: station_api::SystemUpgraderInput::Id(upgrader_id), + accounts: None, + assets: None, + })) + .unwrap(), + install_mode: upgrader_api::InstallMode::Reinstall, + }, + ), ) .expect("Unexpected failed to request disaster recovery"); @@ -757,15 +765,17 @@ fn test_disaster_recovery_in_progress() { upload_canister_chunks_to_asset_canister(&env, new_wasm_module, 50_000); // install the upgrader wasm for the station as a test - let good_request = upgrader_api::RequestDisasterRecoveryInput { - module: base_chunk, - module_extra_chunks: Some(module_extra_chunks), - arg: Encode!(&upgrader_api::InitArg { - target_canister: canister_ids.station - }) - .unwrap(), - install_mode: upgrader_api::InstallMode::Reinstall, - }; + let good_request = upgrader_api::RequestDisasterRecoveryInput::InstallCode( + upgrader_api::RequestDisasterRecoveryInstallCodeInput { + module: base_chunk, + module_extra_chunks: Some(module_extra_chunks), + arg: Encode!(&upgrader_api::InitArg { + target_canister: canister_ids.station + }) + .unwrap(), + install_mode: upgrader_api::InstallMode::Reinstall, + }, + ); request_disaster_recovery(&env, upgrader_id, WALLET_ADMIN_USER, good_request.clone()) .expect("Failed to request disaster recovery"); @@ -836,15 +846,17 @@ fn test_disaster_recovery_install() { let new_wasm_module = get_canister_wasm("upgrader"); let (base_chunk, module_extra_chunks) = upload_canister_chunks_to_asset_canister(&env, new_wasm_module, 50_000); - let good_request = upgrader_api::RequestDisasterRecoveryInput { - module: base_chunk, - module_extra_chunks: Some(module_extra_chunks), - arg: Encode!(&upgrader_api::InitArg { - target_canister: canister_ids.station - }) - .unwrap(), - install_mode: upgrader_api::InstallMode::Install, - }; + let good_request = upgrader_api::RequestDisasterRecoveryInput::InstallCode( + upgrader_api::RequestDisasterRecoveryInstallCodeInput { + module: base_chunk, + module_extra_chunks: Some(module_extra_chunks), + arg: Encode!(&upgrader_api::InitArg { + target_canister: canister_ids.station + }) + .unwrap(), + install_mode: upgrader_api::InstallMode::Install, + }, + ); request_disaster_recovery(&env, upgrader_id, WALLET_ADMIN_USER, good_request.clone()) .expect("Failed to request disaster recovery"); @@ -865,12 +877,14 @@ fn test_disaster_recovery_upgrade() { let new_wasm_module = get_canister_wasm("station"); let (base_chunk, module_extra_chunks) = upload_canister_chunks_to_asset_canister(&env, new_wasm_module, 500_000); - let good_request = upgrader_api::RequestDisasterRecoveryInput { - module: base_chunk, - module_extra_chunks: Some(module_extra_chunks), - arg: Encode!(&station_init_arg).unwrap(), - install_mode: upgrader_api::InstallMode::Upgrade, - }; + let good_request = upgrader_api::RequestDisasterRecoveryInput::InstallCode( + upgrader_api::RequestDisasterRecoveryInstallCodeInput { + module: base_chunk, + module_extra_chunks: Some(module_extra_chunks), + arg: Encode!(&station_init_arg).unwrap(), + install_mode: upgrader_api::InstallMode::Upgrade, + }, + ); request_disaster_recovery(&env, upgrader_id, WALLET_ADMIN_USER, good_request.clone()) .expect("Failed to request disaster recovery"); @@ -902,12 +916,14 @@ fn test_disaster_recovery_failing() { let new_wasm_module = get_canister_wasm("station"); let (base_chunk, module_extra_chunks) = upload_canister_chunks_to_asset_canister(&env, new_wasm_module, 500_000); - let good_request = upgrader_api::RequestDisasterRecoveryInput { - module: base_chunk, - module_extra_chunks: Some(module_extra_chunks), - arg: Encode!(&arg).unwrap(), - install_mode: upgrader_api::InstallMode::Upgrade, - }; + let good_request = upgrader_api::RequestDisasterRecoveryInput::InstallCode( + upgrader_api::RequestDisasterRecoveryInstallCodeInput { + module: base_chunk, + module_extra_chunks: Some(module_extra_chunks), + arg: Encode!(&arg).unwrap(), + install_mode: upgrader_api::InstallMode::Upgrade, + }, + ); request_disaster_recovery(&env, upgrader_id, WALLET_ADMIN_USER, good_request.clone()) .expect("Failed to request disaster recovery"); @@ -999,12 +1015,14 @@ fn test_disaster_recovery_committee_change_with_open_requests() { identities: vec![Principal::from_slice(&i.to_le_bytes())], }) .collect(); - let request = upgrader_api::RequestDisasterRecoveryInput { - module: vec![], - module_extra_chunks: None, - arg: vec![], - install_mode: upgrader_api::InstallMode::Reinstall, - }; + let request = upgrader_api::RequestDisasterRecoveryInput::InstallCode( + upgrader_api::RequestDisasterRecoveryInstallCodeInput { + module: vec![], + module_extra_chunks: None, + arg: vec![], + install_mode: upgrader_api::InstallMode::Reinstall, + }, + ); let disaster_recovery = |i: usize| { request_disaster_recovery( &env, diff --git a/tests/integration/src/upgrader_migration_tests.rs b/tests/integration/src/upgrader_migration_tests.rs index 12937e7cf..edd50a531 100644 --- a/tests/integration/src/upgrader_migration_tests.rs +++ b/tests/integration/src/upgrader_migration_tests.rs @@ -117,12 +117,14 @@ where get_disaster_recovery_committee(&env, upgrader_id, canister_ids.station).unwrap(); for (i, user) in committee.users.into_iter().take(20).enumerate() { let wasm_module = vec![i as u8; 2_000_000]; - let large_request = upgrader_api::RequestDisasterRecoveryInput { - module: wasm_module, - module_extra_chunks: None, - arg: vec![], - install_mode: upgrader_api::InstallMode::Reinstall, - }; + let large_request = upgrader_api::RequestDisasterRecoveryInput::InstallCode( + upgrader_api::RequestDisasterRecoveryInstallCodeInput { + module: wasm_module, + module_extra_chunks: None, + arg: vec![], + install_mode: upgrader_api::InstallMode::Reinstall, + }, + ); request_disaster_recovery( &env, upgrader_id, diff --git a/tests/integration/src/upgrader_test_data.rs b/tests/integration/src/upgrader_test_data.rs index 6a41a6eb7..a820737ec 100644 --- a/tests/integration/src/upgrader_test_data.rs +++ b/tests/integration/src/upgrader_test_data.rs @@ -7,6 +7,7 @@ use time::OffsetDateTime; use upgrader_api::{ Account, AdminUser, Asset, DisasterRecoveryCommittee, LogEntry, MetadataDTO, MultiAssetAccount, RecoveryResult, RecoveryStatus, StationRecoveryRequest, + StationRecoveryRequestInstallCodeOperation, StationRecoveryRequestOperation, }; use uuid::Uuid; @@ -177,12 +178,16 @@ impl<'a> UpgraderDataGenerator<'a> { let wasm_sha256 = orbit_essentials::utils::sha256_hash(&wasm_module); let (base_chunk, module_extra_chunks) = upload_canister_chunks_to_asset_canister(self.env, wasm_module, 4); - let request = upgrader_api::RequestDisasterRecoveryInput { - module: base_chunk, - module_extra_chunks: Some(module_extra_chunks), - arg: next_unique_uuid().as_bytes().to_vec(), - install_mode: upgrader_api::InstallMode::Reinstall, - }; + let arg = next_unique_uuid().as_bytes().to_vec(); + let install_mode = upgrader_api::InstallMode::Reinstall; + let request = upgrader_api::RequestDisasterRecoveryInput::InstallCode( + upgrader_api::RequestDisasterRecoveryInstallCodeInput { + module: base_chunk, + module_extra_chunks: Some(module_extra_chunks), + arg: arg.clone(), + install_mode: install_mode.clone(), + }, + ); let state = get_disaster_recovery_state(self.env, self.upgrader_id, self.some_committee_member()); if state.last_recovery_result.is_none() { @@ -226,9 +231,13 @@ impl<'a> UpgraderDataGenerator<'a> { .unwrap(); let recovery_request = StationRecoveryRequest { user_id: self.committee.as_ref().unwrap().users[i].id.clone(), - wasm_sha256: wasm_sha256.clone(), - install_mode: request.install_mode.clone(), - arg: request.arg.clone(), + operation: StationRecoveryRequestOperation::InstallCode( + StationRecoveryRequestInstallCodeOperation { + wasm_sha256: wasm_sha256.clone(), + install_mode: install_mode.clone(), + arg: arg.clone(), + }, + ), submitted_at: pic_time_to_rfc3339(self.env), }; self.recovery_requests.push(recovery_request); @@ -240,25 +249,33 @@ impl<'a> UpgraderDataGenerator<'a> { for i in num_small_requests..(num_small_requests + num_large_requests) { let wasm_module = vec![i as u8; 2_000_000]; let wasm_sha256 = orbit_essentials::utils::sha256_hash(&wasm_module); - let large_request = upgrader_api::RequestDisasterRecoveryInput { - module: wasm_module, - module_extra_chunks: None, - arg: vec![], - install_mode: upgrader_api::InstallMode::Reinstall, - }; + let arg = vec![]; + let install_mode = upgrader_api::InstallMode::Reinstall; + let large_request = upgrader_api::RequestDisasterRecoveryInput::InstallCode( + upgrader_api::RequestDisasterRecoveryInstallCodeInput { + module: wasm_module, + module_extra_chunks: None, + arg: arg.clone(), + install_mode: install_mode.clone(), + }, + ); let committee_member = &self.committee.as_ref().unwrap().users[i]; request_disaster_recovery( self.env, self.upgrader_id, *committee_member.identities.first().unwrap(), - large_request.clone(), + large_request, ) .unwrap(); let recovery_request = StationRecoveryRequest { user_id: committee_member.id.clone(), - wasm_sha256, - install_mode: large_request.install_mode, - arg: large_request.arg, + operation: StationRecoveryRequestOperation::InstallCode( + StationRecoveryRequestInstallCodeOperation { + wasm_sha256, + install_mode, + arg, + }, + ), submitted_at: pic_time_to_rfc3339(self.env), }; self.recovery_requests.push(recovery_request); @@ -297,6 +314,30 @@ impl<'a> UpgraderDataGenerator<'a> { ); let logs = get_all_upgrader_logs(self.env, &self.upgrader_id, &self.some_committee_member()); - assert_eq!(logs, self.logs); + assert_eq!(logs.len(), self.logs.len()); + for (i, log) in logs.iter().enumerate() { + assert_eq!(log.time, self.logs[i].time); + assert_eq!(log.entry_type, self.logs[i].entry_type); + // we made a breaking change to the log message format + if log.message != self.logs[i].message { + assert!( + log.message + .contains("requested disaster recovery with wasm hash") + || log + .message + .contains("Disaster recovery successfully initiated to") + ); + assert!( + self.logs[i] + .message + .contains("requested disaster recovery with operation") + || self.logs[i] + .message + .contains("Disaster recovery successfully initiated with operation") + ); + } else { + assert_eq!(log.data_json, self.logs[i].data_json); + } + } } }