はじめに

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

今回は、オンチェーンで証明可能なデータを管理・証明するレジストリシステムの仕組みを提案しているERC7812についてまとめていきます!

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

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

概要

ERC7812は、証明可能な情報をオンチェーンで保存し、後からその正当性を検証できるレジストリシステムを提案しています。 ユーザーは、このレジストリを利用して自身の秘密情報に関するコミットメント(改ざん不可能な証拠)を記録し、後でゼロ知識証明を用いてその情報が正しいことを証明できます。 この時、証明の過程で情報の具体的な内容を開示する必要はありません。

また、開発者はこのレジストリを活用し、特定の用途に合わせたビジネス向けの管理システムを組み込むことで、特定の情報の登録・処理を行えるようになります。 そのための基盤として、Ethereum上に「EvidenceRegistry」コントラクトがデプロイされており、アドレスは0x781268D46a654D020922f115D75dd3D56D287812です。

このレジストリを利用することで、異なるプロジェクトやプロトコルが統一されたフォーマットで証明可能な情報を保存・利用できるようになり、セキュリティやプライバシーの向上が期待されます。

動機

ERC7812は、「証明可能な情報の保存と発行の仕組みを定義して標準化されたオンチェーンのレジストリに集約することで、より多くのプロトコルが活用できるようにする」という目的があります。

現在、ゼロ知識証明を活用したプライバシー重視の仕組みは多数存在しますが、それぞれが独立して運用されているため、情報が分散して決まった形式で管理・活用することが難しいです。 そこで、ERC7812では証明可能な情報を一括管理できる仕組みを提供することで、再利用しやすく相互運用性やセキュリティを向上させることを目指しています。

また、このレジストリは汎用性が高く、特定のユースケースに依存しない設計になっています。 そのため、ID認証、reputation管理、出席証明など、さまざまな用途に柔軟に適用することが可能です。 これにより、各プロジェクトは最小限の制約で独自のプロトコルを構築できるようになります。

このEIPは、あくまで基盤を提供するものであり、具体的な情報の形式やコミットメントの構造については別のEIPとして定義されることを想定しています。

仕様

定義

Sparse Merkle Tree (SMT) とは

Sparse Merkle Treeは、keyとvalueのペアを特定の場所に格納するための特殊なマークルツリーです。 このツリーは、決定論的かつ冪等性を持つ方法でデータを保存します。

決定論的とは、同じ入力を与えれば常に同じ結果が得られる性質のことを指します。 冪等性とは、同じ操作を何度行っても結果が変わらない性質のことです。 この性質を持つことで、データの整合性を確保しながら効率的にデータの証明を行うことができます。

Sparseという言葉は「まばらな」という意味を持ち、通常のマークルツリーと異なり、SMTではツリー内の大部分のノードが空(null)の状態になります。 この特徴により、大量のデータを格納せずにツリー全体を管理することができます。

また、SMTではハッシュ関数を活用してデータの一意性を保証します。 特に、ゼロ知識証明との互換性を高めるためにPoseidonハッシュ関数がよく使用されます。 Poseidonは、通常のハッシュ関数(SHA-256など)とは異なり、ZK回路での計算に最適化した設計になっており計算コストを抑えることができます。

ステートメントとは

ステートメントとは、ある事実や証拠を表現するための構造化されたデータです。 文字列(テキスト情報)として表現されることもあれば、Sparse Merkle Tree(SMT)のルートハッシュ(Merkle Root)として表現されることもあります。

例えば、「この人物は特定のイベントに参加した」という情報を証明する場合、単純なテキスト(例: "Alice attended Event X")として記録することもできますが、この情報をハッシュ化してSMTのルートに格納するという、より安全で効率的な管理が可能です。 これにより、データの改ざんを防ぎつつ効率的に検証できるようになります。

コミットメントとは

コミットメントとは、ステートメントを直接公開せずに正当性を証明するために利用される特別な公開値です。

