M-of-N consensus

業務系システムを作っていてとても面倒くさいのが、承認や権限周り。どうしても階層構造にならなければならなかったり、設定が細かくなりがちだったり。人事系システムが下手すると、従業員の各個々人のための制度を作ってしまうような場合もかつてはあったと聞いています。

そんな従来型の、承認フローをもしかしたら単純化できるのではないか、と個人的に注目しているのが、ブロックチェーンの提供するM-of-N consensus、Multi-party consensus (多人数合意形成)や、Multisigと呼ばれる仕組みです。M-of-N consensusは、N人の管理者中、M人が合意を以て、合意形成できたとみなす仕組みです。鍵を開けるのに、N個の秘密鍵のうち、M個があればよいという考え方です。

この仕組みは、Smart Contractのバグなどを攻撃されて、そのSmart Contractによるサービス提供を停止したい場合など、管理者が停止実行できるようにしたいけど、一方で事故でサービスを止めてしまうようなことも避けたい。そのために、複数の人が承認した場合にサービスを停止させる仕組みとして使われたりしているようです。

また、複数の署名が必要なので、一人悪い人行為を抑止する効果や、秘密鍵がなくなった場合の損害を防ぐ効果があります。

Ethereumのホワイトペーパーには、このmultisigについて以下の記述があります。

4. Smart multisignature escrow. Bitcoin allows multisignature transaction contracts where, for example, three out of a given five keys can spend the funds. Ethereum allows for more granularity; for example, four out of five can spend everything, three out of five can spend up to 10% per day, and two out of five can spend up to 0.5% per day. Additionally, Ethereum multisig is asynchronous - two parties can register their signatures on the blockchain at different times and the last signature will automatically send the transaction.
(抜粋訳:ビットコインは、たとえば、特定の5つのキーのうち3つが資金を使用できるマルチ署名トランザクション契約を許可できます。Ethereumでは更に細かく、「たとえば、5人のうち4人がすべてを費やし、5人のうち3人が1日あたり最大10%を費やし、5人のうち2人が1日あたり最大0.5%を費やすこと」ができるそうです。)

ここでは、簡単なmultisig機能を実装例を見てみます。

基本は、Smart Contractの内部変数のフラグで、サービス提供(Operation)の可否をコントロールします。このフラグのOn/Off切り替えを、規定数の管理者が認めた場合のみ実行できるようにします。

2-of-Nのmultisigアプリケーションとして、承認ユーザーを追加することができます。

一度承認したことのある管理者からの二度目以降の承認については無視するようにします。

pragma solidity >=0.4.25 <0.6.0;

contract SimpleMultisig {

    struct UserProfile {
        bool isRegistered;
        bool isAdmin;
    }

    address private contractOwner;
    mapping(address => UserProfile) userProfiles;
    bool private operational = true;
    uint constant M = 2;
    address[] multiCalls = new address[](0);

    constructor () public {
        contractOwner = msg.sender;
    }

    modifier IsContractOwner() {
        require(msg.sender == contractOwner, "Caller is not contract owner");
        _;
    }

    modifier IsOperational() {
        isOperational();
        _;
    }

    function isOperational() public view returns(bool) {
        return operational;
    }

    function isUserRegistered(address account) external view IsOperational returns(bool) {
        require(account != address(0), "'account' must be a valid address.");
        return userProfiles[account].isRegistered;
    }

    function isMultiCalls(address account) private view returns(bool) {
        bool isDuplicate = false;
        for(uint i=0; i<multiCalls.length; i++) {
            if (multiCalls[i] == account) { // true for "found the caller"
                isDuplicate = true;
                break;
            }
        }
        return isDuplicate;
    }

    function addCalledList(address account) private {
        multiCalls.push(account);
    }

    function setOperatingStatus(bool mode) external {
        require(mode != operational, "The mode is already set.");
        require(userProfiles[msg.sender].isAdmin, "Caller is not an admin");
        require(!isMultiCalls(msg.sender), "Caller has already called this function.");

        addCalledList(msg.sender);
        if (multiCalls.length >= M) {
            operational = mode;
            multiCalls = new address[](0);
        }
    }

    function registerUser(address account, bool isAdmin) external IsOperational IsContractOwner {
        require(!userProfiles[account].isRegistered, "User is already registered.");
        userProfiles[account] = UserProfile({isRegistered: true, isAdmin: isAdmin});
    }
}

コメント

タイトルとURLをコピーしました