Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 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
6b514c7
changelog
mraszyk Nov 26, 2025
19a250a
types
mraszyk Nov 26, 2025
c09b506
bump agent-rs and ic-management-canister-types
mraszyk Dec 1, 2025
6e6ff3d
Merge branch 'master' into vincent/SDK-2044
mraszyk Dec 1, 2025
b0342f2
Merge branch 'master' into vincent/SDK-2044
mraszyk Dec 1, 2025
ebfe083
fix lock file
mraszyk Dec 1, 2025
f0587fa
Merge branch 'master' into vincent/SDK-2044
mraszyk Dec 1, 2025
4059be3
agent-rs
mraszyk Dec 1, 2025
f257cab
retry failed migration status calls
mraszyk Dec 2, 2025
a0d33f4
lint
mraszyk Dec 2, 2025
12026c7
check that the two canisters are ready for migration
mraszyk Dec 2, 2025
cfe7a6a
fix
mraszyk Dec 2, 2025
631489f
fix types
mraszyk Dec 10, 2025
bb14a78
more readble error messages for validation
mraszyk Dec 11, 2025
1519c53
types
mraszyk Dec 12, 2025
884722a
source/target -> migrated/replaced
mraszyk Dec 12, 2025
26f676f
fmt
mraszyk Dec 14, 2025
44e2e2f
rename
mraszyk Dec 14, 2025
9124e19
rename source/target to migrated/replaced
mraszyk Dec 15, 2025
0d34796
Merge branch 'master' into vincent/SDK-2044
mraszyk Dec 19, 2025
2b1cca6
use thiserror
mraszyk Dec 19, 2025
ff5f831
fix variants
mraszyk Dec 19, 2025
ba58410
replace_canister_id -> replaced_canister_id
mraszyk Dec 19, 2025
a174fef
Merge branch 'master' into vincent/SDK-2044
mraszyk Jan 13, 2026
73762e2
Merge branch 'master' into vincent/SDK-2044
mraszyk Jan 13, 2026
e5b66fa
Merge branch 'master' into vincent/SDK-2044
mraszyk Jan 14, 2026
f7f68b1
final fixes
mraszyk Jan 14, 2026
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

### feat: Wasm optimization failure issues a warning instead of error

The optimization functionality provided by `ic_wasm::optimize" cannot handle Wasm modules that contains 64-bit table.
Expand Down
86 changes: 86 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,90 @@ service : {
}
```

## dfx canister migrate-id

Use the `dfx canister migrate-id` command to perform canister ID migration
of a canister (called the "migrated" canister) on one subnet
to another subnet replacing the canister ID of a canister
(called the "replaced" canister) on that other subnet.

### Basic usage

``` bash
dfx canister migrate-id <canister> --replace <replaced>
```

### 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 (called the "migrated" canister) on one subnet
to another subnet replacing the canister ID of a canister
(called the "replaced" canister) on that other subnet.

### Basic usage

``` bash
dfx canister migration-status <canister> --replace <replaced>
```

### 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: MigratedCanisterDeleted |
```

## 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 -> 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
}
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
224 changes: 224 additions & 0 deletions src/dfx/src/commands/canister/migrate_id.rs
Original file line number Diff line number Diff line change
@@ -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] <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 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()
.context("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()
}
Loading