具体的には、ステートメントをそのまま保存するのではなく、「ブラインディング」と呼ばれる手法を用いて隠した値を公開することで、元の情報を秘匿しながらその正当性を証明できるようにします。

ブラインディングとは、データにランダムな値を加えることで元のデータを推測できないようにする技術です。 これにより、コミットメントを見ただけでは元のステートメントを知ることはできませんが、ゼロ知識証明を用いることでコミットメントが正しいことを証明できます。

例えば、「この人は資格試験に合格した」という情報を証明したい場合、そのまま公開するとプライバシーが問題となります。 しかし、この情報をブラインディングしてコミットメントとして登録すれば、「この人は資格試験に合格した」という事実を第三者に証明でき、具体的な資格名や合格日などの詳細は伏せることができます。

コミットメントキーとは

コミットメントキーとは、ステートメントをブラインディングする時に使用される秘密の値です。

コミットメントを作成する時には、ステートメント単体ではなくステートメントに秘密のランダムな値(solt)を加えてハッシュ化します。 この時に使用される秘密の値をコミットメントキーと呼びます。

このコミットメントキーが漏れると元のステートメントが推測される可能性があるため、コミットメントキーは厳重に管理する必要があります。

例えば、「あるユーザーが特定の契約に署名した」という情報をコミットメントとして登録する場合、単に「署名しました」というデータをそのまま記録すると誰でも内容を確認できます。 しかし、コミットメントキーを利用してデータをブラインディングすることで、元の契約内容を公開することなく署名の事実だけを証明することが可能になります。

概要

このオンチェーンレジストリシステムは、「EvidenceRegistry」と「EvidenceDB」、「Registrar」の2つのサブシステムで構成されています。 ERC7812では、EvidenceRegistryとEvidenceDBの設計と標準化に焦点を当てています。

EvidenceRegistryは、このプロトコル全体の入口となるコントラクトです。 このRegistryを通じて、任意の32バイトのデータをEvidenceDBというオンチェーンデータベースに保存し、後でそのデータを必要に応じて証明できるようになります。 一方、Registrarは特定のデータ構造を定め、そのデータをEvidenceRegistryを通じてEvidenceDBに格納する役割を担います。

例えば、資格証明のようなユースケースを考えると、Registrarは「あるユーザーが特定の試験に合格した」という情報を管理し、それをハッシュ化してEvidenceDBに記録します。 このデータが正しく記録されているかどうかを検証する時には、ゼロ知識証明やマークル証明を用いることができます。

Merkle証明を用いたデータの検証

EvidenceDBに保存されたデータが存在するかしないかを証明するためには、Merkle証明を使用します。

Merkle証明とは、あるデータが特定のMerkleツリーの一部であることを証明する暗号学的手法です。

あるデータがEvidenceDB内にあることを証明したい場合、Merkleツリーのルートハッシュを用いて特定のデータがそのツリー内に含まれていることを証明できます。 逆に、「このデータはEvidenceDBに登録されていない」ことを証明する場合も、Merkle証明を使うことができます。

この仕組みを利用することで、ユーザーはゼロ知識証明を使用して元のデータを開示することなく「ある情報が存在する」「ある情報が存在しない」ことを証明できます。 例えば、特定の資格を持っていることを証明したい場合、その資格の情報を直接公開するのではなく、証明書がEvidenceDBに登録されているかどうかをMerkle証明を用いて第三者に証明することが可能になります。

RegistrarとオンチェーンZK検証

RegistrarがどのようにデータをEvidenceDBに保存しているかを理解することで、オンチェーンでゼロ知識証明を使ったデータの検証を行うZKバリデータを実装できます。 この検証は、Circom(ZK証明を作成するためのDSL:ドメイン固有言語)や他の技術スタックを利用して実装可能です。

このように、オンチェーンレジストリシステムはEvidenceRegistryとEvidenceDBを基盤とし、Registrarを通じてカスタマイズ可能なデータ管理を実現します。 これにより、異なるプロジェクトが共通の基盤の上でデータの証明と検証を行えるようになり再利用性や相互運用性の向上が期待できます。

