1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[EIP8130] Account ConfigurationでネイティブAAを実現する仕組みを理解しよう!

1
Posted at

はじめに

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

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

今回は「オンチェーンのアカウント設定と新しいトランザクションタイプでアカウント抽象化を実現するEIP8130」についてまとめていきます。

EIP8130は、ウォレットコードを毎回シミュレーションしてトランザクションを受け入れるのではなく、署名検証を担う Verifier と所有者設定をオンチェーンで明示する提案です。
署名方式の追加、バッチ実行、ガススポンサー、既存EOAからの移行を、プロトコルが事前検証しやすいトランザクション構造に組み込みます。

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

提案では AA_TX_TYPE や主要なシステムアドレスが TBD のままです。
値そのものではなく、どの役割をどこへ分けているかを中心に見ていきます。

概要

EIP8130は、アカウント抽象化をネイティブトランザクションとして扱うための提案です。
新しい EIP2718 型トランザクションと、アカウントごとの所有者設定を管理する Account Configuration Contract を組み合わせます。

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

この提案の中心は、署名検証をウォレット実装から切り離すことです。
トランザクションは「どの Verifier で検証するか」を明示し、ノードは任意のウォレットコードを実行する前に、検証コストや受け入れ可否を判断できます。

EIP8130 の全体像

AAトランザクションは、Account ConfigurationVerifier で所有者を確認し、Nonce ManagerTransaction Context を経由して calls を実行します。
ウォレットコードだけに責務を集めず、所有者設定、署名検証、nonce、実行文脈をプロトコル上の別々の要素として扱います。

この分離によって、secp256k1 だけでなく、P-256、パスキー、マルチシグ、将来の耐量子署名のような方式を Verifier として追加できます。
一方で、ノードが受け入れる Verifier を共通セットに制限することで、メモリプールの検証負荷を予測しやすくします。

動機

既存のアカウント抽象化では、ウォレットコードへ検証ロジックを任せる設計が多くなります。
この方式では、ノードがトランザクションを受け入れる前に任意のEVM実行をシミュレーションする必要があります。
そのため、無効な送信を弾くためにも、状態アクセス、実行トレース、評判管理のような追加の仕組みが必要になります。

EIP8130は、検証を「ウォレットの任意コード」から「明示された Verifier」へ移します。
トランザクションに使う検証方式が先に分かるため、ノードは許可された Verifier だけを受け入れ、許可リスト外の検証方式を実行前に拒否できます。

署名方式の拡張

Ethereumの通常トランザクションは secp256k1 のECDSA署名を前提にしています。
しかし、ウォレットの認証方式はすでにそれだけではありません。
パスキー、モバイル端末の安全領域、マルチシグ、組織アカウント、将来の耐量子署名まで考えると、アカウントごとに署名方式を選べる必要があります。

この提案では、署名方式を Verifier コントラクトとして導入します。
Verifier は署名データから ownerId を返し、プロトコルはその ownerId が対象アカウントの所有者として登録されているかを確認します。
署名方式ごとの公開鍵形式や検証手順を、プロトコル本体へ増やさない設計です。

ウォレットとノードの境界

ウォレットは自由な認証体験を作りたい一方で、ノードはメモリプールを守るために検証コストを読める必要があります。
Verifier を明示する構成では、ウォレットは使う認証方式を選び、ノードは共通の Verifier セットを基準に受け入れ可否を判断します。

この境界があることで、ウォレットの柔軟性とノードの予測可能性を両立しやすくなります。
アカウント抽象化をプロトコル内へ入れる時に、任意コード実行をそのままメモリプール検証へ持ち込まないことが狙いです。

仕様

定数

提案で定義される主な値は以下です。
TBD の値は、提案が進む中で確定する前提です。

名前 役割
AA_TX_TYPE TBD 新しいEIP2718トランザクションタイプ
AA_PAYER_TYPE TBD payer 署名のドメイン分離に使う識別バイト
AA_BASE_COST 15000 AAトランザクションの基本intrinsic gas
ACCOUNT_CONFIG_ADDRESS TBD アカウント設定コントラクトのアドレス
ECRECOVER_VERIFIER address(1) ネイティブECDSA検証を表す予約アドレス
REVOKED_VERIFIER type(uint160).max 暗黙EOA所有者を無効化する印
NONCE_MANAGER_ADDRESS TBD nonce管理プリコンパイルのアドレス
TX_CONTEXT_ADDRESS TBD トランザクション文脈プリコンパイルのアドレス
DEFAULT_ACCOUNT_ADDRESS TBD EOA自動委任先のデフォルト実装
DEPLOYMENT_HEADER_SIZE 14 デプロイ用ヘッダのバイト数
NONCE_KEY_MAX 2^256 - 1 nonceなしモードを表す特別なキー

