mcpField
C
cycles-management

cycles-management

github.com/dfinity云与 DevOps39301 次安装今天更新
代码仓库

关于 cycles-management

cycles-management 是面向 Internet Computer 开发者的技能资料,说明 canister cycles 的余额、充值、创建与管理方式。它结合 Motoko、Rust 示例和 icp-cli 操作,重点提示冻结、资金误转及控制器配置等风险。

主要功能

  1. 01检查与接收 cycles
  2. 02创建和管理 canister
  3. 03使用 icp-cli 充值与查询
  4. 04配置冻结阈值和控制器
  5. 05提供 Motoko 与 Rust 示例

适用场景

  1. 01部署前估算并检查 cycles 余额
  2. 02为主网 canister 充值并调整阈值
  3. 03实现 canister 的余额检查和创建流程

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/dfinity/icskills --skill cycles-management

安装后在 Claude Code 中输入/cycles-management确认已加载。

详细介绍

Cycles 与 Canister 管理

内容简介

Cycles 是 Internet Computer 上 Canister 的计算燃料。Canister 的每项操作(执行、存储、消息传递)都会消耗 Cycles。Canister 的 Cycles 耗尽后会被冻结,最终被卸载——其代码和数据会被删除,但元数据(Canister ID、控制者、设置)会保留。1 万亿 Cycles(1T)的成本约为等值 1 美元的 ICP(确切汇率由 NNS 设定,并随 ICP 价格通过 CMC 波动)。

注意: icp-cli 默认使用 cycles ledger(um5iw-rqaaa-aaaaq-qaaba-cai)。cycles ledger 是一个用于追踪所有主体 Cycles 余额的独立 Canister,类似于代币账本。icp cycles balance、icp cycles mint 和 icp canister top-up 等命令都会通过 cycles ledger 执行。icp-cli 中没有传统的 wallet 概念。无论使用哪种资金机制,下面介绍的编程模式(接收 Cycles、通过管理 Canister 创建 Canister)都保持不变。

前提条件

  • 对于 Motoko:mops 包管理器,且在 mops.toml 中设置 core = "2.0.0"
  • 对于 Rust:ic-cdk >= 0.19

Canister ID

服务Canister ID用途
Cycles Minting Canister (CMC)rkp4c-7iaaa-aaaaa-aaaca-cai将 ICP 转换为 Cycles,创建 Canister
Cycles Ledgerum5iw-rqaaa-aaaaq-qaaba-cai追踪所有主体的 Cycles 余额
Management Canisteraaaaa-aaCanister 生命周期管理(创建、安装、停止、删除、状态查询)

Management Canister(aaaaa-aa)是一个虚拟 Canister——它并不存在于某个特定子网中,而是由每个子网的执行层处理。

会导致构建失败的错误

  1. Cycles 耗尽会导致 Canister 静默冻结——不会发出警告。冻结的 Canister 会停止处理消息,但仍需为其内存和分配支付租金,因此余额会持续减少。余额降至零时,Canister 会被卸载:代码和数据将永久丢失,只保留元数据(Canister ID、控制器、设置)。请设置冻结阈值并监控余额。

  2. 未设置 freezing_threshold——默认值为 30 天。如果 Canister 消耗 cycles 很快(流量大、稳定内存大),30 天可能不足以提供预警。请为生产 Canister 设置更高的值。冻结阈值定义了 Canister 在冻结前必须保留相当于多少秒空闲运行所需的 cycles。

  3. 混淆本地 cycles 与主网 cycles——本地副本几乎会为 Canister 提供无限量的 cycles。本地运行正常的代码,可能会因 Canister cycles 不足而无法在主网上运行。部署到主网前,务必使用符合实际情况的 cycles 数量进行测试。

  4. 将 cycles 发送到错误的 Canister——发送给 Canister 的 cycles 无法取回。cycles 转入错误的主体后,没有退款机制。充值前请仔细核对 Canister ID。

  5. 忘记设置 Canister 控制器——如果丢失控制器身份,就会永久失去升级、充值或管理 Canister 的能力。请务必添加备用控制器。使用 icp canister settings update --add-controller PRINCIPAL 添加控制器。

  6. 在 mo:core 中使用 ExperimentalCycles——在 mo:core 2.0 中,该模块已更名为 Cycles。import ExperimentalCycles "mo:base/ExperimentalCycles" 会失败。请使用 import Cycles "mo:core/Cycles"。

  7. 使用 icp cycles transfer 为 Canister 充值——icp cycles transfer <amount> <principal> 会在 cycles 账本余额之间转移 cycles,方式与账户间转移代币相同。将 Canister 的主体作为接收方传入,只会为该主体的 cycles 账本余额充值,不会为 Canister 实际的执行/计算 cycles 余额充值——icp canister status 不会显示收到 cycles,Canister 仍会消耗实际余额并逐渐冻结,而且不会提示发生了错误。请改用 icp canister top-up <canister> --amount <N> -e ic,通过管理 Canister 将 cycles 直接存入 Canister。以下“为 Canister 充值”一节并列介绍了这两条命令。

  8. 在指定 Canister 的命令上使用 -n ic——-n/--network 和 -e/--environment 是不同的选择器,二者互相冲突;同时传入会报错。Canister 名称只能通过环境解析,因此 -n 无法用于名称:

    icp canister status backend -n ic
    # Error: Specifying a network is not supported if you are targeting a canister
    # by name, specify an environment instead
    

    因此,凡是指定 Canister 的命令(top-up、status、settings update、create)都使用 -e ic;接受主体或完全不指定 Canister 的命令(cycles mint、cycles balance、cycles transfer)则使用 -n ic。这就是为什么下面的主网示例会混用这两个标志。加载 icp-cli 以查看完整的标志规则。

