見出し画像

マルチシグ・AA・MPCについて〜Web3 セキュリティ〜

皆さん、こんにちは!

先日、UNCHAINでマルチシグをテーマとした勉強会を実施したのですが思いのほか反響がありましたので多くの方にシェアしたいと思い、ブログでもまとめることにしました!

ぜひ UNCHAINに興味のある方は下のリンクからエントリーしてみてください!
本当に素晴らしいコミュニティですよ!!


1. マルチシグについて

そもそもマルチシグとは?

日本の暗号資産交換所であれば、交換所のラインセンスを取得するために顧客資産をコールドウォレットとマルチシグの仕組みを利用して管理することは必須となっています!

大事な顧客資産(それも膨大な額)を管理することになるので、内部不正を防ぐという意味でマルチシグは重要な鍵を握っていると考えています。

🔽 有名な暗号資産交換所でも取り入れられています!

ビットコインとイーサリアムでの実装の違い

マルチシグといってもその実装方法は、プロトコルレイヤーであるブロックチェーンの仕様にかなり左右されることになる。例えば、ビットコインとイーサリアムを比較しても次の様な違いがあります。

比較表

上記は、ビットコインとイーサリアムを比較した場合の話。その他のチェーンも考慮する必要がああるので日本の交換所は新しいチェーンに対応するのにかなり苦労しているのではないかと考えられます・・。

2. マルチシグコントラクトの実装例について

今回は、非常にシンプルなマルチシグコントラクトの仕組みをポンチ絵にまとめてみました!

ポンチ絵

factoryコントラクトを介してマルチシグコントラクトを生成することで、使う環境にあったownerのアドレス数と閾値を決めることができます!

この場合であれば下記のように処理を進めて目的のアドレスに送金します!

factoryコントラクトを介してマルチシグコントラクトを生成することで、使う環境にあったownerのアドレス数と閾値を決めることができます!

この場合であれば下記のように処理を進めて目的のアドレスに送金します!

♻️ ***送金までの流れ***

1. Factoryコントラクトにマルチシグウォレット作成要求を出す
2. 作成したマルチシグウォレットにETHをdepositする
3. 目的のアドレスにETHを送金するトランザクションデータを作成する。
4. ownerによる承認を行う。(所定の数の承認を得るまで実行)
5. トランザクションを実行してETHを目的のアドレスに送金する。

