# [ERC8326] 契約書や証明書のファイルを決まった手順でハッシュ化してオンチェーンに記録する仕組みを理解しよう！

> 契約に関するPDF、JSON形式の証明書、補足資料を複数人が持つ場合を考えます。ファイルが同じに見えても、名前の大文字小文字、JSONのキー順、登録した順番が異なると、単純なハッシュでは別の値になります。

- URL: https://cardene.dev/articles/7d910bc2a6c6441290e0
- 公開日: 2026-08-02
- 更新日: 2026-08-02
- トピック: Blockchain
- タグ: ethereum, erc, security

## はじめに

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

https://amzn.asia/d/gxvJ0Pw

以下でも情報発信しているので、興味ある記事があればぜひ読んでみてください！

https://cardene.notion.site/EIP-2a03fa3ea33d43baa9ed82288f98d4a9

https://twitter.com/cardene777

https://zenn.dev/heku

https://cardene.substack.com/

https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58

ERC8326は、契約書や証明書のファイル群から同じ `bytes32` を作り、その値をEthereum上で追跡できるようにする仕組みです。
ファイルそのものはオンチェーンへ送らず、内容とファイルの役割を決まった形式でハッシュ化した結果だけを記録します。

2026年8月2日時点でERC8326の状態は `Review` です。
実装や採用を決める時は、最新の原文も確認してください。

https://raw.githubusercontent.com/ethereum/ERCs/master/ERCS/erc-8326.md

## 概要

契約に関するPDF、JSON形式の証明書、補足資料を複数人が持つ場合を考えます。
ファイルが同じに見えても、名前の大文字小文字、JSONのキー順、登録した順番が異なると、単純なハッシュでは別の値になります。

そこで、ERC8326は、各ファイルの内容だけではなく、何のためのファイルか、どの形式か、どの名前か、どの正規化方法を使ったかを含めて1つの値にします。
この値を `bundleHash` と呼びます。

`bundleHash` は「**特定のファイル群とその説明が一致するか**」を再計算で確認するための値です。
ファイルの真正性、法的な効力、誰が作ったか、ファイルを取得できることまで証明する値ではありません。

## 動機

#### ファイルを1つずつではなくまとめて確定する理由

1つの契約に、合意書、本人確認の証明書、改定書が含まれることがあります。
ファイル単位のハッシュだけを保存すると、どれが同じ契約に属するか、どのファイルが主契約かを別の約束で管理することになります。

そこで、ERC8326では、ファイル群全体を1つの束としてハッシュ化します。
同じ入力から常に同じ `bundleHash` を作れるため、利用者は手元のファイル群を再計算して、オンチェーンに残る値と照合できます。

![複数の契約書と証明書ファイルをハッシュ化し決められた順番でbundleHashへまとめる流れ](/images/qiita-0056f95c-2a3b-4c41-9031-9f076f928f65.webp)

ファイルを渡された側は、まず内容を決められた方法で正規化します。
続いて各ファイルの情報をハッシュ化し、決められた順番へ並べます。
この順番まで決めることで、同じファイル群を別の順で渡しても同じ `bundleHash` になります。

## 仕様

### ファイルごとに記録する5つの値

ERC8326は、各ファイルを `DocumentEntry` として表します。
ここでいうファイルは、内容だけを表すものではありません。
同じ内容であっても、契約書として使うのか、証拠として使うのかで別のエントリになります。

```js
struct DocumentEntry {
    bytes32 contentHash;
    bytes32 role;
    bytes32 mimeTypeHash;
    bytes32 filenameHash;
    bytes32 normProfileId;
}
```

| フィールド | 入る値 | 何を区別するか |
| --- | --- | --- |
| `contentHash` | 正規化後の内容の `keccak256` | ファイルの中身 |
| `role` | 用途名のハッシュ | 合意書、証拠、証明書といった役割 |
| `mimeTypeHash` | パラメータを除いて小文字化したMIMEタイプのハッシュ | `application/json` や `application/pdf` |
| `filenameHash` | 正規化後のファイル名のハッシュ | ファイル名 |
| `normProfileId` | 正規化方法を表す識別子のハッシュ | どの手順で内容を揃えたか |

