2017.10.12

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


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

はじめに

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

アプリケーション ver.1,2 おさらい

ver.1 では、サンプルアプリケーションとして、仮想通貨を実装しました。Z.com Cloud ブロックチェーンのスマートコントラクトの作成方法に則り、Main コントラクト、Logic コントラクト、Event コントラクトおよびTable コントラクトの4種類のコントラクトを作成しました。仮想通貨の機能として、所持金の確認および仮想通貨の移動のみを実現しました。
ver.2 では、Z.com Cloud ブロックチェーンの、コントラクトのバージョンアップ可能という特長を利用して、仮想通貨の発行機能を追加しました。

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

一般的にブロックチェーンでは、登録されたデータはすべてのユーザが閲覧することができます。そのため、アクセスコントロールして秘匿したいデータはブロックチェーンに保存することができません。Z.com Cloud ブロックチェーンでは、アクセスコントロールしたいデータの実体はKVS に保存し、そのデータのハッシュ値をブロックチェーンに登録することで、データのアクセスコントロールをしつつ、その時、そのユーザが、そのデータを確かに保存したことを保証することができます。そこで、仮想通貨のためには必要のない機能ですが、アクセスコントロールしたデータを保存する方法を紹介するため、アカウントに名前を設定するという機能を追加します。

スマートコントラクト

名前の設定機能には、Main コントラクト、Logic コントラクトおよびTable コントラクトの変更が必要です。

Main コントラクト

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

import './MyCoinContract_v2.sol';
import './MyCoinLogic_v1.sol';
import './MyCoinLogic_v2.sol';
import './MyCoinLogic_v3.sol';

contract MyCoinContract_v3 is MyCoinContract_v2 {
    MyCoinLogic_v3 public logic_v3;

    function MyCoinContract_v3(ContractNameService _cns, MyCoinLogic_v1 _logic1, MyCoinLogic_v2 _logic2, MyCoinLogic_v3 _logic3) MyCoinContract_v2(_cns, _logic1, _logic2) {
        logic_v3 = _logic3;
    }

    function setName(bytes _sign, bytes32 _objectId, bytes32 _hash) {
        bytes32 hash = calcEnvHash('setName');
        hash = sha3(hash, _objectId);
        hash = sha3(hash, _hash);
        address sender = recoverAddress(hash, _sign);

        logic_v3.setName(sender, _objectId, _hash);
    }

    function addReader(bytes _sign, address _reader) {
        bytes32 hash = calcEnvHash('addReader');
        hash = sha3(hash, _reader);
        address sender = recoverAddress(hash, _sign);

        logic_v3.addReader(sender, _reader);
    }

    function getNameObjectId(address _account) constant returns (bytes32) {
        return logic_v3.getNameObjectId(_account);
    }
}

データの保存・取得関数の引数

setName() がデータを保存するための関数であり、その関数では、sign, objectId, hash が最初の3引数になります。objectId は保存するデータに対してつけるユニークなID であり、hash は保存するデータのハッシュ値です。そのハッシュ値がブロックチェーンに保存されることで確かにデータが保存されたことを保証します。getNameObjectId() がデータを取得するための関数です。返り値が取得したいデータのobjectId となるように実装します。

Logic コントラクト

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

import '../../gmo/contracts/VersionLogic.sol';
import '../../gmo/contracts/AddressGroup_v1.sol';
import '../../gmo/contracts/DataObject_v1.sol';
import './MyCoinAccountTable_v1.sol';
import './MyCoinAccountTable_v3.sol';


contract MyCoinLogic_v3 is VersionLogic {
    MyCoinAccountTable_v1 public myCoinTable_v1;
    MyCoinAccountTable_v3 public myCoinTable_v3;
    ContractNameService public constant gmoCns = ContractNameService(0x09c64940d7208142165da4a7953f98c7d3535438);

    function MyCoinLogic_v3(ContractNameService _cns, MyCoinAccountTable_v1 _myCoinTable1, MyCoinAccountTable_v3 _myCoinTable3) VersionLogic(_cns, 'MyCoin') {
        myCoinTable_v1 = _myCoinTable1;
        myCoinTable_v3 = _myCoinTable3;
    }

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

    function setName(address _sender, bytes32 _objectId, bytes32 _hash) onlyByVersionContractOrLogic {
        if (!exist(_sender)) throw;
        DataObject_v1 dataObject = DataObject_v1(gmoCns.getLatestContract('DataObject'));
        dataObject.create(_objectId, _sender, _hash, cns, 'MyCoin');
        myCoinTable_v3.setObjectId(_sender, _objectId);
    }

    function addReader(address _sender, address _reader) onlyByVersionContractOrLogic {
        if (!exist(_sender)) throw;
        bytes32 objectId = myCoinTable_v3.getObjectId(_sender);
        DataObject_v1 dataObject = DataObject_v1(gmoCns.getLatestContract('DataObject'));
        bytes32 readerId = dataObject.getReaderId(objectId);
        AddressGroup_v1 addressGroup = AddressGroup_v1(gmoCns.getLatestContract('AddressGroup'));
        address[] memory userAddresses = new address[](1);
        userAddresses[0] = _reader;
        addressGroup.addMembers(readerId, userAddresses);
    }

    function getNameObjectId(address _account) constant returns (bytes32) {
        return myCoinTable_v3.getObjectId(_account);
    }
}

DataObject の作成

