# [ERC7813] テーブルベースで構造化されたオンチェーンデータの仕組みを理解しよう！

> ERC7813は、ブロックチェーン上のデータを柔軟に保存・管理するための「オンチェーンストレージパターン」を提案しています。

- URL: https://cardene.dev/articles/6119b5c4579e0d76b09a
- 公開日: 2025-11-07
- 更新日: 2025-11-07
- トピック: Blockchain
- タグ: ethereum, solidity, smartcontract, ブロックチェーン

## はじめに

『DApps開発入門』という本や色々記事を書いているかるでねです。

https://amzn.asia/d/gxvJ0Pw

今回は、ブロックチェーン上のデータをデータベースのように整理・管理できる仕組みを定義し、効率的で拡張性の高いオンチェーンデータ管理を実現する仕組みを提案している**ERC7813**についてまとめていきます！

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

https://eips.ethereum.org/EIPS/eip-7813

他にも様々なEIP・BIP・SLIP・CAIP・ENSIP・RFC・ACPについてまとめています。

https://cardene.notion.site/ERC-EIP-2a03fa3ea33d43baa9ed82288f98d4a9?pvs=4

## 概要

**ERC7813**は、ブロックチェーン上のデータを柔軟に保存・管理するための「**オンチェーンストレージパターン**」を提案しています。
データを「**テーブル**」と呼ばれる構造体で整理し、各テーブルは「**キー（key）**」と「**値（value）**」のスキーマ（構造の定義）を持つ「**レコード**」の集合として構成されます。
これは、一般的なデータベースにおけるテーブルの仕組みと似ています。

この仕組みでは、以下のような要素が定義されています。

- **統一されたコントラクトインターフェース**
    データへのアクセスを行うために、統一的なスマートコントラクトのインターフェースが提供されます。
    これにより、異なる開発者やサービスが同じ形式でデータを操作できるようになります。

- **効率的なバイナリエンコーディング形式**
    静的データ型（サイズが固定のデータ）と動的データ型（サイズが可変のデータ）の両方を対象に、コンパクトな形式でデータをエンコード（符号化）します。
    これにより、保存やイベント発火にかかるガスコスト（取引手数料）を抑えることができます。

- **標準化されたイベントによる状態管理**
    スマートコントラクトの状態が変更されると、標準化された形式のイベントが発行されます。
    このイベントを通じて、オフチェーン（ブロックチェーンの外）で動作するインデクサー（データの追跡・整理を行う仕組み）が自動的に状態を同期し、スキーマに基づいたデータの複製を行うことができます。

- **動的なテーブル登録機能**
    新しいテーブルを、実行時に動的に登録することが可能です。
    これは「**スキーマメタデータ**」を保存する特別なテーブルを通じて行われ、既存のコントラクトや統合システムを壊すことなく、システム全体を拡張できます。

この仕組みにより、ブロックチェーン上のデータ構造をより柔軟に管理し、開発者が安全かつ効率的にデータの拡張や更新を行うことができるようになります。

## 動機

これまでのスマートコントラクト開発では、オンチェーンのデータ管理に統一された基準が存在しませんでした。
その結果、以下のような課題が発生していました。

* データの構造が固定的で、柔軟な拡張や変更が難しい
* コントラクトのロジックとオフチェーンサービスが密結合しており、更新時に互換性が崩れる
* データ構造を変更すると、既存のシステムや連携部分に不具合が起きやすい

**ERC7813**が導入するストレージパターンは、こうした問題を解決するために設計されています。

#### メリット

| 項目  | 説明   |
| ------------ | ----- |
| 自動インデックス化    | ステート変更時に発行される標準イベントを通じて、オフチェーンのサービスが自動的にデータを追跡・同期できます。これにより、スキーマ情報に基づいたAPIが容易に構築できます。                 |
| カスタム読み取り関数の削除 | 特定のデータ取得関数をコントラクトごとに実装する必要がなくなり、統一されたインターフェースを通してデータを参照できます。これにより、コントラクトと外部サービス間の結合度を下げ、開発効率が向上します。 |
| 簡単なアップグレード   | 「**非構造化ストレージ（unstructured storage）**」を採用することで、ストレージレイアウトを固定せずにロジックのみを更新可能です。従来の固定構造に伴うリスクを避けられます。       |
| 柔軟なデータ拡張     | 新しいテーブルを実行時に追加登録できるため、既存のシステムやデータ利用者との互換性を保ちながら機能を拡張できます。  |
| ガスコストの削減     | 効率的なデータパッキング（データの圧縮）により、ストレージ操作やイベント発火時のコストを軽減できます。     |

