空谷に吼える

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

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の例を挙げて説明していくことにしました。

gakumura.hatenablog.com

そんで以下がその例で、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ノードとチャネルのトポロジーを図にしてくれる親切機能があるのでそれで説明します。

f:id:gakumura:20190601134840p:plain
今回の例でのOrg、Peer、Channelの構成

  • 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をクリック。

f:id:gakumura:20190601140335p:plain

以下の画面が開くので、Advanced Deploymentをクリック。

f:id:gakumura:20190601140634p:plain

続いて以下の画面で任意のChaincode Name(ここではIndeterministicCC、↓のキャプチャではつづり間違えてるけど無視してください)、任意のバージョン(ここではデフォルトのv1のまま)を入力し、インストール先のPeerノードを選び(Peer0、Peer1両方)ます。Upload Chaincode Fileをクリックするとファイル選択のダイアログが開くので、先ほどのChaincodeのZIPファイルを選択します。

f:id:gakumura:20190601140850p:plain

そうすると次の画面に移り、"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を選択。

f:id:gakumura:20190601144141p:plain

以下の画面が出るので、Peer0、Peer1両方を選択して、Exportをクリックします。すると、Peerの情報が記載されたJSONファイルがダウンロードされます。

f:id:gakumura:20190601144216p:plain

今度はAliceのOBPコンソールのNodesタブからExport/Import Peersボタンをクリックして今度はImportを選択すると以下の画面が出ます。Upload remote nodes configurationsをクリックするとアップロードするファイル選択のダイアログが出るので、先ほどダウンロードしたBobのPeer情報のJSONを選択します。

f:id:gakumura:20190601144529p:plain

Importに成功すると、AliceにもBob側でIndeterministicCCをインストールしたPeer0、Peer1が存在することが認識された状態になります。

Chaincodeのインスタンス

上記の手順で準備は完了です。ここからChaincodeのインスタンス化を行いチャネル上で実行可能にしていきましょう。

先ほどと同様にAliceのコンソールからChaincodesのタブを開き、インストールしたIndeterministicCCのv1の右にあるハンバーガーメニューからInstantiateをクリックします。

f:id:gakumura:20190601143313p:plain

以下のような画面が開きます。インスタンス化するチャネルとしてここでは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をクリック。

f:id:gakumura:20190601143556p:plain

alphaチャネルでのChaincodeインスタンス化のトランザクションが発行され、元のChaincodesの画面に戻ります。しばらく待っていると、”Success!”のメッセージ通知が画面に届くとともに、IndeterministicCCのInstantiated on Channelsの表示が1になりました。これでインスタンス化は完了です。

f:id:gakumura:20190601145557p:plain

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を選択します。

f:id:gakumura:20190601150854p:plain

以下の画面が出るので+ボタンをクリックして入力欄を追加したうえで、REST Proxyからの呼び出しを有効化したいチャネル、Chaincode、およびEndorsementを依頼するPeerを選択し、Submitします。

f:id:gakumura:20190601151031p:plain

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を送信)するやつです。

f:id:gakumura:20190601154441p:plain

Fabric SDKを用いてgRPCでChaincodeを実行する場合には証明書を用いてトランザクション発行者の認証を行うため、署名鍵などのセットアップが大変です。が、OBPでREST Proxy経由でChaincodeを実行する場合、認証はIdentity Cloud Serviceというクラウドユーザーアカウントの認証サービスと統合されており、予めREST Proxyを使う権限を設定してあるクラウドユーザーのIDとパスワードでのBasic認証で利用することが可能です。なお、API Key認証も利用できるのでアプリケーションから使いたいときはそっちを使うのが一般的になるかと思います。

ここではお手軽にBasic認証を使います。Postmanの場合は以下のようにAuthorization欄でBasic Authを選択したうえでクラウドユーザーのユーザーID、パスワードを入力します。

f:id:gakumura:20190601152816p:plain

Headerには以下のようにJSONのコンテンツタイプを指定してあげます。

f:id:gakumura:20190601153430p:plain

Body部にはチャネル名、Chaincode名、呼び出したい関数名(ここではWriteNow)と引数(ここでは空の配列)を指定します。

f:id:gakumura:20190601153540p:plain

そんでSendしてみると…?以下のようにFailureが返ってきてしまいました。"Proposal not pass"と出ています。

f:id:gakumura:20190601153937p:plain

エラーとは…これはどうしたことか…???と考え込むまでもないですね。賢明な読者の方は覚えておいででしょうが、このポストは非-決定論的な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を設定します。

f:id:gakumura:20190601155259p:plain

そのうえで、先ほどと同様の手順でAliceのREST Proxy#1でbetaチャネルでのIndeterministicCCのREST API経由の呼び出しを有効化します。今回はEndorsementを依頼する先はAliceのPeer0だけにしておきます。

f:id:gakumura:20190601160015p:plain

そんでPostmanではさっきのリクエストをCopyしたうえで、Body部のチャネル名だけalpha⇒betaに変更してSendしてみます。

f:id:gakumura:20190601155932p:plain

すると今度は以下のようにSuccessが返ってきました、やったぜ。また、Chaincodeの返り値が格納されるPayload部には"2019/06/01 07:00:40.657"が返ってきています。トランザクションが成功したのであれば、この返却された時刻値がStateにも書き込まれているはずです。

f:id:gakumura:20190601160113p:plain

ということでStateにちゃんと書かれているのかReadNowを実行してStateから読みだしてみましょう、の結果が↓です。ちゃんと書かれていましたね。

f:id:gakumura:20190601160454p:plain

なんで今回は成功したのかは皆さんおわかりだと思います。今回は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の一覧は以下にあるので詳しく知りたい方はチェックしてください。

docs.oracle.com

docs.oracle.com

Good coding!(あいさつ)