😁

マイナンバーカードで署名をするところを動かしてみよう:ERC-4337 (AccountAbstraction)

2023/05/06に公開

ETHGlobalTokyo2023 のマイナンバーをつかったデモのGithubから
SmartAccount部分の実装をおいかけていくと、署名部分は公開実装からは抜かれていたので、
自分の理解が正しいのか検証をかねて、コードを追加して動作するか試していきたいと思います。

ERC-4337の規格理解は下記
https://zenn.dev/kozayupapa/articles/dbfa38c14775f8

マイナチームのデモ全体については下記
https://zenn.dev/kozayupapa/scraps/782815787c5c33

userOperationに対する署名のつくりかた

ERC-4337(AccountAbstraction) では TransactionをUserOperationという下記の構造体につめて、それをbundler 経由でentryPointにわたすことで SmartAccountに検証してもらいます。

export interface IUserOperation {
  sender: string;
  nonce: BigNumberish;
  initCode: BytesLike;
  callData: BytesLike;
  callGasLimit: BigNumberish;
  verificationGasLimit: BigNumberish;
  preVerificationGas: BigNumberish;
  maxFeePerGas: BigNumberish;
  maxPriorityFeePerGas: BigNumberish;
  paymasterAndData: BytesLike;
  signature: BytesLike;
}

そのuserOperationに詰めるべき署名:signature はどのようにつくればよいでしょうか?

直接的には下記に定義されているように、SmartAccountのvalidateUserOp()の検証が通るように署名をすればよいわけで、具体的にはuserOpHashに署名をすればよいということになります。UserOpHashはUserOperationの署名以外の部分とentryPoint address, chainId でつくるとなっています。

https://eips.ethereum.org/EIPS/eip-4337

interface IAccount {
  function validateUserOp
      (UserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds)
      external returns (uint256 validationData);
}
The userOpHash is a hash over the userOp (except signature), entryPoint and chainId.

実装例としては stackupが提供している userOp lib の下記です。
EIPと対応するlibraryをあわせて読むと理解が早く進みます。

https://github.com/stackup-wallet/userop.js/blob/main/src/context.ts#L15

getUserOpHash() {
    const packed = ethers.utils.defaultAbiCoder.encode(
      [
        "address",
        "uint256",
        "bytes32",
        "bytes32",
        "uint256",
        "uint256",
        "uint256",
        "uint256",
        "uint256",
        "bytes32",
      ],
      [
        this.op.sender,
        this.op.nonce,
        ethers.utils.keccak256(this.op.initCode),
        ethers.utils.keccak256(this.op.callData),
        this.op.callGasLimit,
        this.op.verificationGasLimit,
        this.op.preVerificationGas,
        this.op.maxFeePerGas,
        this.op.maxPriorityFeePerGas,
        ethers.utils.keccak256(this.op.paymasterAndData),
      ]
    );

    const enc = ethers.utils.defaultAbiCoder.encode(
      ["bytes32", "address", "uint256"],
      [ethers.utils.keccak256(packed), this.entryPoint, this.chainId]
    );

    return ethers.utils.keccak256(enc);
  }

mynaチームの署名対応箇所 理解

myna デモでは my number card <-> android app <-> webapp <-> backend server <-> contract という構成になっていますが、 署名のハッシュは backend server で実装されています。
下記

https://github.com/a42io/ETHGlobalTokyo2023/blob/main/Server/src/app.ts#L214

ここで計算したhash を webapp, android app 経由で マイナンバーカードに署名してもらうんですね。なるほど!

Androidでおこなう mynumberカードでの署名処理

わかりやすく下記のコードをみればわかります
https://github.com/a42io/ETHGlobalTokyo2023/blob/main/MynaSigner/app/src/main/java/dev/a42/mynasigner/MainActivity.kt#L201