`role` には `AGREEMENT`、`EVIDENCE`、`CERTIFICATION`、`AMENDMENT` といった既定の値があります。
アプリケーション独自の役割も作れますが、意味が偶然重ならないよう、名前空間とバージョンを含む値を公開します。

#### ファイル名もハッシュの入力になる

`filenameHash` は、パスを除いたファイル名を使います。
ASCIIの英大文字を小文字へ変え、Unicode NFCで正規化したUTF-8バイト列をハッシュ化します。

例えば `records/Café.JSON` は、パスを除き、英大文字を小文字にし、結合文字をNFCで揃えた `café.json` を使います。
この処理を省くと、同じ見た目の名前でも環境ごとに別のハッシュになることがあります。

ただし、この処理はUnicode全体の大文字小文字を同じものとして扱うものではありません。
ASCII以外の文字についてどこまで同じと見なすかは、仕様が定めるNFC正規化の範囲を超えて追加しません。

### 内容をハッシュ化する前に揃える方法

ファイルのバイト列をそのままハッシュ化すれば済む場合もあります。
一方でJSONやXMLは、同じ意味でもキー順や属性順の違いでバイト列が変わります。
ERC8326は、内容を揃える方法を `normProfileId` で明示します。

| 正規化方法 | 使う内容 | 処理 |
| --- | --- | --- |
| `NORM:RAW:V1` | PDF、画像、平文、正規化しない入力 | 入力バイト列を変更しない |
| `NORM:JSON:RFC8785:V1` | JSON | RFC 8785の形式へ変換したUTF-8バイト列を使う |
| `NORM:XML:C14N11:V1` | XML | コメントを含めないCanonical XML 1.1の出力を使う |

JSONを使う時は、重複したキー、不正なUnicode、単独サロゲート、相互運用できる範囲を外れた数値を受け付けません。
JSON文字列のUnicodeを追加で正規化することもありません。

XMLを使う時は、外部エンティティの解決を無効にします。
Canonical XML 1.1とExclusive XML Canonicalizationは別の方法なので、同じものとして扱いません。

PDFや画像のための正規化方法はERC8326で定義されていません。
同じPDFを別のソフトウェアで保存し直すと、見た目が同じでもメタデータや圧縮方法が変わり、別のバイト列になることがあります。
そのため、PDFや画像は原則として `NORM:RAW:V1` を使い、独自の方法を使うなら変換手順とテスト用のファイルを公開します。

### bundleHashを作る手順

各 `DocumentEntry` を作っただけでは、まだファイル群の値は決まりません。
ERC8326は5項目すべてを使って、エントリを昇順へ並べます。

1. **`role` で並べる**

    まず役割の `bytes32` 値をバイト列として比較し、小さい値を前に置きます。

2. **`filenameHash` で並べる**

    役割が同じなら、ファイル名のハッシュを比較します。

3. **残りの3項目で並べる**

    名前まで同じなら、`contentHash`、`mimeTypeHash`、`normProfileId` の順で比較します。

4. **各エントリから `leaf` を作る**

    5つの固定長 `bytes32` を `abi.encodePacked` で連結して `keccak256` を計算します。

5. **スキーマIDとすべての `leaf` をハッシュ化する**

    `SCHEMA_V1` と並べ替え済みの `leaf` を連結して `keccak256` を計算した値が `bundleHash` です。

同じ `DocumentEntry` が2つあれば、1つにまとめません。
重複もファイル群の一部として残します。

