GoQuorumで運用環境を作る

# はじめに

GoQuorumで複数ノードを起動します。
コンソーシアムで利用する場合でも、冗長化のために自社用に複数ノードを立ち上げたいということもあると思います。（この使い方に需要があるのかわかりませんが、開発や試験用の環境作成よりも深い内容になっているのでスクラッチで環境を作成される際の参考になるのではないかと思います。）

:::note warn
ここでは、ネットワークのセキュリティやOSのセキュリティ設定等は扱わないので、別途考える必要があります。
ここで紹介している方法は、公式が推奨している方法というわけではなく、試行錯誤した結果ですのでご注意ください。
:::

コンセンサスアルゴリズムには、raftを使用します。プライベートトランザクションは使用しません。
特に記載がない場合は、OSには、Ubuntu 20.04.2 LTSを使用しています。

:::note warn
この記事を書いた後、コンセンサスアルゴリズムに、raftを使用しているとタイムスタンプの単位がmsになってweb3.jsやethereum.jsでは書き込み時にエラーになることがわかりました。
（現在は改善されているかもしれません。）
コンセンサスアルゴリズムをIBFTに変更するとエラーが解消されました。
IBFTでの構築方法については、また別の記事に書きたいと思います。
:::

# 準備
まず、ノードを作成する前に必要な準備を行います。
## GoQuorumをソースからビルド
実行環境に必要な、gethとbootnodeを作成します。gethは、公式のバイナリとして配布されているのですが、bootnodeが含まれていないため開発環境でソースからビルドします。
GoQuorum v21.4.0は、大幅な変更があったようなので情報が少ないためv21.1.0を使います。（2021年4月23日現在）
(公式のDockerの最新イメージもv21.1.0を使用していました。） 

:::note warn
注意
2021年4月に書いたので内容が古い可能性があります。

:::

開発環境で、ビルドを行います。
ビルドには、GO言語が必要なのでインストールします。
GoQuorum v21.1.0は、Go言語の最新ではビルドできなかったので、go 1.13.15を使用します。

以下のサイトに従ってGo言語のインストールを行います。
https://golang.org/doc/install

``` bash:開発環境
wget https://golang.org/dl/go1.13.15.linux-amd64.tar.gz
sudo tar -C /usr/local -xzf go1.13.15.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin

インストールできているか確認します。
go version
go version go1.13.15 linux/amd64
```
GoQuorumのビルドを行います。
quorum/build/bin/にgethとbootnodeが作成されます。これを使用します。

``` bash:開発環境
quorumのビルドにmakeを使用するので、開発環境をインストールしていない場合はインストールします。
sudo apt-get update
sudo apt-get install build-essential

git clone https://github.com/Consensys/quorum.git -b v21.1.0 --depth 1 
cd quorum
make all
```

genesis.jsonは、以下の公式サンプルを参考にしました。
https://github.com/ConsenSys/quorum-examples/blob/master/examples/7nodes/genesis.json

サンプル等ですとallocでアカウントにetherを与えているのですが、ここでは空にしています。スマートコンントラクトを実行するだけならquorumではgas価格が0に設定されているためetherは必要ありません。quorumでetherをやり取りするケースは少ないと思うので空にしていますが必要なら割り当ててください。

## genesis.jsonの作成
```json:genesis.json
{
  "alloc": {
   },
   "coinbase": "0x0000000000000000000000000000000000000000",
   "config": {
     "homesteadBlock": 0,
     "byzantiumBlock": 0,
     "constantinopleBlock": 0,
     "petersburgBlock": 0,
     "istanbulBlock": 0,
     "chainId": 1337,
     "eip150Block": 0,
     "eip155Block": 0,
     "eip150Hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
     "eip158Block": 0,
     "qip714Block": 20,
     "isQuorum": true,
     "maxCodeSizeConfig" : [
       {
         "block" : 0,
         "size" : 64
       }
     ]
   },
   "difficulty": "0x0",
   "extraData": "0x0000000000000000000000000000000000000000000000000000000000000000",
   "gasLimit": "0xE0000000",
   "mixhash": "0x00000000000000000000000000000000000000647572616c65787365646c6578",
   "nonce": "0x0",
   "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
   "timestamp": "0x00"
 }
```

# GoQuorumノードの作成
次に、GoQuorumノードを作成します。

## ノードキーの作成
GoQuorumノード用のマシンに移動してノードキーを作成します。
ノードを複数作成する場合は、それぞれのマシンで下記の操作を行います。

``` bash:GoQuorumノード
先ほど開発環境で作成したgethとbootnodeを/usr/local/binにコピーします。

GoQuorum用のフォルダを作成します。フォルダ構成は、公式のDockerイメージを参考に作成してきます。
sudo mkdir -p /qdata/dd/geth

ノードの秘密鍵を作成します。
bootnode --genkey=nodekey

ノードアドレスを出力します。ノードアドレスは、後で設定ファイルを作成する際に使用します。
bootnode --nodekey=nodekey --writeaddress

sudo mv nodekey /qdata/dd/geth
```
## static-nodes.jsonの作成
ノードを発見できるようにノードアドレスとIPアドレスをstatic-node.jsonに記載します。
ノードアドレス,IPアドレスは、環境に合わせて変更してください。
ここでは、3ノード分記述しています。

```json:static-nodes.json
[
  "enode://ここにノード１のアドレスを記載@192.168.0.1:21000?discport=0&raftport=50400",
  "enode://ここにノード２のアドレスを記載@192.168.0.2:21000?discport=0&raftport=50400",
  "enode://ここにノード３のアドレスを記載@192.168.0.3:21000?discport=0&raftport=50400"
]
```

## permissioned-nodes.jsonの作成
通信を許可するノードをpermissioned-nodes.jsonに定義します。
内容は、static-node.jsonと同じでいいと思います。static-nodes.jsonをコピーしてpermissioned-nodes.jsonにリネームするのが確実です。

```json:permissioned-nodes.json
[
  "enode://ここにノード１のアドレスを記載@192.168.0.1:21000?discport=0&raftport=50400",
  "enode://ここにノード２のアドレスを記載@192.168.0.2:21000?discport=0&raftport=50400",
  "enode://ここにノード３のアドレスを記載@192.168.0.3:21000?discport=0&raftport=50400"
]
```

作成したファイルをコピーします。

``` bash:GoQuorumノード
/qdata/ddに上記で作成した
static-nodes.json
permissioned-nodes.json
をコピーします。

/qdataに上記で作成した
genesis.json
をコピーします。
```

## ノードにアカウントを作成
次に、どれか１つノード（以降、マスターノードと呼びます。）に管理用のアカウントを作成します。スマートコントラクトをデプロイする際等にノードにアカウントが必要です。また、スマートコントラクトをデプロイしたアカウントは、オーナーとして特別な権限を与えられるので、このノードを外部からアクセスできないようにrpcサーバを起動せずに、ipcでアクセスするように設定しています。

``` bash:GoQuorumノード
geth --datadir /qdata/dd account new
```

## ノードのフォルダ構成
基本的なフォルダ構成は、以下のようになります。

``` bash:GoQuorumノード
/qdata/
| genesis.json
├ dd/
 | static-nodes.json
 | permissioned-nodes.json
 ├ geth/
   | nodekey
 ├ keystore/ <-ここに作成したアカウントのKeySotre/JSONファイルが作成されます。

``` 

## ノードを初期化
ノードを初期化します。これで準備が整いました。

``` bash:GoQuorumノード
cd /qdata
sudo geth --datadir /qdata/dd init genesis.json
```

# サービス化
次に、運用で使用するため、gethをsystemdのサービスとして起動します。
以下のページを参考しにしました。

https://gist.github.com/swaldman/e58a866eafc4ff043c4099e394901a1e

gethのサービス実行ユーザと権限の設定を行います。

``` bash:GoQuorumノード
サービス実行ユーザの作成
sudo groupadd geth --system
sudo adduser geth --group --system --no-create-home 

quorumフォルダの所有者とアクセス権を変更
sudo chown -hR geth:geth /qdata
sudo chmod -R o-wx /qdata
sudo chmod -R o-r /qdata/dd/geth
```
## マスターノード
マスターノードに、systemd用のファイルを以下のように作成します。
ポートやnetworkidは、デフォルトで使用されているものを指定していますが、環境に合わせて変更しても構いません。

``` bash:geth.service
[Unit]
Description=Quorum master node
After=syslog.target network.target

[Service]
Environment="PRIVATE_CONFIG=ignore"
User=geth
Group=geth
Type=simple
ExecStart=/usr/local/bin/geth --datadir /qdata/dd --nodiscover --verbosity 3 --networkid 1337 --permissioned --raft --raftport 50400 --emitcheckpoints --port 21000
KillMode=process
KillSignal=SIGINT
TimeoutStopSec=90
Restart=on-failure
RestartSec=10s

[Install]
WantedBy=multi-user.target
```
## マスタノード以外
マスタノード以外では、systemd用のファイルを以下のように作成します。
rpcでアクセスできるようにし、ipcは無効にしています。

ログのレベルを指定します。
--verbosity 3

RPCで公開する機能を指定します。サンプルだと使用できる全てが記載されているのですが、スマートコントラクトを実行するには、以下の項目で十分だと思います。必要であれば追加してください。
--rpcapi eth,net,web3        

``` bash:geth.service
[Unit]
Description=Quorum node
After=syslog.target network.target

[Service]
Environment="PRIVATE_CONFIG=ignore"
User=geth
Group=geth
Type=simple
ExecStart=/usr/local/bin/geth --datadir /qdata/dd --nodiscover --verbosity 3 --networkid 1337 --permissioned --raft --raftport 50400 --rpc --rpcaddr 0.0.0.0 --rpcport 8545 --rpcapi eth,net,web3 --ipcdisable --emitcheckpoints --port 21000

KillMode=process
KillSignal=SIGINT
TimeoutStopSec=90
Restart=on-failure
RestartSec=10s

[Install]
WantedBy=multi-user.target
```
## ノードの起動
systemdを使用して、gethを起動します。
全てのマシンでノードを起動します。

``` bash:GoQuorumノード
/usr/lib/systemd/system/にgeth.serviceをコピーします。

有効化します。
systemctl enable geth

起動します。
systemctl start geth
```

## 起動の確認
次に、正常に起動したか確認します。

``` bash:GoQuorumノード
サービスのステータスを確認
systemctl status geth

サービスのログを確認
sudo journalctl -u geth.service
```

# 動作確認
ここからは、サンプルのスマートコントラクトをデプロイして動作確認を行います。
以下の手順を行うとテスト用のスマートコントラクトがデプロイされてしまいますのでご注意ください。

##スマートコントラクトのデプロイ
スマートコントラクトをデプロイするスクリプトのサンプルです。
公式のDocker Sampleにあるpublic-contruict.jsにアカウントをアンロック、ロックするコードを加えました。
このコードでは、コントラクトをデプロイする際に、gasが0x47b760に設定されています。このコードを見るまで、gasは設定していなくて、デプロイ時にgasのエラーが出てハマりました。ganacheにデプロイした際はエラーは出ないので、この環境を作るまではgasは必要ないと勘違いしていました。gas価格は0に設定されているのですが、ブロックに書き込みが発生するトランザクションではgas自体は必要です。（価格が0なのでアカウントがetherを持っていなくても大丈夫です。）特にデプロイ時は、デフォルトのgas設定では足りないためエラーが発生していたようです。

``` JavaScript:deploy-contract.js
web3.eth.defaultAccount = eth.accounts[0];

//truffleでコンパイルした内容に置き換えます
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"}],"payable":false,"type":"constructor"}];

//truffleでコンパイルした内容に置き換えます
var bytecode = "0x6060604052341561000f57600080fd5b604051602080610149833981016040528080519060200190919050505b806000819055505b505b610104806100456000396000f30060606040526000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680632a1afcd914605157806360fe47b11460775780636d4ce63c146097575b600080fd5b3415605b57600080fd5b606160bd565b6040518082815260200191505060405180910390f35b3415608157600080fd5b6095600480803590602001909190505060c3565b005b341560a157600080fd5b60a760ce565b6040518082815260200191505060405180910390f35b60005481565b806000819055505b50565b6000805490505b905600a165627a7a72305820d5851baab720bba574474de3d09dbeaabc674a15f4dd93b974908476542c23f00029";

var simpleContract = web3.eth.contract(abi);

personal.unlockAccount(eth.accounts[0]);

var simple = simpleContract.new(42, {from:web3.eth.accounts[0], data: bytecode, gas: 0x47b760}, function(e, contract) {
	if (e) {
		console.log("err creating contract", e);
		personal.lockAccount(eth.accounts[0]);
	} else {
		if (!contract.address) {
			console.log("Contract transaction send: TransactionHash: " + contract.transactionHash + " waiting to be mined...");
		} else {
			console.log("Contract mined! Address: " + contract.address);
			console.log(contract);
			personal.lockAccount(eth.accounts[0]);
		}
	}
});
```

マスターノードでスマートコントラクトのデプロイを行います。

``` bash:GoQuorumノード
deploy-contract.jsをコピーします。

コンソールに接続
sudo geth attach /qdata/dd/geth.ipc

スマートコントラクトをデプロイ。パスはフルパスで指定(パスは、環境に合わせて変更)
loadScript('/home/xxx/deploy-contract.js')
Unlock account 0xb1ce8eaa2...
Password: 
Contract transaction send: TransactionHash: 0x41c4e0f73065e202bac65ac... waiting to be mined...
> Contract mined! Address: 0x9e0b893c...
[object Object]

デプロイされたコントラクトのアドレスが表示されるのでファイル等に記録します。

```
##アカウントの作成
次に開発環境でアカウントを作成します。アカウントを作成するスクリプトを実行するには、node.jsとethersが必要になります。

``` bash:開発環境
node.jsをインストールします。
sudo apt install nodejs
sudo apt install npm

ethers.jsをローカルにインストールします。
npm install --save ethers
export PATH=$PATH:./node_modules/.bin

```

アカウント（ウォレット）は、MetaMaskで作成している例がほとんどなので、ここでは、ethers.jsで作成してみます。
暗号資産を扱うわけではないので、MetaMaskを直接使わせたくないということもあると思います。
ランダムに生成したニーモニックコードからアカウントを作成するサンプルです。
以下のスクリプトを修正して、実際のクライアントアプリケーションで必要な形式で保存します。

``` JavaScript:create_account.js
const { ethers } = require("ethers");
wallet = ethers.Wallet.createRandom();

//ニーモニックコード
console.log(wallet.mnemonic);
//アカウントのアドレス
console.log(wallet.address);
//アカウントの秘密鍵
console.log(wallet.privateKey);
//アカウントの公開鍵
console.log(wallet.publicKey);

//KeyStore/JOSN
wallet.encrypt("password")
.then(function(result) {
    console.log(result);
  });

```

## スマートコントラクトの動作確認
ethersでスマートコントラクトを呼び出すサンプルです。
この例では、192.168.0.2のノードに要求を出しています。

``` JavaScript:test.js
const { ethers } = require("ethers");
const fs = require('fs');

const provider = new ethers.providers.JsonRpcProvider('http://192.168.0.2:8545');

//Keystore/JSONをファイルを読み込みます
const keystore = fs.readFileSync('./keystore.json');

//Keystore/JSONからウォレットを作成します
wallet = ethers.Wallet.fromEncryptedJsonSync(keystore, "password");

const signer = wallet.connect(provider);

//スマートコントラクトのABI
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"}],"payable":false,"type":"constructor"}];

//スマートコントラクトのアドレス
address = "0x3d5EB95a1eA94182Ca8Bc2FfA85736F3Ff19Bbd8";

//スマートコントラクトのインスタンスを取得
const inst = new ethers.Contract(address, abi, signer);

async function init() {
    //スマートコントラクトのメソッドで値を書き込み
    await inst.set(46).catch((err) => console.log(err));

    try {
        //スマートコントラクトのメソッドで値を書き込んだ値を読み出し
        ret = await inst.get();
        console.log(ret);
    } catch (err) { 
        console.log(err);
    }
}
init()

```

# リバースプロキシ
ノードが持っているrpcサーバは、http接続しかできず認証機能もありません。クライアントがインターネット経由で接続する場合、httpsを使用してリバースプロキシで認証する方法を採用していることが多いようです。
GoQuorum公式の拡張機能を使用すれば、ノードでhttps接続と認証が行えるようですが、設定の方法がわかりませんでした。

https://github.com/ConsenSys/quorum-security-plugin-enterprise/tree/master/examples

# さいごに
試行錯誤の結果をまとめてみました。結構手こずりましたが、まとめてみるとそれほど複雑でもないですね。もし誤り等がありましたらご連絡いただけるとうれしいです。