# [ERC7992] ゼロ知識証明でMLモデルの推論結果をオンチェーンで検証するZKMLの仕組みを理解しよう！

> ERC7992は、機械学習（ML）モデルの推論結果をオンチェーンで暗号的に検証するための標準インターフェースです。

- URL: https://cardene.dev/articles/c8711d2ef575f9037d8d
- 公開日: 2026-03-17
- 更新日: 2026-03-17
- トピック: Blockchain
- タグ: ethereum, solidity, smartcontract, ブロックチェーン

## はじめに

『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

今回は、ゼロ知識証明を使ってMLモデルの推論結果をオンチェーンで検証できる標準インターフェースを提案している**ERC7992**についてまとめていきます！

:::message
以下にまとめられているものを解説しながらまとめていきます。
:::

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

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

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

## 概要

**ERC7992**は、機械学習（ML）モデルの推論結果をオンチェーンで暗号的に検証するための標準インターフェースです。
ZKML（Zero-Knowledge Machine Learning）と呼ばれるアプローチを使い、あるモデルが特定の入力に対して特定の出力を返したことを、ゼロ知識証明で証明・検証します。

:::message
**ゼロ知識証明**

「ある事実を知っている」ことを、その事実の中身を明かさずに証明する暗号技術です。
例えば「私はこのパスワードを知っています」と、パスワード自体を見せずに証明できます。
:::

この提案は大きく2つのコンポーネントで構成されています。
1つ目は**レジストリ**で、MLモデルの重み・アーキテクチャ・証明回路・検証鍵のハッシュをオンチェーンに登録します。
2つ目は**検証器**で、提出されたゼロ知識証明を検証し、推論結果の正しさを確認します。

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

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

以下の図は、ZKMLの全体的な処理フローを示しています。

![ZKMLの全体フロー](/images/qiita-39d7d53a-a272-4db8-885d-f827dedb9683.webp)

オフチェーンでMLモデルが推論を実行し、その結果と一緒にゼロ知識証明を生成します。
生成された証明はオンチェーンの検証器に提出され、レジストリに登録されたモデルのコミットメントと照合して検証されます。

**ERC7992**準拠のコントラクトは**ERC165**を実装し、対応するインターフェースの検出を可能にします。

## 動機

#### スマートコントラクトとAIの信頼問題

スマートコントラクトは大規模なMLモデルを直接実行することができません。
ガスコストやEVMの計算能力の制約から、数百万パラメータを持つようなモデルをオンチェーンで動かすのは現実的ではありません。

そのため、現在のプロジェクトは以下の3つのアプローチのいずれかを採用していますが、どれも暗号的な保証を提供できていません。

| アプローチ | 内容 | 問題点 |
|-----------|------|--------|
| 中央集権サーバーの信頼 | オフチェーンのサーバーが推論結果を返す | サーバーが嘘をついても検出できない |
| モデルの簡略化 | モデルを極端に小さくしてオンチェーンで実行 | 精度が大幅に低下し実用に耐えない |
| 社会的委員会 | 複数の関係者が結果の正しさを合意する | 合意コストが高く、暗号的保証がない |

例えば、DeFiプロトコルでAIによるリスク評価を使いたい場合、その推論結果が本当に特定のモデルから正しく計算されたものかを検証する手段がないと、不正な結果に基づいて資金が動いてしまうリスクがあります。

#### ZKMLの標準化が必要な理由

ZKML（Zero-Knowledge Machine Learning）は、ゼロ知識証明を使ってこの信頼のギャップを埋めます。
証明者は「特定のモデルが特定の入力に対して特定の出力を返した」ことを、モデルの重みや入力データを公開せずに証明できます。

しかし、標準インターフェースがないと各dAppと検証器の組み合わせがすべて独自実装になります。
異なるABI、異なるレジストリスキーマ、異なるイベント定義が乱立し、相互運用性が失われます。

**ERC7992**はこの問題を解決し、検証可能なML推論をEthereumの再利用可能なプリミティブに変えます。
**ERC20**や**ERC721**がトークンの標準を確立したように、**ERC7992**はAI出力の検証に対して同じ役割を果たすことを目指しています。

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

