LoginSignup
5
5

More than 5 years have passed since last update.

MultiChain環境構築からJSON-RPC実行まで

Posted at

前提

MultiChain構築記事はほかにもありましたが、いくつかのコマンドの紹介と、JavaでのRPC実行まで説明します。

MultiChainとは

http://www.multichain.com/developers/
オープンプラットホーム(オープンソースとは言ってない)の
プライベートブロックチェーン技術です。
現在はまだアルファ版ですがWindows版も用意するなど、
開発は現在も進められています。
英語でコンタクトすればソースを見せてくれるかもしれません。

バイナリが配布されており簡単に試せて、ほかのブロックチェーン技術に比べ高速です。
ただしEthereumのようにコントラクト実行環境があるわけではなく、
あくまで送金主体のブロックチェーンです。

もともとはBitcoin互換のインタフェースとコマンドをそろえていましたが、
現在は別の方向にシフトし、エンジン部分も独自、コンセンサスも独自、
コマンドも独自のコマンドを用意しています。

今後も開発が続けられるかどうかは不明ですが、
環境構築とRPCの実行までメモしておきます。

環境構築

CentOS7ベースとします。(別にRedhatでもいいです)

AWSのEC2にCentOS7ベースでインスタンス作成

試すだけならmicroインスタンスでも問題ありませんが、
当然ながらパフォーマンスは出ませんので、
パフォーマンス検証まで行うならばせめてm4.largeあたりを使ってください。

事前準備とインストール

sudo yum update
sudo yum upgrade

wget http://www.multichain.com/download/multichain-1.0-alpha-25.tar.gz
tar -xvzf multichain-1.0-alpha-25.tar.gz
cd multichain-1.0-alpha-25
sudo mv multichaind multichain-cli multichain-util /usr/local/bin

下記ページに上記コマンドは書かれています。
versionが上がるとwgetでの取得ファイル名の記載も変わるので
基本的に下記ページを参考にコマンドを実行してください。
http://www.multichain.com/download-install/

ulimit変更

linuxの設定でulimit上限をあげておきます(再起動後も反映されるような設定で)。

 ulimit -n 65536

チェーン作成

$ multichain-util create チェーン名

例)multichain-util create chain1

設定ファイルなどが、

/home/ec2-user/.multichain/chain1

など、homeに作られます。

作られたファイルのうち、params.datがインスタンスのパラメータ設定ファイルです。
ノード間通信に使用するポート番号や、RPC実行時に使用するポート番号などもこれに記載されています。
multichain-util createコマンド実行のたびに番号はかわります。

注意

チェーンを作成後(起動してgenesisブロックが生成されたあと)はparams.datは変更できません(変更した場合、起動時にエラーとなります)。
変更したい場合はチェーンを削除して作り直しになります。
ただし、後から変更するような設定はありません。

チェーン削除

チェーンを停止してからディレクトリごと削除すればOKです。

$ multichain-cli chain1 stop
$ rm -rf chain1

設定

設定はmultichain.confファイルに書かれています。
このファイルは「.multichainディレクトリ」直下と、それぞれのchain1などの各チェーンのディレクトリ配下に存在します。

multichain.confには
rpcuserrpcpasswordが書かれています。
この内容はインストールのたびにかわります。

RPCでAPIを実行する場合、このrpcuserとrpcpasswordでbasic認証を行うことになります。(これはAPI呼び出し側の実装プログラムに組み込みます)

また、.multichainディレクトリ直下にあるmultichain.confはすべてのチェーンに影響するグローバル設定になります。

ここに、

rpcallowip=127.0.0.1/32

と記述すると、ローカルからのみRPCにアクセスできる設定になります。
アクセスを許可するネットワークの指定はここで行います。

API呼び出しを行うプログラムが入ったサーバが外部サーバであれば、
そのIP範囲をこのファイルに定義しておかなければ、アクセスできません。

AWSの場合はセキュリティグループの設定も忘れずに。

起動

$ multichaind チェーン名 -daemon

例)multichaind chain1 -daemon

初回起動時にgenesisブロックが作られます。
チェーンが作られた時点で、ユーザー(アドレス)は1つ既に存在しています(管理者・所有者)。
その後のオペレーションはほとんどmultichain-cliを使って行います。

multichain-cliを使って行うオペレーションはJSON-RPCでも行うことができます。
multichainのJSON-RPCはversion1です。
世の中のJSON-RPCのライブラリはほぼversion2対応なので注意。

