cycles-management について
cycles-management は、Internet Computer の canister cycles を扱う開発者向けのリファレンススキルです。Motoko と Rust の例、icp-cli の手順に加え、凍結や誤送金、コントローラー喪失などのリスクも説明します。
主な機能
- 01cycles の確認と受け入れ
- 02canister の作成と管理
- 03icp-cli による補充と残高確認
- 04凍結しきい値とコントローラーの設定
- 05Motoko と Rust の例
活用シーン
- 01デプロイ前の cycles 残高確認
- 02メインネット canister の補充としきい値調整
- 03canister の残高確認と作成処理の実装
インストール
コマンドをコピーしてターミナルで実行します。
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)は、ICP で約 1 USD 相当です(正確なレートは NNS が設定し、CMC を介して ICP の価格に応じて変動します)。
注: icp-cli はデフォルトで cycles ledger(um5iw-rqaaa-aaaaq-qaaba-cai)を使用します。cycles ledger は、トークン台帳と同様に、すべてのプリンシパルの cycle 残高を追跡する単一の canister です。icp cycles balance、icp cycles mint、icp canister top-up などのコマンドは cycles ledger を経由します。icp-cli に従来のウォレットという概念はありません。以下のプログラムによるパターン(cycles の受け入れ、management 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 | すべてのプリンシパルの cycle 残高を追跡します |
| Management Canister | aaaaa-aa | canister のライフサイクル(作成、インストール、停止、削除、ステータス) |
Management Canister(aaaaa-aa)は仮想 canister です。特定のサブネット上に存在するのではなく、各サブネットの実行レイヤーによって処理されます。
ビルドを壊す間違い
-
サイクルが不足すると、警告なしにカニスターがフリーズします -- 警告はありません。フリーズしたカニスターはメッセージの実行を停止しますが、メモリと割り当ての使用料は引き続き支払うため、残高は減り続けます。残高がゼロになるとカニスターはアンインストールされ、コードとデータは永久に失われ、メタデータ(カニスター ID、コントローラー、設定)だけが残ります。凍結しきい値を設定し、残高を監視してください。
-
freezing_threshold を設定しない -- デフォルトは30日です。カニスターのサイクル消費が速い場合(トラフィックが多い、安定メモリが大きいなど)、30日では警告として不十分な可能性があります。本番環境のカニスターでは、より長く設定してください。凍結しきい値は、カニスターがフリーズする前に保持していなければならない、アイドル状態でのサイクル数を秒単位で定義します。
-
ローカルのサイクルとメインネットのサイクルを混同する -- ローカルレプリカでは、カニスターは実質的に無制限のサイクルを利用できます。ローカルで動作するコードでも、カニスターのサイクルが不足しているとメインネットでは失敗することがあります。メインネットにデプロイする前に、現実的なサイクル数で必ずテストしてください。
-
誤ったカニスターにサイクルを送る -- カニスターに送信したサイクルは取り戻せません。誤ったプリンシパルに転送したサイクルを返金する仕組みはありません。チャージする前に、カニスター ID をよく確認してください。
-
カニスターのコントローラー設定を忘れる -- コントローラーの ID を失うと、カニスターをアップグレード、チャージ、管理する手段を永久に失います。必ずバックアップ用のコントローラーを追加してください。
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を使用する --icp cycles transfer <amount> <principal>は、アカウント間のトークン送金と同様に、サイクル台帳の残高間でサイクルを移動します。受取人にカニスターのプリンシパルを指定すると、そのプリンシパルのサイクル台帳残高に入金されます。カニスターの実際の実行/計算用サイクル残高には入金されません。そのため、icp canister statusにサイクルが受け取られたことは表示されず、何も問題が起きていないように見えるまま、カニスターは実際の残高を消費し続けて凍結に向かいます。代わりにicp canister top-up <canister> --amount <N> -e icを使用してください。これは管理カニスターを介して、カニスターに直接サイクルを入金します。両方のコマンドを並べて確認するには、以下の「カニスターへのチャージ」を参照してください。 -
カニスターを指定するコマンドで
-n icを使用する ---n/--networkと-e/--environmentは異なるセレクターであり、互いに競合します。両方を渡すとエラーになります。カニスターの名前は環境を通じてしか解決できないため、-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したがって、カニスターを名前で指定するコマンド(
top-up、status、settings update、create)には-e icを、プリンシパルを指定するコマンドやカニスターを指定しないコマンド(cycles mint、cycles balance、cycles transfer)には-n icを使用してください。以下のメインネットの例で2種類のフラグが混在しているのはこのためです。すべてのフラグのルールについては、icp-cliのスキルを読み込んでください。
実装
Motoko
サイクルの確認と受け取り
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();
};
}
プログラムによるカニスターの作成
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
残高の確認とサイクルの受け取り
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)
}
カニスターの作成と管理
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");
}
デプロイとテスト
サイクル残高の確認
# 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
カニスターへのチャージ
# 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 によるカニスターの作成
# 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(デフォルトは2T)に加えて、サイクル台帳の手数料100Mが ID の残高から差し引かれます。icp deploy -e ic で複数のカニスターを一度に作成する前に、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 スキルの「メインネット用 ID への資金供給」セクションを参照してください。
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.