核となるMulitiSigWalletコントラクトにはとしては以下のような実装例があります!(Solidity by Exampleから抜粋してきました。)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract MultiSigWallet {
    event Deposit(address indexed sender, uint amount, uint balance);
    event SubmitTransaction(
        address indexed owner,
        uint indexed txIndex,
        address indexed to,
        uint value,
        bytes data
    );
    event ConfirmTransaction(address indexed owner, uint indexed txIndex);
    event RevokeConfirmation(address indexed owner, uint indexed txIndex);
    event ExecuteTransaction(address indexed owner, uint indexed txIndex);

    address[] public owners;
    mapping(address => bool) public isOwner;
    uint public numConfirmationsRequired;

    struct Transaction {
        address to;
        uint value;
        bytes data;
        bool executed;
        uint numConfirmations;
    }

    // mapping from tx index => owner => bool
    mapping(uint => mapping(address => bool)) public isConfirmed;

    Transaction[] public transactions;

    modifier onlyOwner() {
        require(isOwner[msg.sender], "not owner");
        _;
    }

    modifier txExists(uint _txIndex) {
        require(_txIndex < transactions.length, "tx does not exist");
        _;
    }

    modifier notExecuted(uint _txIndex) {
        require(!transactions[_txIndex].executed, "tx already executed");
        _;
    }

    modifier notConfirmed(uint _txIndex) {
        require(!isConfirmed[_txIndex][msg.sender], "tx already confirmed");
        _;
    }

    constructor(address[] memory _owners, uint _numConfirmationsRequired) {
        require(_owners.length > 0, "owners required");
        require(
            _numConfirmationsRequired > 0 &&
                _numConfirmationsRequired <= _owners.length,
            "invalid number of required confirmations"
        );

        for (uint i = 0; i < _owners.length; i++) {
            address owner = _owners[i];

            require(owner != address(0), "invalid owner");
            require(!isOwner[owner], "owner not unique");

            isOwner[owner] = true;
            owners.push(owner);
        }

        numConfirmationsRequired = _numConfirmationsRequired;
    }

    receive() external payable {
        emit Deposit(msg.sender, msg.value, address(this).balance);
    }

    function submitTransaction(
        address _to,
        uint _value,
        bytes memory _data
    ) public onlyOwner {
        uint txIndex = transactions.length;

        transactions.push(
            Transaction({
                to: _to,
                value: _value,
                data: _data,
                executed: false,
                numConfirmations: 0
            })
        );

        emit SubmitTransaction(msg.sender, txIndex, _to, _value, _data);
    }

    function confirmTransaction(
        uint _txIndex
    ) public onlyOwner txExists(_txIndex) notExecuted(_txIndex) notConfirmed(_txIndex) {
        Transaction storage transaction = transactions[_txIndex];
        transaction.numConfirmations += 1;
        isConfirmed[_txIndex][msg.sender] = true;

        emit ConfirmTransaction(msg.sender, _txIndex);
    }

    function executeTransaction(
        uint _txIndex
    ) public onlyOwner txExists(_txIndex) notExecuted(_txIndex) {
        Transaction storage transaction = transactions[_txIndex];

        require(
            transaction.numConfirmations >= numConfirmationsRequired,
            "cannot execute tx"
        );

        transaction.executed = true;

        (bool success, ) = transaction.to.call{value: transaction.value}(
            transaction.data
        );
        require(success, "tx failed");

        emit ExecuteTransaction(msg.sender, _txIndex);
    }

    function revokeConfirmation(
        uint _txIndex
    ) public onlyOwner txExists(_txIndex) notExecuted(_txIndex) {
        Transaction storage transaction = transactions[_txIndex];

        require(isConfirmed[_txIndex][msg.sender], "tx not confirmed");

        transaction.numConfirmations -= 1;
        isConfirmed[_txIndex][msg.sender] = false;

        emit RevokeConfirmation(msg.sender, _txIndex);
    }

    function getOwners() public view returns (address[] memory) {
        return owners;
    }

    function getTransactionCount() public view returns (uint) {
        return transactions.length;
    }

    function getTransaction(
        uint _txIndex
    )
        public
        view
        returns (
            address to,
            uint value,
            bytes memory data,
            bool executed,
            uint numConfirmations
        )
    {
        Transaction storage transaction = transactions[_txIndex];

        return (
            transaction.to,
            transaction.value,
            transaction.data,
            transaction.executed,
            transaction.numConfirmations
        );
    }
}

その他にもhttps://solidity-by-example.org/app/multi-sig-wallet/ やyoutubeなどにシンプルなマルチシグコントラクトを作成する方法は記載されています!

その他、セキュリティのスペシャリストとも言えるBitGo社やGnosisSafeが作成しているマルチシグコントラクトもあり、こちらはテストやコードのなどもかなりしっかりしており、構造も上のコントラクトより複雑になっています!

セキュアなコードを開発するのは難易度がやはり高そうです・・・。

3. デモ

↓ こちらのDappを利用します!

デモでは、2-of-3のマルチシグコントラクトを作成して実際に署名・送金を行います!

デモでやったこと

  1. 2-0f-3のマルチシグコントラクトウォレットを作成する。

  2. トランザクションデータを作成する。

  3. 1.で作成したマルチシグコントラクトウォレットに送金用のETHをdepositする。

  4. 2人のownerにより承認処理を実施する。

  5. 送金処理を実行する。

  6. 送金結果を確認する!

デモで利用するアドレス

owner1 : 0x51908F598A5e0d8F1A3bAbFa6DF76F9704daD072