ECRECOVER_VERIFIER は通常のコントラクトではなく、プロトコルがECDSA検証を直接行うための予約値です。
既存EOAが最初からAAトランザクションを使えるようにするため、暗黙のEOA所有者ルールと組み合わせて使われます。

アカウント設定

各アカウントは、ACCOUNT_CONFIG_ADDRESS にあるアカウント設定コントラクトを通じて所有者を登録します。
所有者は ownerId で識別されます。
ownerId は32バイトの識別子で、どの公開鍵データからどう導くかは Verifier ごとに決めます。

所有者設定は owner_config という1スロットに入ります。

フィールド バイト位置 役割
verifier 0-19 署名検証を担当する Verifier アドレス
scope 20 その所有者が使える認証文脈
reserved 21-31 将来拡張用。ゼロでなければなりません

1所有者を1スロットへ収めることで、プロトコルは所有者確認と権限確認に必要な値を1回の SLOAD で読めます。
公開鍵そのものは保存せず、署名時に Verifier 固有の data として渡します。
大きな公開鍵を永続ストレージへ置かないため、耐量子署名のように公開鍵が大きい方式でも状態肥大を抑えられます。

暗黙のEOA所有者

登録がないアカウントでも、ownerId == bytes32(bytes20(account)) であれば、そのEOAは暗黙に所有者として扱われます。
この場合の scope0x00 で、すべての文脈で使えます。

このルールにより、既存EOAは事前登録なしでAAトランザクションを送れます。
一方で、暗黙EOA所有者を無効化する場合は、対象スロットへ REVOKED_VERIFIER を書き込みます。
空スロットと無効化済みスロットを区別できるようにするためです。

Owner Scope

scope は、所有者がどの認証文脈で使えるかを表すビットマスクです。
0x00 は制限なしを表し、非ゼロの場合は該当ビットが立っている文脈だけで有効です。

ビット 名前 使われる文脈
0 0x01 SIGNATURE verifySignature() によるERC1271互換の署名確認
1 0x02 SENDER sender_auth による送信者認証
2 0x04 PAYER payer_auth によるガス支払い認証
3 0x08 CONFIG 所有者変更の認可

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

Owner Scope の役割

同じアカウントに複数の所有者がいても、scope によって署名確認用、送信者用、payer 用、設定変更用の権限を分けられます。
VerifierownerId を返すだけで、最終的な権限判定はプロトコルが owner_configscope で行います。

例えば、ガススポンサー用の所有者には PAYER だけを与えられます。
その所有者はガス支払いを承認できますが、アカウント本人として calls を実行したり、所有者設定を変更したりできません。

2次元nonce

nonceは NONCE_MANAGER_ADDRESS のプリコンパイルで管理されます。
AAトランザクションは nonce_keynonce_sequence の2つを持ちます。

nonce_key 名前 役割
0 標準チャネル 通常の連番。メモリプールで通常使うチャネル
1 から NONCE_KEY_MAX - 1 ユーザー定義チャネル ウォレットが独立した並列レーンとして使う
NONCE_KEY_MAX nonceなしモード nonce状態を読まず、短い expiry とトランザクションハッシュ重複排除で再利用を抑える

nonce_keynonce_sequence の関係は以下です。

2次元nonce

nonce_key は独立したチャネルを選び、nonce_sequence はそのチャネル内の順序を管理します。
nonceなしモードでは、順序管理を使わない代わりに短い有効期限とハッシュ重複排除で再利用を抑えます。

nonce_key を分けると、用途ごとのnonce競合を避けられます。
例えば、定期支払いと通常の署名操作を別チャネルへ分けることで、一方の未確定トランザクションがもう一方の送信順序を止めにくくなります。

NONCE_KEY_MAX を使う場合、nonce_sequence0 でなければなりません。
また、expiry は非ゼロである必要があります。
nonce状態を使わない代わりに、短い有効期限とメモリプール内の同一ハッシュ排除でリプレイを抑えます。

アカウントロック

アカウントは所有者設定をロックできます。
ロック中は、account_changes による設定変更も、EVM実行中の applySignedOwnerChanges() も拒否されます。