```js
bytes32 leaf = keccak256(
    abi.encodePacked(
        entry.contentHash,
        entry.role,
        entry.mimeTypeHash,
        entry.filenameHash,
        entry.normProfileId
    )
);

bundleHash = keccak256(
    abi.encodePacked(SCHEMA_V1, leaf0, leaf1, /* ... */, leafN)
);
```

`SCHEMA_V1` を含めるのは、将来の別形式と同じハッシュの空間を共有しないためです。
互換性のない形式を追加する場合は、新しいスキーマIDを使います。

#### コントラクト内で並べ替える関数と事前に並べ替える関数

ERC8326の参照実装には、2つのハッシュ計算経路があります。

#### `computeCanonicalBundleHash`

```js
function computeCanonicalBundleHash(DocumentEntry[] memory entries)
    internal pure returns (bytes32);
```

この関数は、受け取ったエントリをコントラクト内で並べ替えてから `bundleHash` を作ります。
入力順が不明な少数のファイルをテストする時には分かりやすい書き方です。

1. **入力を作業用の配列へコピーする**

    呼び出し元が渡した `entries` を、並べ替えられる作業用の配列として扱います。

2. **5項目の順で並べ替える**

    `role`、`filenameHash`、`contentHash`、`mimeTypeHash`、`normProfileId` の順に各エントリを比較します。

3. **並べ替え済みのエントリから `bundleHash` を作る**

    各エントリの `leaf` と `SCHEMA_V1` を連結してハッシュ化します。

ただし、参照実装の並べ替えはメモリ上で二乗時間になります。
大きいファイル群を扱うアプリケーションでは、正規化、並べ替え、ハッシュ化をオフチェーンで行います。

#### `computeBundleHash`

```js
function computeBundleHash(DocumentEntry[] memory entries)
    internal pure returns (bytes32);
```

この関数は、すでに決められた順番に並ぶ入力だけを受け付けます。
順番が違う入力と空のファイル群は `revert` します。

1. **入力が空でないことを確認する**

    エントリが1つもない入力は受け付けません。

2. **隣り合うエントリの並び順を確認する**

    5項目による比較順が崩れている入力は `revert` します。

3. **順序済みのエントリから `bundleHash` を作る**

    各エントリの `leaf` と `SCHEMA_V1` を連結してハッシュ化します。

どちらの経路でも、最終的に作る値は同じです。
利用者が照合する時も、オンチェーンの関数に任せず、手元のファイルから正規化、並べ替え、ハッシュ化を再現します。

### `IDocumentBundleAnchor`

ERC8326は、ファイル群の値だけを保存するのではありません。
その値が誰の何の用途に対応するかを、`subjectId` と `role` で分けます。

例えば、同じ会社に関するファイル群でも、事業者の本人確認用と契約更新用では別の用途です。
同じ `bundleHash` を別の `subjectId` や `role` で記録しても、それぞれ独立した記録になります。

`IDocumentBundleAnchor` は、ファイル群のハッシュを対象と用途へ結び付け、現在有効な値と更新履歴を読み出すためのインターフェースです。
同じ `bundleHash` でも `subjectId` や `role` が違えば、別の記録として扱います。

#### `AnchorRecord`

```js
struct AnchorRecord {
    bytes32 bundleHash;
    bytes32 subjectId;
    bytes32 role;
    address anchoredBy;
    uint64 anchoredAt;
    uint256 documentCount;
    string metadataURI;
    bool superseded;
    bytes32 supersededBy;
}
```

`AnchorRecord` は、1つの `(bundleHash, subjectId, role)` に保存する値をまとめた構造体です。
`anchorBundle` と `supersedeBundle` が作成し、`getAnchor` が読み出します。

- **フィールド**