[\[ERC20\] ERC20を1からわかりやすく解説](/articles/aeefe2d89dd5332c9f43)

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

[\[ERC721\] ERC721を1からわかりやすく解説](/articles/0570a8d05bc7c9b9593f)

## 仕様

#### 用語とID体系

**ERC7992**で使われる主要な概念とIDを以下にまとめます。

| 用語 | 型 | 説明 |
|------|-----|------|
| Model Commitment | 構造体 | モデルの重み、アーキテクチャ、証明回路、検証鍵のハッシュを束ねたデータ。登録されたモデルの「指紋」にあたる。 |
| `modelId` | `uint256` | レジストリがモデル登録時に発行する一意の識別子。 |
| `inputCommitment` | `bytes32` | 推論のプライベート入力に対するハッシュコミットメント。ドメイン分離が必要であり、リプレイ保護のためにノンスやソルトを含めるべきとされている。 |
| `output` | `bytes` | 推論のABIエンコードされた公開出力。スキーマはアプリケーションごとに定義される。 |
| `proof` | `bytes` | ゼロ知識証明のバイトデータ。 |
| `proofSystemId` | `bytes4` | 証明システム＋曲線＋バージョンを識別する4バイトのID。 |

:::message
**ドメイン分離**

異なる用途のハッシュ値が偶然一致しないようにするための仕組みです。
例えば「ZKML_MODEL_V1」のようなプレフィックスをハッシュ計算に含めることで、他のコンテキストで生成されたハッシュとの衝突を防ぎます。
:::

##### `proofSystemId`の算出方法

`proofSystemId`は以下のように算出されます。

```javascript
bytes4(keccak256(abi.encodePacked(<canonical-proof-system-name-and-version>)))
```

`<canonical-proof-system-name-and-version>`は小文字・ハイフン区切りの文字列です。

| 証明システム | 文字列例 |
|------------|---------|
| Groth16（BN254曲線） | `"groth16-bn254-v1"` |
| Plonk（BN254曲線） | `"plonk-bn254-v2"` |
| STARK | `"stark-airfoo-v1"` |

この方式により、各実装間で決定論的かつ衝突耐性のある識別子を生成できます。

### レジストリ

レジストリはMLモデルのコミットメントを登録・管理するコントラクトです。
モデルの「指紋」をオンチェーンに記録することで、検証器がどのモデルの証明を検証すべきかを参照できるようにします。

以下がレジストリの完全なインターフェースです。

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

interface IERCZKMLRegistry /* is IERC165 */ {
    struct ModelCommitment {
        bytes32 modelHash;     // weights + architecture hash
        bytes32 circuitHash;   // arithmetic circuit / AIR hash
        bytes32 vkHash;        // verifying key hash
        bytes4  proofSystemId; // keccak-based identifier
        string  uri;           // optional off-chain metadata (IPFS/HTTP)
    }

    event ModelRegistered(
        uint256 indexed modelId,
        address indexed owner,
        ModelCommitment commitment
    );

    event ModelUpdated(
        uint256 indexed modelId,
        ModelCommitment oldCommitment,
        ModelCommitment newCommitment
    );

    event ModelDeprecated(uint256 indexed modelId);

    error ModelNotFound(uint256 modelId);
    error NotModelOwner(uint256 modelId, address caller);
    error ModelDeprecated(uint256 modelId);

    function registerModel(ModelCommitment calldata commitment)
        external
        returns (uint256 modelId);

    function updateModel(uint256 modelId, ModelCommitment calldata newCommitment)
        external;

    function deprecateModel(uint256 modelId) external;

