スマートコントラクトにおける「乱数」の罠:ブロックハッシュを信じるな
現場の諸君、お疲れ様。今日もどこかのWeb3プロジェクトが「乱数生成のミス」で数億円単位の資産を消失させている。ニュースで見る「ハッキング」という言葉は派手だが、実態はもっと地味で、そして残酷だ。今回は、スマートコントラクトにおける「ブロックハッシュを乱数源にする」という、初歩的かつ致命的な過ちについて深掘りしよう。
なぜブロックハッシュを使ってはいけないのか?
スマートコントラクトでよくある実装として、blockhash(block.number - 1) を使って「当たり判定」を行うものがある。一見すると、ブロックのハッシュ値は予測不可能でランダムに見えるかもしれない。
しかし、攻撃者はノードを動かしているマイナーやバリデーターと結託するか、あるいは自分自身がバリデーターである場合、「自分に都合の良いハッシュ値が出るまでブロックを生成し直す(あるいは破棄する)」ということが可能だ。また、スマートコントラクト内で blockhash を参照する場合、そのトランザクションを実行するマイナーは、計算前に結果を知ることができる。つまり、予測可能であると同時に、操作可能なのだ。
攻撃シナリオ:PoC(Proof of Concept)
例えば、以下のような脆弱なコントラクトを想像してほしい。
// 警告:絶対に本番環境で使わないこと
function play(uint256 guess) public payable {
// ブロックハッシュを乱数源にするのは最悪の選択
uint256 random = uint256(blockhash(block.number - 1));
if (random % 2 == guess) {
payable(msg.sender).transfer(address(this).balance);
}
}
攻撃者は、このコントラクトを呼び出す際、まずローカルで同じロジックを走らせる。もし結果が負けなら、そのトランザクションをブロックチェーンに送信しない(あるいはマイナーであれば、そのブロックを無視して次のブロックを掘る)。これだけで、勝率100%のギャンブルが完成する。
解決策:Chainlink VRF を採用せよ
「じゃあどうすればいいんだ?」という問いに対する唯一の正解は、「外部からの検証可能なランダム性(Verifiable Random Function: VRF)」を使うことだ。Chainlink VRFは、暗号学的な証明を用いて、誰にも操作できない真の乱数を提供してくれる。
セキュアな実装サンプル(Chainlink VRF v2.5)
まず、Chainlinkのコントラクトを継承し、リクエストとレスポンスを分離する。これがWeb3における「非同期」の作法だ。
// VRFv2PlusConsumer.sol
import "@chainlink/contracts/src/v0.8/vrf/dev/VRFConsumerBaseV2Plus.sol";
import "@chainlink/contracts/src/v0.8/vrf/dev/libraries/VRFV2PlusClient.sol";
contract SecureRNG is VRFConsumerBaseV2Plus {
uint256 public s_requestId;
uint256 public s_randomResult;
// VRFリクエストのトリガー
function requestRandomWords() external onlyOwner {
s_requestId = s_vrfCoordinator.requestRandomWords(
VRFV2PlusClient.RandomWordsRequest({
keyHash: 0x..., // ネットワークごとのキーハッシュ
subId: 12345, // サブスクリプションID
requestConfirmations: 3, // ブロック確定数
callbackGasLimit: 100000,
numWords: 1,
extraArgs: VRFV2PlusClient._argsToBytes(VRFV2PlusClient.ExtraArgsV1({nativePayment: false}))
})
);
}
// VRFからのコールバック(ここが乱数の受け取り口)
function fulfillRandomWords(uint256 requestId, uint256[] calldata randomWords) internal override {
s_randomResult = randomWords[0];
// ここで安全にゲームロジックを実行する
}
}
現場のエンジニアへ伝えたいこと
この実装を見ると「面倒くさい」と感じるだろう。リクエストを出して、コールバックを待つ。これだけで、ガス代は増えるし、UXも複雑になる。
しかし、セキュリティの現場では「便利さと安全性は常にトレードオフ」だ。ブロックハッシュを使った「手抜き実装」は、ローンチ直後ではなく、コントラクト内に大きな資金が溜まった瞬間に狙われる。それは「時限爆弾」を仕込んでいるのと同じことだ。
防御を固めるためのチェックリスト
1. ブロックハッシュ禁止令: block.timestamp や blockhash を乱数生成の根拠にすることをチームの規約で禁止せよ。
2. 監視の徹底: 万が一のインシデントに備え、コントラクトの「異常な出金」を検知する OpenZeppelin Defender などのモニターを必ず導入すること。
3. 監査(Audit): どんなにコードに自信があっても、第三者のセキュリティ専門家にコードを投げろ。自分たちのバイアスは自分では消せない。
もし君たちが開発しているシステムで「乱数」が必要になったら、まずは Chainlink のドキュメントを熟読し、なぜこの仕組みが必要なのか、数式レベルで理解してほしい。攻撃者は常に「最も効率的に資産を奪う方法」を考えている。我々はその数歩先を行かなければならない。
現場からは以上だ。次は、Reentrancy(リエントランシー)攻撃の防ぎ方について語ろうか。実装で詰まったら、いつでも相談に来てくれ。
コメント