2017.04.07

Z.com Cloud ブロックチェーンで仮想通貨を実装する ver.1


こんにちは。次世代システム研究室のT.M. です。

はじめに

本ブログでも、いくつかの記事で取り上げているように、Z.com Cloud ブロックチェーンおよびConoHa ブロックチェーンがβ版としてリリースされております。利用方法の解説として、ガイドが公開されておりますが、ガイドには、コントラクトの実装についてサンプル程度のものしかないため、実際にアプリケーションでは、どのような実装になるか、イメージがつかみにくいかもしれません。そこで、本記事では、実践的なアプリケーションの実装を行いましたので、全3回(予定)でそのコード解説およびデモンストレーションを行います。

アプリケーション ver.1 概要

アプリケーションとして、ブロックチェーンの代表的な応用例である仮想通貨を実現します。第1回の機能として、所持金の確認および送金機能を実現します。このアプリケーションのために、Solidity でスマートコントラクトを、HTML とJavascript を用いてWeb アプリケーションを作成します。

スマートコントラクト

Z.com Cloud ブロックチェーンで利用するスマートコントラクトは、コントラクトのインターフェイスとしてのMain コントラクト、ビジネスロジックであるLogic コントラクト、ログを記録するEvent コントラクト、データを記録するTable コントラクトの4つのコントラクトで構成されています。今回実装する仮想通貨のアプリケーション名はMyCoin としました。
各コントラクトの関係は以下のクラス図のようになります。
mycoin_class

Main コントラクト

Main コントラクトとして、MyCoinContract_v1.sol は以下の通りであり、ポイントとなるコードを解説します。
pragma solidity ^0.4.2;

import '../../gmo/contracts/VersionContract.sol';
import './MyCoinLogic_v1.sol';

contract MyCoinContract_v1 is VersionContract {
    uint constant TIMESTAMP_RANGE = 60;
    MyCoinLogic_v1 public logic_v1;

    function MyCoinContract_v1(ContractNameService _cns, MyCoinLogic_v1 _logic) VersionContract(_cns, 'MyCoin') {
        logic_v1 = _logic;
    }

    function setMyCoinLogic_v1(MyCoinLogic_v1 _logic) onlyByProvider {
        logic_v1 = _logic;
    }

    function checkTimestamp(uint _timestamp) private constant returns(bool) {
        return (now - TIMESTAMP_RANGE < _timestamp && _timestamp < now + TIMESTAMP_RANGE);
    }

    function exist(address _account) constant returns (bool) {
        return logic_v1.exist(_account);
    }

    function send(bytes _sign, uint _timestamp, address _to, uint _value) {
        if (!checkTimestamp(_timestamp)) throw;
        bytes32 hash = calcEnvHash('send');
        hash = sha3(hash, _timestamp);
        hash = sha3(hash, _to);
        hash = sha3(hash, _value);
        address sender = recoverAddress(hash, _sign);
        logic_v1.send(sender, _to, _value);
    }

    function getAmount(address _account) constant returns (uint) {
        return logic_v1.getAmount(_account);
    }
}

VersonContract の継承

Main コントラクトはVersonContract を継承する必要があります。また、VersionContract のコンストラクタのパラメータとして、CNS とコントラクト名が必要です。

Logic コントラクトインスタンスの設定

Main コントラクトは一連のコントラクトのインターフェイスであり、ビジネスロジックにリクエストを転送する役割があるため、Logic コントラクトインスタンスが必要です。そこで、コンストラクタでLogic コントラクトインスタンスを設定します。また、バグ修正等でLogic コントラクトを切り替えるために、Logic コントラクトのsetter も用意しておきます。このとき、onlyProvider により、このコントラクトをデプロイしたアカウントでしか、変更できないようにしておきます。

エンドユーザアドレスの取得

Z.com Cloud ブロックチェーンでは代払い機能のため、msg.sender でエンドユーザのアドレスを取得することができません。そこで、エンドユーザのアドレスを取得するために、エンドユーザがサインをしたものをパラメータに含める必要があります。トランザクションの1つめのパラメータは、エンドユーザのサインになりますので、そのサインを利用してエンドユーザのアドレスを復元します。復元するためにはパラメータのハッシュ値が必要であり、calcEncHash を利用します。技術的詳細な内容は、前回のブログに書いていますので、ご確認下さい。

