LoginSignup
4
1

More than 3 years have passed since last update.

Dockerを利用してScalar DLを使ってみた

Last updated at Posted at 2019-12-17

UDFの用語名称がFunctionに変更になったとのことで、本文を修正しました。(2020/1/10)

Scalar DLとは

Scalar社が開発した、ブロックチェーンにヒントを得た分散元帳プラットフォームソフトウェア。

(参考)
Scalar DLT | Scalar, Inc.
Scalar DL Docs

改ざん耐性と分散性を備える点ではブロックチェーンと同様だが、Scalar DLはさらに以下の特長を持っている。

  • ノード数増加に伴う性能劣化が少ない、高いスケーラビリティ
  • 高い可用性を備えたACID準拠のスマートコントラクト実行
  • 正確なファイナリティ
  • 線形化可能な一貫性

同時に、いくつかの制約事項を設けて、設計者や開発者が意図しないコントラクトの挙動を防いでいる。

  • 登録したスマートコントラクトは削除できない
  • 外部システムからスマートコントラクトのAssetを直接操作できない
  • 自作したスマートコントラクトを継承できない

Scalar DLで実行されたスマートコントラクトの実行結果は、同社が開発した分散データベース管理ソフトウェアであるScalar DBによって管理される。

Scalar DBをWSL Ubuntuで動かしてみる(環境構築編)

今回はこのScalar DLがアップデートされ、他のブロックチェーンや類似サービスにはないFunctionという独自の仕組み(後述)が追加されたとのことで、実際にdockerで動かしてみた。

注意

2019年12月現在、Scalar DLは商用ライセンスのみ提供されているため、実際の利用には別途Scalar社への問い合わせが必要。

連絡フォーム

開発環境

Ubuntu16.04 LTS (Hyper-VでWindows10上に構築)
Oracle Java 8

事前準備

Scalar DLの実行環境として、Scalar社からdockerコンテナが提供されている。
https://scalardl.readthedocs.io/en/latest/installation-with-docker/

dockerコンテナの起動にはDocker EngineとDocker Composeが必要なので、以下のサイトを参考にインストールしておく。
https://docs.docker.com/install/
https://docs.docker.com/compose/install/

また、javaの実行環境も必要。

$ apt update
$ sudo apt install openjdk-8-jdk

Scalar DLの実行準備

scalar-samplesリポジトリのクローン

$ git clone https://github.com/scalar-labs/scalar-samples.git
$ cd scalar-samples

ログイン

$ docker login

サービスのビルド

$ sudo docker-compose build
※不要の可能性あり。AWS、Azureのcent os 7では不要。

コンテナの起動

$ docker-compose up

末尾に-dオプションを付けるとバッググラウンドで起動させることができるが、Cassandraの起動が完了しないと以降の操作ができないため、ログを表示させておいて起動を確認する方がいいと思われる。

実行時に「実行可能なファイルがありません」などのエラーが出る場合、ファイルの実行権限を確認し、適宜実行権限を付与する。
$ chmod +x /path/to/file

初期スキーマのロード

Scalar DLを使用するために必要な初期スキーマをCassandraサーバにロードする必要がある。このコマンドは初回起動時の1回のみ実行すればよい。
$ docker-compose exec cassandra cqlsh -f /create_schema.cql

コンテナの停止

Ctrl + Cまたは$ docker-compose down

プロパティファイルの編集

scalar-samples/conf/client.propertiesを編集し、プロパティファイルを構成する。下記は最低限の設定である。

client.properties
# A host name of Scalar DL network server.
scalar.ledger.client.server_host=localhost

# An ID of a certificate holder. It must be configured for each private key and unique in the system.
scalar.ledger.client.cert_holder_id=foo

# A certificate file path to use.
scalar.ledger.client.cert_path=/path/to/foo.pem

# A private key file path to use. 
scalar.ledger.client.private_key_path=/path/to/foo-key.pem

自身の環境に合わせて以下の項目を編集する

  • scalar.ledger.client.server_host : Scalar DLサーバのホスト名 localhostでよい
  • scalar.ledger.client.cert_holder_id : 証明書の持ち主のID。適当な自身のユーザ名を指定しておく
  • scalar.ledger.client.cert_path : 証明書ファイルへのパス
  • scalar.ledger.client.private_key_path : 秘密鍵ファイルへのパス

