はじめに

初めまして。 CryptoGamesというブロックチェーンゲーム企業でエンジニアをしている cardene(かるでね) です! スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。

代表的なゲームはクリプトスペルズというブロックチェーンゲームです。

今回は、名前空間付きストレージパターンにおける構造体の格納場所の仕組みを提案している規格であるERC7201についてまとめていきます!

以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。

他にも様々なERCについてまとめています。

概要

「@custom:storage-location」というアノテーションは、SolidityやVyperで書かれたスマートコントラクトの中でデータがどこに保存されるかを明確に記録するためのものです。 コントラクトは、特定のルールやロジックに従って動くプログラムですが、その中でデータをどこにどう保存するかは非常に重要な部分です。 このアノテーションを使うことで、開発者はデータの保存場所を一目で把握し、管理しやすくなります。

さらに、データの位置を決めるための特別な計算式も用意されています。 この式は、コントラクト内のデータが予期せぬ場所に保存されてしまうことを防ぎます。 これは、ERC20やERC721のようなトークンコントラクトや、複雑なロールややり取りを管理するシステムにとって特に重要です。

たとえば、コントラクトにmintという関数があったとします。 この関数は新しいトークンを作るときに使いますが、@custom:storage-locationを使ってmint関数がどこにデータを保存するかを明確に記録しておくことで、他の開発者がそのコードを見たときに迷わずに済みます。 また、誤ってデータを上書きするリスクも減ります。

つまり、このアノテーションと計算式は、コントラクトの動作をより透明にし、データ管理を効率化して安全性を高めるために役立ちます。 開発者が互いのコードを理解しやすくなるので、チームでの開発がスムーズになり、より信頼性の高いコントラクトが作れるようになります。

ストレージへのデータ保存方法については、以下の記事を参考にしてください。

動機

SolidityやVyperで作られるスマートコントラクトは、データを保存するための「棚(ストレージ)」のようなものを持っています。 この棚(ストレージ) は木構造になっており、最初のスロットから始まって、各変数やデータは順番に配置されます。 しかし、マッピングや配列のような特定のデータは、他のデータと場所が重ならないようにハッシュを使って別の場所に保存されます。 これは多くのコントラクトにとって十分ですが、いくつかの特殊な設計では問題が生じます。

例えば、「モジュラーデザイン」という方法では、DELEGATECALLを使って、1つのコントラクトが他の複数のコントラクトのコードを実行します。 これらのコントラクトは同じ「棚(ストレージ)」を共有しているため、どこに何を置くかを非常に慎重に決める必要があります。 また、アップグレード可能なコントラクトでは、新しいデータを追加する時に既存のデータの場所を変えてしまうかもしれないため、アップグレードが難しいことがあります。

このような特殊な場合では、デフォルトの「棚(ストレージ)」の配置ではなく、ハッシュを使ってデータの場所を擬似ランダムに決める方が良い場合があります。 これにより、各データは全く異なる場所に置かれるかもしれませんが、関連するデータは近くに置かれることが多く、整理されます。 これらの新しい場所は、元の木構造と同じルールに従う新しい「棚(ストレージ)」となりますが、元の「棚(ストレージ)」とは完全に独立していて衝突しません。

ただし、このような配置はSolidityやVyperのコンパイラには見えません。 なので、コントラクトのデータを解析するツールが正確に動作するためには、これらの特別な配置を標準化する必要があります。 そうすれば、これらのデザインパターンを使っているコントラクトでも、ツールは正しくデータの場所を把握できるようになります。

仕様

予備知識

スマートコントラクトでデータを整頓し、整理するために「名前空間」というシステムを使います。 名前空間とは、変数の整理された集まりで、中にはサイズが変わる配列やマッピングも含まれることがあります。 これらの変数は一定の規則に従って配置されますが、必ずしも最初の場所から始まるとは限りません。 このような名前空間を用いることで、コントラクトのストレージが「名前空間ストレージ」として整理されます。

名前空間IDは、コントラクト内で特定の名前空間を指し示すための一意の文字列です。 このIDには空白文字を含めないようにします。

