Oracle Blockchain Platform Cloud Serviceで非-決定論的なChaincodeをインストール、インスタンス化してREST APIで実行
なんの話
- 非-決定論的なChaincodeを例を挙げて説明
- Oracle Blockchain Platform Cloud ServiceでChaincodeのインストール、インスタンス化を行う方法を紹介
- Oracle Blockchain Platform Cloud ServiceでREST Proxyを通じてChaincodeを実行する方法を紹介
- 非-決定論的なChaincodeの挙動を確認
内容
このポストでは非-決定論的な挙動をするChaincodeの例を挙げ、実際に実験してみてその非-決定論性によりどのようにトランザクションが失敗してしまうのかというのを説明していきます。なお実験はHyperledger Fabricをマネージドサービスとして提供しているOracle Blockchain Platform Cloud Serviceを使って行うので、Oracle Blockchain Platform Cloud ServiceでのChaincodeのインストール、インスタンス化およびREST APIでの実行のHow toにもなっているという一石二鳥なポストなんだぜ
非-決定論的なChaincodeの例
↓のポストでも「Chaincodeの中に非-決定論的な処理をつくりこんではイケナイ」という話をしていますが、これはなんだか感覚的にわかりづらい話なのか、もうちょっと詳しく説明してくれという要望を良く聞きます。なので非-決定論的な挙動をするChaincodeの例を挙げて説明していくことにしました。
そんで以下がその例で、Goで書いたChaincodeです。こういう「だめなChaincode」のサンプルってなかなかないのヨネー。ちなみにgakumura氏は「ぜんぜんわからない、おれたちは雰囲気でGo言語をやっている」勢なのでお作法とかがたぶんに雑なんだけど見逃してくれよな!
package main import ( "fmt" "time" "github.com/hyperledger/fabric/core/chaincode/shim" pb "github.com/hyperledger/fabric/protos/peer" ) type SampleChaincode struct { } // =================================================================================== // init // =================================================================================== func (t *SampleChaincode) Init(stub shim.ChaincodeStubInterface) pb.Response { return shim.Success(nil) } // =================================================================================== // Main // =================================================================================== func main() { err := shim.Start(new(SampleChaincode)) if err != nil { fmt.Printf("Error starting Sample chaincode: %s", err) } } // =================================================================================== // Invoke // =================================================================================== func (t *SampleChaincode) Invoke(stub shim.ChaincodeStubInterface) pb.Response { function, args := stub.GetFunctionAndParameters() var result string var err error if function == "WriteNow" { result, err = t.WriteNow(stub, args) } else if function == "ReadNow" { result, err = t.ReadNow(stub, args) } else { return shim.Error("Received unknown function invocation") } if err != nil { return shim.Error(err.Error()) } return shim.Success([]byte(result)) } // =================================================================================== // WriteNow - get peer local current timestamp and write it to the state // =================================================================================== func (t *SampleChaincode) WriteNow(stub shim.ChaincodeStubInterface, args []string) (string, error) { nowTime := time.Now() // Get current timestamp of the Peer's local clock const MilliFormat = "2006/01/02 15:04:05.000" nowTimeInMilli := nowTime.Format(MilliFormat) // Convert the timestamp into YYYY/MM/DD hh:mm:ss.SSS format err := stub.PutState("TimestampRecord", []byte(nowTimeInMilli)) // Put the timestamp into the State with its key TimestampRecored if err != nil { return "", fmt.Errorf("Failed to write state, with error: %s", err) } return string(nowTimeInMilli), nil } // =================================================================================== // ReadNow - read timestamp recorded on the state // =================================================================================== func (t *SampleChaincode) ReadNow(stub shim.ChaincodeStubInterface, args []string) (string, error) { timestampRecord, err := stub.GetState("TimestampRecord") // Get the timestamp from the State with its key TimestampRecored if err != nil { return "", fmt.Errorf("Failed to read state: %s with error: %s", "TimestampRecord", err) } if timestampRecord == nil { return "", fmt.Errorf("Recorded Timestamp not found.") } return string(timestampRecord), nil }
まあまあの長さになってますが、ほとんどの部分はChaincodeのお約束のような部分で今回あんまり重要ではないです。理解してほしいのは以下ふたつ:
WriteNow()
関数:中でtime.Now()
して現在時刻のタイムスタンプを取得し、これをミリ秒までの表現形式に直してからStateに書き込んでいます。また、書き込んだタイムスタンプを返却しています。ReadNow()
関数:書き込まれているはずのタイムスタンプをStateから読んで返却しています。Stateに存在しなかった場合はエラーを返す。
これは非-決定論的な挙動をするChaincodeの例だったことを覚えておいででしょうか?ということで何が非-決定論的な挙動なのかというと、ハイそうです現在時刻のタイムスタンプを取得するところですね。ここで取得する「現在時刻」はChaincodeを実行するPeerのローカルな時計を基準にしています。これがダメですね。
(非-決定論的ではなく)決定論的なChaincodeでは、「Chaincodeの引数」および「台帳(State+Block)」を入力として、同一の入力からは常に同一の出力(Stateに書き込む値)が導かれなければなりません。
この例のChaincodeでは、これらの「入力」以外にPeerのローカル時刻というネットワークで合意も同期も保証できない要素に依存してStateに書き込む値を決めているため、複数のPeer間のローカル時刻ズレにより書き込もうとするStateの値がブレてしまうという非-決定論的な処理を中に作り込んでしまっています。
ちなみにタイムスタンプのフォーマットをミリ秒まで表現に変換しているのは、秒までだと複数Peer間でたまたま合致しちゃったりしそうで例としてイマイチになっちゃうからですね。
ではこのChaincodeを動かして、非-決定論的な挙動によりEndorsement Policyが満たされず、有効なトランザクションを発行できない様子を実験して確かめてみましょう。以降、Oracle Blockchain Platform Cloud Serviceを利用して実験していきます。
Oracle Blockchain Platform Cloud ServiceでのChaincodeのデプロイと実行
ということでOracle Blockchain Platform Cloud Service(以下、OBP)で上記の非-決定論的なChaincodeを動かしてみます。Chaincodeを実行するのは、まずPeerにインストールし、その後チャネルでインスタンス化(Instantiate)したうえで実行(Invoke)するという流れになります。
前提とするネットワーク構成
OBPにはネットワークに存在するOrganization、Peerノードとチャネルのトポロジーを図にしてくれる親切機能があるのでそれで説明します。
- Organization:AliceFounderとBobMemberが存在
- Peer:AliceFounder、BobMemberがそれぞれPeer0、Peer1を所有
- チャネル:Alice、Bobともにチャネルalpha、beta両方に参加。また、それぞれのPeer0、Peer1両方がチャネルalpha、betaに所属。
Chaincodeのインストール
OBPではChaincodeをインストールするには、予めソースコードをZIPで固めておく必要があります。上記の例をコピペして任意のhogehoge.go(ここではIndeterministicCC.go)として任意の空のフォルダ(ここではIndeterministicCCフォルダ)に保存し、ZIPしておきましょう(IndeterministicCC.zipができあがる)。
では、ここからインストールしていきましょう。ただし、この実験では都合上AliceとBob両方のPeerからEndorsementを取得してくる必要があるため、AliceとBobそれぞれで配下のPeerにこのChaincodeをインストールします。
まずはAlice側で作業をしていきます。AliceのOBPのコンソール画面のChaincodesタブを開き、Deploy a New Chaincodeをクリック。
以下の画面が開くので、Advanced Deploymentをクリック。
続いて以下の画面で任意のChaincode Name(ここではIndeterministicCC、↓のキャプチャではつづり間違えてるけど無視してください)、任意のバージョン(ここではデフォルトのv1のまま)を入力し、インストール先のPeerノードを選び(Peer0、Peer1両方)ます。Upload Chaincode Fileをクリックするとファイル選択のダイアログが開くので、先ほどのChaincodeのZIPファイルを選択します。
そうすると次の画面に移り、"Success: The chaincode was installed"のメッセージが上部に表示されます。このままインストールしたChaincodeをインスタンス化することができるのですが、まだもう一方のOrganization(Bob)側でのインストールが済んでおらず準備が整っていないのでいったん閉じましょう。
Alice側でのインストールは済んだので、今度は同様にBobのコンソールを開いて上記の作業を繰り返します。なおこの際、必ずChaincode Nameとバージョンを先ほどのものと揃えておく必要があります(異なるChaincode Name、バージョンだと別物として扱われるため)
Chaincodeをインストール済のPeerの情報を共有
上記の手順でAlice、Bob両方のOrganizationで、それぞれ配下のPeer0、Peer1にIndeterministicCCのインストールが完了しました。次にインスタンス化を行っていきますが、その前にBob側でChaincodeがインストールされているPeerの情報を、インスタンス化の操作を行うAlice側に伝えてあげる必要があります。これはのちほどREST ProxyでChaincodeのREST API経由の呼び出しを有効化する際に、どのPeerに当該Chaincodeがインストールされているか=当該Chaincodeを実行してEndorsementが可能なのかをAlice側がREST Proxyが知っておく必要があるためです。
BobのOBPコンソールを開き、Nodesタブに移動します。Export/Import PeersボタンをクリックしてExportを選択。
以下の画面が出るので、Peer0、Peer1両方を選択して、Exportをクリックします。すると、Peerの情報が記載されたJSONファイルがダウンロードされます。
今度はAliceのOBPコンソールのNodesタブからExport/Import Peersボタンをクリックして今度はImportを選択すると以下の画面が出ます。Upload remote nodes configurationsをクリックするとアップロードするファイル選択のダイアログが出るので、先ほどダウンロードしたBobのPeer情報のJSONを選択します。
Importに成功すると、AliceにもBob側でIndeterministicCCをインストールしたPeer0、Peer1が存在することが認識された状態になります。
Chaincodeのインスタンス化
上記の手順で準備は完了です。ここからChaincodeのインスタンス化を行いチャネル上で実行可能にしていきましょう。
先ほどと同様にAliceのコンソールからChaincodesのタブを開き、インストールしたIndeterministicCCのv1の右にあるハンバーガーメニューからInstantiateをクリックします。
以下のような画面が開きます。インスタンス化するチャネルとしてここではalphaを選択し、Endorsementを行えるPeersとしてPeer0、Peer1両方を指定します。Initial ParameterはChaincodeのinit()に渡されるパラメータですが、今回のChaincodeには不要なので空の配列を指定します。Add IdentityでAliceとBob両方をEndorsementするOrganizationの候補リストに加えたうえで、Signed byでは2 of 2を指定、つまりAliceとBob両方のEndorsementが必須であるというEndorsement Policyを指定します。そしたらInstantiateをクリック。
alphaチャネルでのChaincodeインスタンス化のトランザクションが発行され、元のChaincodesの画面に戻ります。しばらく待っていると、”Success!”のメッセージ通知が画面に届くとともに、IndeterministicCCのInstantiated on Channelsの表示が1になりました。これでインスタンス化は完了です。
ChaincodeのREST Proxyへの登録
ここまででChaincodeのインスタンス化が完了したので、IndeterministicCCはチャネルalpha上で、AliceとBob両方のEndorsemntが必要というEndorsement Policyで実行可能な状態になっています。この状態でFabric SDKからのgRPCで実行は可能ですが、Fabric SDK使うのめんどくさいヨネーなのでお手軽にREST API経由で呼び出せるよう、当該ChaincodeをREST Proxyに登録します。
現状のOBPではひとつのインスタンスにREST Proxyが4つ(REST Proxy #1~#4)付属しています。ここではAliceの持っているREST Proxy#1から、先ほどのChaincodeを実行できるようにします。
AliceのOBPコンソールのNodesタブを開き、REST Proxy#1の行の右のハンバーガーメニューからEdit Configurationを選択します。
以下の画面が出るので+ボタンをクリックして入力欄を追加したうえで、REST Proxyからの呼び出しを有効化したいチャネル、Chaincode、およびEndorsementを依頼するPeerを選択し、Submitします。
Success的なメッセージが一瞬出て元の画面に戻ったら準備完了です。
ChaincodeのREST Proxy経由での実行
ここまででIndeterministicCCのインストール、インスタンス化およびREST Proxyへの登録が済んだので、REST APIで実行できるようになっています。gakumura氏はこのポストを書きながらやっているのでもうちょっとかかっていますが、だいたい上記の手順だけなら作業としては15分くらいで終わるくらいの感覚です。
ではREST APIで先ほどのIndeterministicCCを呼んでみましょう。cURLなどのREST APIを扱える任意の方法で実行できますが、ここではみんな大好きPostmanを使って実行していきます。Postman便利なので使ってない方もこれを機に試してみてください。
REST APIの送信先として先ほどのAliceのREST Proxy#1のinvocationというエントリポイントを指定します。これは指定したChaincodeをInvoke(設定しておいたPeerにTransaction Proposalを送り、Endorsement Policyが集まればOrdererにTransactionを送信)するやつです。
Fabric SDKを用いてgRPCでChaincodeを実行する場合には証明書を用いてトランザクション発行者の認証を行うため、署名鍵などのセットアップが大変です。が、OBPでREST Proxy経由でChaincodeを実行する場合、認証はIdentity Cloud Serviceというクラウドユーザーアカウントの認証サービスと統合されており、予めREST Proxyを使う権限を設定してあるクラウドユーザーのIDとパスワードでのBasic認証で利用することが可能です。なお、API Key認証も利用できるのでアプリケーションから使いたいときはそっちを使うのが一般的になるかと思います。
ここではお手軽にBasic認証を使います。Postmanの場合は以下のようにAuthorization欄でBasic Authを選択したうえでクラウドユーザーのユーザーID、パスワードを入力します。
Headerには以下のようにJSONのコンテンツタイプを指定してあげます。
Body部にはチャネル名、Chaincode名、呼び出したい関数名(ここではWriteNow)と引数(ここでは空の配列)を指定します。
そんでSendしてみると…?以下のようにFailureが返ってきてしまいました。"Proposal not pass"と出ています。
エラーとは…これはどうしたことか…???と考え込むまでもないですね。賢明な読者の方は覚えておいででしょうが、このポストは非-決定論的なChaincodeはトランザクション失敗するのでダメですよ、ということを説明してきているのでこれで想定通りです。
Proposal not passというのは、REST APIによりIndeterministicCCのInvoke依頼を受けたREST Proxyが、設定しておいたAliceとBobそれぞれのPeer0、Peer1にTransaction Proposalを送ったものの、返ってきたEndorsementに含まれているWrite Set、つまりStateにPutしようとしている値がPeer間で異なっているため、Endorsement Policyを満たせなかった、ということを表しています。
というわけで、このように非-決定論的な処理(ここではPeerのローカル時刻に依存した処理)を作りこむとまずいことになりますよ、というお話でした。
おまけコーナー:単独PeerでのEndorsementで実験すると…?
ここまででこのポストの主旨はコンプリートなんですが、ここからおまけでもうちょっと実験してみます。先ほどと同じIndeterministicCCを今度はbetaチャネルでインスタンス化しますが、ここで今回はAliceとBobのうちどちらかひとつ(1 of 2)のEndorsementだけでOKというEndorsement Policyを設定します。
そのうえで、先ほどと同様の手順でAliceのREST Proxy#1でbetaチャネルでのIndeterministicCCのREST API経由の呼び出しを有効化します。今回はEndorsementを依頼する先はAliceのPeer0だけにしておきます。
そんでPostmanではさっきのリクエストをCopyしたうえで、Body部のチャネル名だけalpha⇒betaに変更してSendしてみます。
すると今度は以下のようにSuccessが返ってきました、やったぜ。また、Chaincodeの返り値が格納されるPayload部には"2019/06/01 07:00:40.657"が返ってきています。トランザクションが成功したのであれば、この返却された時刻値がStateにも書き込まれているはずです。
ということでStateにちゃんと書かれているのかReadNowを実行してStateから読みだしてみましょう、の結果が↓です。ちゃんと書かれていましたね。
なんで今回は成功したのかは皆さんおわかりだと思います。今回はEndorsementを依頼するPeerがひとつで、また、ひとつのOrganization配下のPeerだけで充足するEndorsement Policyとしていました。なので複数Peer間で実行された場合にWrite Setが異なってしまう(可能性がある)というChaincodeの非-決定論的処理は今回の単一Peerでの実行では問題にならず、そのまま有効なトランザクションとして台帳に書き込むことができたということです。以上がおまけでした。
まとめ
このポストでは非-決定論的な処理(ここではStateに書き込む値のPeerのローカル時刻への依存)を作りこんでしまっているChaincodeの例を挙げ、そのChaincodeを実際に動かしてみてどのようなまずいことが起こるのかを実験して確かめました。非-決定論的な処理は意識していないとつい作りこんでしまうことがあり、また、上記おまけコーナーのように単一Peer上だけでテストしていたりして問題に気付くのが遅れたりするので、皆様におかれましてはどうぞご注意なされますよう。
また、ついでにOracle Blockchain Platform Cloud Serviceを使ってのChaincodeのインストール、インスタンス化およびREST Proxyへの登録、REST API経由での呼び出しがとてもかんたんにできるということもおわかりいただけていれば幸いです。OBPのドキュメントと、今回使ったInvocation以外にもいろいろあるREST APIの一覧は以下にあるので詳しく知りたい方はチェックしてください。
Good coding!(あいさつ)