ロック状態は lockedunlock_delayunlocks_at を含む1つのスロットにまとめられます。
解除は即時ではなく、まず initiateUnlock() で解除予定時刻を作り、block.timestamp >= unlocks_at になってから設定変更が許可されます。

ロックはメモリプール検証でも意味があります。
所有者集合が固定されていれば、検証済みトランザクションを無効化する主な要因はnonce消費に寄ります。
そのため、ノードはロック済みアカウントに対して高めのメモリプール保持上限を設定しやすくなります。

委任インジケータ

EIP8130は、EIP7702 と同じ委任インジケータを使います。
アカウントのコードが 0xef0100 || target であれば、そのアカウントは target のコードへ委任していると扱われます。

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

委任済みアカウントへコード実行を伴う操作が来ると、実際には target のコードが読み込まれます。
EOAがAAトランザクションを送り、コードがまだ空で、作成や委任の指定もなければ、プロトコルは DEFAULT_ACCOUNT_ADDRESS へ自動委任します。
この自動委任は永続します。

Verifier コントラクト

Verifier は署名検証を行うコントラクトです。
すべての Verifier は、ハッシュと署名データを受け取り、認証された ownerId を返します。

Verifier の最小インターフェースは以下です。

interface IVerifier {
    function verify(
        bytes32 hash,
        bytes calldata data
    ) external view returns (bytes32 ownerId);
}

戻り値が bytes32(0) であれば、署名は無効として扱われます。
プロトコルは返された ownerIdowner_config と照合し、保存されている verifier と一致するかを確認します。

VerifierSTATICCALL で実行されます。
また、委任アカウントを Verifier として使うことはできません。
Verifier のアドレスにあるコードが 0xef0100 で始まる場合、プロトコルは拒否します。

共通 Verifier セット

ノードが何でも受け入れると、ネットワークごとに受け入れ可能な署名方式がばらつきます。
そこで提案では、準拠ノードが必ず受け入れる Verifier の共通セットを定義します。

初期セットは以下です。

名前 アルゴリズム Verifier
k1 secp256k1 ECRECOVER_VERIFIER
p256 P-256 オンチェーンコントラクト
passkey WebAuthn / FIDO2 オンチェーンコントラクト
delegate 署名委任 オンチェーンコントラクト

secp256k1 の場合、プロトコルは address(1) を見て ecrecover を直接実行します。
それ以外の Verifier は、コントラクトの verify() を呼び出します。

対応するアカウントタイプ

EIP8130は、既存EOA、新規アカウント、既存スマートアカウントの3つを扱います。

アカウント 使い方 鍵復旧や移行
既存スマートコントラクト アカウント設定コントラクトに所有者を登録する ウォレット実装側の復旧設計を使う
EOA 既存の secp256k1 鍵でAAトランザクションを送る 標準トランザクションやEIP7702トランザクションも使える
新規アカウント account_changescreate 項目で作る 初期所有者と Verifier を同時に設定する

既存の ERC4337 スマートアカウントは、再デプロイなしで移行できます。
importAccount()ERC1271 署名を確認して初期所有者を登録し、ウォレットロジックから verifySignature()getOwnerId() を使うように更新します。
その後も、既存の EntryPoint 経由の UserOperation とAAトランザクションを併用できます。

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

AAトランザクション形式

AAトランザクションは AA_TX_TYPE で始まるEIP2718型トランザクションです。
構造は以下です。

AA_TX_TYPE || rlp([
  chain_id,
  sender,
  nonce_key,
  nonce_sequence,
  expiry,
  max_priority_fee_per_gas,
  max_fee_per_gas,
  gas_limit,
  account_changes,
  calls,
  payer,
  sender_auth,
  payer_auth
])

call = rlp([to, data])

主なフィールドの役割は以下です。

フィールド 役割
chain_id EIP155 に基づくチェーンID
sender 送信アカウント。EOA署名の処理では空値にし、署名から復元する
nonce_key nonceチャネル
nonce_sequence 対象チャネルの期待シーケンス
expiry 有効期限。0 は期限なし
gas_limit sender 側の検証、intrinsic cost、設定変更、calls 実行に使う上限
account_changes 作成、所有者変更、委任の配列
calls 実行する呼び出しのフェーズ配列
payer 空値なら sender 支払い。アドレスならスポンサー支払い
sender_auth sender の認証データ
payer_auth payer の認証データ

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