スマートカードに下記のコマンドを送ると署名してくれるんですね。感動です。
https://github.com/a42io/ETHGlobalTokyo2023/blob/main/MynaSigner/app/src/main/java/dev/a42/mynasigner/JpkiAP.kt#L52

このあたり詳細は下記も参考にしてください。
https://zenn.dev/kozayupapa/articles/f3f825eeef9aa4

以下の解説はマイナンバーのAPDUコマンドが一覧でのっていてバイトの意味を調べるのに便利でした
https://tex2e.github.io/blog/protocol/jpki-mynumbercard-with-apdu

Tips マイナンバーのパスワードを間違えた回数をリセットしたいとき

Androidのアプリで安全のためにパスワードトライできる回数が3回未満だと処理を中断するコードがはいっています。 一度でも間違えると 次にすすめなくなります。

安全上正しいと思いますがうっかり間違えると大変です。
そんなときは 本家のマイナアプリで ログインを行います。

https://apps.apple.com/jp/app/マイナポータル/id1476359069

ログインの最中に4桁のパスワードをきかれるので、それでログインできれば
無事 パスワードトライできるカウントも3回に復活します。

webapp に hash, 署名, transaction 追加

Githubでは の下記の部分でハッシュが固定値になっているので、このままでは実際のUserOperationは送信できません。backend自体には下記の実装はあるのでそれをwebappから呼び出せば実際の動作で確認できそうです。

1. webappから、backend server の /uo 署名以外のuserOperationと、Hashを計算
2. android経由でマイナンバーカードにhashを渡して署名してもらう
3. 署名をwebappがうけとり、 backend server の /transfer を呼び出す

https://github.com/a42io/ETHGlobalTokyo2023/blob/main/BrowserWallet/main.js#L75

上記の箇所に下記のコードを挿入します

	const modulus = localStorage.getItem(LOCAL_STORAGE_KEY_MODULUS)
	const url = `${BUNDLER_API_ENDPOINT}/uo?t=${to}&amt=${amount}&modulus=0x${modulus}`
	const res = await axios.get(url)
	const messageToSign = toArrayBuffer(res.data.userOpHash);
	local["uop"] = res.data.uop;
	//const messageToSign = toArrayBuffer("68656c6c6f");	// hello

そして、 android app に mynumberカードで署名してもらったものを詰め込むコードを下記の箇所にうめこみます。
https://github.com/a42io/ETHGlobalTokyo2023/blob/main/BrowserWallet/main.js#L152

	const uop = local["uop"];
	uop.signature = "0x"+toHexString(value);
	console.log("upo",uop);
	const url = `${BUNDLER_API_ENDPOINT}/transfer`
	const res = await axios.post(url,uop);
	console.log(res);

下記errors に書いたような errorは色々起こりましたが、
ひとつづつ解決して無事実行されました。

最初のTransactionが成功するとSmartAccount がDeployされる

成功すると下記のTransactionHashログがbackendに出力されます

Waiting for transaction...
Transaction hash: 0x27297b2bbd1cd3d17b0e9cbee8594a46e2589a2258e77698b172513995ebd2e5

実行前に mynumber に対応するContractをEtherscanでみると 下記のようにまだcontractと認識されていません。
faucetから0.2 matic を登録しているのでそれだけ確認できます。

userOperationのtransactionが成功するとEtherscan上もContractとなっていることがわかります。

CreateAccountはどこで?

SmartAccountFactoryのgetAddress()を呼び出すと対応するSmartAccountのDeploy前でもAddressが一意に特定できるという解説をしました。
https://zenn.dev/kozayupapa/articles/b0025b213ed39f

具体的には下記のコードです。
https://github.com/a42io/ETHGlobalTokyo2023/blob/main/playground/test-contract/src/samples/SimpleAccountFactory.sol#L50