Timestamp の確認

エンドユーザのサインにtimestamp が含まれていない場合、同じリクエストパラメータで何度もトランザクションを実行することができます。それを防ぐために、リクエストパラメータにtimestamp を入れ、now で取得できるトランザクション実行時刻と比較します。

Logic コントラクト

Logic コントラクトとして、MyCoinLogic_v1.sol は以下の通りであり、ポイントとなるコードを解説します。
pragma solidity ^0.4.2;

import '../../gmo/contracts/VersionLogic.sol';
import '../../gmo/contracts/AddressGroup_v1.sol';
import './MyCoinAccountTable_v1.sol';
import './MyCoinEvent_v1.sol';

contract MyCoinLogic_v1 is VersionLogic {
    uint constant TOTAL_AMOUNT = 1000000000;

    bool public initDone = true;
    MyCoinAccountTable_v1 public myCoinTable_v1;
    MyCoinEvent_v1 public event_v1;

    modifier onlyFirst() {
        if (!initDone) throw;
        _;
    }

    modifier sendable(address _sender, uint _value) {
        uint amount = getAmount(_sender);
        if (amount < _value) throw;
        _;
    }

    function MyCoinLogic_v1(ContractNameService _cns, MyCoinAccountTable_v1 _myCoinTable, MyCoinEvent_v1 _event) VersionLogic(_cns, 'MyCoin') {
        myCoinTable_v1 = _myCoinTable;
        event_v1 = _event;
    }

    function init(address _initAccount) onlyByProvider onlyFirst {
        myCoinTable_v1.create(_initAccount);
        myCoinTable_v1.setAmount(_initAccount, TOTAL_AMOUNT);
        initDone = false;
    }

    function setMyCoinTable_v1(MyCoinAccountTable_v1 _myCoinTable) onlyByProvider {
        myCoinTable_v1 = _myCoinTable;
    }

    function setMyCoinEvent_v1(MyCoinEvent_v1 _event) onlyByProvider {
        event_v1 = _event;
    }

    function exist(address _account) constant returns (bool) {
        return myCoinTable_v1.exist(bytes32(_account));
    }

    function send(address _from, address _to, uint _value) onlyByVersionContractOrLogic sendable(_from, _value) {
        if (!exist(_to)) myCoinTable_v1.create(_to);
        myCoinTable_v1.setAmount(_from, getAmount(_from) - _value);
        myCoinTable_v1.setAmount(_to, getAmount(_to) + _value);
        event_v1.send(_from, _to, _value);
    }

    function getAmount(address _account) constant returns (uint) {
        return myCoinTable_v1.getAmount(_account);
    }
}

VersionLogic の継承

Logic コントラクトはVersionLogic を継承する必要があります。また、VersionLogic
のコンストラクタのパラメータとして、CNS とコントラクト名が必要です。

通貨の発行

今回のアプリケーションでは、通貨の発行機能は実装しないため、最初に通貨を全部発行しておく必要があります。init により、あるアカウントにTOTAL_AMOUNT だけ通貨を発行します。このとき、onlyByProvider によりコントラクトをデプロイしたアカウントだけが、また、onlyFirst により最初の1回だけ実行できるようにします。

必要コントラクトインスタンスの設定

MyCoinLogic_v1 では、Event コントラクトおよび仮想通貨の所持金データを管理するMyCoinAccountTable_v1 というTable コントラクトへアクセスします。そのため、それぞれのコントラクトインスタンスが必要ですので、コンストラクタおよびsetter で設定します。

トランザクションのアクセス制限

Logic コントラクトでは、Main コントラクト以外からのトランザクションを受け入れることは、セキュリティ上の問題があります。そこで、トランザクションの関数には、onlyByVersionContractOrLogic をつけることで、Main コントラクトおよび別バージョンのLogic コントラクト以外のアクセスを制限します。

送金処理

