📝

【Account Abstraction】UserOperation周りの具体的な実装について

2023/09/19に公開

前置き

最近話題のマイナウォレットについて実装をチェックしてたところ、自分が全然UserOperation周りを理解してないことに気が付いたので、その備忘録です。
このため、本記事ではマイナウォレット、及びAAのサンプル実装をベースに記載します。

https://github.com/a42io/ETHGlobalTokyo2023/tree/main
https://github.com/eth-infinitism/account-abstraction
https://github.com/eth-infinitism/bundler/tree/main

この記事で扱うこと・扱わないこと

扱うこと

  • UserOperationの作成方法
  • UserOperationの検証部分

扱わないこと

  • 細かい暗号技術
  • ガス代/Paymaster周り

UserOperationをBundlerに渡すまで

まずは以下画像のUserOperation→Bundlersまでの道のりを書いていきたいと思います。
そもそもUserOperationとはどういったものなのでしょうか。

引用:https://docs.stackup.sh/docs/erc-4337-overview

Stackupのサイトを見てみると、分かりやすい表が載っていました。
senderやnonceあたりはなんとなくイメージが付きますが、initCode, signatureが謎なので詳しく見ていきます。

initCode

上述の画像では、"senderがまだon-chain上にない場合、デプロイに使用されるコード"と記載がありました。
つまり、ウォレットアカウントを作成するためのコードです。
マイナウォレットでは以下の関数で取得しています。
https://github.com/a42io/ETHGlobalTokyo2023/blob/main/Server/src/app.ts#L55-L70

まず、57行目でmodulusというユニークなKey(マイナンバーの公開鍵)をもとにcodeを取得します。
取得したcodeが"0x"であれば、未デプロイと判断して、FactoryコントラクトのcreateAccount関数を返すようになっています。
これが実行されると、ウォレットコントラクトが出来上がるわけですね。注意としては、現時点ではまだ実行されていません。

Signature

上述の画像では、”認証の時に使われるデータ”と書かれています。(名前からして当然ですが)
Signatureについては、Stackupのサイトの以下に記載があります。
https://docs.stackup.sh/docs/erc-4337-useroperation-hash-guide

これによると、"全てのSignatureは、署名されたuserOpHashであるべき"と書いてあります。
同ページにuserOpHashについても説明があったので見ていきます。

userOpHashは、以下の3つの値をkeccak256によってハッシュ化したものです。

  1. UserOperation全体(Signatureは除く)
  2. EntryPointのアドレス
  3. chainID

こうしてできたuserOpHashに署名をしたものがSignatureとなります。
このため、1でSignatureが除かれるのは当然ですね。

また、一言で署名と書きましたが、どのような署名を行うかは実装者の自由です。
マイナウォレットでは、ここがマイナンバーカード内の秘密鍵による署名となっています。
ここで行われた署名は、後々コントラクトで検証することになります。

マイナウォレットではどのようにUserOperationを生成しているか

UserOperationの中身がちょっと分かったので、実際にどのように生成しているのか見ていきます。

マイナウォレットでは、サーバ内の以下部分で生成していました。
https://github.com/a42io/ETHGlobalTokyo2023/blob/main/Server/src/app.ts#L151-L220

サーバに対して送金先・金額・modulus(マイナンバーの公開鍵)を含めたGETリクエストを送ると、"どのようなトランザクションを発行するか"や、上述のinitCodeの生成・取得が行われます。
また、214行目ではuserOpHashの生成もしています。getUserOpHashが実装されている別ライブラリのコードを見てみましょう。
https://github.com/eth-infinitism/bundler/blob/main/packages/utils/src/ERC4337Utils.ts#L59-L65
https://github.com/eth-infinitism/bundler/blob/main/packages/utils/src/ERC4337Utils.ts#L29-L38

この時点でSignatureはkeyのみでvalueは入っていませんが、packUserOpでSignature部分を取り除き、getUserOpHashではそれをkeccak256でハッシュ化、その後にEntryPointのアドレス・chainIdも結合させて再度ハッシュ化という流れになっています。

userOpHashの取得が完了した後は、これに署名をしてもらってSignatureを生成しなければなりません。
署名はクライアント側で行うため、最初に送ったGETリクエストのレスポンスとして、以下二つを返します。

  • userOpHash
  • 生のuserOperation