では、SmartAccountのDeploy(CreateAccount)は誰がよびだすのでしょうか?
下記コメントをよむと どうやら userOperationの実行中に呼び出されるようです。
役割的にentryPointなどから呼び出されているのだとおもいます。
(気が向いたらそのあたりのコードも追加で見てみようと思います)

    /**
     * create an account, and return its address.
     * returns the address even if the account is already deployed.
     * Note that during UserOperation execution, this method is called only if the account is not deployed.
     * This method returns an existing account address so that entryPoint.getSenderAddress() would work even after account creation
     */
    function createAccount(

useroperationをbundler経由でentrypointに送るときのerros

  • EntryPointが対応していない!

          code: -32601,
          data: 'entryPoint: Implementation not supported'
    

    下記を読むとbundler にあわせて最新のEntryPointにしなさいと書いてあるのでEntryPointを変更

    //const ENTRY_POINT = "0x0576a174D229E3cFA37253523E645A78A0C91B57"; //v0.4
    const ENTRY_POINT = "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789"; //v0.6
    

    これで先に進むが、下記「対応していない2」 が発生したので結局もどすことに。。

  • signature に0x をつけなかったのでError

        code: -32601,
        data: "1 error(s) decoding:\n\n* error decoding 'signature': not byte string"
    

    単純にsignater 文字列に "0x" をつければ解決

  • Client のcall gas limit が低くてError

       code: -32602,
       data: 'callGasLimit: below expected gas of 33100'
    
    //const { callData, callGasLimit } =
    const { callData } =
      await accountAPI.encodeUserOpCallDataAndGasLimit(particial);
    const callGasLimit = "0x7a120";
    

accountAPIから取得している意図は理解しきれていませんが、いったん大きめの固定値をいれて回避します

  • EntryPointが対応していない2
       code: -32500, data: 'AA23 reverted: account: not from EntryPoint'   
      よくみてみると、SmartAccountFactoryをDeployするときに、EntryPopintのアドレスを指定していました。なので、Server側だけ最新のEntryPointにしてもうまくいきません。。。
      古いEntryPointに対応しているという。stackupのVer0.4のbundlerインスタンスをつくりなおします。

https://docs.stackup.sh/docs/entity-addresses

  • verification Gas limit
       code: -32500, data: 'AA40 over verificationGasLimit'   
      環境変数で指定していますが、はいはいそうですかと、一桁あげると、、

      code: -32602,
      data: 'verificationGasLimit: exceeds maxVerificationGas of 1500000'
    

    上限Overと怒られます。
    ほどほどに設定して 無事通るようになりました

EntryPointはどこにあるの?

ERC-4337:AccountAbstractionでは 何をするにもEntryPointというコントラクトが重要になってきます。
では、EntryPoint自体はどこにあるのでしょうか?

下記でDeploy時の指定にEntryPointのアドレスが指定されています。
https://github.com/a42io/ETHGlobalTokyo2023/blob/main/playground/test-contract/script/Deploy.s.sol#L15

        vm.startBroadcast(vm.envUint("PRIVATE_KEY"));
        SimpleAccountFactory factory = new SimpleAccountFactory(
            IEntryPoint(0x0576a174D229E3cFA37253523E645A78A0C91B57)
        );
        vm.stopBroadcast();

また、Serverでも上記userOpHashを使用するときに同じ値を指定しています
https://github.com/a42io/ETHGlobalTokyo2023/blob/main/Server/src/app.ts#L16

これははたして何のアドレスでしょうかと思い、ググると下記がヒットします。

https://twitter.com/erc4337/status/1631088619585937410

OfficialなERC4337の管理グループのひとがDeployしてくれたEntryPointなんですね。
EntryPointというのは 個別につくるものではないということなので、とりあえずは上記にPostされている
アドレスをチェーンごとに使い分ければよいようです。

とおもっていましたが、EntryPointは更新されていく。。

https://docs.stackup.sh/docs/entity-addresses

規格も更新中なのでしょうがないのでしょう。。
EntryPointがかわるとSmartAccountFactoryもDeployし直す必要があります。。

Discussion