【入門編】 コントラクトのガス最適化とDoS攻撃耐性 – IoT・OT(制御システム) & ブロックチェーンセキュリティ防御ガイド

こんにちは!スマートコントラクトの開発や、ブロックチェーンの世界へようこそ。
新しい技術に触れるとき、「なんだか難しそうだな…」と感じてしまいますよね。一歩ずつ、身近な例えから一緒に紐解いていきましょう!

今回は、ブロックチェーンの世界でとっても大切な「ガス最適化とDoS(ドス)攻撃への耐性」についてお話しします。

「ガス? DoS?」と身構えてしまうかもしれませんが、大丈夫です。まずは、私たちの日常生活にある「鍵付きのポスト」に例えて考えてみましょう。

—

1. 身近な例え:郵便ポストの「無限チラシ攻撃」

想像してみてください。あなたの家の玄関に、大きな郵便ポストがあるとします。
このポスト、普通は郵便屋さんが手紙を入れていくものですよね。

ある日、迷惑なイタズラ男がやってきて、「入りきらないほどの大量のチラシ」を一度に無理やり詰め込もうとしました。どうなるでしょうか?

  • ポストの口がチラシでパンパンになり、本当に必要な重要なお手紙(家族からの連絡など)が入らなくなってしまいます。
  • ポストのフタが壊れて、鍵すら閉められなくなってしまいました。

これが、デジタル世界における「DoS攻撃(サービス妨害攻撃)」の本質です。攻撃者は、システムの処理能力の限界や、許された作業スペース(メモリやガス)をわざと使い切ることで、正当なユーザーがサービスを使えなくしてしまうのです。

—

2. スマートコントラクトにおける「ガス(Gas)」とは?

ブロックチェーン(例えばEthereumなど)では、プログラム(スマートコントラクト)を動かすために「ガス(Gas)」という手数料を支払います。これは、プログラムが実行する「計算量」や「データの保存量」に対して支払う燃料のようなものです。

ここで問題になるのが、「1回のトランザクション(手続き)で使えるガスの量には上限がある(Gas Limit)」というルールです。

もし、プログラムの中に「すべてのユーザーを順番にループ処理して確認する」という書き方をしておいたとします。サービス開始初期はユーザーが10人だったのでスムーズに動いていました。しかし、サービスが人気になってユーザーが10,000人に増えたらどうなるでしょうか?

ループ処理が10,000回に増え、必要なガス代が上限を超えてしまいます。その結果、「エラー:ガスが枯渇しました」となり、プログラムが途中で止まってしまうのです。これが、意図せず(あるいは悪意を持って)引き起こされる「ガス枯渇によるDoS攻撃」の仕組みです。

—

3. 危険なコードと安全なコードの比較

新人開発者がやりがちな「危ない書き方」と、それを防ぐ「安全な書き方」をコードで見てみましょう。

❌ 危険なパターン:可変長の配列をぐるぐる回すループ

以下のコードは、参加者が増えれば増えるほど、処理が重くなっていつか必ず動かなくなる(ガス枯渇を起こす)時限爆弾のようなコードです。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract UnsafeLoopExample {
    address[] public participants; // 参加者が無限に増える配列

    // 参加登録する関数
    function register() public {
        participants.push(msg.sender);
    }

    // 全員に報酬を配る関数(非常に危険!)
    function distributeRewards() public {
        // 参加者が数千人を超えると、ここで必ずガスが枯渇して止まります
        for (uint256 i = 0; i < participants.length; i++) {
            // 何らかの重い処理や送金処理
            // (例としてアドレスに送金するモック)
            payable(participants[i]).transfer(10 wei);
        }
    }
}

このコードでは、participants.length が大きくなりすぎると、ブロックチェーンの1ブロックあたりのガス制限を超えてしまい、誰も distributeRewards() を実行できなくなってしまいます。お金がコントラクトの中に閉じ込められたまま、二度と取り出せなくなる大事故につながるのです。

—

⭕ 安全なパターン:「プッシュ」ではなく「プル(引き出し)」モデルを使う

ループ処理で全員に一斉配分するのではなく、「各自で取りに来てもらう(Pull型)」という設計に変えるのが、セキュリティの鉄則です。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract SafePullExample {
    // 各ユーザーの未受取の報酬を管理する台帳
    mapping(address => uint256) public rewards;

    // 報酬を付与する関数(これならループしません)
    function setReward(address _participant, uint256 _amount) public {
        rewards[_participant] += _amount;
    }

    // ユーザー自身が自分のタイミングで報酬を引き出す関数
    function withdrawReward() public {
        uint256 amount = rewards[msg.sender];
        require(amount > 0, "引き出す報酬がありません");

        // 再入可能性攻撃を防ぐため、送金前に状態をゼロにする
        rewards[msg.sender] = 0;

        // 安全に送金
        payable(msg.sender).transfer(amount);
    }
}

このように、一括処理のループを避けて「ユーザー個別に処理を分散させる」設計にすることで、ガス枯渇のリスクを綺麗に取り除くことができます。これがプロの現場で使われる「ガス最適化とDoS耐性」の基本アプローチです。

—

4. 実務で役立つ!設計時のチェックリスト

日々の開発やインフラ構築、スマートコントラクトのレビューを行う際には、以下のポイントをぜひ意識してみてください。

  • ループの回数は予測可能か?
  • 配列の長さがユーザーの数や外部からの入力によって無限に増える仕組みになっていないか確認する。
  • 「Push(押し付け)」より「Pull(引き出し)」
  • まとめて処理するのではなく、必要に応じて個別に処理させる設計デザインパターンを採用する。
  • 外部呼び出し(External Calls)の失敗を考慮しているか?
  • 他のコントラクトを呼び出す際、相手が意図的にエラーを返したり無限ループを仕掛けたりしても、自分のシステム全体が巻き添えにならないようなエラーハンドリング(try/catch の活用など)を入れる。

—

まとめ

今回は、スマートコントラクトのガス最適化とDoS攻撃への耐性について、郵便ポストの例えを交えてお話ししました。

セキュリティ対策というと難しく聞こえますが、要するに「誰か一人のわがままや、データの膨張によって、みんなの大切なシステムが止まってしまわないように優しく設計すること」です。

一歩ずつ、こうしたデザインパターンを身につけていけば、あなたも頼れるセキュアな開発者に一歩近づけますよ。一緒に安全で素晴らしいWeb3の世界を作っていきましょう!

コメント

タイトルとURLをコピーしました