送金する際は、送金元に送金額以上の通貨を持っている必要があります。sendable により、所持金が送金額を下回っているときは、エラーを発生させます。余談ですが、送金可能かのチェックにはpayable という単語を使いたくなるかと思いますが、payable は仮想通貨ではなく、ETH の送金処理を受け入れる際に使うmodifier になりますので、利用できません。
送金元が送金額以上の通貨を持っているならば、送金額だけ送金元の通貨を減らし、送金先の通貨を増やせばよい。

Table コントラクト

Table コントラクトとして、エンドユーザの所持金データを管理するMyCoinAccountTable_v1.sol は以下の通りであり、ポイントとなるコードを解説します。
pragma solidity ^0.4.2;

import '../../gmo/contracts/VersionField.sol';

contract MyCoinAccountTable_v1 is VersionField {
    struct Account {
        bool isCreated;
        uint amount;
    }

    mapping(bytes32 => Account) public accounts;

    function MyCoinAccountTable_v1(ContractNameService _cns) VersionField(_cns, 'MyCoin') {}

    /** OVERRIDE */
    function setDefault(bytes32 _id) private {
        accounts[_id] = Account({ isCreated: true, amount: 0 });
    }

    /** OVERRIDE */
    function existIdAtCurrentVersion(bytes32 _id) constant returns (bool) {
        return accounts[_id].isCreated;
    }

    function create(address _account) onlyByNextVersionOrVersionLogic {
        bytes32 id = bytes32(_account);
        if (exist(id)) throw;
        accounts[id] = Account({ isCreated: true, amount: 0 });
    }

    function setAmount(address _account, uint _amount) onlyByNextVersionOrVersionLogic {
        bytes32 id = bytes32(_account);
        prepare(id);
        accounts[id].amount = _amount;
    }

    function getAmount(address _account) constant returns (uint) {
        bytes32 id = bytes32(_account);
        if (shouldReturnDefault(id)) return 0;
        return accounts[id].amount;
    }
}

VersionField の継承

Table コントラクトはVersionField を継承する必要があります。また、VersionField
のコンストラクタのパラメータとして、データ操作を受け入れるCNS とコントラクト名が必要です。

データ構造

accounts は、連想配列であり、key をユーザのアドレス、value に所持金データを管理するAccount を持つ。ただし、VersionField の制約として、key はbytes32 でしか持てないので、address をbytes32 でキャストして利用する。address は20バイト、bytes32 は32バイトであるため、キャストをしても桁落ちは起こらないため、問題はない。

abstract 関数の実装

VersionField にはsetDefaultexistIdAtCurrentVersion という2つのabstract 関数があるため、それを実装する必要があります。setDefault では、このバージョンでのデータのデフォルト値を設定します。existIdAtCurrentVersion では、このバージョンでのデータが存在する場合、true を返します。

各カラムのsetter

Table コントラクトでは、ロジックを含めず、データの各カラムのsetter とgetter のみを用意します。setter はonlyByNextVersionOrVersionLogic をつけることで、次のバージョンのTable コントラクトおよびLogic コントラクト以外のアクセスを制限します。また、複数バージョンに展開している場合、データを統合するためにprepare を呼びます。

各カラムのgetter

複数バージョンに展開している場合、データを統合するためにshouldReturnDefault を呼び、true ならばdefault 値を返す。そうでないならば、このバージョンでの値を返します。

Event コントラクト

Event コントラクトとして、MyCoinEvent_v1.sol は以下の通りであり、ポイントとなるコードを解説します。
pragma solidity ^0.4.2;

import '../../gmo/contracts/VersionEvent.sol';

contract MyCoinEvent_v1 is VersionEvent {
    function MyCoinEvent_v1(ContractNameService _cns) VersionEvent(_cns, 'MyCoin') {}

    event SendEvent(address indexed _from, address indexed _to, uint _value);

    function send(address _from, address _to, uint _value) onlyByVersionLogic {
        SendEvent(_from, _to, _value);
    }
}

VersionEvent の継承

Event コントラクトはVersionEvent を継承する必要があります。また、VersionEvent
のコンストラクタのパラメータとして、操作を受け入れるCNS とコントラクト名が必要です。

送金イベント

