空谷に吼える

ブロックチェーン/DLTまわりのなにかしらを書いていく所存

Chaincode内での非-決定論的挙動のタブーと信頼可能なタイムスタンプを台帳に記録する方法

なんの話

  • Chaincode内に非-決定論的なロジックを作り込むとコンセンサス取れなくなって死ぬ
  • 信頼可能な(都合よく偽れない)タイムスタンプを台帳上に記録するためのいくつかの方法

内容

Hyperledger Fabricでは台帳になんらかの更新を行うトランザクションを実行したい場合、クライアントから通常複数のPeerノードにChaincodeの実行を依頼しますが、この際にそれぞれのPeerから返されるWrite-Set=このトランザクションの結果台帳に書かれるはずの値が一致する必要があります(不一致のWrite-Setを集めてきてもEndorsement Policyを満たさずトランザクションがコミットされないため)。

ということはどういうことかというと、同一の現在Stateおよびクライアントから渡されるChaincodeの引数のセットを入力されたChaincodeからは、同一のWrite-Set(つまり事後State)が導かれるようにしておかないとならないということです。この条件を満たすChaincodeをdeterministic(決定論的)であると呼び、そうでないものをnon-deterministic(非-決定論的)と呼びます。

ということで、Chaincode内に非-決定論的なロジックを作り込んではイケマセン。なお、この非-決定論的な挙動のタブーはHyperledger FabricのChaincodeに限らずスマートコントラクト一般の話である認識なんですが、わしは他のブロックチェーン/DLTのコンセンサス全部理解してるわけではないのでそうでもないかもしんない。ということでこの記事のスコープはHyperledger FabricのChaincodeのみです。

非-決定論的ロジックの例

わかったけどじゃあ非-決定論的なロジックってどんなんなのさ、と思った?わたしはしんせつなのでいくつか例を挙げてパターンを説明します

ランダムな値の生成

台帳にレコード突っ込む際のKeyに使う、とかでランダムな値を生成する処理(UUID ver4など)を使いたくなりますよね。ダメです

当然複数ノード間で生成したランダムな値がぜんぜん一致しなくなりますね。こういうことやりたい場合は、台帳内にシードを用意しておき、そこから決定論的に生成する処理を利用する(UUID ver3など)、あるいはクライアント側で生成しておいて渡してあげましょう。

↓の記事はパブリックのEthereumで公平な抽選を作り込むには、、、の例ですが、台帳内のブロックハッシュをシードに疑似的に乱数を生成しています。こういうやり方であればノード間で結果が一致するはずなのでもんだいないですね

ブロックチェーンとスマートコントラクトで、抽選の公正性を証明する - Qiita

uint randNumber = ( uint(block.blockhash(blockIdx--)) % numberMaxPerUnit ) + 1;

Peerノードで取得した現在日時の利用

Peerノードでtime.Now()とかしてPeerノードの現在時刻取得するのもやばいやつです。以下の合わせ技により往々にして取得した時刻にPeerノード間でズレが出てしまいます。ミリ秒まで合わないのはもちろん、ちょくちょく秒単位でズレちゃうぜ

  • Peerノード間での時刻同期が不完全、あるいは全然されていないことによる内部時計のズレ
  • Chaincodeの実行タイミングのズレ…クライアントからの依頼メッセージが届くタイミング、処理速度差両方でズレる

ということで、基本的には「現在時刻」はクライアントから渡すしかないということです。そんでこのクライアントから渡される「現在時刻」信用できねぇよ問題の話はあとでする

外部の情報ソースの利用

台帳に含まれている情報でもクライアントから渡された引数でもない、外部に存在する情報をChaincodeから取得しようとすると、以下に挙げるような理由で各Peerノードでそれぞれ取得した結果が一致することを保証するのは困難になります。なお、前述のノードのローカル時刻も「台帳の外部に存在する情報」の一種ですね。

  • Chaincodeの実行タイミングのズレによってそれぞれのノードに外部サービスから違う値が返ってくる、あるいは取得できない可能性
  • 外部からの攻撃(MitM攻撃とかDNS汚染とか)あるいはノード所有者自身の意図によって不正な値を取得させられる可能性

