はじめに

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

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

AIエージェントへ少額の支払いを任せる時、所有者は「どの送付先へ、いくらまで、いつまで送ってよいか」を先に決めます。 しかし、エージェントやそのホストへ秘密鍵を渡すと、許可していない送付まで止められません。

ERC8196は、所有者の許可条件とエージェントが提示する署名済みの操作を照合し、条件を満たす時だけコントラクト呼び出しを実行するウォレットの提案です。 秘密鍵をエージェントやホストへ渡さず、実行結果とは別に後から確認できる監査記録を残します。

ERC8196の仕様は以下です。

概要

検証層と実行層

例えば、所有者が「このエージェントは指定したDEXへ0.01ETHまで、今日の18時まで送れる」と登録したとします。 エージェントが出す操作要求は、その条件を指すハッシュ、送付先、金額、有効期限、使い切り番号を署名とともに渡します。 ウォレットは登録済みの条件と現在の評価を読み、どれか1つでも合わなければ送付先コントラクトを呼び出しません。

スコアは0〜20で、値が低いほど低リスクです。 ウォレットは、今の評価が所有者の許容上限を超えた要求を実行前に拒否します。

所有者の条件と、実行時に確認する現在の検証結果は別々に作られます。 前者は「何を許すか」、後者は「このエージェントを今も使ってよいか」を表します。 ウォレットは両方を実行前に読むため、エージェントが出した要求だけで送付先へ進みません。

trust-stack-v2-final.png

委任の流れ

ウォレットは、操作を出したAIエージェントについて、過去の危険な資金移動や悪性活動の兆候がないかを先に確認します。 この確認結果をエージェントごとに読み出す基盤がERC8126です。 ERC8196は、その現在の評価を所有者が決めた上限と比べ、条件外の要求を実行前に止めます。 つまり、ERC8126が評価を返し、ERC8196が資産を動かす直前の許可判断を担当します。

ERC8126については以下の記事を参考にしてください。

動機

鍵を渡さない委任

AIエージェントへ資産操作を委任すると、ホストが秘密鍵を取得する危険、許可範囲が広すぎる危険、過去に悪性活動を持つエージェントを使う危険が生じます。 さらにホストは、エージェントの出力を隠す、要求を遅らせる、確率的な問い合わせを繰り返すことで、実行する操作へ影響できます。

delegation-boundary-v2-final.png

ERC8196は、ポリシーで実行範囲を固定し、エージェントのリスク評価、署名、nonce、有効期限を実行時に確認します。 ここでポリシーは所有者が登録する許可条件であり、ホストが都合よく書き換える入力ではありません。 成功した操作は前の記録のハッシュを含む監査エントリへつなぐため、途中の記録だけを差し替えると連鎖が一致しなくなります。

ホスト操作と再利用

有効な署名だけでも、ホストが要求を遅らせたり、確率的な問い合わせを繰り返したりする危険は残ります。 期限は古い要求を止め、nonceは同じ要求の再送を止めます。 entropyCommitmentは、先に固定したランダム値と実行時に示す値を照合するための値です。 高価値の操作では、互いに独立した複数ホストを使い、1つのホストだけで出力やタイミングを左右できない構成にします。

仕様

ポリシーと署名は、エージェントが何をいつどこまで実行できるかを分けて表します。 実行側はポリシーの有効性と現在の評価を確認し、成功した操作を監査記録へ追加します。

署名と登録済みポリシーを照合して実行先へ渡す境界を示します。 各EIP712型の全フィールドは、この後の本文で確認します。

policy-signature-binding-v2-final.png

ポリシーと署名

ポリシーは、所有者が1度登録しておく「実行してよい操作の枠」です。 実行時の要求そのものではないため、送付先、金額、期限、エージェント、現在の評価に使う識別子を分けて保存します。

項目 内容
policyId 一意なポリシー識別子です。提案は必須項目としますが、下記interfaceには入力として現れないため、生成・保存方法は実装が定義します。
agentAddress 許可するAIエージェントのアドレスです。
agentId ERC8126のgetLatestRiskScoreで照会する識別子です。
ownerAddress 資産を委任する所有者です。
allowedActions 許可する操作名の配列です。
allowedContracts 実行先として許可するコントラクトの配列です。
blockedContracts 実行先として拒否するコントラクトの配列です。
maxValuePerTx 1トランザクションで許可する最大価値です。単位はweiです。
maxValuePerDay 1日あたりの価値上限です。任意項目です。
validAfter ポリシーが有効になる時刻です。
validUntil ポリシーが期限切れになる時刻です。
minVerificationScore 0〜20の許容上限です。現在値がこの値を超える場合は拒否します。