| フィールド | 詳細 |
| --- | --- |
| `bundleHash` | ファイル群から作ったハッシュです。 |
| `subjectId` | ファイル群が属する対象を表す、ゼロではない識別子です。 |
| `role` | 対象に対するファイル群の用途です。 |
| `anchoredBy` | 記録を作成した呼び出し元アカウントです。 |
| `anchoredAt` | コントラクトが記録を作成したブロック時刻です。 |
| `documentCount` | 作成者が申告したファイル数です。 |
| `metadataURI` | 取得先を示す任意の文字列です。 |
| `superseded` | 後続の記録へ差し替えられたかを示す値です。 |
| `supersededBy` | 差し替え後の `bundleHash` です。 |

`subjectId` はアプリケーションが決めます。
既存の識別子がない場合は、アプリケーションの文脈を含めたドメイン分離済みの値から作ります。

`metadataURI` は、ファイルを取得する場所を表す任意の文字列です。
空でも記録できます。
空の値はファイルが存在しないことを示すのではなく、この記録が取得先を提供しないことを示します。

#### `BundleAnchored`

```js
event BundleAnchored(
    bytes32 indexed bundleHash,
    bytes32 indexed subjectId,
    bytes32 indexed role,
    uint256 documentCount
);
```

`BundleAnchored` イベントは、新しいファイル群の記録が作られた時に発行されます。
インデクサーはこのイベントログから対象と用途ごとの新規記録を検索できます。

- **フィールド**

| フィールド | 詳細 |
| --- | --- |
| `bundleHash` | インデックス付きで記録されるファイル群のハッシュです。 |
| `subjectId` | インデックス付きで記録される対象の識別子です。 |
| `role` | インデックス付きで記録される用途です。 |
| `documentCount` | 作成者が申告したファイル数です。 |

イベントログだけでは `metadataURI`、`anchoredBy`、差し替え状態を完全には読めません。
それらは `getAnchor` が返す保存済みのレコードで確認します。

#### `anchorBundle`

```js
function anchorBundle(
    bytes32 bundleHash,
    bytes32 subjectId,
    bytes32 role,
    uint256 documentCount,
    string calldata metadataURI
) external;
```

`anchorBundle` は、空ではない `bundleHash`、`subjectId`、`role`、`documentCount` を受け取り、ファイル群を記録します。
成功すると呼び出したアカウントを `anchoredBy` に、ブロック時刻を `anchoredAt` に保存し、`BundleAnchored` イベントを発行します。

1. **入力と用途スロットを確認する**

    `bundleHash`、`subjectId`、`role`、`documentCount` がゼロでないことと、同じ `(subjectId, role)` に有効な記録がないことを確認します。

2. **レコードを作成する**

    入力値、`msg.sender`、ブロック時刻を `AnchorRecord` に保存します。
    新しいレコードの `superseded` は `false`、`supersededBy` は `bytes32(0)` にします。

3. **有効なファイル群として記録する**

    `(subjectId, role)` の有効な値を新しい `bundleHash` にし、`BundleAnchored` イベントを発行します。

- **引数**

| 引数 | 詳細 |
| --- | --- |
| `bundleHash` | 正規化、並べ替え、ハッシュ化で作ったファイル群の値です。 |
| `subjectId` | ファイル群が属する対象を表すゼロではない識別子です。 |
| `role` | 対象に対するファイル群の用途です。 |
| `documentCount` | ファイル群に含めたエントリ数です。 |
| `metadataURI` | ファイルの取得先を示す任意の文字列です。 |

- **実行条件**

- 呼び出し元は実装が定める記録権限を満たします。
- `bundleHash`、`subjectId`、`role`、`documentCount` はゼロにしません。
- 同じ `(bundleHash, subjectId, role)` は2度登録しません。
- 同じ `(subjectId, role)` に有効な記録がある時は `revert` し、`supersedeBundle` を使います。

#### ファイル群を差し替えても古い記録を消さない

契約の更新や訂正が起きると、同じ対象と用途のファイル群を新しいものへ差し替えます。
ERC8326は古いレコードの値を書き換えません。
古いレコードに「**新しい `bundleHash` へ置き換わった**」と記録し、新しいレコードを有効な値にします。

