【入門編】 ガス制限攻撃(DoS)の防止:ループ処理の制限とページネーション – IoT・OT(制御システム) & ブロックチェーンセキュリティ防御ガイド

はじめに、ブロックチェーンやスマートコントラクトの世界へようこそ!

「スマートコントラクト」と聞くと、なんだか難しそうなイメージがありますよね。でも、基本は「あらかじめ決めたルール通りに、自動で実行してくれる契約の仕組み」です。

しかし、この便利な仕組みも、書き方を一歩間違えると「泥棒(攻撃者)」に悪用されて、システム全体がピタッと止まってしまうことがあります。それが、今回ご紹介する「ガス制限攻撃(DoS)」というものです。

今回は、この攻撃の仕組みと、それを防ぐための「ページネーション(分割処理)」という優しい防犯対策について、身近な例えを交えながら一歩ずつ学んでいきましょう!

—

1. 「ガス(Gas)」と「ガス制限攻撃」ってなに?

スマートコントラクトを動かすには、「ガス(Gas)」と呼ばれる手数料(燃料)が必要になります。

これを私たちの日常生活で例えるなら、「タクシーの運賃」や「お家に入るための鍵を開けるエネルギー」のようなものです。

タクシーのメーターに例えてみよう

あなたがタクシーに乗って目的地に向かうとします。お財布の中には「1万円(ガスの最大制限)」しか入っていません。
もし、目的地に着く前にメーターが1万円を超えてしまったら、どうなるでしょうか?
タクシーはそこで強制ストップして、目的地にはたどり着けませんよね。

スマートコントラクトもまったく同じです。
プログラムを動かす(処理を実行する)たびに、イーサリアムなどのネットワーク上で「ガス」が消費されます。そして、1回に使えるガスの量には上限(ガスリミット)が決まっているのです。

「ガス制限攻撃(DoS)」のメカニズム

泥棒(攻撃者)はこの仕組みを狙ってきます。

例えば、全員の住所が書かれた「名簿(配列)」を管理するプログラムがあるとします。
新しく人が増えるたびに、名簿の後ろに名前を追加していきます。

【名簿リスト】
[1] アリス
[2] ボブ
[3] チャーリー
...
[100000] 攻撃者が仕込んだ大量のダミーデータ

もし、プログラムの中に「名簿の最初から最後まで全員分を1回でチェックする」という処理(ループ処理)があったらどうなるでしょう?

データが数人分なら問題ありません。しかし、悪意ある人がダミーデータを何万件も登録して名簿をめちゃくちゃに肥大化させたら……。
プログラムが全員分をチェックしようとした瞬間、タクシーのメーター(ガス)が途中で上限に達してしまい、エラー(ガス切れ)で強制終了してしまいます。

結果として、「誰も名簿を読み出せなくなる(システム停止=DoS状態)」という最悪の事態になってしまうのです。これが「ガス制限攻撃」の正体です。

—

2. 防犯対策のアイデア:「ページネーション」で小分けにしよう!

この攻撃を防ぐための最も効果的な方法が、「ページネーション(Pagination)」です。

これは、本をめくるように「データをいくつかのページに分割して、少しずつ読み込む」というテクニックになります。

身の回りの防犯に例えると?

あなたが100個の重い荷物(データ)を家の中に運び入れたいとします。
一気に100個すべてを抱えて玄関に入ろうとしたら、重すぎてドアに挟まって動けなくなってしまいますよね(これがガス切れです)。

そこで、「1回に運ぶのは5個までにして、それを20回に分けて運ぶ」というルールを決めます。これなら、体力を使い果たすことなく、安全にすべての荷物を運び入れることができます。

スマートコントラクトでもこれと同じことを行います。
一度に全てのデータを読み込むのではなく、「今回は〇番目から〇個だけ読み込む」という指定ができるようにプログラムを作るのです。

—

3. 【実践】安全なスマートコントラクトを書いてみよう!

それでは、実際にSolidity(スマートコントラクトを開発する言語)を使って、安全なコードの書き方を見ていきましょう。

まずは、危険なコードと、対策済みの安全なコードを比較してみます。

