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

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

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

（参考）
[Scalar DLT | Scalar, Inc.](https://scalar-labs.com/product/)
[Scalar DL Docs](https://scalardl.readthedocs.io/en/latest/)

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

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

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

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

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

[Scalar DBをWSL Ubuntuで動かしてみる(環境構築編)](https://qiita.com/y18sakamoto/items/07969c960fb776d78990)

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

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

[連絡フォーム](https://scalar-labs.com/contact_us/)

# 開発環境
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_id`と`state`を取得し、Functionの引数から`user_id`を取得して、`user_id`と`state`をキーに`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](https://scalardb.readthedocs.io/en/latest/schema/#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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/448055/c6abe6b9-c382-ebc3-4723-bc1e1cb8e7b0.png)