## 仕様

#### 定義

#### `Store`

```javascript
// Store: A smart contract that implements the interface proposed by this ERC
// and organizes data in Tables. It emits events for each data operation.
```

この「`Store`」は、**ERC**で定義されたインターフェースを実装するスマートコントラクトです。
データを「**テーブル**」単位で整理し、各データ操作ごとにイベントを発行します。
Storeはデータの読み書き・削除といった操作を一元的に扱う中心的な存在です。
オンチェーン上に存在し、操作に応じてイベントを発行することで、オフチェーンのシステム（インデクサーなど）がそのステートを追跡・同期できるようになります。

#### `Table`

```javascript
// Table: A storage structure that holds Records sharing the same Schema.
```

「`Table`」は、同一のスキーマ（データ構造）を持つ複数のレコードを格納するためのストレージ構造です。
テーブルには「**オンチェーンテーブル**」と「**オフチェーンテーブル**」の2種類があります。
オンチェーンテーブルはブロックチェーン上にステートを保持し、イベントも発行します。
オフチェーンテーブルはデータ自体はオンチェーンに保存せず、イベントのみを発行します。

| 種類         | 説明                                     |
| ---------- | -------------------------------------- |
| オンチェーンテーブル | 状態をブロックチェーン上に保存し、インデクサーのためにイベントを発行します。 |
| オフチェーンテーブル | 状態をオンチェーンに保持せず、イベントのみを発行します。 |

#### `Record`

```javascript
// Record: A piece of data stored in a Table, addressed by one or more keys.
```

「`Record`」は、テーブルに格納される1つのデータ単位です。
レコードは1つ以上のキーによって一意に特定されます。
各レコードは、キー（識別子）と値（データ本体）から構成されます。

#### `ResourceId`

```javascript
type ResourceId is bytes32;
```

「`ResourceId`」は各テーブルを一意に識別するための32バイト値です。
この値は最初の2バイトがテーブルの種類を示し、残りの30バイトがユニークな識別子として使われます。
「`0x7462（"tb"）`」はオンチェーンテーブル、「`0x6f74（"ot"）`」はオフチェーンテーブルを意味します。

| バイト範囲 | 内容            |
| ----- | ------------- |
| `0-1`   | テーブルの種類を示す識別子 |
| `2-31`  | テーブル固有のユニークID |

#### `Schema`

```javascript
type Schema is bytes32;
```

「`Schema`」はテーブル内のレコード構造（キーと値の型）を定義するデータ型です。
テーブルには「**キーのスキーマ**」と「**値のスキーマ**」の2種類があります。
キーのスキーマは固定長のデータ型のみで構成され、値のスキーマは固定長・可変長の両方を含むことができます。

| バイト範囲 | 内容               | 制約                 |
| ----- | ---------------- | ------------------ |
| `0-1`   | 静的フィールドの総バイト長    | -                  |
| `2`     | 静的フィールド数         | (28 - 動的フィールド数) 以下 |
| `3`     | 動的フィールド数         | キースキーマは0、値スキーマは最大5 |
| `4-31`  | 各フィールドの型を1バイトで指定 | 動的型は静的型の後に並ぶ必要あり   |

#### `SchemaType`

```javascript
enum SchemaType {
  // uint系: 0x00〜0x1F
  // int系: 0x20〜0x3F
  // bytes系: 0x40〜0x5F
  // bool: 0x60
  // address: 0x61
  // 可変長配列やbytes/string型も含む
}
```

「`SchemaType`」は、スキーマで定義される各フィールドの型を1バイトで表現する値です。
データ型を小さな整数値にマッピングすることで、効率的なバイナリ表現を可能にします。

| 値の範囲      | データ型              |
| --------- | ----------------- |
| `0x00〜0x1F` | uint8〜uint256     |
| `0x20〜0x3F` | int8〜int256       |
| `0x40〜0x5F` | bytes1〜bytes32    |
| `0x60`      | bool              |
| `0x61`      | address           |
| `0x62〜0x81` | uint8[]〜uint256[] |
| `0xC4`      | bytes             |
| `0xC5`      | string            |

#### `FieldLayout`

```javascript
type FieldLayout is bytes32;
```