❌ 危険なコード(ガス切れを起こす例)

以下のコードは、登録されたユーザー全員を一度に取得しようとするため、ユーザー数が増えると動かなくなってしまいます。

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

contract UnsafeContract {
    // ユーザーのアドレスを格納する配列
    address[] public users;

    // ユーザーを追加する関数
    function register() public {
        users.push(msg.sender);
    }

    // ❌ 危険:全員分の配列を一度に返すため、人数が増えるとガス制限でエラーになります
    function getAllUsers() public view returns (address[] memory) {
        return users;
    }
}

⭕ 安全なコード(ページネーションを導入した例)

以下が、データを小分けにして取得できるように対策した安全なコードです。
実務でもそのまま使えるパターンですので、コメントを参考にしながら読んでみてくださいね。

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

contract SafeContract {
    // ユーザーのアドレスを格納する配列
    address[] public users;

    // ユーザーを追加する関数
    function register() public {
        users.push(msg.sender);
    }

    // 登録されている総人数を返す関数(フロントエンドでページ数を計算するのに使います)
    function getUserCount() public view returns (uint256) {
        return users.length;
    }

    // ⭕ 安全:開始位置(offset)と取得件数(limit)を指定して、小分けに取得します
    function getUsersPaginated(uint256 offset, uint256 limit) 
        public 
        view 
        returns (address[] memory) 
    {
        uint256 totalUsers = users.length;
        
        // 開始位置がデータの総数を超えている場合は、空の配列を返します
        if (offset >= totalUsers) {
            return new address[](0);
        }

        // 実際に取得する件数を計算します(最後のページでデータが足りない場合の調整)
        uint256 resultLength = limit;
        if (offset + limit > totalUsers) {
            resultLength = totalUsers - offset;
        }

        // 指定された件数分のメモリ領域を確保します
        address[] memory pageResult = new address[](resultLength);

        // ループ処理を限定された回数(limit分)だけ実行します
        for (uint256 i = 0; i < resultLength; i++) {
            pageResult[i] = users[offset + i];
        }

        return pageResult;
    }
}

このコードの「防犯ポイント」

1. ループの回数が決まっている: for ループが回る回数は、ユーザーが指定した limit(例えば「10回だけ」など)に制限されています。そのため、配列に何百万人のデータがあっても、ガス代が跳ね上がることはありません。
2. データの「つまみ食い」ができる: Webサイトの「1ページ目」「2ページ目」を表示するように、必要な部分だけをスマートに取得できます。

—

4. 開発時に気をつけるべき「もう一つの防犯ルール」

ページネーションの実装に加えて、日頃の開発で意識しておきたい大切なルールがあります。

それは、「スマートコントラクトの内部(状態を変更する処理)で、引き戻せない無限ループを作らないこと」です。

今回は読み込み(view 関数)の例を紹介しましたが、もし「全員にお金を配る」といった、データを書き換える処理(トランザクション)の中で同じような全体ループを書いてしまうと、システムが完全にロックされ、資金が取り出せなくなることもあります。

このような場合は、「コントラクト側から全員に一斉に送る(Push型)」のではなく、「ユーザー自身が自分の取り分をそれぞれ引き出しに来る(Pull型)」という設計にするのが、スマートコントラクトの世界での鉄則(ベストプラクティス)とされています。

—

まとめ:一歩ずつ安全なコントラクトを作っていきましょう!

今回は、スマートコントラクトの「ガス制限攻撃(DoS)」と、その対策である「ページネーション」について解説しました。

最後に、今回のポイントをおさらいしておきましょう。

  • ガスはスマートコントラクトの燃料! 上限を超えると処理が強制ストップします。
  • 配列の全件ループは危険! 泥棒にデータを引き伸ばされると、システムが止まってしまいます。
  • ページネーション(分割処理)で解決! データを小分けにして読み込むことで、安全にシステムを運用できます。

セキュリティ対策と聞くと身構えてしまいますが、こうして「お家の防犯」に例えてみると、やっていることはとてもシンプルで合理的ですよね。

焦らず、一つずつ安全なコードの書き方を学んで、頑丈なシステムを作っていきましょう!

コメント

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