EvidenceDB

EvidenceDBは、証明可能なkeyとvalueを管理するデータベースとして機能するコントラクトです。 このデータベースは、データの追加・更新・削除の機能が必要である、どの操作を行っても冪等性を担保する必要があります。

また、EvidenceDBに採用されるデータ構造はデータが含まれていることと含まれていないことを証明できる必要があります。 この証明にはMerkleツリーが使用されることが多く、特にSparse Merkle Tree(SMT)を基盤とした設計が想定されています。

Proof構造体

EvidenceDBでは、データの証明を行うためにProof(証明)構造体を定義しています。 この構造体は、特定のデータがMerkleツリーの一部であるかどうかを証明するための以下の情報を保持します。

  • root
    • Merkleツリーのルートハッシュ。
    • これはツリー全体の状態を表すハッシュ値で、ツリー内のデータが変更されるたびに変わります。
  • siblings
    • Merkleツリーにおける兄弟ノードのハッシュ値のリスト。
    • これを利用してMerkleルートを再構築し、データの存在を検証できます。
  • existence
    • 証明対象のデータがツリー内に存在する場合はtrue、存在しない場合はfalse。
  • key
    • 検証対象となるデータのキー。
  • value
    • 検証対象となるデータの値。
  • auxExistence
    • 補助ノード(auxiliary node)が存在するかどうかを示すフラグ。
  • auxKey
    • 補助ノードのキー。
  • auxValue
    • 補助ノードの値。

この構造体を利用することで、特定のデータがEvidenceDB内にあるかどうかを証明可能になります。

EvidenceDBの主要な関数

  • add(bytes32 key, bytes32 value)

新しいデータをSMTに追加する関数。 指定したkeyに対してvalueを格納します。

  • remove(bytes32 key)

指定したキーに対応するデータを削除する関数。 冪等性を維持するため、削除後の状態はデータが追加される前と同じある必要があります。

  • update(bytes32 key, bytes32 newValue)

既存のデータの値を更新する関数。 新しい値を指定し、ツリー内の対応するノードを更新します。

  • getRoot()

現在のMerkleツリーのルートハッシュを取得する関数。 ただし、ルートハッシュを取得した直後に他のトランザクションが発生し、値が変わる可能性があるため(フロントランニングのリスク)、この関数をオンチェーンで使用しないほうが良いです。

  • getSize()

EvidenceDB内のデータのノード数を取得する関数。

  • getMaxHeight()

SMTの最大高さ(Merkle証明で使用する枝の数)を取得する関数。 ツリーの高さは、保存されているデータの総量によって決まります。

  • getProof(bytes32 key)

指定したキーに対するMerkle証明を取得する関数。 この証明を利用することで、特定のデータがツリー内に含まれているかどうかを検証できます。

  • getValue(bytes32 key)

指定したキーに対応する値を取得する関数。 EvidenceDB内にデータが存在する場合、その値を返します。

Evidence Registry

Merkle Rootの管理

EvidenceRegistryは、Merkle Rootの変更を追跡して更新されるたびにイベントを発行する必要があります。 Merkle Rootは、現在のEvidenceDBの状態を表すハッシュ値であり、EvidenceDB内のデータが変更されるたびに新しいルートハッシュが生成されます。

RootUpdatedイベントは、過去のルート(prev)と新しいルート(curr)の両方の値を記録し、ルートの更新が正しく行われたことを証明します。 このイベントにより、オフチェーンからEvidenceDBの履歴を追跡できるようになります。

ステートメントの追加、削除、更新

  • addStatement(bytes32 key, bytes32 value)

新しいステートメントをEvidenceDBに追加する関数。 このとき、ステートメントのキー(key)がすでに存在する場合は、トランザクションが失敗します。

  • removeStatement(bytes32 key)