「`FieldLayout`」は、各静的フィールドのバイト長など、スキーマの具体的な構成情報をエンコードします。
オンチェーン上での処理を最適化するために、各フィールドの長さをあらかじめ計算し保持します。

| バイト範囲 | 内容           | 制約                 |
| ----- | ------------ | ------------------ |
| `0-1`   | 静的フィールド全体の長さ | -                  |
| `2`     | 静的フィールド数     | (28 - 動的フィールド数) 以下 |
| `3`     | 動的フィールド数     | キースキーマは0、値スキーマは最大5 |
| `4-31`  | 各静的フィールドの長さ  | -                  |

#### `EncodedLengths`

```javascript
type EncodedLengths is bytes32;
```

「`EncodedLengths`」は、各動的フィールドの長さをまとめて符号化した値です。
動的データのデコードに必要な情報を保持します。
これにより、各動的フィールドの長さを個別に保存する必要がなくなり、効率が向上します。

| バイト範囲     | 型      | 内容             |
| --------- | ------ | -------------- |
| `0x00〜0x06` | uint56 | 動的データ全体の長さ     |
| `0x07〜0x0B` | uint40 | 1つ目の動的フィールドの長さ |
| `0x0C〜0x10` | uint40 | 2つ目の動的フィールドの長さ |
| ...       | ...    | 最大5つまで対応       |

### データエンコーディング

レコードデータは、**Store**メソッドやイベントで返却・発行される時に以下のルールで符号化されます。

* 静的フィールドはパディング（余分なゼロ埋め）を行わずに連結されます。
* 動的フィールドは長さを含めず、すべてを1つの`EncodedLengths`にまとめます。
* 最大フィールド数は28個で、そのうち動的フィールドは最大5個です。

#### エンコーディング例

```javascript
(uint256 id, address owner, string description, uint8[] scores)
```

擬似コード例は以下になります。

```javascript
bytes memory staticData = abi.encodePacked(id, owner);
bytes memory packedScores = packElementsWithoutPadding(scores);
bytes memory dynamicData = abi.encodePacked(description, packedScores);

EncodedLengths encodedLengths = dynamicData.length;
encodedLengths |= (description.length << (56));
encodedLengths |= (encodedData.length << (56 + 8 * 5));

// Full record
(staticData, encodedLengths, dynamicData)
```

### `Store` インターフェース

```javascript
interface IStore {
  function getRecord(ResourceId tableId, bytes32[] calldata keyTuple)
    external view returns (bytes memory staticData, EncodedLengths encodedLengths, bytes memory dynamicData);

  function getField(ResourceId tableId, bytes32[] calldata keyTuple, uint8 fieldIndex)
    external view returns (bytes memory data);

  function getFieldLength(ResourceId tableId, bytes32[] memory keyTuple, uint8 fieldIndex)
    external view returns (uint256);
}
```

#### `getRecord`

指定されたテーブルIDとキーに対応するレコード全体を取得する関数。
静的データ・動的データ・動的データの長さ情報をすべて返します。

**引数**

* `tableId`
  * 対象テーブルの`ResourceId`。
* `keyTuple`
  * レコードを特定するキー配列。

**戻り値**

* `staticData`
  * 静的データのバイナリ。
* `encodedLengths`
  * 動的データの長さ情報。
* `dynamicData`
  * 動的データ本体。

#### `getField`

指定されたフィールドのデータを単独で取得する関数。
個別のフィールドデータをエンコード済みで返します。

**引数**

* `tableId`
  * テーブルの`ResourceId`。
* `keyTuple`
  * 対象レコードのキー。
* `fieldIndex`
  * 取得したいフィールドの番号。

**戻り値**

* `data`
  * エンコード済みフィールドデータ。

#### `getFieldLength`

指定されたフィールドのバイト長を取得する関数。
動的・静的フィールドに関わらず、対象データの長さを返します。

**引数**

* `tableId`
  * テーブルの識別子。
* `keyTuple`
  * 対象レコードのキー。
* `fieldIndex`
  * 対象フィールドの番号。

**戻り値**

* `uint256`
  * バイト長。

### ストア操作とイベント

#### `Store_SetRecord`

```javascript
event Store_SetRecord(
  ResourceId indexed tableId,
  bytes32[] keyTuple,
  bytes staticData,
  EncodedLengths encodedLengths,
  bytes dynamicData
);
```