sender が空値の場合、sender_auth は65バイトのECDSA署名です。
プロトコルは ecrecoversender を復元し、ownerId = bytes32(bytes20(sender)) として扱います。

sender が入っている場合、sender_auth は以下のバイト列です。

sender_auth = verifier(20 bytes) || data

先頭20バイトが使う Verifier を示し、残りの dataVerifier ごとの署名データです。
ECRECOVER_VERIFIER の場合は、data がECDSA署名になります。

署名検証

検証は以下の順に行われます。

  1. sender を解決する
    sender が空値なら sender_auth から ecrecover で復元する。
    sender がある場合は、sender_auth の先頭20バイトを Verifier として読む。
  2. トランザクション文脈を設定する
    TX_CONTEXT_ADDRESSsenderpayercalls を読める状態として持たせる。
  3. 署名を検証する
    EOA署名の処理と ECRECOVER_VERIFIER では、プロトコルが直接 ecrecover を行う。
    それ以外は verifier.verify(hash, data)STATICCALL で呼ぶ。
  4. 所有者を認可する
    owner_config(sender, ownerId) を読み、保存された verifier と一致するか確認する。
    暗黙EOA所有者の条件を満たす場合だけ、空スロットでも認可する。
  5. scope を確認する
    sender_auth では SENDERpayer_auth では PAYERverifySignature() では SIGNATURE、設定変更では CONFIG を要求する。

暗黙EOA所有者ルールは、ネイティブECDSA署名の処理でだけ使えます。
任意の Verifierbytes32(bytes20(sender)) を返しただけでは、未登録EOAの所有者として扱われません。
この制限により、悪意ある Verifier が未設定EOAを勝手に認証する処理を防ぎます。

署名ハッシュ

sender 署名と payer 署名は、異なる識別バイトでドメイン分離されます。
同じ署名が sender 認証と payer 認証で使い回されないようにするためです。

sender 署名は、sender_authpayer_auth を除いたトランザクション本体を対象にします。

keccak256(AA_TX_TYPE || rlp([
  chain_id,
  sender,
  nonce_key,
  nonce_sequence,
  expiry,
  max_priority_fee_per_gas,
  max_fee_per_gas,
  gas_limit,
  account_changes,
  calls,
  payer
]))

payer 署名は、AA_PAYER_TYPE を使い、payer と両方の認証データを除外します。

keccak256(AA_PAYER_TYPE || rlp([
  chain_id,
  sender,
  nonce_key,
  nonce_sequence,
  expiry,
  max_priority_fee_per_gas,
  max_fee_per_gas,
  gas_limit,
  account_changes,
  calls
]))

EOA署名の処理では、トランザクションにエンコードされた sender フィールドが空値です。
しかし payer 署名ハッシュを作る時は、復元済みの sender アドレスを sender 位置へ入れます。
復元済み sender を入れないと、別のEOAが同じ nonce_keynonce_sequence、手数料、calls を持つトランザクションを作り、payer 署名を流用できてしまいます。

sender 認証と payer 認証の分担は以下です。

sender認証とpayer認証

sender_authcalls 実行の承認に使われ、payer_auth はガス支払いの承認に使われます。
それぞれ別の scope と署名ハッシュを使うことで、実行権限と支払い権限を混同しないようにします。

ガス支払い

ガス支払いは payerpayer_auth で決まります。

