Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
5c02350
Add 'dfx canister rename' subcommand stub.
vincent-dfinity Oct 28, 2025
8f689ed
Update to the revision that pocket-ic added support for migration can…
vincent-dfinity Oct 28, 2025
e8986b1
Implemented 'dfx canister rename'.
vincent-dfinity Oct 28, 2025
e8a66af
Check snapshot and add --yes flag.
vincent-dfinity Oct 28, 2025
4cd85de
Add spinner to update the renaming status.
vincent-dfinity Oct 28, 2025
5c1ead5
Remove the NNS canister from the controllers.
vincent-dfinity Oct 28, 2025
2d9134e
Ensure the canisters are stopped instead of stopping them.
vincent-dfinity Oct 28, 2025
201fed7
Map ValidationError.
vincent-dfinity Oct 28, 2025
ba230a4
Fixed the method name.
vincent-dfinity Oct 28, 2025
d0fa4d1
Fixed the wrong canister...
vincent-dfinity Oct 28, 2025
a8386ed
Updated to 'dfx canister migrate-id'
vincent-dfinity Oct 28, 2025
ad76446
Addressed review comments.
vincent-dfinity Oct 28, 2025
8859433
Set the cycles minimum value to 10T and maximum value to 15T
vincent-dfinity Oct 28, 2025
6aacb44
Add a new e2e test
vincent-dfinity Oct 28, 2025
190c132
No need to remove the migration canister as it will remove on its own
vincent-dfinity Oct 28, 2025
69f229f
Merge branch 'master' into vincent/SDK-2044
vincent-dfinity Oct 29, 2025
4a031c1
Implemented 'dfx canister migration-status'
vincent-dfinity Oct 29, 2025
b4d58b7
Addressed review comments.
vincent-dfinity Oct 29, 2025
5846899
update replica to elected version
viviveevee Nov 17, 2025
49cc88d
docs
mraszyk Nov 26, 2025
e347696
Merge branch 'master' into vincent/SDK-2044
mraszyk Nov 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions docs/cli-reference/dfx-canister.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down Expand Up @@ -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 <canister> --replace <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 <canister> --replace <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: SourceDeleted |
```

## dfx canister request-status

Use the `dfx canister request-status` command to request the status of a call to a canister. This command
Expand Down
41 changes: 41 additions & 0 deletions e2e/tests-dfx/canister_migration.bash
Original file line number Diff line number Diff line change
@@ -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
}
2 changes: 1 addition & 1 deletion src/dfx/src/actors/pocketic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
200 changes: 200 additions & 0 deletions src/dfx/src/commands/canister/migrate_id.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
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] <CANISTER> --replace <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 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))?;
let target_canister_id = Principal::from_text(target_canister)
.or_else(|_| canister_id_store.get(target_canister))?;

if source_canister_id == target_canister_id {
bail!("The canisters to migrate and replace are identical.");
}

if !opts.yes {
ask_for_consent(
env,
&format!("Canister '{source_canister}' will be removed from its own subnet. Continue?"),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could additionally check if the canister is uninstalled and has any data - e.g., snapshots.

)?;
}

let source_status = get_canister_status(env, source_canister_id, call_sender)
.await
.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(source_status.status, source_canister)?;
ensure_canister_stopped(target_status.status, target_canister)?;

// Check the cycles balance of source_canister.
let cycles = source_status
.cycles
.0
.to_u128()
.expect("Unable to parse cycles");
if cycles < 10_000_000_000_000 {
bail!("Canister '{source_canister}' has less than 10T cycles");
}
if !opts.yes && cycles > 15_000_000_000_000 {
ask_for_consent(
env,
&format!(
"Canister '{source_canister}' has more than 15T 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 canister '{}' whose canister ID will be replaced has snapshots",
target_canister
);
}

// Check that the two canisters are on different subnets.
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 canisters '{source_canister}' and '{target_canister}' are on the same subnet");
}

// 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 {
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?;
}

// Add the NNS migration canister as a controller to the target canister.
let mut controllers = target_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, target_canister_id, settings, call_sender).await?;
}

// Migrate the from canister to the rename_to canister.
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, source_canister_id, target_canister_id).await?;
match statuses.first() {
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 => (),
}

tokio::time::sleep(Duration::from_secs(1)).await;
}

canister_id_store.remove(log, target_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()
}
Loading
Loading