レコード全体が設定（上書き）されたときに発行されるイベント。
レコードが新規または既存に関係なく、完全に書き換えられた際に発火します。

**パラメータ**

* `tableId`
  * 対象テーブルのID。
* `keyTuple`
  * レコードのキー配列。
* `staticData`
  * 静的データの符号化結果。
* `encodedLengths`
  * 動的データの長さ情報。
* `dynamicData`
  * 動的データの符号化結果。

#### `Store_SpliceStaticData`

```javascript
event Store_SpliceStaticData(
  ResourceId indexed tableId,
  bytes32[] keyTuple,
  uint48 start,
  bytes data
);
```

静的データの一部が上書きされたときに発行されるイベント。
静的データの特定位置を上書きする操作が行われた場合に発行されます。

**パラメータ**

* `tableId`
  * 対象テーブルのID。
* `keyTuple`
  * レコードキー。
* `start`
  * 上書きを開始するバイト位置。
* `data`
  * 上書き内容。

#### `Store_SpliceDynamicData`

```javascript
event Store_SpliceDynamicData(
  ResourceId indexed tableId,
  bytes32[] keyTuple,
  uint8 dynamicFieldIndex,
  uint48 start,
  uint40 deleteCount,
  EncodedLengths encodedLengths,
  bytes data
);
```

動的データの一部が変更されたときに発行されるイベント。
動的データの追加・削除・置換が行われたときに発行されます。

**パラメータ**

* `tableId`
  * 対象テーブルのID。
* `keyTuple`
  * レコードのキー。
* `dynamicFieldIndex`
  * 動的フィールドの番号。
* `start`
  * 操作の開始位置。
* `deleteCount`
  * 削除するバイト数。
* `encodedLengths`
  * 新しい動的フィールドの長さ情報。
* `data`
  * 挿入するデータ。

#### `Store_DeleteRecord`

```javascript
event Store_DeleteRecord(ResourceId indexed tableId, bytes32[] keyTuple);
```

レコードが削除されたときに発行されるイベント。
指定されたキーのレコードがテーブルから削除された時に発行されます。

**パラメータ**

* `tableId`
  * テーブルのID。
* `keyTuple`
  * 削除対象のキー。

### 特殊テーブル：Tables テーブル

このテーブルは、すべてのテーブルの情報を管理する特別なオンチェーンテーブルです。
新しいテーブルを動的に登録するために使用されます。

| キー  | 説明                 |
| --------- | ------------------ |
| `tableId` | 対象テーブルのResourceId。 |

| 値                      | 説明                               |
| ---------------------- | -------------------------------- |
| `fieldLayout`          | 各静的データ型のバイト長を符号化した`FieldLayout`。 |
| `keySchema`  | キーのデータ型を示すスキーマ。                  |
| `valueSchema`  | 値のデータ型を示すスキーマ。      |
| `abiEncodedKeyNames`   | キー名をABI形式でエンコードしたバイト列。   |
| `abiEncodedFieldNames` | フィールド名をABI形式でエンコードしたバイト列。 |

このテーブルに保存されたレコードは不変です。
登録時に一度だけ`Store_SetRecord`イベントが発行され、その後は変更されません。

登録時に使用される`tableId`の例は以下です。

```javascript
ResourceId tableId = ResourceId.wrap(
  0x746273746f72650000000000000000005461626c657300000000000000000000
);
```

この仕組みにより、オフチェーンのインデクサーは全テーブルの構造を解釈でき、
構造化されたデータベースのようにブロックチェーンデータを扱うことが可能になります。

## 補足

### Spliceイベント

`Store` には、レコード全体を上書きする`Store_SetRecord`イベントがありますが、これだけでは効率面に課題があります。
このため、**ERC7813**では「**部分的な更新**」を可能にする2種類のスプライスイベント — `Store_SpliceStaticData`と`Store_SpliceDynamicData` — を導入しています。

#### 部分更新が必要な理由

もし一部のデータしか変更していないにもかかわらず`Store_SetRecord`イベントを発行すると、全レコードを読み出して再エンコードし、再度発行する必要があります。
これはガスコスト（手数料）と処理量の両面で非効率です。
特に大きな可変長データ（例：長文の文字列や配列）を持つレコードでは、このコストが急増します。

スプライスイベントを使えば、変更箇所だけを最小限のデータで更新できます。
これにより、以下のような効果が得られます。