payer payer_auth 支払い元
空値 空値 sender が自分で払う
20バイトアドレス `verifier

スポンサー支払いでは、payer 側の owner_config を読み、payer_authPAYER 文脈で検証します。
sender 側の gas_limit とは別に payer 認証コストを計測するため、payer が選んだ Verifier によって calls に使えるgasが減ることはありません。

sender は、どの payer に紐づけるかを自分の署名対象に含めます。
そのため、誰でも好きな payer を後から差し替えられるわけではありません。

account_changes

account_changes は、実行前にアカウント状態を変更するための配列です。
種類は3つあります。

type 名前 役割
0x00 create 新規アカウントを作り、初期所有者を登録する
0x01 config change 所有者の追加や削除を行う
0x02 delegation EIP7702形式のコード委任を設定する

account_changes と calls の流れ

account_changescalls より先に処理されます。
新規作成、所有者変更、委任、コード配置を終えたあとで、フェーズ分けされた calls が実行されます。

create 項目

create 項目は、新しいスマートアカウントを1回のトランザクションで作成します。
フォーマットは以下です。

rlp([
  0x00,
  user_salt,
  runtimeBytecode,
  initial_owners
])

initial_owners[verifier, ownerId, scope] の配列です。
アドレスは、ACCOUNT_CONFIG_ADDRESS をデプロイヤーとした CREATE2 形式で計算されます。
この計算では、初期所有者を ownerId でソートしてからハッシュに含めます。
同じ所有者集合なら、指定順が違っても同じアドレスになるようにするためです。

sorted_owners = sort(initial_owners, by: ownerId)
owners_commitment = keccak256(ownerId_0 || verifier_0 || scope_0 || ...)
effective_salt = keccak256(user_salt || owners_commitment)
address = keccak256(
  0xff || ACCOUNT_CONFIG_ADDRESS || effective_salt || keccak256(deployment_code)
)[12:]

runtimeBytecode はそのままアカウントアドレスへ配置されます。
デプロイ時に初期化コードを実行するのではなく、初期化が必要なら後続の calls で行います。

config change 項目

config change 項目は、所有者の追加や削除を表します。
フォーマットは以下です。

rlp([
  0x01,
  chain_id,
  sequence,
  owner_changes,
  auth
])

chain_id0 の場合、同じ署名済み変更を複数チェーンで使えます。
特定のチェーンIDを入れると、そのチェーン専用の変更になります。
どちらの適用方法でも sequence が増えるため、所有者変更の順序を決定できます。

owner_changes の各要素は以下です。

change_type 名前 使う値
0x01 authorizeOwner verifierownerIdscope
0x02 revokeOwner ownerId

auth は、変更適用時点の所有者設定に対して検証されます。
複数の config change 項目が並ぶ場合、前の項目を適用したあとの状態で、後ろの項目の auth を検証します。
署名者には CONFIG scope が必要です。

delegation 項目

delegation 項目は、sender アカウントへEIP7702形式のコード委任を設定します。

rlp([
  0x02,
  target
])

targetaddress(0) なら委任を解除し、アカウントを純粋なEOAへ戻します。
委任できるのは、アカウントにコードがない場合、または既存コードが 0xef0100 で始まる委任インジケータである場合だけです。
通常のコントラクトコードを、この項目で上書きすることはできません。

この項目は sender_auth で認可されます。
ただし、認可できるのは sender の暗黙EOA所有者で、かつ CONFIG scope を持つ場合です。
非8130チェーンでは、標準のEIP7702トランザクションで同じ委任を行います。

calls 実行

account_changes を処理したあと、プロトコルは calls を実行します。
callsender 名義で対象アドレスへ直接ディスパッチされます。

パラメータ
from sender
to call.to
tx.origin sender
対象側の msg.sender sender
msg.value 0
data call.data

calls にはETH value がありません。
ETH送金が必要な場合は、ウォレットコードが CALL オペコードを使って送金します。
プロトコルが sender の代わりにETHを直接動かさない設計です。

calls はフェーズ配列です。
フェーズ内の call はアトミックに実行され、1つでもrevertすると、そのフェーズ内の変更はすべて破棄されます。
さらに、revertしたフェーズ以降は実行されません。
ただし、成功済みの前フェーズの状態変更は残ります。

よく使う実行パターンは以下です。

実行単位 挙動
単純な呼び出し [[{to, data}]] 1フェーズで1callを実行
アトミックバッチ [[call_a, call_b, call_c]] すべて成功するか、すべて戻す
スポンサー支払い + ユーザー操作 [[sponsor_payment], [user_action_a, user_action_b]] phase 0の支払いが成功した後、phase 1をまとめて実行

Transaction Context

TX_CONTEXT_ADDRESS のプリコンパイルは、現在のAAトランザクションに関する読み取り専用情報を返します。
Verifier やウォレットコードは、必要な情報だけをこのプリコンパイルから読み取れます。

主な関数は以下です。

struct Call {
    address to;
    bytes data;
}

interface ITxContext {
    function getSender() external view returns (address);
    function getPayer() external view returns (address);
    function getOwnerId() external view returns (bytes32);
    function getCalls() external view returns (Call[][] memory);
    function getMaxCost() external view returns (uint256);
    function getGasLimit() external view returns (uint256);
}

getSender()getPayer()getCalls()getMaxCost()getGasLimit() は検証時と実行時の両方で使えます。
getOwnerId() は、認証済み所有者が決まったあとの実行時だけ使えます。

非8130チェーンでは、このアドレスにコードがありません。
そのため、STATICCALL はゼロ値を返す前提になり、8130文脈に依存する Verifier は自然に失敗します。

移植性

この提案は、8130チェーンと非8130チェーンの両方を意識しています。
構成要素ごとの扱いは以下です。

要素 8130チェーン 非8130チェーン
Account Configuration Contract プロトコルがストレージを直接読む。EVMインターフェースも使える 標準コントラクトとして動く
Verifier プロトコルが STATICCALL で呼ぶ ウォレットや設定コントラクトから呼ぶ
コード委任 account_changesdelegation 項目 標準EIP7702トランザクション
Transaction Context プリコンパイルが文脈を返す ゼロ値を返す
Nonce Manager プリコンパイルで管理 ERC4337のEntryPointなど既存の仕組み

全コントラクトを決定的な CREATE2 アドレスへデプロイすることで、複数チェーンで同じ管理基盤を使いやすくします。

メモリプールでの検証

ノードがメモリプールで受け入れる時の流れは以下です。

  1. 構造を確認する
    sender_auth をパースし、create 項目が最大1つで先頭にあること、delegation 項目が最大1つであることを確認する。
  2. sender を解決する
    sender がある場合はその値を使い、空値の場合は sender_auth から ecrecover で復元する。
  3. 有効な所有者状態を決める
    create 項目があれば初期所有者を使い、なければアカウント設定ストレージを読む。
  4. 設定変更と委任を確認する
    アカウントがロックされていないか、委任先が置き換え可能なコード状態かを確認する。
  5. sender 認証を検証する
    解決した所有者に SENDER scope があることを確認する。
    delegation 項目があれば、暗黙EOA所有者かつ CONFIG scope も必要になる。
  6. payer を解決する
    自己支払いなら sender 残高を確認し、スポンサー支払いなら payer 側の owner_configPAYER scope を確認する。
  7. nonce、残高、有効期限を確認する
    標準nonceでは現在値と nonce_sequence の一致を要求する。
    nonceなしモードでは expiry を短く要求し、同一ハッシュを重複排除する。
  8. メモリプール保持上限を確認する
    ガス支払い元ごとの未確定トランザクション数が、ノード設定の上限内かを見る。

ノードは共通 Verifier セットを含む許可リストを持ちます。
共通セット以外の Verifier を追加で受け入れると、ノードごとの受け入れ差が生まれるため、提案では拡張しない方向を推奨しています。

ブロック実行

ブロックへ含まれたAAトランザクションは以下の順で処理されます。

  1. ロック状態と委任条件を確認する
    account_changes に設定変更や委任があれば、sender がロックされていないことを確認する。
  2. payer からガスを差し引く
    自己支払いなら sender、スポンサー支払いなら指定 payer から差し引く。
  3. nonceを進める
    nonce_key != NONCE_KEY_MAX の場合、(sender, nonce_key) のnonceをインクリメントする。
  4. 必要なら自動委任する
    sender にコードがなく、create 項目も delegation 項目もなければ、DEFAULT_ACCOUNT_ADDRESS へ自動委任する。
  5. account_changes を処理する
    createconfig changedelegation、コード配置を順に適用する。
  6. トランザクション文脈を設定する
    senderpayerownerIdcallsTransaction Context から読めるようにする。
  7. calls を実行する
    フェーズ単位のrevertルールに従って実行する。

未使用gasは payer へ返金されます。
設定変更やアカウント作成、委任が成功した場合、プロトコルが OwnerAuthorizedAccountCreatedDelegationApplied のようなログをreceiptへ注入します。
このログは8130トランザクションでだけ発行されます。

RPC拡張

eth_getTransactionCount は、任意の nonceKey を指定できるように拡張されます。
この拡張により、標準チャネル以外のnonceも問い合わせできます。

receiptには以下が追加されます。

項目 役割
payer ガス支払い元。自己支払いなら sender、スポンサー支払いなら指定 payer
status 全フェーズ成功なら 0x01、どれかがrevertすれば 0x00
phaseStatuses 各フェーズの成功・失敗を表す配列

既存ツールが status == 1 を成功として見る挙動は、全フェーズ成功の場合に維持されます。
フェーズごとの詳細は phaseStatuses で確認できます。

補足

なぜ Verifier コントラクトなのか

Verifier は署名方式の追加点です。
プロトコルは「どのアルゴリズムで公開鍵から署名を検証するか」を知らず、Verifier から返る ownerId だけを扱います。
そのため、署名方式を追加しても、プロトコルにアルゴリズム固有の分岐を増やしません。

また、権限分離は Verifier ではなく scope で行います。
Verifier が協力しなくても、プロトコルが SENDERPAYERCONFIG などの文脈を強制できます。

なぜ Nonce Manager がプリコンパイルなのか

nonceはほぼすべてのAAトランザクションで更新されます。
一方、所有者設定は頻繁には変わりません。
そこで、nonceをアカウント設定コントラクトの通常ストレージへ混ぜず、専用の Nonce Manager プリコンパイルへ分けます。

EVMからは読み取り用の getNonce() だけを公開します。
書き込みはプロトコルがAAトランザクション処理の中で直接行います。

interface INonceManager {
    function getNonce(address account, uint256 nonceKey) external view returns (uint64);
}

なぜ callsvalue がないのか

AAトランザクションの callstodata だけを持ち、msg.value は常に 0 です。
ETHを送る場合は、アカウントのウォレットコードが CALL で送金します。

この設計では、プロトコルが sender のETHを直接移動しません。
ETH移動のポリシーはウォレットコード側へ残し、AAトランザクション本体は呼び出しの列と認証の仕組みに集中します。

なぜ Owner Scope が必要なのか

scope がないと、登録された所有者はすべて同じ権限を持ちます。
この場合、署名確認用の所有者、トランザクション送信用の所有者、ガススポンサー用の所有者、所有者変更用の所有者を分けられません。

実際のウォレットでは、パスキーで日常操作を行い、ハードウェアキーで設定変更を行い、別の payer 鍵でガス支払いだけを許可する、といった構成にできます。
scope は、その役割分担をプロトコルレベルで強制するためのビットマスクです。

なぜ公開鍵を保存しないのか

公開鍵をオンチェーンに保存すると、所有者ごとに永続ストレージが増えます。
P-256程度ならまだ小さくても、耐量子署名では公開鍵が大きくなり、1所有者で大量のストレージを使います。

EIP8130では、保存するのは verifierownerIdscope だけです。
公開鍵材料は署名時の data に含め、Verifier がそこから ownerId を導きます。
公開鍵材料を一時的なcalldataに置くことで、永続ストレージの増加を抑えます。

互換性

既存EOAと既存スマートコントラクトは、そのまま動作します。
AA機能は opt-in です。

EOAは通常トランザクションを送り続けられます。
AAトランザクションを初めて送る時は、必要に応じて DEFAULT_ACCOUNT_ADDRESS へ自動委任されます。
自分で委任先を選ぶ場合は、account_changesdelegation 項目や標準EIP7702トランザクションを使えます。

ERC4337インフラも継続利用できます。
既存スマートアカウントは、所有者登録と verifySignature() への委譲を追加することで、ネイティブAAトランザクションとERC4337 UserOperation の両方を扱えます。

非8130チェーンでも、アカウント設定コントラクトや Verifier コントラクト自体は使えます。
ただし、Transaction ContextNonce Manager はプロトコル側の機能なので、同じ挙動を完全に再現するには8130対応チェーンが必要です。

参考実装

提案では、IAccountConfigurationIVerifierITxContextINonceManager が示されています。
ここでは主要なインターフェースを抜粋します。

IAccountConfiguration は、アカウント作成、所有者変更、ロック、署名確認、設定参照をまとめて扱います。

interface IAccountConfiguration {
    struct OwnerConfig {
        address verifier;
        uint8 scopes;
    }

    struct Owner {
        bytes32 ownerId;
        OwnerConfig config;
    }

    struct OwnerChange {
        bytes32 ownerId;
        uint8 changeType;
        bytes configData;
    }

    event OwnerAuthorized(address indexed account, bytes32 indexed ownerId, OwnerConfig config);
    event OwnerRevoked(address indexed account, bytes32 indexed ownerId);
    event AccountCreated(address indexed account, bytes32 userSalt, bytes32 codeHash);
    event DelegationApplied(address indexed account, address target);

    function createAccount(
        bytes32 userSalt,
        bytes calldata bytecode,
        Owner[] calldata initialOwners
    ) external returns (address);

    function applySignedOwnerChanges(
        address account,
        uint64 chainId,
        OwnerChange[] calldata ownerChanges,
        bytes calldata auth
    ) external;

    function verifySignature(
        address account,
        bytes32 hash,
        bytes calldata signature
    ) external view returns (bool verified);

    function getOwnerConfig(
        address account,
        bytes32 ownerId
    ) external view returns (OwnerConfig memory);
}

createAccount() は、初期所有者を含めてアカウントアドレスを決定し、ランタイムバイトコードを配置します。
applySignedOwnerChanges() は、署名済みの所有者変更をEVM経由で適用する移植可能な実行方法です。
verifySignature() は、ERC1271互換の署名確認に使われます。

IVerifier は非常に小さいです。
署名方式ごとの差分は data の中に閉じ込められます。

interface IVerifier {
    function verify(
        bytes32 hash,
        bytes calldata data
    ) external view returns (bytes32 ownerId);
}

この小ささが、署名方式を追加しやすくしている部分です。
新しい認証方式を使いたい場合、verify() で同じ戻り値形式に合わせれば、プロトコルは owner_configscope の確認だけで済みます。

セキュリティ

検証面

純粋な Verifier では、検証済みトランザクションを無効化する主な要因は owner_config の取り消しとnonce消費です。
状態に依存する Verifier では、読んだ状態が変わることも無効化要因になります。
その追跡はメモリプール側の課題です。

リプレイ保護

AAトランザクションは chain_id、2次元nonce、expiry を持ちます。
通常nonceでは (sender, nonce_key) ごとに nonce_sequence を確認します。
nonceなしモードではnonce状態を使わないため、短い expiry と同一ハッシュの重複排除が重要になります。

ブロックビルダーは、同じハッシュのnonceなしトランザクションを重複して含めてはいけません。
ノードも、nonceなしモードの有効期限が長すぎるトランザクションをメモリプールで拒否する前提です。

Owner Scope

scopeVerifier 実行後にプロトコルが確認します。
そのため、Verifier がどの ownerId を返しても、登録された scope を越えた権限は得られません。

特に payer 用の所有者と sender 用の所有者を分けられる点が重要です。
PAYER だけの所有者はガス支払いを承認できますが、sender として操作を実行できません。
SENDER だけの所有者は操作を実行できますが、payer として他者のガス支払いを承認できません。

暗黙EOAルール

暗黙EOA所有者として認められるのは、ネイティブ secp256k1 署名の処理だけです。
sendersender_auth から ecrecover で復元する場合、または ECRECOVER_VERIFIER を使う場合に限られます。

任意の Verifierbytes32(bytes20(sender)) を返しただけで暗黙EOA所有者として認めると、未登録EOAを勝手に認証できる危険があります。
この提案は、暗黙EOAルールをECDSA署名の処理へ限定して、その攻撃面を閉じています。

payer 署名の再利用

sender 署名と payer 署名は AA_TX_TYPEAA_PAYER_TYPE で分けられます。
さらに、payer 署名ハッシュには解決済み sender アドレスを入れます。

EOA署名の処理ではトランザクションにエンコードされた sender フィールドが空値ですが、payer 署名ハッシュでは復元済み sender を使います。
復元済み sender をハッシュに含めないと、別のEOAが同じ nonce_keynonce_sequence、手数料、calls を持つトランザクションを作り、payer 署名を再利用できてしまいます。
復元済み sender をハッシュへ入れることで、payer の承認は特定 sender 専用になります。

アカウント作成

create 項目では、initial_owners をsaltへコミットします。
そのため、攻撃者が同じ user_salt とコードで、異なる所有者を持つアカウントを先に作ることを防げます。

ウォレットバイトコードは、初期化前でも安全に置かれる必要があります。
create 項目ではランタイムバイトコードが直接配置されるため、初期化が必要な処理は後続の calls で行う設計になります。

最後に

今回は「オンチェーンのアカウント設定と新しいトランザクションタイプでアカウント抽象化を実現するEIP8130」についてまとめてきました。

EIP8130は、アカウント抽象化の検証をウォレットコードから切り離し、Verifierowner_configscope、2次元nonce、payer 認証として分解する提案です。
ノードが受け入れ前に検証コストを読みやすくしつつ、ウォレット側にはパスキーや耐量子署名のような認証方式を追加できる余地を残しています。
既存EOAやERC4337スマートアカウントからの移行手順も含んでいるため、アカウント抽象化をプロトコルの標準機能へ近づける大きな設計案として理解しておきたい内容です。

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

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?