multichain-cliでコマンドを実行すると、実際に送信されたJSONが表示されるので、これをもとにAPIアクセス用のプログラムでJSONを組み立てます。

ホームページ上にもAPI一覧はありますが、JSON形式で書かれているわけではないので、multichain-cliでコマンドを実行して実際の形式を確認した方がいいかもしれません。ただし、API一覧にはパラメータも書かれているので、API一覧の確認も必要です。

ほかのサーバーからこのノード(チェーン)に接続すると、ノードの持つアドレス間で通貨の送受信ができます。

別サーバで起動しているノードへの接続

IPとポート番号は確認してください。

$ multichaind チェーン名@IPアドレス:ポート番号 -daemon 

例)multichaind chain1@XX.XXX.XXX.XXX:9561 -daemon

これで、接続したノード同士での送金ができるようになります。

コマンド類

ユーザ(アドレス)一覧取得

$ multichain-cli chain1 getaddresses

新規ユーザ作成

$ multichain-cli chain1 getnewaddress
12n5Qv57P3LPWYdn2reTzhjBsmiT2EGjhKVyK9

アドレス(ユーザ)情報は、作成したノード(サーバ)にのみ情報が存在します。ほかのサーバで、存在しないアドレスに対してコマンドを実行してもエラーにはなりませんが、結果が空要素で返却されます。

例)getaddressbalancesなど

そのため実際のシステムとしては、アドレスとそのアドレスが登録されているノードPCのIPをDBなどに保存しておく必要があると思います。
(送金などのオペレーションを行うアドレスが存在するサーバに対して、APIを実行する)

権限付与

作成したばかりのユーザ(アドレス)には権限がありません。
管理者が権限を付与してから使用します。
とりあえず、一般ユーザにはconnect,send,receiveは付与しておきます。

一般ユーザ(createはバージョン1.0-alpha-24から追加)
$ multichain-cli chain1 grant 12n5Qv57P3LPWYdn2reTzhjBsmiT2EGjhKVyK8 connect,send,receive,create
管理ユーザ
$ multichain-cli chain1 grant 12n5Qv57P3LPWYdn2reTzhjBsmiT2EGjhKVyK8 connect,send,receive,issue,create,mine,admin

権限については以下のページにざっくりと書かれています。
http://www.multichain.com/developers/permissions-management/

connect to connect to other nodes and see the blockchain’s contents.
send to send funds, i.e. sign inputs of transactions.
receive to receive funds, i.e. appear in the outputs of transactions.
issue to issue assets, i.e. sign inputs of transactions which create new native assets.
create to create streams, i.e. sign inputs of transactions which create new streams.
mine to mine blocks, i.e. to sign the metadata of coinbase transactions.
activate to change connect, send and receive permissions for other users, i.e. sign transactions which change those permissions.
admin to change all permissions for other users, including issue, mine, activate and admin.

通貨定義と供給

作ったばかりのチェーンにはアセット(通貨)が存在しません。
通貨を定義かつ、供給します。
実行するにはissue権限が必要です。

$ multichain-cli チェーン名 issue 供給先アドレス 通貨名 供給量 単位

例)multichain-cli chain1 issue 1AK4o9uvBCsjB8AFARsXEm3ot7oB8GM3qpPFcD orgcoin 900000000 0.01

供給量を途中で増額する場合、通貨の定義時に、open:trueを指定しておく必要があります。実質こちらを使うと思います。

$ multichain-cli chain1 issue 1AK4o9uvBCsjB8AFARsXEm3ot7oB8GM3qpPFcD '{"name":"orgcoin2","open":true}' 900000000 0.01

送金

$ multichain-cli chain1 sendassetfrom 送金元アドレス 送金先アドレス 通貨名 送金額

例)multichain-cli chain1 sendassetfrom 1AK4o9uvBCsjB8AFARsXEm3ot7oB8GM3qpPFcD 12n5Qv57P3LPWYdn2reTzhjBsmiT2EGjhKVyK8 orgcoin2 10000

アドレスはノードPCそれぞれ独立して管理されますが、異なるノードに存在するアドレスへの送金は可能です。

・Aサーバに接続し、Aサーバのアドレス1からBサーバのアドレス2に対して送金を実行した場合

Bサーバに接続し、Bサーバのアドレス2の残高を確認すると、送金後の金額が表示される。

・Bサーバに接続し、Aサーバのアドレス1からBサーバのアドレス2に対して送金はできない。

BサーバにはAサーバのアドレス1の情報がない為。

アドレスの残高確認