![古いbundleHashの記録を残し新しいbundleHashを有効な記録としてつなぐ流れ](/images/qiita-ffda5aab-b98f-4388-a26c-0091689582fe.webp)

古いレコードは `superseded` が `true` になり、`supersededBy` が新しい `bundleHash` を指します。
古いファイル群を再確認する必要がある時も、古いレコード自体は読み出せます。

#### `supersedeBundle`

```js
function supersedeBundle(
    bytes32 oldBundleHash,
    bytes32 newBundleHash,
    bytes32 subjectId,
    bytes32 role,
    uint256 documentCount,
    string calldata metadataURI
) external;
```

`supersedeBundle` は、有効な古い `bundleHash` を新しい `bundleHash` で差し替えます。
古い記録の更新、新しい記録の作成、用途に対応する有効な値の変更は、1つのトランザクションで行います。

1. **古い記録と新しい入力を確認する**

    古いレコードが存在し、まだ差し替えられておらず、指定した `(subjectId, role)` で現在有効であることを確認します。
    新しい値、対象、用途、件数がゼロでないことと、古い値と新しい値が異なることも確認します。

2. **古いレコードを差し替え済みにする**

    古いレコードの `superseded` を `true` にし、`supersededBy` へ新しい `bundleHash` を保存します。

3. **新しいレコードを有効にする**

    新しい `AnchorRecord` を作り、用途スロットの有効な値を更新します。
    `BundleSuperseded` と新しいレコードの `BundleAnchored` を発行します。

- **引数**

| 引数 | 詳細 |
| --- | --- |
| `oldBundleHash` | 現在有効な、置き換える前のファイル群のハッシュです。 |
| `newBundleHash` | 新しく有効にするファイル群のハッシュです。 |
| `subjectId` | ファイル群が属する対象を表すゼロではない識別子です。 |
| `role` | 対象に対するファイル群の用途です。 |
| `documentCount` | 新しいファイル群に含めたエントリ数です。 |
| `metadataURI` | 新しいファイル群の取得先を示す任意の文字列です。 |

- **実行条件**

- 呼び出し元はその用途スロットを差し替える権限を満たします。
- 古いレコードは存在し、差し替え済みではなく、指定した用途で現在有効です。
- `newBundleHash`、`subjectId`、`role`、`documentCount` はゼロにしません。
- 古い値と新しい値は異なり、新しい `(bundleHash, subjectId, role)` は未登録です。
- 関係のない記録作成者が他人の `(subjectId, role)` を取得できる認可にしません。

#### `BundleSuperseded`

```js
event BundleSuperseded(
    bytes32 indexed oldBundleHash,
    bytes32 indexed newBundleHash,
    bytes32 indexed subjectId,
    bytes32 role
);
```

このイベントを追うと、ある用途でどの `bundleHash` が後続の記録に置き換えられたかを確認できます。
差し替え時には、新しいレコードに対する `BundleAnchored` も発行されます。

- **フィールド**

| フィールド | 詳細 |
| --- | --- |
| `oldBundleHash` | インデックス付きで記録される、差し替え前のファイル群のハッシュです。 |
| `newBundleHash` | インデックス付きで記録される、差し替え後のファイル群のハッシュです。 |
| `subjectId` | インデックス付きで記録される対象の識別子です。 |
| `role` | 差し替えたファイル群の用途です。 |

このイベントで新旧の値の関係を追えます。
新しいレコードの取得先や記録時刻は `getAnchor` で確認します。

#### 有効な記録を読む時の確認

レコードが存在することと、現在有効であることは別です。
そのため、ERC8326は、この2つを分けて読む関数を用意します。

#### `getAnchor`

```js
function getAnchor(
    bytes32 bundleHash,
    bytes32 subjectId,
    bytes32 role
) external view returns (AnchorRecord memory);
```

