[ERC7231] NFTを通じた新しい自己管理型アイデンティティの仕組みを理解しよう！


# はじめに

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

https://cryptogames.co.jp/

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

https://cryptospells.jp/

今回は、Web2とWeb3のアイデンティティを、個人の認可によりNFTに集約する仕組みを提案している規格である**ERC7231**についてまとめていきます！

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

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

:::note warn
**7231**は現在（2023年12月16日）では「**Last Call**」段階です。
:::

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

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

# 概要

この新しいシステムは、**ERC721**という基準をさらに進化させています。
主な目的は、私たちが普段使っているインターネット（**Web2**）と、ブロックチェーン技術に基づく新しいインターネット（**Web3**）の両方における個人のアイデンティティ（**身分情報**）を、特別なトークン、つまり非代替トークン（NFT）やソウルバウンドトークン（SBT）にリンクさせることです。

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

https://chaldene.net/erc721

これはどういう意味かというと、あなたのオンラインでの存在や活動がこれらのトークンによって表されるようになるということです。
例えば、あなたがソーシャルメディアで行った活動や、特定のサービスへのログイン情報などがNFTやSBTによって証明されるようになります。

このプロセスは、メタデータ（トークンに関する情報）に特定のスキーマ（構造）を加えることから始まります。
そして、そのスキーマのハッシュ（一種のデジタル指紋）をブロックチェーン上のコントラクトで更新し検証します。
これにより、NFTがただのデジタルアセットではなく、あなたのアイデンティティや行動の記録を持つものになるわけです。

:::note info
この標準はあなたのオンラインでの身分や行動を、ブロックチェーン上のトークンによって証明し、それを様々な場所で活用できるようにするためのものです。
これにより、より安全で、個人にとって有益なオンライン体験が提供されることを目指しています。
:::

# 動機

Web3の面白い点は、個々の人々が自分自身のアイデンティティ（身分情報）を様々なアプリケーションに適用できるところです。
もっとも重要なのは、中央集権型の管理者に依存することなく、個人が自分のアカウントを完全に所有しているという点です。
これにより、個人は自分で認証した必要な情報を異なるアプリに開示することが可能になります。

現存するソリューションであるENS（イーサリアムネームサービス）は、オープンで分散型のシステムであり、イーサリアムベースのアプリケーションにとって便利です。
しかし、匿名性が本質的な特徴であるため、データの標準化やアイデンティティの認証には不足があります。
SBT（ソウルバウンドトークン）などの他のソリューションは中央集権型の証明者に依存し、データの改ざんを防げず、プライバシーを保護する形でデータを台帳に記録することもできません。

提案されている新しいソリューションは、Identity Aggregated NFTを用いてアイデンティティの問題を解決しようとしています。
これは、個人が認証したWeb2とWeb3のアイデンティティ情報をNFT（SBTも含む）に集約することを意味します。
この方法により、個人のアイデンティティはより広範囲にわたり、より安全で信頼性のある方法で異なるアプリケーション間で活用されることを目指しています。

# 仕様

## インターフェース

この規格に準拠する全てのコントラクトは、以下のインターフェースを全て実装する必要があります。

```javascript
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.15;

interface IERC7231 {

    /**
     * @notice emit the use binding informain
     * @param id nft id 
     * @param identitiesRoot new identity root
     */
    event SetIdentitiesRoot(
        uint256 id,
        bytes32 identitiesRoot
    );

    /**
     * @notice 
     * @dev set the user ID binding information of NFT with identitiesRoot
     * @param id nft id 
     * @param identitiesRoot multi UserID Root data hash
     * MUST allow external calls
     */
    function setIdentitiesRoot(
        uint256 id,
        bytes32 identitiesRoot
    ) external;

    /**
     * @notice 
     * @dev get the multi-userID root by  NFTID
     * @param id nft id 
     * MUST return the bytes32 multiUserIDsRoot
     * MUST NOT modify the state
     * MUST allow external calls
     */
    function getIdentitiesRoot(
        uint256 id
    ) external returns(bytes32);

    /**
     * @notice 
     * @dev verify the userIDs binding 
    * @param id nft id 
     * @param userIDs userIDs for check
     * @param identitiesRoot msg hash to veriry
     * @param signature ECDSA signature 
     * MUST If the verification is passed, return true, otherwise return false
     * MUST NOT modify the state
     * MUST allow external calls
     */
    function verifyIdentitiesBinding(
        uint256 id,address nftOwnerAddress,string[] memory userIDs,bytes32 identitiesRoot, bytes calldata signature
    ) external returns (bool);
}
```

