🧐

マイナンバーカードの公開鍵からSmartAccountのアドレスを一意に導出する方法 Study

2023/04/30に公開
1

ETHGlobalTokyo2023 のマイナチームがデモしていた マイナンバーの公開鍵からWalletが一意に特定できるという部分の仕組みを理解していきます。

mynaチームのデモ全体の解説はこっち

Smart contract 側のコード

https://github.com/a42io/ETHGlobalTokyo2023/blob/main/playground/test-contract/src/samples/SimpleAccountFactory.sol

下記のように公開鍵から抜き出したmodulus と、固定のsalt を渡すことで計算しているようです。

    /**
     * calculate the counterfactual address of this account as it would be returned by createAccount()
     */
    function getAddress(
        bytes memory modulus,
        uint256 salt
    ) public view returns (address) {
        return
            Create2.computeAddress(
                bytes32(salt),
                keccak256(
                    abi.encodePacked(
                        type(ERC1967Proxy).creationCode,
                        abi.encode(
                            address(accountImplementation),
                            abi.encodeCall(SimpleAccount.initialize, (modulus))
                        )
                    )
                )
            );
    }

下記で詳しくみていきますが、  saltはそのまま渡し, EIP1014(ComputeAddress)で定義されているbytecodeHashとして、 ERC1967(Proxy) のcreationCode, SimpleAccountのImplementationConstructのアドレスを simpleAccount initialize に modulus を渡す呼び出しをencodeしたもののハッシュで、 計算したものを使用して一意に特定しているということがわかりました。

つまり、 salt や、 このSmartAccountFactory自体のコントラクトアドレスがかわれば、導出される Wallet(SmartAccount)もことなることになると理解できました!

このあたり、まだ理解できていないEIPやsolidityの文法が多くでてきたのでついでに理解をしておきましょう。

EIP1014 Create2.computeAddress

公式の説明の 要約

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

EIP-1014: Skinny CREATE2は、Ethereumの新しいオペコード(CREATE2)を追加する提案で、Vitalik Buterinによって作成されました。このオペコードは、4つのスタック引数(endowment, memory_start, memory_length, salt)を取り、通常の送信者とnonceハッシュの代わりに、特定の初期化コードによってのみコントラクトが初期化されるアドレスを計算するために、keccak256(0xff ++ address ++ salt ++ keccak256(init_code))[12:] を使用します。

Skinny CREATE2の主な目的は、チャネル内で実際または仮想的に行われるインタラクションを、まだオンチェーンに存在しないが、特定の初期化コードで作成される可能性のあるコードを含むアドレスとともに行うことを可能にすることです。これは、カウンターファクトによるコントラクトとのインタラクションを含むステートチャネルのユースケースに重要です。

EIP-1014は、既存のアドレス生成方法(keccak256(rlp([sender, nonce])))と衝突しないように設計されており、ハッシュの事前イメージが固定サイズになることを保証します。また、DoS攻撃への対策として、init_codeのハッシングに依存するアドレス計算のため、同じコストパーサードをSHA3オペコードとして使用しています。

理解したこと

この方法でAddressを計算することで、まだ実際にそれぞれのマイナンバーカードのRSA pubkeyに対応するSmartAccountがデプロイされるまえから アドレスが特定できるのですね。 なるほど、さすがvitalikさん。頭いい。

OpenZeppelinの実装

OpenZeppelinが提供するSmartContractのアドレスを導出する関数です。
下記のように分かりやすい解説つきの実装がのっています。
salt, hash, deployer でkeccakで計算して一意な値を返してくれます。

    /**
     * @dev Returns the address where a contract will be stored if deployed via {deploy}. Any change in the
     * `bytecodeHash` or `salt` will result in a new destination address.
     */
    function computeAddress(bytes32 salt, bytes32 bytecodeHash) internal view returns (address) {
        return computeAddress(salt, bytecodeHash, address(this));
    }
    /**
     * @dev Returns the address where a contract will be stored if deployed via {deploy} from a contract located at
     * `deployer`. If `deployer` is this contract's address, returns the same value as {computeAddress}.
     */
    function computeAddress(
        bytes32 salt,
        bytes32 bytecodeHash,
        address deployer
    ) internal pure returns (address addr) {
        /// @solidity memory-safe-assembly
        assembly {
            let ptr := mload(0x40) // Get free memory pointer

            // |                   | ↓ ptr ...  ↓ ptr + 0x0B (start) ...  ↓ ptr + 0x20 ...  ↓ ptr + 0x40 ...   |
            // |-------------------|---------------------------------------------------------------------------|
            // | bytecodeHash      |                                                        CCCCCCCCCCCCC...CC |
            // | salt              |                                      BBBBBBBBBBBBB...BB                   |
            // | deployer          | 000000...0000AAAAAAAAAAAAAAAAAAA...AA                                     |
            // | 0xFF              |            FF                                                             |
            // |-------------------|---------------------------------------------------------------------------|
            // | memory            | 000000...00FFAAAAAAAAAAAAAAAAAAA...AABBBBBBBBBBBBB...BBCCCCCCCCCCCCC...CC |
            // | keccak(start, 85) |            ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑ |

            mstore(add(ptr, 0x40), bytecodeHash)
            mstore(add(ptr, 0x20), salt)
            mstore(ptr, deployer) // Right-aligned with 12 preceding garbage bytes
            let start := add(ptr, 0x0b) // The hashed data starts at the final garbage byte which we will set to 0xff
            mstore8(start, 0xff)
            addr := keccak256(start, 85)
        }
    }