どうしても外部情報を使いたい場合はオラクルを使うことを検討しましょう。「オッ、宣伝」と思った?ちがう、ここで言うオラクルは赤いロゴの会社のほうではなく、中立的かつ信頼できるかたちでブロックチェーンネットワークに外部の情報をデジタル署名付きで提供してくれるサービスのほうのオラクルのことです。この暗号技術界隈のほうのオラクルに思い至らなかった方は↓などを読んでお勉強しましょう

オラクル(Oracle)とは何か? ブロックチェーン | block-chain.jp

信頼可能な「現在時刻」のタイムスタンプを台帳に記録する方法

前述の理由によりChaincode内、つまりコンセンサスに含まれ信用できるかたちで現在時刻のタイムスタンプを取得することはふつうできません。そうするとクライアントから渡してあげなければならないということになりますが、これでは何も対策を講じないとクライアント側で「現在時刻」をいくらでも都合よく偽れてしまうということになります。「現在時刻」を実際より早い時間で申告することにインセンティブが働くようなユースケースの場合には何らかの対策が必要です。

ということで、思い至った対策をふたつ挙げておきます。他にもあるぜという方は教えてください

クライアントから渡されたタイムスタンプをPeerノード側の現在時刻と比較してチェックする

クライアントから渡されたタイムスタンプと、Chaincode内で取得したPeerノード内部の現在時刻を比較し、それが許容誤差の範囲内に収まっているかチェックする方法です。収まってなかったらChaincodeからエラーを返しましょう。

許容誤差をどの程度にするかはクライアントと各Peerノードの内部時計のズレと、クライアントからPeerノードにChaincode実行依頼が届くまでのネットワーク伝送の時間、Chaincodeそのものの実行時間が収まる範囲というあたりで計算しましょう。リーズナブルなところでは±5秒くらいとか?

この方法は実施が簡単なところがメリットですが、許容誤差の範囲内でクライアント側が都合良くタイムスタンプを申告してくる可能性があるので、タイムスタンプの精度が必要になるケースでは使えません。

時計オラクルを利用する

Hyperledger Fabricのネットワークの外部に時刻を教えてくれるオラクルを用意しておき、そのオラクルの署名付きのタイムスタンプをクライアントから渡させるようにする、という方法です。署名の検証をChaincode内に作り込んでおき、署名が不正だった場合は受け付けないようにしておく必要があります。

また、「常時オラクルに時刻を問い合わせておいて署名付きタイムスタンプをストックしておいて、Chaincodeに渡す時点で都合の良いタイムスタンプを選ぶ」というやり方にも対処が必要です。オラクルへの問い合わせにコスト(課金)がかかるようにしておくなどが考えられると思います。

秒以下の単位での精度が高いタイムスタンプが利用できるというところがメリットです。一方で、現在時刻を返すだけとはいえオラクル用意するのは高可用性が必須だったりして結構たいへんです。「なぜ現在時刻を取得するだけのためにこんな苦労をしなければならないのか…?」という心情が発生しますね、にんげんだもの

Hyperledger Fabricでのタイムスタンプ関連の補足

ブロックチェーンに記録されているトランザクション履歴(Transaction History)にはトランザクションごとのタイムスタンプが必ず付与されていますが、これはTransaction Proposal時にクライアントから渡したものが使用され、つまりクライアントの言い値です。信用してはいけません。

あと、たまに話題に上がるんですが、「Ordering Serviceがブロック生成時に付与するブロックごとのタイムスタンプ」は今のところありません。KafkaとかRAFTとかSBFTとかの、複数ノードからなるOrdering Serviceの構成で整合したタイムスタンプ振るのめちゃめちゃ難しいんですよね。

前述の対策を行っていようがいまいがどっちにせよ、トランザクションの整合性の確認などのためになんらかの順序付け、ソートが必要な場合は、Ordering Serviceでのブロック生成時に確定しているBlock No.とTransaction No.のみを使用し、タイムスタンプに依存するのはやめましょう