はじめに、ブロックチェーンやスマートコントラクトの世界へようこそ!
「スマートコントラクト」と聞くと、なんだか難しそうなイメージがありますよね。でも、基本は「あらかじめ決めたルール通りに、自動で実行してくれる契約の仕組み」です。
しかし、この便利な仕組みも、書き方を一歩間違えると「泥棒(攻撃者)」に悪用されて、システム全体がピタッと止まってしまうことがあります。それが、今回ご紹介する「ガス制限攻撃(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)」と、その対策である「ページネーション」について解説しました。
最後に、今回のポイントをおさらいしておきましょう。
- ガスはスマートコントラクトの燃料! 上限を超えると処理が強制ストップします。
- 配列の全件ループは危険! 泥棒にデータを引き伸ばされると、システムが止まってしまいます。
- ページネーション(分割処理)で解決! データを小分けにして読み込むことで、安全にシステムを運用できます。
セキュリティ対策と聞くと身構えてしまいますが、こうして「お家の防犯」に例えてみると、やっていることはとてもシンプルで合理的ですよね。
焦らず、一つずつ安全なコードの書き方を学んで、頑丈なシステムを作っていきましょう!
コメント