AgentAction型は、エージェントが実行を求める1件の操作を署名対象へ並べる形式です。 agent、action、target、value、data、nonce、validUntil、policyHash、entropyCommitmentを1つの署名に含めます。 特にpolicyHashを含めるため、少額送付だけを許すポリシー向けの署名を、別のポリシーで使い回せません。

Delegation型は、誰が誰へどのポリシーを使わせるかを署名する形式です。 delegator、delegatee、policyHash、validUntil、nonceを一体として署名します。 ただし、Delegationを実行interfaceへどう渡し、どの関数で検証するかは規格が固定していません。

一方でexecuteAction関数の引数にはactionがありません。 規格のinterfaceだけでは、allowedActionsと署名したactionをどの保存値から復元して照合するかは定めていません。 実装はこの照合方法、policyIdの生成方法、日ごとの上限を集計する保存先を明示します。

EIP712については以下の記事を参考にしてください。

IAIAgentAuthenticatedWallet

IAIAgentAuthenticatedWalletは、ポリシー登録、実行、失効、照会を定めるinterfaceです。 実装は操作を実行する前にERC8126による検証を行い、ポリシーのスコア上限を強制します。

execution-checks-v2-final.png

registerPolicy

js
function registerPolicy(address agent, uint256 agentId, string[] calldata allowedActions, address[] calldata allowedContracts, address[] calldata blockedContracts, uint256 maxValuePerTx, uint256 maxValuePerDay, uint256 validAfter, uint256 validUntil, uint8 minVerificationScore) external returns (bytes32 policyHash);

所有者がエージェントの許可範囲を登録し、実行署名へ束縛するpolicyHashを返す関数です。 返された値は、後でエージェントが持ち込む操作要求と、ここで登録した許可条件が同じ組であることを確認するために使います。

  1. 最初の処理

    実装はエージェント、許可操作、対象コントラクト、価値上限、有効期間、スコア上限を受け取り、値の整合性と登録者の認可を確認します。

  2. ポリシーを保存する

    実装はポリシーを保存し、実行時に参照できるpolicyHashを導出します。

  3. 結果を返す、または処理を終える

    実行時に参照するpolicyHashを返します。

  • 引数
引数 詳細
agent 許可するAIエージェントのアドレスです。
agentId ERC8126のリスク照会に使う識別子です。
allowedActions 許可する操作名です。照合方法は実装が定義します。
allowedContracts 許可する実行先コントラクトです。
blockedContracts 拒否する実行先コントラクトです。
maxValuePerTx 1回の実行で許可する最大価値です。
maxValuePerDay 1日あたりの価値上限です。
validAfter ポリシーが有効になる時刻です。
validUntil ポリシーが期限切れになる時刻です。
minVerificationScore 現在のERC8126スコアが超えてはならない上限です。
  • 戻り値
戻り値 詳細
policyHash 登録済みポリシーを署名へ束縛するハッシュです。

実行条件

確認する項目 条件と満たさない時の結果
ポリシーを登録する主体 規格は所有者のポリシーとして扱いますが、呼び出し元をどう認可するかは実装が定義します。実装は第三者が他人の許可範囲を登録できないように確認します。
期限と上限の整合性 validAfter、validUntil、1回および1日あたりの上限を保存前に確認する設計が必要です。規格は具体的な検査式やrevertするエラーを定めません。
識別子と保存形式 policyIdの生成、policyHashからどの値を読めるように保存するかは実装が決めます。ここが曖昧な実装では後の実行判定が再現できません。

executeAction

js
function executeAction(bytes32 policyHash, address target, uint256 value, bytes calldata data, uint256 nonce, bytes32 entropyCommitment, bytes calldata signature) external returns (bool success, bytes32 auditEntryId);

ポリシー、現在のリスク評価、署名を検証してからエージェント操作を実行する関数です。 この関数が実行先を呼び出す前に止まることが、ホストやエージェントが所有者の枠を越えないための中心になります。

  1. 最初の処理

    ポリシーの有効期間、対象、価値、nonce、署名を確認し、agentIdで得た現在のERC8126スコアがminVerificationScoreを超えた場合は拒否します。

  2. 操作と監査記録を追加する

    実装は条件を満たした操作だけを実行し、previousHashを含む監査エントリを追加します。

  3. 結果を返す、または処理を終える

    成否とauditEntryIdを返します。

  • 引数