コントラクトに名前空間を実装する際は、構造体(struct)タイプを使い、「@custom:storage-location <FORMULA_ID>:<NAMESPACE_ID>」という形のNatSpecタグを注釈として付けます。 ここで、<FORMULA_ID>は名前空間の根本となるストレージの位置を計算する式を指し、<NAMESPACE_ID>はその名前空間のIDを指します。 この仕組みを使うことで、Solidityコンパイラのバージョンv0.8.20以降では、名前空間がコード内でどのように扱われるかがより明確になります。 ただし、コントラクトの外部にあるこのタグ付きの構造体は、どのコントラクトにとっても名前空間とはみなされません。

このようにして、コントラクト開発者はデータをもっと自由に、そして整然と配置できるようになります。 これは特に、多くの部分が連携して動く大規模なコントラクトや、時間と共にアップデートされるコントラクトの管理にとって大きな利点をもたらします。

NatSpec(Natural Specification)

Solidity言語で使用されるドキュメンテーションツールです。 これは、スマートコントラクトのコード内に直接コメントとして書かれるもので、コントラクトの振る舞いや関数、変数などについての詳細な説明を提供します。 NatSpecタグは特定の形式に従っており、コントラクトのユーザーや他の開発者にとって理解しやすいドキュメントを生成するのに役立ちます。

NatSpecコメントは通常、「///」で始まる一行コメント、または「/** ... */」で囲まれた複数行コメントです。 これらのコメントには以下のようなタグが含まれることがあります。

  • @title
    • コントラクトの簡潔な説明。
  • @author
    • コントラクトの作者の名前。
  • @notice
    • コントラクトや関数を使うユーザーに示す説明。
    • 例えば、関数が何をするか、どんな効果があるかなど。
  • @dev
    • 開発者向けの詳細な説明。内部の動作や注意点などを記述します。
  • @param
    • 関数の各パラメータに対する説明。
  • @return
    • 関数の返り値に関する説明。

NatSpecコメントは、コントラクトのソースコード内に直接組み込まれ、開発者がコードを読む際の理解を助けるだけでなく、自動化されたツールを使用してドキュメントを生成する際の基礎としても機能します。 ユーザーインターフェースにこれらの説明を表示することで、エンドユーザーがコントラクトの機能をよりよく理解し、信頼を持って使うことができるようになります。

Formula

スマートコントラクトで特定のデータの場所を決めるために、「erc7201」という特別な計算方法があります。

text
erc7201(id: string) = keccak256(keccak256(id) - 1) & ~0xff

これは、名前空間のIDという一意の文字列を使って、データがどこに保存されるかを計算する式です。 具体的には、このIDを特定の方法(keccak256というハッシュ関数を使った計算)で変換し、最終的な場所を求めます。 Solidityでは、この計算は少し複雑なコードになりますが、結局のところデータを保存する場所を特定するための数値を生成します。

コントラクトでこの計算方法を使う時は、@custom:storage-location erc7201:<NAMESPACE_ID>という形で注釈を付けます。 例えば、@custom:storage-location erc7201:foobarという注釈は、「foobar」というIDを持つ名前空間のデータが、erc7201の計算方法に従ってどこに保存されるかを示しています。

このような計算方法は将来も新しく作られるかもしれません。 それぞれにユニークな識別子(例:erc1234)が付けられ、コントラクト開発者はそれを使ってデータをより効率的に管理できるようになります。 このシステムを使うことで、データがどこにあるかを明確にし、コントラクトがより安全かつ効率的に動作するようにすることが目指されています。

補足

SolidityやVyperで作られるコントラクトは、データを保存するために特定のルールに従った「棚(ストレージ)」のような場所を持っています。 この「棚(ストレージ)」は木構造の形をしており、通常はスロット0から始まります。 しかし、複数のデータセットや機能を持つコントラクトでは、この標準の場所以外にもデータを保存する必要があることがあります。 ここで「名前空間」という概念が登場します。 これは、標準の棚(ストレージ)とは別の場所にデータを整理して保存する方法です。 名前空間を使うことで、複数のデータセットが互いに干渉することなく、同じコントラクト内で共存できます。