ERC-1967

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

要約

ERC-1967は、Ethereumブロックチェーン上で使用されるプロキシストレージスロットの標準化に関するEthereum Improvement Proposal (EIP) です。プロキシコントラクトはアップグレード可能性やガスの節約を目的として広く利用されており、ロジックコントラクト(または実装コントラクト)をdelegatecallを使って呼び出すことで、状態(ストレージとバランス)を維持しながらコードがロジックコントラクトに委任されます。

プロキシコントラクトとロジックコントラクト間のストレージ使用の衝突を回避するために、ロジックコントラクトのアドレスはコンパイラによって割り当てられることがない特定のストレージスロット(例:0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc in OpenZeppelin contracts)に保存されます。ERC-1967は、プロキシ情報を保存するための標準スロットを提案しています。これにより、ブロックエクスプローラなどのクライアントがこの情報を適切に抽出し、エンドユーザーに表示したり、ロジックコントラクトがそれに応じて処理を行うことが可能になります。

ERC-1967の主な目的は、プロキシ情報に対する標準化です。以下のストレージスロットが提案されています。

  1. ロジックコントラクトアドレス: プロキシが委任するロジックコントラクトのアドレスを保持します。
  2. ビーコンコントラクトアドレス: プロキシがビーコンコントラクトに依存する場合に保持されます。ビーコンは複数のプロキシのロジックアドレスを1か所に保持するために使用されます。
  3. 管理者アドレス: プロキシのロジックコントラクトアドレスをアップグレードする権限を持つアドレスを保持します。

この標準化により、ブロックエクスプローラや他のツールが、異なるプロキシ実装に対して共通の方法でロジックコントラクトのアドレスを取得し、それに応じて情報を表示できるようになります。また、ロジックコントラクトがプロキシされていることを認識して、アップデートなどの処理を行うことができます。

例えば、ブロックエクスプローラは、エンドユーザーがプロキシではなく、基本となるロジックコントラクトと対話したい場合があります。プロキシからロジックコントラクトアドレスを取得する共通方法があれば、ブロックエクスプローラはロジックコントラクトのABIを表示し、プロキシのそれではなく表示できます。エクスプローラは、指定されたスロットでコントラクトのストレージを確認してプロキシであるかどうかを判断し、プロキシとロジックコントラクトの両方の情報を表示します。

また、プロキシされていることに基づいて処理を行うロジックコントラクトもあります。これにより、ロジックコントラクトは、その処理の一部としてコードの更新をトリガーすることができます。共通のストレージスロットにより、これらのユースケースが特定のプロキシ実装に依存せずに可能となります。

このERC1967では、以下のプロキシ固有の情報のためのストレージスロットが提案されています。必要に応じて、追加情報のためのさらなるスロットが後続のERCで追加されることができます。

  1. ロジックコントラクトアドレス
    ストレージスロット0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbcは、プロキシが委任先としているロジックコントラクトのアドレスを保持します。このスロットの変更は、以下のイベントによって通知されるべきです:

event Upgraded(address indexed implementation);

  1. ビーコンコントラクトアドレス
    ストレージスロット0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50は、プロキシが依存するビーコンコントラクトのアドレスを保持します。このスロットの変更は、以下のイベントによって通知されるべきです:

event BeaconUpgraded(address indexed beacon);

ビーコンは、複数のプロキシのロジックアドレスを一元管理し、単一のストレージスロットを変更することで複数のプロキシをアップグレードできるようにするために使用されます。ビーコンコントラクトは、以下の関数を実装する必要があります:

function implementation() returns (address)

  1. 管理者アドレス
    ストレージスロット0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103は、プロキシのロジックコントラクトアドレスをアップグレードすることが許可されているアドレスを保持します(オプション)。このスロットの変更は、以下のイベントによって通知されるべきです:

event AdminChanged(address previousAdmin, address newAdmin);