登録済みのステートメントをEvidenceDBから削除する関数。 このとき、削除しようとするキーが存在しない場合は、トランザクションが失敗します。

  • updateStatement(bytes32 key, bytes32 newValue)

既存のステートメントを新しい値に更新する関数。 このとき、更新しようとするキーが存在しない場合は、トランザクションが失敗します。

ステートメントキーの隔離

EvidenceRegistryは、msg.senderごとに独立したnamespaceを確保する必要があります。 これは、異なるユーザーが同じキーを使用しても、データの衝突が発生しないようにするためです。

キーは次のように変換されます。

js
bytes32 isolatedKey = hash(msg.sender, key)

ここで使用するハッシュ関数は、プロトコル全体で安全性が保証されたものを採用する必要があります。 これにより、同じkeyを異なるユーザーが使用しても、それぞれ異なるisolatedKeyが生成され、独立したデータとして管理されます。

例えば、0xABC...というアドレスのユーザーが"user_identity"というキーを使ってデータを登録した場合、そのisolatedKeyは以下のようになります。

bash
isolatedKey = hash(0xABC..., "user_identity")

一方、0xDEF...という別のアドレスのユーザーが同じ"user_identity"というキーを使用しても、isolatedKeyは異なるものになります。

この設計により、異なるユーザーが同じキーを利用しても、それぞれのデータが独立して管理されるため安全性とデータの一貫性が確保されます。

Merkle Rootの履歴管理

EvidenceRegistryは、EvidenceDBのMerkle Rootの履歴を管理する必要があります。 これにより、過去のデータの状態を検証できるようになります。

  • getRootTimestamp(bytes32 root)

特定のMerkle Rootの作成時刻を取得する関数。 この関数は、存在しないルートを指定された場合は0を返します。
最新のルートを指定した場合は、ブロックのタイムスタンプ(block.timestamp)を返します。

これにより、EvidenceRegistryは、どの時点でどのMerkle Rootが有効だったのかを追跡できるようになります。

ハッシュ関数

ハッシュ関数の選定と要件

EvidenceRegistryとEvidenceDBの両方で同じハッシュ関数を使用する必要があります。 これは、データの一貫性を保ち証明プロセスが正しく機能するようにするためです。 異なるハッシュ関数を使用すると、同じデータに対して異なるハッシュ値が生成されてデータの整合性が崩れます。

ERC7812では、ゼロ知識証明と互換性のある「ZKフレンドリーなハッシュ関数」が推奨されています。 特に、Poseidonハッシュ関数の使用が推奨されています。

ZKフレンドリーなハッシュ関数とは

ZKフレンドリーなハッシュ関数とは、ゼロ知識証明の回路内での計算が効率的に行えるように設計されたハッシュ関数のことです。 一般的なハッシュ関数(SHA-256やKeccak-256など)は、ZK証明を作成する時に高い計算コストがかかるため証明の生成に時間とリソースが必要になります。

Poseidonハッシュ関数は、ZK証明の効率を考慮して設計されており、証明の生成にかかる計算コストを大幅に削減できます。 このため、EvidenceRegistryやEvidenceDBがZK証明と連携する場合には、Poseidonを採用することでデータの証明プロセスがスムーズになります。

ZKフレンドリーなハッシュ関数の制約

ZKフレンドリーなハッシュ関数、特にPoseidonを採用する場合、EvidenceRegistryはkeyやvalueのサイズが楕円曲線の素数有限体を超えないように制約する必要があります。

具体的には、BN128(Barreto-Naehrig 128-bit Curve)という楕円曲線を使用する場合、データのサイズは以下の上限を超えてはいけません。

text
21888242871839275222246405745257275088548364400416034343698204186575808495617

この数値は、BN128の素数有限体の上限値を表しており、これを超える値をハッシュ関数に入力すると計算結果がフィールド内に収まらず正しく動作しなくなる可能性があります。

例えば、keyやvalueとして256ビット(32バイト)のデータを扱う場合、その数値表現がこの素数より大きくならないようにする必要があります。

