关于 cycles-management
cycles-management 是面向 Internet Computer 开发者的技能资料,说明 canister cycles 的余额、充值、创建与管理方式。它结合 Motoko、Rust 示例和 icp-cli 操作,重点提示冻结、资金误转及控制器配置等风险。
主要功能
- 01检查与接收 cycles
- 02创建和管理 canister
- 03使用 icp-cli 充值与查询
- 04配置冻结阈值和控制器
- 05提供 Motoko 与 Rust 示例
适用场景
- 01部署前估算并检查 cycles 余额
- 02为主网 canister 充值并调整阈值
- 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 Ledger | um5iw-rqaaa-aaaaq-qaaba-cai | 追踪所有主体的 Cycles 余额 |
| Management Canister | aaaaa-aa | Canister 生命周期管理(创建、安装、停止、删除、状态查询) |
Management Canister(aaaaa-aa)是一个虚拟 Canister——它并不存在于某个特定子网中,而是由每个子网的执行层处理。
会导致构建失败的错误
-
Cycles 耗尽会导致 Canister 静默冻结——不会发出警告。冻结的 Canister 会停止处理消息,但仍需为其内存和分配支付租金,因此余额会持续减少。余额降至零时,Canister 会被卸载:代码和数据将永久丢失,只保留元数据(Canister ID、控制器、设置)。请设置冻结阈值并监控余额。
-
未设置 freezing_threshold——默认值为 30 天。如果 Canister 消耗 cycles 很快(流量大、稳定内存大),30 天可能不足以提供预警。请为生产 Canister 设置更高的值。冻结阈值定义了 Canister 在冻结前必须保留相当于多少秒空闲运行所需的 cycles。
-
混淆本地 cycles 与主网 cycles——本地副本几乎会为 Canister 提供无限量的 cycles。本地运行正常的代码,可能会因 Canister cycles 不足而无法在主网上运行。部署到主网前,务必使用符合实际情况的 cycles 数量进行测试。
-
将 cycles 发送到错误的 Canister——发送给 Canister 的 cycles 无法取回。cycles 转入错误的主体后,没有退款机制。充值前请仔细核对 Canister ID。
-
忘记设置 Canister 控制器——如果丢失控制器身份,就会永久失去升级、充值或管理 Canister 的能力。请务必添加备用控制器。使用
icp canister settings update --add-controller PRINCIPAL添加控制器。 -
在 mo:core 中使用 ExperimentalCycles——在 mo:core 2.0 中,该模块已更名为
Cycles。import ExperimentalCycles "mo:base/ExperimentalCycles"会失败。请使用import Cycles "mo:core/Cycles"。 -
使用
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 充值”一节并列介绍了这两条命令。 -
在指定 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.