DataObject と呼ばれるサービスで共通のコントラクトにデータのハッシュ値を保存することで、データを保存することができます。DataObject のコントラクトはGMO のCNS から’DataObject’ の名前でアドレスを引いてきます。DataObject_v1.sol のcreate() がデータを作成する関数で、ユニークなobjectId に対してデータのハッシュ値を保存します。このとき、今後リクエストを許可するコントラクトを設定するため、CNS アドレスとコントラクト名を設定します。また、アカウントとobjectId を紐付けるため、Table コントラクトに保存します。

Table コントラクト

Table コントラクトとして、MyCoinAccountTable_v3.sol は以下の通りです。objectId を保存するためにバージョンアップをします。コントラクトはver.1 とほぼ変わりませんが、コントラクトのデプロイ時にsetPrevVersion() およびsetNextVersion() を呼び、前後のバージョンを紐付ける必要があります。
pragma solidity ^0.4.2;

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

contract MyCoinAccountTable_v3 is VersionField {
    struct Account {
        bool isCreated;
        bytes32 objectId;
    }

    mapping(bytes32 => Account) public accounts;

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

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

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

    function setObjectId(address _account, bytes32 _objectId) onlyByNextVersionOrVersionLogic {
        bytes32 id = bytes32(_account);
        prepare(id);
        accounts[id].objectId = _objectId;
    }

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

Web アプリケーション

上記で実装したMyCoin に対して、Web アプリケーションとして利用する方法を解説します。ここでは、上記コントラクトがデプロイされているものとします。

ABI の変更

Main コントラクトが変更されたため、ABI を変更する必要があります。変更箇所は、管理画面およびクライアントアプリです。

クライアント

クライアントは以下の通りです。
<!DOCTYPE html>
<html>

<head>
    <title></title>
    <script src="http://beta.blockchain.z.com/static/client/lib/eth-client.js"></script>
    <!--script src="../../lib/bundle.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 = '0x7d0fb27bbb9f45a32305f724cd010d75056db19e';
    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":true,"inputs":[],"name":"logic_v3","outputs":[{"name":"","type":"address"}],"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":"_sign","type":"bytes"},{"name":"_timestamp","type":"uint256"},{"name":"_value","type":"uint256"}],"name":"create","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_logic","type":"address"}],"name":"setMyCoinLogic_v1","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_sign","type":"bytes"},{"name":"_reader","type":"address"}],"name":"addReader","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"logic_v1","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"logic_v2","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_logic2","type":"address"}],"name":"setMyCoinLogic_v2","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_account","type":"address"}],"name":"getNameObjectId","outputs":[{"name":"","type":"bytes32"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"isVersionContractOrLogic","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_sign","type":"bytes"},{"name":"_objectId","type":"bytes32"},{"name":"_hash","type":"bytes32"}],"name":"setName","outputs":[],"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":"_logic1","type":"address"},{"name":"_logic2","type":"address"},{"name":"_logic3","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);
            }
        });
    }

    function getName() {
        contract.getData('password', 'MyCoin', 'getNameObjectId', [account.getAddress()], MY_COIN_ABI, function(err, [name]) {
            if (err) {
                alert('err');
                console.error(err);
            } else {
                console.log(name);
                if (name.status === 200) $('#name').text(name.data);
                else $('#name').text('no name');
            }
        });
    }

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

    function setName() {
        contract.sendData('password', 'MyCoin', 'setName', createRandomId(64), $("#registered-name").val(), [], MY_COIN_ABI, function(err, res) {
            if (err) {
                alert('error');
                console.error(err);
            } else {
                console.log(res);
            };
        });
    }

    function createRandomId(len) {
        const c = '0123456789abcdef';
        let id = '0x';
        for (let i = 0; i < len; i++) {
            id += c[Math.floor(Math.random() * c.length)];
        }
        return id;
    }

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

<body>
    <h3>アカウント情報</h3>
    <div>
        <div>アドレス</div>
        <div id="my-address"></div>
    </div>
    <div>
        <div>所持金</div>
        <div id="amount"></div>
    </div>
    <div>
        <div>名前</div>
        <div id="name"></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>
    <h3>発行</h3>
    <div>
        <div>金額</div>
        <input id="create-value" />
    </div>
    <div>
        <button onclick="create()">決定</button>
    </div>
    <h3>登録</h3>
    <div>
        <div>名前</div>
        <input id="registered-name" />
    </div>
    <div>
        <button onclick="setName()">登録</button>
    </div>
</body>

</html>

データの保存・取得

sendData() によりデータを保存するリクエストを送ります。sendTransaction() との違いはobjectId と保存したいデータが引数に入ります。
getData() によりデータを取得するリクエストを送ります。結果は以下のようなかたちで返ってきます。

[{"objectId":"0x4990950210585d9a037198101dadf95059686502187ae7cf388ea472516a232a","error":"Not yet confirmed","status":204}]

getData() では、複数のデータを同時に取得することができるため、結果は配列であり、objectId, status, error, data のキーがあります。status が200 のときは、正常にデータが取得できたことを意味し、data にそのデータが入っています。データの保存の完了には数秒かかるため、完了する前にデータ取得した場合、204 のステータスが返ってきます。また、データが存在しない404 やデータ取得権限がない403 といったエラーがあります。

デモンストレーション

実装したアプリケーションのデモンストレーションを行います。

名前をつける前は、”no name” が入ります。
coin-admin-info
入力フォームに名前を入力して登録します。

coin-admin-info
数秒後、リロードすると入力した名前が表示されます。

coin-admin-info

さいごに

Z.com Cloud ブロックチェーンの特長である、データのアクセスコントロールを利用した機能を追加しました。一般的なプログラムとは異なるため、理解しづらいかもしれません。この記事が理解の助けになればと思います。


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

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