owner2 : 0x9f9115893713934AA5AFd3540937eB60ea43eb3a

owner3 : 0xb5a05Af300C50baAadE18f8EA8AA7e856177Afe8

送信先:0xEef377Bdf67A227a744e386231fB3f264C158CDF

実際にデプロイしてあるコントラクトの情報

4. 残課題と拡張性について

現段階で感じている残課題・拡張性については以下の通りです。

  • 残課題

  • 拡張性

5. マルチシグと コントラクトウォレットについて

MultiSigWalletコントラクトには、「コントラクトウォレット」という考え方が採用されています。

コントラクトウォレットの応用としては、**AA(Account Abstraction)がかなり盛り上がってきており、ETHIndiaでもAA(Account Abstraction)**の考え方を採用したプロダクトが1st prizeを獲得するなど注目を集めています。

AAは使っていませんが、せっかくなのでコントラクトウォレットAAの考え方についても触れていきたいと思います!!

一旦、関連用語の整理をします!

💡 コントラクトウォレットとは

コントラクトを個人のウォレットアカウントのように使っているコントラクトのこと。

👉 EOAではないが、EOAの代わりのように扱うことができる。
👉 今日共有しているマルチシグコントラクトにもこの考え方を採用している。
👉 EOAのように秘密鍵が存在しないので、鍵管理するコストがなくなる!
👉アプリ提供側で独自のリカバリー機能を実装することができる。

💡 AA(Account Abstraction)とは

Vitalik が 2016 年頃からその実現に向けて取り組んできたトピック!
AAを利用することで、本来はトランザクションの起点することができないコントラクトアカウントを最上位アカウントにすることができます!

👉 EOAのようにコントラクトを振る舞わせることができる!

💡 EIP4337とは

2021.09.29 に提案されたAAに関する規格案。
バンドラーと呼ばれるユーザーのトランザクションの肩代わりを行うEOAが最低1つ存在する形にはなるものの、その他のユーザーのウォレットはコントラクトウォレットとして扱うことのできるシステム体系が考案されている!

👉 メタトランザクションを思い浮かべると分かりやすいかもしれません!
👉 バンドラーにトラストが集中する形になるので、バンドラーの鍵は権限分離して管理
  するか信頼できる第三者に預けてもらうのが良いかもしれません!
👉 とにかくユーザーが秘密鍵を管理しなくて良くなる点はでかいです!
👉 エントリーポイントコントラクトが、核となる機能部分を担っている。            
👉 エントリーポイントコントラクトに脆弱性があるとやばい・・。

EIP-4337の詳細についてはヴィタリックさんの記事の中でも紹介されている。

重要になるのは、Bundlerエントリーポイントコントラクト!!

今回のデモで共有したマルチシグDappdeでは、factoryコントラクトからMultiSigWalletコントラクトを生成し、owner達の共有の資金プールとしてMultiSigWalletコントラクトを利用しています。(複数人で一つのウォレットを共有して使っているイメージ)

AAとして実装されてはいないのでユーザーであるownerは、自分で鍵を所有して署名を行う必要がありますが、実行したいトランザクションデータをコントラクトウォレットに登録しておき、その後実行指示を出して送金用のトランザクションを実行しています。(ETHが出るのはコントラクトウォレットから!)

外からみると個人のウォレット(EOA)と何ら変わらないように振る舞っていますが、通常のウォレットと異なる点を整理すると下記の通りです。

  1. 個人のウォレット(EOA)のように秘密鍵が存在しないこと

  2. 承認が一定数得られた後宛先のアドレスにETHを送るという機能を持っていること

  3. deposit機能を持っていること

コントラクトウォレットの考え方については下記の記事とスライドがわかりやすくまとめてくれています!

AAについてさらに深く知りたいという方は、CryptoGamesのYukiさんがわかりやすく解説してくれている記事があるのでこちらも参照をおすすめします!