補足

ERC7812を設計する時、各プロトコルが独自のレジストリを持つ方式と、全てのプロトコルが統一された単一のレジストリを共有する方式の2つのアプローチを検討しました 最終的に、単一のレジストリを採用する方式を選択したのは、以下のような利点があるためです。

クロスチェーンでの移植性

単一のレジストリを採用することで、異なるブロックチェーン間でデータを検証する時に送信するデータ量を最小限に抑えることができます。 具体的には、SMT(Sparse Merkle Tree)のルートハッシュ(bytes32型の値)を1つ送信するだけで、レジストリの状態を証明することが可能になります。

もし各プロトコルが独自のレジストリを持っていた場合、チェーンごとに異なるデータ構造が使われる可能性があり、相互運用が難しくなります。

信頼の集中化

全てのプロトコルが共通のレジストリを使用することで、ユーザーは単一のコントラクトを信頼すればよくなります。

もし各プロトコルが独自のレジストリを持っていた場合、ユーザーは各レジストリの信頼性を個別に検証しなければならずリスクが増大します。 一方、単一のレジストリを使用することで、信頼の基盤が統一されて透明性とセキュリティが向上します。

統合の簡素化

単一のレジストリを採用することで、全てのプロトコルが統一されたシステムインターフェース、ハッシュ関数、証明の構造を使用できるようになります。 これにより、新しいプロジェクトがこのシステムを統合する時の開発コストが削減されてより迅速に利用できるようになります。

もし各プロトコルが独自のレジストリを持っていた場合、それぞれのレジストリで異なるデータ構造や証明方式が使われる可能性があり、統合の手間が増えてシステムの互換性が低下するリスクがあります。

汎用性

ERC7812は、特定のビジネスユースケースに依存せず、幅広い用途に対応できるようにしています。

例えば、以下のような異なる用途のために、カスタムのレジストリを開発することが可能です。

  • 国家発行のパスポート情報をオンチェーンで管理
  • EIP4337を活用したプライバシー保護型アカウント管理
  • POAP(Proof of Attendance Protocol:イベント参加証明)の管理
    このように、単一のレジストリをベースにしながらも、各プロジェクトが独自のレジストラを実装して必要に応じたデータの証明と管理ができる設計になっています。

EvidenceDBのnamespace設計

EvidenceDBでは、データの書き込みアクセスを制御するためにnamespaceを用いています。

この設計により、発行者(issuer)以外の第三者が他人のデータを変更することができないようになっています。 例えば、ある認証機関が証明データを発行した場合、そのデータは発行者のみが管理できて他のユーザーが勝手に改ざんできません。

ただし、この仕組みは、アクセス制御の責任をレジストラ側に委ねることになります。 つまり、各レジストラがどのようなアクセス制御を行うかが重要です

Merkle Rootの履歴管理

EvidenceRegistryは、最低限のガス消費を考慮したMerkle Rootの履歴をオンチェーンに保持するように設計されています。 これにより、レジストラがEvidenceRegistryを統合する時の負担を軽減します。

ただし、より詳細な履歴が必要な場合は、オフチェーンサービスを利用して「RootUpdatedイベント」を解析することが推奨されています。

例えば、過去の全履歴を保持したい場合は、オフチェーンのデータベースを用いてEvidenceRegistryのイベントを監視し、過去のMerkle Rootを保存しておくことでてより詳細なデータ管理が可能になります。

互換性

互換性の問題はありません。

デプロイ方法

EvidenceRegistryは、0x781268D46a654D020922f115D75dd3D56D287812にアドレスが固定されています。 このアプローチにより、全てのプロトコルが同じアドレスを参照できるため、一貫性を持ったデータの証明が可能になります。

このコントラクトのデプロイには、「deterministic deployment proxy」が使用されています。 これは、特定の条件(deployerアドレス、デプロイ時のsalt値)を指定することで、常に同じアドレスにコントラクトをデプロイできる技術**です。