    function getModel(uint256 modelId)
        external
        view
        returns (ModelCommitment memory commitment, bool deprecated, address owner);
}
```

#### `ModelCommitment`構造体

`ModelCommitment`はモデルの同一性を暗号的に保証するためのデータの集まりです。
各フィールドはモデルの異なる側面のハッシュを保持しており、登録されたモデルが後から改ざんされていないことを検証できます。

| フィールド | 型 | 説明 |
|-----------|-----|------|
| `modelHash` | `bytes32` | モデルの重みとアーキテクチャのハッシュ。モデルそのものの「指紋」にあたり、同じモデルであれば常に同じ値になる。 |
| `circuitHash` | `bytes32` | 算術回路またはAIR（Algebraic Intermediate Representation）のハッシュ。モデルの推論をゼロ知識証明で検証するための回路を特定する。 |
| `vkHash` | `bytes32` | 検証鍵のハッシュ。証明の検証に使う鍵の同一性を保証する。 |
| `proofSystemId` | `bytes4` | 使用する証明システムの識別子。Groth16、Plonk、STARKなど、どの証明システムで生成された証明かを示す。 |
| `uri` | `string` | オフチェーンメタデータへのURI（IPFSやHTTP）。モデルの説明、学習データの情報、ライセンスなどを格納できる。省略可能。 |

#### `registerModel`

```javascript
function registerModel(ModelCommitment calldata commitment)
    external
    returns (uint256 modelId);
```

新しいモデルのコミットメントをレジストリに登録する関数です。
モデルの所有者がこの関数を呼び出すと、レジストリは一意の`modelId`を割り当てて返します。
登録が完了すると`ModelRegistered`イベントが発行されます。

- **引数**
    - `commitment`
        - 登録するモデルのコミットメントデータ。

- **戻り値**

`uint256` レジストリが発行した一意のモデルID。

#### `updateModel`

```javascript
function updateModel(uint256 modelId, ModelCommitment calldata newCommitment)
    external;
```

既存モデルのコミットメントを更新する関数です。
モデルの再学習や回路の改善など、コミットメントの内容を変更する必要がある場合に使います。
この関数はモデルの所有者のみが呼び出せます。
更新が完了すると`ModelUpdated`イベントが発行されます。

- **引数**
    - `modelId`
        - 更新対象のモデルID。
    - `newCommitment`
        - 新しいコミットメントデータ。

:::message alert
コミットメントを更新すると、古いコミットメントに基づいて生成された証明は無効になる可能性があります。
利用者は特定の`modelId` + コミットメントハッシュの組み合わせを固定するか、更新前に非推奨フラグを確認する方法が有効です。
:::

#### `deprecateModel`

```javascript
function deprecateModel(uint256 modelId) external;
```

モデルを非推奨としてマークする関数です。
非推奨になったモデルに対する新しい証明の検証は拒否されます。
この関数もモデルの所有者のみが呼び出せます。
非推奨が設定されると`ModelDeprecated`イベントが発行されます。

- **引数**
    - `modelId`
        - 非推奨にするモデルID。

#### `getModel`

```javascript
function getModel(uint256 modelId)
    external
    view
    returns (ModelCommitment memory commitment, bool deprecated, address owner);
