Private Blockchainでフォークをなおす
GoQuorumでプライベートブロックチェーンを構築し運用していく中でフォークが起こった。フォークが起こった原因は厳密にはわかっていないが、フォークが起きたことがどのようにわかったか、考えられる原因、さらにフォークが起こった時の修正方法を記録する。
環境
Consensysの公式ページのTutorialに従ってAWSのEKSを使用してkubernetesでPrivate blockchainを構築した。
- kubernetes version : 1.26
- githubソース version : quorum-kubernetes Tag 0.1.1
- validator : 4台
- rpcノード : 1台
Gethについてはブラックボックス化されているが、validatorノードの中に入って確認した。
- Version: 1.10.3-stable
- Quorum Version: 23.4.0
- Architecture: arm64
- Network Id: 1337
- Go Version: go1.17.8
- Operating System: darwin
- GOPATH=
- GOROOT=go
フォークの確認
問題の確認ができたのが、突如ブロック生成が止まってしまったことだった。それまで問題なく1ヶ月運用していたのだがある日突然Txが発行できなくなり、validatorノードのログを確認したところブロックの生成が次に進んでいなかった。
調査をしているところQuorumのQBFT(コンセンサスアルゴリズムの一種)について詳細でわかりやすい記事を見つけ、その著者に相談したところ様々なことがわかっていった。以降の内容は @H.HSELさんの記事や、実際のやりとりで学んだことが多い。この場を借りて感謝申し上げたい。
⏬ QuorumのQBFTを理解したいならかなりおすすめ
異なるブロックハッシュが生成されていた
ブロック番号におけるブロックハッシュがvalidatorごとに違うものを認識していたということから、フォークが起きていたということがわかる。
goquorum-node-validator-1-0 goquorum-node-validator-1-quorum DEBUG[10-03|04:40:59.452] Current full block not old enough number=51044 hash=474faa..cfa230
goquorum-node-validator-2-0 goquorum-node-validator-2-quorum DEBUG[10-03|04:40:44.103] Current full block not old enough number=51044 hash=5db7a5..307312
goquorum-node-validator-3-0 goquorum-node-validator-3-quorum DEBUG[10-03|04:41:18.006] Current full block not old enough number=51044 hash=5db7a5..307312
goquorum-node-validator-4-0 goquorum-node-validator-4-quorum DEBUG[10-03|04:40:46.560] Current full block not old enough number=51044 hash=474faa..cfa230
上記のログを確認すると
validator-1
とvalidator-4
はnumber=51044
のハッシュ値はhash=474faa..cfa230
だが、
validator-2
とvalidator-3
はnumber=51044
のハッシュ値はhash=5db7a5..307312
と異なっている。
フォークの原因
フォークの原因としては空のブロックを生成することで起こる脆弱性があるとのことだった。@H.HSELさんの修正したものを取り込むことで上記の心配なく運用することができるだろう。
フォーク修正方法
- 全てのvalidatorノードの通信を隔離する
- 各validatorノードのブロックを1つ前に戻す
- 全てのvalidatorノードの通信を再開する
このように見た感じシンプルだが、kubernetesとして具体的な操作として行ったことを下記に示す。
1. 全てのvalidatorノードの通信を隔離する
全てのvalidatorノードの通信を隔離するためにはConfigMapで指定しているstatic-node.json
を変更する必要がある。(validatorノードたちはこのstatic-node.json
を見に行って通信するpeerを探す。)
static-node.json
を指定してしているのはgoquorum-peers
というConfigMap。それを変更する前にまずはvalidatorノードの挙動に異常が出ないように、replica数を0にして全てのノードを落としておく。
- validatorノードを落とす
kubectl scale -n <namespace> sts <sts name> --replicas=0
(<sts name>
はkubectl get sts -n <namespace>
で確認できる。)
mameta dentsu %kubectl get sts -n <namespace>
NAME READY AGE
alertmanager-monitoring-kube-prometheus-alertmanager 1/1 58d
goquorum-node-rpc-1 1/1 15d
goquorum-node-validator-1 1/1 15d
goquorum-node-validator-2 1/1 15d
goquorum-node-validator-3 1/1 15d
goquorum-node-validator-4 1/1 15d
prometheus-monitoring-kube-prometheus-prometheus 1/1 58d
今回の私の場合では4つのvalidatorノードはgoquorum-node-validator-1
のような名前だったので下記のようにノード4台分を順次実行していく。
kubectl scale -n <namespace> sts goquorum-node-validator-1 --replicas=0
- static-node.jsonの値を変更
下記コマンドでvimで修正できる。
kubectl edit cm -n <namespace> <config map name>
<config map name>
は今回はgoquorum-peers
。
すると下記のように表示されるのでstatic-nodes.jsonの値をまるまる空([])にする。
その際それぞれのenodeの値はメモしておくことを忘れないように。
apiVersion: v1
data:
static-nodes.json: |
[
"enode://...@goquorum-node-validator-4-0...",
"enode://...@goquorum-node-validator-1-0...",
"enode://...@goquorum-node-rpc-1-0...",
"enode://...@goquorum-node-validator-3-0...",
"enode://...@goquorum-node-validator-2-0.."
]
kind: ConfigMap
metadata:
creationTimestamp: "creationTimestamp"
name: goquorum-peers
namespace: <namespace>
resourceVersion: "resourceVersion"
uid: <uid>
⏬
apiVersion: v1
data:
static-nodes.json: |
[]
kind: ConfigMap
metadata:
creationTimestamp: "creationTimestamp"
name: goquorum-peers
namespace: <namespace>
resourceVersion: "resourceVersion"
uid: <uid>
- 落としたvalidatorノードを起動する
kubectl scale -n ryukyu sts <sts name> --replicas=1
- それぞれのvalidatorノードが他のノードと接続ができない状態になっているかを確認する。
# まずgethにattachする
kubectl exec -it goquorum-node-validator-1-0 -n <namespace> -- geth attach http://localhost:8545
# 次にpeer数が0になっていることを確認
> net.peerCount
0
これで無事にvalidatorノードの通信を断絶し隔離することができた。
2. 各validatorノードのブロックを1つ前に戻す
gethに接続していることを前提とする。
kubectl exec -it goquorum-node-validator-1-0 -n <namespace>
でkubernetesのPodの中に入った上で geth attach http://localhost:8545
でgethに接続できる。
- まず現在のブロック番号(フォークが起きているブロック番号)を確認する
eth.blockNumber
- ブロックを1つ前に戻す
debug.setHead(<current blocknumber -1>)
<current blocknumber -1>には16進数を入れて実行した。今回ブロック番号51044
でフォークが起きていたので51043
16進数になおして0xc763
で実行。またstringで渡す必要があるので実際に実行したコードはこのようになる。
debug.setHead("0xc763")
ちなみに実際に実行した時のvalidatorノードのログ(Podのログ)は下記のようになっていた。
goquorum-node-validator-1-0 goquorum-node-validator-1-quorum WARN [10-12|06:48:06.011] Rewinding blockchain target=51043
goquorum-node-validator-1-0 goquorum-node-validator-1-quorum DEBUG[10-12|06:48:06.011] Account Extra Data root hash=000000..000000
goquorum-node-validator-1-0 goquorum-node-validator-1-quorum DEBUG[10-12|06:48:06.011] Rewound to block with state number=51043 hash=0c147c..0d2d5c
goquorum-node-validator-1-0 goquorum-node-validator-1-quorum DEBUG[10-12|06:48:06.012] Account Extra Data root hash=000000..000000
goquorum-node-validator-1-0 goquorum-node-validator-1-quorum INFO [10-12|06:48:06.012] Loaded most recent local header number=51043 hash=0c147c..0d2d5c td=51044 age=3w6d7h
goquorum-node-validator-1-0 goquorum-node-validator-1-quorum INFO [10-12|06:48:06.012] Loaded most recent local full block number=51043 hash=0c147c..0d2d5c td=51044 age=3w6d7h
goquorum-node-validator-1-0 goquorum-node-validator-1-quorum INFO [10-12|06:48:06.012] Loaded most recent local fast block number=51043 hash=0c147c..0d2d5c td=51044 age=3w6d7h
goquorum-node-validator-1-0 goquorum-node-validator-1-quorum DEBUG[10-12|06:48:06.012] Served debug_setHead conn=[::1]:56148 reqid=20 t="685.263µs"
goquorum-node-validator-1-0 goquorum-node-validator-1-quorum DEBUG[10-12|06:48:19.384] Current full block not old enough number=51043 hash=0c147c..0d2d5c delay=3,162,240
goquorum-node-validator-1-0 goquorum-node-validator-1-quorum DEBUG[10-12|06:48:19.653] Recalculated downloader QoS values rtt=20s confidence=1.000 ttl=1m0s
goquorum-node-validator-1-0 goquorum-node-validator-1-quorum DEBUG[10-12|06:48:39.654] Recalculated downloader QoS values rtt=20s confidence=1.000 ttl=1m0s
goquorum-node-validator-1-0 goquorum-node-validator-1-quorum DEBUG[10-12|06:48:59.654] Recalculated downloader QoS values rtt=20s confidence=1.000 ttl=1m0s
きちんと戻っていることがわかる。念のためgeth
コマンドでも確認。
eth.blockNumber
debug.setHeadについて
余談です。
debug.setHead
で戻ったブロックでどのように状態を参照しているかというと stateRoot
の値が大きく関わってきます。
debug.setHeadとstateRootの関係について
stateRoot
はEthereumのブロックヘッダに含まれるフィールドの一つ。その時点でのEthereumの状態(すべてのアカウントの情報、ストレージ、スマートコントラクトのコードなど)をハッシュとして表したもの。このstateRootは、ブロックごとのEthereumの状態をMerkle Patricia Trieというデータ構造で保持しており、このTrieのルートハッシュがstateRootとしてブロックヘッダに格納されている。
debug.setHead
このコマンドは指定されたブロック番号にHead(チェーンの最新のブロック)をセットし、それ以降のブロックとトランザクションをローカルのデータベースから削除する機能がある。
なので、debug.setHeadを使用してブロックを巻き戻すと、その巻き戻されたブロックのstateRootに基づいてチェーンの状態も同時に巻き戻される。具体的には、指定したブロックのstateRootを持つ状態に全体が戻るということになる。
全てのvalidatorノードでブロックを戻しても通信を切っているのでブロック生成されることはないと思います。
3. 全てのvalidatorノードの通信を再開する
それでは全てのvalidatorノードが無事1つ前のブロックに戻ったので通信を再開していきましょう。
1. 全てのvalidatorノードの通信を隔離するでおこなったことと同じようなことをしていきます。
- まずvalidatorノード(Pod)を落とす。
kubectl scale -n <namespace> sts <sts name> --replicas=0
- configMap の中身を戻す
kubectl edit cm -n <namespace> <config map name>
ここでstatic.nodes.jsonの値を元に戻します。
1. 全てのvalidatorノードの通信を隔離するでメモしておいた値を貼り付けます。
- validatorノード(Pod)を再起動する
kubectl scale -n <namespace> sts <sts name> --replicas=1
- validator(Pod)のログを見てブロック生成されていることを確認
stern goquorum-node-validator -n <namespace>
これでブロック生成が無事進んでいれば成功である。
Discussion