| 項目           | 効果                       |
| ------------ | ----------------------------------------- |
| 更新コストの削減     | レコード全体を再構築せず、変更箇所だけを処理するためガスコストが低下します。    |
| スケーラビリティの向上  | 大きなデータ構造を扱う場合でもコストが増えにくく、スケールしやすい設計になります。 |
| オフチェーン同期の効率化 | インデクサーが差分だけを処理すればよいため、状態同期が高速になります。       |

つまり、スプライスイベントは、オンチェーンストレージの「**効率的な差分更新**」を可能にするための仕組みです。

### 動的型配列の禁止

**ERC**では、`string[]`や`bytes[]`のような「**動的型の配列**」をスキーマで定義することを明示的に禁止しています。
これは意図的な制約であり、システムの単純化と効率性を維持するために重要です。

#### 制約の意図

動的型の配列は「**ネストされた可変長データ構造**」を生むため、ストレージの実装やエンコーディング処理が複雑化します。
これにより、ストレージ操作のガスコストが増加し、データ復元（デコード）も重くなります。

この問題を避けるため、仕様では「**フラットなデータ構造**」を推奨しています。
つまり、すべてのレコードは一次元的なデータ構造で表現し、複雑な配列構造は別のテーブルを用いて管理します。

#### 代替モデルの例

もし`string[]`のような可変配列を保存したい場合は、以下のようなテーブル構成を使用します。

| カラム名  | 型         | 説明          |
| ----- | --------- | ----------- |
| `index` | `uint256` | 配列要素のインデックス |
| `data`  | `bytes`   | 実際の要素データ    |

このように「**1要素＝1レコード**」として保存すれば、配列の操作をテーブル操作として安全に実現できます。
これにより、設計が単純化され、ガス効率も大幅に向上します。

### FieldLayoutによる最適化（FieldLayout Optimization）

`FieldLayout`は、テーブル内の各静的フィールド（固定長データ）のバイト長を事前に計算して保持する構造です。
これをスキーマ（Tablesテーブル）内に含めることで、実行時の効率を大幅に向上させることができます。

#### 最適化の仕組み

通常、スマートコントラクトは各フィールドの長さやオフセットを動的に計算します。
しかし、この処理は計算量が多く、特に複雑なスキーマではガス消費が増えます。

`FieldLayout`を導入すると、各フィールドのバイト長をあらかじめ記録しておけるため、実行時に再計算する必要がありません。
これにより、ストレージ操作のコストが減り、データの読み書きが効率化されます。

#### 主な利点

| 項目 | 説明         |
| ---------- | --------------- |
| 実行コストの削減   | 長さやオフセットを都度計算する必要がなくなるため、ガス使用量を削減できます。                   |
| 読み取り処理の高速化 | オフチェーン側もTablesテーブルから`FieldLayout`情報を取得し、データを素早くデコードできます。 |
| 実装の一貫性  | すべてのテーブルで統一的な方法でスキーマ情報を扱えるようになります。                       |

つまり、`FieldLayout`は「**スキーマ情報のキャッシュ**」として働き、オンチェーン・オフチェーン両方で処理の最適化を実現する仕組みです。

### 特別なTablesテーブル

最後に、**ERC7813**では「**`Tables`テーブル**」という特別なオンチェーンテーブルが定義されています。
これはすべてのテーブルのスキーマ情報を登録・管理するメタデータテーブルとして機能します。

#### 意義と役割

このテーブルを設けることで、オフチェーンのインデクサーがテーブル構造（スキーマ）を自動的に把握できるようになります。
スキーマを知らない状態で単なるバイナリデータを処理するよりも、構造を理解した形でデータを扱えるようになるため、
SQLのようなクエリ機能や高度なデータ分析が可能になります。

#### 主なメリット

| 項目  | 説明                  |
| ---------------- | ---------- |
| スキーマ認識型インデクサーの実現 | 各テーブルのスキーマ情報を基に、高度なデータクエリをサポートするAPIを構築できます。          |
| 実装の簡素化           | 既存の `Store` イベントを再利用してテーブル登録を行うため、新しいイベントを追加する必要がありません。  |
| 一貫性と拡張性          | すべてのテーブルが同一の `Store` インターフェース上で管理されるため、システム全体の整合性が保たれます。 |

#### イメージ図