引数 詳細
policyHash 適用する登録済みポリシーです。
target 実行先コントラクトです。
value 実行時に渡す価値です。
data 実行先へ渡す呼び出しデータです。
nonce リプレイを防ぐ一意の値です。
entropyCommitment 確率的な操作に対するホストの影響を抑えるコミットメントです。
signature ポリシーへ束縛したEIP712署名です。
  • 戻り値
戻り値 詳細
success 実行の成否です。
auditEntryId 監査ハッシュ連鎖のエントリIDです。

実行条件

確認する項目 条件と満たさない時の結果
ポリシーの有効状態 policyHashが登録済みで、validAfter以後かつvalidUntil以前であり、失効していないことを確認します。期限切れならPolicyExpiredエラーで実行を止めます。
実行先と送付額 targetが許可先に入り拒否先に入らず、valueがmaxValuePerTxを超えないことを確認します。上限超過はValueExceedsLimitエラーで止めます。日ごとの上限の集計方法は実装が定めます。
エージェントの現在の評価 ポリシーのagentIdでERC8126の現在のスコアを取得し、minVerificationScore以下であることを確認します。超える要求は実行しません。悪性活動を示す検証結果がある時の拒否・失効も実装します。
署名と使い切り番号 signatureから復元する署名者と期待する署名者を照合し、nonceを未使用として確認します。不一致はInvalidSignature、再利用やその他の条件違反はPolicyViolationで止める設計になります。
操作名の照合 allowedActionsと署名したactionをどう対応させるかは規格が固定しません。実装は復元元と比較方法を定め、照合できない要求を実行しません。

revokePolicy

js
function revokePolicy(bytes32 policyHash, string calldata reason) external;

登録済みポリシーを失効させるために、対象のpolicyHashと失効理由を受け取る関数です。 実装は失効状態を保存し、同じpolicyHashを使う以後のexecuteAction関数を入口で拒否します。

  1. 最初の処理

    実装はpolicyHashでポリシーを特定し、呼び出し元が失効させる権限を持つか確認します。

  2. ポリシーを無効にする

    ポリシーを無効にし、以後の実行判定から除外します。

  3. 結果を返す、または処理を終える

    以後の実行でそのポリシーを拒否して終えます。

  • 引数
引数 詳細
policyHash 失効させるポリシーです。
reason 失効理由です。

実行条件

確認する項目 条件と満たさない時の結果
失効を要求する主体 誰が失効できるかは規格が固定しません。実装は少なくとも所有者以外が他人のポリシーを無効にできない認可を置きます。
対象ポリシー policyHashから有効なポリシーを特定します。存在しない値や既に失効した値をどう扱うか、PolicyRevokedイベントをいつ発行するかは実装が定義します。

getPolicy

js
function getPolicy(bytes32 policyHash) external view returns (address agent, address owner, uint256 maxValuePerTx, uint256 validUntil, bool isActive);

登録済みポリシーの実行判断に必要な要約値を返す参照関数です。 返されるのはエージェント、所有者、1回上限、期限、有効状態だけであり、許可先一覧や日次上限まで復元できるとは限りません。

  1. 最初の処理

    policyHashで登録済みポリシーを特定します。

  2. 要約値を読み出す

    エージェント、所有者、1回あたりの上限、有効期限、有効状態を読み出します。

  3. 結果を返す、または処理を終える

    読み出した5つの値を返します。

  • 引数
引数 詳細
policyHash 照会するポリシーです。
  • 戻り値
戻り値 詳細
agent 許可するエージェントです。
owner 資産を委任した所有者です。
maxValuePerTx 1回あたりの価値上限です。
validUntil 有効期限です。
isActive ポリシーが有効かを示す値です。

実行条件

確認する項目 条件と満たさない時の結果
照会対象 policyHashでポリシーを特定します。存在しないハッシュに返す値やrevertするかは規格が固定しないため、利用側は実装の仕様を確認します。

PolicyRegistered

js
event PolicyRegistered(bytes32 indexed policyHash, address indexed owner, address indexed agent, uint256 validUntil);

PolicyRegisteredイベントは、実装がポリシーの登録を外部の監視サービスへ知らせる時に発行するイベントです。 イベントログからpolicyHash、所有者、エージェントで絞り込めますが、許可先や金額上限の全内容はこのイベントだけでは復元できません。

  • フィールド
フィールド 詳細
policyHash インデックス付きで記録するポリシーのハッシュです。
owner インデックス付きで記録する所有者です。
agent インデックス付きで記録するエージェントです。
validUntil ポリシーの有効期限です。