---

### SetIdentitiesRoot

```javascript
event SetIdentitiesRoot(
    uint256 id,
    bytes32 identitiesRoot
);
```

**概要**

このイベントは、NFTのIDと新しいアイデンティティルートが関連付けられた時に発行されるイベント。

**詳細**

`SetIdentitiesRoot`イベントは、NFTの特定のIDに対して新しいアイデンティティ情報のルートが設定されたときに、その情報をブロックチェーン上に記録するために使用されます。
これにより、NFTに紐付けられたアイデンティティ情報の更新が外部に通知され、追跡可能になります。

**パラメータ**

- `id`
    - NFTの一意識別子。
- `identitiesRoot`
    - 新しく関連付けられたアイデンティティ情報のルートハッシュ値。

---

### `setIdentitiesRoot`

```javascript
function setIdentitiesRoot(
    uint256 id,
    bytes32 identitiesRoot
) external;
```

**概要**

NFTに対するユーザーIDバインディング情報を`identitiesRoot`に設定する関数。

**詳細**

この関数は、指定されたNFTのIDに対して、複数のユーザーID情報のハッシュである`identitiesRoot`を結び付けます。

**引数**

- `id`
    - NFTの一意識別子。
- `identitiesRoot`
    - バインドする複数のユーザーID情報のルートデータのハッシュ。

---

### `getIdentitiesRoot`

```javascript
function getIdentitiesRoot(
    uint256 id
) external returns(bytes32);
```

**概要**

指定されたNFT IDに紐づいている複数のユーザーIDのルートを取得する関数。

**詳細**

この関数は、指定したNFT IDに関連付けられた`identitiesRoot`を返します。
状態を変更せず、外部からの呼び出しを許可する必要があります。

**引数**

- `id`
    - NFTの一意識別子。

**戻り値**

- `bytes32`
    - 関連付けられた複数のユーザーIDのルートハッシュ値。

---

### `verifyIdentitiesBinding`

```javascript
function verifyIdentitiesBinding(
    uint256 id,
    address nftOwnerAddress,
    string[] memory userIDs,
    bytes32 identitiesRoot,
    bytes calldata signature
) external returns (bool);
```

**概要**

提供された情報を検証し、ユーザーIDがバインディングされているか確認する関数。

**詳細**

この関数は、NFTのID、NFTのオーナーのアドレス、チェックするためのユーザーIDの配列、`identitiesRoot`、そしてECDSA署名を引数として受け取り、これらの情報が正しくバインディングされているかを検証します。
検証が成功すれば`true`を、そうでなければ`false`を返します。
この関数は状態を変更せず、外部からの呼び出しが可能である必要があります。

**引数**

- `id`
    - NFTの一意識別子。
- `nftOwnerAddress`
    - NFTの所有者のアドレス。
- `userIDs`
    - 検証するユーザーIDの配列。
- `identitiesRoot`
    - メッセージハッシュを検証するための`identitiesRoot`。
- `signature`
    - ECDSA署名。

**戻り値**

- `bool`
    - 検証が成功したか否かの`bool`値。

---

## メタデータJSONスキーマ

```json
{
  "title": "Asset Metadata",
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "description": "Identifies the asset to which this NFT represents"
    },
    "description": {
      "type": "string",
      "description": "Describes the asset to which this NFT represents"
    },
    "image": {
      "type": "string",
      "description": "A URI pointing to a resource with mime type image"
    },
    "MultiIdentities": [
      {
        "userID": {
          "type": "string",
          "description": "User ID of Web2 and web3(DID)"
        },
        "verifierUri": {
          "type": "string",
          "description": "Verifier Uri of the userID"
        },
        "memo": {
          "type": "string",
          "description": "Memo of the userID"
        },
        "properties": {
          "type": "object",
          "description": "properties of the user ID information"
        }
      }
    ]
  }
}
```

上記のJSONは、NFTのメタデータを定義するためのスキーマです。
このスキーマは、NFTが表すアセットに関する情報を記述するために使用されます。

- `name`
    - NFTが代表するアセットの名前を識別します。
- `description`
    - NFTが代表するアセットについて説明します。
- `image`
    - mimeタイプが画像であるリソースを指すURIです。

さらに、このスキーマには`MultiIdentities`というフィールドが含まれており、これは複数のユーザーID情報を含む配列です。
各ユーザーID情報には以下のフィールドがあります。

