diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c700e6ad8..f0ad4253f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ # UNRELEASED +### feat: support for canister ID migration + +Canister ID migration can be performed using `dfx canister migrate-id` +and its status can be checked out using `dfx canister migration-status`. + # 0.30.2 ### Improve frontend canister sync logic diff --git a/Cargo.lock b/Cargo.lock index 46cf27d959..86260c089b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1663,8 +1663,8 @@ dependencies = [ "ic-asset", "ic-cdk", "ic-identity-hsm", - "ic-management-canister-types 0.4.1", - "ic-utils 0.44.2", + "ic-management-canister-types 0.5.0", + "ic-utils 0.44.3", "ic-wasm", "icrc-ledger-types", "idl2json", @@ -1742,7 +1742,7 @@ dependencies = [ "humantime-serde", "ic-agent", "ic-identity-hsm", - "ic-utils 0.44.2", + "ic-utils 0.44.3", "itertools 0.10.5", "k256 0.11.6", "keyring", @@ -2860,9 +2860,8 @@ dependencies = [ [[package]] name = "ic-agent" -version = "0.44.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6777a6893e52254747abfd0a218aeafc03ada83faa9bff67ee8801739241b5ca" +version = "0.44.3" +source = "git+https://github.com/dfinity/agent-rs?rev=57befac12a96a90868744f48660f31aff3835739#57befac12a96a90868744f48660f31aff3835739" dependencies = [ "arc-swap", "async-channel", @@ -2884,7 +2883,7 @@ dependencies = [ "http-body-util", "ic-certification 3.0.3 (registry+https://github.com/rust-lang/crates.io-index)", "ic-ed25519", - "ic-transport-types 0.44.2", + "ic-transport-types 0.44.3", "ic-verify-bls-signature", "k256 0.13.4", "leb128", @@ -2923,7 +2922,7 @@ dependencies = [ "globset", "hex", "ic-agent", - "ic-utils 0.44.2", + "ic-utils 0.44.3", "itertools 0.10.5", "json5", "mime", @@ -3370,9 +3369,8 @@ dependencies = [ [[package]] name = "ic-identity-hsm" -version = "0.44.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14ea5968ef808815e917f9a8bfab2bb2c7fbe6e822911276128295b16ce2bbff" +version = "0.44.3" +source = "git+https://github.com/dfinity/agent-rs?rev=57befac12a96a90868744f48660f31aff3835739#57befac12a96a90868744f48660f31aff3835739" dependencies = [ "hex", "ic-agent", @@ -3530,9 +3528,8 @@ dependencies = [ [[package]] name = "ic-transport-types" -version = "0.44.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "827e8e1da3e75382137f552b088c124618e770974e670fc4f40add86858a4743" +version = "0.44.3" +source = "git+https://github.com/dfinity/agent-rs?rev=57befac12a96a90868744f48660f31aff3835739#57befac12a96a90868744f48660f31aff3835739" dependencies = [ "candid", "hex", @@ -3601,15 +3598,14 @@ dependencies = [ [[package]] name = "ic-utils" -version = "0.44.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b28a3fdfbf4bf4ef470a4d3980f823db299a2e021819171266827d9725efea5a" +version = "0.44.3" +source = "git+https://github.com/dfinity/agent-rs?rev=57befac12a96a90868744f48660f31aff3835739#57befac12a96a90868744f48660f31aff3835739" dependencies = [ "async-trait", "candid", "futures-util", "ic-agent", - "ic-management-canister-types 0.4.1", + "ic-management-canister-types 0.5.0", "once_cell", "semver", "serde", @@ -3619,7 +3615,6 @@ dependencies = [ "strum_macros 0.26.4", "thiserror 2.0.12", "time", - "tokio", ] [[package]] @@ -3838,7 +3833,7 @@ dependencies = [ "humantime", "ic-agent", "ic-asset", - "ic-utils 0.44.2", + "ic-utils 0.44.3", "libflate 1.4.0", "num-traits", "pem 1.1.1", diff --git a/Cargo.toml b/Cargo.toml index b322d2efbb..c971f8ec45 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,12 +26,12 @@ future_not_send = "warn" candid = "0.10.18" candid_parser = "0.2.4" dfx-core = { path = "src/dfx-core", version = "0.2.0" } -ic-agent = "0.44.2" +ic-agent = { git = "https://github.com/dfinity/agent-rs", rev = "57befac12a96a90868744f48660f31aff3835739" } ic-asset = { path = "src/canisters/frontend/ic-asset", version = "0.25.0" } ic-cdk = "0.19.0-beta.2" -ic-identity-hsm = "0.44.2" -ic-utils = "0.44.2" -ic-management-canister-types = "0.4.1" +ic-identity-hsm = { git = "https://github.com/dfinity/agent-rs", rev = "57befac12a96a90868744f48660f31aff3835739" } +ic-utils = { git = "https://github.com/dfinity/agent-rs", rev = "57befac12a96a90868744f48660f31aff3835739" } +ic-management-canister-types = "0.5.0" aes-gcm = { version = "0.10.3", features = ["std"] } anyhow = "1.0.56" diff --git a/docs/cli-reference/dfx-canister.mdx b/docs/cli-reference/dfx-canister.mdx index 2033b71f1d..80de1bdc79 100644 --- a/docs/cli-reference/dfx-canister.mdx +++ b/docs/cli-reference/dfx-canister.mdx @@ -34,6 +34,8 @@ For reference information and examples that illustrate using `dfx canister` comm | [`install`](#dfx-canister-install) | Installs compiled code in a canister. | | [`logs`](#dfx-canister-logs) | Returns the logs from a canister. | | [`metadata`](#dfx-canister-metadata) | Displays metadata of a canister. | +| [`migrate-id`](#dfx-canister-migrate-id) | Performs canister ID migration. | +| [`migration-status`](#dfx-canister-migration-status) | Displays the current status for a canister ID migration. | | [`request-status`](#dfx-canister-request-status) | Requests the status of a call to a canister. | | [`send`](#dfx-canister-send) | Send a previously-signed message. | | [`set-id`](#dfx-canister-id) | Sets the identifier of a canister. | @@ -718,6 +720,88 @@ service : { } ``` +## dfx canister migrate-id + +Use the `dfx canister migrate-id` command to perform canister ID migration +of a canister on one subnet (called the "migrated" canister) +to another subnet replacing the canister ID of a canister on that other subnet. + +### Basic usage + +``` bash +dfx canister migrate-id --replace +``` + +### Arguments + +You can use the following arguments with the `dfx canister migrate-id` command. + +| Argument | Description | +|-----------------|----------------------------------------------------------------------------------------------------------| +| `canister` | Specifies the name or id of the canister whose canister ID you want to migrate. | +| `replace` | Specifies the name or id of the canister whose canister ID will be replaced by the migrated canister ID. | + +### Examples + +To migrate the canister ID of the canister called `migrated` and +replace the canister ID of the canister called `replaced`, +you can run the following command: + +```bash +$ dfx canister migrate-id migrated --replace replaced +``` + +The command displays output similar to the following: + +``` +WARNING! +Canister 'migrated' will be removed from its own subnet. Continue? +Do you want to proceed? yes/No +yes +Migration succeeded at 2025-11-26 08:57:41 UTC +``` + +## dfx canister migration-status + +Use the `dfx canister migration-status` command to display the current status +for a canister ID migration (triggered by a separate command [`migrate-id`](#dfx-canister-migrate-id)) +of a canister on one subnet (called the "migrated" canister) +to another subnet replacing the canister ID of a canister on that other subnet. + +### Basic usage + +``` bash +dfx canister migration-status --replace +``` + +### Arguments + +You can use the following arguments with the `dfx canister migration-status` command. + +| Argument | Description | +|-----------------|----------------------------------------------------------------------------------------------------------| +| `canister` | Specifies the name or id of the canister whose canister ID you want to migrate. | +| `replace` | Specifies the name or id of the canister whose canister ID will be replaced by the migrated canister ID. | + +### Examples + +To display the current status for a canister ID migration +of the canister called `migrated` and +replacing the canister ID of the canister called `replaced`, +you can run the following command: + +```bash +$ dfx canister migration-status migrated --replace replaced +``` + +The command displays output similar to the following: + +``` +| Canister | Canister To Be Replaced | Migration Status | +| --------------------------- | --------------------------- | ---------------------------- | +| uqqxf-5h777-77774-qaaaa-cai | ahree-maaaa-aaaar-q777q-cai | In progress: MigratedDeleted | +``` + ## dfx canister request-status Use the `dfx canister request-status` command to request the status of a call to a canister. This command diff --git a/e2e/tests-dfx/canister_migration.bash b/e2e/tests-dfx/canister_migration.bash new file mode 100755 index 0000000000..080d694595 --- /dev/null +++ b/e2e/tests-dfx/canister_migration.bash @@ -0,0 +1,41 @@ +#!/usr/bin/env bats + +load ../utils/_ + +setup() { + standard_setup + dfx_new hello +} + +teardown() { + dfx_stop + standard_teardown +} + +@test "canister migrate canister id" { + dfx_start --system-canisters + install_asset counter + + # Update dfx.json: rename hello_backend -> migrated, and add replaced canister + jq '.canisters.migrated = .canisters.hello_backend | del(.canisters.hello_backend)' dfx.json | sponge dfx.json + jq '.canisters.replaced = { "main": "counter.mo", "type": "motoko" }' dfx.json | sponge dfx.json + + # Deploy the migrated canister to the application subnet. + dfx deploy migrated + + # Create the replaced canister on the fiduciary subnet. + dfx canister create replaced --subnet-type fiduciary + + dfx canister stop migrated + dfx canister stop replaced + + # Make sure the migrated canister has enough cycles to do the migration. + dfx ledger fabricate-cycles --canister migrated --cycles 10000000000000 + + # The migration will take a few minutes to complete. + assert_command dfx canister migrate-id migrated --replace replaced --yes + assert_contains "Migration succeeded" + + assert_command dfx canister status migrated + assert_command_fail dfx canister status replaced +} diff --git a/src/dfx/src/actors/pocketic.rs b/src/dfx/src/actors/pocketic.rs index 638de5b1dd..fb24bc672d 100644 --- a/src/dfx/src/actors/pocketic.rs +++ b/src/dfx/src/actors/pocketic.rs @@ -394,7 +394,7 @@ async fn initialize_pocketic( ii: Some(IcpFeaturesConfig::default()), nns_ui: Some(IcpFeaturesConfig::default()), bitcoin: icp_features.bitcoin, - canister_migration: None, + canister_migration: Some(IcpFeaturesConfig::default()), dogecoin: icp_features.dogecoin, } } else { diff --git a/src/dfx/src/commands/canister/migrate_id.rs b/src/dfx/src/commands/canister/migrate_id.rs new file mode 100644 index 0000000000..92a42aa74b --- /dev/null +++ b/src/dfx/src/commands/canister/migrate_id.rs @@ -0,0 +1,224 @@ +use crate::lib::environment::Environment; +use crate::lib::error::DfxResult; +use crate::lib::ic_attributes::CanisterSettings; +use crate::lib::operations::canister::{ + get_canister_status, list_canister_snapshots, update_settings, +}; +use crate::lib::operations::canister_migration::{ + MigrationStatus, NNS_MIGRATION_CANISTER_ID, migrate_canister, migration_status, +}; +use crate::lib::root_key::fetch_root_key_if_needed; +use crate::lib::subnet::get_subnet_for_canister; +use crate::util::ask_for_consent; +use anyhow::{Context, bail}; +use candid::Principal; +use clap::Parser; +use dfx_core::identity::CallSender; +use ic_management_canister_types::CanisterStatusType; +use num_traits::ToPrimitive; +use slog::{debug, error, info}; +use std::time::Duration; +use time::{OffsetDateTime, macros::format_description}; + +/// Migrate a canister ID from one subnet to another. +#[derive(Parser)] +#[command(override_usage = "dfx canister migrate-id [OPTIONS] --replace ")] +pub struct CanisterMigrateIdOpts { + /// Specifies the name or id of the canister to migrate. + canister: String, + + /// Specifies the name or id of the canister to replace. + #[arg(long)] + replace: String, + + /// Skips yes/no checks by answering 'yes'. Not recommended outside of CI. + #[arg(long, short)] + yes: bool, +} + +pub async fn exec( + env: &dyn Environment, + opts: CanisterMigrateIdOpts, + call_sender: &CallSender, +) -> DfxResult { + fetch_root_key_if_needed(env).await?; + + let log = env.get_logger(); + let agent = env.get_agent(); + let canister_id_store = env.get_canister_id_store()?; + + // Get the canister IDs. + let migrated_canister = opts.canister.as_str(); + let replaced_canister = opts.replace.as_str(); + let migrated_canister_id = Principal::from_text(migrated_canister) + .or_else(|_| canister_id_store.get(migrated_canister))?; + let replaced_canister_id = Principal::from_text(replaced_canister) + .or_else(|_| canister_id_store.get(replaced_canister))?; + + if migrated_canister_id == replaced_canister_id { + bail!("The canisters to migrate and replace are identical."); + } + + if !opts.yes { + ask_for_consent( + env, + &format!( + "Canister '{migrated_canister}' will be removed from its own subnet. Continue?" + ), + )?; + } + + let migrated_canister_status = get_canister_status(env, migrated_canister_id, call_sender) + .await + .with_context(|| format!("Could not retrieve status of canister {migrated_canister}"))?; + let replaced_canister_status = get_canister_status(env, replaced_canister_id, call_sender) + .await + .with_context(|| format!("Could not retrieve status of canister {replaced_canister}"))?; + + // Check that the two canisters are stopped. + ensure_canister_stopped(migrated_canister_status.status, migrated_canister)?; + ensure_canister_stopped(replaced_canister_status.status, replaced_canister)?; + + // Check that the canister is ready for migration. + if !migrated_canister_status.ready_for_migration { + bail!( + "Canister '{migrated_canister}' is not ready for migration. Wait a few seconds and try again" + ); + } + + // Check the cycles balance of migrated canister. + let cycles = migrated_canister_status + .cycles + .0 + .to_u128() + .expect("Unable to parse cycles"); + if cycles < 10_000_000_000_000 { + bail!("Canister '{migrated_canister}' has less than 10T cycles"); + } + if !opts.yes && cycles > 15_000_000_000_000 { + ask_for_consent( + env, + &format!( + "Canister '{migrated_canister}' has more than 15T cycles. The extra cycles will get burned during the migration. Continue?" + ), + )?; + } + + // Check that the replaced canister has no snapshots. + let snapshots = list_canister_snapshots(env, replaced_canister_id, call_sender).await?; + if !snapshots.is_empty() { + bail!( + "The canister '{}' whose canister ID will be replaced has snapshots", + replaced_canister + ); + } + + // Check that the two canisters are on different subnets. + let migrated_canister_subnet = get_subnet_for_canister(agent, migrated_canister_id).await?; + let replaced_canister_subnet = get_subnet_for_canister(agent, replaced_canister_id).await?; + if migrated_canister_subnet == replaced_canister_subnet { + bail!( + "The canisters '{migrated_canister}' and '{replaced_canister}' are on the same subnet" + ); + } + + // Add the NNS migration canister as a controller to the migrated canister. + let mut controllers = migrated_canister_status.settings.controllers.clone(); + if !controllers.contains(&NNS_MIGRATION_CANISTER_ID) { + controllers.push(NNS_MIGRATION_CANISTER_ID); + let settings = CanisterSettings { + controllers: Some(controllers), + compute_allocation: None, + memory_allocation: None, + freezing_threshold: None, + reserved_cycles_limit: None, + wasm_memory_limit: None, + wasm_memory_threshold: None, + log_visibility: None, + environment_variables: None, + }; + update_settings(env, migrated_canister_id, settings, call_sender).await?; + } + + // Add the NNS migration canister as a controller to the replaced canister. + let mut controllers = replaced_canister_status.settings.controllers.clone(); + if !controllers.contains(&NNS_MIGRATION_CANISTER_ID) { + controllers.push(NNS_MIGRATION_CANISTER_ID); + let settings = CanisterSettings { + controllers: Some(controllers), + compute_allocation: None, + memory_allocation: None, + freezing_threshold: None, + reserved_cycles_limit: None, + wasm_memory_limit: None, + wasm_memory_threshold: None, + log_visibility: None, + environment_variables: None, + }; + update_settings(env, replaced_canister_id, settings, call_sender).await?; + } + + // Migrate the from canister to the rename_to canister. + debug!( + log, + "Migrate '{migrated_canister}' replacing '{replaced_canister}'" + ); + migrate_canister(agent, migrated_canister_id, replaced_canister_id).await?; + + // Wait for migration to complete. + let spinner = env.new_spinner("Waiting for migration to complete...".into()); + loop { + match migration_status(agent, migrated_canister_id, replaced_canister_id).await { + Ok(status) => match status { + Some(MigrationStatus::InProgress { status }) => { + spinner.set_message(format!("Migration in progress: {status}").into()); + } + Some(MigrationStatus::Succeeded { time }) => { + spinner.finish_and_clear(); + info!(log, "Migration succeeded at {}", format_time(&time)); + break; + } + Some(MigrationStatus::Failed { reason, time }) => { + spinner.finish_and_clear(); + error!( + log, + "Migration failed at {}: {}", + format_time(&time), + reason + ); + break; + } + None => (), + }, + Err(e) => { + spinner.set_message(format!("Could not fetch migration status: {e}").into()); + } + }; + + tokio::time::sleep(Duration::from_secs(1)).await; + } + + canister_id_store.remove(log, replaced_canister)?; + + Ok(()) +} + +fn ensure_canister_stopped(status: CanisterStatusType, canister: &str) -> DfxResult { + match status { + CanisterStatusType::Stopped => Ok(()), + CanisterStatusType::Running => { + bail!("Canister {canister} is running. Run 'dfx canister stop' first"); + } + CanisterStatusType::Stopping => { + bail!("Canister {canister} is stopping. Wait a few seconds and try again"); + } + } +} + +fn format_time(time: &u64) -> String { + let format = format_description!("[year]-[month]-[day] [hour]:[minute]:[second] UTC"); + OffsetDateTime::from_unix_timestamp_nanos(*time as i128) + .unwrap() + .format(&format) + .unwrap() +} diff --git a/src/dfx/src/commands/canister/migration_status.rs b/src/dfx/src/commands/canister/migration_status.rs new file mode 100644 index 0000000000..4bb862e44d --- /dev/null +++ b/src/dfx/src/commands/canister/migration_status.rs @@ -0,0 +1,138 @@ +use crate::lib::environment::Environment; +use crate::lib::error::DfxResult; +use crate::lib::operations::canister_migration::{MigrationStatus, migration_status}; +use crate::lib::root_key::fetch_root_key_if_needed; + +use candid::Principal; +use clap::Parser; +use slog::info; +use time::{OffsetDateTime, macros::format_description}; + +/// Show the status of a migration. +#[derive(Parser)] +#[command( + override_usage = "dfx canister migration-status [OPTIONS] --replace " +)] +pub struct CanisterMigrationStatusOpts { + /// Specifies the name or id of the canister to migrate. + canister: String, + + /// Specifies the name or id of the canister to replace. + #[arg(long)] + replace: String, +} + +pub async fn exec(env: &dyn Environment, opts: CanisterMigrationStatusOpts) -> DfxResult { + fetch_root_key_if_needed(env).await?; + + let log = env.get_logger(); + let agent = env.get_agent(); + let canister_id_store = env.get_canister_id_store()?; + + // Get the canister IDs. + let migrated_canister = opts.canister.as_str(); + let replaced_canister = opts.replace.as_str(); + let migrated_canister_id = Principal::from_text(migrated_canister) + .or_else(|_| canister_id_store.get(migrated_canister)) + .map_err(|_| { + anyhow::anyhow!( + "Cannot find canister '{migrated_canister}'. Please use canister id instead" + ) + })?; + let replaced_canister_id = Principal::from_text(replaced_canister) + .or_else(|_| canister_id_store.get(replaced_canister)) + .map_err(|_| { + anyhow::anyhow!( + "Cannot find canister '{replaced_canister}'. Please use canister id instead" + ) + })?; + + let Some(status) = migration_status(agent, migrated_canister_id, replaced_canister_id).await? + else { + info!( + log, + "No migration status found for canister '{migrated_canister}' to '{replaced_canister}'" + ); + return Ok(()); + }; + + // Print the statuses in a table with aligned columns. + let migrated_canister_text = migrated_canister_id.to_text(); + let replaced_canister_text = replaced_canister_id.to_text(); + let status_strings: Vec = vec![format_status(&status)]; + + let header_migrated_canister = "Canister"; + let header_replaced_canister = "Canister To Be Replaced"; + let header_status = "Migration Status"; + + let migrated_canister_width = header_migrated_canister + .len() + .max(migrated_canister_text.len()); + let replaced_canister_width = header_replaced_canister + .len() + .max(replaced_canister_text.len()); + let status_width = header_status + .len() + .max(status_strings.iter().map(|s| s.len()).max().unwrap_or(0)); + + let sep_migrated_canister = "-".repeat(migrated_canister_width); + let sep_replaced_canister = "-".repeat(replaced_canister_width); + let sep_status = "-".repeat(status_width); + + info!( + log, + "| {: String { + match status { + MigrationStatus::InProgress { status } => { + format!("In progress: {status}") + } + MigrationStatus::Failed { reason, time } => { + format!("Failed: {reason} at {}", format_time(time)) + } + MigrationStatus::Succeeded { time } => { + format!("Succeeded at {}", format_time(time)) + } + } +} + +fn format_time(time: &u64) -> String { + let format = format_description!("[year]-[month]-[day] [hour]:[minute]:[second] UTC"); + OffsetDateTime::from_unix_timestamp_nanos(*time as i128) + .unwrap() + .format(&format) + .unwrap() +} diff --git a/src/dfx/src/commands/canister/mod.rs b/src/dfx/src/commands/canister/mod.rs index 03beb19828..ccc0520cd5 100644 --- a/src/dfx/src/commands/canister/mod.rs +++ b/src/dfx/src/commands/canister/mod.rs @@ -15,6 +15,8 @@ mod info; mod install; mod logs; mod metadata; +mod migrate_id; +mod migration_status; mod request_status; mod send; mod set_id; @@ -53,6 +55,8 @@ pub enum SubCommand { Info(info::InfoOpts), Install(install::CanisterInstallOpts), Metadata(metadata::CanisterMetadataOpts), + MigrateId(migrate_id::CanisterMigrateIdOpts), + MigrationStatus(migration_status::CanisterMigrationStatusOpts), RequestStatus(request_status::RequestStatusOpts), Send(send::CanisterSendOpts), SetId(set_id::CanisterSetIdOpts), @@ -88,6 +92,8 @@ pub fn exec(env: &dyn Environment, opts: CanisterOpts) -> DfxResult { SubCommand::Install(v) => install::exec(env, v, &call_sender()?).await, SubCommand::Info(v) => info::exec(env, v).await, SubCommand::Metadata(v) => metadata::exec(env, v).await, + SubCommand::MigrateId(v) => migrate_id::exec(env, v, &call_sender()?).await, + SubCommand::MigrationStatus(v) => migration_status::exec(env, v).await, SubCommand::RequestStatus(v) => request_status::exec(env, v).await, SubCommand::Send(v) => send::exec(env, v, &call_sender()?).await, SubCommand::SetId(v) => set_id::exec(env, v).await, diff --git a/src/dfx/src/commands/canister/snapshot.rs b/src/dfx/src/commands/canister/snapshot.rs index 05fb75cc4b..f7d3f258c3 100644 --- a/src/dfx/src/commands/canister/snapshot.rs +++ b/src/dfx/src/commands/canister/snapshot.rs @@ -506,7 +506,11 @@ async fn upload( canister_id, replace_snapshot: replace.as_ref().map(|x| x.0.clone()), wasm_module_size: metadata.wasm_module_size, - globals: metadata.globals, + globals: metadata + .globals + .into_iter() + .map(|x| x.ok_or(anyhow!("Could not parse global in snapshot metadata"))) + .collect::, _>>()?, wasm_memory_size: metadata.wasm_memory_size, stable_memory_size: metadata.stable_memory_size, certified_data: metadata.certified_data, diff --git a/src/dfx/src/lib/operations/canister_migration.rs b/src/dfx/src/lib/operations/canister_migration.rs new file mode 100644 index 0000000000..7003210885 --- /dev/null +++ b/src/dfx/src/lib/operations/canister_migration.rs @@ -0,0 +1,132 @@ +use crate::lib::error::DfxResult; +use candid::{CandidType, Principal, Reserved}; +use ic_agent::Agent; +use ic_utils::Canister; +use serde::Deserialize; +use std::fmt; +use thiserror::Error; + +pub const NNS_MIGRATION_CANISTER_ID: Principal = + Principal::from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11, 0x01, 0x01]); +const MIGRATE_CANISTER_METHOD: &str = "migrate_canister"; +const MIGRATION_STATUS_METHOD: &str = "migration_status"; + +#[derive(Clone, CandidType, Deserialize)] +pub struct MigrateCanisterArgs { + pub migrated_canister_id: Principal, + pub replaced_canister_id: Principal, +} + +#[derive(Clone, Debug, Error, CandidType, Deserialize)] +enum ValidationError { + #[error("Canister migrations are disabled at the moment.")] + MigrationsDisabled(Reserved), + #[error("Canister migration has been rate-limited. Try again later.")] + RateLimited(Reserved), + #[error("Validation for canister {canister} is already in progress.")] + ValidationInProgress { canister: Principal }, + #[error("Canister migration for canister {canister} is already in progress.")] + MigrationInProgress { canister: Principal }, + #[error("The canister {canister} does not exist.")] + CanisterNotFound { canister: Principal }, + #[error("Both canisters are on the same subnet.")] + SameSubnet(Reserved), + #[error("The canister {canister} is not controlled by the calling identity.")] + CallerNotController { canister: Principal }, + #[error( + "The NNS canister sbzkb-zqaaa-aaaaa-aaaiq-cai is not a controller of canister {canister}." + )] + NotController { canister: Principal }, + #[error("The migrated canister is not stopped.")] + MigratedCanisterNotStopped(Reserved), + #[error("The migrated canister is not ready for migration. Try again later.")] + MigratedCanisterNotReady(Reserved), + #[error("The replaced canister is not stopped.")] + ReplacedCanisterNotStopped(Reserved), + #[error("The replaced canister has snapshots.")] + ReplacedCanisterHasSnapshots(Reserved), + #[error( + "The migrated canister does not have enough cycles for canister migration. Top up the migrated canister with the required amount of cycles." + )] + MigratedCanisterInsufficientCycles(Reserved), + #[error("Internal IC error: a call failed due to {reason}")] + CallFailed { reason: String }, +} + +#[derive(Clone, CandidType, Deserialize, Debug)] +pub enum MigrationStatus { + InProgress { status: String }, + Failed { reason: String, time: u64 }, + Succeeded { time: u64 }, +} + +impl fmt::Display for MigrationStatus { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + MigrationStatus::InProgress { status } => { + write!(f, "MigrationStatus::InProgress {{ status: {status} }}") + } + MigrationStatus::Failed { reason, time } => { + write!( + f, + "MigrationStatus::Failed {{ reason: {reason}, time: {time} }}", + ) + } + MigrationStatus::Succeeded { time } => { + write!(f, "MigrationStatus::Succeeded {{ time: {time} }}") + } + } + } +} + +pub async fn migrate_canister( + agent: &Agent, + migrated_canister: Principal, + replaced_canister: Principal, +) -> DfxResult { + let canister = Canister::builder() + .with_agent(agent) + .with_canister_id(NNS_MIGRATION_CANISTER_ID) + .build()?; + + let arg = MigrateCanisterArgs { + migrated_canister_id: migrated_canister, + replaced_canister_id: replaced_canister, + }; + + let (result,): (Result<(), Option>,) = canister + .update(MIGRATE_CANISTER_METHOD) + .with_arg(arg) + .build() + .await?; + + match result { + Ok(()) => Ok(()), + Err(None) => Err(anyhow::anyhow!("Validation failed with an unknown error.")), + Err(Some(err)) => Err(anyhow::anyhow!("Validation failed: {err}")), + } +} + +pub async fn migration_status( + agent: &Agent, + migrated_canister: Principal, + replaced_canister: Principal, +) -> DfxResult> { + let canister = Canister::builder() + .with_agent(agent) + .with_canister_id(NNS_MIGRATION_CANISTER_ID) + .build()?; + + let arg = MigrateCanisterArgs { + migrated_canister_id: migrated_canister, + replaced_canister_id: replaced_canister, + }; + + let (result,): (Option,) = canister + .query(MIGRATION_STATUS_METHOD) + .with_arg(arg) + .build() + .await?; + + Ok(result) +} diff --git a/src/dfx/src/lib/operations/mod.rs b/src/dfx/src/lib/operations/mod.rs index 9c2838deee..d68b65d657 100644 --- a/src/dfx/src/lib/operations/mod.rs +++ b/src/dfx/src/lib/operations/mod.rs @@ -1,4 +1,5 @@ pub mod canister; +pub mod canister_migration; pub mod cmc; pub mod cycles_ledger; pub mod ledger;