ICP上でチェーンキー暗号を使ってあらゆるブロックチェーンと接続する
- ICP Japan
- 4月28日
- 読了時間: 10分
ICPにおける信頼不要なクロスチェーン通信のための開発者ガイド

ブロックチェーンの世界には、あまり語られない「汚い秘密」があります。ブリッジ(異なるブロックチェーンを接続する仕組み)は、長らくセキュリティの悪夢として知られ、ユーザーを深刻なリスクにさらしてきました。実際に、Ronin Bridgeの6億ドル超のハッキング事件をはじめ、2022年だけでおよそ20億ドルもの損失がブリッジを介して発生しています。異なるブロックチェーン同士を安全につなぐことは、いまだに大きな課題です。
多くのブリッジは中央集権的な運営者に依存していたり、新たなセキュリティ上の脆弱性を導入してしまったりと、「分散化」という本来の目的を損なう構造になってしまっています。その結果、ブリッジに関連したハッキングは後を絶たず、大きなリスクであることが浮き彫りになっています。
この問題の本質は「信頼」にあります。あるブロックチェーン上のアプリケーションが、別のチェーンとやり取りする際に、中継者を信頼せずに済む方法はあるのか?この「信頼不要な相互運用性」の要件こそが、複数のブロックチェーンをまたぐ本当に分散化されたシステムを構築する上で不可欠なものです。もしこれがなければ、Web3アプリケーションの可能性は、それぞれ孤立したチェーン内にとどまってしまいます。
その答えが Chain Key Cryptography(チェーンキー暗号) です。これは、ICPのノード自身が他のブロックチェーン(Bitcoin、Ethereumなど)の鍵を直接保持し、トランザクションに署名できるようにする組み込み型の暗号技術です。
本ガイドでは、開発者向けにこの仕組みをどのように使うかを実践的に解説します。Chain Key Cryptographyの動作原理、実装のコード例、セキュリティ上の考慮点、ICPアプリケーションを外部ネットワークと効果的に接続するためのテクニックなどが含まれています。
詳細な仕組みについては「Chain Fusion Overview」を、対応チェーンの拡大リストについては「Supported Chains」をご覧ください。
Chain Key Cryptography Explained(Chain Key暗号の仕組み)
ICP(Internet Computer Protocol)におけるChain Key Cryptographyは、その中核に**しきい値暗号(Threshold Cryptography)**を用いています。
簡単に言えば、通常1つの場所に保管される秘密鍵を複数の「秘密分散パーツ」に分けて、ICPネットワークを構成する独立したノード群に分散して保有させるのです。
特定のアクション(例:Bitcoinトランザクションへの署名)を実行するには、あらかじめ定められた最小数(しきい値)のノードが協調して暗号計算を行う必要があります。
個々のノード単体では秘密鍵を復元することはできず、複数ノードの協調署名がなければ何もできません。
この構造により単一障害点(SPOF)が排除され、セキュリティが飛躍的に高まります。
この仕組みにより生成・管理される資産は、たとえば ckBTC や ckETH といった ckToken(Chain Key Token) という形式で表されます。
たとえば、ある canister(スマートコントラクト)が ckBTC を外部へ送金したいとします。このとき canister は ICPネットワーク自体に署名の実行を依頼します。
すると、ノード群が協調してChain Keyの秘密共有情報を使って署名を行い、完全に分散化されたICP内部で署名が完結します。この仕組みにより、外部のブリッジや中央集権的なオペレーターを必要としません。
なぜこの仕組みが重要か?
✅ トラストレス:従来のように「信頼できるブリッジ運営者」に依存する必要がない
✅ セキュア:秘密鍵が単一点に存在せず、攻撃者が奪う鍵そのものが存在しない
✅ 効率的:ICP上で完結するため、外部サーバーや中継チェーンを使わないシンプルな構造
クロスチェーンCanisterの実装例(Motoko)
以下は、Chain Key Cryptographyを利用してBitcoinのトランザクションに署名する処理を行うMotokoコードの簡易例です:
motoko
コピーする編集する
actor { // ICの管理Canisterのインターフェース定義(ECDSA署名APIを使う) let ic : actor { sign_with_ecdsa : ({ message_hash : Blob; derivation_path : [Blob]; key_id : { curve : { #secp256k1 }; name : Text }; }) -> async { signature : Blob }; } = actor("aaaaa-aa"); // 管理Canisterのプリンシパル // Bitcoinトランザクションの署名をリクエストする関数 public shared (msg) func sign_btc_tx(message_hash: Blob) : async { #Ok : Blob; #Err : Text } { try { Cycles.add(10_000_000_000); // 署名リクエストに必要なCycleを追加 let result = await ic.sign_with_ecdsa({ message_hash = message_hash; derivation_path = [Principal.toBlob(msg.caller)]; key_id = { curve = #secp256k1; name = "dfx_test_key" }; }); #Ok(result.signature) // 成功時、署名を返す } catch (err) { #Err(Error.message(err)) // エラー発生時はメッセージを返す } } }
Ethereumとの連携も可能
Ethereumと連携する場合も基本的な仕組みは同じですが、EVM互換チェーン特有のnonce管理やガス価格設定などが必要になります。これらはChain Fusionが内部で自動的に処理してくれるように設計されており、開発者は高レベルのAPIで安全なクロスチェーン操作を記述できます。
このように、ICPのChain Key Cryptographyを活用することで、Web3におけるクロスチェーン通信を“本当に分散型かつセキュア”に実現できます。
次回は、Ethereum連携におけるckETH運用の仕組みや、canisterベースのクロスチェーンDAppの全体アーキテクチャについても解説できます。希望があればお知らせください。
Rustコード:
// 概念的:EVM RPC Canister 経由で Ethereum スマートコントラクトとやり取りする
rust
コピーする編集する
use candid::{CandidType, Deserialize, Principal}; use ic_cdk::api::call::call_with_payment128; #[derive(CandidType, Deserialize)] struct EthCallArgs { block: Option<String>, transaction: EthTransaction, } #[derive(CandidType, Deserialize)] struct EthTransaction { to: Option<String>, input: Option<String>, // ... 他のフィールドは簡略化のため省略 } #[ic_cdk::update] async fn call_eth_contract() -> Result<String, String> { let canister_id = Principal::from_text("evm_rpc_canister_id").unwrap(); let args = EthCallArgs { block: None, transaction: EthTransaction { to: Some("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48".to_string()), input: Some("0x70a08231000000000000000000000000b25eA1D493B49a1DeD42aC5B1208cC618f9A9B80".to_string()), // ... 必要に応じて他のフィールドも追加 }, }; // 必要なサイクルを添付 let (result,): (Result<String, String>,) = call_with_payment128( canister_id, "eth_call", (args,), 2_000_000_000u128, ) .await .map_err(|e| format!("Call failed: {:?}", e.1))?; result }
外部ネットワークにトランザクションをブロードキャストした後、Canister はその成功を確認する方法を必要とします。
これは通常、対象のブロックチェーンに対してクエリを送ることで行います。たとえば、Canister 内にそのためのロジックを組み込んだり、ICP の統合 API を使って対象チェーンを直接クエリすることもできます。Bitcoin の状態確認には bitcoin_get_utxos や bitcoin_get_balance といった Bitcoin 統合 API のメソッドが使用可能です。Ethereum や他の EVM チェーンに関しては、EVM RPC Canister を利用して、外部チェーン上のトランザクションハッシュの状態を定期的にチェックできます。
確認が取れた後、Canister は自身の状態を適切に更新することができます。例えば、注文を完了としてマークしたり、ICP 上の対応する資産を解放するなど。この検証ステップは、堅牢なクロスチェーンアプリケーションにとって本質的に重要です。
トラストレス通信のためのセキュリティパターン
Chain Key Cryptography は、ICP アプリケーションと外部ブロックチェーンを接続します。以下は、Bitcoin および Ethereum に対する直接的なアプローチです。
Bitcoin(ckBTC)
ckBTC を使った Bitcoin 統合では、あなたの Canister は ICP 上の ckBTC minter canister と通信します。Bitcoin をそのアドレスに送るか、ICP トークンを minter に送って、ICP 上で ckBTC を受け取ります。Bitcoin を外部に送るには、トランザクションの詳細を準備し、Chain Key API を用いて ckBTC のシステム Canister に署名を依頼します。このプロセスにより、Bitcoin の流動性が ICP の DeFi アプリケーション内に取り込まれます。
概念的には、出金をリクエストする処理は次のような簡略化された例になります。
import EvmRpc "canister:evm_rpc";
import Cycles "mo:base/ExperimentalCycles";
import Debug "mo:base/Debug";
actor {
public func call() : async ?Text {
// Ethereumネットワークの指定(この例ではメインネット)
let services = #EthMainnet(null);
let config = null;
// RPC呼び出しに必要なサイクルを添付
Cycles.add<system>(2_000_000_000);
// Ethereumスマートコントラクト呼び出し(例:ERC20のbalanceOf)
let result = await EvmRpc.eth_call(services, config, {
block = null;
transaction = {
to = ?"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; // コントラクトアドレス
input = ?"0x70a08231000000000000000000000000b25eA1D493B49a1DeD42aC5B1208cC618f9A9B80"; // ABIエンコードされたcall data(balanceOfアドレス)
accessList = null;
blobVersionedHashes = null;
blobs = null;
chainId = null;
from = null;
gas = null;
gasPrice = null;
maxFeePerBlobGas = null;
maxFeePerGas = null;
maxPriorityFeePerGas = null;
nonce = null;
type_ = null;
value = null;
};
});
// レスポンスの処理
switch result {
case (#Consistent(#Ok response)) {
Debug.print("Success: " # debug_show response);
?response; // ABIエンコードされたレスポンス
};
case (#Consistent(#Err error)) {
Debug.trap("Error: " # debug_show error);
null;
};
case (#Inconsistent(_results)) {
Debug.trap("Inconsistent results");
null;
};
};
};
};
Rustコード(ckBTCの引き出しリクエストの概念例)
rust
コピーする
編集する
use ic_cdk::api::call::call;
use candid::{Nat, Principal};
// ステップ1:ckBTC minterにckBTC使用の承認を与える
async fn approve_ckbtc(amount: Nat) -> Result<(), String> {
let spender = Principal::from_text("mqygn-kiaaa-aaaar-qaadq-cai").unwrap(); // ckBTC minterのCanister ID
let args = (
spender,
amount,
None::<Vec<u8>>, // from_subaccount
None::<Nat>, // expected_allowance
None::<u64>, // expires_at
None::<Nat>, // fee
None::<Vec<u8>>, // memo
None::<u64>, // created_at_time
);
let _: () = call(
Principal::from_text("ckbtc_ledger_canister_id").unwrap(),
"icrc2_approve",
args,
)
.await
.map_err(|e| format!("Approve failed: {:?}", e.1))?;
Ok(())
}
// ステップ2:BTCアドレスへの出金リクエスト
async fn withdraw_ckbtc(btc_address: String, amount: Nat) -> Result<u64, String> {
let args = (
btc_address,
amount,
None::<Vec<u8>>, // from_subaccount
);
let (block_index,): (u64,) = call(
Principal::from_text("mqygn-kiaaa-aaaar-qaadq-cai").unwrap(), // ckBTC minterのCanister ID
"retrieve_btc_with_approval",
args,
)
.await
.map_err(|e| format!("Withdraw failed: {:?}", e.1))?;
Ok(block_index)
}
このフローは、ICRC-2規格に準拠した「承認→出金」の流れに従っています。コードの詳細な実装は公式ソースなどで確認できます。
技術的な詳細については、ckBTC OverviewおよびckBTC API Referenceをご参照ください。
Ethereum(ckETH)
ckETH のミントやバーンを行う際、Internet Computer と Ethereum の統合は、ckETH ミンター・キャニスターと Ethereum 上のヘルパーコントラクトによって処理されます。開発者は、Ethereum トランザクション全体を手動で構築したり、ガス代、ノンス、受取人などのパラメータを扱ったりする必要はありません。
ckETH をミントするには、ユーザーが自分の ICP プリンシパル(ウォレットアドレス)を指定して、Ethereum 上の ckETH ヘルパーコントラクトに ETH を入金します。ckETH ミンター・キャニスターは Ethereum ネットワークを監視し、有効な入金を検知すると、対応する ckETH をユーザーの ICP アカウントにミントします。Ethereum のトランザクション(すべての必要なパラメータを含む)は、ICP キャニスターではなくユーザー自身が Ethereum 上で構築・送信します。
ckETH をバーンして ETH を引き出すには、ユーザーが ICP 上の ckETH ミンター・キャニスターと対話し、出金額と送金先の Ethereum アドレスを指定します。ミンター・キャニスターは ckETH をバーンし、対応する Ethereum トランザクションを内部で構築します。ミンターは Chain-Key ECDSA 署名を使用して安全にトランザクションに署名し、HTTPS アウトコールを通じて Ethereum ネットワークへリレーします。ユーザーが Ethereum トランザクションを手動で構築・署名する必要はなく、ckETH ミンター・キャニスターがそれを抽象化してくれます。
以下は ckETH 送信の概念的な例で、Bitcoin と比較して追加のパラメータがあることを示しています。
Motoko 例:
motoko
コピーする編集する
// ckETH の出金リクエスト(ckETH をバーンして Ethereum 上で ETH を受け取る) import CkethLedger "canister:cketh_ledger_canister"; import CkethMinter "canister:cketh_minter_canister"; import Principal "mo:base/Principal"; actor { // ステップ1:ckETH ミンターに支出許可を与える(ICRC-2 承認) public shared ({ caller }) func approve_cketh(amount: Nat) : async () { let spender = Principal.fromText("jzenf-aiaaa-aaaar-qaa7q-cai"); // ckETH ミンターのキャニスターID let res = await CkethLedger.icrc2_approve({ spender = spender; amount = amount; from_subaccount = null; expected_allowance = null; expires_at = null; fee = null; memo = null; created_at_time = null; }); // 結果の処理(省略) }; // ステップ2:Ethereum アドレスへ出金リクエスト public shared func withdraw_cketh(eth_address: Text, amount: Nat) : async () { let res = await CkethMinter.withdraw_eth_with_approval({ eth_address = eth_address; amount = amount; from_subaccount = null; }); // 結果の処理(トランザクションハッシュまたはエラー) }; }
Rust 例:
rust
コピーする編集する
// ckETH の出金リクエスト(ckETH をバーンして Ethereum 上で ETH を受け取る) use ic_cdk::api::call::call; use candid::{Nat, Principal}; // ステップ1:ckETH ミンターに支出許可を与える async fn approve_cketh(amount: Nat) -> Result<(), String> { let spender = Principal::from_text("jzenf-aiaaa-aaaar-qaa7q-cai").unwrap(); // ミンターID let args = ( spender, amount, None::<Vec<u8>>, // from_subaccount None::<Nat>, // expected_allowance None::<u64>, // expires_at None::<Nat>, // fee None::<Vec<u8>>, // memo None::<u64>, // created_at_time ); let : () = call( Principal::fromtext("cketh_ledger_canister_id").unwrap(), "icrc2_approve", args, ) .await .map_err(|e| format!("Approve failed: {:?}", e.1))?; Ok(()) } // ステップ2:Ethereum アドレスへ出金リクエスト async fn withdraw_cketh(eth_address: String, amount: Nat) -> Result<String, String> { let args = ( eth_address, amount, None::<Vec<u8>>, // from_subaccount ); let (tx_hash,): (String,) = call( Principal::from_text("jzenf-aiaaa-aaaar-qaa7q-cai").unwrap(), // ミンターID "withdraw_eth_with_approval", args, ) .await .map_err(|e| format!("Withdraw failed: {:?}", e.1))?; Ok(tx_hash) }
両方のコードスニペットは、ICP ドキュメントで説明されている ICRC-2 承認および出金フローに従っています。
開発者は ICP ETH 開発者ワークフローに従い、EVM RPC キャニスターを活用することでこれらの操作を行えます。
この基本的なパターンは、Chain Fusion によってサポートされる他のブロックチェーンにも拡張可能です。通常は、そのブロックチェーン専用のシステムキャニスターまたは API とやり取りします。これらのコンポーネントは、アドレス生成、トランザクションの整形、しきい値署名の要求、外部トランザクションのステータス確認などを管理します。これにより、ICP の Chain Fusion が拡張される中で新しいチェーンのサポートが容易になります。
Comments