コントラクトの作成

Scalar DLのコントラクトはContractクラスを拡張し、invokeメソッドをオーバーライドするJavaクラスである。今回はサンプルとして、asset_idとstateを入力されると、asset_idで指定されたアセットにstateを値として登録するサンプルコントラクトを作成する。

$ vi src/main/java/com/org1/contract/StateUpdater.java

StateUpdater.java
package com.org1.contract;

import com.scalar.ledger.asset.Asset;
import com.scalar.ledger.contract.Contract;
import com.scalar.ledger.exception.ContractContextException;
import com.scalar.ledger.ledger.Ledger;
import java.util.Optional;
import javax.json.Json;
import javax.json.JsonObject;

public class StateUpdater extends Contract {
  @Override
  public JsonObject invoke(Ledger ledger, JsonObject argument, Optional<JsonObject> properties) {

    if (!argument.containsKey("asset_id") || !argument.containsKey("state")) {
      // ContractContextException is the only throwable exception in a contract and
      // it should be thrown when a contract faces some non-recoverable error
      throw new ContractContextException("please set asset_id and state in the argument");
    }

    String assetId = argument.getString("asset_id");
    int state = argument.getInt("state");

    Optional<Asset> asset = ledger.get(assetId);

    if (!asset.isPresent() || asset.get().data().getInt("state") != state) {
      ledger.put(assetId, Json.createObjectBuilder().add("state", state).build());
    }
    return null;
  }
}

コンパイルや実行についての手順は後述

FunctionとFunctionで使用するスキーマの作成

Functionとは

Java言語で書かれたプログラムである。
コントラクト実行と同一トランザクション内で、Scalar DBに対してGetとPut、Deleteを実行することができる。

つまり何ができるかというと、Functionは、「改ざん耐性が求められるデータ(Immutable Data)処理と変更が必要となるデータ(Mutable Data)処理の両方を、同一のトランザクションで実現する」ことができるようになる。

設計・実装の際は、Function内ではコントラクトの引数を参照して処理に使用することができるが、コントラクト側ではFunctionの引数を参照することはできないという点に注意が必要。

Functionのサンプル

コントラクトの引数からasset_idstateを取得し、Functionの引数からuser_idを取得して、user_idstateをキーにasset_idを登録するFunctionを作成する。

$ vi src/main/java/com/scalar/ist/function/SchemaUpdater.java

SchemaUpdater.java
package com.scalar.ist.function;

import com.scalar.database.api.Get;
import com.scalar.database.api.Put;
import com.scalar.database.api.Result;
import com.scalar.database.io.IntValue;
import com.scalar.database.io.Key;
import com.scalar.database.io.TextValue;
import com.scalar.ledger.database.MutableDatabase;
import com.scalar.ledger.udf.Function;
import java.util.Optional;
import javax.json.JsonObject;

public class SchemaUpdater extends Function {
  @Override
  public void invoke(
    MutableDatabase database,
    JsonObject contractArgument,
    Optional<JsonObject> functionArgument) {

    String userId = functionArgument.get().getString("user_id");
    int state = contractArgument.getInt("state");
    String assetId = contractArgument.getString("asset_id");

    Get get =
      new Get(
        new Key(new TextValue("user_id", userId)),
        new Key(new IntValue("state", state)))
        .forNamespace("test")
        .forTable("test_schema");
    database.get(get);

    Put put =
      new Put(
        new Key(new TextValue("user_id", userId)),
        new Key(new IntValue("state", state)))
        .withValue(new TextValue("value",assetId))
        .forNamespace("test")
        .forTable("test_schema");
    database.put(put);
  }
}

Functionで使用するスキーマの作成

Functionが値をScalar DBに登録するためのスキーマを作成する。
ただし、ここで作成するスキーマはトランザクションに対応した形のスキーマである必要がある。
参考:Scalar DB Docs - Internal metadata in Scalar DB

スキーマ作成はCassandraのシェルからコマンドを入力して行う

$ docker-compose exec cassandra cqlsh
cqlsh> create table test.test_schema    
 (
  user_id text,
  state int,
  value text,
  before_value text,

  before_tx_committed_at bigint,
  before_tx_id text,
  before_tx_prepared_at bigint,
  before_tx_state int,
  before_tx_version int,
  tx_committed_at bigint,
  tx_id text,
  tx_prepared_at bigint,
  tx_state int,
  tx_version int,
  primary key (user_id, state)
 );