实现

Motoko
检查并接收 Cycles
import Cycles "mo:core/Cycles";
import Principal "mo:core/Principal";
import Runtime "mo:core/Runtime";

persistent actor {

  // Check this canister's cycle balance
  public query func getBalance() : async Nat {
    Cycles.balance()
  };

  // Accept cycles sent with a call (for "tip jar" or payment patterns)
  public func deposit() : async Nat {
    let available = Cycles.available();
    if (available == 0) {
      Runtime.trap("No cycles sent with this call")
    };
    let accepted = Cycles.accept<system>(available);
    accepted
  };

  // Send cycles to another canister via inter-canister call
  public func topUpCanister(target : Principal) : async () {
    let targetActor = actor (Principal.toText(target)) : actor {
      deposit_cycles : shared () -> async ();
    };
    // Attach 1T cycles to the call
    await (with cycles = 1_000_000_000_000) targetActor.deposit_cycles();
  };
}
以编程方式创建 Canister
import Principal "mo:core/Principal";

persistent actor Self {

  type CanisterId = { canister_id : Principal };

  type CreateCanisterSettings = {
    controllers : ?[Principal];
    compute_allocation : ?Nat;
    memory_allocation : ?Nat;
    freezing_threshold : ?Nat;
  };

  // Management canister interface
  let ic = actor ("aaaaa-aa") : actor {
    create_canister : shared { settings : ?CreateCanisterSettings } ->
      async CanisterId;
    canister_status : shared { canister_id : Principal } ->
      async {
        status : { #running; #stopping; #stopped };
        memory_size : Nat;
        cycles : Nat;
        settings : CreateCanisterSettings;
        module_hash : ?Blob;
      };
    deposit_cycles : shared { canister_id : Principal } -> async ();
    stop_canister : shared { canister_id : Principal } -> async ();
    delete_canister : shared { canister_id : Principal } -> async ();
  };

  // Create a new canister with 1T cycles
  public func createNewCanister() : async Principal {
    let result = await (with cycles = 1_000_000_000_000) ic.create_canister({
      settings = ?{
        controllers = ?[Principal.fromActor(Self)];
        compute_allocation = null;
        memory_allocation = null;
        freezing_threshold = ?2_592_000; // 30 days in seconds
      };
    });
    result.canister_id
  };

  // Check a canister's status and cycle balance
  public func checkStatus(canisterId : Principal) : async Nat {
    let status = await ic.canister_status({ canister_id = canisterId });
    status.cycles
  };

  // Top up another canister
  public func topUp(canisterId : Principal, amount : Nat) : async () {
    await (with cycles = amount) ic.deposit_cycles({ canister_id = canisterId });
  };
}
Rust
检查余额并接收 Cycles
use ic_cdk::{query, update};
use candid::Nat;

#[query]
fn get_balance() -> Nat {
    Nat::from(ic_cdk::api::canister_cycle_balance())
}

#[update]
fn deposit() -> Nat {
    let available = ic_cdk::api::msg_cycles_available();
    if available == 0 {
        ic_cdk::trap("No cycles sent with this call");
    }
    let accepted = ic_cdk::api::msg_cycles_accept(available);
    Nat::from(accepted)
}
创建和管理 Canister
use candid::{Nat, Principal};
use ic_cdk::update;
use ic_cdk::management_canister::{
    create_canister_with_extra_cycles, canister_status, deposit_cycles, stop_canister, delete_canister,
    CreateCanisterArgs, CanisterStatusArgs, DepositCyclesArgs, StopCanisterArgs, DeleteCanisterArgs,
    CanisterSettings, CanisterStatusResult,
};

#[update]
async fn create_new_canister() -> Principal {
    let caller = ic_cdk::api::canister_self(); // capture canister's own principal
    let user = ic_cdk::api::msg_caller(); // capture caller before await

    let settings = CanisterSettings {
        controllers: Some(vec![caller, user]),
        compute_allocation: None,
        memory_allocation: None,
        freezing_threshold: Some(Nat::from(2_592_000u64)), // 30 days
        reserved_cycles_limit: None,
        log_visibility: None,
        wasm_memory_limit: None,
        wasm_memory_threshold: None,
        environment_variables: None,
    };

    let arg = CreateCanisterArgs {
        settings: Some(settings),
    };

    // Send 1T cycles with the create call
    let result = create_canister_with_extra_cycles(&arg, 1_000_000_000_000u128)
        .await
        .expect("Failed to create canister");

    result.canister_id
}

#[update]
async fn check_status(canister_id: Principal) -> CanisterStatusResult {
    canister_status(&CanisterStatusArgs { canister_id })
        .await
        .expect("Failed to get canister status")
}

#[update]
async fn top_up(canister_id: Principal, amount: u128) {
    deposit_cycles(&DepositCyclesArgs { canister_id }, amount)
        .await
        .expect("Failed to deposit cycles");
}

#[update]
async fn stop_and_delete(canister_id: Principal) {
    stop_canister(&StopCanisterArgs { canister_id })
        .await
        .expect("Failed to stop canister");

    delete_canister(&DeleteCanisterArgs { canister_id })
        .await
        .expect("Failed to delete canister");
}

部署与测试

检查 Cycles 余额
# Check your canister's cycle balance
icp canister status backend
# Look for "Balance:" line in output

# Check balance on mainnet
icp canister status backend -e ic

# Check any canister by ID
icp canister status ryjl3-tyaaa-aaaaa-aaaba-cai -e ic
为 Canister 充值
# Top up with cycles from the cycles ledger (local)
icp canister top-up backend --amount 1000000000000
# Adds 1T cycles to the backend canister

# Top up on mainnet
icp canister top-up backend --amount 1000000000000 -e ic

# Convert ICP to cycles and top up in one step (mainnet)
icp cycles mint --icp 1.0 -n ic   # no canister argument, so -n is fine (see Pitfall 8)

# No ICP to convert? Buy cycles with a card for the identity instead (load the `buy-cycles` skill)
icp cycles buy --amount 10 -n ic
icp canister top-up backend --amount 1000000000000 -e ic
# CORRECT -- funds the canister's execution balance:
icp canister top-up backend --amount 600b -e ic

# WRONG -- sends to the recipient's cycles-ledger balance, canister cannot use it:
icp cycles transfer 600000000000 <canister-principal> -n ic

icp canister top-up 的 --amount 接受 k/m/b/t 后缀(分别表示千/百万/十亿/万亿)。

通过 icp 创建 Canister
# Create an empty canister (local)
icp canister create my_canister

# Create on mainnet with specific cycles
icp canister create my_canister -e ic --cycles 2000000000000

# Add a backup controller
icp canister settings update my_canister --add-controller BACKUP_PRINCIPAL_HERE

每次创建都会从身份余额中扣除 --cycles 指定的 cycles(默认值为 2T),以及 cycles 账本收取的 100M 手续费。在使用 icp deploy -e ic 一次创建多个 Canister 之前,请估算 N × (2T + 100M),将其与 icp cycles balance -n ic 的结果比较,并在部署前补足差额——使用 ICP 时运行 icp cycles mint --cycles <gap> -n ic,使用银行卡时运行 icp cycles buy --cycles <gap> -n ic。icp-cli 技能中的“为主网充值身份”一节介绍了具体步骤。

Set Freezing Threshold
# Set freezing threshold to 90 days (in seconds: 90 * 24 * 60 * 60 = 7776000)
icp canister settings update backend --freezing-threshold 7776000

# Mainnet
icp canister settings update backend --freezing-threshold 7776000 -e ic

Verify It Works

# Deploy and check status
icp network start -d
icp deploy backend
icp canister status backend
# Expected output includes:
#   Status: Running
#   Balance: 3_100_000_000_000 Cycles (local default, varies)
#   Freezing threshold: 2_592_000

# Check balance programmatically (if you added getBalance)
icp canister call backend getBalance '()'
# Expected: a large nat value, e.g. (3_100_000_000_000 : nat)

# On mainnet: verify cycles balance is not zero
icp canister status backend -e ic
# If Balance shows 0, the canister will freeze. Top up immediately.

评论

支持 Markdown