Hyperledger Besu を動かしてみる(IBFT 2.0編)
はじめに
今回はコンセンサス・アルゴリズムとして IBPF 2.0 を使って Besu を動かしてみたい。前回は何も考えず PoW で動かしていたが、コンセンサスアルゴリズムが変わると Besuの設定や動作がどうなるのか気になるところである。
なお、だんだん Hyperledger Besu と書くのがめんどくさくなったので、Besu と書きます。
IBFT 2.0 とは
Istanbul Byzantine Fault Tolerance (イスタンブール・ビザンチン・フォルト・トレランス)の略で、PoA(プルーフ・オブ・オーソリティ)コンセンサスアルゴリズムの一種らしい。なぜイスタンブールなのかは不明。ビザンチン帝国ならコンスタンチノープルの方がいい気がするが、そういう話ではないのだろう。
IBFT 2.0の特徴としては、ビザンチン耐性が高いため、競合関係にある他社や、不正アクセスの危険があるノードを含むような信頼のできないネットワークに向いているとのこと。完全なプライベートチェーンよりも、複数の企業によるコンソーシアム型のチェーンに向いているのかもしれない。
参考サイト
IBFT 2.0
バリデータ
IBFT2.0 ネットワークでは、バリデータと呼ばれる承認済アカウントが取引とブロックを検証する。バリデーターは交代で次のブロックを生成し、バリデータの大多数(66%以上)がブロックに署名する必要がある。
バリデーターはネットワークに常に2/3以上いなければならない。バリデータの2/3以上が参加しなくなると、新しいブロックが作成されなくなり、ネットワークが停止する。そうなると、回復にかなりの時間がかかる。
実際にはバリデータの最小数は4となる。逆に、最大数は決まっていないが、バリデータの数が増えるほど、パフォーマンスは劣化するので多ければ多いほどいいというわけではない。検証の結果として、バリデータが30まではパフォーマンスの劣化はなかったとのこと。そしてバリデータ以外のノードがパフォーマンスに影響することはない。
既存のバリデータは、バリデータの追加や削除の提案と投票を行う。提案の実施にはバリデータの過半数(50%以上)の賛成を必要とする。
Genesis ファイル
当然だが、IBFT 2.0を使うには IBFT 2.0用のGenesisファイルが必要である。
Genesis ファイルの例
以下はGenesisファイルの例であるが、詳細は上記の公式ドキュメントを参照のこと。
{
"config": {
"chainId": 1981,
"muirglacierblock": 0,
"ibft2": {
"blockperiodseconds": 2,
"epochlength": 30000,
"requesttimeoutseconds": 4,
"blockreward": "5000000000000000",
"miningbeneficiary": "0xfe3b557e8fb62b89f4916b721be55ceb828dbd73"
}
},
"nonce": "0x0",
"timestamp": "0x58ee40ba",
"extraData": "0xf83ea00000000000000000000000000000000000000000000000000000000000000000d594c2ab482b506de561668e07f04547232a72897daf808400000000c0",
"gasLimit": "0x1fffffffffffff",
"difficulty": "0x1",
"mixHash": "0x63746963616c2062797a616e74696e65206661756c7420746f6c6572616e6365",
"alloc": {}
}
プライベートネットワークの構築
今回教育用として IBFT 2.0を使ったプライベートネットワークを構築する。ビザンチン耐性のため4つのノードを立ち上げるが、その全てがバリデータとなる。
1. ディレクトリの作成
4つのノードそれぞれデータディレクトリが必要なので作成する。
$ mkdir IBFT-Network; cd IBFT-Network
$ mkdir -p Node-1/data
$ mkdir -p Node-2/data
$ mkdir -p Node-3/data
$ mkdir -p Node-4/data
$ tree IBFT-Network/
IBFT-Network/
├── Node-1
│ ├── data
├── Node-2
│ ├── data
├── Node-3
│ ├── data
└── Node-4
├── data
2. 設定ファイルの作成
設定ファイルには IBFTのGenesisファイルとノード数分のキーペアを定義する。
{
"genesis": {
"config": {
"chainId": 1337,
"muirglacierblock": 0,
"ibft2": {
"blockperiodseconds": 2,
"epochlength": 30000,
"requesttimeoutseconds": 4
}
},
"nonce": "0x0",
"timestamp": "0x58ee40ba",
"gasLimit": "0x47b760",
"difficulty": "0x1",
"mixHash": "0x63746963616c2062797a616e74696e65206661756c7420746f6c6572616e6365",
"coinbase": "0x0000000000000000000000000000000000000000",
"alloc": {
"fe3b557e8fb62b89f4916b721be55ceb828dbd73": {
"privateKey": "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63",
"comment": "private key and this comment are ignored. In a real chain, the private key should NOT be stored",
"balance": "0xad78ebc5ac6200000"
},
"627306090abaB3A6e1400e9345bC60c78a8BEf57": {
"privateKey": "c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3",
"comment": "private key and this comment are ignored. In a real chain, the private key should NOT be stored",
"balance": "90000000000000000000000"
},
"f17f52151EbEF6C7334FAD080c5704D77216b732": {
"privateKey": "ae6ae8e5ccbfb04590405997ee2d52d2b330726137b875053c36d94e974d162f",
"comment": "private key and this comment are ignored. In a real chain, the private key should NOT be stored",
"balance": "90000000000000000000000"
}
}
},
"blockchain": {
"nodes": {
"generate": true,
"count": 4
}
}
}
これを ibftConfigFile.json という名前で IBFT-Network ディレクトリに置く。
3. node keys とGenesisファイルの作成
$ ../besu-22.1.1/bin/besu operator generate-blockchain-config --config-file=ibftConfigFile.json --to=networkFiles --private-key-file-name=key
これで networkFiles というディレクトリの下に node keys とGenesisファイルが作成される。
$ tree .
.
├── Node-1
│ └── data
├── Node-2
│ └── data
├── Node-3
│ └── data
├── Node-4
│ └── data
├── ibftConfigFile.json
└── networkFiles
├── genesis.json
└── keys
├── 0x59b1276f7295f3dfa96d16e38b5385e233a461ab
│ ├── key
│ └── key.pub
├── 0xc470b991787b76269b39f9a8971c8c2e577e0c4f
│ ├── key
│ └── key.pub
├── 0xd7fac01a40d24ed463fcf2f67ee2ea708b34787b
│ ├── key
│ └── key.pub
└── 0xe93f819df50df397cff7986f74a7aba6644bf0ef
├── key
└── key.pub
4. genesis ファイルのコピー
作成された genesis.json ファイルを IBFT-Network ディレクトリにコピーする。
$ cp networkFiles/genesis.json .
5. node private keys のコピー
作成された node private keys を各ノードのディレクトリにコピーする。
$ cp 0x59b1276f7295f3dfa96d16e38b5385e233a461ab/* ../../Node-1/data/
$ cp 0xd7fac01a40d24ed463fcf2f67ee2ea708b34787b/* ../../Node-2/data/
$ cp 0xc470b991787b76269b39f9a8971c8c2e577e0c4f/* ../../Node-3/data/
$ cp 0xe93f819df50df397cff7986f74a7aba6644bf0ef/* ../../Node-4/data/
6. ブートノードとして最初のノードを起動
Node-1 ディレクトリに移動し、Node-1を起動する。
$ ../../besu-22.1.1/bin/besu --data-path=data --genesis-file=../genesis.json --rpc-http-enabled --rpc-http-api=ETH,NET,IBFT --host-allowlist="*" --rpc-http-cors-origins="all"
起動するとメッセージの最後の方で enode URL というものが表示されるのでコピーしておく
Enode URL enode://e1f0f86fce7ed83061095d5d6e359aacc7ffe0304467d0faaf17da9cc54b3ce1fd46919537dd7ebfd8b1ed8938bb1c899cbf7fe2f3a44019fc4cd1445040647d@127.0.0.1:30303
7. Node-2の起動
次のNode-2を起動させる。--bootnodes= の部分には Node-1の Enode URLを入れる。
$ ../../besu-22.1.1/bin/besu --data-path=data --genesis-file=../genesis.json --bootnodes=enode://e1f0f86fce7ed83061095d5d6e359aacc7ffe0304467d0faaf17da9cc54b3ce1fd46919537dd7ebfd8b1ed8938bb1c899cbf7fe2f3a44019fc4cd1445040647d@127.0.0.1:30303 --p2p-port=30304 --rpc-http-enabled --rpc-http-api=ETH,NET,IBFT --host-allowlist="*" --rpc-http-cors-origins="all" --rpc-http-port=8546
ところが、以下のエラーで止まってしまう。
Service engineJsonRpc failed to start
To display full help:
besu [COMMAND] --help
2022-03-09 15:50:40.480+09:00 | BesuCommand-Shutdown-Hook | ERROR | Besu | Failed to stop Besu
どうやら、Node-1で起動された2つの JSON-RPC-HTTP サービスのうちの一つのポート番号がバッティングしているようだ。
Node-1で起動されたサービス
JsonRpcHttpService | Starting JSON-RPC service on 127.0.0.1:8545
JsonRpcHttpService | Starting JSON-RPC service on 127.0.0.1:8550
Node-2で起動しようとするサービス
JsonRpcHttpService | Starting JSON-RPC service on 127.0.0.1:8546
JsonRpcHttpService | Starting JSON-RPC service on 127.0.0.1:8550
最初の方はオプションで変更してあるのでバッティングしないが、2つ目のポートが特に指定がないためバッティングしている。
けれども、指定の方法が公式ドキュメントに書いてなく、やり方が不明だった。色々調べた結果、以下のオプションを追加すればうまくいった。
--engine-rpc-http-port=8551
よくわからないが、RPCのAPIには通常のやつと Engine 版があり、このように指定する必要があるようだ。
$ ../../besu-22.1.1/bin/besu --data-path=data --genesis-file=../genesis.json --bootnodes=enode://e1f0f86fce7ed83061095d5d6e359aacc7ffe0304467d0faaf17da9cc54b3ce1fd46919537dd7ebfd8b1ed8938bb1c899cbf7fe2f3a44019fc4cd1445040647d@127.0.0.1:30303 --p2p-port=30304 --rpc-http-enabled --rpc-http-api=ETH,NET,IBFT --host-allowlist="*" --rpc-http-cors-origins="all" --rpc-http-port=8546 --engine-rpc-http-port=8551
これでNode-2も無事に起動した。
8. Node-3の起動
ポート番号以外、Node-2と同じ。
$ ../../besu-22.1.1/bin/besu --data-path=data --genesis-file=../genesis.json --bootnodes=enode://e1f0f86fce7ed83061095d5d6e359aacc7ffe0304467d0faaf17da9cc54b3ce1fd46919537dd7ebfd8b1ed8938bb1c899cbf7fe2f3a44019fc4cd1445040647d@127.0.0.1:30303 --p2p-port=30305 --rpc-http-enabled --rpc-http-api=ETH,NET,IBFT --host-allowlist="*" --rpc-http-cors-origins="all" --rpc-http-port=8547 --engine-rpc-http-port=8552
9. Node-4の起動
ポート番号以外、Node-2と同じ。
$ ../../besu-22.1.1/bin/besu --data-path=data --genesis-file=../genesis.json --bootnodes=enode://e1f0f86fce7ed83061095d5d6e359aacc7ffe0304467d0faaf17da9cc54b3ce1fd46919537dd7ebfd8b1ed8938bb1c899cbf7fe2f3a44019fc4cd1445040647d@127.0.0.1:30303 --p2p-port=30306 --rpc-http-enabled --rpc-http-api=ETH,NET,IBFT --host-allowlist="*" --rpc-http-cors-origins="all" --rpc-http-port=8548 --engine-rpc-http-port=8553
10. 動作確認
curl -X POST --data '{"jsonrpc":"2.0","method":"ibft_getValidatorsByBlockNumber","params":["latest"], "id":1}' http://127.0.0.1:8545
このコマンドを叩いて、4つのバリデータが表示されればOK。
{
"jsonrpc" : "2.0",
"id" : 1,
"result" : [ "0x59b1276f7295f3dfa96d16e38b5385e233a461ab", "0xc470b991787b76269b39f9a8971c8c2e577e0c4f", "0xd7fac01a40d24ed463fcf2f67ee2ea708b34787b", "0xe93f819df50df397cff7986f74a7aba6644bf0ef" ]
なんかブロックも生成されているようだし、動いているようだ。
しかし、一つ気になったのが、pythonで叩いたときに接続できなかったこと。
>>> from web3 import Web3
>>> w3 = Web3(Web3.HTTPProvider('http://127.0.0.1:8545'))
>>> w3.isConnected()
False
接続に失敗する。んー、curlやtelnetで叩けるのに pythonで叩け無いということがあるのか。。
ちょっとまた別の機会に調べることにして、今回はこれで終わりとします。
3/31 追記
上記のエラーについて色々調べた結果、なにかしらのタイミングで環境変数にHTTP_PROXYを追加していたのが原因だった。。pythonがURIにアクセスする際、環境変数があることでプロキシを使おうとしてエラーになっていたようだ。
気づくまでに2時間ぐらいかかった。tcpdumpとかしてたらもっと早くわかったはずだった。あほや。。
Discussion