```
      Tables テーブル                              
  ┌─────────────────────┐
  │ tableId             │→ 各テーブルの識別子
  │ fieldLayout         │→ 静的フィールドの長さ情報
  │ keySchema           │→ キーの型情報
  │ valueSchema         │→ 値の型情報
  │ abiEncodedKeyNames  │→ キー名（文字列配列）
  │ abiEncodedFieldNames│→ フィールド名（文字列配列）
  └─────────────────────┘
             ↓ 登録イベント（Store_SetRecord）
   他のすべてのテーブルがここで登録される
```

このように、Tablesテーブルはブロックチェーン上での「**スキーマ管理の中枢**」として機能し、オフチェーンサービスがデータ構造を正確に理解できるようにします。

## 参考実装

#### Storeイベントインデックス

以下の例は、単純なインメモリインデクサーが `Store` イベントを利用して `Store` 状態をオフチェーンで複製する方法を示しています。
このインデクサーは生のエンコード済みデータに対して動作します。
この例ではTypeScriptを使用していますが、他の言語でも容易に再現できます。

```ts
type Hex = `0x${string}`;

type Record = {
  staticData: Hex;
  encodedLengths: Hex;
  dynamicData: Hex;
};

const store = new Map<string, Record>();

// Create a key string from a table ID and key tuple to use in our store Map above
function storeKey(tableId: Hex, keyTuple: Hex[]): string {
  return `${tableId}:${keyTuple.join(",")}`;
}

// Like `Array.splice`, but for strings of bytes
function bytesSplice(
  data: Hex,
  start: number,
  deleteCount = 0,
  newData: Hex = "0x"
): Hex {
  const dataNibbles = data.replace(/^0x/, "").split("");
  const newDataNibbles = newData.replace(/^0x/, "").split("");
  return `0x${dataNibbles
    .splice(start, deleteCount * 2)
    .concat(newDataNibbles)
    .join("")}`;
}

function bytesLength(data: Hex): number {
  return data.replace(/^0x/, "").length / 2;
}

function processStoreEvent(log: StoreEvent) {
  if (log.eventName === "Store_SetRecord") {
    const key = storeKey(log.args.tableId, log.args.keyTuple);

    // Overwrite all of the Record's fields
    store.set(key, {
      staticData: log.args.staticData,
      encodedLengths: log.args.encodedLengths,
      dynamicData: log.args.dynamicData,
    });
  } else if (log.eventName === "Store_SpliceStaticData") {
    const key = storeKey(log.args.tableId, log.args.keyTuple);
    const record = store.get(key) ?? {
      staticData: "0x",
      encodedLengths: "0x",
      dynamicData: "0x",
    };

    // Splice the static field data of the Record
    store.set(key, {
      staticData: bytesSplice(
        record.staticData,
        log.args.start,
        bytesLength(log.args.data),
        log.args.data
      ),
      encodedLengths: record.encodedLengths,
      dynamicData: record.dynamicData,
    });
  } else if (log.eventName === "Store_SpliceDynamicData") {
    const key = storeKey(log.args.tableId, log.args.keyTuple);
    const record = store.get(key) ?? {
      staticData: "0x",
      encodedLengths: "0x",
      dynamicData: "0x",
    };

    // Splice the dynamic field data of the Record
    store.set(key, {
      staticData: record.staticData,
      encodedLengths: log.args.encodedLengths,
      dynamicData: bytesSplice(
        record.dynamicData,
        log.args.start,
        log.args.deleteCount,
        log.args.data
      ),
    });
  } else if (log.eventName === "Store_DeleteRecord") {
    const key = storeKey(log.args.tableId, log.args.keyTuple);

    // Delete the whole Record
    store.delete(key);
  }
}
```

## セキュリティ

#### アクセス制御

**ERC7813**が定義しているのは、`Store` からデータを読み取るための関数のみです。
具体的には次の3つです。

| 関数名              | 役割                              |
| ---------------- | ------------------------------- |
| `getRecord`      | 指定されたテーブルIDとキーに基づき、レコード全体を取得する。 |
| `getField`       | 指定されたフィールドのみを取得する。              |
| `getFieldLength` | 指定されたフィールドの長さを取得する。             |

これらは読み取り専用の関数です。
一方で、「**データを書き込む**」、「**変更する**」、「**削除する**」といった操作に関する関数は、この標準では定義されていません。
そのため、`Store` の実装を行う開発者は、それぞれのユースケースに合わせて適切なアクセス制御（Access Control）を設ける必要があります。