ERC-4337で実装されたシステムがどのように動くのか実際に手を動かしてみたいという方は、StackUp社がサンプルのリポジトリを公開してくれているのでお手軽にAAを利用した送金の仕組みを体験することができます!

  • 送金処理の実行例

    • MATICの送金処理

Signed UserOperation: {
      "sender": "0x100cD9e97EdAEe0950d97f75251313e45213C8Fb",
      "nonce": "0x0",
      "initCode": "0xe19e9755942bb0bd0cccce25b1742596b8a8250b3bf2c3e700000000000000000000000078d4f01f56b982a3b03c4e127a5d3afa8ebee68600000000000000000000000051908f598a5e0d8f1a3babfa6df76f9704dad0720000000000000000000000000000000000000000000000000000000000000000",
      "callData": "0x80c5c7d00000000000000000000000001431ea8af860c3862a919968c71f901aede1910e000000000000000000000000000000000000000000000000006a94d74f43000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000",
      "callGasLimit": "0x5580",
      "verificationGasLimit": "0x129727",
      "maxFeePerGas": "0xa2c22e83",
      "maxPriorityFeePerGas": "0xa2c22e65",
      "paymasterAndData": "0x",
      "preVerificationGas": "0xc644",
      "signature": "0xff954fe43538bf8fff0f928aa03f58c62b7c28d9b14912c4e1b452853124bd622053de4915a85e55697f714e80bf7668670e043467d2116df33979a1c2fd2f551b"
    }
    UserOpHash: 0x854c14b08bd913b7960edc3d4cbef6d65666e20e1494c4a0831a46cb18829fd1
    Waiting for transaction...
    Transaction hash: 0xc561b6a5410ffc8c10bd36b4102a08e57dc92040e4706526c3acedc42143d20d
    ✨  Done in 13.44s.
  • ERC20規格トークンの送金処理

Transferring 0.04 LINK...
    Signed UserOperation: {
      "sender": "0x100cD9e97EdAEe0950d97f75251313e45213C8Fb",
      "nonce": "0x1",
      "initCode": "0x",
      "callData": "0x80c5c7d0000000000000000000000000326c977e6efc84e512bb9c30f76e30c160ed06fb000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000044a9059cbb0000000000000000000000001431ea8af860c3862a919968c71f901aede1910e000000000000000000000000000000000000000000000000008e1bc9bf04000000000000000000000000000000000000000000000000000000000000",
      "callGasLimit": "0xe6fc",
      "verificationGasLimit": "0x186a0",
      "maxFeePerGas": "0xca0f4bb3",
      "maxPriorityFeePerGas": "0xca0f4b95",
      "paymasterAndData": "0x",
      "preVerificationGas": "0xc37c",
      "signature": "0x06bcf3dcded028a8c42ef90c72f4fea98e54ecd4da2366288cb50851ee60ee0512d6f74a998633109d9352ad0453e3665aa3171147bcf8a876676449499eeef61b"
    }
    UserOpHash: 0xade156da0c2b036926618ed914b2f87dca006d14ba4de29e949326be89cc4096
    Waiting for transaction...
    Transaction hash: 0xdc695a878c6bbfa8e149ea19fddcaad782f98707971d81201d4954113810dd2c
    ✨  Done in 13.16s.
  • MATICの送金処理(複数の一括送金)

