From 5c02350f6042e9a71124684a84da9e8101764de0 Mon Sep 17 00:00:00 2001 From: Vincent Zhang Date: Tue, 28 Oct 2025 10:03:47 +0800 Subject: [PATCH 01/17] Add 'dfx canister rename' subcommand stub. --- src/dfx/src/commands/canister/mod.rs | 3 +++ src/dfx/src/commands/canister/rename.rs | 30 +++++++++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 src/dfx/src/commands/canister/rename.rs diff --git a/src/dfx/src/commands/canister/mod.rs b/src/dfx/src/commands/canister/mod.rs index 03beb19828..4e8dda70b8 100644 --- a/src/dfx/src/commands/canister/mod.rs +++ b/src/dfx/src/commands/canister/mod.rs @@ -15,6 +15,7 @@ mod info; mod install; mod logs; mod metadata; +mod rename; mod request_status; mod send; mod set_id; @@ -53,6 +54,7 @@ pub enum SubCommand { Info(info::InfoOpts), Install(install::CanisterInstallOpts), Metadata(metadata::CanisterMetadataOpts), + Rename(rename::CanisterRenameOpts), RequestStatus(request_status::RequestStatusOpts), Send(send::CanisterSendOpts), SetId(set_id::CanisterSetIdOpts), @@ -88,6 +90,7 @@ 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::Rename(v) => rename::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/rename.rs b/src/dfx/src/commands/canister/rename.rs new file mode 100644 index 0000000000..be5f50b36a --- /dev/null +++ b/src/dfx/src/commands/canister/rename.rs @@ -0,0 +1,30 @@ +use crate::lib::environment::Environment; +use crate::lib::error::DfxResult; +use clap::Parser; + +/// Renames a canister. +#[derive(Parser)] +#[command(override_usage = "dfx canister rename --rename-to ")] +pub struct CanisterRenameOpts { + /// Specifies the name of the canister to rename. + from_canister: String, + + /// Specifies the new name of the canister. + #[arg(long)] + rename_to: String, +} + +pub async fn exec(env: &dyn Environment, opts: CanisterRenameOpts) -> DfxResult { + println!( + "Renaming canister from {} to {}", + opts.from_canister, opts.rename_to + ); + + let log = env.get_logger(); + let canister_id_store = env.get_canister_id_store()?; + let canister_id = canister_id_store.get(opts.from_canister.as_str())?; + + // TODO: Implement the renaming logic. + + Ok(()) +} From 8f689ed8fbf17972cc03f819f759f8ea1979bb1f Mon Sep 17 00:00:00 2001 From: Vincent Zhang Date: Tue, 28 Oct 2025 10:08:23 +0800 Subject: [PATCH 02/17] Update to the revision that pocket-ic added support for migration canister. --- Cargo.lock | 2 +- src/dfx/Cargo.toml | 2 +- src/dfx/assets/dfx-asset-sources.json | 26 +++++++++++++------------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fc6ce9071d..cffd446f82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5148,7 +5148,7 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "pocket-ic" version = "10.0.0" -source = "git+https://github.com/dfinity/ic?rev=575bcd0954e9d00066fd465223b755bda645edd6#575bcd0954e9d00066fd465223b755bda645edd6" +source = "git+https://github.com/dfinity/ic?rev=9276bb4c90c5adc5718cd144342e6b56c13a3ff6#9276bb4c90c5adc5718cd144342e6b56c13a3ff6" dependencies = [ "backoff", "base64 0.13.1", diff --git a/src/dfx/Cargo.toml b/src/dfx/Cargo.toml index 4204b5d29c..3a58cfb164 100644 --- a/src/dfx/Cargo.toml +++ b/src/dfx/Cargo.toml @@ -84,7 +84,7 @@ os_str_bytes = { version = "6.3.0", features = ["conversions"] } patch = "0.7.0" pem.workspace = true petgraph = "0.6.0" -pocket-ic = { git = "https://github.com/dfinity/ic", rev = "575bcd0954e9d00066fd465223b755bda645edd6" } +pocket-ic = { git = "https://github.com/dfinity/ic", rev = "9276bb4c90c5adc5718cd144342e6b56c13a3ff6" } rand = "0.8.5" regex = "1.5.5" reqwest = { workspace = true, features = ["blocking", "json"] } diff --git a/src/dfx/assets/dfx-asset-sources.json b/src/dfx/assets/dfx-asset-sources.json index 26b3b9737c..04f796b03f 100644 --- a/src/dfx/assets/dfx-asset-sources.json +++ b/src/dfx/assets/dfx-asset-sources.json @@ -1,5 +1,5 @@ { - "replica-rev": "575bcd0954e9d00066fd465223b755bda645edd6", + "replica-rev": "9276bb4c90c5adc5718cd144342e6b56c13a3ff6", "x86_64-darwin": { "motoko": { "url": "https://github.com/dfinity/motoko/releases/download/0.16.2/motoko-Darwin-x86_64-0.16.2.tar.gz", @@ -7,9 +7,9 @@ "version": "0.16.2" }, "pocket-ic": { - "url": "https://download.dfinity.systems/ic/575bcd0954e9d00066fd465223b755bda645edd6/binaries/x86_64-darwin/pocket-ic.gz", - "sha256": "aae66c74224421a9a8055b48d2189891eea12d8fa932ec5fb36a6c827e0be43a", - "rev": "575bcd0954e9d00066fd465223b755bda645edd6" + "url": "https://download.dfinity.systems/ic/9276bb4c90c5adc5718cd144342e6b56c13a3ff6/binaries/x86_64-darwin/pocket-ic.gz", + "sha256": "3af2af65b87d6a6b3c3bfb4ee08a42d840c29a881a652a1e63eeac496c029597", + "rev": "9276bb4c90c5adc5718cd144342e6b56c13a3ff6" } }, "arm64-darwin": { @@ -19,9 +19,9 @@ "version": "0.16.2" }, "pocket-ic": { - "url": "https://download.dfinity.systems/ic/575bcd0954e9d00066fd465223b755bda645edd6/binaries/arm64-darwin/pocket-ic.gz", - "sha256": "988020fde4cfba0abfd9957d4364dcab6d5c2b64aae19c786f33474a192ff11e", - "rev": "575bcd0954e9d00066fd465223b755bda645edd6" + "url": "https://download.dfinity.systems/ic/9276bb4c90c5adc5718cd144342e6b56c13a3ff6/binaries/arm64-darwin/pocket-ic.gz", + "sha256": "a9b8b29b88a47069babb18e524abdbd9f54a7a32ff878d93ba5c93230faae7de", + "rev": "9276bb4c90c5adc5718cd144342e6b56c13a3ff6" } }, "x86_64-linux": { @@ -31,9 +31,9 @@ "version": "0.16.2" }, "pocket-ic": { - "url": "https://download.dfinity.systems/ic/575bcd0954e9d00066fd465223b755bda645edd6/binaries/x86_64-linux/pocket-ic.gz", - "sha256": "99edb5b28c1e62ad3d2b86b3ad022c98ddc734671dad9abe1fc672bc0048d1e9", - "rev": "575bcd0954e9d00066fd465223b755bda645edd6" + "url": "https://download.dfinity.systems/ic/9276bb4c90c5adc5718cd144342e6b56c13a3ff6/binaries/x86_64-linux/pocket-ic.gz", + "sha256": "08e45801214ec1f0e95a483e39a007553785a5cb676c4144eb8a5eb2749bf67b", + "rev": "9276bb4c90c5adc5718cd144342e6b56c13a3ff6" } }, "arm64-linux": { @@ -43,9 +43,9 @@ "version": "0.16.2" }, "pocket-ic": { - "url": "https://download.dfinity.systems/ic/575bcd0954e9d00066fd465223b755bda645edd6/binaries/arm64-linux/pocket-ic.gz", - "sha256": "bd5aae42649a68e7febd715d7cadf4a78a6b9b5e2cd5fb6ae528750a463fdd77", - "rev": "575bcd0954e9d00066fd465223b755bda645edd6" + "url": "https://download.dfinity.systems/ic/9276bb4c90c5adc5718cd144342e6b56c13a3ff6/binaries/arm64-linux/pocket-ic.gz", + "sha256": "f4db572e52cda7d92a4b6a233c240df0fa9cbd1f1e91034a97e03de75a43e375", + "rev": "9276bb4c90c5adc5718cd144342e6b56c13a3ff6" } }, "common": { From e8986b107e8ab8edf944ca87d28da98c7d481535 Mon Sep 17 00:00:00 2001 From: Vincent Zhang Date: Tue, 28 Oct 2025 12:20:26 +0800 Subject: [PATCH 03/17] Implemented 'dfx canister rename'. --- src/dfx/src/actors/pocketic.rs | 1 + src/dfx/src/commands/canister/mod.rs | 2 +- src/dfx/src/commands/canister/rename.rs | 128 ++++++++++++++++-- .../src/lib/operations/migration_canister.rs | 39 ++++++ src/dfx/src/lib/operations/mod.rs | 1 + 5 files changed, 161 insertions(+), 10 deletions(-) create mode 100644 src/dfx/src/lib/operations/migration_canister.rs diff --git a/src/dfx/src/actors/pocketic.rs b/src/dfx/src/actors/pocketic.rs index 3c65ba1386..2351bf2d62 100644 --- a/src/dfx/src/actors/pocketic.rs +++ b/src/dfx/src/actors/pocketic.rs @@ -396,6 +396,7 @@ async fn initialize_pocketic( ii: Some(IcpFeaturesConfig::default()), nns_ui: Some(IcpFeaturesConfig::default()), bitcoin: None, + canister_migration: Some(IcpFeaturesConfig::default()), }) } else { None diff --git a/src/dfx/src/commands/canister/mod.rs b/src/dfx/src/commands/canister/mod.rs index 4e8dda70b8..56b5b8437c 100644 --- a/src/dfx/src/commands/canister/mod.rs +++ b/src/dfx/src/commands/canister/mod.rs @@ -90,7 +90,7 @@ 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::Rename(v) => rename::exec(env, v).await, + SubCommand::Rename(v) => rename::exec(env, v, &call_sender()?).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/rename.rs b/src/dfx/src/commands/canister/rename.rs index be5f50b36a..35a5aa3a74 100644 --- a/src/dfx/src/commands/canister/rename.rs +++ b/src/dfx/src/commands/canister/rename.rs @@ -1,30 +1,140 @@ use crate::lib::environment::Environment; use crate::lib::error::DfxResult; +use crate::lib::ic_attributes::CanisterSettings; +use crate::lib::operations::canister::{get_canister_status, stop_canister, update_settings}; +use crate::lib::operations::migration_canister::{NNS_MIGRATION_CANISTER_ID, migrate_canister}; +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 num_traits::ToPrimitive; +use slog::info; /// Renames a canister. #[derive(Parser)] #[command(override_usage = "dfx canister rename --rename-to ")] pub struct CanisterRenameOpts { - /// Specifies the name of the canister to rename. + /// Specifies the name or id of the canister to rename. from_canister: String, - /// Specifies the new name of the canister. + /// Specifies the name or id of the canister to rename to. #[arg(long)] rename_to: String, } -pub async fn exec(env: &dyn Environment, opts: CanisterRenameOpts) -> DfxResult { - println!( - "Renaming canister from {} to {}", - opts.from_canister, opts.rename_to - ); +pub async fn exec( + env: &dyn Environment, + opts: CanisterRenameOpts, + 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()?; - let canister_id = canister_id_store.get(opts.from_canister.as_str())?; - // TODO: Implement the renaming logic. + // Get the canister IDs. + let from_canister = opts.from_canister.as_str(); + let to_canister = opts.rename_to.as_str(); + let from_canister_id = + Principal::from_text(from_canister).or_else(|_| canister_id_store.get(from_canister))?; + let to_canister_id = + Principal::from_text(to_canister).or_else(|_| canister_id_store.get(to_canister))?; + + if from_canister_id == to_canister_id { + bail!("From and rename_to canister IDs are the same"); + } + + // Stop both canisters. + info!( + log, + "Stopping canister {}, with canister_id {}", + from_canister, + from_canister_id.to_text(), + ); + stop_canister(env, from_canister_id, call_sender).await?; + info!( + log, + "Stopping canister {}, with canister_id {}", + to_canister, + to_canister_id.to_text(), + ); + stop_canister(env, to_canister_id, call_sender).await?; + + // Check the cycles balance of from_canister. + let from_status = get_canister_status(env, from_canister_id, call_sender) + .await + .with_context(|| format!("Could not retrieve status of canister {}", from_canister))?; + + let cycles = from_status + .cycles + .0 + .to_u128() + .expect("Unable to parse cycles"); + if cycles < 5_000_000_000_000 { + bail!("Canister {} has less than 10T cycles", from_canister); + } + if cycles > 10_000_000_000_000 { + ask_for_consent( + env, + &format!( + "Canister {} has more than 10T cycles. Continue?", + from_canister + ), + )?; + } + + // Check if the two canisters are on different subnets. + let from_subnet = get_subnet_for_canister(agent, from_canister_id).await?; + let to_subnet = get_subnet_for_canister(agent, to_canister_id).await?; + if from_subnet == to_subnet { + bail!("From and rename_to canisters are on the same subnet"); + } + + // Add the NNS migration canister as a controller to the from canister. + let mut controllers = from_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, from_canister_id, settings, call_sender).await?; + } + + // Add the NNS migration canister as a controller to the rename_to canister. + let to_status = get_canister_status(env, to_canister_id, call_sender) + .await + .with_context(|| format!("Could not retrieve status of canister {}", to_canister))?; + let mut controllers = to_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, to_canister_id, settings, call_sender).await?; + } + + // Migrate the from canister to the rename_to canister. + migrate_canister(agent, from_canister_id, to_canister_id).await?; Ok(()) } diff --git a/src/dfx/src/lib/operations/migration_canister.rs b/src/dfx/src/lib/operations/migration_canister.rs new file mode 100644 index 0000000000..7c9a33fa1a --- /dev/null +++ b/src/dfx/src/lib/operations/migration_canister.rs @@ -0,0 +1,39 @@ +use crate::lib::error::DfxResult; +use candid::{CandidType, Principal}; +use ic_agent::Agent; +use ic_utils::Canister; + +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 MIGRATE_STATUS_METHOD: &str = "migrate_status"; + +#[derive(CandidType)] +pub struct MigrateCanisterArg { + pub from_canister: Principal, + pub to_canister: Principal, +} + +pub async fn migrate_canister( + agent: &Agent, + from_canister: Principal, + to_canister: Principal, +) -> DfxResult { + let canister = Canister::builder() + .with_agent(agent) + .with_canister_id(NNS_MIGRATION_CANISTER_ID) + .build()?; + + let arg = MigrateCanisterArg { + from_canister, + to_canister, + }; + + let _: () = canister + .update(MIGRATE_CANISTER_METHOD) + .with_arg(arg) + .build() + .await?; + + Ok(()) +} diff --git a/src/dfx/src/lib/operations/mod.rs b/src/dfx/src/lib/operations/mod.rs index 9c2838deee..50867de4c2 100644 --- a/src/dfx/src/lib/operations/mod.rs +++ b/src/dfx/src/lib/operations/mod.rs @@ -2,6 +2,7 @@ pub mod canister; pub mod cmc; pub mod cycles_ledger; pub mod ledger; +pub mod migration_canister; const ICRC1_BALANCE_OF_METHOD: &str = "icrc1_balance_of"; const ICRC1_TRANSFER_METHOD: &str = "icrc1_transfer"; From e8a66afa65c56bbdfe8714844383455c5ebeba62 Mon Sep 17 00:00:00 2001 From: Vincent Zhang Date: Tue, 28 Oct 2025 14:24:33 +0800 Subject: [PATCH 04/17] Check snapshot and add --yes flag. --- src/dfx/src/commands/canister/rename.rs | 42 ++++++++++++++++++------- 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/src/dfx/src/commands/canister/rename.rs b/src/dfx/src/commands/canister/rename.rs index 35a5aa3a74..982ae49aac 100644 --- a/src/dfx/src/commands/canister/rename.rs +++ b/src/dfx/src/commands/canister/rename.rs @@ -1,7 +1,9 @@ use crate::lib::environment::Environment; use crate::lib::error::DfxResult; use crate::lib::ic_attributes::CanisterSettings; -use crate::lib::operations::canister::{get_canister_status, stop_canister, update_settings}; +use crate::lib::operations::canister::{ + get_canister_status, list_canister_snapshots, stop_canister, update_settings, +}; use crate::lib::operations::migration_canister::{NNS_MIGRATION_CANISTER_ID, migrate_canister}; use crate::lib::root_key::fetch_root_key_if_needed; use crate::lib::subnet::get_subnet_for_canister; @@ -15,7 +17,7 @@ use slog::info; /// Renames a canister. #[derive(Parser)] -#[command(override_usage = "dfx canister rename --rename-to ")] +#[command(override_usage = "dfx canister rename [OPTIONS] --rename-to ")] pub struct CanisterRenameOpts { /// Specifies the name or id of the canister to rename. from_canister: String, @@ -23,6 +25,10 @@ pub struct CanisterRenameOpts { /// Specifies the name or id of the canister to rename to. #[arg(long)] rename_to: String, + + /// Skips yes/no checks by answering 'yes'. Not recommended outside of CI. + #[arg(long, short)] + yes: bool, } pub async fn exec( @@ -48,6 +54,15 @@ pub async fn exec( bail!("From and rename_to canister IDs are the same"); } + if !opts.yes { + ask_for_consent( + env, + &format!( + "The from canister '{from_canister}' will be removed from its own subnet. Continue anyway?", + ), + )?; + } + // Stop both canisters. info!( log, @@ -67,7 +82,7 @@ pub async fn exec( // Check the cycles balance of from_canister. let from_status = get_canister_status(env, from_canister_id, call_sender) .await - .with_context(|| format!("Could not retrieve status of canister {}", from_canister))?; + .with_context(|| format!("Could not retrieve status of canister {from_canister}"))?; let cycles = from_status .cycles @@ -75,23 +90,26 @@ pub async fn exec( .to_u128() .expect("Unable to parse cycles"); if cycles < 5_000_000_000_000 { - bail!("Canister {} has less than 10T cycles", from_canister); + bail!("The from canister {} has less than 10T cycles", from_canister); } - if cycles > 10_000_000_000_000 { + if !opts.yes && cycles > 10_000_000_000_000 { ask_for_consent( env, - &format!( - "Canister {} has more than 10T cycles. Continue?", - from_canister - ), + &format!("The from canister {from_canister} has more than 10T cycles. Continue?"), )?; } - // Check if the two canisters are on different subnets. + // Check that the from canister has no snapshots. + let from_snapshots = list_canister_snapshots(env, from_canister_id, call_sender).await?; + if !from_snapshots.is_empty() { + bail!("The from canister {} has snapshots", from_canister); + } + + // Check that the two canisters are on different subnets. let from_subnet = get_subnet_for_canister(agent, from_canister_id).await?; let to_subnet = get_subnet_for_canister(agent, to_canister_id).await?; if from_subnet == to_subnet { - bail!("From and rename_to canisters are on the same subnet"); + bail!("The from and rename_to canisters are on the same subnet"); } // Add the NNS migration canister as a controller to the from canister. @@ -115,7 +133,7 @@ pub async fn exec( // Add the NNS migration canister as a controller to the rename_to canister. let to_status = get_canister_status(env, to_canister_id, call_sender) .await - .with_context(|| format!("Could not retrieve status of canister {}", to_canister))?; + .with_context(|| format!("Could not retrieve status of canister {to_canister}"))?; let mut controllers = to_status.settings.controllers.clone(); if !controllers.contains(&NNS_MIGRATION_CANISTER_ID) { controllers.push(NNS_MIGRATION_CANISTER_ID); From 4cd85de736966bdac8cea3693bbaa60c63896930 Mon Sep 17 00:00:00 2001 From: Vincent Zhang Date: Tue, 28 Oct 2025 15:05:47 +0800 Subject: [PATCH 05/17] Add spinner to update the renaming status. --- src/dfx/src/commands/canister/rename.rs | 41 ++++++- .../src/lib/operations/migration_canister.rs | 105 +++++++++++++++++- 2 files changed, 143 insertions(+), 3 deletions(-) diff --git a/src/dfx/src/commands/canister/rename.rs b/src/dfx/src/commands/canister/rename.rs index 982ae49aac..b5b179388a 100644 --- a/src/dfx/src/commands/canister/rename.rs +++ b/src/dfx/src/commands/canister/rename.rs @@ -4,7 +4,9 @@ use crate::lib::ic_attributes::CanisterSettings; use crate::lib::operations::canister::{ get_canister_status, list_canister_snapshots, stop_canister, update_settings, }; -use crate::lib::operations::migration_canister::{NNS_MIGRATION_CANISTER_ID, migrate_canister}; +use crate::lib::operations::migration_canister::{ + MigrationStatus, NNS_MIGRATION_CANISTER_ID, migrate_canister, migrate_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; @@ -14,6 +16,8 @@ use clap::Parser; use dfx_core::identity::CallSender; use num_traits::ToPrimitive; use slog::info; +use std::time::Duration; +use time::{OffsetDateTime, macros::format_description}; /// Renames a canister. #[derive(Parser)] @@ -90,7 +94,7 @@ pub async fn exec( .to_u128() .expect("Unable to parse cycles"); if cycles < 5_000_000_000_000 { - bail!("The from canister {} has less than 10T cycles", from_canister); + bail!("The from canister {from_canister} has less than 5T cycles"); } if !opts.yes && cycles > 10_000_000_000_000 { ask_for_consent( @@ -154,5 +158,38 @@ pub async fn exec( // Migrate the from canister to the rename_to canister. migrate_canister(agent, from_canister_id, to_canister_id).await?; + // Wait for migration to complete. + let spinner = env.new_spinner("Waiting for renaming to complete...".into()); + loop { + let statuses = migrate_status(agent, from_canister_id, to_canister_id).await?; + match statuses.last() { + Some(MigrationStatus::InProgress { status }) => { + spinner.set_message(format!("Renaming in progress: {status}").into()); + } + Some(MigrationStatus::Succeeded { time }) => { + spinner.finish_and_clear(); + info!(log, "Renaming succeeded at {}", format_time(time)); + break; + } + Some(MigrationStatus::Failed { reason, time }) => { + spinner.finish_and_clear(); + info!(log, "Renaming failed at {}: {}", format_time(time), reason); + } + None => (), + } + + tokio::time::sleep(Duration::from_secs(1)).await; + } + + // TODO: should we remove the NNS migration canister as a controller from TO canister and start the it? + Ok(()) } + +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/lib/operations/migration_canister.rs b/src/dfx/src/lib/operations/migration_canister.rs index 7c9a33fa1a..69fe641f1f 100644 --- a/src/dfx/src/lib/operations/migration_canister.rs +++ b/src/dfx/src/lib/operations/migration_canister.rs @@ -2,11 +2,13 @@ use crate::lib::error::DfxResult; use candid::{CandidType, Principal}; use ic_agent::Agent; use ic_utils::Canister; +use serde::Deserialize; +use std::fmt; 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 MIGRATE_STATUS_METHOD: &str = "migrate_status"; +const MIGRATE_STATUS_METHOD: &str = "migrate_status"; #[derive(CandidType)] pub struct MigrateCanisterArg { @@ -14,6 +16,83 @@ pub struct MigrateCanisterArg { pub to_canister: Principal, } +#[derive(Clone, Debug, CandidType, Deserialize)] +pub enum ValidationError { + MigrationsDisabled, + RateLimited, + MigrationInProgress { canister: Principal }, + CanisterNotFound { canister: Principal }, + SameSubnet, + CallerNotController { canister: Principal }, + NotController { canister: Principal }, + SourceNotStopped, + SourceNotReady, + TargetNotStopped, + TargetHasSnapshots, + SourceInsufficientCycles, + CallFailed { reason: String }, +} + +impl fmt::Display for ValidationError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ValidationError::MigrationsDisabled => write!(f, "MigrationsDisabled"), + ValidationError::RateLimited => write!(f, "RateLimited"), + ValidationError::MigrationInProgress { canister } => write!( + f, + "ValidationError::MigrationInProgress {{ canister: {canister} }}", + ), + ValidationError::CanisterNotFound { canister } => write!( + f, + "ValidationError::CanisterNotFound {{ canister: {canister} }}", + ), + ValidationError::SameSubnet => write!(f, "SameSubnet"), + ValidationError::CallerNotController { canister } => write!( + f, + "ValidationError::CallerNotController {{ canister: {canister} }}", + ), + ValidationError::NotController { canister } => write!( + f, + "ValidationError::NotController {{ canister: {canister} }}", + ), + ValidationError::SourceNotStopped => write!(f, "SourceNotStopped"), + ValidationError::SourceNotReady => write!(f, "SourceNotReady"), + ValidationError::TargetNotStopped => write!(f, "TargetNotStopped"), + ValidationError::TargetHasSnapshots => write!(f, "TargetHasSnapshots"), + ValidationError::SourceInsufficientCycles => write!(f, "SourceInsufficientCycles"), + ValidationError::CallFailed { reason } => { + write!(f, "ValidationError::CallFailed {{ reason: {reason} }}") + } + } + } +} + +#[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, from_canister: Principal, @@ -37,3 +116,27 @@ pub async fn migrate_canister( Ok(()) } + +pub async fn migrate_status( + agent: &Agent, + from_canister: Principal, + to_canister: Principal, +) -> DfxResult> { + let canister = Canister::builder() + .with_agent(agent) + .with_canister_id(NNS_MIGRATION_CANISTER_ID) + .build()?; + + let arg = MigrateCanisterArg { + from_canister, + to_canister, + }; + + let (result,): (Vec,) = canister + .query(MIGRATE_STATUS_METHOD) + .with_arg(arg) + .build() + .await?; + + Ok(result) +} From 5c1ead5fd73e1c594ee4de961faa47faa73c855c Mon Sep 17 00:00:00 2001 From: Vincent Zhang Date: Tue, 28 Oct 2025 15:24:46 +0800 Subject: [PATCH 06/17] Remove the NNS canister from the controllers. --- src/dfx/src/commands/canister/rename.rs | 32 +++++++++++++++++++++---- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/src/dfx/src/commands/canister/rename.rs b/src/dfx/src/commands/canister/rename.rs index b5b179388a..a71e649fcd 100644 --- a/src/dfx/src/commands/canister/rename.rs +++ b/src/dfx/src/commands/canister/rename.rs @@ -15,7 +15,7 @@ use candid::Principal; use clap::Parser; use dfx_core::identity::CallSender; use num_traits::ToPrimitive; -use slog::info; +use slog::{debug, error, info}; use std::time::Duration; use time::{OffsetDateTime, macros::format_description}; @@ -68,14 +68,14 @@ pub async fn exec( } // Stop both canisters. - info!( + debug!( log, "Stopping canister {}, with canister_id {}", from_canister, from_canister_id.to_text(), ); stop_canister(env, from_canister_id, call_sender).await?; - info!( + debug!( log, "Stopping canister {}, with canister_id {}", to_canister, @@ -138,6 +138,8 @@ pub async fn exec( let to_status = get_canister_status(env, to_canister_id, call_sender) .await .with_context(|| format!("Could not retrieve status of canister {to_canister}"))?; + + let mut controller_added = false; let mut controllers = to_status.settings.controllers.clone(); if !controllers.contains(&NNS_MIGRATION_CANISTER_ID) { controllers.push(NNS_MIGRATION_CANISTER_ID); @@ -153,9 +155,11 @@ pub async fn exec( environment_variables: None, }; update_settings(env, to_canister_id, settings, call_sender).await?; + controller_added = true; } // Migrate the from canister to the rename_to canister. + debug!(log, "Renaming {from_canister} to {to_canister}"); migrate_canister(agent, from_canister_id, to_canister_id).await?; // Wait for migration to complete. @@ -173,7 +177,8 @@ pub async fn exec( } Some(MigrationStatus::Failed { reason, time }) => { spinner.finish_and_clear(); - info!(log, "Renaming failed at {}: {}", format_time(time), reason); + error!(log, "Renaming failed at {}: {}", format_time(time), reason); + break; } None => (), } @@ -181,7 +186,24 @@ pub async fn exec( tokio::time::sleep(Duration::from_secs(1)).await; } - // TODO: should we remove the NNS migration canister as a controller from TO canister and start the it? + // Remove the NNS migration canister from the controllers if added. + if controller_added { + let controllers = to_status.settings.controllers.clone(); + 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, to_canister_id, settings, call_sender).await?; + } + + // TODO: should we start the TO canister? Ok(()) } From 2d9134ec0f84a7117152d40caf0d38c6585574f9 Mon Sep 17 00:00:00 2001 From: Vincent Zhang Date: Tue, 28 Oct 2025 17:35:58 +0800 Subject: [PATCH 07/17] Ensure the canisters are stopped instead of stopping them. --- src/dfx/src/commands/canister/rename.rs | 47 ++++++++++++------------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/src/dfx/src/commands/canister/rename.rs b/src/dfx/src/commands/canister/rename.rs index a71e649fcd..c6eaf11288 100644 --- a/src/dfx/src/commands/canister/rename.rs +++ b/src/dfx/src/commands/canister/rename.rs @@ -2,7 +2,7 @@ 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, stop_canister, update_settings, + get_canister_status, list_canister_snapshots, update_settings, }; use crate::lib::operations::migration_canister::{ MigrationStatus, NNS_MIGRATION_CANISTER_ID, migrate_canister, migrate_status, @@ -14,6 +14,7 @@ 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; @@ -67,27 +68,17 @@ pub async fn exec( )?; } - // Stop both canisters. - debug!( - log, - "Stopping canister {}, with canister_id {}", - from_canister, - from_canister_id.to_text(), - ); - stop_canister(env, from_canister_id, call_sender).await?; - debug!( - log, - "Stopping canister {}, with canister_id {}", - to_canister, - to_canister_id.to_text(), - ); - stop_canister(env, to_canister_id, call_sender).await?; - - // Check the cycles balance of from_canister. let from_status = get_canister_status(env, from_canister_id, call_sender) .await .with_context(|| format!("Could not retrieve status of canister {from_canister}"))?; + let to_status = get_canister_status(env, to_canister_id, call_sender) + .await + .with_context(|| format!("Could not retrieve status of canister {to_canister}"))?; + ensure_canister_stopped(from_status.status, from_canister)?; + ensure_canister_stopped(to_status.status, to_canister)?; + + // Check the cycles balance of from_canister. let cycles = from_status .cycles .0 @@ -135,10 +126,6 @@ pub async fn exec( } // Add the NNS migration canister as a controller to the rename_to canister. - let to_status = get_canister_status(env, to_canister_id, call_sender) - .await - .with_context(|| format!("Could not retrieve status of canister {to_canister}"))?; - let mut controller_added = false; let mut controllers = to_status.settings.controllers.clone(); if !controllers.contains(&NNS_MIGRATION_CANISTER_ID) { @@ -166,7 +153,7 @@ pub async fn exec( let spinner = env.new_spinner("Waiting for renaming to complete...".into()); loop { let statuses = migrate_status(agent, from_canister_id, to_canister_id).await?; - match statuses.last() { + match statuses.first() { Some(MigrationStatus::InProgress { status }) => { spinner.set_message(format!("Renaming in progress: {status}").into()); } @@ -203,11 +190,21 @@ pub async fn exec( update_settings(env, to_canister_id, settings, call_sender).await?; } - // TODO: should we start the TO 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) From 201fed71cfb8cfe17a9afd01703b4a9edafb575a Mon Sep 17 00:00:00 2001 From: Vincent Zhang Date: Tue, 28 Oct 2025 17:48:39 +0800 Subject: [PATCH 08/17] Map ValidationError. --- src/dfx/src/lib/operations/migration_canister.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/dfx/src/lib/operations/migration_canister.rs b/src/dfx/src/lib/operations/migration_canister.rs index 69fe641f1f..d27d8cb5d3 100644 --- a/src/dfx/src/lib/operations/migration_canister.rs +++ b/src/dfx/src/lib/operations/migration_canister.rs @@ -108,11 +108,14 @@ pub async fn migrate_canister( to_canister, }; - let _: () = canister + canister .update(MIGRATE_CANISTER_METHOD) .with_arg(arg) .build() - .await?; + .map(|result: (Result<(), ValidationError>,)| (result.0,)) + .await + .map(|(result,)| result)? + .map_err(|error| anyhow::anyhow!(error))?; Ok(()) } From ba230a4199f77dfd83dfeadf546e6ec125668ab4 Mon Sep 17 00:00:00 2001 From: Vincent Zhang Date: Tue, 28 Oct 2025 17:58:19 +0800 Subject: [PATCH 09/17] Fixed the method name. --- src/dfx/src/lib/operations/migration_canister.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dfx/src/lib/operations/migration_canister.rs b/src/dfx/src/lib/operations/migration_canister.rs index d27d8cb5d3..859adf340f 100644 --- a/src/dfx/src/lib/operations/migration_canister.rs +++ b/src/dfx/src/lib/operations/migration_canister.rs @@ -8,7 +8,7 @@ use std::fmt; 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 MIGRATE_STATUS_METHOD: &str = "migrate_status"; +const MIGRATION_STATUS_METHOD: &str = "migration_status"; #[derive(CandidType)] pub struct MigrateCanisterArg { @@ -136,7 +136,7 @@ pub async fn migrate_status( }; let (result,): (Vec,) = canister - .query(MIGRATE_STATUS_METHOD) + .query(MIGRATION_STATUS_METHOD) .with_arg(arg) .build() .await?; From d0fa4d1884b13d99b1c7f3b2ae728e0bbdae6181 Mon Sep 17 00:00:00 2001 From: Vincent Zhang Date: Tue, 28 Oct 2025 19:06:10 +0800 Subject: [PATCH 10/17] Fixed the wrong canister... --- src/dfx/src/commands/canister/rename.rs | 10 ++++--- .../src/lib/operations/migration_canister.rs | 29 +++++++++---------- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/dfx/src/commands/canister/rename.rs b/src/dfx/src/commands/canister/rename.rs index c6eaf11288..c54fb5e13e 100644 --- a/src/dfx/src/commands/canister/rename.rs +++ b/src/dfx/src/commands/canister/rename.rs @@ -5,7 +5,7 @@ use crate::lib::operations::canister::{ get_canister_status, list_canister_snapshots, update_settings, }; use crate::lib::operations::migration_canister::{ - MigrationStatus, NNS_MIGRATION_CANISTER_ID, migrate_canister, migrate_status, + 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; @@ -152,7 +152,7 @@ pub async fn exec( // Wait for migration to complete. let spinner = env.new_spinner("Waiting for renaming to complete...".into()); loop { - let statuses = migrate_status(agent, from_canister_id, to_canister_id).await?; + let statuses = migration_status(agent, from_canister_id, to_canister_id).await?; match statuses.first() { Some(MigrationStatus::InProgress { status }) => { spinner.set_message(format!("Renaming in progress: {status}").into()); @@ -175,7 +175,7 @@ pub async fn exec( // Remove the NNS migration canister from the controllers if added. if controller_added { - let controllers = to_status.settings.controllers.clone(); + let controllers = from_status.settings.controllers.clone(); let settings = CanisterSettings { controllers: Some(controllers), compute_allocation: None, @@ -187,9 +187,11 @@ pub async fn exec( log_visibility: None, environment_variables: None, }; - update_settings(env, to_canister_id, settings, call_sender).await?; + update_settings(env, from_canister_id, settings, call_sender).await?; } + canister_id_store.remove(log, to_canister)?; + Ok(()) } diff --git a/src/dfx/src/lib/operations/migration_canister.rs b/src/dfx/src/lib/operations/migration_canister.rs index 859adf340f..c15b6e6d81 100644 --- a/src/dfx/src/lib/operations/migration_canister.rs +++ b/src/dfx/src/lib/operations/migration_canister.rs @@ -10,10 +10,10 @@ pub const NNS_MIGRATION_CANISTER_ID: Principal = const MIGRATE_CANISTER_METHOD: &str = "migrate_canister"; const MIGRATION_STATUS_METHOD: &str = "migration_status"; -#[derive(CandidType)] -pub struct MigrateCanisterArg { - pub from_canister: Principal, - pub to_canister: Principal, +#[derive(Clone, CandidType, Deserialize)] +pub struct MigrateCanisterArgs { + pub source: Principal, + pub target: Principal, } #[derive(Clone, Debug, CandidType, Deserialize)] @@ -103,24 +103,21 @@ pub async fn migrate_canister( .with_canister_id(NNS_MIGRATION_CANISTER_ID) .build()?; - let arg = MigrateCanisterArg { - from_canister, - to_canister, + let arg = MigrateCanisterArgs { + source: from_canister, + target: to_canister, }; - canister + let _: () = canister .update(MIGRATE_CANISTER_METHOD) .with_arg(arg) .build() - .map(|result: (Result<(), ValidationError>,)| (result.0,)) - .await - .map(|(result,)| result)? - .map_err(|error| anyhow::anyhow!(error))?; + .await?; Ok(()) } -pub async fn migrate_status( +pub async fn migration_status( agent: &Agent, from_canister: Principal, to_canister: Principal, @@ -130,9 +127,9 @@ pub async fn migrate_status( .with_canister_id(NNS_MIGRATION_CANISTER_ID) .build()?; - let arg = MigrateCanisterArg { - from_canister, - to_canister, + let arg = MigrateCanisterArgs { + source: from_canister, + target: to_canister, }; let (result,): (Vec,) = canister From a8386ed639592926e77878e9078bc4be13cf59ea Mon Sep 17 00:00:00 2001 From: Vincent Zhang Date: Tue, 28 Oct 2025 19:49:21 +0800 Subject: [PATCH 11/17] Updated to 'dfx canister migrate-id' --- .../canister/{rename.rs => migrate_id.rs} | 66 +++++++++---------- src/dfx/src/commands/canister/mod.rs | 6 +- 2 files changed, 36 insertions(+), 36 deletions(-) rename src/dfx/src/commands/canister/{rename.rs => migrate_id.rs} (74%) diff --git a/src/dfx/src/commands/canister/rename.rs b/src/dfx/src/commands/canister/migrate_id.rs similarity index 74% rename from src/dfx/src/commands/canister/rename.rs rename to src/dfx/src/commands/canister/migrate_id.rs index c54fb5e13e..48f443d174 100644 --- a/src/dfx/src/commands/canister/rename.rs +++ b/src/dfx/src/commands/canister/migrate_id.rs @@ -20,16 +20,16 @@ use slog::{debug, error, info}; use std::time::Duration; use time::{OffsetDateTime, macros::format_description}; -/// Renames a canister. +/// Migrate a canister ID from one subnet to another. #[derive(Parser)] -#[command(override_usage = "dfx canister rename [OPTIONS] --rename-to ")] -pub struct CanisterRenameOpts { - /// Specifies the name or id of the canister to rename. +#[command(override_usage = "dfx canister migrate-id [OPTIONS] --replace ")] +pub struct CanisterMigrateIdOpts { + /// Specifies the name or id of the canister to migrate. from_canister: String, - /// Specifies the name or id of the canister to rename to. + /// Specifies the name or id of the canister to replace. #[arg(long)] - rename_to: String, + replace: String, /// Skips yes/no checks by answering 'yes'. Not recommended outside of CI. #[arg(long, short)] @@ -38,7 +38,7 @@ pub struct CanisterRenameOpts { pub async fn exec( env: &dyn Environment, - opts: CanisterRenameOpts, + opts: CanisterMigrateIdOpts, call_sender: &CallSender, ) -> DfxResult { fetch_root_key_if_needed(env).await?; @@ -49,21 +49,21 @@ pub async fn exec( // Get the canister IDs. let from_canister = opts.from_canister.as_str(); - let to_canister = opts.rename_to.as_str(); + let target_canister = opts.replace.as_str(); let from_canister_id = Principal::from_text(from_canister).or_else(|_| canister_id_store.get(from_canister))?; - let to_canister_id = - Principal::from_text(to_canister).or_else(|_| canister_id_store.get(to_canister))?; + let target_canister_id = Principal::from_text(target_canister) + .or_else(|_| canister_id_store.get(target_canister))?; - if from_canister_id == to_canister_id { - bail!("From and rename_to canister IDs are the same"); + if from_canister_id == target_canister_id { + bail!("From and target canister IDs are the same"); } if !opts.yes { ask_for_consent( env, &format!( - "The from canister '{from_canister}' will be removed from its own subnet. Continue anyway?", + "The target canister '{target_canister}' will be removed from its own subnet. Continue anyway?", ), )?; } @@ -71,12 +71,12 @@ pub async fn exec( let from_status = get_canister_status(env, from_canister_id, call_sender) .await .with_context(|| format!("Could not retrieve status of canister {from_canister}"))?; - let to_status = get_canister_status(env, to_canister_id, call_sender) + let target_status = get_canister_status(env, target_canister_id, call_sender) .await - .with_context(|| format!("Could not retrieve status of canister {to_canister}"))?; + .with_context(|| format!("Could not retrieve status of canister {target_canister}"))?; ensure_canister_stopped(from_status.status, from_canister)?; - ensure_canister_stopped(to_status.status, to_canister)?; + ensure_canister_stopped(target_status.status, target_canister)?; // Check the cycles balance of from_canister. let cycles = from_status @@ -94,17 +94,17 @@ pub async fn exec( )?; } - // Check that the from canister has no snapshots. - let from_snapshots = list_canister_snapshots(env, from_canister_id, call_sender).await?; - if !from_snapshots.is_empty() { - bail!("The from canister {} has snapshots", from_canister); + // Check that the target canister has no snapshots. + let snapshots = list_canister_snapshots(env, target_canister_id, call_sender).await?; + if !snapshots.is_empty() { + bail!("The target canister {} has snapshots", target_canister); } // Check that the two canisters are on different subnets. let from_subnet = get_subnet_for_canister(agent, from_canister_id).await?; - let to_subnet = get_subnet_for_canister(agent, to_canister_id).await?; - if from_subnet == to_subnet { - bail!("The from and rename_to canisters are on the same subnet"); + let target_subnet = get_subnet_for_canister(agent, target_canister_id).await?; + if from_subnet == target_subnet { + bail!("The from and target canisters are on the same subnet"); } // Add the NNS migration canister as a controller to the from canister. @@ -127,7 +127,7 @@ pub async fn exec( // Add the NNS migration canister as a controller to the rename_to canister. let mut controller_added = false; - let mut controllers = to_status.settings.controllers.clone(); + let mut controllers = target_status.settings.controllers.clone(); if !controllers.contains(&NNS_MIGRATION_CANISTER_ID) { controllers.push(NNS_MIGRATION_CANISTER_ID); let settings = CanisterSettings { @@ -141,30 +141,30 @@ pub async fn exec( log_visibility: None, environment_variables: None, }; - update_settings(env, to_canister_id, settings, call_sender).await?; + update_settings(env, target_canister_id, settings, call_sender).await?; controller_added = true; } // Migrate the from canister to the rename_to canister. - debug!(log, "Renaming {from_canister} to {to_canister}"); - migrate_canister(agent, from_canister_id, to_canister_id).await?; + debug!(log, "Migrate {from_canister} to {target_canister}"); + migrate_canister(agent, from_canister_id, target_canister_id).await?; // Wait for migration to complete. - let spinner = env.new_spinner("Waiting for renaming to complete...".into()); + let spinner = env.new_spinner("Waiting for migration to complete...".into()); loop { - let statuses = migration_status(agent, from_canister_id, to_canister_id).await?; + let statuses = migration_status(agent, from_canister_id, target_canister_id).await?; match statuses.first() { Some(MigrationStatus::InProgress { status }) => { - spinner.set_message(format!("Renaming in progress: {status}").into()); + spinner.set_message(format!("Migration in progress: {status}").into()); } Some(MigrationStatus::Succeeded { time }) => { spinner.finish_and_clear(); - info!(log, "Renaming succeeded at {}", format_time(time)); + info!(log, "Migration succeeded at {}", format_time(time)); break; } Some(MigrationStatus::Failed { reason, time }) => { spinner.finish_and_clear(); - error!(log, "Renaming failed at {}: {}", format_time(time), reason); + error!(log, "Migration failed at {}: {}", format_time(time), reason); break; } None => (), @@ -190,7 +190,7 @@ pub async fn exec( update_settings(env, from_canister_id, settings, call_sender).await?; } - canister_id_store.remove(log, to_canister)?; + canister_id_store.remove(log, target_canister)?; Ok(()) } diff --git a/src/dfx/src/commands/canister/mod.rs b/src/dfx/src/commands/canister/mod.rs index 56b5b8437c..b6543794d0 100644 --- a/src/dfx/src/commands/canister/mod.rs +++ b/src/dfx/src/commands/canister/mod.rs @@ -15,7 +15,7 @@ mod info; mod install; mod logs; mod metadata; -mod rename; +mod migrate_id; mod request_status; mod send; mod set_id; @@ -54,7 +54,7 @@ pub enum SubCommand { Info(info::InfoOpts), Install(install::CanisterInstallOpts), Metadata(metadata::CanisterMetadataOpts), - Rename(rename::CanisterRenameOpts), + MigrateId(migrate_id::CanisterMigrateIdOpts), RequestStatus(request_status::RequestStatusOpts), Send(send::CanisterSendOpts), SetId(set_id::CanisterSetIdOpts), @@ -90,7 +90,7 @@ 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::Rename(v) => rename::exec(env, v, &call_sender()?).await, + SubCommand::MigrateId(v) => migrate_id::exec(env, v, &call_sender()?).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, From ad764465136b12b10c99b28f778161b4e5b1b835 Mon Sep 17 00:00:00 2001 From: Vincent Zhang Date: Tue, 28 Oct 2025 20:46:49 +0800 Subject: [PATCH 12/17] Addressed review comments. --- src/dfx/src/commands/canister/migrate_id.rs | 61 +++++++++++---------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/src/dfx/src/commands/canister/migrate_id.rs b/src/dfx/src/commands/canister/migrate_id.rs index 48f443d174..551fdfe6d2 100644 --- a/src/dfx/src/commands/canister/migrate_id.rs +++ b/src/dfx/src/commands/canister/migrate_id.rs @@ -22,10 +22,10 @@ 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 ")] +#[command(override_usage = "dfx canister migrate-id [OPTIONS] --replace ")] pub struct CanisterMigrateIdOpts { /// Specifies the name or id of the canister to migrate. - from_canister: String, + canister: String, /// Specifies the name or id of the canister to replace. #[arg(long)] @@ -48,67 +48,70 @@ pub async fn exec( let canister_id_store = env.get_canister_id_store()?; // Get the canister IDs. - let from_canister = opts.from_canister.as_str(); + let source_canister = opts.canister.as_str(); let target_canister = opts.replace.as_str(); - let from_canister_id = - Principal::from_text(from_canister).or_else(|_| canister_id_store.get(from_canister))?; + let source_canister_id = Principal::from_text(source_canister) + .or_else(|_| canister_id_store.get(source_canister))?; let target_canister_id = Principal::from_text(target_canister) .or_else(|_| canister_id_store.get(target_canister))?; - if from_canister_id == target_canister_id { - bail!("From and target canister IDs are the same"); + if source_canister_id == target_canister_id { + bail!("The canisters to migrate and replace are identical."); } if !opts.yes { ask_for_consent( env, - &format!( - "The target canister '{target_canister}' will be removed from its own subnet. Continue anyway?", - ), + &format!("Canister '{source_canister}' will be removed from its own subnet. Continue?"), )?; } - let from_status = get_canister_status(env, from_canister_id, call_sender) + let source_status = get_canister_status(env, source_canister_id, call_sender) .await - .with_context(|| format!("Could not retrieve status of canister {from_canister}"))?; + .with_context(|| format!("Could not retrieve status of canister {source_canister}"))?; let target_status = get_canister_status(env, target_canister_id, call_sender) .await .with_context(|| format!("Could not retrieve status of canister {target_canister}"))?; - ensure_canister_stopped(from_status.status, from_canister)?; + ensure_canister_stopped(source_status.status, source_canister)?; ensure_canister_stopped(target_status.status, target_canister)?; - // Check the cycles balance of from_canister. - let cycles = from_status + // Check the cycles balance of source_canister. + let cycles = source_status .cycles .0 .to_u128() .expect("Unable to parse cycles"); if cycles < 5_000_000_000_000 { - bail!("The from canister {from_canister} has less than 5T cycles"); + bail!("Canister '{source_canister}' has less than 5T cycles"); } if !opts.yes && cycles > 10_000_000_000_000 { ask_for_consent( env, - &format!("The from canister {from_canister} has more than 10T cycles. Continue?"), + &format!( + "Canister '{source_canister}' has more than 10T cycles. The extra cycles will get burned during the migration. Continue?" + ), )?; } // Check that the target canister has no snapshots. let snapshots = list_canister_snapshots(env, target_canister_id, call_sender).await?; if !snapshots.is_empty() { - bail!("The target canister {} has snapshots", target_canister); + bail!( + "The canister to be replaced '{}' has snapshots", + target_canister + ); } // Check that the two canisters are on different subnets. - let from_subnet = get_subnet_for_canister(agent, from_canister_id).await?; + let source_subnet = get_subnet_for_canister(agent, source_canister_id).await?; let target_subnet = get_subnet_for_canister(agent, target_canister_id).await?; - if from_subnet == target_subnet { + if source_subnet == target_subnet { bail!("The from and target canisters are on the same subnet"); } - // Add the NNS migration canister as a controller to the from canister. - let mut controllers = from_status.settings.controllers.clone(); + // Add the NNS migration canister as a controller to the source canister. + let mut controllers = source_status.settings.controllers.clone(); if !controllers.contains(&NNS_MIGRATION_CANISTER_ID) { controllers.push(NNS_MIGRATION_CANISTER_ID); let settings = CanisterSettings { @@ -122,10 +125,10 @@ pub async fn exec( log_visibility: None, environment_variables: None, }; - update_settings(env, from_canister_id, settings, call_sender).await?; + update_settings(env, source_canister_id, settings, call_sender).await?; } - // Add the NNS migration canister as a controller to the rename_to canister. + // Add the NNS migration canister as a controller to the target canister. let mut controller_added = false; let mut controllers = target_status.settings.controllers.clone(); if !controllers.contains(&NNS_MIGRATION_CANISTER_ID) { @@ -146,13 +149,13 @@ pub async fn exec( } // Migrate the from canister to the rename_to canister. - debug!(log, "Migrate {from_canister} to {target_canister}"); - migrate_canister(agent, from_canister_id, target_canister_id).await?; + debug!(log, "Migrate '{source_canister}' to '{target_canister}'"); + migrate_canister(agent, source_canister_id, target_canister_id).await?; // Wait for migration to complete. let spinner = env.new_spinner("Waiting for migration to complete...".into()); loop { - let statuses = migration_status(agent, from_canister_id, target_canister_id).await?; + let statuses = migration_status(agent, source_canister_id, target_canister_id).await?; match statuses.first() { Some(MigrationStatus::InProgress { status }) => { spinner.set_message(format!("Migration in progress: {status}").into()); @@ -175,7 +178,7 @@ pub async fn exec( // Remove the NNS migration canister from the controllers if added. if controller_added { - let controllers = from_status.settings.controllers.clone(); + let controllers = source_status.settings.controllers.clone(); let settings = CanisterSettings { controllers: Some(controllers), compute_allocation: None, @@ -187,7 +190,7 @@ pub async fn exec( log_visibility: None, environment_variables: None, }; - update_settings(env, from_canister_id, settings, call_sender).await?; + update_settings(env, source_canister_id, settings, call_sender).await?; } canister_id_store.remove(log, target_canister)?; From 8859433b1c6b5053d45d38c279ef3401f46529c9 Mon Sep 17 00:00:00 2001 From: Vincent Zhang Date: Tue, 28 Oct 2025 21:39:22 +0800 Subject: [PATCH 13/17] Set the cycles minimum value to 10T and maximum value to 15T --- src/dfx/src/commands/canister/migrate_id.rs | 10 +++++----- .../{migration_canister.rs => canister_migration.rs} | 0 src/dfx/src/lib/operations/mod.rs | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) rename src/dfx/src/lib/operations/{migration_canister.rs => canister_migration.rs} (100%) diff --git a/src/dfx/src/commands/canister/migrate_id.rs b/src/dfx/src/commands/canister/migrate_id.rs index 551fdfe6d2..326469f65f 100644 --- a/src/dfx/src/commands/canister/migrate_id.rs +++ b/src/dfx/src/commands/canister/migrate_id.rs @@ -4,7 +4,7 @@ use crate::lib::ic_attributes::CanisterSettings; use crate::lib::operations::canister::{ get_canister_status, list_canister_snapshots, update_settings, }; -use crate::lib::operations::migration_canister::{ +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; @@ -82,14 +82,14 @@ pub async fn exec( .0 .to_u128() .expect("Unable to parse cycles"); - if cycles < 5_000_000_000_000 { - bail!("Canister '{source_canister}' has less than 5T cycles"); + if cycles < 10_000_000_000_000 { + bail!("Canister '{source_canister}' has less than 10T cycles"); } - if !opts.yes && cycles > 10_000_000_000_000 { + if !opts.yes && cycles > 15_000_000_000_000 { ask_for_consent( env, &format!( - "Canister '{source_canister}' has more than 10T cycles. The extra cycles will get burned during the migration. Continue?" + "Canister '{source_canister}' has more than 15T cycles. The extra cycles will get burned during the migration. Continue?" ), )?; } diff --git a/src/dfx/src/lib/operations/migration_canister.rs b/src/dfx/src/lib/operations/canister_migration.rs similarity index 100% rename from src/dfx/src/lib/operations/migration_canister.rs rename to src/dfx/src/lib/operations/canister_migration.rs diff --git a/src/dfx/src/lib/operations/mod.rs b/src/dfx/src/lib/operations/mod.rs index 50867de4c2..d68b65d657 100644 --- a/src/dfx/src/lib/operations/mod.rs +++ b/src/dfx/src/lib/operations/mod.rs @@ -1,8 +1,8 @@ pub mod canister; +pub mod canister_migration; pub mod cmc; pub mod cycles_ledger; pub mod ledger; -pub mod migration_canister; const ICRC1_BALANCE_OF_METHOD: &str = "icrc1_balance_of"; const ICRC1_TRANSFER_METHOD: &str = "icrc1_transfer"; From 6aacb445b933fa50d4d8d89f57fa78fef03268ba Mon Sep 17 00:00:00 2001 From: Vincent Zhang Date: Tue, 28 Oct 2025 21:39:39 +0800 Subject: [PATCH 14/17] Add a new e2e test --- e2e/tests-dfx/canister_migration.bash | 41 +++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100755 e2e/tests-dfx/canister_migration.bash diff --git a/e2e/tests-dfx/canister_migration.bash b/e2e/tests-dfx/canister_migration.bash new file mode 100755 index 0000000000..e34a52e488 --- /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 -> source, and add target canister + jq '.canisters.source = .canisters.hello_backend | del(.canisters.hello_backend)' dfx.json | sponge dfx.json + jq '.canisters.target = { "main": "counter.mo", "type": "motoko" }' dfx.json | sponge dfx.json + + # Deploy the source to the application subnet. + dfx deploy source + + # Create the target canister on the fiduciary subnet. + dfx canister create target --subnet-type fiduciary + + dfx canister stop source + dfx canister stop target + + # Make sure the source has enough cycles to do the migration. + dfx ledger fabricate-cycles --canister source --cycles 10000000000000 + + # The migration will take a few minutes to complete. + assert_command dfx canister migrate-id source --replace target --yes + assert_contains "Migration succeeded" + + assert_command dfx canister status source + assert_command_fail dfx canister status target +} From 190c1323e73c1bd6f92785482eb7386a4b0fd4c4 Mon Sep 17 00:00:00 2001 From: Vincent Zhang Date: Tue, 28 Oct 2025 21:51:36 +0800 Subject: [PATCH 15/17] No need to remove the migration canister as it will remove on its own --- src/dfx/src/commands/canister/migrate_id.rs | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/src/dfx/src/commands/canister/migrate_id.rs b/src/dfx/src/commands/canister/migrate_id.rs index 326469f65f..3f41cd5790 100644 --- a/src/dfx/src/commands/canister/migrate_id.rs +++ b/src/dfx/src/commands/canister/migrate_id.rs @@ -129,7 +129,6 @@ pub async fn exec( } // Add the NNS migration canister as a controller to the target canister. - let mut controller_added = false; let mut controllers = target_status.settings.controllers.clone(); if !controllers.contains(&NNS_MIGRATION_CANISTER_ID) { controllers.push(NNS_MIGRATION_CANISTER_ID); @@ -145,7 +144,6 @@ pub async fn exec( environment_variables: None, }; update_settings(env, target_canister_id, settings, call_sender).await?; - controller_added = true; } // Migrate the from canister to the rename_to canister. @@ -176,23 +174,6 @@ pub async fn exec( tokio::time::sleep(Duration::from_secs(1)).await; } - // Remove the NNS migration canister from the controllers if added. - if controller_added { - let controllers = source_status.settings.controllers.clone(); - 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, source_canister_id, settings, call_sender).await?; - } - canister_id_store.remove(log, target_canister)?; Ok(()) From 4a031c1ec6c7fbdf011bf28d2f96c44c90e72802 Mon Sep 17 00:00:00 2001 From: Vincent Zhang Date: Wed, 29 Oct 2025 11:40:01 +0800 Subject: [PATCH 16/17] Implemented 'dfx canister migration-status' --- .../src/commands/canister/migration_status.rs | 135 ++++++++++++++++++ src/dfx/src/commands/canister/mod.rs | 3 + 2 files changed, 138 insertions(+) create mode 100644 src/dfx/src/commands/canister/migration_status.rs 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..ec606d50df --- /dev/null +++ b/src/dfx/src/commands/canister/migration_status.rs @@ -0,0 +1,135 @@ +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 source_canister = opts.canister.as_str(); + let target_canister = opts.replace.as_str(); + let source_canister_id = Principal::from_text(source_canister) + .or_else(|_| canister_id_store.get(source_canister)) + .map_err(|_| { + anyhow::anyhow!( + "Cannot find canister '{source_canister}'. Please use canister id instead" + ) + })?; + let target_canister_id = Principal::from_text(target_canister) + .or_else(|_| canister_id_store.get(target_canister)) + .map_err(|_| { + anyhow::anyhow!( + "Cannot find canister '{target_canister}'. Please use canister id instead" + ) + })?; + + let statuses = migration_status(agent, source_canister_id, target_canister_id).await?; + + if statuses.is_empty() { + info!( + log, + "No migration status found for canister '{source_canister}' to '{target_canister}'" + ); + return Ok(()); + } + + // Print the statuses in a table with aligned columns. + let source_text = source_canister_id.to_text(); + let target_text = target_canister_id.to_text(); + let status_strings: Vec = statuses.iter().map(format_status).collect(); + + let header_source = "Canister"; + let header_target = "Canister To Be Replaced"; + let header_status = "Migration Status"; + + let source_width = header_source.len().max(source_text.len()); + let target_width = header_target.len().max(target_text.len()); + let status_width = header_status + .len() + .max(status_strings.iter().map(|s| s.len()).max().unwrap_or(0)); + + let sep_source = "-".repeat(source_width); + let sep_target = "-".repeat(target_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 b6543794d0..ccc0520cd5 100644 --- a/src/dfx/src/commands/canister/mod.rs +++ b/src/dfx/src/commands/canister/mod.rs @@ -16,6 +16,7 @@ mod install; mod logs; mod metadata; mod migrate_id; +mod migration_status; mod request_status; mod send; mod set_id; @@ -55,6 +56,7 @@ pub enum SubCommand { 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), @@ -91,6 +93,7 @@ pub fn exec(env: &dyn Environment, opts: CanisterOpts) -> DfxResult { 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, From b4d58b70add877a1b87deea390ee19b8ac4a45cd Mon Sep 17 00:00:00 2001 From: Vincent Zhang Date: Wed, 29 Oct 2025 11:42:50 +0800 Subject: [PATCH 17/17] Addressed review comments. --- src/dfx/src/commands/canister/migrate_id.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dfx/src/commands/canister/migrate_id.rs b/src/dfx/src/commands/canister/migrate_id.rs index 3f41cd5790..cf7d1af695 100644 --- a/src/dfx/src/commands/canister/migrate_id.rs +++ b/src/dfx/src/commands/canister/migrate_id.rs @@ -98,7 +98,7 @@ pub async fn exec( let snapshots = list_canister_snapshots(env, target_canister_id, call_sender).await?; if !snapshots.is_empty() { bail!( - "The canister to be replaced '{}' has snapshots", + "The canister '{}' whose canister ID will be replaced has snapshots", target_canister ); } @@ -107,7 +107,7 @@ pub async fn exec( let source_subnet = get_subnet_for_canister(agent, source_canister_id).await?; let target_subnet = get_subnet_for_canister(agent, target_canister_id).await?; if source_subnet == target_subnet { - bail!("The from and target canisters are on the same subnet"); + bail!("The canisters '{source_canister}' and '{target_canister}' are on the same subnet"); } // Add the NNS migration canister as a controller to the source canister.