指定した `(bundleHash, subjectId, role)` の完全なレコードを返します。
存在しない3つ組を渡すと `revert` します。

1. **3つ組に対応するレコードを探す**

    `bundleHash`、`subjectId`、`role` をキーとして、保存済みの記録を検索します。

2. **完全な `AnchorRecord` を返す**

    レコードがあれば、取得先、記録時刻、差し替え状態を含む全フィールドを返します。

- **引数**

| 引数 | 詳細 |
| --- | --- |
| `bundleHash` | 確認したいファイル群のハッシュです。 |
| `subjectId` | 確認したい対象の識別子です。 |
| `role` | 確認したい用途です。 |

- **戻り値**

| 戻り値 | 詳細 |
| --- | --- |
| `AnchorRecord` | 指定した3つ組に保存された完全なレコードです。 |

- **実行条件**

- 指定した `(bundleHash, subjectId, role)` が存在しない時は `revert` します。

#### `isAnchored`

```js
function isAnchored(
    bytes32 bundleHash,
    bytes32 subjectId,
    bytes32 role
) external view returns (bool);
```

レコードが1度でも作られていれば `true` を返します。
すでに差し替えられた古いレコードも `true` になるため、この関数だけで現在有効かは判断しません。

1. **3つ組に対応するレコードを確認する**

    `bundleHash`、`subjectId`、`role` をキーとして、レコードが保存されているかを確認します。

2. **存在するかを返す**

    差し替え済みかに関わらず、存在するなら `true`、未知の3つ組なら `false` を返します。

- **引数**

| 引数 | 詳細 |
| --- | --- |
| `bundleHash` | 存在を確認したいファイル群のハッシュです。 |
| `subjectId` | 存在を確認したい対象の識別子です。 |
| `role` | 存在を確認したい用途です。 |

- **戻り値**

| 戻り値 | 詳細 |
| --- | --- |
| `bool` | 指定した3つ組のレコードが存在する時は `true` です。 |

- **実行条件**

- 読み取り専用の関数です。
- 差し替え済みのレコードも存在するレコードとして `true` を返します。

#### `activeBundle`

```js
function activeBundle(
    bytes32 subjectId,
    bytes32 role
) external view returns (bytes32);
```

対象と用途に対応する現在の `bundleHash` を返します。
1度も記録されていない場合は `bytes32(0)` を返します。

1. **用途スロットを確認する**

    `subjectId` と `role` で、対象の現在有効なファイル群を検索します。

2. **有効な値またはゼロ値を返す**

    1度も登録されていない用途なら `bytes32(0)` を返し、登録済みなら現在有効な `bundleHash` を返します。

- **引数**

| 引数 | 詳細 |
| --- | --- |
| `subjectId` | 現在有効なファイル群を確認したい対象の識別子です。 |
| `role` | 現在有効なファイル群を確認したい用途です。 |

- **戻り値**

| 戻り値 | 詳細 |
| --- | --- |
| `bytes32` | 現在有効なファイル群のハッシュ、または未登録を示す `bytes32(0)` です。 |

- **実行条件**

- 読み取り専用の関数です。
- 未登録の用途は `revert` せず `bytes32(0)` を返します。

利用者がファイル群を確認する手順はこの6つです。

1. **有効な `bundleHash` を取得する**

    `activeBundle(subjectId, role)` を呼び、`bytes32(0)` なら確認を止めます。

2. **保存済みのレコードを取得する**

    返された値と同じ `subjectId`、`role` を使って `getAnchor` を呼びます。

3. **対象と用途が一致するか確認する**

    返された `bundleHash`、`subjectId`、`role` が要求した値と一致することを確認します。

4. **記録時刻と件数を確認する**

    `anchoredAt` と `documentCount` がゼロでないことを確認します。

5. **差し替え済みでないことを確認する**

    `superseded` が `false` であることを確認します。