このEIPの理由は、プロキシがエンドユーザーに対してロジックコントラクトと競合する可能性のある関数を公開しないようにすることです。なお、機能名が異なっていても競合が発生する可能性があります。これは、ABIが関数セレクタに4バイトしか使わないためです。これにより、予期しないエラーや攻撃が発生し、プロキシが呼び出しを妨害して独自の値で応答する可能性があります。

プロキシのパブリック関数が潜在的に悪用可能であるため、ロジックコントラクトアドレスを別の方法で標準化する必要があります。

openzeppelinの実装例

contract ERC1967Proxy is Proxy, ERC1967Upgrade {
    /**
     * @dev Initializes the upgradeable proxy with an initial implementation specified by `_logic`.
     *
     * If `_data` is nonempty, it's used as data in a delegate call to `_logic`. This will typically be an encoded
     * function call, and allows initializing the storage of the proxy like a Solidity constructor.
     */
    constructor(address _logic, bytes memory _data) payable {
        _upgradeToAndCall(_logic, _data, false);
    }

...
abstract contract ERC1967Upgrade is IERC1967 {
    // This is the keccak-256 hash of "eip1967.proxy.rollback" subtracted by 1
    bytes32 private constant _ROLLBACK_SLOT = 0x4910fdfa16fed3260ed0e7147f7cc6da11a60208b5b9406d12a635614ffd9143;

    /**
     * @dev Storage slot with the address of the current implementation.
     * This is the keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1, and is
     * validated in the constructor.
     */
    bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
    /**
     * @dev Stores a new address in the EIP1967 implementation slot.
     */
    function _setImplementation(address newImplementation) private {
        require(Address.isContract(newImplementation), "ERC1967: new implementation is not a contract");
        StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation;
    }

    /**
     * @dev Perform implementation upgrade
     *
     * Emits an {Upgraded} event.
     */
    function _upgradeTo(address newImplementation) internal {
        _setImplementation(newImplementation);
        emit Upgraded(newImplementation);
    }

    /**
     * @dev Perform implementation upgrade with additional setup call.
     *
     * Emits an {Upgraded} event.
     */
    function _upgradeToAndCall(
        address newImplementation,
        bytes memory data,
        bool forceCall
    ) internal {
        _upgradeTo(newImplementation);
        if (data.length > 0 || forceCall) {
            Address.functionDelegateCall(newImplementation, data);
        }
    }


理解したこと

erc1967は、マイナンバーと紐づくSimpleAccountの例では下記のように利用されています。
Proxyで実装を分離しつつ、modulus(マイナンバーのRSA pubkeyの一部)と一意に紐づくSmartAccountのアドレスをロジックコントラクトアドレスのストレージスロットに確保しているという意図だとわかりました。

        ret = SimpleAccount(
            payable(
                new ERC1967Proxy{salt: bytes32(salt)}(
                    address(accountImplementation),
                    abi.encodeCall(SimpleAccount.initialize, (modulus))
                )
            )
        );

type(C).creationCode

以下に詳細の説明があります。
https://solidity-jp.readthedocs.io/ja/latest/units-and-global-variables.html#type-information

SmartContruct:C のコンストラクタの内容そのもののバイトコードと理解しておけばよいとおもいます。

理解したこと

下記のように一意のアドレスを計算するときに ERC1967Proxyが同じロジックであれば同じアドレスにわりあたるはずなので、ERC1967Oroxyのコンストラクションコードと、 SmartAccountを生成するときに渡すパラメータをセットで渡して keccak256 をとれば 下記のどれかが変わればbytecodehashを変えることができます。

  1. ERC1967のProxy実装
  2. SmartAccountの実装
  3. modulus (マイナンバーのRSA Pub key)
           Create2.computeAddress(
                bytes32(salt),
                keccak256(
                    abi.encodePacked(
                        type(ERC1967Proxy).creationCode,
                        abi.encode(
                            address(accountImplementation),
                            abi.encodeCall(SimpleAccount.initialize, (modulus))
                        )
                    )
                )
            );

まとめ

以上で マイナンバーカードから一意のSmartAccount(AccountAbstraction)のアドレスを一意にきめるために、 EIP-1014: Skinny CREATE2は、Ethereumの新しいオペコード(CREATE2)と、ERC-1967:Ethereumブロックチェーン上で使用されるプロキシストレージスロットの標準化 と Saltを利用しています。

Saltや、SmartAccountの実装自体をどのように管理するかということが Walletが同じものが使える、使えないに直結するので、使い勝手、ユースケースを大きく左右します。 今後マイナンバーと連携して実現するという事例が増えてくると思うので 裏でつかわれている Salt, smart Accountをチェックしてみるようにしていきたいとおもいます。

Discussion