マイナンバーカードで署名をするところを動かしてみよう:ERC-4337 (AccountAbstraction)
ETHGlobalTokyo2023 のマイナンバーをつかったデモのGithubから
SmartAccount部分の実装をおいかけていくと、署名部分は公開実装からは抜かれていたので、
自分の理解が正しいのか検証をかねて、コードを追加して動作するか試していきたいと思います。
ERC-4337の規格理解は下記
マイナチームのデモ全体については下記
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 でつくるとなっています。
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をあわせて読むと理解が早く進みます。
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 で実装されています。
下記
ここで計算したhash を webapp, android app 経由で マイナンバーカードに署名してもらうんですね。なるほど!
Androidでおこなう mynumberカードでの署名処理
わかりやすく下記のコードをみればわかります
スマートカードに下記のコマンドを送ると署名してくれるんですね。感動です。
このあたり詳細は下記も参考にしてください。
以下の解説はマイナンバーのAPDUコマンドが一覧でのっていてバイトの意味を調べるのに便利でした
Tips マイナンバーのパスワードを間違えた回数をリセットしたいとき
Androidのアプリで安全のためにパスワードトライできる回数が3回未満だと処理を中断するコードがはいっています。 一度でも間違えると 次にすすめなくなります。
安全上正しいと思いますがうっかり間違えると大変です。
そんなときは 本家のマイナアプリで ログインを行います。
ログインの最中に4桁のパスワードをきかれるので、それでログインできれば
無事 パスワードトライできるカウントも3回に復活します。
webapp に hash, 署名, transaction 追加
Githubでは の下記の部分でハッシュが固定値になっているので、このままでは実際のUserOperationは送信できません。backend自体には下記の実装はあるのでそれをwebappから呼び出せば実際の動作で確認できそうです。
1. webappから、backend server の /uo 署名以外のuserOperationと、Hashを計算
2. android経由でマイナンバーカードにhashを渡して署名してもらう
3. 署名をwebappがうけとり、 backend server の /transfer を呼び出す
上記の箇所に下記のコードを挿入します
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カードで署名してもらったものを詰め込むコードを下記の箇所にうめこみます。
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が一意に特定できるという解説をしました。
具体的には下記のコードです。
では、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インスタンスをつくりなおします。
-
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のアドレスが指定されています。
vm.startBroadcast(vm.envUint("PRIVATE_KEY"));
SimpleAccountFactory factory = new SimpleAccountFactory(
IEntryPoint(0x0576a174D229E3cFA37253523E645A78A0C91B57)
);
vm.stopBroadcast();
また、Serverでも上記userOpHashを使用するときに同じ値を指定しています
これははたして何のアドレスでしょうかと思い、ググると下記がヒットします。
OfficialなERC4337の管理グループのひとがDeployしてくれたEntryPointなんですね。
EntryPointというのは 個別につくるものではないということなので、とりあえずは上記にPostされている
アドレスをチェーンごとに使い分ければよいようです。
とおもっていましたが、EntryPointは更新されていく。。
規格も更新中なのでしょうがないのでしょう。。
EntryPointがかわるとSmartAccountFactoryもDeployし直す必要があります。。
Discussion