Quorumのサンプル「7nodes」を試す。
前回までどこぞの小宇宙に寄り道していましたが、1週周ってEthereum系に帰ってきました。 実はこれから「Kaleido」(ConsenSys社がAWS上で提供しているBaaS)でQuorumを触ろうと考えているのですが、その前に素のQuorumを触っておこうという趣旨です。 Quorum+Raftコンセンサスアルゴリズムの特徴に惹かれたのがきっかけです。
- Ethereum互換
- プライベートチェーン。許可ノードのみでクローズネットワークを構成する。(ビザンチン障害耐性は考慮しない)
- ノードをグループ化(セグメンテーション)して、指定ノードのみにBC内容を公開する制限が可能
- ブロック生成タイミングがトランザクション発生時のみ
- リーダーノードがブロックを生成して伝搬
- リーダーが倒れたら他のノードからリーダーを選出
- ブロック生成速度が高速(50ms?)
- ファイナリティあり
- ブロックがフォークしない
- gas代がかからない?
クローズドの仕組みを作りたい場合は魅力的な特徴ですね。
Quorumのサンプルは「7nodes」と名付けられていて、その名の通り7つのQuorumノードを起動させてノード間の動きを確認するような内容になっています。 「7nodesやってみた」系の記事はQiitaを中心にネット上にまあまあありますが、公式サイトの日本語訳+ちょっとだけ補足的な内容にとどまっているようですので、本記事では付加情報も書いていきたいと思います。
それでは始めましょう。
環境
- Windows10
- git for Windows 2.27.0
- VirtualBox 6.1.4
- Vagrant 2.2.7
Quorum自体の動作環境はVirtualBox上のUbuntu 16.04.6 LTSになります。
あと、あまり関係ないですがターミナルはWindows Terminalを使っています。
準備
公式サイトはこちら。 github.com
7nodesサンプルファイルの取得、仮想マシンの起動
基本的に公式サイトの記載通りで問題ないかと思います。
gitリポジトリからサンプルをクローン
git clone https://github.com/jpmorganchase/quorum-examples.git
作成されたquorum-examples
フォルダへ移動して仮想マシンの初期化と起動
vagrant up
かなり時間がかかるので気長に待ちましょう。途中で止めるとフォルダごと消して最初からやり直しです。
vagrant ssh
Raftでの初期化、Quorumノードの起動
sshでログインすると、ホームディレクトリ直下に3つのフォルダと1つのファイルが既に存在しています。
tessera
ディレクトリ、cakeshop
ディレクトリには1~2個のjarファイル、warファイルしか格納されていません。
quorum-examples/7nodes
ディレクトリに移動します。
cd quorum-examples/7nodes
今回はコンセンサスアルゴリズムにRaftを使用するので、Raft用のスクリプトで初期化します。
./raft-init.sh
USBデバイス関連のエラーが出るかもしれませんが、無視しても動きます。
あとは、Quorumノードを起動するだけです。起動には少し時間がかかります。
./raft-start.sh
メモ:raft-init.shが実施している内容
手順だけだと理解が深まらないので、初期化スクリプト、起動スクリプトを覗いてみましょう。
まずはraft-init.sh
です。ざっと以下のようなことをしているようです。
create-permissioned-nodes.sh
の実行- 7つのノードの最初のブロックの初期化
tessera-init.sh
の実行cakeshop-init.sh
の実行
1.はpermissioned-nodes-${nodes}.json
ファイルに色々書き込んでいるようです。(大雑把)
2.はQuorumノード単位で
geth --datadir qdata/dd${i} init genesis.json
が実行されています。
${i}
はノード番号で1~7です。
genesis.json
はカレントディレクトリに用意されているethereumでお馴染みのジェネシスブロック用ファイルです。内容で特徴的な箇所は以下のあたりでしょうか。
"chainId": 10, "difficulty": "0x0", "isQuorum": true, "gasLimit": "0xE0000000", "alloc": { "0xed9d02e382b34818e88b88a309c7fe71e65f419d": { "balance": "1000000000000000000000000000" }, ... },
- chainIdはEthereumと同様ですね。7つのノードすべてのchainIdが10に設定されます。
- difficultyはマイニング時のハッシュ計算の難易度ですが、0に設定されています。
- isQuorum はQuorum独自のパラメータでしょうかね。
- gasLimitには大きな数値が設定されていますが、この値はなんでもいいのでしょうか(未調査)。ちなみに、後述するコントラクトデプロイ時のgas代は0になってました。
- allocは特定のアカウントにあらかじめethトークンを割り当てる設定です。大きな数値が設定されています。 アカウントは実際にノードに存在するものですが、どこで生成されているのか詳細を確認できていません。 あらかじめ生成されているように思われます。
3.はTesseraノードの初期化です。
QuorumにはTesseraと呼ばれるPrivacy Managerがあります。
TesseraはJavaで実装されていて、Jarファイルが前述のホームディレクトリ直下、~/tessera
ディレクトリに格納されています。
スクリプト内では、Tessera用のConfigファイルを元にjavaコマンドでkeyが生成されています。
java -jar $tesseraJar -configfile keygenconfig.json -keygen -filename tm < /dev/null
Quorumノードと対になっているようで、Tesseraノードも7つのノードとなっています。
4.は別のサンプル「cakeshop」関連のスクリプトのようです。
メモ:raft-start.shが実施している内容
- Tesseraノードの起動
- Quorumノードの起動
1.は同ディレクトリにあるtessera-start.sh
が実行され、そのなかでJavaコマンドによりTesseraノードが起動されています。
java $jvmParams $DEBUG $MEMORY -jar ${tesseraJar} -configfile $DDIR/tessera-config$TESSERA_CONFIG_TYPE$i.json
Tesseraノードは前述の通り、7つあるようです。
なお、上述のraft-start.sh
実行時は引数が省略されていますが、規定値としてPrivacy ManagerにTesseraが使用される設定になっています。
2.は、Tesseraノード起動の後に、gethコマンドにより7つのQuorumノードが起動されています。
geth --datadir qdata/dd${i} ${ARGS} ${permissioned} --raftport ${raftPort} --rpcport ${rpcPort} --port ${port} 2
メモ:7nodesのファイル、ディレクトリ構成(抜粋)
ここでサンプルに登場する7nodes配下のファイル類、ディレクトリをまとめておきましょう。
ファイル類
raft-init.sh
:Raftによる初期化処理のスクリプトtessera-init.sh
:Tesseraの初期化スクリプトcreate-permissioned-nodes.sh
:permissioned-nodes.jsonの設定?permissioned-nodes.json
:ネットワークへの参加が許可されているQuorumノードのリスト。各ノードのID(enode)、IP、ポート等の情報を保持raft-start.sh
:ノード起動スクリプトtessera-start.sh
: Tesseraノード起動スクリプトgenesis.json
:Quorumノードのジェネシスブロックの情報runscript.sh
:Javascript実行用スクリプト(後述)private-contract.js
:サンプルのコントラクトデプロイ用スクリプト(後述)simplestorage.sol
:サンプルのコントラクトのSolidityコード(後述)
permissioned-nodes.jsonには、7つのノード分のenodeと呼ばれるノード固有のIDが記載されていますが、
各ノードにおいてadmin.peers
コマンドで自ノード以外の6つのenodeが確認できます。
qdata
:ノード毎のデータ類の格納先keys
:ノード毎のキー情報の格納先。おそらくTesseraで生成、利用されている。
7nodesの動作確認
公式サイトはこちら。 github.com
プライベートコントラクトのデプロイ
以下のスクリプト実行によりコントラクトをノードにデプロイします。
./runscript.sh private-contract.js
実行するとトランザクションのハッシュ値が表示されます。この値は後で使います。
また、このとき、1番目のブロック(ジェネシスブロックの次のブロック)が生成されます。
eth.getBlock(1)
でトランザクションのハッシュ値が一致することを確認できます。
この時点でブロックはジェネシスブロック1つ、コントラクトデプロイのトランザクション分1つ、計2つのブロックの状態になっています。
ちなみにデプロイ時のgasUsedは0になっています。
メモ:runscript.shの内容
ノード1にてgethコマンドで引数のスクリプトprivate-contract.js
を実行しています。
geth --exec "loadScript(\"$1\")" attach ipc:qdata/dd1/geth.ipc
メモ:private-contract.jsの内容
- 使用するアカウントとして0番目のアカウントを指定
- abiとbytecodeを記載
- コントラクトをデプロイ
1.の0番目のアカウントは、0xed9d02e382b34818e88b88a309c7fe71e65f419d
でした。
このアカウントは初期化の際のgenesis.jsonのallocにてethトークンが割り当たっていたものです。ノード1上でコマンドeth.accounts
でも存在を確認できます。
なお、各ノードにはアカウントは1つしか存在していませんでした。
2.は、あらかじめsimplestorage.sol
がコンパイルされた結果のabiとbytecodeが記載されています。
simplestorage.sol
の内容は単純なもので、storedDataという値と、コンストラクタ、setter/getter関数を備えたコントラクトです。
3.はQuorumで特徴的な記載があります。
simpleContract.new(42, {from:web3.eth.accounts[0], data: bytecode, gas: 0x47b760, privateFor: ["ROAZBWtSacxXQrOe3FGAqJDyJjFePR5ce4TSIzmJ0Bc="]}
コントラクトをデプロイして、その際にstoredDataを42で初期化するという内容ですが、privateForというパラメータでこのコントラクトがどのノードから参照可能かを指定しています。 この場合、"ROAZBWtSacxXQrOe3FGAqJDyJjFePR5ce4TSIzmJ0Bc="というキー値のノード、すなわちノード7と、自ノード(ノード1)で参照可能ということです。 他のノードからはこのコントラクトの存在すら確認することはできません。
ちなみに、ノード7のキー値はkeys/tm7.pub
ファイルで確認できます。他のノードのキー値も同様に番号違いのファイルにて確認できます。
あとgasに値が入っているのですが、ここは詳細未調査です。(gas値は必要なの?)
コントラクトの動作確認
ノード1のipcファイルを指定してgethのコンソールを開きます。
geth attach ipc:qdata/dd1/geth.ipc
コントラクトをデプロイした際に表示されたトランザクションを確認してみます。
eth.getTransaction("0xd07344932b92dbeabd46aa288e0a11ada1c95c9e711cf7047476b5e82978a481")
確認できる値として特徴的なのは、v: "0x26"
でしょうかね。
QuorumのPrivate Transactionの場合はvの値が10進数で37か38(16進数で"0x25"か"0x26")とのこと。ethereumの場合この値は27か28になるとのことです。
ここから、ノード1、ノード4、ノード7でコントラクトが保持している値を確認していきます。 ノード1と同じ要領で、ノード4、ノード7でgethコンソールをそれぞれ別画面で開きます。
ノード4
vagrant ssh
cd quorum-examples/7nodes
geth attach ipc:qdata/dd4/geth.ipc
ノード7
vagrant ssh
cd quorum-examples/7nodes
geth attach ipc:qdata/dd7/geth.ipc
3つのコンソールで以下の同じ操作を実施します。
var address = "0x1932c48b2bf8102ba33b4a6b545c32236e342f34"; var abi = [{"constant":true,"inputs":[],"name":"storedData","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"x","type":"uint256"}],"name":"set","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"get","outputs":[{"name":"retVal","type":"uint256"}],"payable":false,"type":"function"},{"inputs":[{"name":"initVal","type":"uint256"}],"type":"constructor"}]; var private = eth.contract(abi).at(address) private.get()
結果、ノード1とノード7では初期値の42、ノード4では0という結果が返ってきます。 これは、デプロイしたコントラクトの参照権限がノード1とノード7にしかないためです。
ところで、addressに指定している値(コントラクトのアドレス)ですが、ノード1のログから取得しました。
cat qdata/logs/1.log
Submitted contract creation
のログのto=以下がaddressになります。
次に、ノード1上でコントラクトの状態を変更してみます。
private.set(4,{from:eth.accounts[0],privateFor:["ROAZBWtSacxXQrOe3FGAqJDyJjFePR5ce4TSIzmJ0Bc="]});
初期値42だったstoredDataをset関数で4へ変更しています。またしてもprivateForでノード7を指定しています。実行結果としてはトランザクションのハッシュ値が表示されます。 この時点で2番目のブロックが生成され、合計3つのブロックとなります。ちなみにこの時のトランザクションでもgasUsed: 0です。
再度、ノード1、4、7のコンソールでコントラクトの状態を確認してみます。
private.get()
結果、ノード1とノード7では4、ノード4では0という結果が返ってきます。うまく動いてますね。
この後、公式サイトでは他のサンプルの説明や、「Cakeshop」という7nodesの状態を確認するダッシュボード画面の説明があります。本記事では公式サイトの紹介だけ。。。 docs.goquorum.com github.com
今回はここまで。次回は多分Kaleido+Quorum(+Truffle?)です。