🐡

Private Blockchainでフォークをなおす

2023/10/15に公開

GoQuorumでプライベートブロックチェーンを構築し運用していく中でフォークが起こった。フォークが起こった原因は厳密にはわかっていないが、フォークが起きたことがどのようにわかったか、考えられる原因、さらにフォークが起こった時の修正方法を記録する。

環境

Consensysの公式ページのTutorialに従ってAWSのEKSを使用してkubernetesでPrivate blockchainを構築した。
https://docs.goquorum.consensys.net/tutorials/kubernetes

  • 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ノードのログを確認したところブロックの生成が次に進んでいなかった。
https://github.com/Consensys/quorum-kubernetes/issues/207

調査をしているところQuorumのQBFT(コンセンサスアルゴリズムの一種)について詳細でわかりやすい記事を見つけ、その著者に相談したところ様々なことがわかっていった。以降の内容は @H.HSELさんの記事や、実際のやりとりで学んだことが多い。この場を借りて感謝申し上げたい。
⏬ QuorumのQBFTを理解したいならかなりおすすめ
https://qiita.com/hhsel/items/3c94c5583b1c1fc32626

異なるブロックハッシュが生成されていた

ブロック番号におけるブロックハッシュが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-1validator-4number=51044のハッシュ値はhash=474faa..cfa230だが、
validator-2validator-3number=51044のハッシュ値はhash=5db7a5..307312と異なっている。

フォークの原因

フォークの原因としては空のブロックを生成することで起こる脆弱性があるとのことだった。
https://qiita.com/hhsel/items/3c94c5583b1c1fc32626#高負荷時の挙動との関係
https://github.com/Consensys/quorum/issues/1630
@H.HSELさんの修正したものを取り込むことで上記の心配なく運用することができるだろう。

フォーク修正方法

  1. 全てのvalidatorノードの通信を隔離する
  2. 各validatorノードのブロックを1つ前に戻す
  3. 全ての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でフォークが起きていたので5104316進数になおして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