$ multichain-cli chain1 getaddressbalances 1AK4o9uvBCsjB8AFARsXEm3ot7oB8GM3qpPFcD

通貨供給量増額(通貨定義時にopen:trueを指定した場合のみ実行可能)

$ multichain-cli chain1 issuemore 1AK4o9uvBCsjB8AFARsXEm3ot7oB8GM3qpPFcD orgcoin2 50000

RPC接続確認

RPCで外部から接続可能かどうか、とりあえずcurlで確認。

multichain.confに書かれたbasic認証のユーザとパスワードを確認しておくことと、curlを実行するサーバのIPがmultichain.confのrpcallowipに書かれていることを確認しておくこと。
RPC接続するポートは、起動時に表示されるポートではなく、params.datにかかれたポートなので確認すること。
JSON-RPC 1.0形式かつ、「chain_name」というパラメータがparamsとは別で指定できて、リクエストを投げられれば、どんな方法でも構いません。

$ curl -X POST --user multichainrpc:4hdtFi8aS7b5WN1Q9nLuEubWL64Z3hzduRqb44BZ7jpU http://XX.XXX.XXX.XXX:9560/  --data '{"method":"getaddresses","params":[],"id":1,"chain_name":"chain1"}'
{"result":["1KTBxbKPVwFa3kvUF37mKG2hp48toGaqJ9Mimz","1Z7W2g5rMFUhXKvqZhSfU77e8ZpHQtb8twu3qk"],"error":null,"id":1}

プログラムからアクセス

multichain-cliからコマンド実行したときに表示されるJSONを組み立ててPOSTします。
JSON-RPC実行側の言語はなんでもOKです。以下Javaでのプログラム例をあげておきます。
動かしてみたレベルなのであくまで参考で(エラー時のJsonレスポンスも組み立ててない)。

プログラム抜粋

通貨送金

package com.example.service;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import com.example.enums.Globals;
import com.example.request.BaseRequestObjectDto;
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;
import com.mashape.unirest.http.exceptions.UnirestException;

@Service
public class SendAssetFromService {

    private static final Logger LOGGER = LoggerFactory
            .getLogger(SendAssetFromService.class.getName());

    /**
     * 通貨送金
     * http://unirest.io/java.html
     * @param chainName チェーン名
     * @param fromAddress 送信元アドレス
     * @param toAddress 送信先アドレス
     * @param assetName 通貨名
     * @param quantity 金額
     * @return 結果Json
     */
    public String send(String chainName, String fromAddress, String toAddress,
            String assetName, long quantity) {

        Object[] params = { fromAddress, toAddress, assetName, quantity };
        HttpResponse<String> txid = null;
        BaseRequestObjectDto requestDto = new BaseRequestObjectDto();
        requestDto.setChainName(chainName);
        requestDto.setParams(params);
        requestDto.setId(1);
        requestDto.setMethod("sendassetfrom");
        String result = null;
        try {
            txid = Unirest.post(Globals.HOST)
                    .header("accept", Globals.JSON_MIME_TYPE)
                    .header("Content-Type", Globals.JSON_MIME_TYPE)
                    .basicAuth(Globals.TEST_USER, Globals.TEST_USER_PASS)
                    .body(requestDto).asString();

            result = txid.getBody();
            LOGGER.info(result);
            return result;
        } catch (UnirestException e) {
            LOGGER.warn(e.getMessage());
            result = e.getMessage();
        }
        return result;
    }

}
package com.example.service;

import java.io.IOException;

import org.springframework.stereotype.Service;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.mashape.unirest.http.ObjectMapper;
import com.mashape.unirest.http.Unirest;

@Service
public class UnirestInitService {
    static  {
        Unirest.setObjectMapper(new ObjectMapper() {
            private com.fasterxml.jackson.databind.ObjectMapper jacksonObjectMapper
                        = new com.fasterxml.jackson.databind.ObjectMapper();

            public <T> T readValue(String value, Class<T> valueType) {
                try {
                    return jacksonObjectMapper.readValue(value, valueType);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }

            public String writeValue(Object value) {
                try {
                    return jacksonObjectMapper.writeValueAsString(value);
                } catch (JsonProcessingException e) {
                    throw new RuntimeException(e);
                }
            }
        });
    }
}
package com.example.request;

public class BaseRequestObjectDto {

    private String method;

    private Object[] params;

    private int id;

    private String chainName;

    /**
     * @return method
     */
    public String getMethod() {
        return method;
    }