6. **手元のファイル群から値を再計算する**

    正規化、5項目の作成、並べ替え、`bundleHash` の計算を再現し、オンチェーンの値と比べます。

手元で再計算した値と、`activeBundle` が返す現在の値を同じものとして比較します。
一致した時だけ、手元のファイル群がその対象と用途で現在有効な記録と同じ入力から作られたことを確認できます。

![手元のファイルを正規化してbundleHashを再計算し現在有効なオンチェーン記録と照合する流れ](/images/qiita-622e702d-8d25-40f5-afa3-34636681b158.webp)

`documentCount` はコントラクトがファイル群の中身から検証する値ではありません。
再計算したエントリ数を数え、保存済みの件数だけを信用しないようにします。

### `IDocumentBundleAnchorRecovery`

記録を作るアカウントを失った場合や、権限を持つアカウントが先に用途を占有した場合に備え、ERC8326には任意の回復用インターフェースがあります。
回復を実装するレジストリは、管理者が特定の `(subjectId, role)` の担当アカウントを変更できます。

#### `SlotPrincipalAssigned`

```js
event SlotPrincipalAssigned(
    bytes32 indexed subjectId,
    bytes32 indexed role,
    address indexed principal
);
```

`SlotPrincipalAssigned` イベントは、回復管理者が用途スロットの担当アカウントを変更した時に発行されます。
監視側はこのイベントログから、対象と用途ごとの現在の担当者を追えます。

- **フィールド**

| フィールド | 詳細 |
| --- | --- |
| `subjectId` | インデックス付きで記録される対象の識別子です。 |
| `role` | インデックス付きで記録される用途です。 |
| `principal` | インデックス付きで記録される新しい担当アカウントです。 |

イベントだけでは、担当者が実装ごとの一般的な記録権限を満たすかは分かりません。
実際の認可条件はレジストリ実装が確認します。

#### `slotPrincipal`

```js
function slotPrincipal(
    bytes32 subjectId,
    bytes32 role
) external view returns (address);
```

`slotPrincipal` は、用途スロットに割り当てられた担当アカウントを返します。
担当者がいない時は `address(0)` を返します。

1. **用途スロットを検索する**

    `subjectId` と `role` を使って、回復用に保存した担当アカウントを検索します。

2. **担当アカウントまたはゼロアドレスを返す**

    担当者が割り当て済みならそのアドレスを返し、未割り当てなら `address(0)` を返します。

- **引数**

| 引数 | 詳細 |
| --- | --- |
| `subjectId` | 担当者を確認したい対象の識別子です。 |
| `role` | 担当者を確認したい用途です。 |

- **戻り値**

| 戻り値 | 詳細 |
| --- | --- |
| `address` | 担当アカウント、または未割り当てを示す `address(0)` です。 |

- **実行条件**

- 読み取り専用の関数です。
- 未割り当ての用途は `revert` せず `address(0)` を返します。

#### `assignSlotPrincipal`

```js
function assignSlotPrincipal(
    bytes32 subjectId,
    bytes32 role,
    address principal
) external;
```

`assignSlotPrincipal` は、用途スロットの担当アカウントを変更します。
この呼び出しだけで、一般的な記録権限を新しい担当者に与えるわけではありません。

1. **呼び出し元と入力を確認する**

    呼び出し元が回復管理者であることと、`subjectId`、`role`、`principal` がゼロでないことを確認します。

2. **担当アカウントを保存する**

    指定した `(subjectId, role)` の担当者を `principal` に更新します。

3. **担当者の変更を発行する**

    `SlotPrincipalAssigned` イベントを発行し、監視側が変更を追えるようにします。

- **引数**

| 引数 | 詳細 |
| --- | --- |
| `subjectId` | 担当者を変更する対象の識別子です。 |
| `role` | 担当者を変更する用途です。 |
| `principal` | 新しく割り当てるゼロではない担当アカウントです。 |

- **実行条件**

