はじめに
初めまして。
『DApps開発入門』という本や色々記事を書いているかるでねです。
以下でも情報発信しているので、興味ある記事があればぜひ読んでみてください!
今回は「オンチェーンのアカウント設定と新しいトランザクションタイプでアカウント抽象化を実現するEIP8130」についてまとめていきます。
EIP8130は、ウォレットコードを毎回シミュレーションしてトランザクションを受け入れるのではなく、署名検証を担う Verifier と所有者設定をオンチェーンで明示する提案です。
署名方式の追加、バッチ実行、ガススポンサー、既存EOAからの移行を、プロトコルが事前検証しやすいトランザクション構造に組み込みます。
以下にまとめられているものを解説しながらまとめていきます。
提案では AA_TX_TYPE や主要なシステムアドレスが TBD のままです。
値そのものではなく、どの役割をどこへ分けているかを中心に見ていきます。
概要
EIP8130は、アカウント抽象化をネイティブトランザクションとして扱うための提案です。
新しい EIP2718 型トランザクションと、アカウントごとの所有者設定を管理する Account Configuration Contract を組み合わせます。
EIP2718については以下の記事を参考にしてください。
この提案の中心は、署名検証をウォレット実装から切り離すことです。
トランザクションは「どの Verifier で検証するか」を明示し、ノードは任意のウォレットコードを実行する前に、検証コストや受け入れ可否を判断できます。
AAトランザクションは、Account Configuration と Verifier で所有者を確認し、Nonce Manager と Transaction 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は暗黙に所有者として扱われます。
この場合の scope は 0x00 で、すべての文脈で使えます。
このルールにより、既存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については以下の記事を参考にしてください。
同じアカウントに複数の所有者がいても、scope によって署名確認用、送信者用、payer 用、設定変更用の権限を分けられます。
Verifier は ownerId を返すだけで、最終的な権限判定はプロトコルが owner_config と scope で行います。
例えば、ガススポンサー用の所有者には PAYER だけを与えられます。
その所有者はガス支払いを承認できますが、アカウント本人として calls を実行したり、所有者設定を変更したりできません。
2次元nonce
nonceは NONCE_MANAGER_ADDRESS のプリコンパイルで管理されます。
AAトランザクションは nonce_key と nonce_sequence の2つを持ちます。
nonce_key |
名前 | 役割 |
|---|---|---|
0 |
標準チャネル | 通常の連番。メモリプールで通常使うチャネル |
1 から NONCE_KEY_MAX - 1
|
ユーザー定義チャネル | ウォレットが独立した並列レーンとして使う |
NONCE_KEY_MAX |
nonceなしモード | nonce状態を読まず、短い expiry とトランザクションハッシュ重複排除で再利用を抑える |
nonce_key と nonce_sequence の関係は以下です。
nonce_key は独立したチャネルを選び、nonce_sequence はそのチャネル内の順序を管理します。
nonceなしモードでは、順序管理を使わない代わりに短い有効期限とハッシュ重複排除で再利用を抑えます。
nonce_key を分けると、用途ごとのnonce競合を避けられます。
例えば、定期支払いと通常の署名操作を別チャネルへ分けることで、一方の未確定トランザクションがもう一方の送信順序を止めにくくなります。
NONCE_KEY_MAX を使う場合、nonce_sequence は 0 でなければなりません。
また、expiry は非ゼロである必要があります。
nonce状態を使わない代わりに、短い有効期限とメモリプール内の同一ハッシュ排除でリプレイを抑えます。
アカウントロック
アカウントは所有者設定をロックできます。
ロック中は、account_changes による設定変更も、EVM実行中の applySignedOwnerChanges() も拒否されます。
ロック状態は locked、unlock_delay、unlocks_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) であれば、署名は無効として扱われます。
プロトコルは返された ownerId を owner_config と照合し、保存されている verifier と一致するかを確認します。
Verifier は STATICCALL で実行されます。
また、委任アカウントを 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_changes の create 項目で作る |
初期所有者と 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署名です。
プロトコルは ecrecover で sender を復元し、ownerId = bytes32(bytes20(sender)) として扱います。
sender が入っている場合、sender_auth は以下のバイト列です。
sender_auth = verifier(20 bytes) || data
先頭20バイトが使う Verifier を示し、残りの data は Verifier ごとの署名データです。
ECRECOVER_VERIFIER の場合は、data がECDSA署名になります。
署名検証
検証は以下の順に行われます。
-
senderを解決する
senderが空値ならsender_authからecrecoverで復元する。
senderがある場合は、sender_authの先頭20バイトをVerifierとして読む。 - トランザクション文脈を設定する
TX_CONTEXT_ADDRESSにsender、payer、callsを読める状態として持たせる。 - 署名を検証する
EOA署名の処理とECRECOVER_VERIFIERでは、プロトコルが直接ecrecoverを行う。
それ以外はverifier.verify(hash, data)をSTATICCALLで呼ぶ。 - 所有者を認可する
owner_config(sender, ownerId)を読み、保存されたverifierと一致するか確認する。
暗黙EOA所有者の条件を満たす場合だけ、空スロットでも認可する。 -
scopeを確認する
sender_authではSENDER、payer_authではPAYER、verifySignature()ではSIGNATURE、設定変更ではCONFIGを要求する。
暗黙EOA所有者ルールは、ネイティブECDSA署名の処理でだけ使えます。
任意の Verifier が bytes32(bytes20(sender)) を返しただけでは、未登録EOAの所有者として扱われません。
この制限により、悪意ある Verifier が未設定EOAを勝手に認証する処理を防ぎます。
署名ハッシュ
sender 署名と payer 署名は、異なる識別バイトでドメイン分離されます。
同じ署名が sender 認証と payer 認証で使い回されないようにするためです。
sender 署名は、sender_auth と payer_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_key、nonce_sequence、手数料、calls を持つトランザクションを作り、payer 署名を流用できてしまいます。
sender 認証と payer 認証の分担は以下です。
sender_auth は calls 実行の承認に使われ、payer_auth はガス支払いの承認に使われます。
それぞれ別の scope と署名ハッシュを使うことで、実行権限と支払い権限を混同しないようにします。
ガス支払い
ガス支払いは payer と payer_auth で決まります。
payer |
payer_auth |
支払い元 |
|---|---|---|
| 空値 | 空値 |
sender が自分で払う |
| 20バイトアドレス | `verifier |
スポンサー支払いでは、payer 側の owner_config を読み、payer_auth を PAYER 文脈で検証します。
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 より先に処理されます。
新規作成、所有者変更、委任、コード配置を終えたあとで、フェーズ分けされた 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_id が 0 の場合、同じ署名済み変更を複数チェーンで使えます。
特定のチェーンIDを入れると、そのチェーン専用の変更になります。
どちらの適用方法でも sequence が増えるため、所有者変更の順序を決定できます。
owner_changes の各要素は以下です。
change_type |
名前 | 使う値 |
|---|---|---|
0x01 |
authorizeOwner |
verifier、ownerId、scope
|
0x02 |
revokeOwner |
ownerId |
auth は、変更適用時点の所有者設定に対して検証されます。
複数の config change 項目が並ぶ場合、前の項目を適用したあとの状態で、後ろの項目の auth を検証します。
署名者には CONFIG scope が必要です。
delegation 項目
delegation 項目は、sender アカウントへEIP7702形式のコード委任を設定します。
rlp([
0x02,
target
])
target が address(0) なら委任を解除し、アカウントを純粋なEOAへ戻します。
委任できるのは、アカウントにコードがない場合、または既存コードが 0xef0100 で始まる委任インジケータである場合だけです。
通常のコントラクトコードを、この項目で上書きすることはできません。
この項目は sender_auth で認可されます。
ただし、認可できるのは sender の暗黙EOA所有者で、かつ CONFIG scope を持つ場合です。
非8130チェーンでは、標準のEIP7702トランザクションで同じ委任を行います。
calls 実行
account_changes を処理したあと、プロトコルは calls を実行します。
各 call は sender 名義で対象アドレスへ直接ディスパッチされます。
| パラメータ | 値 |
|---|---|
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_changes の delegation 項目 |
標準EIP7702トランザクション |
Transaction Context |
プリコンパイルが文脈を返す | ゼロ値を返す |
Nonce Manager |
プリコンパイルで管理 | ERC4337のEntryPointなど既存の仕組み |
全コントラクトを決定的な CREATE2 アドレスへデプロイすることで、複数チェーンで同じ管理基盤を使いやすくします。
メモリプールでの検証
ノードがメモリプールで受け入れる時の流れは以下です。
- 構造を確認する
sender_authをパースし、create項目が最大1つで先頭にあること、delegation項目が最大1つであることを確認する。 -
senderを解決する
senderがある場合はその値を使い、空値の場合はsender_authからecrecoverで復元する。 - 有効な所有者状態を決める
create項目があれば初期所有者を使い、なければアカウント設定ストレージを読む。 - 設定変更と委任を確認する
アカウントがロックされていないか、委任先が置き換え可能なコード状態かを確認する。 -
sender認証を検証する
解決した所有者にSENDERscope があることを確認する。
delegation項目があれば、暗黙EOA所有者かつCONFIGscope も必要になる。 -
payerを解決する
自己支払いならsender残高を確認し、スポンサー支払いならpayer側のowner_configとPAYERscope を確認する。 - nonce、残高、有効期限を確認する
標準nonceでは現在値とnonce_sequenceの一致を要求する。
nonceなしモードではexpiryを短く要求し、同一ハッシュを重複排除する。 - メモリプール保持上限を確認する
ガス支払い元ごとの未確定トランザクション数が、ノード設定の上限内かを見る。
ノードは共通 Verifier セットを含む許可リストを持ちます。
共通セット以外の Verifier を追加で受け入れると、ノードごとの受け入れ差が生まれるため、提案では拡張しない方向を推奨しています。
ブロック実行
ブロックへ含まれたAAトランザクションは以下の順で処理されます。
- ロック状態と委任条件を確認する
account_changesに設定変更や委任があれば、senderがロックされていないことを確認する。 -
payerからガスを差し引く
自己支払いならsender、スポンサー支払いなら指定payerから差し引く。 - nonceを進める
nonce_key != NONCE_KEY_MAXの場合、(sender, nonce_key)のnonceをインクリメントする。 - 必要なら自動委任する
senderにコードがなく、create項目もdelegation項目もなければ、DEFAULT_ACCOUNT_ADDRESSへ自動委任する。 -
account_changesを処理する
create、config change、delegation、コード配置を順に適用する。 - トランザクション文脈を設定する
sender、payer、ownerId、callsをTransaction Contextから読めるようにする。 -
callsを実行する
フェーズ単位のrevertルールに従って実行する。
未使用gasは payer へ返金されます。
設定変更やアカウント作成、委任が成功した場合、プロトコルが OwnerAuthorized、AccountCreated、DelegationApplied のようなログを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 が協力しなくても、プロトコルが SENDER、PAYER、CONFIG などの文脈を強制できます。
なぜ Nonce Manager がプリコンパイルなのか
nonceはほぼすべてのAAトランザクションで更新されます。
一方、所有者設定は頻繁には変わりません。
そこで、nonceをアカウント設定コントラクトの通常ストレージへ混ぜず、専用の Nonce Manager プリコンパイルへ分けます。
EVMからは読み取り用の getNonce() だけを公開します。
書き込みはプロトコルがAAトランザクション処理の中で直接行います。
interface INonceManager {
function getNonce(address account, uint256 nonceKey) external view returns (uint64);
}
なぜ calls に value がないのか
AAトランザクションの calls は to と data だけを持ち、msg.value は常に 0 です。
ETHを送る場合は、アカウントのウォレットコードが CALL で送金します。
この設計では、プロトコルが sender のETHを直接移動しません。
ETH移動のポリシーはウォレットコード側へ残し、AAトランザクション本体は呼び出しの列と認証の仕組みに集中します。
なぜ Owner Scope が必要なのか
scope がないと、登録された所有者はすべて同じ権限を持ちます。
この場合、署名確認用の所有者、トランザクション送信用の所有者、ガススポンサー用の所有者、所有者変更用の所有者を分けられません。
実際のウォレットでは、パスキーで日常操作を行い、ハードウェアキーで設定変更を行い、別の payer 鍵でガス支払いだけを許可する、といった構成にできます。
scope は、その役割分担をプロトコルレベルで強制するためのビットマスクです。
なぜ公開鍵を保存しないのか
公開鍵をオンチェーンに保存すると、所有者ごとに永続ストレージが増えます。
P-256程度ならまだ小さくても、耐量子署名では公開鍵が大きくなり、1所有者で大量のストレージを使います。
EIP8130では、保存するのは verifier、ownerId、scope だけです。
公開鍵材料は署名時の data に含め、Verifier がそこから ownerId を導きます。
公開鍵材料を一時的なcalldataに置くことで、永続ストレージの増加を抑えます。
互換性
既存EOAと既存スマートコントラクトは、そのまま動作します。
AA機能は opt-in です。
EOAは通常トランザクションを送り続けられます。
AAトランザクションを初めて送る時は、必要に応じて DEFAULT_ACCOUNT_ADDRESS へ自動委任されます。
自分で委任先を選ぶ場合は、account_changes の delegation 項目や標準EIP7702トランザクションを使えます。
ERC4337インフラも継続利用できます。
既存スマートアカウントは、所有者登録と verifySignature() への委譲を追加することで、ネイティブAAトランザクションとERC4337 UserOperation の両方を扱えます。
非8130チェーンでも、アカウント設定コントラクトや Verifier コントラクト自体は使えます。
ただし、Transaction Context と Nonce Manager はプロトコル側の機能なので、同じ挙動を完全に再現するには8130対応チェーンが必要です。
参考実装
提案では、IAccountConfiguration、IVerifier、ITxContext、INonceManager が示されています。
ここでは主要なインターフェースを抜粋します。
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_config と scope の確認だけで済みます。
セキュリティ
検証面
純粋な Verifier では、検証済みトランザクションを無効化する主な要因は owner_config の取り消しとnonce消費です。
状態に依存する Verifier では、読んだ状態が変わることも無効化要因になります。
その追跡はメモリプール側の課題です。
リプレイ保護
AAトランザクションは chain_id、2次元nonce、expiry を持ちます。
通常nonceでは (sender, nonce_key) ごとに nonce_sequence を確認します。
nonceなしモードではnonce状態を使わないため、短い expiry と同一ハッシュの重複排除が重要になります。
ブロックビルダーは、同じハッシュのnonceなしトランザクションを重複して含めてはいけません。
ノードも、nonceなしモードの有効期限が長すぎるトランザクションをメモリプールで拒否する前提です。
Owner Scope
scope は Verifier 実行後にプロトコルが確認します。
そのため、Verifier がどの ownerId を返しても、登録された scope を越えた権限は得られません。
特に payer 用の所有者と sender 用の所有者を分けられる点が重要です。
PAYER だけの所有者はガス支払いを承認できますが、sender として操作を実行できません。
SENDER だけの所有者は操作を実行できますが、payer として他者のガス支払いを承認できません。
暗黙EOAルール
暗黙EOA所有者として認められるのは、ネイティブ secp256k1 署名の処理だけです。
sender を sender_auth から ecrecover で復元する場合、または ECRECOVER_VERIFIER を使う場合に限られます。
任意の Verifier が bytes32(bytes20(sender)) を返しただけで暗黙EOA所有者として認めると、未登録EOAを勝手に認証できる危険があります。
この提案は、暗黙EOAルールをECDSA署名の処理へ限定して、その攻撃面を閉じています。
payer 署名の再利用
sender 署名と payer 署名は AA_TX_TYPE と AA_PAYER_TYPE で分けられます。
さらに、payer 署名ハッシュには解決済み sender アドレスを入れます。
EOA署名の処理ではトランザクションにエンコードされた sender フィールドが空値ですが、payer 署名ハッシュでは復元済み sender を使います。
復元済み sender をハッシュに含めないと、別のEOAが同じ nonce_key、nonce_sequence、手数料、calls を持つトランザクションを作り、payer 署名を再利用できてしまいます。
復元済み sender をハッシュへ入れることで、payer の承認は特定 sender 専用になります。
アカウント作成
create 項目では、initial_owners をsaltへコミットします。
そのため、攻撃者が同じ user_salt とコードで、異なる所有者を持つアカウントを先に作ることを防げます。
ウォレットバイトコードは、初期化前でも安全に置かれる必要があります。
create 項目ではランタイムバイトコードが直接配置されるため、初期化が必要な処理は後続の calls で行う設計になります。
最後に
今回は「オンチェーンのアカウント設定と新しいトランザクションタイプでアカウント抽象化を実現するEIP8130」についてまとめてきました。
EIP8130は、アカウント抽象化の検証をウォレットコードから切り離し、Verifier、owner_config、scope、2次元nonce、payer 認証として分解する提案です。
ノードが受け入れ前に検証コストを読みやすくしつつ、ウォレット側にはパスキーや耐量子署名のような認証方式を追加できる余地を残しています。
既存EOAやERC4337スマートアカウントからの移行手順も含んでいるため、アカウント抽象化をプロトコルの標準機能へ近づける大きな設計案として理解しておきたい内容です。
他でも色々記事を書いているのでぜひよろしければ読んでいってください!




