はじめに
初めまして。
『DApps開発入門』という本や色々記事を書いているかるでねです。
以下でも情報発信しているので、興味ある記事があればぜひ読んでみてください!
今回はERC8004で登録されたAIエージェントに対し、オフチェーンでコントラクト・メディア・Web・ウォレットなど複数の検証とリスクスコアを扱う枠組みを提案している「ERC8126」についてまとめていきます!
この記事ではERC8004に登録したエージェントの検証がオフチェーンでどう進み、スコアと証明がどう扱われるかを提案の順に整理します。
オンチェーンでエージェントを登録したり探したりする流れは進んでも、信頼度をどう測るかはまだサービスごとに違います。
ERC8126はERC8004のIdentity Registryに登録されたエージェントを起点に、検証プロバイダがオフチェーンでコントラクト・メディア・Solidity・Web・ウォレットに対応する5つの検証を実行し、結果をZKPや0〜100のリスクスコアにまとめる枠組みを定めた提案です。
ZKP・PDV
ZKP(ゼロ知識証明)は中身を開示せずに「検証済み」を示せる暗号の仕組みです。
PDV(Private Data Verification)は検証の途中データをそのままオンチェーンに書き込まず、ZKPやProof IDで扱う方式です。
概要
ERC8126はERC8004のIdentity Registryにmintされたエージェント(ERC721の tokenId が agentId )を起点に、メタデータを読んでコントラクト(ETV)、メディア(MCV)、Solidity(SCV)、HTTPSのエンドポイント(WAV)、ウォレット(WV)の5つを実行し、Private Data Verification(PDV)と総合リスクまで扱う枠組みです。
検証の本体はオフチェーンで、任意でERC8004のValidation Registryへアテステーションを投稿できます。
Identity Registry と tokenURI
Identity RegistryはERC8004のレジストリの一つで、エージェントをERC721の tokenId(ここでは agentId)として登録し、メタデータのURLを tokenURI で返します。
Validation Registry とアテステーション
Validation RegistryもERC8004のコンポーネントで、検証結果をアテステーション(署名付きの主張)として投稿し、ほかの指標と並べてエージェントを探しやすくします。
要点の整理
agentId から tokenURI を辿ってJSONを取り、入っているフィールドだけを材料に各検証を実行し、スコアの算術平均で全体リスクを出します。
そのうえで検証の途中で出たデータはPDVでZKPにまとめ、Proof IDとして扱います。
一方、詳しい結果はウォレット保持者だけが閲覧でき、ほかの利用者には要約が伝わる前提で区切られています。
| 項目 | 内容 |
|---|---|
| 正式名称 | AI Agent Verification |
| 対象 |
ERC8004に登録されたエージェント(agentId で特定) |
| 検証の場所 | オフチェーン(ガスを抑え、複雑なチェックを扱える) |
| 専門検証 | コントラクト(ETV)、メディア(MCV)、Solidity(SCV)、HTTPSのエンドポイント(WAV)、ウォレット(WV)(メタデータのフィールドに応じて実行) |
| 集約 | PDV で ZKP と Proof ID、総合スコアは各タイプの算術平均 |
| 任意 | ERC8004のValidation Registryへアテステーションを投稿する |
コントラクト(ETV)はコントラクトの検査、メディア(MCV)はメディア、Solidity(SCV)はソース、HTTPSのエンドポイント(WAV)はHTTPS、ウォレット(WV)はウォレットの取引履歴と脅威情報の照合です。
総合は0〜100のスコアに揃え、必要ならValidation Registryへ投稿し、ほかの指標と並べてエージェントを探しやすくします。
ERC8004については以下の記事を参考にしてください。
ERC721については以下の記事を参考にしてください。
動機
背景
AIエージェントをオンチェーンで登録したり一覧したりする流れは進んでも、「どこまで信頼していいか」を見る手順や指標はまだサービスごとにバラバラです。
そこで見るべき観点は複数あります。
コントラクトが意図どおりか、HTTPSやメディアの真正性、ウォレットの送受履歴や脅威情報との照合などです。
しかしそれぞれ別のツールに頼むと運用が分散します。
検証とスコアと登録の扱い
提案では信頼性をコントラクト(ETV)、メディア(MCV)、Solidity(SCV)、HTTPSのエンドポイント(WAV)、ウォレット(WV)の5つに分けて見ます。
そのうえでプロバイダはどのエージェントでも同じ手順で検証できるよう、コントラクト(ETV)、メディア(MCV)、Solidity(SCV)、HTTPSのエンドポイント(WAV)、ウォレット(WV)の5つを同じ形で実装します。
検証の途中で出たデータはZKPにまとめてから扱います。
送受履歴の内容など、見せたくない情報をチェーンに丸ごと書き込まないための工夫です。
スコアは0〜100に揃え、エージェント同士で比較しやすくします。
エージェントの登録はERC8004に寄せ、エージェントIDとメタデータの取り方を一本化します。
楕円曲線に基づく ZKP は将来の量子コンピュータが進むと、理論上解読に弱くなる可能性があると指摘されています。
長期間保管するデータをそうした脅威からも守りたい場合に備え、任意のオプションとして QCV(Quantum Cryptography Verification)があり、対称鍵暗号など別の仕組みを重ねるオプションが提案に含まれています。
QCV
QCV(Quantum Cryptography Verification)は必須ではなく、長期保管するデータを対称鍵暗号などで守るためのあとから足せるオプションです。
量子計算の進展への懸念に対する選択肢としても提案に含まれます。
楕円曲線ベースのZKPの上に、長期保管向けの仕組みを任意で重ねるイメージを図にすると以下です。
下層がPDVで扱うZKP(楕円曲線ベースの回路など)、その上に点線や別枠で示した任意オプションのQCV(長期保管向けに対称鍵など別仕組みを重ねる)の関係を、必須ではない追加の箱として示した図です。
上下のレイヤは、上ほど任意・補助的である、という読み方で足ります。
仕様
検証対象は ERC8004 の agentId に固定し、Identity Registryの tokenURI からJSONを取り、入っているフィールドだけを材料に、コントラクト(ETV)、メディア(MCV)、Solidity(SCV)、HTTPSのエンドポイント(WAV)、ウォレット(WV)の5つの検証を実行します。
その結果をスコアの平均で全体リスクにまとめ、必要ならValidation Registryへ投稿します。
また記録用コントラクトにイベントや最新スコアを返す呼び出し(view など)を定義することも提案に含まれています。
ERC8126 の検証フロー全体像は以下の図です。
提案本文付属のこの図は、agentId と Identity Registry / tokenURI によるメタデータへの入り口から、コントラクト(ETV)、メディア(MCV)、Solidity(SCV)、HTTPSのエンドポイント(WAV)、ウォレット(WV)による各スコア(0〜100)、全体リスクの集約、PDV と Proof ID、必要ならオンチェーン記録や Validation Registry までを、レイアウト上のつながりで一望できるようにしたものです。
スイムレーンやブロックの境界、矢印の向き・ラベルは図中に任せます。図だけを見ても追えるよう、枠と矢印で役割が区切られています。
続く図は、処理内容はこの公式図と対応させつつ、同じ流れを縦方向の一本道に単純化した読み取り用です。
俯瞰で構造を掴むには公式図、手順の先頭から順に言葉で追うときはこの図を併用すると読みやすい、という関係です。
この図は、agentId から tokenURI でJSONを取り、該当フィールドがある分だけ5種類の検証へ進むところから、各タイプのスコア(0〜100)、全体リスクの平均、PDV(ZKP と Proof ID)、任意の Validation Registry までを上から順に言葉で辿れるようにしたものです。
ノード A から H は、上の公式図と対応する塊として読んでください。
-
agentId を起点にする
検証対象は常に ERC8004 の Identity Registry に登録されたagentId(uint256のtokenId)に固定され、別ルートから検証を始められません。 -
Identity Registry で tokenURI を取得する
プロバイダはレジストリに対しtokenURI(agentId)を呼び、返った URI 文字列を次の取得先として使います。 -
JSON メタデータを取得する
その URI をfetch等で取得し、提案どおりのキー(contractAddressやurl、walletAddressなど)を読み取れる JSON にします。 -
該当フィールドがある分だけ ETV・MCV・SCV・WAV・WV を実行する
5種類はすべて実装しますが、メタデータに材料が無いタイプは実行しないかスキップされ、平均に入る分母だけが変わります。 -
各タイプのスコア(0〜100)を得る
実行されたそれぞれの検証が 0〜100 のリスクスコアを返します(プロバイダのルールに従うヒューリスティックです)。 -
全体リスクを算術平均で求める
適用された各スコアの算術平均が総合リスクになり、同じ 0〜100 の尺に揃えてエージェント同士を比べやすくします。 -
PDV で ZKP と Proof ID を扱う
生の中間結果は丸ごとチェーンに載せず、ZKP と Proof ID などにまとめてから扱う前提です(各検証の中身が数学的に自動で正しいことの証明になるわけではありません)。 -
任意で Validation Registry に進む
総合スコアと証明を ERC8004 の Validation Registry にアテステーションとして載せるのは任意で、運用や製品の都合で使う分だけが動きます。
この図は、ERC8004 の Identity Registry(エージェント登録・tokenURI)や Validation Registry(アテステーション)といったオンチェーン側の枠に対し、ERC8126 の検証プロバイダがオフチェーンでどの処理を担い、どこに結果が戻るかの関係を示した概要です。
レジストリや Registry のブロックと、検証プロバイダ・PDV のブロックがどの矢印でつながるかを、位置の近さと矢印で読み取ってください。
エントリとメタデータの解決
検証はいつも ERC8004 のIdentity Registry上の agentId (uint256 )から始めます。
まずプロバイダは tokenURI(agentId) を呼び、返ってきたURIからJSONを取得します。
agentWallet や walletAddress、chain_id、contractAddress、endpoints や url、image や imageUrl、platform_id、solidityCode など、提案で列挙されているフィールドはこのJSONから読みます。
ERC8004 の登録スキーマに合わせて解釈します。
コントラクトやウォレットを調べるには、「どのネットワーク上の、どのアドレスか」がはっきりしていなければ次に進めません。
そのため chain_id が無い・無効なときは、プロバイダがあらかじめ決めたチェーンに合わせるなどして、実装側でチェーンを確定してから eth_getCode や HTTPS 検証に進みます。
この記事では、その「指し先がはっきりした」状態を短く解決済みと書きます。
解決済みは、どのチェーン上のどのアドレスを検証するかが、もう迷わず一つに決まっているという意味です。
検証の入り口は agentId からメタデータをたどる形に限られます。
そのためアドレスやURLだけをバラバラに渡して検証を始めることはできません。
Registry に登録されたエージェントと検証が読む情報がずれないようにするためです。
5つの検証タイプ
プロバイダはコントラクト(ETV)、メディア(MCV)、Solidity(SCV)、HTTPSのエンドポイント(WAV)、ウォレット(WV)の5つをすべて実装し、取れたメタデータに応じて使い分けます。
そのうえでフィールドが無い検証タイプはスキップするか、平均を取る対象の数だけが変わります(各タイプは0〜100のスコアを返します)。
各タイプの「適切」、「安全」、「真正性」などは提案に書かれた MUST/SHOULD と 0〜100 のスコアに基づくヒューリスティックな評価です。
ETV(下記)では、提案の MUST における「適切」と「安全」の意味を、本文でそのまま噛み砕いています。
静的解析の深さや参照するパターン一覧はプロバイダごとに異なり、万能の保証ではありません(後半の「検証の意味とウォレットとURL」も参照)。
JSONのどのフィールドがどの検証タイプに効くか、対応例を図にすると以下です。
この図は、JSON に左列のフィールド名があるとき、右列のどの検証タイプ(ETV〜WV)へ材料として渡るかを、行ごとの矢印で示した対応例です。
行に現れない検証タイプはメタデータに材料が無いので実行されないかスキップされ、平均の分母だけが変わります。
Ethereum Token Verification(ETV)
提案では Ethereum Token Verification(ETV)と呼ばれます。
メタデータに contractAddress があり、かつ chain_id などが解決できているとき、そのアドレスが指すチェーン上でデプロイ済みのスマートコントラクトとして存在するかと既知の脆弱性パターンに照らしたリスクを 0〜100 のスコアにまとめます。
名称の Token は ERC-20 トークンに限るわけではなく、オンチェーンのコントラクト先を扱う検証です。
解決した chain_id 上で eth_getCode を呼び、返るバイトコードが空でないことを確認します。
これは EOA ではなく、実行可能なコードがそのアドレスに存在することの確認です。
そこからプロバイダが保持する既知の脆弱性パターンと突き合わせ、リスクスコアを出します。
実装の目安として、OWASPのSmart Contract Security Verification Standard(SCSVS)が挙げられています。
提案の MUST に沿うと、「適切」には少なくとも legitimacy(オンチェーン上の正当な足がかり)、つまり chain_id 上でコントラクトとしてデプロイされていることが含まれます。
「安全」は、絶対安全の証明ではなく、既知パターンとの照合結果を 0〜100 に数値化した相対的な指標です。
静的解析の網羅性やパターンの更新頻度はプロバイダ依存で、監査や形式検証の代替にはなりません。
メディアコンテンツ検証(MCV)
imageUrl などメディアを指すフィールドがあるとき、真正性(コンテンツが主張どおりの由来か)、出所(誰が・どの経路で作られたかの手がかり)、完全性(途中で改ざんされていないか)を扱います。
取得したバイナリに対し埋め込みメタデータの有無と内容、改ざんの痕跡、透かし・ステガノグラフィ・署名があればそれらの検証までプロバイダが実装します。
AI生成や合成映像の手がかりを調べるフォレンジック解析は SHOULD(取り組むとよい)とされています。
0〜100のスコアを返します。
C2PAの実装ガイドなど既存の枠組みを使う例が挙げられています。
Solidity コード検証(SCV)
solidityCode がメタデータにあるとき、メタデータに書かれた Solidity と、オンチェーンにデプロイされているバイトコードが少なくとも「デプロイ済みのコードとして解釈できる」状態かを確認し、よくある実装欠陥の観点でリスクを 0〜100 にまとめます。
解決した chain_id で eth_getCode を呼び、バイトコードが空でないことを確認します(オンチェーンに実体のあるコードがあることの確認です)。
再入やフラッシュローン攻撃など一般的に列挙される弱点パターンでチェックし、0〜100のスコアを出します。
ソースとバイトコードの厳密な一致証明までをこの枠組みだけで保証するわけではありません。
SCSVSに沿う形が実装の目安として挙げられています。
Web アプリ検証(WAV)
url または endpoints 配列についてHTTPS でエンドポイントが応答すること、TLS 証明書が有効であること、ウェブでよく知られた脆弱性カテゴリに当たらないかを確認し、結果を 0〜100 に落とし込みます。
「一般的な脆弱性」はプロバイダが OWASP 系などの公開されているテスト観点に沿って機械的・半自動的に当てはめるイメージで、サイト全体のペネトレーション検査の完了を意味するものではありません。
提案では OWASP Web Security Testing Guide(WSTG)を目安にすると書かれています。
ウォレット検証(WV)
walletAddress または agentWallet についてそのアドレスにオンチェーンの送受信の履歴があるかを確認し、脅威インテリジェンスのデータベース(悪用の報告や制裁・詐欺関連の指標など、プロバイダが参照するリスト)と照合して0〜100 のスコアを出します。
「履歴がある」こと自体が善悪を決めるわけではなく、新規・未使用に近いアドレスか、既知の悪用パターンと重なるかがスコアの根拠になります。
脅威インテリジェンス
ウォレットアドレスが不正送金や制裁リストなど第三者が集約した悪用・リスクのシグナルと照合できるかを見るデータベースのイメージです。
データソースや更新頻度はプロバイダごとに異なります。
履歴の有無や悪用シグナルとの照合を、一文の代わりに関係だけ示すと以下です。
図では、ウォレットアドレスが検証の入力となり、履歴の有無や第三者が集約したリストとどう突き合わされ、0〜100 のスコアへつながるかの流れだけを抜き出しています。
実際の参照先・更新頻度・スコアの決め方はプロバイダ依存であり、図はイメージ用の整理です。
PDVと結果の見え方
各タイプの生の検証結果はPrivate Data Verification(PDV)を通じてZKPに変換され、Proof IDとして扱われます。
ただし ZKP は途中の生データをチェーンに書き込まずに「検証済み」を示すための仕組みであり、各専門検証の中身が正しいことの数学的証明を自動で与えるものではありません(回路の設計とプロバイダの実装に依存します)。
そのうえで、ほかの利用者には総合リスクスコアと Proof ID などの要約が伝わり、細部はウォレット保持者が閲覧する役割の分かれ方になっています。
生データを載せずに要約だけを広く見せる、という見え方の違いを一枚にすると以下です。
図では、検証プロバイダ側の生データや詳細がチェーン上にそのまま出ないこと、一般の利用者に届くのは要約(スコア・Proof ID など)にとどまること、ウォレット保持者だけが詳細に触れられること、を領域の分かれ方で示しています。
検証の中身の原文はオンチェーンに載せず、スコアと Proof ID などの要約が一般に伝わり、詳細はウォレット保持者が見る、という提案の役割分担のイメージです。
ERC8004との連携
リスクスコアとProof IDはERC8004のValidation Registryにアテステーションとして投稿されます。
その目的は、ほかの指標と並べてエージェントを探しやすくすることです。
オフチェーンで検証する理由
検証本体はオフチェーンで行います。
主な理由は以下です。
- 検証のたびにガス代を発生させない
- オンチェーンでは高すぎる複雑なロジックを許容する
- 基準をアップグレードなしで更新しやすい
- 複数のプロバイダが競争できる
支払いプロトコル
手数料を取る場合は検証の前に料金体系をはっきり書くことが必須です。
決済は安定コインで行い、ガスレス送金にはEIP3009の TransferWithAuthorization を使うと提案に書かれています。
リスクスコアとティア
全体スコアは適用された各検証スコアの算術平均として計算されます。
| ティア | スコア範囲 | 説明 |
|---|---|---|
| 低リスク | 0〜20 | 懸念が小さい |
| 中程度 | 21〜40 | 一部見直しを推奨 |
| やや高め | 41〜60 | 注意が必要 |
| 高リスク | 61〜80 | 重大な懸念 |
| 重大 | 81〜100 | 避けるのが無難 |
表と同じ区分を、直線上の帯に読み替えた目安を図にすると以下です。
横一列の帯では、左ほど低スコア・低リスク、右ほど高スコア・重大と読み、各区分にラベル(低リスク〜重大)とスコア範囲が対応していることを示しています。
区分の境界の数値は上表を正とし、図は色と帯の切れ目のイメージです。
標準エラーとカスタムエラー
実装では以下のコードと名前を使い分けます。
| コード | 名前 | 意味 |
|---|---|---|
0x01 |
InvalidAddress |
アドレス形式が無効 |
0x02 |
InvalidURL |
URLが不正またはHTTPSでない |
0x03 |
AgentNotFound |
agentId にエージェントが無い |
0x04 |
VerificationFailed |
プロバイダ側エラー |
0x05 |
InsufficientCredits |
クレジット不足 |
0x06 |
InvalidProof |
PDVの証明検証失敗 |
0x07 |
ProviderUnavailable |
プロバイダが応答しない |
0x08 |
InvalidScore |
スコアが0〜100の範囲外 |
0x09 |
ContractNotFound |
チェーン上にコントラクトが無い |
0x0A |
SolidityCodeNotFound |
Solidityコードの指定が解決できない |
0x0B |
ImageNotFound |
画像の取得に失敗 |
0x0C |
SteganographyFailed |
ステガノグラフィ抽出の技術的失敗 |
0x0D |
MediaVerificationFailed |
メディア検証の一般失敗 |
チェーン上で処理が失敗してリバートするときは、提案で列挙されているエラー名のカスタムエラー(上表の「名前」列と対応する error )として返される想定です。
数値コードは主にオフチェーン実装やAPIがログやレスポンスで使うための対応表です。
オプションのオンチェインインターフェース
検証の実行そのものはオフチェーンが中心ですが、プロバイダや統合者はスコアやアテステーションを記録するコントラクトを任意でデプロイできます。
IERC8126インターフェース
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.0;
interface IERC8126 {
event AgentVerified(
uint256 indexed agentId,
uint8 overallRiskScore,
bytes32 etvProofId,
bytes32 mcvProofId,
bytes32 scvProofId,
bytes32 wavProofId,
bytes32 wvProofId,
bytes32 summaryProofId
);
event AttestationPosted(
uint256 indexed agentId,
uint8 riskScore,
bytes32 proofId
);
function getLatestRiskScore(uint256 agentId) external view returns (uint8);
}
提案が掲げるオプションの記録用コントラクト向けの例は、上記の IERC8126 です。
検証ロジックそのものは含まれず、イベントが2つと参照用の関数が1つ(external view)だけが定義されています。
オフチェーンで総合スコアと Proof ID が確定したあと、任意の記録用コントラクトと Validation Registry がどう絡むかを、提案の想定どおりに一続きで示すと以下です。
図を見たあと、すぐ下の本文で各ノードの意味を押さえてください。
A〜B はオフチェーン側で結果が揃う段階、C〜D は任意で記録用コントラクトに AgentVerified を emit する枝、E〜G は任意で Registry に書き込んだあと AttestationPosted を emit する枝です。
AgentVerified は検証結果一式(各 Proof ID と総合スコア)を記録用コントラクトに載せる、AttestationPosted はRegistry への投稿が終わった事実をログでも残す、という役割の違いを図では上下の枝に分けています。
どちらも必須ではなく、運用で使う分だけが動きます。
つづいて各イベント・関数についてシグネチャを抜き出し、コードのあとに表でフィールドや引数・戻り値を整理します(イベントはフィールド一覧、関数は引数と戻り値を別表にします。ISPVGateway など同系の規格解説と同じく、全文→(要素ごとに)コード→説明→表の並びです)。
AgentVerified
event AgentVerified(
uint256 indexed agentId,
uint8 overallRiskScore,
bytes32 etvProofId,
bytes32 mcvProofId,
bytes32 scvProofId,
bytes32 wavProofId,
bytes32 wvProofId,
bytes32 summaryProofId
);
このイベントは、検証が一通り終わったあとに総合リスクと各タイプの Proof ID をまとめてオンチェーンに残すものです。
フロントやインデクサは、このログから「どの agentId が、どの Proof ID の束で、何点だったか」を追跡します。
| フィールド | 説明 |
|---|---|
agentId(indexed) |
このイベントがどのエージェントの検証結果を指すかを示す ID です。 ERC8004 の Identity Registry 上の agentId(uint256 の tokenId)と同じ番号を想定します。 |
overallRiskScore |
コントラクト(ETV)からウォレット(WV)までの各スコアを平均した総合リスクで、0 から 100 の整数です。 |
etvProofId |
コントラクト検証(ETV)だけを PDV 経由でまとめたあとに付く識別子です。 オンチェーンには中身の原文は載せず、この ID で参照できる前提です。 |
mcvProofId |
メディア検証(MCV)の結果を PDV に載せたあとに付く識別子です。 |
scvProofId |
Solidity コード検証(SCV)の結果を PDV に載せたあとに付く識別子です。 |
wavProofId |
Web エンドポイント検証(WAV)の結果を PDV に載せたあとに付く識別子です。 |
wvProofId |
ウォレット検証(WV)の結果を PDV に載せたあとに付く識別子です。 |
summaryProofId |
上記を束ねた「ひとまとまりの検証結果」を指す Proof ID です。 個別タイプの ID とあわせて、監査や表示で「この検証ランナの結果一式」を辿るときに使います。 |
各 bytes32 は生データそのものではなく、PDV(ZKP や Proof ID の枠組み)で扱う識別子です。
イベントを読む側は、どのタイプの証明がどの ID に対応するかを、このフィールド名と対応させて解釈します。
AttestationPosted
event AttestationPosted(
uint256 indexed agentId,
uint8 riskScore,
bytes32 proofId
);
このイベントは、ERC8004 の Validation Registry へアテステーションを書き込んだあとに emit するものです。
リスクスコアと Proof ID を、チェーンのログにも残します。
統合者は、Registry をまたいだ手順を追わず、このログの積み重ねだけで登録フローを追えます。
| フィールド | 説明 |
|---|---|
agentId(indexed) |
どのエージェントへの投稿かを示す ID です。 Validation Registry への書き込みと同じ agentId を想定します。 |
riskScore |
アテステーションに載せた内容と同じ総合リスク(0〜100)を、イベント側にも明示するための値です。 |
proofId |
その投稿に紐づく PDV の Proof ID です。 ログだけ見てもスコアと証明の対応が追えるようにするためのフィールドです。 |
Registry のトランザクション本文だけでは拾いにくい利用者向けに、イベントで同じ事実をもう一段残すイメージです。
getLatestRiskScore
function getLatestRiskScore(uint256 agentId) external view returns (uint8);
この関数は、 external view として定義され、指定した agentId についていま記録されている最新の総合リスクスコア(0〜100)を返します。
ストレージの更新は行わず、オンチェーンにデプロイした記録用コントラクトに対して読み取りだけ行います。
dApp やほかのコントラクトが、エージェント一覧やフィルタに数値だけを載せるときに使います。
引数
| 名前 | 型 | 説明 |
|---|---|---|
agentId |
uint256 |
総合スコアを知りたいエージェントの ID です。 イベントの agentId と同じ番号体系を指します。 |
戻り値
| 型 | 説明 |
|---|---|
uint8 |
コントラクト内に保存されている最新の総合リスク(0〜100)です。 まだ記録が無い場合の挙動は、デプロイした実装の設計に従います。 |
UI からはガスを消さずに「いま幾つと表示すればよいか」だけを引き出す用途を想定しています。
補足
署名・支払い・登録が参照する規格
検証の依頼や手数料の承認でエージェント所有者が署名するとき、EIP155でチェーンを固定してリプレイを防ぎ、EIP191で署名データの形式を揃え、EIP712で agentId やメタデータのハッシュ、nonce、支払い情報をウォレットに読みやすい形で署名ペイロードに含めます。
手数料を取るなら、ガスを肩代わりしつつ安定コインで払う文脈でEIP3009の TransferWithAuthorization が使われます。
登録の単一ソースはERC8004のIdentity Registryで、ERC721の tokenId が agentId です。
EIP155・EIP191・EIP712 については以下の記事を参考にしてください。
検証タイプの並びと役割
コントラクト(ETV)、メディア(MCV)、Solidity(SCV)、HTTPSのエンドポイント(WAV)、ウォレット(WV)の順で、コントラクト・メディア・Solidity・Web・ウォレットに対応します。
リスクスコアの幅とPDV・QCVの狙い
0〜100にそろえるのはエージェント同士を同じ尺度で並べて比べられるようにするためです。
PDV(Private Data Verification)は途中のデータを丸ごと公開せずZKPにまとめます。
個々の送受履歴の内容などはチェーンに書かずに済ませてよい方針です。
QCV(Quantum Cryptography Verification)は必須ではなく、長期保管するデータを対称鍵暗号などで守るためのあとから足せる任意のオプションです。
テストケース
提案の Test Cases に合わせ、Foundry 形式のテスト関数の骨子を示します。
名前とコメントはドラフトの意図に沿った例であり、実装の詳細はプロバイダ側の検証ロジックに依存します。
検証テスト
ここでは、メタデータのフィールドに応じて各タイプの検証が完了すること、PDV/ZKP・総合スコア・イベント emit・カスタムエラーでのリバートなどが、期待どおりかを切り分ける例を並べます。
contractAddress があるとき ETV が完了する
メタデータに contractAddress が含まれるとき、ETV が成功することを確認する。
function testETVCompletesWhenContractAddressPresent() public {
uint256 agentId = 1;
vm.prank(user);
// bytes32 proofId = verifier.verifyETV(agentId);
}
imageUrl があるとき MCV が完了する
imageUrl があるとき MCV が成功することを確認する。
function testMCVCompletesWhenimageUrlPresent() public {
uint256 agentId = 42;
vm.prank(user);
// verifier.verifyMCV(agentId);
}
solidityCode があるとき SCV が完了する
solidityCode があるとき SCV が成功することを確認する。
function testSCVCompletesWhensolidityCodePresent() public {
uint256 agentId = 42;
vm.prank(user);
// verifier.verifySCV(agentId);
}
複数の agentId で WAV が完了する
agentId を複数(例 10, 20, 30)にわたって変え、WAV が正しく実行されることを確認する。
function testWAVCompletesForAllAgentIds() public {
uint256[] memory ids = new uint256[](3);
ids[0] = 10; ids[1] = 20; ids[2] = 30;
for (uint i = 0; i < ids.length; i++) {
vm.prank(user);
// verifier.verifyWAV(ids[i]);
}
}
複数の agentId で WV が完了する
複数の agentId で WV が成功することを確認する。
function testWVCompletesForAllAgentIds() public {
uint256[] memory ids = new uint256[](3);
ids[0] = 10; ids[1] = 20; ids[2] = 30;
for (uint i = 0; i < ids.length; i++) {
vm.prank(user);
// verifier.verifyWV(ids[i]);
}
}
各タイプの PDV / ZKP が生成される
agentId について、すべてのタイプの証明が生成されることを確認する。
function testGeneratesPDVZKPForEachType() public {
uint256 agentId = 100;
vm.prank(user);
// verifier.verifyAgent(agentId);
}
overallRiskScore の平均計算
overallRiskScore の平均計算が正しいことを検証する。
function testCalculatesOverallRiskScore() public {
uint8 score = verifier.calculateRiskScore(10, 20, 30, 40);
assertEq(score, 25);
}
AgentVerified イベントの emit
Proof ID 付きで AgentVerified が emit されることを確認する。
function testEmitsAgentVerifiedEvent() public {
uint256 agentId = 999;
vm.expectEmit(true, true, true, true);
emit AgentVerified(agentId, 45, [bytes32(1), bytes32(2), bytes32(3), bytes32(4)]);
}
VerificationFailed でリバートする
プロバイダ失敗時に VerificationFailed でリバートすることを確認する。
function testRevertsWithVerificationFailed() public {
vm.expectRevert(VerificationFailed.selector);
}
InsufficientCredits でリバートする
クレジットのないユーザーが InsufficientCredits でブロックされることを確認する。
function testRevertsWithInsufficientCredits() public {
vm.prank(poorUser);
vm.expectRevert(InsufficientCredits.selector);
}
アクセス制御
オンチェーンで Proof や履歴を管理するコンポーネントを置く場合に、エージェント所有者などの許可された主体以外が参照できないことを、リバートで確認する例です。
権限のない者による Proof へのアクセス
所有者以外が proofs にアクセスできないようにする。
function testUnauthorizedProofAccess() public {
vm.prank(0xBBBB);
vm.expectRevert(UnauthorizedAccess.selector);
registry.getAgentProofs(1234);
}
リスクスコア
数値のスコアから RiskTier(低・中程度 など)への分類が、しきい値と矛盾しないことを assert で確認する例です。
リスクティアの分類
スコアが RiskTier に正しく対応することを確認する。
function testRiskScoreTiers() public {
assertEq(verifier.getRiskTier(15), RiskTier.Low);
assertEq(verifier.getRiskTier(35), RiskTier.Moderate);
}
互換性
ERC8004に登録したエージェントなら、この提案どおりの検証の流れをそのまま使えます。
別のトークン規格を壊す必要はありません。
ERC8004を使っていない実装はIdentity Registryへmintし、tokenURI 先に仕様どおりのJSONを置く必要があります。
参考実装
この実装がすること
ERC8126(ERCS/erc-8126.md)に TypeScript と viem の参考実装があります。
参考実装が示す処理の骨格は以下です。
-
Identity Registry から tokenURI を読み、JSON を取得する
resolveAgentMetadataが viem でtokenURIを呼び、返った URI をfetchして JSON にする。 -
コントラクト(ETV)、メディア(MCV)、Solidity(SCV)、HTTPS(WAV)、ウォレット(WV)の5検証を Promise.all で並べる
5つは同時実行で、メタデータをそれぞれが読む。 -
平均リスク・ティア・PDV 用の ID を求め、任意で Validation Registry に関わる提出処理に進み、結果オブジェクトを返す
validationRegistryが無ければ Registry 向けの処理は飛ぶ。
getAddress の扱いは以下を前提にします。
- チェックサム付きアドレス用の関数ですが、このコードでは
imageUrlやsolidityCodeにも当てています。
仕様上は URL やソース文字列の想定なので、本番に近いコードではフィールドの種類に合わせて文字列として扱います。
フロー図
図は verifyAgent の段階を左から右へボックスで並べています。
各箱は次の番号付き手順と対応させて読み、細部はコードとあわせてください。
図中の流れは以下です。
-
メタデータを取得する
agentIdと ERC8004 の Identity Registry からtokenURIを解決し、返った URI 先の JSON を読む。 -
Promise.all で5検証(ETV・MCV・SCV・WAV・WV)を並列に実行する
各検証は固定スコアとハッシュ由来の proofId を返す形の例である。 -
平均リスクとティアを求める
5つのスコアを平均し、閾値でティアに割り振る。 - PDV 用の識別子を作る
- 任意で QCV を実行する
-
設定があるときだけ Validation Registry に関わる処理に進む
参考実装ではsubmitToValidationRegistryが PDV 識別子を返すだけである。 - 結果オブジェクトを返す
参考実装では、入口から戻り値まで一続きで追えるようにしています。
import crypto from 'crypto';
import { createPublicClient, http, getAddress, parseAbi } from 'viem';
class ERC8126Error extends Error {
constructor(
public code: number,
message: string
) {
super(message);
this.name = 'ERC8126Error';
}
}
enum VerificationStatus {
Passed = 'passed',
Failed = 'failed',
Inconclusive = 'inconclusive',
}
enum RiskTier {
Low = 'low',
Moderate = 'moderate',
Elevated = 'elevated',
HighRisk = 'high',
Critical = 'critical',
}
interface AgentMetadata {
contractAddress?: string;
solidityCode?: string;
url?: string;
walletAddress?: string;
chain_id?: number;
[key: string]: any;
}
interface VerificationProviderConfig {
chainId?: number;
identityRegistry?: string;
validationRegistry?: string;
rpcUrl?: string;
}
interface VerificationResult {
type: string;
status: VerificationStatus;
score: number;
proofId: string;
}
const ERC8004_ABI = parseAbi([
'function tokenURI(uint256 tokenId) external view returns (string)',
]);
async function resolveAgentMetadata(
agentId: bigint,
config: VerificationProviderConfig
): Promise<AgentMetadata> {
if (!config.identityRegistry) {
throw new ERC8126Error(0x03, 'ERC-8004 Identity Registry address is required');
}
if (!config.rpcUrl) {
throw new ERC8126Error(0x04, 'RPC URL is required to resolve ERC-8004 metadata');
}
const client = createPublicClient({
chain: { id: config.chainId || 1, name: 'Ethereum', nativeCurrency: { name: 'ETH', symbol: 'ETH', decimals: 18 } },
transport: http(config.rpcUrl),
});
try {
const uri = await client.readContract({
address: getAddress(config.identityRegistry),
abi: ERC8004_ABI,
functionName: 'tokenURI',
args: [agentId],
});
if (!uri || typeof uri !== 'string') {
throw new ERC8126Error(0x05, 'Invalid tokenURI returned from ERC-8004 registry');
}
const response = await fetch(uri);
if (!response.ok) {
throw new ERC8126Error(0x06, `Failed to fetch metadata from ${uri}`);
}
const metadata: AgentMetadata = await response.json();
return {
contractAddress: metadata.contractAddress ? getAddress(metadata.contractAddress) : undefined,
imageUrl: metadata.imageUrl ? getAddress(metadata.imageUrl) : undefined,
solidityCode: metadata.solidityCode ? getAddress(metadata.solidityCode) : undefined,
walletAddress: metadata.walletAddress ? getAddress(metadata.walletAddress) : undefined,
url: metadata.url,
chain_id: metadata.chain_id,
...metadata,
};
} catch (error) {
if (error instanceof ERC8126Error) throw error;
throw new ERC8126Error(0x07, `Metadata resolution failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async function ETV(m: AgentMetadata, c: VerificationProviderConfig): Promise<VerificationResult> {
return {
type: 'ETV',
status: VerificationStatus.Passed,
score: 25,
proofId: generateProofId('ETV', m.contractAddress || ''),
};
}
async function MCV(m: AgentMetadata, c: VerificationProviderConfig): Promise<VerificationResult> {
return {
type: 'MCV',
status: VerificationStatus.Passed,
score: 30,
proofId: generateProofId('MCV', m.imageUrl || ''),
};
}
async function SCV(m: AgentMetadata, c: VerificationProviderConfig): Promise<VerificationResult> {
return {
type: 'SCV',
status: VerificationStatus.Passed,
score: 30,
proofId: generateProofId('SCV', m.solidityCode || ''),
};
}
async function WAV(m: AgentMetadata): Promise<VerificationResult> {
return {
type: 'WAV',
status: VerificationStatus.Passed,
score: 15,
proofId: generateProofId('WAV', m.url || ''),
};
}
async function WV(m: AgentMetadata): Promise<VerificationResult> {
return {
type: 'WV',
status: VerificationStatus.Passed,
score: 20,
proofId: generateProofId('WV', m.walletAddress || ''),
};
}
function calculateOverallRiskScore(scores: number[]): number {
const valid = scores.filter((s) => s >= 0 && s <= 100);
return valid.length ? Math.round(valid.reduce((a, b) => a + b, 0) / valid.length) : 0;
}
function getRiskTier(score: number): RiskTier {
if (score <= 20) return RiskTier.Low;
if (score <= 40) return RiskTier.Moderate;
if (score <= 60) return RiskTier.Elevated;
if (score <= 80) return RiskTier.HighRisk;
return RiskTier.Critical;
}
function generatePDVProof(result: any): string {
return '0x' + crypto.createHash('sha256').update(JSON.stringify(result) + Date.now().toString()).digest('hex');
}
async function QCV(data: any) {
return {
recordId: '0x' + crypto.createHash('sha256').update(Date.now().toString()).digest('hex'),
algorithm: 'AES-256-GCM',
};
}
export async function verifyAgent(agentId: bigint, config: VerificationProviderConfig) {
const metadata = await resolveAgentMetadata(agentId, config);
const [etv, mcv, scv, wav, wv] = await Promise.all([
ETV(metadata, config),
MCV(metadata, config),
SCV(metadata, config),
WAV(metadata),
WV(metadata),
]);
const overallRiskScore = calculateOverallRiskScore([etv.score, mcv.score, scv.score, wav.score, wv.score]);
const riskTier = getRiskTier(overallRiskScore);
const pdvProofId = generatePDVProof({ etv, mcv, scv, wav, wv, overallRiskScore, agentId: agentId.toString() });
const qcvRecord = await QCV({ overallRiskScore, pdvProofId });
const validationRecord = config.validationRegistry
? await submitToValidationRegistry(agentId, overallRiskScore, pdvProofId, config)
: null;
return {
agentId,
overallRiskScore,
riskTier,
etv,
mcv,
scv,
wav,
wv,
pdvProofId,
qcvRecord,
validationRecord,
verifiedAt: new Date().toISOString(),
};
}
async function submitToValidationRegistry(
agentId: bigint,
score: number,
pdvProofId: string,
config: VerificationProviderConfig
) {
return pdvProofId;
}
function generateProofId(type: string, data: string): string {
const input = `${type}:${data}:${Date.now()}`;
return '0x' + crypto.createHash('sha256').update(input).digest('hex');
}
export { verifyAgent };
型とエラーの見方
主な型の役割は以下です。
| 型 | 役割 |
|---|---|
ERC8126Error |
数値コードとメッセージで失敗理由を区別します。 |
VerificationStatus |
各検証の成否(合格・不合格・判定不能)を表します。 |
RiskTier |
総合スコアを段階ラベルに落とします。 |
ここに出てくる値は例示であり、実際の合格・不合格のルールは別の実装で書きます。
メタデータの解決とプロバイダ設定
resolveAgentMetadata の流れは以下です。
-
identityRegistry と rpcUrl が無ければエラーにする
ERC8126Errorのコード0x03と0x04がそれぞれに対応する。 -
viem で tokenURI だけの最小 ABI を呼び、URI を得る
readContractでレジストリから文字列の URI を読む。 -
その URI を fetch し、JSON をパースする
response.okで無いときは0x06、JSON 以外の失敗はcatchで0x07にまとめる。 -
いずれの失敗も ERC8126Error にまとめる
すでにERC8126Errorならそのまま再投げする。
ERC8004 の tokenURI 先の JSON から、5検証が参照するフィールドを AgentMetadata にまとめます。
AgentMetadata の主なフィールドと検証との対応は以下です。
| フィールド(参考実装) | 主に使う検証 | 説明 |
|---|---|---|
contractAddress |
ETV(コントラクト) | 対象コントラクトのアドレス。 |
imageUrl |
MCV(メディア) | 画像などの URL。コードでは getAddress が当たっている行もあるが、本来は文字列として扱う想定。 |
solidityCode |
SCV(Solidity) | ソース文字列。 |
url |
WAV(HTTPS) | 検証対象の HTTPS URL。 |
walletAddress |
WV(ウォレット) | ウォレットアドレス。 |
chain_id など |
— | その他。インデックスシグネチャにより仕様にないキーも受け取れる。 |
VerificationProviderConfig のフィールドは以下です。
| フィールド | 意味 |
|---|---|
chainId |
チェーン ID。 |
identityRegistry |
ERC8004 の Identity Registry のアドレス。 |
validationRegistry |
任意。Validation Registry。 |
rpcUrl |
JSON-LD を取りに行く RPC。 |
各検証の戻りと総合スコア
参考実装では、各専門検証は静的解析やメディア検査の本体の代わりに、固定スコアと種別・入力・現在時刻から作ったハッシュ文字列を証明識別子として返します。
各検証の要点は以下です。
| 検証 | 固定スコア(参考) | 第二引数にプロバイダ設定 | proofId の主な入力 |
|---|---|---|---|
| ETV(コントラクト) | 25 | 受け取る | contractAddress |
| MCV(メディア) | 30 | 受け取る | imageUrl |
| SCV(Solidity) | 30 | 受け取る | solidityCode |
| WAV(HTTPS) | 15 | 受け取らない | url |
| WV(ウォレット) | 20 | 受け取らない | walletAddress |
集計と付随処理の内容は以下です。
| 内容 | 参考実装での扱い |
|---|---|
| 総合スコアとティア | 0〜100 のスコアだけを算術平均し、20 点刻みでティアに振り分ける。 |
| PDV 用の文字列 | 結果に時刻を混ぜた SHA-256。サーキット付きの ZKP ではない。 |
| QCV | 返却形の例示にとどまり、本番相当の検査は含まない。 |
verifyAgent から返却まで
外部から呼び出す入口は verifyAgent のみです。
関数内の処理の順序は以下です。
-
resolveAgentMetadata でメタデータを得る
identityRegistryとrpcUrlを確認してからreadContractでtokenURIを読み、URI をfetchして JSON にし、失敗はERC8126Errorに揃える。 -
Promise.all で ETV・MCV・SCV・WAV・WV を呼ぶ
ETV・MCV・SCV はconfigを第2引数に渡し、WAV・WV はメタデータだけ渡す。 -
calculateOverallRiskScore で平均を取り、getRiskTier でティアを決める
5つのスコアを 0〜100 の範囲にそろえてから平均し、ティアは 20 点刻みの閾値である。 -
generatePDVProof で PDV 用 ID を作る
中間結果とagentIdを JSON 化し、時刻を混ぜて SHA-256 する。 -
QCV を実行する
返りは例示用の recordId とアルゴリズム名である。 -
validationRegistry が設定されていれば submitToValidationRegistry を呼ぶ(PDV 識別子を返すだけでありオンチェーン送信は含まない)
未設定ならこの分岐はスキップしvalidationRecordは null である。 -
結果オブジェクトを返す
agentId、総合とティア、各検証結果、PDV・QCV・Registry 用の値、verifiedAtを含む。
補足は以下です。
| 項目 | 内容 |
|---|---|
| 各専門検証の proofId | 種別・入力・現在時刻のハッシュで、再現性はない。 |
| Validation Registry 向けの戻り | PDV 識別子を返すだけにとどまる。 |
| 本番の Registry 連携 | ERC8004 の Validation Registry に合わせたトランザクションや社内 API は運用で差し替える。 |
セキュリティ
検証の意味とウォレットとURL
検証はその時点の技術的なチェックにすぎず、あとからの振るまいや意図までは保証しません。
スコアは目安で、最後は利用者が判断します。
登録に使ったウォレットを奪られると、正規のエージェントのふりをされるおそれがあります。
秘密鍵を他人に知られないよう守る必要があります。
登録後にURLが乗っ取られると、悪意のある内容を配信される可能性があります。
利用前に最新の状態を確認し、WAVで再検証して異常を拾うことが重要です。
コントラクトを触るとき
contractAddress を登録しているエージェントではふつうのスマートコントラクトと同様のリスクが残ります。
ETVやSCVは入口の確認にすぎないので、実際に触るコードは別途レビューや監査を検討すべきと提案に書かれています。
ZKPと量子計算
PDV が出す ZKP を実運用で頼れるものにするには使う回路が監査され、セットアップが改ざんされにくいことが必要です。
多者が参加するセレモニーで分散したり、オンチェーン検証には実績のある検証器を使うといった指針があります。
楕円曲線に基づく ZKP は長期的には量子計算の進展も意識します。
価値の高いデータを長く保管する場合、楕円曲線のZKPだけでは将来の量子計算が心配になる点に対し、QCVで対称鍵暗号など別の仕組みを重ねる案が提案に含まれます。
プロバイダの信頼と攻撃シナリオ
プロバイダのサービス内容や独立性はまちまちなので、利用者が選び、PDVのZKPを第三者が検証できる点も材料にします。
ERC8004で大量のエージェントを作るシビルはmintコストやレピュテーションで抑えられる一方、プロバイダが悪意のあるエージェントと組んで不当に高い評価を出すといったリスクも書かれています。
リスクの高い用途では独立したプロバイダを複数使うのが望ましいとされています。
レジストリ依存
ERC8004のレジストリを使う以上、正しいデプロイアドレスを使い、tokenURI の内容が改ざんされていないかは運用で見る必要があります。
最後に
ERC8004のIdentity Registryに登録されたエージェントを起点に、オフチェーンでコントラクト(ETV)、メディア(MCV)、Solidity(SCV)、HTTPSのエンドポイント(WAV)、ウォレット(WV)の5つの検証とPDV、0〜100のリスク、任意のValidation Registryまでを一続きで扱う枠組みでした。
エージェントをどこまで信頼するかを共通の指標で扱う話はこれからも重くなると思います。
提案本文・Ethereum Magicians・公式図のSVGは以下です。
他でも色々記事を書いているのでぜひよろしければ読んでいってください!