- 呼び出し元は実装が定める回復管理者でなければなりません。
- `subjectId`、`role`、`principal` はゼロにしません。

`assignSlotPrincipal` は、回復管理者だけが呼び出せます。
担当アカウントを変える操作だけでは一般的な記録権限を与えません。
担当アカウントも、実装ごとの認可条件を満たして初めて記録や差し替えを実行できます。

用途を奪われた時に管理者が直接 `supersedeBundle` を呼ぶと、元の担当者が先に差し替えるトランザクションを送る可能性があります。
回復用の拡張は、先に担当者を原子的に変更してから正しい担当者が差し替えられるようにするためのものです。

![回復管理者がassignSlotPrincipalで担当者を先に切り替え新しい担当者がsupersedeBundleで差し替える流れ](/images/qiita-ed370e4c-d135-4b24-95e0-98aa35c0f051.webp)

この図で示す順序は、`assignSlotPrincipal` だけで一般的な記録権限を与えるという意味ではありません。
新しい担当者は、レジストリ実装が定める認可条件も満たした時だけ `supersedeBundle` を実行できます。

ERC8326に従うレジストリは `ERC165` を実装し、`IDocumentBundleAnchor` をサポートすると返します。

:::message
**ERC165**については以下の記事を参考にしてください。
:::

[\[ERC165\] コントラクトが特定のインターフェースをサポートしているか確認する仕組みを理解しよう！](/articles/541184977a5cba51b759)

ただし、`ERC165` の応答はインターフェースを実装したという申告です。
ファイル群が正しくハッシュ化されたこと、ファイルが本物であること、記録作成者を信頼できることまでは確認しません。

## 互換性

ERC8326は新しいインターフェースを追加するもので、既存のトークン標準やレジストリ標準の動作は変えません。
既存のシステムは、作った `bundleHash` を既存の記録項目へ保存したり、別の `IDocumentBundleAnchor` レジストリを用意したりして組み合わせられます。

ただし、従来の方法で作ったハッシュ値が、ERC8326の並べ替え、5項目、スキーマIDを使っているとは限りません。
同じ `bytes32` 型へ保存できても、作り方が違えば再計算による照合はできません。

## 参考実装

公式の議論には、Solidityの参照実装、ハッシュライブラリ、単体テスト、プロパティテスト、利用者側の検証コード、監査へのリンクがあります。
参照実装は `DocumentEntry` の並べ替えとハッシュ計算を行いますが、JSONやXMLを正規化する処理はオフチェーンで行います。

https://ethereum-magicians.org/t/erc-8326-canonical-document-bundle-anchor/28935

## セキュリティ

ERC8326を使うと、同じ正規化方法と同じ5項目から作ったファイル群が、記録済みの `bundleHash` と一致するかを確認できます。
また、対象と用途ごとに、どの記録が現在有効か、過去にどの値へ差し替えられたかを追えます。

一方で、ハッシュ値だけからファイルを取得することはできません。
`metadataURI` が空でも記録は有効であり、URIがあってもファイルが将来も取得できるとは限りません。

ハッシュが一致しても、作成者の署名、発行者の権限、法的な効力は別に確認します。
個人情報や機密情報をURIへ直接書き込むことも避けます。

## 最後に

ERC8326は、契約書や証明書のファイル群を、内容、役割、MIMEタイプ、名前、正規化方法まで含めて1つの `bundleHash` にします。
利用者は手元のファイル群から同じ値を再計算し、現在有効なオンチェーン記録と照合できます。
差し替えでは古い記録を消さず、新しい値へつなぐため、更新前の記録も確認できます。

他でも色々記事を書いているのでぜひよろしければ読んでいってください！

https://amzn.asia/d/gxvJ0Pw

https://cardene.notion.site/EIP-2a03fa3ea33d43baa9ed82288f98d4a9

https://zenn.dev/heku

https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58

https://twitter.com/cardene777

https://cardene.substack.com/