このデプロイメントプロキシは、0x4e59b44847b379578588920ca78fbf26c0b4956cから実行されており、デプロイ時に使用されたsalt値は以下です。

text
0x9a533395526948e0860194b5dbd307de82d332d7fb268e02659096f3c904bf9f

以下のcreate2などを使用していると思います。

参考実装

SolidityおよびCircomでのSMT実装

EvidenceRegistryとEvidenceDBの基盤として、SolidityとCircomの両方でSparse Merkle Tree(SMT)の低レベル実装が提供されています。

この実装により、EvidenceDBが効率的に機能するようになり、各ステートメントがマークルツリー構造の中で管理されます。 特に、Circomの実装はコントラクト外でZK証明を作成するために使用され、証明を行うプロセスを最適化します。

また、SMTの高さ(height)は80に設定されています。 これは、ツリー内の最大データ数を制御し、証明の計算量を適切に管理するためのものです。 ツリーの高さが高すぎると、証明のコストが増加するため、バランスの取れた値として80が選ばれています。

依存ライブラリ

  • @openzeppelin/contracts v5.1.0
  • circomlib v2.0.5

EvidenceDB

js
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.21;

import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol";

import {IEvidenceDB} from "./interfaces/IEvidenceDB.sol";

import {SparseMerkleTree} from "./libraries/SparseMerkleTree.sol";
import {PoseidonUnit2L, PoseidonUnit3L} from "./libraries/Poseidon.sol";

contract EvidenceDB is IEvidenceDB, Initializable {
    using SparseMerkleTree for SparseMerkleTree.SMT;

    address private _evidenceRegistry;

    SparseMerkleTree.SMT private _tree;

    modifier onlyEvidenceRegistry() {
        _requireEvidenceRegistry();
        _;
    }

    function __EvidenceDB_init(address evidenceRegistry_, uint32 maxDepth_) external initializer {
        _evidenceRegistry = evidenceRegistry_;

        _tree.initialize(maxDepth_);

        _tree.setHashers(_hash2, _hash3);
    }

    /**
     * @inheritdoc IEvidenceDB
     */
    function add(bytes32 key_, bytes32 value_) external onlyEvidenceRegistry {
        _tree.add(key_, value_);
    }

    /**
     * @inheritdoc IEvidenceDB
     */
    function remove(bytes32 key_) external onlyEvidenceRegistry {
        _tree.remove(key_);
    }

    /**
     * @inheritdoc IEvidenceDB
     */
    function update(bytes32 key_, bytes32 newValue_) external onlyEvidenceRegistry {
        _tree.update(key_, newValue_);
    }

    /**
     * @inheritdoc IEvidenceDB
     */
    function getRoot() external view returns (bytes32) {
        return _tree.getRoot();
    }

    /**
     * @inheritdoc IEvidenceDB
     */
    function getSize() external view returns (uint256) {
        return _tree.getNodesCount();
    }

    /**
     * @inheritdoc IEvidenceDB
     */
    function getMaxHeight() external view returns (uint256) {
        return _tree.getMaxDepth();
    }

    /**
     * @inheritdoc IEvidenceDB
     */
    function getProof(bytes32 key_) external view returns (Proof memory) {
        return _tree.getProof(key_);
    }

    /**
     * @inheritdoc IEvidenceDB
     */
    function getValue(bytes32 key_) external view returns (bytes32) {
        return _tree.getNodeByKey(key_).value;
    }

    /**
     * @notice Returns the address of the Evidence Registry.
     */
    function getEvidenceRegistry() external view returns (address) {
        return _evidenceRegistry;
    }

    function _requireEvidenceRegistry() private view {
        if (_evidenceRegistry != msg.sender) {
            revert NotFromEvidenceRegistry(msg.sender);
        }
    }

    function _hash2(bytes32 element1_, bytes32 element2_) private pure returns (bytes32) {
        return PoseidonUnit2L.poseidon([element1_, element2_]);
    }

    function _hash3(
        bytes32 element1_,
        bytes32 element2_,
        bytes32 element3_
    ) private pure returns (bytes32) {
        return PoseidonUnit3L.poseidon([element1_, element2_, element3_]);
    }
}