Signed UserOperation: {
      "sender": "0x100cD9e97EdAEe0950d97f75251313e45213C8Fb",
      "nonce": "0x0",
      "initCode": "0x",
      "callData": "0x80c5c7d0000000000000000000000000100cd9e97edaee0950d97f75251313e45213c8fb000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000204d0cb75fa000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000002000000000000000000000000100cd9e97edaee0950d97f75251313e45213c8fb000000000000000000000000100cd9e97edaee0950d97f75251313e45213c8fb0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000044a9059cbb00000000000000000000000051908f598a5e0d8f1a3babfa6df76f9704dad07200000000000000000000000000000000000000000000000003782dace9d90000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044a9059cbb0000000000000000000000001431ea8af860c3862a919968c71f901aede1910e00000000000000000000000000000000000000000000000003782dace9d900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
      "callGasLimit": "0xe31a",
      "verificationGasLimit": "0x186a0",
      "maxFeePerGas": "0x7d7a574e",
      "maxPriorityFeePerGas": "0x7d7a5730",
      "paymasterAndData": "0x",
      "preVerificationGas": "0xd56c",
      "signature": "0x804964cc35c21fcb0fd1b4c4fe854a9deb5c7ea9117d68c31b4948914d282fdc53812ebb003ee542e35af8793b60917b0600be54fd05192598a6df396c6c47a81b"
    }
    UserOpHash: 0x77b6dd28dbd8691ea0ae23cc63bbd32f18889723e1c31f04cf534ae7e47569e5
    Waiting for transaction...
    Transaction hash: 0x381f4d559a033559d38370b24280ecad09017bf657f41e6eb277a0691cf5f248
    ✨  Done in 16.04s.
  • ERC20規格トークンの送金処理(複数の一括送金)

Batch transferring 0.04 LINK to 2 recipients...
    Signed UserOperation: {
      "sender": "0x100cD9e97EdAEe0950d97f75251313e45213C8Fb",
      "nonce": "0x2",
      "initCode": "0x",
      "callData": "0x80c5c7d0000000000000000000000000100cd9e97edaee0950d97f75251313e45213c8fb000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000204d0cb75fa000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000002000000000000000000000000326c977e6efc84e512bb9c30f76e30c160ed06fb000000000000000000000000326c977e6efc84e512bb9c30f76e30c160ed06fb0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000044a9059cbb0000000000000000000000001431ea8af860c3862a919968c71f901aede1910e000000000000000000000000000000000000000000000000008e1bc9bf040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044a9059cbb00000000000000000000000051908f598a5e0d8f1a3babfa6df76f9704dad072000000000000000000000000000000000000000000000000008e1bc9bf0400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
      "callGasLimit": "0xea85",
      "verificationGasLimit": "0x186a0",
      "maxFeePerGas": "0x99adc148",
      "maxPriorityFeePerGas": "0x99adc12a",
      "paymasterAndData": "0x",
      "preVerificationGas": "0xd560",
      "signature": "0x9efbbce4eedb58268350032f6033db120bfe54d34ca920bb15ff72c46f681a2032b2609cb910b646abb5cd87b65a5e1d1e86205bcc64e60d971fff210e72cd611c"
    }
    UserOpHash: 0x856e6d3741c710a76eb4e300fee208b8a33f93425f59be5ed3fea4a35ace9db9
    Waiting for transaction...
    Transaction hash: 0xab22e6bf970c6784aa6d9aa0e44a19adf428999b28370fbe4390fdcc63a31c63
    ✨  Done in 16.64s.

残課題にも記載した通り、秘密鍵を必要としないウォレットアプリ(その代わりKYCには厳格な仕様が求められるはず)の開発にも繋げられそうです!!

今の状態ではセキュリティを意識した仕組みになっていますが、様々なユースケースにも応用が効きそうな気がしています!

先日、ETHIndiaで1st prizeを獲得したFireWalletでもAAの考え方が採用されており、今後は使いやすく且つ安全なウォレットを躍起になって開発する動きが活発化していくと考えられるのでやはりこの設計パターンはマスターしたい・・!

秘密鍵の管理は、Web3のマスアダプションを妨げている大きな要因の一つだと考えており、AIやクラウドなどの様な世の中に受け入れられる汎用的な技術に昇華するためにはこの課題解決が必須!!

ただ、中にはAAが浸透していくことについて疑問視する方もいるので、どのようなウォレットがもっとも受け入れられていくかは、引き続きアンテナを貼って情報を収集しつつ、ハッカソンなどに出てプロトタイプを作っていくことが重要だと考えています!!

和組DAOukishimaさんのツイートは参考になりました!!