ActionExecuted

js
event ActionExecuted(bytes32 indexed policyHash, address indexed agent, address target, uint256 value, bytes32 auditEntryId);

ActionExecutedイベントは、許可された操作を実行した時に発行するイベントです。 監視サービスはpolicyHashとagentを起点に、どのtargetへどのvalueを渡したかを追えます。 ただし、呼び出しデータの中身や実行先が返した結果はフィールドに含まれないため、必要ならトランザクション入力とコントラクトの保存値を別途確認します。

  • フィールド
フィールド 詳細
policyHash インデックス付きで記録する適用ポリシーです。
agent インデックス付きで記録する実行エージェントです。
target 実行先コントラクトです。
value 実行時に渡した価値です。
auditEntryId 対応する監査エントリの識別子です。

PolicyRevoked

js
event PolicyRevoked(bytes32 indexed policyHash, string reason);

PolicyRevokedイベントは、ポリシーを失効させた時に発行するイベントです。 イベントログを読む側は失効理由を表示できますが、失効が実行を止める根拠はイベントではなく、ウォレットが保存する有効状態です。

  • フィールド
フィールド 詳細
policyHash インデックス付きで記録する失効ポリシーです。
reason 失効理由です。

AuditEntryLogged

js
event AuditEntryLogged(bytes32 indexed entryId, uint256 sequence, bytes32 sessionId, string actionType);

AuditEntryLoggedイベントは、監査ハッシュ連鎖へエントリを追加した時に発行するイベントです。 entryId、sequence、sessionIdを使うと、同じ委任セッションの順序をイベントログからたどれます。 一方、改変検出に必要なpreviousHashはこのイベントのフィールドではないため、実装が保管する監査エントリ本体を取得して連鎖を検証します。

  • フィールド
フィールド 詳細
entryId インデックス付きで記録する監査エントリIDです。
sequence セッション内の順序です。
sessionId 監査対象のセッション識別子です。
actionType 記録した操作種別です。

監査とエラー

監査エントリは、成功した操作について前の記録を指すpreviousHashを持ちます。 実装はエントリをオンチェーンへ直接保存するか、オフチェーンへ保管してMerkle rootをオンチェーンへ固定するかを選べます。 どちらを選んでも、照合する側がエントリ本体と前後のハッシュを取得できなければ、途中の改変は検出できません。

PolicyExpired

js
error PolicyExpired(bytes32 policyHash, uint256 validUntil);

PolicyExpiredエラーは、有効期限を過ぎたポリシーによる実行を拒否します。

  • 引数
引数 詳細
policyHash 期限切れのポリシーです。
validUntil そのポリシーの有効期限です。

ValueExceedsLimit

js
error ValueExceedsLimit(uint256 value, uint256 maxValue);

ValueExceedsLimitエラーは、実行価値がポリシーの上限を超えた操作を拒否します。

  • 引数
引数 詳細
value 要求された価値です。
maxValue ポリシーが許可する最大価値です。

InvalidSignature

js
error InvalidSignature(address recovered, address expected);

InvalidSignatureエラーは、復元した署名者が期待する署名者と一致しない操作を拒否します。

  • 引数
引数 詳細
recovered 署名から復元したアドレスです。
expected 期待する署名者のアドレスです。

EntropyVerificationFailed

js
error EntropyVerificationFailed(bytes32 commitment, bytes32 revealed);

EntropyVerificationFailedエラーは、エントロピーの公開値がコミットメントと一致しない操作を拒否します。

  • 引数
引数 詳細
commitment 事前に固定したコミットメントです。
revealed 実行時に提示した公開値です。

PolicyViolation

js
error PolicyViolation(bytes32 policyHash, string reason);

PolicyViolationエラーは、ポリシーに適合しない操作を拒否します。

  • 引数
引数 詳細
policyHash 違反したポリシーです。
reason 拒否理由です。

補足

検証の責務分担

ERC8126は、エージェントを識別し、検証結果とリスク評価を読むための前段です。 ウォレットはポリシーに保存したagentIdでその結果を取得し、現在の評価が所有者の上限を越えていないか判断します。 ERC8196は、その判断に加え、送付先、金額、期限、署名、nonceを照合して、今この操作を通すかを決めます。

送信前の確認

ホスト、ウォレットUI、relayerも送信前に同じ検証を行えます。 この確認は利用者へ早く拒否理由を見せ、無効なトランザクションを送らないために役立ちます。 ただし、ホストやUIは信頼境界の外にあるため、オンチェーンのexecuteAction関数も実行直前に同じ種類の条件を確認します。