    /**
     * @param method セットする method
     */
    public void setMethod(String method) {
        this.method = method;
    }

    /**
     * @return params
     */
    public Object[] getParams() {
        return params;
    }

    /**
     * @param params セットする params
     */
    public void setParams(Object[] params) {
        this.params = params;
    }

    /**
     * @return id
     */
    public int getId() {
        return id;
    }

    /**
     * @param id セットする id
     */
    public void setId(int id) {
        this.id = id;
    }

    /**
     * @return chainName
     */
    public String getChainName() {
        return chainName;
    }

    /**
     * @param chainName セットする chainName
     */
    public void setChainName(String chainName) {
        this.chainName = chainName;
    }

}
package com.example.enums;

public class Globals {

    public static final String HOST = "http://XX.XXX.XXX.XXX:9560";
        // basic auth
    public static final String TEST_USER = "multichainrpc";
    public static final String TEST_USER_PASS = "4Bn3AYowKWWbtBX5Y11RLPK1qU5upbsWKmoh8qsUsai7";
    public static final String JSON_MIME_TYPE = "application/json";

}

残高確認

package com.example.service;

import java.util.ArrayList;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import com.example.enums.Globals;
import com.example.request.BaseRequestObjectDto;
import com.example.response.BalancesDto;
import com.example.response.ResponseGetAddressBalancesDto;
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.JsonNode;
import com.mashape.unirest.http.Unirest;
import com.mashape.unirest.http.exceptions.UnirestException;

@Service
public class AddressBalancesService {

    private static final Logger LOGGER = LoggerFactory.getLogger(AddressBalancesService.class.getName());

    /**
     * 残高確認
     * @param chainName チェーン名
     * @param address アドレス
     * @return 残高リスト
     */
    public List<BalancesDto> getAddressBalancesForView(String chainName,String address) {
        String[] params = {address};

        BaseRequestObjectDto requestDto = new BaseRequestObjectDto();
        requestDto.setChainName(chainName);
        requestDto.setParams(params);
        requestDto.setId(1);
        requestDto.setMethod("getaddressbalances");
        HttpResponse<ResponseGetAddressBalancesDto> innerResponse;
        List<BalancesDto> resultList = new ArrayList<>();
        try {
            innerResponse = Unirest.post(Globals.HOST)
              .header("accept", Globals.JSON_MIME_TYPE)
              .header("Content-Type", Globals.JSON_MIME_TYPE)
              .basicAuth(Globals.TEST_USER, Globals.TEST_USER_PASS)
              .body(requestDto)
              .asObject(ResponseGetAddressBalancesDto.class);
            ResponseGetAddressBalancesDto responseDto = innerResponse.getBody();
            if (responseDto.getResult() != null && responseDto.getResult().size()>0){
                return responseDto.getResult();
            }
        } catch (UnirestException e) {
            LOGGER.warn(e.getMessage());
        }
        return resultList;
    }
}
package com.example.response;

import java.util.List;

import com.fasterxml.jackson.annotation.JsonProperty;


public class ResponseGetAddressBalancesDto {

    @JsonProperty("result")
    private List<BalancesDto> result;

    private String error;

    private Integer id;

    /**
     * @return result
     */
    public List<BalancesDto> getResult() {
        return result;
    }

    /**
     * @param result セットする result
     */
    public void setResult(List<BalancesDto> result) {
        this.result = result;
    }

    /**
     * @return error
     */
    public String getError() {
        return error;
    }

    /**
     * @param error セットする error
     */
    public void setError(String error) {
        this.error = error;
    }

    /**
     * @return id
     */
    public Integer getId() {
        return id;
    }

    /**
     * @param id セットする id
     */
    public void setId(Integer id) {
        this.id = id;
    }


}
package com.example.response;

import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;

public class BalancesDto {

    private String name;

    private String assetref;

    private double qty;

    /**
     * @return name
     */
    public String getName() {
        return name;
    }

    /**
     * @param name セットする name
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * @return assetref
     */
    public String getAssetref() {
        return assetref;
    }

    /**
     * @param assetref セットする assetref
     */
    public void setAssetref(String assetref) {
        this.assetref = assetref;
    }

    /**
     * @return qty
     */
    public double getQty() {
        return qty;
    }

    /**
     * @param qty セットする qty
     */
    public void setQty(double qty) {
        this.qty = qty;
    }

    @Override
    public String toString() {
        return ToStringBuilder.reflectionToString(this, ToStringStyle.DEFAULT_STYLE);
    }
}
5
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
5