EvidenceRegistry

js
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.21;

import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol";

import {IEvidenceDB} from "./interfaces/IEvidenceDB.sol";
import {IEvidenceRegistry} from "./interfaces/IEvidenceRegistry.sol";

import {PoseidonUnit2L} from "./libraries/Poseidon.sol";

contract EvidenceRegistry is IEvidenceRegistry, Initializable {
    uint256 public constant BABY_JUB_JUB_PRIME_FIELD =
        21888242871839275222246405745257275088548364400416034343698204186575808495617;

    IEvidenceDB private _evidenceDB;

    mapping(bytes32 => uint256) private _rootTimestamps;

    modifier onlyInPrimeField(bytes32 key) {
        _requireInPrimeField(key);
        _;
    }

    modifier onRootUpdate() {
        bytes32 prevRoot_ = _evidenceDB.getRoot();
        _rootTimestamps[prevRoot_] = block.timestamp;
        _;
        emit RootUpdated(prevRoot_, _evidenceDB.getRoot());
    }

    function __EvidenceRegistry_init(address evidenceDB_) external initializer {
        _evidenceDB = IEvidenceDB(evidenceDB_);
    }

    /**
     * @inheritdoc IEvidenceRegistry
     */
    function addStatement(
        bytes32 key_,
        bytes32 value_
    ) external onlyInPrimeField(key_) onlyInPrimeField(value_) onRootUpdate {
        bytes32 isolatedKey_ = _getIsolatedKey(key_);

        if (_evidenceDB.getValue(isolatedKey_) != bytes32(0)) {
            revert KeyAlreadyExists(key_);
        }

        _evidenceDB.add(isolatedKey_, value_);
    }

    /**
     * @inheritdoc IEvidenceRegistry
     */
    function removeStatement(bytes32 key_) external onlyInPrimeField(key_) onRootUpdate {
        bytes32 isolatedKey_ = _getIsolatedKey(key_);

        if (_evidenceDB.getValue(isolatedKey_) == bytes32(0)) {
            revert KeyDoesNotExist(key_);
        }

        _evidenceDB.remove(isolatedKey_);
    }

    /**
     * @inheritdoc IEvidenceRegistry
     */
    function updateStatement(
        bytes32 key_,
        bytes32 newValue_
    ) external onlyInPrimeField(key_) onlyInPrimeField(newValue_) onRootUpdate {
        bytes32 isolatedKey_ = _getIsolatedKey(key_);

        if (_evidenceDB.getValue(isolatedKey_) == bytes32(0)) {
            revert KeyDoesNotExist(key_);
        }

        _evidenceDB.update(isolatedKey_, newValue_);
    }

    /**
     * @inheritdoc IEvidenceRegistry
     */
    function getRootTimestamp(bytes32 root_) external view returns (uint256) {
        if (root_ == bytes32(0)) {
            return 0;
        }

        if (root_ == _evidenceDB.getRoot()) {
            return block.timestamp;
        }

        return _rootTimestamps[root_];
    }

    function getEvidenceDB() external view returns (address) {
        return address(_evidenceDB);
    }

    function _getIsolatedKey(bytes32 key_) internal view returns (bytes32) {
        return PoseidonUnit2L.poseidon([bytes32(uint256(uint160(msg.sender))), key_]);
    }

    function _requireInPrimeField(bytes32 key_) private pure {
        if (uint256(key_) >= BABY_JUB_JUB_PRIME_FIELD) {
            revert NumberNotInPrimeField(key_);
        }
    }
}

EvidenceRegistry Verifier

js
// LICENSE: CC0-1.0
pragma circom 2.1.9;

include "SparseMerkleTree.circom";