```

指定したモデルIDのコミットメント情報を取得する関数です。
検証器がモデルの証明を検証する時、この関数を通じてモデルのコミットメントを参照します。

- **引数**
    - `modelId`
        - 取得対象のモデルID。

- **戻り値**

`ModelCommitment` モデルのコミットメントデータ。
`bool` 非推奨フラグ。`true`の場合、モデルは非推奨。
`address` モデルの所有者アドレス。

#### イベント

##### `ModelRegistered`

```javascript
event ModelRegistered(
    uint256 indexed modelId,
    address indexed owner,
    ModelCommitment commitment
);
```

新しいモデルがレジストリに登録された時に発行されるイベントです。

| パラメータ名 | 型 | indexed | 説明 |
|------------|-----|---------|------|
| `modelId` | `uint256` | yes | 割り当てられたモデルID。 |
| `owner` | `address` | yes | モデルの所有者アドレス。 |
| `commitment` | `ModelCommitment` | no | 登録されたコミットメントデータ。 |

##### `ModelUpdated`

```javascript
event ModelUpdated(
    uint256 indexed modelId,
    ModelCommitment oldCommitment,
    ModelCommitment newCommitment
);
```

モデルのコミットメントが更新された時に発行されるイベントです。
更新前と更新後のコミットメントの両方が含まれるため、変更の追跡が可能です。

| パラメータ名 | 型 | indexed | 説明 |
|------------|-----|---------|------|
| `modelId` | `uint256` | yes | 更新されたモデルID。 |
| `oldCommitment` | `ModelCommitment` | no | 更新前のコミットメント。 |
| `newCommitment` | `ModelCommitment` | no | 更新後のコミットメント。 |

##### `ModelDeprecated`

```javascript
event ModelDeprecated(uint256 indexed modelId);
```

モデルが非推奨になった時に発行されるイベントです。

| パラメータ名 | 型 | indexed | 説明 |
|------------|-----|---------|------|
| `modelId` | `uint256` | yes | 非推奨になったモデルID。 |

#### エラー

| エラー名 | 説明 |
|---------|------|
| `ModelNotFound(uint256 modelId)` | 指定されたモデルIDが存在しない場合に返される。 |
| `NotModelOwner(uint256 modelId, address caller)` | 呼び出し元がモデルの所有者ではない場合に返される。`updateModel`や`deprecateModel`の権限チェックで使用される。 |
| `ModelDeprecated(uint256 modelId)` | 非推奨のモデルに対して操作が行われた場合に返される。 |

### 検証器

検証器はゼロ知識証明を受け取り、MLモデルの推論結果が正しいかどうかをオンチェーンで検証するコントラクトです。
レジストリからモデルのコミットメントを取得し、提出された証明がそのコミットメントに対して有効かどうかを確認します。

以下が検証器の完全なインターフェースです。

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

interface IERCZKMLVerifier /* is IERC165 */ {
    event InferenceVerified(
        uint256 indexed modelId,
        bytes32 indexed inputCommitment,
        bytes   output,
        address indexed caller
    );

    error InvalidProof();
    error ModelMismatch();
    error InputCommitmentMismatch();
    error OutputMismatch();
    error UnsupportedProofSystem(bytes4 proofSystemId);
    error VerificationRefused_ModelDeprecated(uint256 modelId);

    function verifyInference(
        uint256 modelId,
        bytes32 inputCommitment,
        bytes calldata output,
        bytes calldata proof
    ) external;

    function proofSystemOf(uint256 modelId) external view returns (bytes4);
    function registry() external view returns (address registryAddress);
}
```

#### `verifyInference`

```javascript
function verifyInference(
    uint256 modelId,
    bytes32 inputCommitment,
    bytes calldata output,
    bytes calldata proof
) external;
```

ZK証明を検証するための中核的な関数です。
この関数はレジストリからモデルのコミットメントを取得し、証明がそのコミットメントに対して有効であるかを確認します。
検証に失敗した場合は必ずリバートし、成功した場合は`InferenceVerified`イベントを発行することが推奨されています。
戻り値が`void`（何も返さない）設計になっているのは、失敗時はリバート・成功時はvoidという二択にすることで、冗長なbool返却を省くためです。
リバートしなければ検証成功、リバートすれば検証失敗と判断できます。

以下は`verifyInference`の処理フローです。

```mermaid
sequenceDiagram
    participant Caller as 呼び出し元
    participant Verifier as 検証器
    participant Registry as レジストリ

    Caller->>Verifier: verifyInference(modelId, inputCommitment, output, proof)
    Verifier->>Registry: getModel(modelId)
    Registry-->>Verifier: commitment, deprecated, owner
    alt 非推奨モデル
        Verifier-->>Caller: revert VerificationRefused_ModelDeprecated
    else 証明検証
        Verifier->>Verifier: 証明をコミットメントに対して検証
        alt 検証失敗
            Verifier-->>Caller: revert InvalidProof / ModelMismatch / etc.
        else 検証成功
            Verifier-->>Caller: InferenceVerified イベント発行（推奨）
        end
    end
```

呼び出し元が証明を提出すると、検証器はまずレジストリからモデルのコミットメントを取得します。
モデルが非推奨の場合はリバートし、そうでなければ証明の検証に進みます。
検証に成功すると`InferenceVerified`イベントの発行が推奨されています。