- `userID`
    - **Web2**と**Web3**（DIDを含む）のユーザーIDです。
- `verifierUri`
    - ユーザーIDの検証者URIです。
- `memo`
    - ユーザーIDに関するメモです。
- `properties`
    - ユーザーID情報のプロパティを表すオブジェクトです。

このメタデータスキーマを利用することで、NFTに関連するユーザーの身元情報やその他の属性を、構造化された形で提供できるようになります。
それぞれのフィールドには、データの種類（例えば`string`や`object`など）と、そのデータが何を意味するのかを説明する`description`が含まれています。
これにより、NFTと関連するデータ間の明確な関連付けが可能になります。

# 補足

この新しい標準は、インターネットの旧来の形式（**Web2**）と新しいブロックチェーンベースの形式（**Web3**）の両方で使用される個人のIDを、より効果的に統合する方法を提供します。
具体的には、次のような解決策を提供します。

![Identity-aggregated-NFT-flow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570269/0286b679-5678-91e8-bd7f-084b7899b72b.png)


#### 1. **Web2**と**Web3**のアイデンティティ情報を1つにまとめる
「**MultiIdentities**」スキーマをNFTのメタデータに取り入れることで、個人の様々なアイデンティティ情報とNFTを正式に結び付けることができます。
これにより、異なる**Web2**プラットフォームの「**userID**」や、ブロックチェーン上で生成された分散型アイデンティティ（**DID**）をNFTに関連付け、個人の様々なアイデンティティを一つのNFT IDに集約することが可能になります。

:::note info
画像の右上の「**ERC-721 Metadata JSON Schema**」の部分を指しています。
個人に紐ずくさまざまな情報を格納したJSONデータをNFTに紐づけることを意味しています。
:::

#### 2. ユーザーが自身のデータの完全な所有権と制御権を持つ
ユーザーはメタデータを設定した後、`setIdentitiesRoot`関数を使って、自分のuserIDのハッシュとNFT IDとの間に安全な結びつきを作ることができます。
この結びつけを行う権限はユーザーだけが持つため、データがユーザーのものであることが確実になります。

:::note info
画像の真ん中上の「**Set Multi-Identities when modify the metadata**」の部分を指しています。
先ほど定義したJSONデータと自身のユーザーIDとを紐付ける処理をしています。
:::

#### 3. ERC1271に基づく署名を通じて、チェーン上とチェーン外のデータの関連性を検証
**ERC1271**基準の署名手法により、`verifyIdentitiesBinding`関数を通じて、チェーン上のデータ（NFTの所有権情報など）とチェーン外のデータ（ユーザーIDなど）との関連性を確認できます。これには以下のような検証ステップが含まれます。
    - NFTが誰のものであるかを確認する所有権の検証。
    - ユーザーIDが正しい形式であるかの検証。
    - IdentitiesRootが一貫しているかの検証。
    - NFTの所有者の署名が正しいかの検証。

:::note info
画像の真ん中下の「**Verify the binding relationship through verifierUri**」の部分を指しています。
この処理により、複数の検証を実行してチェーン内外のデータを紐付けることができます。
:::

この標準により、ユーザーは自分のアイデンティティ情報を安全に管理し、異なるアプリケーション間で信頼性の高いやり取りを行うことができるようになります。


# 後方互換性

# テスト

以下からテストファイルをダウンロードできます。

https://eips.ethereum.org/assets/eip-7231/test/erc7231.ts

テストコマンドは以下になります。

```bash
cd ../assets/eip-7231
npm install
npx hardhat test
```

# 参考実装

