Child canisters on other subnets

The dfx canister create command has a --subnet option which can be used to deploy on a specific subnet.

If a canister dynamically creates a child canister is there any way to have a similar choice and to get the child on a different subnet than the parent?

1 Like

I recently achieved this by calling the CMC create_canister

use candid::{CandidType, Nat, Principal};
use ic_cdk::{api::management_canister::main::CanisterSettings, update};
use ic_ledger_types::MAINNET_CYCLES_MINTING_CANISTER_ID;
use serde::Deserialize;

#[derive(CandidType, Deserialize)]
struct CreateCanisterArg {
    settings: Option<CanisterSettings>,
    subnet_type: Option<String>,
    subnet_selection: Option<SubnetSelection>,
}

#[derive(CandidType, Deserialize)]
enum SubnetSelection {
    Subnet { subnet: Principal },
    Filter { filter: SubnetFilter },
}

#[derive(CandidType, Deserialize)]
struct SubnetFilter {
    subnet_type: Option<String>,
}

#[derive(CandidType, Deserialize)]
enum CreateCanisterResult {
    Ok(Principal),
    Err(CreateCanisterError),
}

#[derive(CandidType, Deserialize)]
enum CreateCanisterError {
    Refunded {
        refund_amount: Nat,
        create_error: String,
    },
    RefundFailed {
        create_error: String,
        refund_error: String,
    },
}

#[update]
async fn create_canister(subnet: String) -> Principal {
    let settings = CanisterSettings {
        controllers: Some(vec![
            // dev wallet
            Principal::from_text("uc5c3-viaaa-aaaal-qaaja-cai").unwrap(),
            // dev id
            Principal::from_text("hopg6-qb5bk-fywb2-44xhe-2ovp4-stuku-oitob-vkpim-zrip3-7f375-rae")
                .unwrap(),
        ]),
        compute_allocation: None,
        memory_allocation: None,
        freezing_threshold: None,
        reserved_cycles_limit: None,
    };

    let create_canister_result: CreateCanisterResult =
        ic_cdk::api::call::call_with_payment::<(CreateCanisterArg,), (CreateCanisterResult,)>(
            MAINNET_CYCLES_MINTING_CANISTER_ID,
            "create_canister",
            (CreateCanisterArg {
                settings: Some(settings),
                subnet_type: None,
                subnet_selection: Some(SubnetSelection::Subnet {
                    subnet: Principal::from_text(subnet).unwrap(),
                }),
            },),
            // 5T cycles
            5_000_000_000_000,
        )
        .await
        .expect("Error calling create_canister")
        .0;

    match create_canister_result {
        CreateCanisterResult::Ok(canister_id) => return canister_id,
        CreateCanisterResult::Err(create_canister_error) => match create_canister_error {
            CreateCanisterError::Refunded {
                refund_amount,
                create_error,
            } => {
                ic_cdk::trap(&format!(
                    "Refunded {} cycles due to error: {}",
                    refund_amount, create_error
                ));
            }
            CreateCanisterError::RefundFailed {
                create_error,
                refund_error,
            } => {
                ic_cdk::trap(&format!(
                    "Error creating canister: {}. Error refunding cycles: {}",
                    create_error, refund_error
                ));
            }
        },
    }
}
1 Like

And btw, if I knew we had a dfx command for that, I would not have spent an hour writing that code!

Nice to have though

I should also add that I tried the application subnets with lowest canister count first. Most failed until I got to subnets with ~4k canisters

Any idea why?

dfx supports --subnet, --subnet-type, and --next-to <canister>

The CMC has a list of ‘default subnets’ (plus some lists with public access, e.g. the fiduciary subnet type). If you don’t pick a subnet on one of these public lists then the CMC will not let you create a canister there. I would assume that the subnets with low canister counts simply are not on these lists

1 Like

If this is a feature now I’d imagine we’d need a command in motoko similar to ExperimentalCycles.add() to direct the subnet.

ExperamentalC.subnet(xxxxx);
let x = await myActor.Actor(initargs);

I know the ideal design of motoko was to try to abstract away this layer, but in reality, we sometimes need it.

Doesn’t the CMC canister require payment in ICP to create a canister? Or can I get away with cycles only here?

As you can see in the code, i used a call_with_payment

No ledger transfers, just cycles from the canister within the call

notify_create_canister requires ICP. As part of the cycles ledger work I added create_canister on the CMC, which uses cycles attached to the call like the management canisters’s create_canister does it too

1 Like

So it is relatively new that this is possible?

Just double-checking my understanding. If I use the management canister’s create_canister then I get a canister on the same subnet as the canister that is making the call?

1 Like

Yes, it’s been available a month or three.

You are right. If you go through the management canister you always land on the subnet you make the call from

1 Like

@timo a possible workaround in Motoko might be to first create the canister id on a specific subnet with the CMC, and then use the lower level (system Lib.Class)(#install ...id..)(args) syntax to install the code to that canister id. Haven’t tried it though.

This might give you some inspiration…

2 Likes

@Severin do you know if there are any plans to unify this? It seems a bit weird that there are two canisters I can call (management and CMC) to create a child canister, and they allow different arguments. Will it be possible in the future to specify a subnet when creating a child through the management canister?

1 Like

It’s possible, but we don’t have any plans for now. While it would be possible, it would be surprisingly complex since it would break a few assumptions both in the management canister and the CMC implementations. Not that it would be super hard, just more work than it sounds like at first

And can I test this entire flow locally? I mean can I deploy a CMC on a local replica and when calling create_canister on it then it will actually create one?

I just tried this and it did not work for me locally

Reject text: IC0503: Canister bkyz2-fmaaa-aaaaa-qaaaq-cai trapped explicitly: Couldn't create canister: #Refunded({create_error = "No subnets in which to create a canister."; refund_amount = 99_994_900_000_000})

You can find the code here.

FYI, currently it’s not easily possible to get the CMC running locally with full functionality. We’re working on fixing this by polishing the dfx nns extension.