6. マルチシグ VS MPC

マルチシグと並んで、Web3領域で注目されている技術が MPC(マルチパーティ計算)を活用した署名の仕組みです!!

また用語の整理として、秘密計算・秘密分散・MPCの意味を整理します!

秘密計算とは

データを暗号化したまま計算することができる技術の総称。

秘匿計算などとも呼ばれ、データ分析でのプライバシー保護を強化する技術のひとつ。
秘密計算技術により、機密データの直接的な送受信を避け、暗号化したままデータ分析が実施できることから、組織間のデータ共有などアナリティクスの高度化につながると期待されている。

https://www.nri.com/jp/knowledge/glossary/lst/ha/secure_computation

この秘密計算の仕組みを実現させるために主要な方法が2種類存在する!🚀🚀🚀🚀🚀🚀🚀

  1. 準同型暗号方式

  2. 秘密分散方式

秘密分散とは

データを「それ自体は意味を持たない、いくつかの乱数の断片(シェア)」に分け、それぞれのシェアを別の環境で管理することで、外部へのデータ流出を防ぐ暗号化技術のこと。

👉 今日の話だと守りたいのは、秘密鍵!

https://www.eaglys.co.jp/news/column/secure-computing/mpc/

🔑 MPC(マルチパーティー計算)とは

上述した秘密分散方式の秘密計算の仕組みのことを指す。
秘匿マルチパーティ計算とも呼ばれる。

MPCでは、守りたいデータを分割したシェアを持った多数の参加者(パーティー)間で通信を行いながら秘密計算を実施する。そして、最終的な秘密計算の処理結果をアウトプットとして出す!

👉 ブロックチェーンに応用する場合には、署名データが最終的なアウトプットか

秘密計算と秘密分散について詳しく知りたい方は、下記リンクを確認してください!

↓ 秘密分散を利用して秘密鍵を分散し、そこから再度秘密鍵を復元⇨ アドレス復元 ⇨ 送金までをテストできる簡易スクリプトを作成してみましたので興味ある方はぜひこちらも!!

MPCを利用した署名機能を実装すると何が嬉しいのかというと、計算するサーバーのメモリ上など含めて一度も秘密情報(この場合だと秘密鍵)を外部に漏らすもしくは記録することなく最終的な署名済みトランザクションのデータを得ることができる点です!!

秘密鍵のデータをそのまま使うマルチシグに比べて、単体では意味のないデータに分割して管理することになるMPCの方がよりセキュアだと考えてられています。(万が一、一箇所で漏れてもダメージはMPCの方が小さいはずです。)

比較表

🔥 MPC型のウォレットソリューションを提供している代表的な事業者は、FireBlocks社!!

もっと知りたい!!という方は下記の記事がおすすめです!

7. 最後に

ウォレットコントラクトの考え方と合わせるとめちゃめちゃセキュアなウォレットアプリが作れるかも?? (KYCはゼロ知識証明や生体認証などを活用して厳格化iは必須ですね!!)

結局どこかに必ずトラストポイントが生まれるので、そこを如何に守りながら魅力的なアプリを提供していけるかが重要なポイントになりそうです!(一般ユーザーが全員公開鍵暗号方式のことを理解できれば良いのですが、実現できたとしてもとんでもない時間がかかりそうです・・。)

あとは、MetaMaskだとUX的にも少しハードルが高いのでもっとシンプルなプロダクトにする必要がありそうです。下のツイートは面白いと思った一例ですが、指輪の中に署名鍵を入れておいてそれで署名とかできたら見た目的にもオシャレだし使いやすいので世の中に浸透しそうですね!

ウォレットの話になるとソフトウェアだけでなくハード面も考えなくてはいけないので難しいですが最後はここを握ったところが勝ちそうですね!!

長文となりましたが、最後まで読んでいただきありがとうございました!
2023年もよろしくお願いいたします!!

参考文献


この記事が気に入ったらサポートをしてみませんか?