```javascript
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.17;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";

import "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol";
import { StrSlice, toSlice } from "@dk1a/solidity-stringutils/src/StrSlice.sol";


import "./interfaces/IERC7231.sol";


contract ERC7231 is IERC7231,ERC721 {

    using { toSlice } for string;
    mapping (uint256 => bytes32) _idMultiIdentitiesRootBinding;
    mapping(uint256 => mapping(bytes32 => bool)) internal _idSignatureSetting;

    constructor(string memory _name, string memory _symbol) ERC721(_name, _symbol) {}

    /**
     * @dev Checks if the sender owns NFT with ID tokenId
     * @param id   NFT ID of the signing NFT
     */
    modifier onlyTokenOwner(uint256 id) {
        //nft owner check
        require(ownerOf(id) == msg.sender,"nft owner is not correct");
        _;
    }


    /**
     * @notice 
     * @dev set the user ID binding information of NFT with multiIdentitiesRoot
     * @param id nft id 
     * @param multiIdentitiesRoot multi UserID Root data hash
     */
    function setIdentitiesRoot(
        uint256 id,
        bytes32 multiIdentitiesRoot
    ) external {

        _idMultiIdentitiesRootBinding[id] = multiIdentitiesRoot;
        emit SetIdentitiesRoot(id,multiIdentitiesRoot);
    }
    
    /**
     * @notice 
     * @dev Update the user ID binding information of NFT
     * @param id nft id 
     */
    function getIdentitiesRoot(
        uint256 id
    ) external view returns(bytes32){

        return _idMultiIdentitiesRootBinding[id];
    }

    /**
     * @notice 
     * @dev verify the userIDs binding 
     * @param id nft id 
     * @param multiIdentitiesRoot msg hash to veriry
     * @param userIDs userIDs for check
     * @param signature ECDSA signature 
     */
    function verifyIdentitiesBinding(
        uint256 id,address nftOwnerAddress,string[] memory userIDs,bytes32 multiIdentitiesRoot, bytes calldata signature
    ) external view returns (bool){

        //nft ownership validation
        require(ownerOf(id) == nftOwnerAddress,"nft owner is not correct");

        //multi-identities root consistency validation
        require(_idMultiIdentitiesRootBinding[id] == multiIdentitiesRoot,"");

        //user id format validtion
        uint256 userIDLen = userIDs.length;
        require(userIDLen > 0,"userID cannot be empty");

        for(uint i = 0 ;i < userIDLen ;i ++){
            _verifyUserID(userIDs[i]);
        }

        //signature validation from nft owner
        bool isVerified = SignatureChecker.isValidSignatureNow(msg.sender,multiIdentitiesRoot,signature);
        return isVerified;

    }

    /**
     * @notice 
     * @dev verify the userIDs binding 
     * @param userID nft id 
     */
    function _verifyUserID(string memory userID) internal view{

        require(bytes(userID).length > 0,"userID can not be empty");

        //first part(encryption algorithm or did) check
        string memory strSplit = ":";
        bool found;
        StrSlice left;
        StrSlice right = userID.toSlice();
        (found, left, right) = right.splitOnce(strSplit.toSlice());
        require(found,"the first part delimiter does not exist");
        require(bytes(left.toString()).length > 0,"the first part does not exist");

        //second part(Organization Information) check
        (found, left, right) = right.splitOnce(strSplit.toSlice());
        require(found,"the second part delimiter does not exist");
        require(bytes(left.toString()).length > 0,"the second part does not exist");

        //id hash check
        require(bytes(right.toString()).length == 64,"id hash length is not correct");

    }

    /**
     * @dev ERC-165 support
     */
    function supportsInterface(
        bytes4 interfaceId
    ) public view virtual override returns (bool) {
        return
            interfaceId == type(IERC7231).interfaceId ||
            super.supportsInterface(interfaceId);
    }

}
```


# セキュリティ考慮事項

このEIP標準を使うと、個人は自分自身のアイデンティティやウォレット、それに関連するデータを完全に自分で管理できるようになります。
具体的には、自分自身でNFTをウォレットに追加したり、自分に関する情報をNFTから削除したりすることができます。
これは、デジタルの身分証明や財産のようなものを、自分の意志で安全にコントロールできるということを意味します。

たとえば、あなたが新しいサービスを利用するときに、自分の身元情報を含むNFTを自分のウォレットに追加して、そのサービスにログインします。
後でそのサービスを使わなくなったときには、その情報を自分のウォレットから削除することもできます。
このプロセス全体をユーザー自身が行うため、第三者に情報が漏れる心配がありません。
このようにして、ユーザーは自分のデジタル情報をより自由に、かつプライバシーを保ちながら使用できるようになるのです。

# 引用

Chloe Gu <chloe@carv.io>, Navid X. (@xuxinlai2002), Victor Yu <victor@carv.io>, Archer H., "ERC-7231: Identity-aggregated NFT [DRAFT]," Ethereum Improvement Proposals, no. 7231, June 2023. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7231.

# 最後に

今回は「Web2とWeb3のアイデンティティを、個人の認可によりNFTに集約する仕組みを提案している規格である**ERC7231**」についてまとめてきました！
いかがだったでしょうか？

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

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

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

https://chaldene.net/

https://zenn.dev/heku

https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58


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