今回のアプリケーションでは、ログとして記録に残す必要のある処理は、送金処理のみです。送金イベントを記録するためにSendEvent を定義します。記録するパラメータは送金元、送金先および送金額である。このとき、indexed をつけることで、イベントログから検索することができるようになります。

送金イベント関数

Logic コントラクトから送金イベントを発生させるために、送金イベントを呼ぶ関数を用意します。onlyByVersionLogic をつけることで、Logic コントラクト以外からのアクセスを制限します。

Web アプリケーション

上記で実装したMyCoin に対して、Web アプリケーションとして利用する方法を解説します。ここでは、上記コントラクトがデプロイされており、管理画面からCNS やABI を設定しているものとします。

鍵作成

Z.com Cloud ブロックチェーンでは、エンドユーザが確かに操作をした、ということを証明するために秘密鍵でサインをします。その鍵を作成する必要があり、作成方法は以下の通りであり、ポイントとなるコードを解説します。
<!DOCTYPE html>
<html>

<head>
    <title></title>
    <script src="http://beta.blockchain.z.com/static/client/lib/eth-client.min.js"></script>
    <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
    <script type="text/javascript">
    var account;

    function create() {
        ethClient.Account.create('https://beta.blockchain.z.com', $('#password').val(), function(err, instance) {
            if (err) {
                console.log(err);
            } else {
                account = instance;
                localStorage.setItem('account', account.serialize());
                $('#address').html(account.serialize());
            }
        });
    };

    function getAddress() {
        var serializedAccount = localStorage.getItem('account');
        account = ethClient.Account.deserialize(serializedAccount);
        $('#address').html(account.getAddress());
    };

    </script>
</head>

<body>
    <table>
        <tr>
            <td>password:</td>
            <td>
                <input id="password" value="password" />
            </td>
            <td>
                <button onclick="create()">create</button>
                <button onclick="getAddress()">getAddress</button>
            </td>
            <td><span id="address" /></td>
        </tr>
    </table>
</body>

</html>

ライブラリの読み込み

http://beta.blockchain.z.com/static/client/lib/eth-client.min.js にライブラリがありますので、それを読み込みます。

アカウントの作成

ethClient.Account.create でエンドユーザのアカウントが作成されます。1つ目のパラメータはサービスのURL であり、2つ目はアカウントのパスワードになります。返り値として、アカウントインスタンスが返るので、ローカルストレージに保存します。

アドレスの取得

ローカルストレージからアカウントインスタンスを生成し、getAddress でそのアカウントのアドレスを取得できます。

送金アプリ

<!DOCTYPE html>
<html>