互換性

ERC4337ウォレット

ERC8196はERC4337ウォレットと既存規格に互換であり、既存の実行モデルを壊しません。 ERC4337はユーザー操作をUserOperationとしてBundlerへ渡し、アカウント自身が検証して実行できるウォレットの枠組みです。 そのアカウント、またはアカウントから呼ばれるポリシー用モジュールがIAIAgentAuthenticatedWalletを実装すると、通常の実行経路へ委任条件の確認を追加できます。

ERC8196はERC8126による検証を実行前に追加します。 オフチェーンのホスト、ウォレットUI、relayerも、トランザクションやUserOperationを送る前に同じ評価を確認します。

ERC4337については以下の記事を参考にしてください。

署名の前提規格

EIP712は、送付先、金額、期限、ポリシーのハッシュのように複数の値を、何の値か分かる構造のまま署名する形式です。 ウォレットは復元した署名者とポリシーを照合し、別の値へ差し替えられていないかを確認できます。 EIP191は署名済みデータの形式を定め、EIP155はチェーンIDを使い別チェーンでのトランザクション再利用を防ぎます。

EIP191については以下の記事を参考にしてください。

EIP155については以下の記事を参考にしてください。

セキュリティ

再利用と改変の検出

実装はnonceをポリシーまたは署名者ごとに1度しか使えない値として保存し、有効期限を過ぎた要求を実行前に拒否します。 この2つを分けることで、同じ署名をもう1度送る攻撃と、以前は有効だった署名を時間を置いて送る攻撃の両方を止めます。

監査エントリは前のエントリのpreviousHashを含みます。 監査用の保存先から連鎖を読み直す時、途中のエントリだけを改変すると後続エントリに保存済みのハッシュと一致しません。 規格はエントリをオンチェーンに全件保存する方法を固定せず、オフチェーンへ保存してMerkle rootだけをオンチェーンへ定期記録する方法も許容します。

audit-revocation-v2-final.png

ERC8126のWallet Verificationが制裁対象資金、mixer利用、悪性アドレスとのクラスタリング、強い自動化指標などの悪性活動を示した場合、ウォレットは総合スコアが上限以下でも委任を拒否または失効させます。 スコアだけを通行証として使わず、検証結果の個別の危険信号も実行判断へ入れるためです。

検証結果と所有者の停止権

所有者はrevokePolicy関数で委任を止められます。 失効後に受け取った署名済み要求も、ウォレットが保存したポリシーの有効状態を確認して実行しません。 この停止はホストの画面だけで行うものではなく、実行コントラクトの判定に残します。

また、ERC8126のWallet Verificationが制裁対象資金、mixer利用、悪性アドレスとのクラスタリング、強い自動化指標を示した時は、総合スコアが上限以下でも委任を拒否または失効させます。 スコアを低く見せる入力だけで実行を通さないため、実装はWallet Verificationの個別の危険信号をどの保存値で受け取り、どの時点で拒否するかを定めます。 ただし、検証データの収集範囲、更新間隔、誤判定をどう扱うかはERC8196のinterfaceにはありません。 所有者はこの境界を踏まえ、失効、再検証、承認の運用を別途用意します。

ホスト操作への備え

ホストを完全に信頼する設計にはしません。 ポリシー、署名、実行前検証、失効をオンチェーンで確認し、ホストが自分だけで送付先を変更できないようにします。 一方、確率的なAIエージェントの出力を何度も引き直す操作はオンチェーンの検証だけでは防げません。 高価値の操作では、複数ホストで同じ要求を照合するか、ホストごとに独立した承認経路を置きます。

例えば、単一ホストが最初の出力を捨て、送付先を変える出力が現れるまで再サンプリングしても、署名の検証だけではその試行回数を観測できません。 entropyCommitmentは実行時に示す値が事前に固定した値と一致するかを確かめますが、どの入力でエージェントを実行したかまでは証明しません。 そのため、複数ホストの独立した記録、金額に応じた追加承認、監査ログの照合を組み合わせ、ホストが操作した余地を後から調査できるようにします。

最後に

AIエージェントへ資産操作を委任する時、所有者は許可先、金額、期限、エージェントをポリシーとして登録します。 ERC8196のウォレットは、現在のERC8126評価、署名、nonce、有効期限を実行前に照合し、条件を満たす要求だけを実行先へ渡します。 さらに失効状態と監査ハッシュ連鎖を持つため、所有者は委任を止め、成功した操作の履歴を後から検証できます。

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