template BuildIsolatedKey() {
    signal output isolatedKey;

    signal input address;
    signal input key;

    component hasher = Poseidon(2);
    hasher.inputs[0] <== address;
    hasher.inputs[1] <== key;

    hasher.out ==> isolatedKey;
}

template EvidenceRegistrySMT(levels) {
    // Public Inputs
    signal input root;

    // Private Inputs
    signal input address;
    signal input key;

    signal input value;

    signal input siblings[levels];

    signal input auxKey;
    signal input auxValue;
    signal input auxIsEmpty;

    signal input isExclusion;

    // Build isolated key
    component isolatedKey = BuildIsolatedKey();
    isolatedKey.address <== address;
    isolatedKey.key <== key;

    // Verify Sparse Merkle Tree Proof
    component smtVerifier = SparseMerkleTree(levels);
    smtVerifier.siblings <== siblings;

    smtVerifier.key <== isolatedKey.isolatedKey;
    smtVerifier.value <== value;

    smtVerifier.auxKey <== auxKey;
    smtVerifier.auxValue <== auxValue;
    smtVerifier.auxIsEmpty <== auxIsEmpty;

    smtVerifier.isExclusion <== isExclusion;

    smtVerifier.root <== root;
}

component main {public [root]} = EvidenceRegistrySMT(80);

セキュリティ

レジストラの管理と証明機能

各レジストラは、ステートメント(証明可能なデータ)の管理および証明機能の両方を提供する必要があります。 特に、ゼロ知識証明を活用してデータの真正性を証明する場合、ZK証明の検証システムが適切に設定されていないと悪意のある証明が通ってしまう可能性があります。

ゼロ知識証明を用いたシステムでは、「trusted setup」と呼ばれる初期設定が必要になる場合があります。 これは、証明回路が動作するための公開パラメータを生成するプロセスです。 この設定が適切に行われないと、攻撃者が秘密鍵を生成できてしまう可能性があり、偽の証明を作成して不正なデータを正当なものとして認識させることができてしまいます。

したがって、レジストラの実装者はtrusted setupの安全性を確保し、第三者による監査を受けることが推奨されます。 また、trusted setupを必要としない証明システム(STARKsなど)を検討することも1つの選択肢です。

getRoot メソッドのオンチェーン利用のリスク

EvidenceDBには、getRoot関数があり、現在のMerkle Rootを取得できます。 しかし、この関数をオンチェーンで使用してデータベースの状態を検証することは推奨されていません。

その理由として、「フロントランニング」が発生するリスクがあるためです。

具体的には、getRootを使って最新のルートを取得してそのデータを基に何らかの処理を行うレジストラが存在した場合、攻撃者はその直前にデータを変更するトランザクションを送信して最新のルートを意図的に書き換えることが可能になります。 その結果、レジストラが意図しない無効なデータを処理してしまう可能性があります。

この問題を回避するために、レジストラはgetRootを直接使用するのではなく、関数の引数として適切なルートを渡してそれをgetRootTimestampメソッドを使って検証する必要があります。

getRootTimestampを使用すると、指定したMerkle Rootの作成時刻を取得できるため、攻撃者がフロントランニングを行って不正なルートを作成しても意図したルートと異なることを検出できます。

このように、適切なメソッドを使用することで、攻撃者による不正なデータのすり替えを防いでレジストリシステムの安全性を確保することができます。

引用

Artem Chystiakov (@arvolear) artem@rarilabs.com, Oleksandr Kurbatov oleksandr@rarilabs.com, Yaroslav Panasenko yaroslav@rarilabs.com, Michael Elliot (@michaelelliot) mike@zkpassport.id, Vitalik Buterin (@vbuterin), "ERC-7812: ZK Identity Registry [DRAFT]," Ethereum Improvement Proposals, no. 7812, November 2024. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7812.

最後に

今回は「オンチェーンで証明可能なデータを管理・証明するレジストリシステムの仕組みを提案しているERC7812」についてまとめてきました! いかがだったでしょうか?

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

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