| パラメータ名 | 型 | 説明 |
|------------|-----|------|
| `modelId` | `uint256` | レジストリに登録されたモデルの識別子。 |
| `inputCommitment` | `bytes32` | プライベート入力に対するハッシュコミットメント。リプレイ保護のためにノンスやソルトを含めるべきとされている。 |
| `output` | `bytes` | 推論のABIエンコードされた公開出力。 |
| `proof` | `bytes` | ゼロ知識証明のバイトデータ。 |

検証器は基本的にステートレス（状態を持たない）であり、イベントの発行のみを行います。
状態の保持が必要な場合は、後述のストレージ拡張を使用します。

#### `proofSystemOf`

```javascript
function proofSystemOf(uint256 modelId) external view returns (bytes4);
```

指定したモデルが使用する証明システムのIDを返す関数です。
利用者が証明を生成する前に、どの証明システムを使うべきかを確認できます。

- **引数**
    - `modelId`
        - 対象のモデルID。

- **戻り値**

`bytes4` 証明システムの識別子。

#### `registry`

```javascript
function registry() external view returns (address registryAddress);
```

検証器が参照しているレジストリコントラクトのアドレスを返す関数です。

- **戻り値**

`address` レジストリのコントラクトアドレス。

#### イベント

##### `InferenceVerified`

```javascript
event InferenceVerified(
    uint256 indexed modelId,
    bytes32 indexed inputCommitment,
    bytes   output,
    address indexed caller
);
```

推論の検証が成功した時に発行されるイベントです。

| パラメータ名 | 型 | indexed | 説明 |
|------------|-----|---------|------|
| `modelId` | `uint256` | yes | 検証されたモデルのID。 |
| `inputCommitment` | `bytes32` | yes | 入力のハッシュコミットメント。 |
| `output` | `bytes` | no | 推論の公開出力。 |
| `caller` | `address` | yes | 検証を呼び出したアドレス。 |

#### エラー

| エラー名 | 説明 |
|---------|------|
| `InvalidProof()` | 証明が無効な場合に返される。 |
| `ModelMismatch()` | 証明は有効だが、指定された`modelId`のモデルに紐付いていない場合に返される。 |
| `InputCommitmentMismatch()` | 入力コミットメントが証明と一致しない場合に返される。 |
| `OutputMismatch()` | 出力が証明またはコミットメントと一致しない場合に返される。 |
| `UnsupportedProofSystem(bytes4 proofSystemId)` | 検証器がその証明システムをサポートしていない場合に返される。 |
| `VerificationRefused_ModelDeprecated(uint256 modelId)` | 非推奨モデルに対する検証が拒否された場合に返される。 |

### ストレージ拡張

基本の検証器はステートレスですが、監査可能性や決定論的な決済が必要な場合に、検証済みの推論記録を永続化するためのオプションの拡張インターフェースです。

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

interface IERCZKMLStorageExtension {
    event InferenceStored(bytes32 indexed inferenceId);

    function verifyAndStoreInference(
        uint256 modelId,
        bytes32 inputCommitment,
        bytes calldata output,
        bytes calldata proof
    ) external returns (bytes32 inferenceId);

    function getInference(bytes32 inferenceId)
        external
        view
        returns (uint256 modelId, bytes32 inputCommitment, bytes memory output);
}
```

#### `verifyAndStoreInference`

```javascript
function verifyAndStoreInference(
    uint256 modelId,
    bytes32 inputCommitment,
    bytes calldata output,
    bytes calldata proof
) external returns (bytes32 inferenceId);
```

証明を検証し、検証成功した推論記録をオンチェーンに保存する関数です。
証明が無効な場合はリバートします。
保存が完了すると`InferenceStored`イベントが発行されます。

`inferenceId`は`keccak256(abi.encodePacked(modelId, inputCommitment, output))`で算出されます。
リプレイが懸念される場合は、`inputCommitment`にノンスやソルトを埋め込む必要があります。

- **戻り値**

`bytes32` 保存された推論記録の一意な識別子。

#### `getInference`

```javascript
function getInference(bytes32 inferenceId)
    external
    view
    returns (uint256 modelId, bytes32 inputCommitment, bytes memory output);
