皆さん、こんにちは! 最先端Web3セキュリティリサーチャー兼セキュリティバイブル主筆ライターの私がお届けする、今日のセキュリティ深掘りタイムです。
ブロックチェーンの世界に足を踏み入れたばかりの皆さんや、セキュリティってなんだか難しそう…と思っているIT担当者の皆さん、ご安心くださいね。今日は、スマートコントラクトにおける「乱数」という、一見地味ながらも実はサイバー攻撃者が目を光らせている超重要ポイントについて、まるで家の防犯対策を学ぶように、優しく、そして泥臭い現場のリアルを交えながら解説していきます。
スマートコントラクトの「運命」を握る乱数生成の落とし穴
ブロックチェーン上で動く「スマートコントラクト」って、自動販売機みたいに「この条件が満たされたら、この処理を実行する」という約束事をコードにしたもの、ですよね。DeFiの宝くじ、NFTのミント(発行)時のレアリティ抽選、ゲームのアイテムドロップ率決定など、さまざまな場面で「ランダムな要素」が必要になります。
でも、この「ランダム」というやつが、ブロックチェーンの世界ではとんでもなく厄介な代物なんです。
宝くじの抽選箱が透明だったら? ブロックハッシュの危険性
皆さんは、スマートコントラクトの中で「ランダムな数字」が必要になった時、どんな方法を思いつきますか? 「そうだ、次のブロックのハッシュ値を使おう!」と考える人もいるかもしれませんね。
「ブロックハッシュ」というのは、ブロックチェーンを構成する一つ一つの「ブロック」という箱に付けられた、唯一無二のデジタルなIDのようなものです。一見すると、このハッシュ値はランダムに見えるし、毎回違う値が出てくるから、これを乱数源に使えばいいんじゃないか? と思ってしまいますよね。
でも、ここがサイバー攻撃者が狙う、まさに「盲点」なんです。
例え話で考えてみましょう。
あなたの家で、大切な抽選会をするとします。参加者には、ある数字を予想してもらい、その数字が抽選で当たったら豪華景品がもらえる、という企画です。
この抽選で、「次のブロックのハッシュ値」を使うというのは、まるで「抽選箱が透明で、しかも抽選員が箱の中の玉の配置をある程度操作できる」ようなものなんです。
どういうことか?
1. マイナー(バリデーター)の存在: ブロックチェーンでは、マイナー(あるいはバリデーター)と呼ばれる人たちが、新しいブロックを作成する役割を担っています。彼らは、どのトランザクション(取引)をブロックに含めるか、そしてそのブロックをどんな順番で並べるかを、ある程度コントロールできるんです。
2. ハッシュ値の予測可能性: マイナーは、次のブロックのハッシュ値がどうなるかを、ある程度事前に計算できます。完全に「予測」できるわけではないですが、「このトランザクションを入れれば、ハッシュ値が特定の条件を満たすはずだ」といった操作は可能な範囲でできてしまうことがあるんです。
3. 攻撃シナリオ:フロントランニング: もしあなたが「次のブロックハッシュが偶数だったら勝ち」という宝くじスマートコントラクトを作ったとします。悪意ある攻撃者(あるいはマイナー自身)は、自分のトランザクションをブロックに含める際に、事前にハッシュ値を計算して「このままでは奇数になるから、自分のトランザクションを一旦外して、別のトランザクションを先に入れるか、あるいはちょっとだけデータを変えて再試行しよう」といった操作ができてしまいます。
これはまるで、泥棒があなたの家の鍵穴を事前に調べて、特定の鍵が合うタイミングで侵入しようと計画するようなものです。彼らはあなたの行動(トランザクション)を先読みし、自分にとって有利な状況を作り出すことができるのです。
DeFiの世界では、実際にこの種の脆弱性を突かれて、抽選系のイベントやホワイトリスト登録などが操作され、多くのプロジェクトが痛い目に遭ってきました。現場でのインシデントハンドリングでは、原因が特定されにくい「予測できないはずの出来事」の裏に、このような巧妙な操作が隠れていることが少なくありません。
信頼できる鍵師に依頼する! Chainlink VRFという解決策
では、どうすればこの「抽選箱が透明で、抽選員が操作できる」問題を解決できるのでしょうか? そこで登場するのが、Chainlink VRF (Verifiable Random Function) です。
Chainlink VRFは、まるで「絶対に複製できない特殊な鍵を、信頼できるプロの鍵師に依頼して作ってもらう」ようなサービスだと考えてください。
Chainlink VRFって何?
Chainlink VRFは、ブロックチェーンの外(オフチェーン)で、暗号学的に安全な方法で乱数を生成し、その乱数が本当にランダムで、かつ誰にも操作されていないことを「証明」して、ブロックチェーンの中(オンチェーン)に安全に届けてくれるサービスです。
仕組みをもう少し詳しく見てみましょう。
1. リクエスト: あなたのスマートコントラクトが「乱数が欲しい!」とChainlink VRFにリクエストを送ります。
2. オフチェーンでの乱数生成と暗号証明: ChainlinkのVRFノード(信頼できる計算機)が、あなたのリクエストを受け取ります。そして、秘密の鍵を使って乱数を生成し、その乱数が「自分の秘密鍵を使って、公正に生成されたものである」ということを示す「暗号証明」も一緒に作成します。
3. オンチェーンへの送信と検証: 生成された乱数と暗号証明が、Chainlinkのコントラクトを通じてあなたのスマートコントラクトに送り返されます。あなたのコントラクトは、送られてきた乱数が本当にVRFノードの公開鍵と暗号証明に合致しているかを確認(検証)します。もし一致しなければ、その乱数は受け入れられません。
これは、「公正な第三者機関が、誰も中を覗き込めない頑丈な金庫の中で、厳重な管理のもとサイコロを振り、その結果と、その結果が本当に公正に得られたものであるという証明書をセットで届けてくれる」ようなものです。しかも、その証明書は誰でも後から確認できるので、「本当に公正だったのか?」という疑念を一切抱かせません。
Chainlink VRF v2 を使った安全な抽選ロジックの実装例
それでは、実際にSolidityでChainlink VRF v2を使って、安全な乱数を生成するスマートコントラクトの例を見てみましょう。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
// Chainlink VRF v2 のインターフェースをインポート
import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";
import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol";
// VRFConsumerBaseV2 を継承することで、Chainlink VRFのコールバック関数を簡単に実装できます
contract RandomNumberConsumerV2 is VRFConsumerBaseV2 {
// Chainlink VRF Coordinator コントラクトのアドレス
// ネットワークによって異なるため、デプロイ先のネットワークに合わせて設定してください
// 例: Sepoliaテストネット: 0x8103B0A8A00be2DDC778e67E7e4DA18ad62606C7
VRFCoordinatorV2Interface COORDINATOR;
// Chainlink VRFのサブスクリプションID
// これはChainlink VRFを使うために事前に登録・資金提供が必要なIDです
uint64 s_subscriptionId;
// VRFリクエストで使用するkeyHash
// どのVRFノードの公開鍵を使うかを指定するハッシュ値です
// ネットワークによって異なるため、デプロイ先のネットワークに合わせて設定してください
// 例: Sepoliaテストネット: 0x474e236d8ae01f8d4806a3fcd779b292e07718e479a3226a3d6049c302d1d013
bytes32 s_keyHash;
// コールバック関数のガスリミット
// 乱数生成後に`fulfillRandomWords`が呼ばれる際に消費されるガスの最大値です
uint32 s_callbackGasLimit;
// リクエストが確定するまでに必要なブロックの確認数
// 高いほどセキュリティは向上しますが、結果が返ってくるまでに時間がかかります
uint16 s_requestConfirmations;
// 生成をリクエストする乱数の数
uint32 s_numWords;
// 乱数リクエストIDを保存する変数
// このIDを使って、どのリクエストに対する乱数結果かを追跡します
uint256[] public s_randomWords;
uint256 public s_requestId;
address public s_owner; // コントラクトのデプロイヤー
// コンストラクタ
// コントラクトデプロイ時に必要なパラメータを設定します
constructor(
uint64 subscriptionId,
address vrfCoordinator
) VRFConsumerBaseV2(vrfCoordinator) {
COORDINATOR = VRFCoordinatorV2Interface(vrfCoordinator);
s_subscriptionId = subscriptionId;
s_keyHash = 0x474e236d8ae01f8d4806a3fcd779b292e07718e479a3226a3d6049c302d1d013; // 例: SepoliaテストネットのkeyHash
s_callbackGasLimit = 500000; // 500k gas
s_requestConfirmations = 3;
s_numWords = 1; // 1つの乱数をリクエスト
s_owner = msg.sender;
}
// 乱数をリクエストする関数
function requestRandomNumber() public returns (uint256 requestId) {
// VRF Coordinatorに乱数生成をリクエストします
// この関数が呼ばれると、VRFノードが乱数を生成し、`fulfillRandomWords`関数を呼び出します
s_requestId = COORDINATOR.requestRandomWords(
s_keyHash,
s_subscriptionId,
s_requestConfirmations,
s_callbackGasLimit,
s_numWords
);
return s_requestId;
}
// Chainlink VRFから乱数が返ってきたときに呼び出されるコールバック関数
// この関数はVRF Coordinatorによってのみ呼び出されるため、外部からの直接呼び出しはできません
function fulfillRandomWords(
uint256 requestId,
uint256[] memory randomWords
) internal override {
// リクエストIDが一致するか確認する(念のため)
require(s_requestId == requestId, "Request ID mismatch");
// 生成された乱数を保存します
s_randomWords = randomWords;
// ここで、抽選ロジックやゲームのアイテムドロップなど、
// 取得した乱数を使った処理を実行します
// 例: if (s_randomWords[0] % 100 < 10) { /* 10%の確率でレアアイテム */ }
}
// サブスクリプションにLINKトークンを追加するための関数(オプション)
// サブスクリプションの残高が不足するとVRFリクエストができなくなります
function topUpSubscription(uint256 amount) public {
// LINKトークンをこのコントラクトに送信してから、この関数を呼び出す必要があります
// 実際にはLINKトークンのコントラクトを呼び出して転送する必要があります
// これは概念的なコードであり、実際のLINK転送ロジックは別途実装が必要です
// COORDINATOR.addConsumer(s_subscriptionId, address(this)); // 必要であればコンシューマ追加
// COORDINATOR.fundSubscription(s_subscriptionId, amount); // LINKトークンのファンド
}
// コントラクトのオーナーのみが実行できる関数(例:VRFパラメータの更新など)
modifier onlyOwner() {
require(msg.sender == s_owner, "Only owner can call this function");
_;
}
}
コードのポイント解説:
VRFConsumerBaseV2の継承: これにより、Chainlink VRFからのコールバックを受け取るためのfulfillRandomWords関数を簡単に実装できるようになります。COORDINATOR: Chainlink VRFサービスの中央ハブとなるコントラクトのアドレスを指定します。これはネットワークごとに異なります。s_subscriptionId: Chainlink VRFを利用するには、「サブスクリプション」という形で事前にChainlink (LINK) トークンを預けておく必要があります。このIDはそのサブスクリプションを識別するものです。s_keyHash: どのChainlink VRFノードの公開鍵を使って乱数を生成するかを指定するハッシュ値です。これもネットワークごとに提供されています。requestRandomNumber(): この関数を呼び出すと、Chainlink VRFサービスに乱数生成をリクエストします。fulfillRandomWords(): ここが一番重要です! Chainlink VRFノードが乱数を生成し、その暗号証明とともにこの関数を呼び出します。この関数内で、取得した乱数randomWordsを使って、あなたのアプリケーション固有のロジック(抽選結果の決定、NFTのレアリティ割り当てなど)を実行します。この関数はVRF Coordinatorによってのみ呼び出されるため、外部から勝手に操作される心配はありません。
このコードは、まさに「信頼できる第三者機関が、絶対に不正のできない方法で抽選を行い、その結果を証明書付きであなたの元に届けてくれる」というプロセスをスマートコントラクトで実現しているわけですね。
一歩ずつ対策を学んでいきましょう!
ブロックチェーンのセキュリティは、従来のWebセキュリティとは異なる独特の盲点や攻撃手法が存在します。特に「乱数」のように、一見すると些細な部分に見えても、プロジェクトの信頼性やユーザーの資産に直結する重要な要素になり得ます。
ブロックハッシュを乱数源にすることは、泥棒に家の鍵の複製方法を教えているようなもの。Chainlink VRFのような検証可能な乱数生成サービスを導入することは、最先端の防犯システムと信頼できる警備会社にあなたの家を守ってもらうようなものです。
新しい技術を学ぶのは大変なことも多いですが、一つ一つの脆弱性とその対策を理解していくことで、より安全で信頼できるWeb3の世界を一緒に作っていけるはずです。
これからも、皆さんのプロジェクトがサイバー攻撃の餌食にならないよう、現場のリアルな知見を共有していきますので、一緒にセキュリティ意識を高めていきましょうね!
コメント