cloud-engine-canisters について
cloud-engine-canistersは、Internet Computer Cloud Engineで開発する人向けのスキルです。プロトコル上の呼び出し制約やHTTPSアウトコールを説明し、CloudEngineサブネットとApplicationサブネットの違いに沿った実装を案内します。
主な機能
- 01呼び出しにcyclesを付けない規則の説明
- 02サブネット間のbounded-wait呼び出しの案内
- 03標準的なHTTPSアウトコールラッパーの説明
- 04コンソールプロキシが必要なサービスの一覧
- 05MotokoとRustの呼び出し例
活用シーン
- 01エンジンCanisterのcyclesエラーの調査
- 02サブネット間のbounded-wait呼び出しの実装
- 03エンジンCanisterへのHTTPSアウトコール追加
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dfinity/ icskills --skill cloud-engine-canistersインストール後、Claude Code で/cloud-engine-canistersを入力して読み込まれたか確認します。
詳細
Cloud Engine カニスター
概要
クラウドエンジンとは、ウェブコンソールから管理する、ユーザー所有の Internet Computer 容量の一部です(コードをデプロイする方法については、deploy-to-cloud-engine スキルを参照してください)。エンジンは専用の CloudEngine サブネットタイプ上で動作し、無料のサイクル料金体系が適用されます。エンジンカニスターの処理には、実行、ストレージ、メッセージング、HTTPS アウトコールのいずれもサイクル料金がかからず、エンジンカニスターのサイクル残高は設計上 0 サイクルです。
このモデルには、プロトコルによって強制される呼び出しルールがあります。通常の Application サブネットで動作するコードがエンジン上では失敗することがあり、その失敗は実際の原因である「間違ったサブネットタイプ向けに書かれたコード」ではなく、サイクルやコンセンサスの問題のように見えます。ルールは次のとおりです。
| ルール | クラウドエンジン上での対応 |
|---|---|
| 1 | どの呼び出しにもサイクルを付加しないでください — (with cycles = …) と、サイクルを付加するラッパーを削除します |
| 2 | サブネットをまたぐ呼び出しには、バウンド付き待機を指定する必要があります — Motoko では (with timeout = N)、Rust では Call::bounded_wait を使用します |
| 3 | HTTPS アウトコールは通常のコードで実行でき、料金は無料です — 標準ラッパーをそのまま使用し、自身のカニスター上で変換して、プロキシ経由では呼び出さないでください |
| 4 | サイクルを必要とするサブネット間呼び出し先(XRC、しきい値署名、vetKD、Bitcoin API、EVM/SOL RPC)は、コンソールプロキシカニスター経由で呼び出します。このコードを書く前に、ユーザーにプロキシの ID を尋ねてください |
ルール 1 — 自分で計算したサイクル量を付加しない
エンジンカニスターのサイクル残高は 0 で、支払いは発生しません。エンジンの無料料金体系では、Application サブネットのカニスターで発生するすべての料金(実行、メッセージ送信、HTTPS アウトコール、しきい値署名、vetKD、ストレージ)が 0 として課金されます。
-
**自分で記述するカニスター間呼び出しには、cycles 句を一切付けません。**購入するものはありません。エンジン上ではすべての料金が 0 として課金されるため、プロトコルが許容するかどうかにかかわらず、サイクルの付加は不要です。
await service.method(args)と記述し、await (with cycles = 1_000_000) service.method(args)とは記述しないでください。稼働中のエンジンカニスター(Cycles: 0)で確認したところ、0 以外の値はすべて失敗し、次のようになります。Canister <id> is out of cycles, error code Some("IC0504")値を 0 にして「修正」しないでください。プラットフォームチームの指針は、エンジン上の呼び出しではサイクルについて一切記述しないことです。
cycles = 0を残しても不要な記述であり、値が 0 以外になった時点(計算した料金やコピーした定数など)で再び問題を引き起こします。エンジンカニスターからIC0504が出た場合、いずれかの呼び出しが 0 以外の値を付加しています。 -
**Rust:**同じルールが適用されます。
Callビルダーで.with_cycles(…)を呼び出してはいけません。(プラットフォームチームは Motoko のルールを「サイクルについて一切記述しない」としています。.with_cyclesを省略することが Rust での同じ対応です。) -
管理カニスターの呼び出しでは、標準ラッパーをそのまま使用します。
Call.httpRequest(Motoko、mo:ic)と、使用するクレートのバージョン系列向けの Rust アウトコールラッパー(ルール 3)は、料金を確認するためにic0.cost_*システム API を呼び出します。この API は料金体系に対応しており、アウトコールの料金は自身のサブネットの料金体系に基づいて算出されます。エンジンでは無料なので 0 が返され、ラッパーは何も付加しません。そのため、同じソースを Application サブネットでもそのまま使用できます。問題になるのは、ハードコードした料金や手計算した料金に置き換える場合です。https-outcallsの料金表から数値を取得して、エンジン上で付加しないでください。 -
どのサブネットの料金体系が参照されるかが、両者の違いです。チェーンキーの料金 API(
ic0.cost_sign_with_ecdsaなど)は、自身のサブネットではなくキーを保持するサブネットに基づいて料金を算出します。そのサブネットでは通常どおり料金が発生するため、エンジンから呼び出した場合も 0 以外の料金が返され、ラッパーがその料金を付加します。これが、署名を行うヘルパーはここでは動作しない一方、同じクレートのアウトコールビルダーは動作する理由です(ルール 3 と 4)。 -
唯一の例外は、サブネットをまたぐ呼び出し先が請求するサイクルです。サイクルはコンソールプロキシの
ProxyArgs.cyclesの内部に指定します(ルール 4)。カニスターから送信する呼び出しには、決して付加しません。
ルール 2 — サブネット間の呼び出しは待機時間を制限する必要があります
プロトコルでは、CloudEngine サブネットとの間で、待機時間が制限されていない(応答が保証される)リクエスト、またはサイクルを伴うリクエストを、サブネット間(XNet)で送信すると拒否されます。呼び出し元には、次のテキストと完全に一致する拒否が返されます。
Unbounded-wait calls and calls with cycles are not allowed to CloudEngine subnets
エンジンの canister から実際に検証したところ、別のサブネット上の canister に単純な await を行うと、まったく同じ #system_fatal 拒否が返されます。一方、(with timeout = 30) を指定し、サイクル句を付けずに同じ呼び出しを行うと成功します。
サブネット間の呼び出しはすべて、待機時間を制限し、サイクルを伴わないようにしてください。
// Motoko: `timeout : Nat32` is in seconds
let result = await (with timeout = 30) service.method(args);
// Rust, ic-cdk >= 0.18 — bounded_wait defaults to a 300-second timeout (the IC maximum)
let reply: T = Call::bounded_wait(canister_id, "method")
.with_arg(arg)
.await? // Err on reject / SYS_UNKNOWN — handle, do not unwrap
.candid()?; // decode the reply
- 同一サブネット内の呼び出しは対象外です:同じエンジン上の canister 同士であれば、待機時間を制限せずに呼び出せます。ただし、待機時間を制限する方法がより適切なデフォルトです。応答しない呼び出し先への待機時間が制限されていない呼び出しは、呼び出し元のアップグレードを無期限にブロックします(
multi-canisterスキルを参照してください)。 - この制限は双方向に適用されます:通常のサブネット上の canister からエンジンの canister を呼び出す場合も、待機時間を制限し、サイクルを伴わない呼び出しにする必要があります。
- 待機時間を制限した呼び出しでは、
SYS_UNKNOWN(結果不明)になることがあります。適切に処理してください。パターンについてはmulti-canisterスキルで説明しています。
ルール 3 — HTTPS アウトコール:通常のコードで、コストはゼロ
HTTPS アウトコールはエンジン自身のサブネット上で実行され、その環境ではコスト API が無料と報告するため、標準のラッパーを変更せずに使用できます。エンジン固有のアウトコールコードは必要ありません。
import IC "mo:ic/Types";
import Call "mo:ic/Call";
// Build `request : IC.HttpRequestArgs` exactly as in the https-outcalls skill,
// with the transform query on THIS canister. Call.httpRequest asks the cost API,
// which returns 0 on an engine, so nothing is attached.
let response = await Call.httpRequest(request);
-
ラッパーはそのまま使い、ハードコードした手数料に置き換えないでください(ルール 1)。ラッパーに呼び出し料金を計算させる方法は、現在稼働している Motoko エンジンアプリでアウトコールを書く際の方法です。
-
Rust では、使用しているクレートのバージョン系列に対応するラッパーを使ってください —
ic-cdk-management-canister0.2 のHttpRequest::new(url)…send()、0.1 の無料のic_cdk_management_canister::http_request、または ic-cdk 0.19 のic_cdk::management_canister::http_request(このモジュールは ic-cdk 0.20 で削除されました)。この用途では、3 つとも同じ動作をします。それぞれがic0.cost_*を呼び出し、自分のサブネットを基準に料金を計算するため、エンジン上では 0 を返し、その 0 を付加します。プロジェクトが実際に使用しているバージョン系列に合わせて実装してください。エンジンのアウトコールを「修正」するためにクレートのバージョンを上げてはいけません。修正すべきものはありません。3 つの違いについてはhttps-outcallsを参照してください。 -
ここでは、
ic-cdk-management-canisterのアウトコールビルダーがサポートされている方法です — エンジンで使用できないのは、chain-key ヘルパーだけです(ルール 4)。このクレート自体は使用できます。可否を決めるのはターゲットのサブネットです。 -
エンジンでは、Pricing version 2 を調整する必要はありません。 0.2 のビルダーは常に version 2(「pay-as-you-go」)で料金を計算します。この方式では、付加された cycles が各ノードの支出予算も兼ねています。そのため、料金が発生するサブネットでは、
with_expected_*の値が小さすぎると、リモートサーバーへの接続後に呼び出しが途中で失敗することがあります。料金が発生しないエンジンでは、そのようなことは起こりません。無料の料金スケジュールでは料金が請求されないため、予算によって制限されることはありません。往復タイムアウトと transform の命令数上限はプロトコルの最大値となり、InsufficientCyclesが発生することもありません。ここではwith_expected_*のセッターは何もしないものとして扱ってください。移植性のために設定してもかまいませんが、エンジンのアウトコールを修正する目的で調整したり、エンジンのアウトコール失敗を予算の問題と判断したりしてはいけません。それでも適用されるのは、独自に設定するmax_response_bytesです。これはエンジンでも他と同じようにレスポンスのサイズを制限するため、サイズ超過のレスポンスは実際の失敗原因となり、ここで確認すべき唯一のアウトコール制限です。 -
エンジンではどちらの Pricing version も使えるため、ここでバージョンを管理する必要はありません。 version 2 が受け入れられるのは、レプリカで pay-as-you-go が有効な場合に限られます。有効でない場合、要求された version 2 はエラーなしで 黙って version 1 に戻されます。料金が発生するサブネットでは、これは資金不足につながります。一方、どちらのバージョンでもアウトコールの料金が 0 のエンジンでは問題になりません。エンジン専用のバージョン処理を追加しないでください。
-
アウトコールのその他の動作に変更はありません。自分のカナスター上の transform 関数、
max_response_bytes、冪等性などです。プラットフォームチームによると、エンジンでは複製の有無にかかわらず、アウトコール機能をすべてサポートしています。そのため、冪等でない API やレート制限のある API ではis_replicated = ?falseを利用できます。また、大量のリクエストを処理する場合にも利用できます。複製された呼び出しでは、リクエスト数がレプリカ数倍になるためです。詳細はhttps-outcallsスキルを読み込んでください。 -
HTTPS アウトコールをコンソールのプロキシカナスター経由で行ってはいけません。 プラットフォームチームは、すべての HTTP アウトコールを直接行うよう明示しています。プロキシは、cycles が必要な mainnet サービス専用です。IC では transform 関数を
http_requestを呼び出すカナスター上に置く必要があります。プロキシ経由の場合、そのカナスターはプロキシ自身ですが、プロキシは transform メソッドを公開していません(実際に稼働中のインターフェースで確認済みで、チームも追加予定がないと確認しています)。実際の API の多くでは、transform を適用しないレスポンスはレプリカ間で異なるため、呼び出しは次のエラーで失敗します。SYS_TRANSIENT: No consensus could be reached. Replicas had different responsesプロキシ経由のアウトコールでは、料金も発生します。プロキシは通常の Application サブネット上にあるため、資金をチャージした残高から実際のアウトコール料金が請求されます。一方、エンジンのカナスターから同じ呼び出しを行えば、料金はかかりません。移行規模の数千回に及ぶ呼び出しでは残高が減り、すべての呼び出しが
ProxyError::InsufficientCyclesで失敗し始めます。これはアウトコールをプロキシ経由にした症状であり、エンジンの資金不足ではありません。どちらの失敗も解決方法は同じです。カナスターからアウトコールを直接行ってください(無料で、正常に動作します)。リトライしても、プロキシに追加で資金をチャージしても解決しません。
ルール 4 — コンソールプロキシ経由での、サブネットをまたぐサイクル付き呼び出し
2つの別々の事実が、同じ結論に行き着きます。ルール 2 の制限により、エンジンキャニスターはサブネットをまたぐ、サイクルを伴うメッセージを送信できません。さらに、プラットフォームチームによると、クラウドエンジンはしきい値署名をまったく提供していないため、これらの機能はメインネットで利用する必要があります。いずれにせよ、呼び出しを直接行うことはできません。
- しきい値 ECDSA/Schnorr 署名(
sign_with_ecdsa、sign_with_schnorr)とそれらの公開鍵メソッド、および vetKD(vetkd_derive_key、vetkd_public_key)— エンジンでは利用できないため、プロキシ経由でメインネットのものを使用します。 - レート換算キャニスター(XRC)— サイクルを消費するメインネットキャニスターです。
- Bitcoin API(管理キャニスターの
bitcoin_*メソッド)、および呼び出しごとにサイクルを消費する EVM RPC と SOL RPC キャニスター。
コンソールプロキシは、あなたのキャニスターに代わってそれ以外のものを中継することはありません(下記の「コンソールプロキシが中継するもの」を参照)。これがプロキシの唯一の目的です。つまり、サイクルを必要とするメインネットサービスにアクセスすることです。HTTPS アウトコールには使用しません(ルール 3)。
回避策はコンソールプロキシキャニスターです。通常のアプリケーションサブネットにデプロイし、エンジンの残高とは別に資金を維持します。エンジンキャニスターは、サイクルを消費しない、待機時間に上限のある安価な呼び出しをプロキシに送り、プロキシはサイクルを付けてローカルで呼び出しを再発行し、生の応答を中継して返します。同じサブネットへの呼び出しやサイクルを伴わない呼び出しには必要ありません。HTTPS アウトコールにも必要ありません(ルール 3)。
コードを書く前に、ユーザーにプロキシを確認してください
コンソールプロキシを作成できるのは、あなたでもキャニスターコードでもありません。エンジンの所有者がコンソールからデプロイし、アプリには設定としてキャニスター ID のみが渡されます。そのため、上記リストの対象のいずれかが必要なタスク(機能リクエスト、計画、既存コードの移植)では、エンジンで最初の呼び出しが失敗してからではなく、コードを書く前にユーザーに確認してください。
-
アプリにはエンジンのプロキシキャニスターが必要であることと、その理由を1文で伝えます。
-
エンジンにすでにプロキシがあるか尋ねます。
- ある場合:キャニスター ID を尋ねます(エンジン → Canisters → Proxy canisters)。複数ある場合で、アプリが署名または鍵の導出を行う場合は、アプリでどれを使用するか尋ねます。導出される鍵はすべて、その選択に従います(「プロキシ経由のしきい値鍵」を参照)。選択を代行してはいけません。
- ない場合:所有者にデプロイを依頼します。エンジン → Canisters → Proxy canisters → Deploy a proxy の順に選択し、完了を確認してください。カードも金額の選択も必要ありません。プロキシの費用はエンジンの残高から支払われ、残高が少なくなるとそこから自動で補充されます。その後、表示された ID を尋ねます。
たとえば、「このアプリはエンジンのプロキシキャニスター経由で署名します。エンジンにプロキシがある場合は、その ID を送ってください(エンジン → Canisters → Proxy canisters)。ない場合は、エンジンの所有者がそこから Deploy a proxy を選んでデプロイできます。費用はエンジンの残高から支払われ、カードは不要です。その後、ID を送ってください。」
-
その間も作業を続けます。ID はキャニスターの環境変数(
PROXY_CANISTER_ID、deploy-to-cloud-engineを参照)から読み込み、ソースコードに直接記述しないでください。コードを書くのに ID は必要ありません。デプロイ時に必要です。 -
ID が届いたら、コンソールプロキシであることを確認します。このクエリは無料で、プロジェクトも不要です。
icp canister call <proxy-id> get_allowed_ranges '()' -n ic --queryvec { record { start = principal "…"; end = principal "…" } }のように空でない応答が返れば、それはエンジンのキャニスターがアクセスできるコンソールプロキシです。該当するメソッドがキャニスターにないというエラーが出る場合や、空のリストが返る場合は、コンソールプロキシではありません(自分でデプロイしたプロキシ、またはプロキシではないものです)。もう一度確認してください。現在のコンソールプロキシでは、同じ形式のget_fundingにbalanceも表示され、エンジンが補充するとfunding = opt record { engine_canister = …; … }も表示されます。
ID を捏造したり、別のエンジンやプロジェクトのものを再利用したり、待っている間に代替として自分でデプロイしたプロキシを使ったりしてはいけません。そのようなプロキシはエンジンのキャニスターを拒否し、異なる鍵を導出します(下の表を参照)。
プロキシを取得する
2 種類の異なるカニスターがこの役割を担えますが、互換性はありません。
| コンソールプロキシ(エンジンのもの) | セルフデプロイしたプロキシ(icp new … --subfolder proxy) | |
|---|---|---|
| デプロイする人 | コンソールからエンジンの所有者がデプロイします | icp を使ってあなたがデプロイします |
| 呼び出せる人 | エンジンのカニスター ID 範囲とコントローラー | コントローラーのみ |
| エンジンのカニスターから呼び出せるか | はい。追加設定は不要です | それぞれをコントローラーとして追加した場合のみ可能です |
| リレーする対象 | 上記の対象のみ | 任意の対象 |
| 閾値鍵の導出 | 呼び出し元ごとに分離(下記参照) | 分離なし:そのままパススルーします |
| 資金の調達元 | エンジンの残高:不足すると自動で補充されるため、カードは不要です | 保有済みのサイクル(icp cycles mint) |
エンジンのカニスターには、コンソールプロキシを使用してください。セルフデプロイしたプロキシは、CLI からあなたが行う呼び出し(icp canister call --proxy、icp deploy --proxy)に適しています。コンソールプロキシは、コントローラーではないプリンシパルからの ingress 呼び出しを拒否するため、その用途には使用できません。CLI の ID はコントローラーではありません。
どちらのプロキシも、デプロイと資金調達はカニスターコードの作業ではありません。これについては deploy-to-cloud-engine スキルで説明しています。ここで必要なのは、取得したプロキシカニスター IDだけです。ユーザーまたはアプリの設定から取得し、推測した ID をハードコードしてはいけません。また、プロキシ ID を別のものに置き換えられると考える前に、以下の導出に関する警告を読んでください。
カニスターからプロキシ経由で呼び出す
ターゲットカニスターを直接呼び出す代わりに、ターゲット ID、メソッド名、Candid でエンコードした引数バイト列、添付するサイクル数を指定して、プロキシの proxy メソッドを呼び出します。その Candid インターフェースは次のとおりです。
type ProxyArgs = record { canister_id : principal; method : text; args : blob; cycles : nat };
type ProxySucceed = record { result : blob };
type ProxyError = variant {
InsufficientCycles : record { available : nat; required : nat };
CallFailed : record { reason : text };
UnauthorizedUser;
};
type ProxyResult = variant { Ok : ProxySucceed; Err : ProxyError };
service : {
proxy : (ProxyArgs) -> (ProxyResult);
get_allowed_ranges : () -> (vec record { start : principal; end : principal }) query;
};
argsはターゲットメソッドの Candid エンコード済み引数です(エンコードするのは呼び出し側です)。resultはターゲットからの生の応答バイト列です(デコードするのは呼び出し側です)。cyclesは、プロキシがリレーする呼び出しに添付するサイクル数です。ターゲットが請求する量(XRC や署名の手数料など)に合わせて設定してください。プラットフォームチームによると、プロキシが添付する量は呼び出し元が選択します。また、プロキシはargsを不透明なデータとして受け取るため、必要な量を決められません。渡した定数がそのまま必要量となり、InsufficientCyclesのrequiredフィールドにはプロキシ側の予約額ではなく、指定した数値がそのまま返されます。外側のproxy呼び出し自体にはサイクルを添付しないでください。エンジンのサブネットではそれが禁止されており、必要もありません。ProxyErrorを処理してください。InsufficientCyclesはプロキシの残高が少なすぎることを意味します(コンソールプロキシはエンジンの残高から自動で補充されるため、これが継続する場合はその残高が不足しています。落とし穴 8 を参照)。UnauthorizedUserは呼び出し元がエンジンの範囲外であることを意味します。CallFailedには下流からの拒否理由が含まれるか、コンソールプロキシがその対象へのリレーを許可されていないことを示します(「コンソールプロキシがリレーする対象」を参照)。- サイクルを多めに添付しても安全ですが、少なすぎるのは安全ではありません。 呼び出し先は消費しなかった分を返金し、その返金はプロキシに戻ります(この呼び出し段階ではプロキシが呼び出し元です)。そのため、
cyclesを余裕を持って設定してもコストはかからず、手数料が上がっても対応できます。InsufficientCyclesのavailableはプロキシの流動性のある残高です(凍結用の準備金はすでに除外されています)。そのため、コンソール上では残高が表示されていても、プロキシがInsufficientCyclesを返すことがあります。 CallFailedが返されても、呼び出しが実行されなかったとは限りません。 プロキシは待機時間に上限のある呼び出しでリレーするため、下流のSYS_UNKNOWNは、その拒否コードがreasonに含まれたCallFailedとして返されます。ターゲットはすでに実行され、サイクルも消費済みである可能性があります。「失敗」ではなく「結果不明」として扱ってください。パターンについてはmulti-canisterスキルで説明しています。
Motoko の例(Rust でも同様に candid::encode_one / decode_one を使用します)。
// `proxy` is an actor typed to the candid interface above.
let arg = to_candid (request); // encode the TARGET method's argument
let res = await (with timeout = 30) proxy.proxy({
canister_id = xrcPrincipal;
method = "get_exchange_rate";
args = arg;
cycles = 1_000_000_000; // the XRC fee the proxy forwards
});
switch (res) {
case (#Ok { result }) { let ?reply = from_candid (result) else return; /* … */ };
case (#Err e) { /* InsufficientCycles | CallFailed | UnauthorizedUser */ };
};
コンソールプロキシが中継するもの
コンソールプロキシはエンジンの残高から支払うため、エンジンのカナスター向けには、プロキシが本来中継するものだけを中継します。
- 管理カナスター(
aaaaa-aa):sign_with_ecdsa、ecdsa_public_key、sign_with_schnorr、schnorr_public_key、vetkd_derive_key、vetkd_public_key、http_request、およびbitcoin_*メソッド(bitcoin_get_balance、bitcoin_get_utxos、bitcoin_send_transaction、bitcoin_get_current_fee_percentiles、bitcoin_get_block_headers); - 次のメインネットカナスターの任意のメソッド:為替レートカナスター
uf6dk-hyaaa-aaaaq-qaaaq-cai、EVM RPC7hfb6-caaaa-aaaar-qadga-cai、SOL RPCtghme-zyaaa-aaaar-qarca-cai、Bitcoin メインネットghsi2-tqaaa-aaaan-aaaca-caiとテストネットg4xu7-jiaaa-aaaan-aaaaq-cai、Dogecoingordg-fyaaa-aaaan-aaadq-cai。
それ以外(別のカナスター、deposit_cycles、create_canister、Cycles Ledger)は、理由が a proxy funded by its engine relays only to … で始まる CallFailed として返されます。補充しても、この制限は変わりません。以前のカード払いのコンソールプロキシは、エンジンの残高に移行するまでは何でも中継しますが、それを前提に構築しないでください。http_request はリストに含まれていますが、ルール 3 は引き続き適用されます。アウトコールは独自のカナスターから行ってください。アプリが本当にこのリスト外のカナスターへの支払いを必要とする場合は、呼び出し元の各カナスターをコントローラーとして追加した自己デプロイのプロキシが必要です(deploy-to-cloud-engine を参照)。そのプロキシには、保有するサイクルを使って資金を供給します。
プロキシ経由のしきい値鍵(鍵を導出する前にお読みください)
しきい値 ECDSA、Schnorr、vetKD は、エンジン上でプロキシが担う主な役割です。また、呼び出しを誤ると、単なるエラーメッセージ以上の損失につながる唯一の場面でもあります。再現できない鍵は、資金を移動できない Bitcoin または Ethereum のアドレスになるからです。
チェーンキー用ヘルパーはここでは機能しません。 ic-vetkeys、ic-cdk-management-canister のチェーンキー関数(sign_with_ecdsa、sign_with_schnorr、vetkd_derive_key と、それらに対応する公開鍵関数)、および Motoko の mo:ic-vetkeys/ManagementCanister は、いずれも aaaaa-aa を直接呼び出し、手数料を自動で付加します。チェーンキーのコスト API は鍵を保持するサブネットを基準に料金を算出するため、エンジン上でもその手数料はゼロではありません。また、ヘルパーは無制限待機の呼び出しに手数料を載せて発行します。これはサブネット間のサイクル付き呼び出しとなり、ルール 1 と 2 の両方に違反するため、二重に拒否されます。便利さはまさに手放す必要があるものです。管理カナスターへの引数を自分でエンコードし、プロキシ経由で送信してください。vetkeys スキルのコードは通常のサブネット向けであり、エンジン上で動かすには事前にこの書き換えが必要です。
これはチェーンキー関数に限った話であり、クレート全体に当てはまるわけではありません。 ic-cdk-management-canister には HTTPS アウトコール用ビルダーも含まれており、これはエンジンカナスターからアウトコールを行うためのサポートされた方法です(ルール 3)。ビルダーは自分のサブネットを対象とし、料金は 0 で、プロキシ経由で送信してはいけません。「ヘルパーはエンジン上で機能しない」を「アウトコールもプロキシ経由で手動エンコードする」と解釈すると、落とし穴 4 のコンセンサスおよび資金供給の失敗がそのまま発生します。対象のサブネットに応じてクレートの使い方を分けてください。ローカルのアウトコール ⇒ 通常のビルダー、リモートのチェーンキー ⇒ プロキシ。
プロキシが書き換える内容。 6 つの鍵メソッド(sign_with_ecdsa、ecdsa_public_key、sign_with_schnorr、schnorr_public_key、vetkd_derive_key、vetkd_public_key)では、コンソールプロキシが引数をデコードし、導出対象を呼び出し元のみに分離してから、再度エンコードします。
- ECDSA/Schnorr:呼び出し元のカナスターのプリンシパルが、
derivation_pathの最初の要素として挿入されます。 - vetKD:呼び出し元のプリンシパルが、長さタグ付きで
contextの先頭に追加されます。プリンシパルの長さを示す 1 バイト、その後にプリンシパルのバイト列、さらにその後に元のコンテキストが続きます。 - 3 つの
*_public_keyメソッドでは、canister_idも強制的にNoneに設定されます。 プリンシパルはmsg_caller()から取得されるため、偽造できず、あるエンジンカニスターが別のカニスターの鍵にアクセスすることは決してありません。(検証済み:cargo test -p proxy-canister --lib、10/10;ecdsa_sign_prepends_caller_and_preserves_the_rest、vetkd_prefixes_caller_into_context_length_tagged、distinct_callers_get_distinct_derivations。)
手数料。 ecdsa_public_key、schnorr_public_key、vetkd_public_key は無料です。cycles = 0 を渡してください。支払いが必要なメソッドの料金は、あなたのサブネットではなく、鍵を保持するサブネットに請求されます。
| キー名 | sign_with_ecdsa / sign_with_schnorr / vetkd_derive_key |
|---|---|
key_1(本番用) | 26_153_846_153 |
test_key_1(テスト用) | 10_000_000_000 |
正確な金額ではなく、切り上げた金額を渡してください。未使用分はプロキシに返金されます。
実例:ECDSA、Motoko。 管理カニスターの型は自分で宣言してください。プロキシが blob をデコードして再エンコードするため、フィールド名とバリアントタグは candid インターフェースと完全に一致している必要があります。フィールドが欠落している、または名前が異なる場合や、未知のバリアントタグがある場合、デコードに失敗します(誤った鍵の派生ではなく、CallFailed として表面化します)。プロキシがモデル化していないフィールドは検出されません。candid のレコード部分型付けではデコード時にそのフィールドがスキップされ、再エンコード時に削除されます。
type EcdsaKeyId = { curve : { #secp256k1 }; name : Text };
type EcdsaPublicKeyArgs = { canister_id : ?Principal; derivation_path : [Blob]; key_id : EcdsaKeyId };
type EcdsaPublicKeyReply = { public_key : Blob; chain_code : Blob };
type SignWithEcdsaArgs = { message_hash : Blob; derivation_path : [Blob]; key_id : EcdsaKeyId };
type SignWithEcdsaReply = { signature : Blob };
let mgmt = Principal.fromText("aaaaa-aa");
let keyId : EcdsaKeyId = { curve = #secp256k1; name = "key_1" };
let signFee = 30_000_000_000; // >= 26_153_846_153; the excess comes back
// One helper for every proxied management call.
func viaProxy(method : Text, arg : Blob, cycles : Nat) : async Blob {
let res = await (with timeout = 30) proxy.proxy({
canister_id = mgmt; // the TARGET, not the proxy
method = method;
args = arg;
cycles = cycles; // 0 for the *_public_key methods
});
switch (res) {
case (#Ok { result }) { result };
case (#Err e) { Runtime.trap("proxy: " # debug_show e) }; // real code: return an error
};
};
// Address: free, no cycles.
let pkArg = to_candid ({
canister_id = null; // forced to null by the proxy anyway
derivation_path = [Text.encodeUtf8("user-42")]; // your own path; the proxy prefixes itself
key_id = keyId;
} : EcdsaPublicKeyArgs);
let ?pk : ?EcdsaPublicKeyReply = from_candid (await viaProxy("ecdsa_public_key", pkArg, 0))
else Runtime.trap("decode ecdsa_public_key");
// Signature: pays the fee. `message_hash` must be exactly 32 bytes.
let sigArg = to_candid ({
message_hash = messageHash;
derivation_path = [Text.encodeUtf8("user-42")]; // the SAME path as above
key_id = keyId;
} : SignWithEcdsaArgs);
let ?sig : ?SignWithEcdsaReply = from_candid (await viaProxy("sign_with_ecdsa", sigArg, signFee))
else Runtime.trap("decode sign_with_ecdsa");
sign_with_ecdsa には canister_id フィールドがないことに注意してください。署名に使う鍵は常に呼び出し元のものであり、プロキシの背後では呼び出し元はプロキシです。だからこそ、公開する公開鍵と生成する署名の両方を、必ず同じプロキシ経由で取得する必要があります。
実例:vetKD、Motoko。 構造は同じですが、型が異なります。vetkd_public_key は無料で、vetkd_derive_key は有料です。
type VetkdKeyId = { curve : { #bls12_381_g2 }; name : Text };
type VetkdPublicKeyArgs = { canister_id : ?Principal; context : Blob; key_id : VetkdKeyId };
type VetkdPublicKeyReply = { public_key : Blob };
type VetkdDeriveKeyArgs = {
input : Blob; context : Blob; transport_public_key : Blob; key_id : VetkdKeyId
};
type VetkdDeriveKeyReply = { encrypted_key : Blob };
let vetKeyId : VetkdKeyId = { curve = #bls12_381_g2; name = "key_1" };
let dkArg = to_candid ({
input = Text.encodeUtf8("note-7");
context = Text.encodeUtf8("my-app"); // the proxy length-tags its prefix onto this
transport_public_key = transportPk;
key_id = vetKeyId;
} : VetkdDeriveKeyArgs);
let ?dk : ?VetkdDeriveKeyReply = from_candid (await viaProxy("vetkd_derive_key", dkArg, 30_000_000_000))
else Runtime.trap("decode vetkd_derive_key");
Rust でも同じ流れで、candid::encode_one / decode_one とバウンデッドウェイト呼び出しを使います。Rule 2 の Call::bounded_wait(proxy_id, "proxy") の例で構造を確認してください。
派生鍵はアプリではなく、プロキシに属します。 管理カニスターはその呼び出し元(つまりプロキシ)から派生するため、取得する鍵は経由したプロキシカニスター IDによって異なります。
- アプリが別のプロキシ(コンソールの「Deploy another proxy」で作成した2つ目のプロキシ、または自分でデプロイしたプロキシ)を使うように変更すると、すべてのアドレスが変わります。新しいプロキシから以前のアドレスを復元することはできません。
- プロキシを削除すると、その鍵にアクセスできなくなります。 コンソールの削除ボタンは、未使用のサイクルをエンジンの残高に戻しますが、派生鍵を返すことはできません。そのプロキシ経由で派生したアドレスに保有されている Bitcoin または Ethereum の資金は、永久に使用できなくなります。署名アプリのプロキシ ID は恒久的なインフラとして扱い、経由して派生したアドレスからすべての資産を移す前に、「サイクルを節約する」ためにプロキシを削除してはいけません。
- 自分でデプロイした上流プロキシでは、分離は一切行われません(プリンシパルのプレフィックス付与も、
canister_id = Noneの強制もありません)。そのため、鍵はコンソールのプロキシとは異なり、ID を変更する前から両者は互換ではありません。 - オフチェーンで派生を再現するもの(たとえば、フロントエンドで同じ公開鍵を派生する場合)は、vetKD の長さタグを含め、プロキシが挿入するプレフィックスを再現する必要があります。
よくある落とし穴
- 作成した呼び出しに cycles の量を指定する。 エンジン canister からの canister 間呼び出しに
(with cycles = N)句は不要です。また、ゼロ以外のNはIC0504/is out of cyclesで失敗します(実環境で確認済み)。cycles = 0に設定するのではなく、句自体を削除してください。ゼロは受け付けられるため、習慣を改めるのではなく間違いを見えにくくしてしまいます。エンジンの無料コストスケジュールでは、アウトコール、署名、メッセージングの手数料はすべてゼロなので、エンジン canister の処理に cycles は必要ありません。 - サブネットをまたぐ呼び出しで、無制限待機を使う。 別のサブネット上の canister に対する通常の
await service.method(args)は無制限待機となり、"Unbounded-wait calls and calls with cycles are not allowed to CloudEngine subnets" で拒否されます。Motoko では(with timeout = N)、Rust ではCall::bounded_waitを使い、SYS_UNKNOWNに対処してください。同一サブネット(エンジン内)の呼び出しは対象外です。 - エンジン向けにアウトコールのコードを書き換える、または手数料をハードコードする。 アウトコールにエンジン固有の形式は必要ありません。
Call.httpRequest(Motoko)をそのまま使うか、Rust ではプロジェクトのクレート系列に応じたラッパー(ic-cdk-management-canister0.2 のHttpRequest::new(url)…send()、またはic-cdk0.19 までのic_cdk::management_canister::http_request。0.20 ではこのモジュールが削除されています)を使ってください。これらが参照するic0.cost_*API は、自分のサブネットの料金を計算し、エンジンでは 0 を返します。誤りは、この呼び出しを Application サブネットのコスト表から手計算した手数料に置き換え、残高が 0 の状態でゼロ以外の金額を添付することです。 - HTTPS アウトコールをコンソールプロキシ経由でルーティングする。 原因は 1 つですが、失敗は 2 種類あります。コンセンサスの問題は、
SYS_TRANSIENT: No consensus could be reached. Replicas had different responsesです。transform は呼び出し元の canister に置く必要がありますが、プロキシ経由では呼び出し元はプロキシ自体であり、プロキシは transform を公開していません(今後追加される予定もありません)。コストの問題は、プロキシが通常の Application サブネット上にあるため、エンジンの残高から補充されるプロキシ自身の残高を使って実際のアウトコール手数料を支払い、大量の処理によってその残高が枯渇し、ProxyError::InsufficientCyclesになることです。アウトコールは自分の canister から直接実行してください。エンジンでは無料かつ無制限なので、使い切る予算はありません。 - エンジン canister に cycles を補充する。 エンジン canister は設計上 cycles を 0 に保ちます。エンジン canister に cycles を送ることはできず、その必要もありません。エンジン canister の「0 cycles」という表示は正常であり、緊急事態ではありません。Application サブネット上のアプリから移植した補充ロジックや cycles 残高アラートは追加しないでください。
- エンジン canister から XRC/しきい値署名/vetKD を直接呼び出す。 クラウドエンジンはしきい値署名をまったく提供していないため(プラットフォームチーム)、
sign_with_ecdsa/sign_with_schnorr/vetKD は mainnet 上で呼び出す必要があります。しかし、サブネットをまたぎ cycles を伴う呼び出しは、CloudEngine サブネット上の canister が送信できない呼び出しそのものです。これらは資金があるコンソールプロキシ経由でルーティングしてください(ルール 4)。同一サブネット内の通常の呼び出しや、cycles を伴わない呼び出しにはプロキシは不要です。 - 外側の
proxy呼び出しに cycles を付ける。 エンジンのサブネットでは、サブネットをまたぐメッセージに cycles を付けることは禁止されています。それがプロキシを使う理由です。cycles はProxyArgs.cyclesの中に設定してください(プロキシがローカルで添付します)。proxy自体の呼び出しにwith cyclesを付けてはいけません。 - 何を中継していたかを確認せずにプロキシへ cycles を補充する。
ProxyError::InsufficientCyclesが発生したら、資金不足として扱う前に呼び出しの種類を確認してください。HTTPS アウトコールは、そもそもプロキシを使うべきではありません(ルール 3、落とし穴 4)。何もコストがかからない処理の予算を買うのではなく、自分の canister 上に移してください。残高を増やしても、同じ停止が先延ばしになるだけです。残高の枯渇が本当の資金不足となるのは、プロキシ本来の用途(XRC、しきい値署名、vetKD、Bitcoin API、EVM/SOL RPC)の場合に限られます。コンソールプロキシは、1 時間あたりの上限の範囲内で、数分おきにエンジンの残高から自動補充されます。そのため、InsufficientCyclesが継続する場合は、エンジンの残高に送れる余剰分がない(所有者がエンジンに資金を追加します)か、アプリの消費が補充上限を超えています。プロキシには補充ボタンはありません。以前のカード決済方式のプロキシには、エンジンの Canisters ページにある Proxy canisters セクションに Top up と Automatic top-up があります。残高についてはdeploy-to-cloud-engineを参照してください。 - プロキシ経由でも直接呼び出しと同じキーが得られると期待する。 プロキシ経由のしきい値キー導出は呼び出し元ごとに分離されるため、導出されたキー/アドレスは管理 canister を直接呼び出した場合と同じではありません。公開鍵の取得と署名は一貫してプロキシ経由で行い、同じ ID に対して直接呼び出しとプロキシ経由のキー呼び出しを混在させないでください。
- エンジンでチェーンキーのヘルパーライブラリを使う。
ic-vetkeys、ic-cdk-management-canisterのチェーンキー関数、mo:ic-vetkeys/ManagementCanisterはaaaaa-aaを直接呼び出し、手数料も独自に添付します。チェーンキーのコスト API はキーを保有するサブネットに基づいて料金を計算するため、エンジンでもゼロ以外の値となります。その結果、サブネットをまたぐ cycles 付き呼び出しとなり、ルール 1 と 2 に従ってエンジンに拒否されます。これらのライブラリの主な利点(正しい型、自動的な cycles 添付)こそが不要になる部分です。管理 canister の引数を手動でエンコードし、プロキシ経由で中継してください。vetkeysスキルからコピーしたコードは、エンジンでは変更なしで動作しません。 - 前述の警告を過剰に解釈して、アウトコールまでプロキシ経由にする。 上記の警告の対象はチェーンキー関数であり、それらを含むクレートではありません。Rust のアウトコールコードでも
ic-cdk-management-canisterを使う必要があります(ルール 3)。そのHttpRequestビルダーは、エンジンでもそのまま正しく使えます。「このクレートはエンジンで動かないので、アウトコールを手動でエンコードしてプロキシ経由にしなければならない」と結論づけると、落とし穴 4 に直行します。適用できない transform と、枯渇するプロキシ残高が問題になります。クレート名ではなく、対象サブネットで判断してください。aaaaa-aaのhttp_requestはローカルで無料ですが、署名と vetKD はリモートで有料です。 - エンジン向けに pricing-version-2 の予算を調整する。
ic-cdk-management-canister0.2 は常に version 2 の料金体系を使います。この体系では、添付する cycles が各ノードの予算にもなり、with_expected_*の上限が狭すぎると呼び出しが途中で失敗することがあります。エンジンでは無料のコストスケジュールによる課金は発生しないため、この予算で制限されることはありません。タイムアウトと transform の命令数上限はプロトコル上限に設定され、InsufficientCyclesが発生することもありません。したがって、ここで設定値を変更しても効果はありません。エンジンのアウトコールを修正するためにこれらの設定を追加したり、エンジンのアウトコール失敗を予算切れと診断したりしないでください。例外は独自のmax_response_bytesです。これは引き続きレスポンスを制限するため、返信が大きすぎることは実際の失敗原因になり得ます。(料金バージョン自体も管理する必要はありません。レプリカが従量課金に対応していない場合、version 2 は暗黙に version 1 に切り替わり、どちらもエンジンではコストがゼロです。) - 署名アプリのプロキシを削除したり、別のものに切り替えたりする。 管理 canister は呼び出し元のキーを導出するため、プロキシの canister ID はアプリが所有するすべてのアドレスの一部です。プロキシを削除したり(コンソールはプロキシの cycles をエンジンの残高に戻します)、アプリの接続先を別のプロキシに変更したりすると、アドレスが気づかないうちに変わり、旧アドレスにある Bitcoin や Ethereum の資金は永久に使えなくなります。署名アプリのプロキシ ID は、交換可能なリソースではなく、永続的なインフラストラクチャです。
CallFailedを「処理は実行されなかった」と解釈する。 プロキシは有界待機呼び出しで中継するため、下流のSYS_UNKNOWNは、拒否コードをreasonに含むProxyError::CallFailedとして返されます。対象が実行済みで、cycles も消費済みの可能性があります。この場合、署名の再試行は無害ですが、状態を変更する呼び出しの再試行は安全ではありません。不明な結果として扱ってください(multi-canisterを参照)。- CLI からコンソールプロキシを呼び出す。
icp canister call --proxy <console-proxy-id>は失敗します。コンソールプロキシが許可するのは、エンジンの canister ID の範囲と自身のコントローラーのみです。CLI の ID からの ingress 呼び出しはいずれにも該当せず、複製状態に到達する前にinspect_messageで拒否されます。CLI 側でプロキシを使う場合は、自分のコントローラーである自分を認可する独自のプロキシをデプロイしてください(icp new … --subfolder proxy)。 - プロキシの有無を確認せずに、プロキシ依存のコードを書く。 署名、vetKD、XRC、Bitcoin API、EVM/SOL RPC のコードは、プロキシがなくても作成でき、デプロイさえできますが、最初の呼び出しで失敗します。プレースホルダーや推測した ID では
UnauthorizedUserが返るか、どこにも到達しません。また、別のエンジンやプロジェクトからコピーした ID を使うと、他者のキーで署名されます。コンソールプロキシは自分で作成できません。icpコマンドはなく、エンジンの所有者だけがコンソールからデプロイできます。必要性が分かった時点ですぐに依頼し(ルール 4 の「コードを書く前に」)、設定から ID を読み込み、エンジン用のプロキシの代わりに自分でデプロイしたプロキシを使わないでください。 - コンソールプロキシが対応していない対象へ中継する。 コンソールプロキシがエンジンの canister に代わって中継するのは、管理 canister のしきい値キー、
http_request、bitcoin_*メソッドと、XRC、EVM RPC、SOL RPC、Bitcoin、Dogecoin の canister だけです。それ以外の呼び出しは、cycles を課金するサードパーティの canister を含め、残高に関係なくCallFailed(a proxy funded by its engine relays only to …)となります。補充や再試行はせず、呼び出し元の canister をコントローラーとして認可する独自のプロキシ経由でルーティングするか、その依存関係を削除してください。
追加の参考資料
- アプリをエンジンにデプロイする方法については、
deploy-to-cloud-engineを読み込んでください。CLI の ID リンク、サブネットを指定したデプロイ、コンソールのアプリメタデータ、およびプロキシのデプロイと資金提供について説明しています。このスキルのルール 4 は、コンソールでの手順(エンジンの所有者がデプロイし、エンジンの残高から支払う)、それぞれの費用をどの残高が支払うか、および CLI で自分でデプロイする方法に依存します。 - vetKD とは何か、その鍵がどのように使われるか(IBE、暗号化マップ、トランスポート鍵)については、
vetkeysを読み込んでください。呼び出しコードは通常のサブネットを対象とし、ヘルパーライブラリを通じてサイクルを付与します。エンジンでは概念を維持し、呼び出しを上記のプロキシ経由の形式に置き換えてください。 - エンジン固有ではないアウトコールのすべての内容については、
https-outcallsを読み込んでください。変換関数、max_response_bytes、冪等性、コンセンサス失敗のデバッグについて説明しています。 - キャニスター間呼び出しの設計、制限付きおよび無制限の待機セマンティクス、
SYS_UNKNOWNの処理については、multi-canisterを読み込んでください。