```

保存済みの推論記録を取得する関数です。
他のコントラクトが過去の検証結果を参照する時に使います。
例えば、DeFiプロトコルが保険金請求の判定にAI推論結果を使う場合、この関数で過去の推論記録を確認し、紛争の解決に利用できます。

- **引数**
    - `inferenceId`
        - 取得する推論記録のID。

- **戻り値**

`uint256` モデルID。
`bytes32` 入力のハッシュコミットメント。
`bytes` 推論の公開出力。

## 補足

#### レジストリと検証器の分離設計

**ERC7992**がレジストリと検証器を別々のコントラクトに分けているのは、モジュール性を重視した設計上の決定です。
レジストリはモデルの登録情報を管理し、検証器は証明の検証ロジックに特化します。

![レジストリと検証器の分離アーキテクチャ](/images/qiita-ba397332-13eb-4302-bf4a-d5abeab187a0.webp)

この分離により、チームは検証器を独立してアップグレードできます。
例えば、新しい証明システム（STARK→次世代の証明方式）に対応する時、レジストリのデータを保持したまま検証器だけを差し替えることが可能です。
逆に、レジストリの管理ポリシーを変更する時も検証器には影響しません。

#### voidリターンの設計理由

`verifyInference`が戻り値を返さない設計は、OpenZeppelinの`SafeERC20`と同じパターンです。
失敗時はリバート、成功時はvoidという二択にすることで、冗長なbool返却を省いています。
呼び出し元は単に関数が正常に実行されたかどうかを確認するだけで、検証結果を判断できます。

#### ノンスによるリプレイ保護

`inputCommitment`にノンスやソルトを埋め込むことで、同じモデル・同じ入力・同じ出力に対する証明の再利用を防止します。
リプレイ保護が特に重要なのは以下のような場合です。

- モデルの出力が非決定論的な場合（同じ入力でも実行ごとに異なる出力を返す可能性がある場合）
- 推論結果が1回限りの判定に使われる場合（保険金の請求判定など）

ストレージ拡張を使う場合、`inferenceId`の衝突を避けるためにも`inputCommitment`にノンスを含めることが重要です。

#### `proofSystemId`の決定論的設計

`proofSystemId`をkeccakベースの決定論的な方式で算出しているのは、衝突と曖昧性を防ぐためです。
複数の証明システムが混在する検証器では、どの証明システムで検証すべきかを確実に判別する必要があります。
人間が手動でIDを割り当てる方式では重複のリスクが生じますが、正規化された文字列からハッシュを計算することで、誰が計算しても同じIDが得られ、予測可能なディスパッチが実現できます。

#### 証明システムのプラガビリティ

`proof`と`output`を`bytes`型にしているのは、特定の証明システムやスキーマに依存しない設計のためです。
Groth16、Plonk、STARKなど異なる証明システムを使う場合でも、ABIを変更する必要がありません。
`proofSystemId`による識別と内部的なディスパッチにより、検証器は適切な検証ロジックに処理を振り分けます。

## 互換性

#### 既存規格との関係

**ERC7992**は既存の規格と完全に後方互換性があります。

| 規格 | 関係 |
|------|------|
| **ERC165** | インターフェースの検出に使用。**ERC721**や**ERC1155**と同様のパターン。 |
| **ERC20**/**ERC721** | トークン所有権規格への依存はない。最小限の衝突で既存プロトコルと共存できる。 |

レジストリインターフェースは`owner`を各`modelId`に紐づけて返します。
所有権管理の実装方法は自由であり、単純なオーナーストレージ、**ERC173**、ロールベースの制御など、プロジェクトの要件に合わせて選択できます。

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

[\[ERC1155\] ERC1155を1からわかりやすく解説](/articles/c3ef92722b057161bebd)

## セキュリティ

#### リプレイ攻撃

推論結果の一意性が重要な場合、入力にはノンスやソルトを含める必要があります。
ノンスを含めないと、過去の有効な証明が再利用され、同じ推論結果が何度も「検証済み」として扱われる危険性があります。
ストレージ拡張を使う場合は、消費済みの`inferenceId`を追跡することでも対策できます。

#### モデルコミットメントの更新リスク

`updateModel`でコミットメントを更新すると、古いコミットメントに対して生成された証明が検証に失敗する可能性があります。
利用者は以下のいずれかの方法で対策する必要があります。

- 特定の`modelId`とコミットメントハッシュの組み合わせを固定して参照する
- `deprecated`フラグを事前に確認する
- バージョニングポリシーを実装する（新しいコミットメントには新しい`modelId`を発行する方式）

#### ハッシュドメイン分離

異なるコンテキスト間でのハッシュ衝突を防ぐため、「ZKML_MODEL_V1」のようなプレフィックスを使用してドメイン分離を行う必要があります。
ドメイン分離がないと、他のプロトコルで生成されたハッシュがZKMLのコミットメントと誤って一致するリスクがあります。

#### 出力データの曖昧性

`output`パラメータは`bytes`型であるため、スキーマが明示されていないと悪意のあるバイト列を出力として送りつけることが可能です。
利用するコントラクトは、出力のスキーマを事前に合意するか、出力コミットメントを追加で検証する必要があります。

#### DoS攻撃（重い検証処理）

ZK証明の検証はガスコストが高い処理です。
大量の検証リクエストを送りつけることで、コントラクトのガス上限に達してサービスを妨害するDoS攻撃のリスクがあります。
対策としては、以下のアプローチが考えられます。

- オフチェーンでの事前検証を行い、オンチェーンでは簡潔なアテステーション（証明書）のみを検証する
- 複数の証明をバッチ処理またはアグリゲーション（集約）して検証する
- 検証のみの呼び出しとストレージへの書き込みを分離し、利用者がコストを選択できるようにする
- 監査証跡としてはストレージへの永続化よりイベントの発行の方がガスコストが安いため、永続化が不要な場合はイベントのみで済ませる

## 参考実装

以下は**ERC7992**の検証器コントラクトの参考実装です。
レジストリからモデルのコミットメントを取得し、証明を検証する基本的な流れを示しています。

```javascript:contracts/ZKMLVerifier.sol
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.20;