<head>
    <title></title>
    <script src="http://beta.blockchain.z.com/static/client/lib/eth-client.min.js"></script>
    <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
    <script type="text/javascript">
    const CNS_ADDRESS = '0xf4eaf03dff63deafdfb68f202734ce15ec53eead';
    const MY_COIN_ABI = [{"constant":true,"inputs":[],"name":"provider","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"isVersionContract","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_sign","type":"bytes"},{"name":"_timestamp","type":"uint256"},{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"send","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_account","type":"address"}],"name":"exist","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"isVersionLogic","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"contractName","outputs":[{"name":"","type":"bytes32"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_logic","type":"address"}],"name":"setMyCoinLogic_v1","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"logic_v1","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"isVersionContractOrLogic","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"cns","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_account","type":"address"}],"name":"getAmount","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"getContractName","outputs":[{"name":"","type":"bytes32"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"getCns","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"inputs":[{"name":"_cns","type":"address"},{"name":"_logic","type":"address"}],"payable":false,"type":"constructor"}];
    const serializedAccount = localStorage.getItem('account');
    const account = ethClient.Account.deserialize(serializedAccount);
    const contract = new ethClient.AltExecCnsContract(account, CNS_ADDRESS);

    function send() {
        contract.sendTransaction('password', 'MyCoin', 'send', [Math.floor(new Date().getTime() / 1000), $("#to-address").val(), $("#value").val()], MY_COIN_ABI, function(err, res) {
            if (err) {
                alert('error');
                console.error(err);
            } else {
                console.log(res);
            };
        });
    }

    function getAmount() {
        contract.call('password', 'MyCoin', 'getAmount', [account.getAddress()], MY_COIN_ABI, function(err, amount) {
            if (err) {
                alert('err');
                console.error(err);
            } else {
                $('#amount').text(amount);
            }
        });
    }

    $(document).ready(function() {
        $('#my-address').text(account.getAddress());
        getAmount();
    });
    </script>
</head>

<body>
    <h3>アカウント情報</h3>
    <div>
        <div>アドレス</div>
        <div id="my-address"></div>
    </div>
    <div>
        <div>所持金</div>
        <div id="amount"></div>
    </div>
    <h3>送金</h3>
    <div>
        <div>送金先</div>
        <input id="to-address" />
    </div>
    <div>
        <div>金額</div>
        <input id="value" />
    </div>
    <div>
        <button onclick="send()">決定</button>
    </div>
</body>
</html>

コントラクトへのアクセス設定

API を経由してコントラクトにアクセスするためには、コントラクトを登録しているCNS のアドレス、コントラクト名およびABI が必要です。また、ethClient.AltExecCnsContract を利用して、コントラクトに対してsendTransactioncall を行います。

所持金表示

MyCoin コントラクトのgetAmount に対して、自分のアドレスをパラメータとしてcall します。

送金処理

MyCoin コントラクトのsend に対して、sendTransaction します。パラメータは、timestamp、送金先アドレス、送金額です。

デモンストレーション

今回作成したアプリのデモを行います。内容としては、所持金表示および送金処理です。

所持金表示

初期に全通貨を所持するアカウントのアドレスを0xfc865f061f183129985e9224a52293a95bb1f916、一般ユーザのアカウントのアドレスを0xadd1c664db02f7fcb898ed6cbb1fc63635c11a1d とする。初期全通貨所持アカウントの所持金は、1000000000、一般ユーザは所持金が0 になっている。
coin-admin-info coin-user-info

送金処理

初期全通貨所持アカウントから一般ユーザに送金処理を行います。成功すると、一般ユーザの所持金が増えます。
coin-admin-send coin-user-receive
送金処理の際、所持金以上の金額を送金するとき、エラーが発生します。
coin-user-send-error coin-user-error21
送金処理が成功すると、イベントが発生します。Truffle コンソールから以下の手順で初期全通貨アカウントからの送金ログを取得することができます。
var event = MyCoinEvent_v1.deployed().SendEvent({from:'0xfc865f061f183129985e9224a52293a95bb1f916'}, {fromBlock:0,toBlock:'latest'});
event.get(function(err, res) {
  console.log(res);
});
取得したログは以下になります。args にMyCoinEvent_v1.sol で定義したイベントのパラメータが記録されています。
[
  {
    address: '0xcba5b1c1a823505e6003d59019572dd50d80aca8',
    blockHash: '0xc42d0a275227258d7eee1d3ce84564e7bc35f746c610988989d90eafc0a59905',
    blockNumber: 1051987,
    logIndex: 0,
    transactionHash: '0xc4c2e25a8f2b9e3d1e727ecdd79f9af3bee8ff4a1f126e9bf9bcf7cfefb19107',
    transactionIndex: 0,
    transactionLogIndex: '0x0',
    type: 'mined',
    event: 'SendEvent',
    args:
    {
      _from: '0xfc865f061f183129985e9224a52293a95bb1f916',
      _to: '0xadd1c664db02f7fcb898ed6cbb1fc63635c11a1d',
      _value: [Object]
    }
  }
]

さいごに

Z.com Cloud ブロックチェーンを利用した仮想通貨アプリケーションを実装しました。特に、スマートコントラクトのSolidity のコードについては、サービス特有のものであるため、本記事が理解の助けになればと思います。また、この記事をきっかけにブロックチェーンのサービスは簡単に作れると感じていただければと思います。

次回予告

Z.com Cloud ブロックチェーンを利用したアプリケーションの特長としてバージョンアップができる、というものがあります。その特長を利用して、仮想通貨アプリケーションに機能追加をしたいと思います。

次世代システム研究室では、グループ全体のインテグレーションを支援してくれるアーキテクトを募集しています。インフラ設計、構築経験者の方、次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ 募集職種一覧 からご応募をお願いします。

皆さんのご応募をお待ちしています。