作成したテーブルの確認

cqlsh> use test;
cqlsh:test> describe tables;

test_schema

コントラクトとFunctionの登録・実行

コントラクト、Functionを実行するには、事前に各コントラクト、Functionに一意なIDを指定して秘密鍵で署名してScalar DLに登録する必要がある。この仕組みがあると誰が実行したかが明確になる、権限を持たないユーザの実行を防ぐことができる、などのメリットがある。

コンパイル

$ ./gradlew assemble

コントラクトのクラスファイルはbuild/classes/java/main/com/org1/contract/StateUpdater.classに作成される。

コントラクトの登録

登録用のシンプルなツールが用意されているので、利用する。登録の際にはプロパティファイルのパス、グローバルに一意なコントラクトのID、コントラクトのバイナリ名、クラスファイルのパスが必要になる。

$ client/bin/register-contract -properties conf/client.properties -contract-id StateUpdater -contract-binary-name com.org1.contract.StateUpdater -contract-class-file build/classes/java/main/com/org1/contract/StateUpdater.class

成功すると、status:200と表示される。

Functionの登録

コントラクトと同様ツールを使って登録する。プロパティファイルのパス、グローバルに一意なID、バイナリ名、クラスファイルのパスが必要な点も同じ。

client/bin/register-function -properties conf/client.properties -function-id SchemaUpdater -function-binary-name com.scalar.ist.function.SchemaUpdater -function-class-file build/classes/java/main/com/scalar/ist/function/SchemaUpdater.class

成功すると、status:200と表示される。

実行

上述の手順で登録したコントラクトとFunctionを実行する。実行もツールで行い、-contract-argumentオプションでコントラクトの引数を指定し、"_functions_": ["FunctionのID1","FunctionのID2",...](配列)という形式で同トランザクションで実行するFunctionの指定を行う。Functionで用いる引数は-function-argumentオプションで指定する。

client/bin/execute-contract -properties conf/client.properties -contract-id StateUpdater -contract-argument '{"asset_id": "my_asset", "state": 1, "_functions_": ["SchemaUpdater"]}' -function-argument '{"user_id": "john"}'

実行に成功するとstatus: 200と表示される。

注意点として、コントラクトでアセットの更新が行われない場合、Functionによるスキーマの更新も行われない。

確認

コントラクトを実行したら、問題なく実行されたかどうか確認を行う。コントラクトの実行結果についてはアセットの最新の値を取得するコントラクトを実装し、登録・実行して確認する。

StateReader.java
package com.org1.contract;

import com.scalar.ledger.asset.Asset;
import com.scalar.ledger.asset.InternalAsset;
import com.scalar.ledger.contract.Contract;
import com.scalar.ledger.ledger.Ledger;

import javax.json.Json;
import javax.json.JsonObject;
import javax.json.JsonObjectBuilder;
import java.util.Optional;

public class StateReader extends Contract {

  @Override
  public JsonObject invoke(Ledger ledger, JsonObject argument, Optional<JsonObject> properties) {
    String assetId = argument.getString("asset_id");

    Optional<Asset> asset = ledger.get(assetId);
    InternalAsset internal = (InternalAsset) asset.get();

    JsonObjectBuilder builder = Json.createObjectBuilder()
        .add("state", internal.data());

    return builder.build();
  }
}

コントラクト登録

$ client/bin/register-contract -properties conf/client.properties -contract-id StateReader -contract-binary-name com.org1.contract.StateReader -contract-class-file build/classes/java/main/com/org1/contract/StateReader.class

成功すると、status:200と表示される。

実行

client/bin/execute-contract -properties conf/client.properties -contract-id StateReader -contract-argument '{"asset_id": "my_asset"}'

正しく実行されていれば"state":{"state":1}と表示されるはず。

Functionの実行確認については、Cassandraのスキーマを直接確認する

$ docker-compose exec cassandra cqlsh
cqlsh> select * from test.test_schema;

正しく実行されていれば以下のような結果が得られるはず(tx_idなどの値はおそらく異なる)
image.png

4
1
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
4
1