名前空間を作る時に重要なのは、その根となる場所が標準の棚(ストレージ)や他の名前空間と重ならないことです。 これにより、意図しないデータの上書きや混乱を防ぎます。 名前空間の位置を決めるためにkeccak256(id) - 1という計算式を使いますが、名前空間が複数のスロットにまたがる可能性があるため、この計算だけでは不十分です。 そのため、さらに別のハッシュ計算を加えて、名前空間が完全に独立し、標準の棚とは完全に異なる場所になるようにしています。

Lroot:=root∣Lroot+n∣keccak256(Lroot)∣keccak256(H(k)⊕Lroot)∣keccak256(Lroot⊕H(k))L_{root} := \mathit{root} \mid L_{root} + n \mid \texttt{keccak256}(L_{root}) \mid \texttt{keccak256}(H(k) \oplus L_{root}) \mid \texttt{keccak256}(L_{root} \oplus H(k))

さらに、ガスの使用量を最適化するため、名前空間の位置は256の倍数に合わせて配置されます。 これは将来、Verkle状態ツリーへの移行によってガスの計算方法が変わるかもしれないための予防策です。 この変更により、256個のスロットが一度に使用されるようになる場合があり、その場合に備えています。

このようにして、コントラクト開発者はデータをより安全かつ効率的に管理できるようになります。

計算式は、スマートコントラクトで使用されるデータの保存位置(ストレージロケーション)を決定するためのものです。 ここでの

LrootL_{root}

は、データが保存される基点(根)の位置を表しています。 この式は複数の部分から成り立っており、それぞれが特定の種類のデータや状況に応じてストレージロケーションを計算します。

root{root}

これは基本的な根の位置を示します。 通常は0から始まりますが、名前空間などを使用する場合は異なる値を持つことがあります。

Lroot+nL_{root} + n

これは基点からの相対位置を示します。

nn

は任意の整数で、

LrootL_{root}

からのオフセットを表します。

keccak256(Lroot){keccak256}(L_{root})

これは

(Lroot)(L_{root})

をハッシュ関数(ここではkeccak256)に通して計算される新しい位置です。 ハッシュ関数はデータをランダムに見える一意の値に変換します。

keccak256(H(k)⊕Lroot){keccak256}(H(k) \oplus L_{root})

ここでの

(H(k))(H(k))

は、キー

(k)(k)

をハッシュ化したものです。 そして、

⊕\oplus

はビット単位のXOR(排他的論理和)を表し、2つの値を組み合わせて新しい値を生成します。 この計算により、さらに複雑な位置が求められます。

keccak256(Lroot⊕H(k)){keccak256}(L_{root} \oplus H(k))

この部分も上記と似ており、XORを使って根の位置とキーのハッシュを組み合わせた後、その結果をハッシュ関数に通しています。

全体として、この式は様々な方法でデータの保存位置を計算し、特に衝突を避けるために複数の計算手法を組み合わせています。 これにより、スマートコントラクト内でデータが安全に、かつ効率的に保存されることを目指しています。

ネーミング

このパターンはかつて「ダイヤモンドストレージ」と呼ばれていましたが、それが「ダイヤモンドプロキシパターン」と混同されることが多かったため、区別を明確にする新しい名称が用いられることになりました。 ダイヤモンドプロキシパターンは、コントラクトのアップグレードや機能拡張に関連するもので、ダイヤモンドストレージはデータの管理方法に特化していますが、これらは完全に独立した概念です。 この混乱を避けるため、イーサリアム改善提案(EIP)では、コントラクトのデータ管理アプローチを指すために、ダイヤモンドストレージという用語を避け、新しい用語を採用することにしました。

互換性

互換性の問題は見つかっていません。

参考実装

javascript
pragma solidity ^0.8.20;

contract Example {
    /// @custom:storage-location erc7201:example.main
    struct MainStorage {
        uint256 x;
        uint256 y;
    }

    // keccak256(abi.encode(uint256(keccak256("example.main")) - 1)) & ~bytes32(uint256(0xff));
    bytes32 private constant MAIN_STORAGE_LOCATION =
        0x183a6125c38840424c4a85fa12bab2ab606c4b6d0e7cc73c0c06ba5300eab500;

    function _getMainStorage() private pure returns (MainStorage storage $) {
        assembly {
            $.slot := MAIN_STORAGE_LOCATION
        }
    }

    function _getXTimesY() internal view returns (uint256) {
        MainStorage storage $ = _getMainStorage();
        return $.x * $.y;
    }
}