##### アクセス制御が必要な理由

ブロックチェーン上では、すべてのコントラクトやアカウントが公開されており、関数が「**公開状態（public/external）**」であれば、誰でも呼び出すことができます。
そのため、もしデータ更新のメソッドを無制限に呼び出せるようにしてしまうと、悪意のある第三者がストレージの内容を自由に書き換えるリスクがあります。

##### 実装上の対策例

| 対策  | 説明  |
| ------------ | -- |
| ロールベースアクセス制御 | `Ownable`や`AccessControl`を利用して、管理者（owner）または特定のロールを持つアドレスのみが書き込みできるようにする。 |
| ホワイトリスト方式    | 許可された特定のコントラクトまたはアカウントのみが `Store` への書き込み操作を行えるよう制限する。                         |
| 認可付き関数呼び出し   | トランザクション発行時に署名検証を行い、署名者が正当な権限を持つか確認する。                                    |

開発者は、自身のアプリケーションがどのようなデータを管理するのか、誰がどのようにそのデータを変更できるのかを明確に定義し、それに基づいたアクセス制御を設計する必要があります。

#### オンチェーンデータの公開性

もう1つ重要なセキュリティの特徴は、`Store` に保存されたデータは**完全に公開されているという点です。

通常のスマートコントラクトでは、`storage`変数は「**内部状態**」として扱われ、他のコントラクトから直接読み取ることはできません。
もし他のコントラクトに読み取らせたい場合は、開発者が明示的に「**読み取り関数**」を用意する必要があります。

しかし、**ERC7813**で定義された `Store` はそれとは異なります。
`Store` 内にあるデータは、以下の読み取り関数を通じて、あらゆるコントラクトからアクセス可能です。

| 関数名              | 概要                             |
| ---------------- | ------------------------------ |
| `getRecord`      | テーブルIDとキーを指定すると、レコード全体を取得できます。 |
| `getField`       | 特定のフィールドを単独で取得できます。            |
| `getFieldLength` | フィールドのバイト長を取得できます。             |

この仕組みにより、オンチェーン上の他のスマートコントラクトも、`Store` に保存されたデータを直接読み取ることができます。
これは透明性と相互運用性を高める利点がありますが、一方で「**秘匿すべき情報を保存してはいけない**」という明確な制約も生まれます。

##### 公開データであることの影響

- **他コントラクトからの読み取り**
    他のスマートコントラクトが`getRecord`などを呼び出すことで、データを参照できます。
    これにより、複数のアプリケーション間でデータを共有することが容易になります。
- **オフチェーンからのアクセス**
    誰でもブロックチェーンをスキャンすることで、`Store` 内のすべてのデータを復元できます。
    そのため、個人情報や機密データを格納してはいけません。
- **暗号化が必要なケース**
    どうしても機密データを扱いたい場合は、あらかじめ暗号化した状態で保存し、復号はオフチェーンで行うように設計することが推奨されます。

##### 注意点のまとめ

| 注意点           | 説明                                           |
| ------------- | -------------------------------------------- |
| すべてのデータは公開状態  | Store内に保存されたデータは、オンチェーン・オフチェーン問わず誰でも閲覧可能です。  |
| センシティブ情報の格納禁止 | 個人情報、秘密鍵、パスワード、内部ロジックに関するデータなどを直接保存してはいけません。 |
| 暗号化保存を検討      | 機密性を求める場合は、暗号化後のデータのみを保存する方式を取る必要があります。      |

## 引用

alvarius (@alvrs), dk1a (@dk1a), frolic (@frolic), ludens (@ludns), vdrg (@vdrg), yonada <yonada@proton.me>, "ERC-7813: Store, Table-Based Introspectable Storage [DRAFT]," Ethereum Improvement Proposals, no. 7813, November 2024. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7813.

## 最後に

今回は「ブロックチェーン上のデータをデータベースのように整理・管理できる仕組みを定義し、効率的で拡張性の高いオンチェーンデータ管理を実現する仕組みを提案している**ERC7813**」についてまとめてきました！
いかがだったでしょうか？

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

[Twitter @cardene777](https://twitter.com/cardene777)

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

https://chaldene.net/

https://zenn.dev/heku

https://cardene.substack.com/

https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58

https://cardene.notion.site/ERC-EIP-2a03fa3ea33d43baa9ed82288f98d4a9?pvs=4