クライアント側でuserOpHashに署名をしてもらった後、それを生のuserOperationに追加してもらうことで完全なUserOperationが出来上がるってわけですね!

その後、完全なUserOperationをクライアント側から送り返してもらったら、あとはそれをBundlerへ送信するだけです!
マイナウォレットでは、UserOperationを含めたPOSTリクエストをサーバ側に送ると、sendUserOpToBundlerという関数によってBundlerへ送信します。
https://github.com/a42io/ETHGlobalTokyo2023/blob/main/Server/src/app.ts#L222-L238

一応sendUserOpToBundlerの実装箇所も載せておきます。
https://github.com/eth-infinitism/bundler/blob/main/packages/sdk/src/HttpRpcClient.ts#L41-L48

これでUserOperationをBundlerに渡すまでの流れは確認できました。

EntryPointがUserOperationを検証するまで

続いて、EntryPointがUserOperationを検証するまでを見ていきます。

EntryPoint

まずは、BundlerがいくつかのUserOperationを集め、それをEntryPointのhandleOps関数の引数として設定してトランザクションを発行します。
https://github.com/eth-infinitism/account-abstraction/blob/develop/contracts/core/EntryPoint.sol#L112-L144

126行目の_validatePrepaymentの戻り値の中にvalidationDataというものがあり怪しそうなので、中を見ていきます。
https://github.com/eth-infinitism/account-abstraction/blob/develop/contracts/core/EntryPoint.sol#L561-L630

579行目でgetUserOpHash関数をコールしてuserOpHashを生成していました。
(もちろんSignature部分は除く処理がgetUserOpHashの中で行われています)
後で、このuserOpHashとSignatureを突き合わせて検証を行うため、UserOperationが改竄はできません。

その後、595行目でコールしている_validateAccountPrepaymentの中身は以下です。
https://github.com/eth-infinitism/account-abstraction/blob/develop/contracts/core/EntryPoint.sol#L392-L451

少し検証からは逸れますが、417行目で_createSenderIfNeeded関数をコールしています。
もしウォレットコントラクトが未デプロイだった場合、UserOperation内のinitCodeを使って、ウォレットコントラクトをデプロイします。このためのinitCode!

その後、428行目でウォレットコントラクトのvalidateUserOpをコールしています。
引数にはUserOperationと上で生成したuserOpHashが設定されていますね 。ウォレットコントラクト側の実装を見てみましょう。

ウォレットコントラクト

BaseAccount.sol

validateUserOpはBaseAccount.solに実装がされています。
独自のウォレットを作る場合はこのコントラクトを継承する形になると思います。
EntryPointからのコールかをチェックした後、_validateSignatureを呼んでいるので見てみます。
https://github.com/a42io/ETHGlobalTokyo2023/blob/main/playground/test-contract/src/core/BaseAccount.sol#L36-L48

SimpleAccount.sol

こちらはSimpleAccount.solにありました。
この部分にオリジナルの検証処理を実装することで、様々な認証方法が実現できるってことですね!

以下サンプルでは、ECDSAの検証処理が実装されています。
https://github.com/eth-infinitism/account-abstraction/blob/develop/contracts/samples/SimpleAccount.sol#L99-L106

マイナウォレットの_validateSignature

マイナウォレットでは、RSAの検証処理が実装されています。
https://github.com/a42io/ETHGlobalTokyo2023/blob/main/playground/test-contract/src/samples/SimpleAccount.sol#L117-L126

開発者の方のツイートによるとRSAの検証はガス代が高いらしいですが、それを解決する方法も研究されているとのことです。
https://twitter.com/7pastelblackcat/status/1701198076206334148

検証できた後はUserOperation内のcallDataの実行に移っていくわけですが、まだ調べきれていないため、いつか続きを書こうと思います。いつか。。

終わりに

なんとなく雰囲気だけ理解してた部分がしっかり理解できたので良かったです。
間違ってる部分があれば、ご指摘いただけると幸いです。

以下は大変参考にさせていただいた記事です。ありがとうございます。

https://zenn.dev/kozayupapa/scraps/782815787c5c33
https://zenn.dev/taxio/articles/834e6a04bd6b80
https://zenn.dev/yuki2020/articles/00242351b3b3aa

Discussion