MainStorage

javascript
struct MainStorage {
    uint256 x;
    uint256 y;
}

概要

二つのuint256型の変数xとyを持つ構造体。

詳細

この構造体は、コントラクト内で使用されるメインのデータストレージを表します。 xとyは、計算やデータ保管など、さまざまな目的で使用される数値です。 MainStorage構造体は、メモリ上でのデータ管理をより効率的かつ組織的に行うために使用されます。

パラメータ

  • uint256 x - 数値データを保持するための変数。
  • uint256 y - 数値データを保持するための別の変数。

MAIN_STORAGE_LOCATION

javascript
bytes32 private constant MAIN_STORAGE_LOCATION =
    0x183a6125c38840424c4a85fa12bab2ab606c4b6d0e7cc73c0c06ba5300eab500;

概要

メインストレージの位置を指定するための定数。

詳細

この定数は、MainStorage構造体がブロックチェーン上のどの位置に保存されるかを定義します。 これは、コントラクトのデータを一意かつ安全に配置するための固定されたアドレスです。 bytes32型で、特定のハッシュ値が割り当てられています。


_getMainStorage

javascript
function _getMainStorage() private pure returns (MainStorage storage $) {
    assembly {
        $.slot := MAIN_STORAGE_LOCATION
    }
}

概要

メインストレージの位置を取得する関数。

詳細

この関数は、Solidityのインラインアセンブリを使用して、MainStorage構造体のインスタンスへの参照を取得します。 これはMAIN_STORAGE_LOCATIONに定義された特定のストレージスロットを指します。 この位置は、コントラクトのメインデータ領域を表しており、xとyという2つのuint256型の変数を含んでいます。

戻り値

  • MainStorage storage $ - メインストレージのMainStorage型への参照を返します。

_getXTimesY

javascript
function _getXTimesY() internal view returns (uint256) {
    MainStorage storage $ = _getMainStorage();
    return $.x * $.y;
}

概要

メインストレージのxとyの積を計算して返す関数。

詳細

この関数は最初に_getMainStorageを呼び出して、メインストレージの位置を特定します。 次に、このストレージ位置からxとyを取得し、その二つの値の積を計算して返します。 これはコントラクトの状態を読み取る内部ビュー関数で、ブロックチェーンの状態を変更しません。

戻り値

  • uint256 - メインストレージのxとyの積を表すuint256型の値を返します。

セキュリティ

コントラクトのデータ管理において、名前空間は他の名前空間やSolidityおよびVyperの標準ストレージと干渉しないように設計されています。 特定のERC(イーサリアム改善提案)で提案された計算式は、keccak256ハッシュ関数の衝突耐性を前提として、どんな名前空間IDにもこの衝突回避の特性を保証します。 これにより、様々な名前空間を安全に並行して使用できます。

@custom:storage-locationは、開発者がコントラクト内でどのように名前空間を使っているかを示すためのNatSpecアノテーションです。 現在のコンパイラはこの注釈に特に意味を与えたりルールを適用したりはしませんが、開発者はこのアノテーションを使って、自分のコントラクトの中でどのように名前空間が配置されているかを文書化します。 実際にこれが機能するかどうかは、開発者がパターンを正しく実装しているかにかかっています。

コントラクト開発者は、自分のコントラクトが複数のデータセットを扱う際にそれらが衝突しないように、名前空間を使ってデータの場所を特定し整理します。 @custom:storage-locationアノテーションを文書として使いながら、実際の動作を正しく実装することが重要です。

引用

Francisco Giordano (@frangio), Hadrien Croubois (@Amxx), Ernesto García (@ernestognw), Eric Lau (@ericglau), "ERC-7201: Namespaced Storage Layout," Ethereum Improvement Proposals, no. 7201, June 2023. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7201.

最後に

今回は「名前空間付きストレージパターンにおける構造体の格納場所の仕組みを提案している規格であるERC7201」についてまとめてきました! いかがだったでしょうか?

質問などがある方は以下のTwitterのDMなどからお気軽に質問してください!

他の媒体でも情報発信しているのでぜひ他も見ていってください!