記事の内容
Hyperledger Irohaのドキュメントに書いてあるJavaSDKのサンプルコードを動かしてみました。
動かすまでにかなり時間がかかってしまったのでやった内容を残しておきます。
環境
- JDK 1.8
- Hyperledger Iroha:1.0.0
環境構築
手順はこちらのドキュメントを参考にしました。
https://iroha.readthedocs.io/ja/latest/getting_started/index.html
実装
eclipseでmavenプロジェクトを作成してサンプルコードを動かしました。
pom.xml
サンプルコードの前にかなり躓いた点ですが、コンパイルを通すまでにかなり苦戦しました。
いろいろ試した結果、以下のpom.xmlになりました。
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<name>iroha</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
<maven.compiler.target>${java.version}</maven.compiler.target>
<maven.compiler.source>${java.version}</maven.compiler.source>
</properties>
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
<repository>
<id>mvn</id>
<url>https://mvnrepository.com</url>
</repository>
</repositories>
<dependencies>
<!-- https://mvnrepository.com/artifact/io.grpc/protoc-gen-grpc-java -->
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
<version>1.22.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.grpc/grpc-netty -->
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty</artifactId>
<version>1.22.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.grpc/grpc-stub -->
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-stub</artifactId>
<version>1.22.1</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>protoc-gen-grpc-java</artifactId>
<version>1.12.0</version>
</dependency>
<dependency>
<groupId>com.github.hyperledger</groupId>
<artifactId>iroha-java</artifactId>
<version>6.1.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/jp.co.soramitsu/iroha -->
<dependency>
<groupId>jp.co.soramitsu</groupId>
<artifactId>iroha</artifactId>
<version>0.0.8</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
<scope>provided</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/com.google.protobuf/protobuf-java-util -->
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java-util</artifactId>
<version>3.10.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.google.protobuf/protobuf-java -->
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.10.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.testinfected.hamcrest-matchers/core-matchers -->
<dependency>
<groupId>org.testinfected.hamcrest-matchers</groupId>
<artifactId>core-matchers</artifactId>
<version>1.5</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
<compilerArgument>-Werror</compilerArgument>
</configuration>
</plugin>
</plugins>
</build>
</project>
躓いた点は2点です。
1点目は「protoc-gen-grpc-java」のjarファイルと何か依存関係があるみたいで、jarファイルが無いとエラーになりました。
依存関係を追加したものの、「protoc-gen-grpc-java」のmavenリポジトリにjarファイルが存在しませんでした。
色々試してみた結果、適当なjarファイルをリネームし、ローカルに配置することで解決しました。
どうやら、依存関係はあるものの呼び出されてはいないみたいです。
2点目はirohaのjarの中でjunitの「TestRule」というクラスを参照しているみたいです。
このクラスはjunitのバージョン4.12から追加されたものらしく、バージョンを4.12以上にする必要がありました。
サンプルコード
ドキュメントに載っているサンプルコードです。
import iroha.protocol.BlockOuterClass;
import iroha.protocol.Primitive.RolePermission;
import java.math.BigDecimal;
import java.security.KeyPair;
import java.util.Arrays;
import jp.co.soramitsu.crypto.ed25519.Ed25519Sha3;
import jp.co.soramitsu.iroha.testcontainers.IrohaContainer;
import jp.co.soramitsu.iroha.testcontainers.PeerConfig;
import jp.co.soramitsu.iroha.testcontainers.detail.GenesisBlockBuilder;
import lombok.val;
public class Example1 {
private static final String bankDomain = "bank";
private static final String userRole = "user";
private static final String usdName = "usd";
private static final Ed25519Sha3 crypto = new Ed25519Sha3();
private static final KeyPair peerKeypair = crypto.generateKeypair();
private static final KeyPair useraKeypair = crypto.generateKeypair();
private static final KeyPair userbKeypair = crypto.generateKeypair();
private static String user(String name) {
return String.format("%s@%s", name, bankDomain);
}
private static final String usd = String.format("%s#%s", usdName, bankDomain);
/**
* <pre>
* Our initial state cosists of:
* - domain "bank", with default role "user" - can transfer assets and can query their amount
* - asset usd#bank with precision 2
* - user_a@bank, which has 100 usd
* - user_b@bank, which has 0 usd
* </pre>
*/
private static BlockOuterClass.Block getGenesisBlock() {
return new GenesisBlockBuilder()
// first transaction
.addTransaction(
// transactions in genesis block can have no creator
Transaction.builder(null)
// by default peer is listening on port 10001
.addPeer("0.0.0.0:10001", peerKeypair.getPublic())
// create default "user" role
.createRole(userRole,
Arrays.asList(
RolePermission.can_transfer,
RolePermission.can_get_my_acc_ast,
RolePermission.can_get_my_txs,
RolePermission.can_receive
)
)
.createDomain(bankDomain, userRole)
// create user A
.createAccount("user_a", bankDomain, useraKeypair.getPublic())
// create user B
.createAccount("user_b", bankDomain, userbKeypair.getPublic())
// create usd#bank with precision 2
.createAsset(usdName, bankDomain, 2)
// transactions in genesis block can be unsigned
.build() // returns ipj model Transaction
.build() // returns unsigned protobuf Transaction
)
// we want to increase user_a balance by 100 usd
.addTransaction(
Transaction.builder(user("user_a"))
.addAssetQuantity(usd, new BigDecimal("100"))
.build()
.build()
)
.build();
}
public static PeerConfig getPeerConfig() {
PeerConfig config = PeerConfig.builder()
.genesisBlock(getGenesisBlock())
.build();
// don't forget to add peer keypair to config
config.withPeerKeyPair(peerKeypair);
return config;
}
/**
* Custom facade over GRPC Query
*/
public static int getBalance(IrohaAPI api, String userId, KeyPair keyPair) {
// build protobuf query, sign it
val q = Query.builder(userId, 1)
.getAccountAssets(userId)
.buildSigned(keyPair);
// execute query, get response
val res = api.query(q);
// get list of assets from our response
val assets = res.getAccountAssetsResponse().getAccountAssetsList();
// find usd asset
val assetUsdOptional = assets
.stream()
.filter(a -> a.getAssetId().equals(usd))
.findFirst();
// numbers are small, so we use int here for simplicity
return assetUsdOptional
.map(a -> Integer.parseInt(a.getBalance()))
.orElse(0);
}
public static void main(String[] args) {
// for simplicity, we will create Iroha peer in place
IrohaContainer iroha = new IrohaContainer()
.withPeerConfig(getPeerConfig());
// start the peer. blocking call
iroha.start();
// create API wrapper
IrohaAPI api = new IrohaAPI(iroha.getToriiAddress());
// transfer 100 usd from user_a to user_b
val tx = Transaction.builder("user_a@bank")
.transferAsset("user_a@bank", "user_b@bank", usd, "For pizza", "10")
.sign(useraKeypair)
.build();
// create transaction observer
// here you can specify any kind of handlers on transaction statuses
val observer = TransactionStatusObserver.builder()
// executed when stateless or stateful validation is failed
.onTransactionFailed(t -> System.out.println(String.format(
"transaction %s failed with msg: %s",
t.getTxHash(),
t.getErrOrCmdName()
)))
// executed when got any exception in handlers or grpc
.onError(e -> System.out.println("Failed with exception: " + e))
// executed when we receive "committed" status
.onTransactionCommitted((t) -> System.out.println("Committed :)"))
// executed when transfer is complete (failed or succeed) and observable is closed
.onComplete(() -> System.out.println("Complete"))
.build();
// blocking send.
// use .subscribe() for async sending
api.transaction(tx)
.blockingSubscribe(observer);
/// now lets query balances
val balanceUserA = getBalance(api, user("user_a"), useraKeypair);
val balanceUserB = getBalance(api, user("user_b"), userbKeypair);
// ensure we got correct balances
assert balanceUserA == 90;
assert balanceUserB == 10;
}
}
とりあえず、動かしてみます。
[main] INFO org.testcontainers.dockerclient.DockerClientProviderStrategy - Will use 'okhttp' transport
[main] INFO org.testcontainers.dockerclient.DockerClientProviderStrategy - Will use 'okhttp' transport
[main] INFO org.testcontainers.dockerclient.DockerMachineClientProviderStrategy - Found docker-machine, and will use machine named default
[main] INFO org.testcontainers.dockerclient.DockerMachineClientProviderStrategy - Docker daemon IP address for docker machine default is 192.168.99.100
[main] INFO org.testcontainers.dockerclient.DockerClientProviderStrategy - Will use 'okhttp' transport
[main] ERROR org.testcontainers.dockerclient.DockerClientProviderStrategy - Could not find a valid Docker environment. Please check configuration. Attempted configurations were:
[main] ERROR org.testcontainers.dockerclient.DockerClientProviderStrategy - NpipeSocketClientProviderStrategy: failed with exception InvalidConfigurationException (ping failed). Root cause TimeoutException (null)
[main] ERROR org.testcontainers.dockerclient.DockerClientProviderStrategy - WindowsClientProviderStrategy: failed with exception TimeoutException (Timeout waiting for result with exception). Root cause ConnectException (Connection refused: connect)
[main] ERROR org.testcontainers.dockerclient.DockerClientProviderStrategy - DockerMachineClientProviderStrategy: failed with exception TimeoutException (Timeout waiting for result with exception). Root cause ConnectException (Connection refused: connect)
[main] ERROR org.testcontainers.dockerclient.DockerClientProviderStrategy - As no valid configuration was found, execution cannot continue
Exception in thread "main" java.lang.IllegalStateException: Could not find a valid Docker environment. Please see logs and check configuration
はい。エラーがでました。
「Could not find a valid Docker environment.」
このエラーですが、どうもJavaからDockerを操作しようとして「Dockerfile」が存在しない場合にこのエラーが出るみたいです。
私はDockerではなく、VirtualBoxで環境を作っているので動く訳がありませんでした。
ここでプログラムをちゃんと読んでみると、Peerを追加して、追加したPeerにアカウントを作成し、Asset(トークン)の送信を行っています。
main文の処理からDockerを操作する部分を消し、ユーザーもデフォルトで作成されている、「admin@test」と「test@test」に変更して動かしてみます。
public static void main(String[] args) throws Exception{
URI uri = new URI(null,null, "IPアドレス",50051,null,null,null);
// create API wrapper
IrohaAPI api = new IrohaAPI(uri);
// transfer 100 usd from user_a to user_b
val tx = Transaction.builder("admin@test")
.transferAsset("admin@test", "test@test", usd, "For pizza", "10")
.sign(useraKeypair)
.build();
// create transaction observer
// here you can specify any kind of handlers on transaction statuses
val observer = TransactionStatusObserver.builder()
// executed when stateless or stateful validation is failed
.onTransactionFailed(t -> System.out.println(String.format(
"transaction %s failed with msg: %s",
t.getTxHash(),
t.getErrOrCmdName()
)))
// executed when got any exception in handlers or grpc
.onError(e -> System.out.println("Failed with exception: " + e))
// executed when we receive "committed" status
.onTransactionCommitted((t) -> System.out.println("Committed :)"))
// executed when transfer is complete (failed or succeed) and observable is closed
.onComplete(() -> System.out.println("Complete"))
.build();
// blocking send.
// use .subscribe() for async sending
api.transaction(tx)
.blockingSubscribe(observer);
/// now lets query balances
val balanceUserA = getBalance(api, user("user_a"), useraKeypair);
val balanceUserB = getBalance(api, user("user_b"), userbKeypair);
// ensure we got correct balances
assert balanceUserA == 90;
assert balanceUserB == 10;
}
修正したポイントとしては「IrohaApi」クラスのコンストラクタです。
このコンストラクタはURIクラスを受け取りますが、コンストラクタの中で受け取ったURIクラスからドメインとポートを取得しているだけです。
なので、ドメインとポートだけを設定したURIクラスを作成し、引数に設定します。
ここまで出来たら実行します。
transaction c3d966d1cbc521b99e1cb60cbe444f129947bdfbe348abe7b7262dd81079c505 failed with msg: signatures validation
Complete
エラーは出てますが、とりあえず動きました。
このエラーの原因はトランザクション署名に使用したKeyPairがadmin@testのものではないことが原因です。
原因は分かっていますが、まだ対応方法を調べている最中なので、分かり次第更新します。
2019/10/09 追記
Transaction送信の方法が分かったので追記します。
public static void main(String[] args) throws Exception{
URI uri = new URI(null,null, "192.168.33.10",50051,null,null,null);
// create API wrapper
IrohaAPI api = new IrohaAPI(uri);
byte[] pubByte = Hex.decodeHex("admin@test.pubの中身(公開鍵)");
byte[] privByte = Hex.decodeHex("test@test.privの中身(秘密鍵)");
KeyPair adminKeyPair = Ed25519Sha3.keyPairFromBytes(privByte, pubByte);
// transfer 100 usd from user_a to user_b
val tx = Transaction.builder("admin@test")
.transferAsset("admin@test", "test@test", usd, "For pizza", "10")
.sign(adminKeyPair)
.build();
// create transaction observer
// here you can specify any kind of handlers on transaction statuses
val observer = TransactionStatusObserver.builder()
// executed when stateless or stateful validation is failed
.onTransactionFailed(t -> System.out.println(String.format(
"transaction %s failed with msg: %s",
t.getTxHash(),
t.getErrOrCmdName()
)))
// executed when got any exception in handlers or grpc
.onError(e -> System.out.println("Failed with exception: " + e))
// executed when we receive "committed" status
.onTransactionCommitted((t) -> System.out.println("Committed :)"))
// executed when transfer is complete (failed or succeed) and observable is closed
.onComplete(() -> System.out.println("Complete"))
.build();
// blocking send.
// use .subscribe() for async sending
api.transaction(tx)
.blockingSubscribe(observer);
/// now lets query balances
val balanceUserA = getBalance(api, user("user_a"), useraKeypair);
val balanceUserB = getBalance(api, user("user_b"), userbKeypair);
// ensure we got correct balances
assert balanceUserA == 90;
assert balanceUserB == 10;
}
ポイントとしては公開鍵と秘密鍵からKeyPairのオブジェクトを作成します。
byte[] pubByte = Hex.decodeHex("admin@test.pubの中身(公開鍵)");
byte[] privByte = Hex.decodeHex("test@test.privの中身(秘密鍵)");
KeyPair adminKeyPair = Ed25519Sha3.keyPairFromBytes(privByte, pubByte);
これをsignメソッドの引数に設定すれば完了です。
実行してみます。
Committed :)
Complete
正常に終了しました。
それではブロックの中身を見てみます。
公式ドキュメントの手順で環境構築をすると「/tmp/block_store」ディレクトリに出力されているはずです。
中身は整形したJSONです。
{
"blockV1": {
"payload": {
"transactions": [
{
"payload": {
"reducedPayload": {
"commands": [
{
"transferAsset": {
"srcAccountId": "admin@test",
"destAccountId": "test@test",
"assetId": "usd#bank",
"description": "For pizza",
"amount": "10"
}
}
],
"creatorAccountId": "admin@test",
"createdTime": "1570624170543",
"quorum": 1
}
},
"signatures": [
{
"publicKey": "313A07E6384776ED95447710D15E59148473CCFC052A681317A72A69F2A49910",
"signature": "4B2B45A4F2FDB9A7DE6F30E110D8DEA5E5AAB30C40F5685CFA71FDC38E72BF3839954DDA13FE027FEA18DA9F97332E5E265822922204D38F1667D60E5F8E9601"
}
]
}
],
"height": "17",
"prevBlockHash": "ff33c8483725758e09583e7f670b9c37a091357bc027602a72452d44907861f1",
"createdTime": "1570624184188"
},
"signatures": [
{
"publicKey": "bddd58404d1315e0eb27902c5d7c8eb0602c16238f005773df406bc191308929",
"signature": "680ee97a530314e877ee7518b22975607e328a232a60f963029cb07b18b9101d0de6bf174ecd07ab29bac8123e8c47f12761835a121b301f192c0de9a908670e"
}
]
}
}
無事ブロックに取り込まれたことを確認できました。
#感想
最近ethereumを勉強していたので、マイニングをしなくてもブロックに取り込まれるところが、うん?ってなりましたが、考えてみたら当たり前ですね。
今回のサンプルプログラムを動かすにしても、やはり日本語の情報がとても少ないという印象を受けました。
簡単なウォレットアプリの作成やチェーンコードのデプロイなどやっていこうと思うので積極的に情報を残していければなと思います。