import "./IERCZKMLRegistry.sol";
import "./IERCZKMLVerifier.sol";

contract ZKMLVerifier is IERCZKMLVerifier {
    IERCZKMLRegistry public immutable override registry;

    constructor(IERCZKMLRegistry _registry) {
        registry = _registry;
    }

    function verifyInference(
        uint256 modelId,
        bytes32 inputCommitment,
        bytes calldata output,
        bytes calldata proof
    ) external override {
        (IERCZKMLRegistry.ModelCommitment memory cm, bool deprecated,) =
            registry.getModel(modelId);

        if (deprecated) revert ModelDeprecated(modelId);

        // Dispatch based on proofSystemId. Example only.
        // bool ok = VerifierLib.verify(proof, cm.vkHash, inputCommitment, output);
        bool ok = _dummyVerify(proof, cm.vkHash, inputCommitment, output);
        if (!ok) revert InvalidProof();

        emit InferenceVerified(modelId, inputCommitment, output, msg.sender);
    }

    function proofSystemOf(uint256 modelId) external view override returns (bytes4) {
        (IERCZKMLRegistry.ModelCommitment memory cm,,) = registry.getModel(modelId);
        return cm.proofSystemId;
    }

    function _dummyVerify(
        bytes calldata,
        bytes32,
        bytes32,
        bytes calldata
    ) private pure returns (bool) {
        return true;
    }
}
```

この参考実装では`_dummyVerify`関数が常に`true`を返しますが、実際のプロダクション環境ではここにGroth16やPlonkなどの証明検証ロジックを実装します。
`proofSystemId`に基づいて適切な検証ライブラリにディスパッチする仕組みを組み込むことで、複数の証明システムに対応できます。

## 引用

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

https://ethereum-magicians.org/t/erc-7992-verifiable-ml-model-inference-zkml/24896

## 最後に

今回は「**ERC7992**によるゼロ知識証明を使ったMLモデルの推論検証の仕組み」についてまとめてきました！
いかがだったでしょうか？

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

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

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

https://zenn.dev/heku

https://cardene.substack.com/

https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58

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