この記事は、OSSの分散型台帳技術であるHyperledger/fabricを用いて簡易な入出金処理を作り、docker swarm modeで分散処理させてみる、という連載の2回目です。
今回は、Hyperledger/fabricのchaincodeを開発するための環境構築手順と、前回動作させた入金・送金・出金サンプルアプリのchaincodeの内容を解説します。Hyperledger/fabricのAPI利用方法がわかれば、chaincodeの記述は難しく無いと思います。
- OSSの分散型台帳 Hyperledger/fabric をdocker swarmで分散させてみよう(1/3)〜 動作確認編
- OSSの分散型台帳 Hyperledger/fabric をdocker swarmで分散させてみよう(2/3)〜 chaincode解説編
- OSSの分散型台帳 Hyperledger/fabric をdocker swarmで分散させてみよう(3/3)〜 分散環境解説編
Hyperledger/fabricのchaincode開発環境の構築
Hyperledger/fabric 1.1のchaincodeは、Goあるいはnode.jsで記述します。最終的にはJava等も視野に入っているようですが、リポジトリ(hyperledger/fabric-chaincode-java)を見る限りまだまだというところです。
今回はGoでchaincodeを記述することにします。
goとdockerのインストール
-
goのインストール
- 1.9以降のgolangをインストールし、適切にGOPATHを設定してください。
-
dockerとdoker-composeのインストール
- chaincodeの動作確認を行えるように、17.06.2-ce 以降のdocker、及び対応するバージョンのdocker-composeをインストールしてください。
Hyperledger/fabricの取得
-
Hyperledger/fabricの開発用ライブラリを取得する
ubuntu@node0:~$ go get -d github.com/hyperledger/fabric/protos/peer ubuntu@node0:~$ go get -u --tags nopkcs11 github.com/hyperledger/fabric/core/chaincode/shim
-
注意
- githubで公開されているHyperledger/fabricをそのままgit clone (go get) しただけでは、暗号処理関連のライブラリが足りないためビルドができません。
- そのため暗号処理を用いない開発用tag
nopkcs11
を指定して
github.com/hyperledger/fabric/core/chaincode/shim
を上書きすることで、手元でビルドの成否が確認できるようにします。
-
注意
chaincodeのスケルトンを作成しビルド
開発用ライブラリを正しくgo get
できているか確認するために、ログを出すだけで何もしない空のchaincodeを作ってビルドしてみます。
-
chaincode用のディレクトリを${GOPATH}/src以下に作成する
ubuntu@node0:~$ mkdir -p ${GOPATH}/src/github.com/nmatsui/fabric-payment-sample-chaincode/ ubuntu@node0:~$ cd ${GOPATH}/src/github.com/nmatsui/fabric-payment-sample-chaincode/
-
chaincodeのスケルトンを作成する
${GOPATH}/src/github.com/nmatsui/fabric-payment-sample-chaincode/fabric-payment.gopackage main import ( "fmt" "github.com/hyperledger/fabric/core/chaincode/shim" sc "github.com/hyperledger/fabric/protos/peer" ) // shim.NewLoggerでロガーが取得できる var logger = shim.NewLogger("main") type EntryPoint struct { } // chaincodeのinstantiate時に起動される初期化メソッド // (今回のサンプルでは、何もしない) func (s *EntryPoint) Init(APIstub shim.ChaincodeStubInterface) sc.Response { logger.Info("instantiated chaincode") return shim.Success(nil) } // cliやSDKからchaincodeを呼び出した際に起動するエントリーポイントとなるメソッド // 最終的には、指定されたfunctionに従って、具体的な処理を他のメソッドに委譲する func (s *EntryPoint) Invoke(APIstub shim.ChaincodeStubInterface) sc.Response { function, args := APIstub.GetFunctionAndParameters() msg := fmt.Sprintf("No such function. function = %s, args = %s", function, args) logger.Error(msg) return shim.Error(msg) } func main() { if err := shim.Start(new(EntryPoint)); err != nil { logger.Errorf("Error creating new Chaincode. Error = %s\n", err) } }
-
Init
とInvoke
を実装している型であれば、Hyperledger/fabricネットワーク上で実行可能なchaincodeとなります。
-
-
chaincodeのスケルトンをビルドする
- ビルド時にも、
--tags nopkcs11
を指定してください。
ubuntu@node0:~/go/src/github.com/nmatsui/fabric-payment-sample$ go build --tags nopkcs11 fabric-payment.go
- 何もエラーメッセージが表示されず、実行ファイル
fabric-payment
が生成されていればビルド成功です。 - 実行ファイルを動作させると、次のようなログが出力されると思います。
ubuntu@node0:~/go/src/github.com/nmatsui/fabric-payment-sample$ ./fabric-payment 2018-03-04 09:49:19.168 JST [shim] SetupChaincodeLogging -> INFO 001 Chaincode log level not provided; defaulting to: INFO 2018-03-04 09:49:19.168 JST [shim] SetupChaincodeLogging -> INFO 002 Chaincode (build level: ) starting up ... 2018-03-04 09:49:19.168 JST [main] Error -> ERRO 003 Error creating new Chaincode. Error = Error chaincode id not provided
- ビルド時にも、
モデルの作成
Hyperledger/fabricは、JSON(のバイト配列)としてデータを台帳に記帳します。そのため、JSON化できるのであればどのような非定型データであれ扱うことはできますが、ある程度固定されたデータ構造を定義しておいた方がchaincodeの見通しがよくなります。また記帳する最新データ(state)の保存先としてcouchdbを用いる場合は、データ構造が固定されていればIndexを有効に活用できます。
そこでまず、記帳したいデータ構造をstructとして定義します。
以下で紹介するコードでは、importやエラー処理を省略しています。実際のコードはgithubリポジトリを参照してください。
モデルを定義する際の注意点
Hyperleder/fabricは、台帳にデータを記帳する際に以下のようなステップで処理を行います。
- SDKはpeerにトランザクションのproposalを送信
- peerは自身の最新の台帳状態を用いてchaincodeをシミュレート実行する
- 無限ループに陥るなどしないことを確認
- シミュレートに成功した場合、「シミュレート時点の台帳の状態」と「更新後の台帳の状態」をSDKに返す
- この時点ではまだ台帳に記帳されない
- シミュレート実行が全て成功した場合、SDKは(peerをproxyにして)ordererにトランザクション処理を依頼する
- ordererは依頼されたトランザクションを整列させ、各peerに順序立ててコミットを依頼する
- 各peerは、「シミュレート時点の台帳の状態」が他のトランザクションによって更新されていないことを確認し、更新後の状態を記帳してトランザクションをコミットする
出典:http://hyperledger-fabric.readthedocs.io/en/release-1.1/arch-deep-dive.html
そのため、複数のトランザクションが並列して「同じデータ」を更新しようとすると、どれか一つのトランザクション以外は全て却下されてしまいます。つまり、毎回同じデータを更新するようなchaincodeは、更新スループットが劣悪になってしまうということです(実際に性能テストをしたわけではありませんが・・・)。
モデルの定義時には、このトランザクション処理のアーキテクチャに十分注意しましょう。
モデル種別を定義する
今回のサンプルアプリでは、「口座」と「イベント(入金・送金・出金)」という2つのモデルを扱います。これらの種別を判別するための型を定義します。
- 口座(AccountModel)
- イベント(EventModel)
- 入金(DepositEvent)
- 送金(RemitEvent)
- 出金(WithdrawEvent)
ただしGoには、Enum型がありません。そこで以下のような、JSON serializableなEnumもどきの型を定義します。
const (
unknownModelStr = "unknown"
accountModelStr = "account"
eventModelStr = "event"
)
// ModelType : model type
type ModelType int
// concrete ModelType
const (
UnKnownModel ModelType = iota
AccountModel
EventModel
)
// String : Stringer interface
func (t ModelType) String() string {
switch t {
case AccountModel:
return accountModelStr
case EventModel:
return eventModelStr
default:
return unknownModelStr
}
}
// MarshalJSON : Marshaler interface
func (t ModelType) MarshalJSON() ([]byte, error) {
return json.Marshal(t.String())
}
// UnmarshalJSON : Marshaler interface
func (t *ModelType) UnmarshalJSON(data []byte) error {
var s string
switch s {
case accountModelStr:
*t = AccountModel
case eventModelStr:
*t = EventModel
default:
*t = UnKnownModel
}
return nil
}
モデルを定義する
AccountモデルとEventモデルは、次のようになります。非常にシンプルです。
台帳からモデル種別で絞り込んでstateを検索できるように、ModelTypeやEventTypeを型に組み込んでいます。
type Account struct {
ModelType types.ModelType `json:"model_type"`
No string `json:"no"`
Name string `json:"name"`
Balance int `json:"balance"`
}
type AccountState struct {
No string `json:"no"`
Name string `json:"name"`
PreviousBalance int `json:"previous_balance"`
CurrentBalance int `json:"current_balance"`
}
type Event struct {
ModelType types.ModelType `json:"model_type"`
EventType types.EventType `json:"event_type"`
No string `json:"no"`
Amount int `json:"amount"`
FromAccountState *AccountState `json:"from_account"`
ToAccountState *AccountState `json:"to_account"`
}
口座のCRUD
次に口座(Account)の検索、作成、変更、削除を行うchaincodeを記述します。単なる関数として実装しても良いのですが、処理をグループ化するためにAccountContract
型のメソッドとして定義します。
REST APIで指定したパラメータは、SDK経由で引数のargs []string
に渡されてきます。
口座一覧の取得(ListAccount)
couchdbのクエリ文字列を引数としてshim.ChaincodeStubInterface.GetQueryResult
を呼び出すことで、stateから最新の口座を一覧で取得します。
LevelDBを用いる場合、クエリを利用することができません。この場合、CompositeKeyという機能を使うことになります。詳細は、公式のサンプルコードを参照してください。
// AccountContract : a struct which has the methods related to manage Account
type AccountContract struct {
}
// ListAccount : return a list of all accounts
func (ac *AccountContract) ListAccount(APIstub shim.ChaincodeStubInterface, args []string) sc.Response {
accountLogger.Infof("invoke ListAccount, args=%s\n", args)
if len(args) != 0 { //パラメータ数をチェック(口座一覧取得にはパラメータが無いはず)
}
// stateから最新のAccountを全て取得するクエリを生成(couchdbのQuery文法に従う)
query := map[string]interface{}{
"selector": map[string]interface{}{
"model_type": types.AccountModel,
},
}
queryBytes, err := json.Marshal(query)
accountLogger.Infof("Query string = '%s'", string(queryBytes))
// クエリを用いてstateから最新のAccount一覧を取得するIteratorを生成
resultsIterator, err := APIstub.GetQueryResult(string(queryBytes))
// 取得したIteratorは、メソッド離脱時にCloseする
defer resultsIterator.Close()
// stateから取得したJSONを*Account型にキャストしてsliceに追加
results := make([]*models.Account, 0)
for resultsIterator.HasNext() {
queryResponse, err := resultsIterator.Next()
account := new(models.Account)
results = append(results, account)
}
// レスポンスとなるJSONのバイト配列を生成
jsonBytes, err := json.Marshal(results)
return shim.Success(jsonBytes)
}
口座の新規作成(CreateAccount)
AccontオブジェクトのJSONのバイト配列を、口座番号をキーとしてshim.ChaincodeStubInterface.PutState
に渡せば、口座情報を新規登録できます。
なお同一キーがある場合、PutStateはそのオブジェクトを上書きしてしまうため、口座番号は一意でなければなりません。
// CreateAccount : create a new account
func (ac *AccountContract) CreateAccount(APIstub shim.ChaincodeStubInterface, args []string) sc.Response {
accountLogger.Infof("invoke CreateAccount, args=%s\n", args)
if len(args) != 1 {
}
name := args[0] // 一つ目のパラメータが "name"
// 一意な口座番号を生成する(詳細はgithubのソースコードを参照)
no, err := utils.GetAccountNo(APIstub)
// Accountオブジェクトを生成してJSONバイト配列化
account := models.Account{
ModelType: types.AccountModel,
No: no,
Name: name,
Balance: 0,
}
jsonBytes, err := json.Marshal(account)
// Accountオブジェクトを保存
if err := APIstub.PutState(no, jsonBytes); err != nil {
}
// 作成したAccountオブジェクトのJSONバイト配列をレスポンスとして返す
return shim.Success(jsonBytes)
}
口座の取得(RetrieveAccount)
Accountオブジェクトは口座番号をキーとして台帳に記帳されていますので、shim.ChaincodeStubInterface.GetState
に口座番号を渡せばAccountオブジェクト(のバイト配列)が得られます。Accountオブジェクトは口座名の更新や口座削除、入送出金処理でも必要になりますので、関数化しています。
func GetAccount(APIstub shim.ChaincodeStubInterface, no string) (*models.Account, error) {
var account = new(models.Account) // 空のAccountオブジェクトを生成
accountBytes, err := APIstub.GetState(no)
if err != nil { // GetStateでエラーが発生した場合は、空のAccountオブジェクトと発生したerrを返す
return account, err
} else if accountBytes == nil { // Accountオブジェクトが無い場合は、Error interfaceを実装したWarningResultオブジェクトを返す(WarningResultの詳細はgithubのソースコードを参照)
msg := fmt.Sprintf("Account does not exist, no = %s", no)
warning := &WarningResult{StatusCode: 404, Message: msg}
return account, warning
}
if err := json.Unmarshal(accountBytes, account); err != nil {
return account, err // AccountオブジェクトをJSONバイト配列化できなかった場合はエラーを返す
}
return account, nil // 正常に取得できた場合、エラーをnilとしてAccountオブジェクトを返す
}
func (ac *AccountContract) RetrieveAccount(APIstub shim.ChaincodeStubInterface, args []string) sc.Response {
accountLogger.Infof("invoke RetrieveAccount, args=%s\n", args)
if len(args) != 1 {
}
no := args[0]
// 引数として渡された口座番号をキーにしてAccountオブジェクトをstateから取得
account, err := utils.GetAccount(APIstub, no)
jsonBytes, err := json.Marshal(account)
// 取得したAccountオブジェクトのJSONバイト配列をレスポンスとして返す
return shim.Success(jsonBytes)
}
口座名の更新(UpdateAccountName)
shim.ChaincodeStubInterface.GetState
に口座番号を指定してAccountオブジェクトを取得し、名前を変更した後にshim.ChaincodeStubInterface.PutState
すれば、口座名を変更できます。
// UpdateAccountName : update the name of an account
func (ac *AccountContract) UpdateAccountName(APIstub shim.ChaincodeStubInterface, args []string) sc.Response {
accountLogger.Infof("invoke UpdateAccountName, args=%s\n", args)
if len(args) != 2 {
}
no := args[0]
name := args[1]
// 引数として渡された口座番号をキーにしてAccountオブジェクトをstateから取得
account, err := utils.GetAccount(APIstub, no)
// 取得したAccountオブジェクトのnameを更新
account.Name = name
jsonBytes, err := json.Marshal(account)
// nameを変更したAccountオブジェクトを保存
if err := APIstub.PutState(no, jsonBytes); err != nil {
}
// 更新後のAccountオブジェクトのJSONバイト配列をレスポンスとして返す
return shim.Success(jsonBytes)
}
口座の削除(DeleteAccount)
口座番号を指定してshim.ChaincodeStubInterface.DelState
すれば、その口座を削除することができます。存在しない口座は削除できないので、DelStateする前にAccountオブジェクトの存在確認をしています。
// DeleteAccount : delete an account
func (ac *AccountContract) DeleteAccount(APIstub shim.ChaincodeStubInterface, args []string) sc.Response {
accountLogger.Infof("invoke DeleteAccount, args=%s\n", args)
if len(args) != 1 {
}
no := args[0]
// 引数として渡された口座番号をキーにしてAccountオブジェクトをstateから取得(取得したAccountオブジェクトは使わないため、_で受ける
_, err := utils.GetAccount(APIstub, no)
// 口座番号を指定して、保存されているオブジェクトをstate dbから削除
if err := APIstub.DelState(no); err != nil {
}
return shim.Success(nil)
}
入金・送金・出金
入金・出金・送金処理も、口座の操作と同様、GetQueryResultやGetState、PutStateを用いて記述することができます。
入金(Deposit)
入金先のAccountオブジェクトの残高更新と、入金イベントの記録を同時に行います。これら二つのオブジェクトの更新は、一つのトランザクションとして処理されます。
// EventContract : a struct which has the methods related to event
type EventContract struct {
}
// Deposit : deposit amount to an account
func (ec *EventContract) Deposit(APIstub shim.ChaincodeStubInterface, args []string) sc.Response {
eventLogger.Infof("invoke Deposit, args=%s\n", args)
if len(args) != 2 {
}
toAccountNo := args[0] // 一つ目の引数は、入金先の口座番号
amountStr := args[1] // 二つ目の引数は、入金額(の文字列)
// 入金額をvalidate(0より大きい整数であること)
amount, err := utils.GetAmount(amountStr)
// 入金先のAccountオブジェクトを取得
toAccount, err := utils.GetAccount(APIstub, toAccountNo)
// 一意なイベント番号を生成
eventNo, err := utils.GetEventNo(APIstub)
// 入金前の残高と入金後の残高を計算
toAccountPreviousBalance := toAccount.Balance
toAccount.Balance += amount
// 残高の変化を記録したAccountStateオブジェクトを作成
toAccountState := &models.AccountState{
No: toAccount.No,
Name: toAccount.Name,
PreviousBalance: toAccountPreviousBalance,
CurrentBalance: toAccount.Balance,
}
// 入金イベントを記録したEventオブジェクトを作成
event := &models.Event{
ModelType: types.EventModel,
EventType: types.DepositEvent,
No: eventNo,
Amount: amount,
FromAccountState: nil,
ToAccountState: toAccountState,
}
// 残高更新後のAccountオブジェクトを保存
toAccountBytes, err := json.Marshal(toAccount)
if err := APIstub.PutState(toAccount.No, toAccountBytes); err != nil {
}
// 入金Eventオブジェクトを保存
eventBytes, err := json.Marshal(event)
if err := APIstub.PutState(event.No, eventBytes); err != nil {
}
return shim.Success(eventBytes)
}
送金(Remit)
送金元と送金先のAccountオブジェクトの残高更新と、送金イベントの記録を同時に行います。送金元の残高が0より小さくなる場合はエラーを発生させます。
// Remit : remit amount from an account to another account
func (ec *EventContract) Remit(APIstub shim.ChaincodeStubInterface, args []string) sc.Response {
eventLogger.Infof("invoke Remit, args=%s\n", args)
if len(args) != 3 {
}
fromAccountNo := args[0]
toAccountNo := args[1]
amountStr := args[2]
// 送金額をvalidate(0より大きい整数であること)
amount, err := utils.GetAmount(amountStr)
// 送金元のAccountオブジェクトを取得
fromAccount, err := utils.GetAccount(APIstub, fromAccountNo)
// 送金先のAccountオブジェクトを取得
toAccount, err := utils.GetAccount(APIstub, toAccountNo)
if fromAccount.Balance < amount {
// 残高不足の場合はエラー発生
}
// 一意なイベント番号を生成
eventNo, err := utils.GetEventNo(APIstub)
// 送金元の残高から送金額を減額
fromAccountPreviousBalance := fromAccount.Balance
fromAccount.Balance -= amount
// 送金先の残高へ送金額を増額
toAccountPreviousBalance := toAccount.Balance
toAccount.Balance += amount
// 送金元の残高の変化を記録したAccountStateオブジェクトを作成
fromAccountState := &models.AccountState{
No: fromAccount.No,
Name: fromAccount.Name,
PreviousBalance: fromAccountPreviousBalance,
CurrentBalance: fromAccount.Balance,
}
// 送金先の残高の変化を記録したAccountStateオブジェクトを作成
toAccountState := &models.AccountState{
No: toAccount.No,
Name: toAccount.Name,
PreviousBalance: toAccountPreviousBalance,
CurrentBalance: toAccount.Balance,
}
// 送金Eventオブジェクトを作成
event := &models.Event{
ModelType: types.EventModel,
EventType: types.RemitEvent,
No: eventNo,
Amount: amount,
FromAccountState: fromAccountState,
ToAccountState: toAccountState,
}
// 残高更新後のAccountオブジェクトを保存
fromAccountBytes, err := json.Marshal(fromAccount)
if err := APIstub.PutState(fromAccount.No, fromAccountBytes); err != nil {
}
toAccountBytes, err := json.Marshal(toAccount)
if err := APIstub.PutState(toAccount.No, toAccountBytes); err != nil {
}
// 送金Eventオブジェクトを保存
eventBytes, err := json.Marshal(event)
if err := APIstub.PutState(event.No, eventBytes); err != nil {
}
return shim.Success(eventBytes)
}
出金(Withdraw)
出金元のAccountオブジェクトの残高更新と、出金イベントを記録します。送金と同様、出金元の残高が0より小さくなる場合はエラーを発生させます。
// Withdraw : withdraw amount from an account
func (ec *EventContract) Withdraw(APIstub shim.ChaincodeStubInterface, args []string) sc.Response {
eventLogger.Infof("invoke Withdraw, args=%s\n", args)
if len(args) != 2 {
}
fromAccountNo := args[0]
amountStr := args[1]
// 出金額をvalidate(0より大きい整数であること)
amount, err := utils.GetAmount(amountStr)
// 出金元のAccountオブジェクトを取得
fromAccount, err := utils.GetAccount(APIstub, fromAccountNo)
if fromAccount.Balance < amount {
// 残高不足の場合はエラー発生
}
// 一意なイベント番号を生成
eventNo, err := utils.GetEventNo(APIstub)
// 出金後の残高を計算
fromAccountPreviousBalance := fromAccount.Balance
fromAccount.Balance -= amount
// 出金元の残高の変化を記録したAccountStateオブジェクトを作成
fromAccountState := &models.AccountState{
No: fromAccount.No,
Name: fromAccount.Name,
PreviousBalance: fromAccountPreviousBalance,
CurrentBalance: fromAccount.Balance,
}
// 出金Eventオブジェクトを作成
event := &models.Event{
ModelType: types.EventModel,
EventType: types.WithdrawEvent,
No: eventNo,
Amount: amount,
FromAccountState: fromAccountState,
ToAccountState: nil,
}
// 残高更新後のAccountオブジェクトを保存
fromAccountBytes, err := json.Marshal(fromAccount)
if err := APIstub.PutState(fromAccount.No, fromAccountBytes); err != nil {
}
// 出金Eventオブジェクトを保存
eventBytes, err := json.Marshal(event)
if err := APIstub.PutState(event.No, eventBytes); err != nil {
}
return shim.Success(eventBytes)
}
入金・送金・出金の一覧取得
口座一覧の取得と同様、shim.ChaincodeStubInterface.GetQueryResult
を呼び出すことでstateから最新のイベント(入金・送金・出金)を一覧で取得します。また引数として"deposit|remit|withdraw"のいずれかが引き渡されている場合は、それらのイベントに絞り込んで検索することもできるようにしています。
// ListEvent : return a list of all events
func (ec *EventContract) ListEvent(APIstub shim.ChaincodeStubInterface, args []string) sc.Response {
eventLogger.Infof("invoke ListEvent, args=%s\n", args)
if len(args) > 1 {
}
// "model_type"として"event"を指定したクエリを生成
query := map[string]interface{}{
"selector": map[string]interface{}{
"model_type": types.EventModel,
},
}
// 一つ目の引数が渡されている場合、"event_type"として"deposit" or "remit" or "withdraw"をクエリに追加指定
if len(args) == 1 {
switch args[0] {
case types.DepositEvent.String():
query["selector"].(map[string]interface{})["event_type"] = types.DepositEvent
case types.RemitEvent.String():
query["selector"].(map[string]interface{})["event_type"] = types.RemitEvent
case types.WithdrawEvent.String():
query["selector"].(map[string]interface{})["event_type"] = types.WithdrawEvent
default:
// "deposit", "remit", "withdraw" 以外の引数はエラー
}
}
// クエリJSONのバイト配列を生成
queryBytes, err := json.Marshal(query)
eventLogger.Infof("Query string = '%s'", string(queryBytes))
// クエリを用いてstateから最新のEvent一覧を取得するIteratorを生成
resultsIterator, err := APIstub.GetQueryResult(string(queryBytes))
// 取得したIteratorは、メソッド離脱時にCloseする
defer resultsIterator.Close()
// stateから取得したJSONを*Event型にキャストしてsliceに追加
results := make([]*models.Event, 0)
for resultsIterator.HasNext() {
queryResponse, err := resultsIterator.Next()
event := new(models.Event)
if err := json.Unmarshal(queryResponse.Value, event); err != nil {
}
results = append(results, event)
}
jsonBytes, err := json.Marshal(results)
return shim.Success(jsonBytes)
}
更新履歴の取得
Hyperledter/fabricは台帳に記帳されたデータの更新情報を全て記録していますので、更新履歴を取得することも容易です。キーを指定して更新履歴を取得するshim.ChaincodeStubInterface.GetHistoryForKey
というAPIが提供されてます。
// HistoryContract : a struct which has the methods related to query Histories
type HistoryContract struct {
}
// ListHistory : return all histories of a contract
func (hc *HistoryContract) ListHistory(APIstub shim.ChaincodeStubInterface, args []string) sc.Response {
historyLogger.Infof("invoke ListHistory, args=%s\n", args)
if len(args) != 1 {
}
no := args[0] // 引数としてキー(口座番号かイベント番号)を渡す
// 指定したキーの更新履歴のIteratorを取得する
resultsIterator, err := APIstub.GetHistoryForKey(no)
// 取得したIteratorは、メソッド離脱時にCloseする
defer resultsIterator.Close()
// 指定した更新履歴をhistoryType型にキャストしてsliceに追加
histories := make([]*historyType, 0)
for resultsIterator.HasNext() {
response, err := resultsIterator.Next()
// 指定したキーが口座であれイベントであれ同じように扱えるように、Account型やEvent型ではなく、どのようなJSONでも受け付けることのできる map[string]interface{} 型を宣言する
var state map[string]interface{}
// 削除済みのオブジェクトでは無いならば、response.valueとして記帳時点のオブジェクトの状態がJSONバイト配列として得られる
if !response.IsDelete {
if err := json.Unmarshal(response.Value, &state); err != nil {
historyLogger.Error(err.Error())
return shim.Error(err.Error())
}
}
// 変更履歴オブジェクトを生成
history := &historyType{
TxID: response.TxId, // Hyperledger/fabricが生成したトランザクションID
No: no, // 口座番号かイベント番号
State: state, // 記帳された時点でのオブジェクトの状態
Timestamp: time.Unix(response.Timestamp.Seconds, int64(response.Timestamp.Nanos)).String(), // トランザクションがコミットされた日時
IsDelete: response.IsDelete, // 削除済みフラグ
}
// 変更履歴オブジェクトをsliceに追加
histories = append(histories, history)
}
jsonBytes, err := json.Marshal(histories)
return shim.Success(jsonBytes)
}
エントリポイントの更新
口座の操作や入送出金処理が実装できたら、cliやSDKから呼び出せるようにInvokeメソッドを更新します。
var accountContract = new(contracts.AccountContract)
var eventContract = new(contracts.EventContract)
var historyContract = new(contracts.HistoryContract)
func (s *EntryPoint) Invoke(APIstub shim.ChaincodeStubInterface) sc.Response {
function, args := APIstub.GetFunctionAndParameters()
switch function {
case "listAccount":
return accountContract.ListAccount(APIstub, args)
case "createAccount":
return accountContract.CreateAccount(APIstub, args)
case "retrieveAccount":
return accountContract.RetrieveAccount(APIstub, args)
case "updateAccountName":
return accountContract.UpdateAccountName(APIstub, args)
case "deleteAccount":
return accountContract.DeleteAccount(APIstub, args)
case "listEvent":
return eventContract.ListEvent(APIstub, args)
case "deposit":
return eventContract.Deposit(APIstub, args)
case "remit":
return eventContract.Remit(APIstub, args)
case "withdraw":
return eventContract.Withdraw(APIstub, args)
case "listHistory":
return historyContract.ListHistory(APIstub, args)
}
msg := fmt.Sprintf("No such function. function = %s, args = %s", function, args)
logger.Error(msg)
return shim.Error(msg)
}
couchdbのインデックス作成
クエリを用いてオブジェクトを取得するshim.ChaincodeStubInterface.GetQueryResult
は柔軟に利用できて便利ですが、デフォルトの状態だと全オブジェクトを探索してしまいます。Hyperledger 1.1からは、couchdbにindexを作成させることができるようになりました。またindexを活用すると、検索結果のsortも指定することができます。(CouchDB as the State Database)
クエリを用いる場合は、indexを上手く活用しましょう。
実行されるクエリ文字列を確認する
chaincodeコンテナのログを確認すると、どのようなクエリ文字列が利用されたのかがわかります。
例えば口座一覧を取得した場合は{"selector":{"model_type":"account"}}
、入金イベント一覧を取得した場合は{"selector":{"event_type":"deposit","model_type":"event"}}
のようなクエリ文字列が利用されていることがわかります。
ubuntu@node0:~$ docker logs -f $(docker ps -q -f name=dev-peer)
...
2018-03-04 03:16:50.976 UTC [contracts/account] Infof -> INFO 004 Query string = '{"selector":{"model_type":"account"}}'
...
2018-03-04 03:19:44.746 UTC [contracts/event] Infof -> INFO 012 Query string = '{"selector":{"event_type":"deposit","model_type":"event"}}'
発行されるクエリ文字列にマッチするように、indexを定義します。
indexを定義する
chaincodeに META-INF/statedb/couchdb/indexes/*.json
というJSONファイルが含まれていると、couchdbのindexを自動生成します。
例えばmodel_type
を指定したクエリに対するindexとしては、次のようなJSONになります。
{
"index": {
"fields": ["model_type"]
},
"ddoc": "modelIndexDoc",
"name":"modelIndex",
"type":"json"
}
index定義JSONのフォーマットは、couchdbのマニュアルを参照してください。
クエリ文字列のパターンを見ると、これ以外にもmodel_type
とevent_type
を双方指定したクエリに対するindexも必要なことがわかります。
couchdbのDBを確認する
チャネルにchaincodeをinstallしてinstantiateすると、couchdbに <<CHANNEL_NAME>>_<<CHAINCODE_NAME>>
というstateを保持するためのDBが作成されます。今回のサンプルでは、 fabric-sample_fabric-payment
がstate DBになります。
ubuntu@node0:~$ docker exec cli curl http://localhost:5984/_all_dbs/ | jq .
[
"_global_changes",
"_replicator",
"_users",
"fabric-sample_",
"fabric-sample_fabric-payment",
"fabric-sample_lscc"
]
設定したindexを確認する
上記で確認したstate DBに対して /_index/
をGETすると、state DBにどのようなindexが設定されているか確認できます。model_type
を指定したクエリに対するindexと、model_type
及びevent_type
を指定したクエリに対するindexが定義されていることがわかります。
ubuntu@node0:~$ docker exec cli curl http://couchdb0:5984/fabric-sample_fabric-payment/_index/ | jq .
{
"total_rows": 3,
"indexes": [
{
"ddoc": null,
"name": "_all_docs",
"type": "special",
"def": {
"fields": [
{
"_id": "asc"
}
]
}
},
{
"ddoc": "_design/modelEventIndexDoc",
"name": "modelEventIndex",
"type": "json",
"def": {
"fields": [
{
"model_type": "asc"
},
{
"event_type": "asc"
}
],
"partial_filter_selector": {}
}
},
{
"ddoc": "_design/modelIndexDoc",
"name": "modelIndex",
"type": "json",
"def": {
"fields": [
{
"model_type": "asc"
}
],
"partial_filter_selector": {}
}
}
]
}
indexが利用されることを確認する
couchdbのDBに対して /_explain/
にクエリ文字列を与えてPOSTすると、そのクエリが発行された場合にどのようなindexが利用されるか、実行計画を確認することができます。設定したindexが利用されていることを確認しましょう。
ubuntu@node0:~$ docker exec cli curl -H "Content-Type: application/json" http://couchdb0:5984/fabric-sample_fabric-payment/_explain/ -X POST -d '{"selector":{"model_type":"account"}}' | jq .
{
"dbname": "fabric-sample_fabric-payment",
"index": {
"ddoc": "_design/modelIndexDoc",
"name": "modelIndex",
"type": "json",
"def": {
"fields": [
{
"model_type": "asc"
}
],
"partial_filter_selector": {}
}
},
"selector": {
"model_type": {
"$eq": "account"
}
},
...
ubuntu@node0:~$ docker exec cli curl -H "Content-Type: application/json" http://couchdb0:5984/fabric-sample_fabric-payment/_explain/ -X POST -d '{"selector":{"event_type":"deposit","model_type":"event"}}' | jq .
{
"dbname": "fabric-sample_fabric-payment",
"index": {
"ddoc": "_design/modelEventIndexDoc",
"name": "modelEventIndex",
"type": "json",
"def": {
"fields": [
{
"model_type": "asc"
},
{
"event_type": "asc"
}
],
"partial_filter_selector": {}
}
},
"selector": {
"$and": [
{
"event_type": {
"$eq": "deposit"
}
},
{
"model_type": {
"$eq": "event"
}
}
]
},
...
chaincodeの実行
お疲れ様でした。これで一通りchaincodeが完成です。作成したchaincodeを実行してみましょう。第一回で構築した、単体のdocker環境を用います。
ただし、REST APIからSDK経由で呼び出すのではなく、cliコンテナから直接chaincodeを起動します。
開発用dockerコンテナを起動
-
fabric-payment-sample-docker/dev-solo/ に移動
ubuntu@node0:~$ cd fabric-payment-sample-docker/dev-solo/
-
各コンポーネントの暗号鍵やアーティファクトを生成する
ubuntu@node0:~/fabric-payment-sample-docker/dev-solo$ source .env ubuntu@node0:~/fabric-payment-sample-docker/dev-solo$ export CA_ADMIN_PASSWORD=<<任意の文字列>> ubuntu@node0:~/fabric-payment-sample-docker/dev-solo$ ./generate_artifact.sh ${CA_ADMIN_PASSWORD}
1. 環境変数を設定する
```bash
ORDERER_ADDRESS="orderer:7050"
PEER_ADDRESS="peer:7051"
LOCALMSPID="Org1MSP"
PEER_MSPCONFIGPATH="/etc/hyperledger/crypto-config/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp"
CHAINCODE_SRC="github.com/nmatsui/fabric-payment-sample-chaincode"
CHAINCODE_VERSION="0.1"
```
* `fabric-payment-sample-chaincode`以外の名前にした場合、CHAINCODE_SRCを適切に設定してください。
1. docker内部にbridge networkを作成する
```bash
docker network create --driver=bridge --attachable=true fabric-sample-nw
```
* docker-compose.yamlでは、`fabric-sample-nw`というネットワークにコンテナを接続します。ネットワーク名を変更した場合はdocker-compose.yamlも修正してください。
1. docker-composeを用いてコンテナ群を起動する
```bash
ubuntu@node0:~/fabric-payment-sample-docker/dev-solo$ docker-compose -f docker-compose.yaml up -d
```
* peer, couchdb, orderer, cliの各コンテナが起動していることを確認
```bash
ubuntu@node0:~/fabric-payment-sample-docker/dev-solo$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
41b3370d272c fabric-payment/api "npm start" 37 seconds ago Up 35 seconds 0.0.0.0:3000->3000/tcp api
070fe3e78f64 hyperledger/fabric-tools "/bin/bash" 37 seconds ago Up 36 seconds cli
23efcf53f8c1 hyperledger/fabric-peer "peer node start" 38 seconds ago Up 36 seconds 0.0.0.0:32772->7051/tcp, 0.0.0.0:32771->7053/tcp peer
b13b50dcffc8 hyperledger/fabric-couchdb "tini -- /docker-ent…" 38 seconds ago Up 37 seconds 4369/tcp, 9100/tcp, 0.0.0.0:32769->5984/tcp couchdb
bf9770e74b1e hyperledger/fabric-orderer "orderer" 38 seconds ago Up 37 seconds 0.0.0.0:32770->7050/tcp orderer
85d6c89d3596 hyperledger/fabric-ca "sh -c 'fabric-ca-se…" 38 seconds ago Up 36 seconds 0.0.0.0:32768->7054/tcp ca
-
cliコンテナからチャネルを作成する
ubuntu@node0:~/fabric-payment-sample-docker/dev-solo$ docker exec -e "CORE_PEER_LOCALMSPID=${LOCALMSPID}" -e "CORE_PEER_MSPCONFIGPATH=${PEER_MSPCONFIGPATH}" cli peer channel create -o ${ORDERER_ADDRESS} -c ${CHANNEL_NAME} -f /etc/hyperledger/artifacts/channel.tx
-
peerをチャネルにjoinさせる
ubuntu@node0:~/fabric-payment-sample-docker/dev-solo$ docker exec -e "CORE_PEER_LOCALMSPID=${LOCALMSPID}" -e "CORE_PEER_MSPCONFIGPATH=${PEER_MSPCONFIGPATH}" -e "CORE_PEER_ADDRESS=${PEER_ADDRESS}" cli peer channel join -b /etc/hyperledger/artifacts/${CHANNEL_NAME}.block
-
anchorを更新する
ubuntu@node0:~/fabric-payment-sample-docker/dev-solo$ docker exec -e "CORE_PEER_LOCALMSPID=${LOCALMSPID}" -e "CORE_PEER_MSPCONFIGPATH=${PEER_MSPCONFIGPATH}" -e "CORE_PEER_ADDRESS=${PEER_ADDRESS}" cli peer channel update -o ${ORDERER_ADDRESS} -c ${CHANNEL_NAME} -f /etc/hyperledger/artifacts/Org1MSPanchors.tx
chaincodeの初回インストール
-
作成したchaincodeをインストールする
ubuntu@node0:~/fabric-payment-sample-docker/dev-solo$ docker exec -e "CORE_PEER_LOCALMSPID=${LOCALMSPID}" -e "CORE_PEER_MSPCONFIGPATH=${PEER_MSPCONFIGPATH}" -e "CORE_PEER_ADDRESS=${PEER_ADDRESS}" cli peer chaincode install -n ${CHAINCODE_NAME} -p ${CHAINCODE_SRC} -v ${CHAINCODE_VERSION}
-
chaincodeをinstantiateする
-
-c '{"Args":[""]}'
にて渡す文字列は、chaincodeのInit
メソッドの引数となります。今回のchaincodeのInitメソッドは何の引数も取らないため、何も渡しません。
ubuntu@node0:~/fabric-payment-sample-docker/dev-solo$ docker exec -e "CORE_PEER_LOCALMSPID=${LOCALMSPID}" -e "CORE_PEER_MSPCONFIGPATH=${PEER_MSPCONFIGPATH}" -e "CORE_PEER_ADDRESS=${PEER_ADDRESS}" cli peer chaincode instantiate -o ${ORDERER_ADDRESS} -C ${CHANNEL_NAME} -n ${CHAINCODE_NAME} -v ${CHAINCODE_VERSION} -c '{"Args":[""]}' -P "OR ('Org1MSP.member')"
- instantiateに成功すると、
dev-<<peerのID>>-<<CHAINCODE_NAME>>-バージョン番号-...
というchaincodeコンテナが自動起動しているはずです。
ubuntu@node0:~/fabric-payment-sample-docker/dev-solo$ docker ps
-
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
6e4dd2c7baff dev-peer0.org1.example.com-fabric-payment-0.1-5937be8eee46b8bca564d23f7316cb6fd253fb536b7aece7268323db087b8073 "chaincode -peer.add…" 2 minutes ago Up 2 minutes dev-peer0.org1.example.com-fabric-payment-0.1
41b3370d272c fabric-payment/api "npm start" 12 minutes ago Up 12 minutes 0.0.0.0:3000->3000/tcp api
070fe3e78f64 hyperledger/fabric-tools "/bin/bash" 12 minutes ago Up 12 minutes cli
23efcf53f8c1 hyperledger/fabric-peer "peer node start" 12 minutes ago Up 12 minutes 0.0.0.0:32772->7051/tcp, 0.0.0.0:32771->7053/tcp peer
b13b50dcffc8 hyperledger/fabric-couchdb "tini -- /docker-ent…" 12 minutes ago Up 12 minutes 4369/tcp, 9100/tcp, 0.0.0.0:32769->5984/tcp couchdb
bf9770e74b1e hyperledger/fabric-orderer "orderer" 12 minutes ago Up 12 minutes 0.0.0.0:32770->7050/tcp orderer
85d6c89d3596 hyperledger/fabric-ca "sh -c 'fabric-ca-se…" 12 minutes ago Up 12 minutes 0.0.0.0:32768->7054/tcp ca
```
cliからchaincodeを実行(検索)
ListAccountやRetrieveAccountなど、台帳の状態を更新しないchaincodeを実行する場合、cliコンテナでpeer chaincode query
というコマンドを実行します。
-c '{"Args":["listAccount"]}'
の一つ目の引数は実際の処理を委譲する関数のラベルで、二つ目以降は関数に引数として渡されます。
func (s *EntryPoint) Invoke(APIstub shim.ChaincodeStubInterface) sc.Response {
function, args := APIstub.GetFunctionAndParameters() // 一つ目の引数がfunction、それ以降がargsに代入される
switch function {
case "listAccount":
return accountContract.ListAccount(APIstub, args) // 一つ目の引数が "listAccount" だった場合、ListAccountメソッドに処理を委譲する
...
}
ubuntu@node0:~/fabric-payment-sample-docker/dev-solo$ docker exec -e "CORE_PEER_LOCALMSPID=${LOCALMSPID}" -e "CORE_PEER_MSPCONFIGPATH=${PEER_MSPCONFIGPATH}" -e "CORE_PEER_ADDRESS=${PEER_ADDRESS}" cli peer chaincode query -C ${CHANNEL_NAME} -n ${CHAINCODE_NAME} -c '{"Args":["listAccount"]}'
2018-03-04 07:51:50.577 UTC [msp] GetLocalMSP -> DEBU 001 Returning existing local MSP
2018-03-04 07:51:50.578 UTC [msp] GetDefaultSigningIdentity -> DEBU 002 Obtaining default signing identity
2018-03-04 07:51:50.578 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 003 Using default escc
2018-03-04 07:51:50.578 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 004 Using default vscc
2018-03-04 07:51:50.578 UTC [chaincodeCmd] getChaincodeSpec -> DEBU 005 java chaincode disabled
2018-03-04 07:51:50.578 UTC [msp/identity] Sign -> DEBU 006 Sign: plaintext: 0AB9070A7508031A0C0896B4FED40510...741A0D0A0B6C6973744163636F756E74
2018-03-04 07:51:50.578 UTC [msp/identity] Sign -> DEBU 007 Sign: digest: 5AB6FA2458A4B4B88C0D50A9FABBBEE4B085D23F93F6662F94F021378493BDA3
2018-03-04 07:51:50.678 UTC [main] main -> INFO 008 Exiting.....
Query Result: []
まだ台帳に口座を登録していないため、結果は空配列です。
cliからchaincodeを実行(更新)
CreateAccountやDepositなど、台帳の状態を更新する処理は peer chaincode invoke
コマンドを用います。invokeの場合、トランザクションのコミット処理をordererに依頼しなければならないため、依頼先のordererのアドレスを引数として渡す必要があります。
ubuntu@node0:~/fabric-payment-sample-docker/dev-solo$ docker exec -e "CORE_PEER_LOCALMSPID=${LOCALMSPID}" -e "CORE_PEER_MSPCONFIGPATH=${PEER_MSPCONFIGPATH}" -e "CORE_PEER_ADDRESS=${PEER_ADDRESS}" cli peer chaincode invoke -o ${ORDERER_ADDRESS} -C ${CHANNEL_NAME} -n ${CHAINCODE_NAME} -c '{"Args":["createAccount", " ほげほげ"]}'
2018-03-04 08:03:04.194 UTC [msp] GetLocalMSP -> DEBU 001 Returning existing local MSP
2018-03-04 08:03:04.194 UTC [msp] GetDefaultSigningIdentity -> DEBU 002 Obtaining default signing identity
2018-03-04 08:03:04.196 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 003 Using default escc
2018-03-04 08:03:04.197 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 004 Using default vscc
2018-03-04 08:03:04.197 UTC [chaincodeCmd] getChaincodeSpec -> DEBU 005 java chaincode disabled
2018-03-04 08:03:04.198 UTC [msp/identity] Sign -> DEBU 006 Sign: plaintext: 0AB8070A7408031A0B08B8B9FED40510...6E740A0CE381BBE38192E381BBE38192
2018-03-04 08:03:04.198 UTC [msp/identity] Sign -> DEBU 007 Sign: digest: E3951FEBD9BCAA12081D106639FB9F1EA3B9D00FD00B06808F3CF195C08B5DF8
2018-03-04 08:03:04.214 UTC [msp/identity] Sign -> DEBU 008 Sign: plaintext: 0AB8070A7408031A0B08B8B9FED40510...42E66439C107E3EAD2B99A6ED0E8BE91
2018-03-04 08:03:04.215 UTC [msp/identity] Sign -> DEBU 009 Sign: digest: 30E4C37CA4F49674731E8F2C119D7136F9D3AC98F2292337D4F06F9682E1AEEA
2018-03-04 08:03:04.220 UTC [chaincodeCmd] chaincodeInvokeOrQuery -> DEBU 00a ESCC invoke result: version:1 response:<status:200 message:"OK" payload:"{\"model_type\":\"account\",\"no\":\"3391863708800601\",\"name\":\"\343\201\273\343\201\222\343\201\273\343\201\222\",\"balance\":0}" > payload:"\n BZG\005\236N2\020\000~N\336\215H\255.\230f$\356Dm\251|\230\220\t \355\232\376X\022\244\002\n\261\001\022\216\001\n\016fabric-payment\022|\n\022\n\0203391863708800601\032f\n\0203391863708800601\032R{\"model_type\":\"account\",\"no\":\"3391863708800601\",\"name\":\"\343\201\273\343\201\222\343\201\273\343\201\222\",\"balance\":0}\022\036\n\004lscc\022\026\n\024\n\016fabric-payment\022\002\010\002\032W\010\310\001\032R{\"model_type\":\"account\",\"no\":\"3391863708800601\",\"name\":\"\343\201\273\343\201\222\343\201\273\343\201\222\",\"balance\":0}\"\025\022\016fabric-payment\032\0030.1" endorsement:<endorser:"\n\007Org1MSP\022\226\006-----BEGIN CERTIFICATE-----\nMIICGTCCAcCgAwIBAgIRAMytpI0Cnu3FKTpKDIAe3xIwCgYIKoZIzj0EAwIwczEL\nMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNhbiBG\ncmFuY2lzY28xGTAXBgNVBAoTEG9yZzEuZXhhbXBsZS5jb20xHDAaBgNVBAMTE2Nh\nLm9yZzEuZXhhbXBsZS5jb20wHhcNMTgwMzA3MDcyMzM2WhcNMjgwMzA0MDcyMzM2\nWjBbMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMN\nU2FuIEZyYW5jaXNjbzEfMB0GA1UEAxMWcGVlcjAub3JnMS5leGFtcGxlLmNvbTBZ\nMBMGByqGSM49AgEGCCqGSM49AwEHA0IABDzdWDN8BOKdV8fC21wqvR+JmA6jwnai\n7quHCeynh2XZmFDFXE9aAeXDagKe94Amhs3epx6J8Qp8NGKC8gMbm+ejTTBLMA4G\nA1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAAMCsGA1UdIwQkMCKAIN/NM15GEaD1\n/7sk/O/LETlo5/vsn0/9v1o80vvtqsZCMAoGCCqGSM49BAMCA0cAMEQCIGLVBC25\nBGy5ik8w6pCy7egFiOk5q7/5B0DNvB5ZMjrgAiA/cAA4fe4sqzYBmfR1M0sVbSG5\nZbIaHX1pHlNydQ7HRg==\n-----END CERTIFICATE-----\n" signature:"0E\002!\000\361\007\251\034\327\220g\340~m`gb1\252d\000\014\314\303\036\220\t\275Am8\207lF\255^\002 \013\353\221\026,e\365>\335_\376\254\\\034\272\266B\346d9\301\007\343\352\322\271\232n\320\350\276\221" >
2018-03-04 08:03:04.221 UTC [chaincodeCmd] chaincodeInvokeOrQuery -> INFO 00b Chaincode invoke successful. result: status:200 payload:"{\"model_type\":\"account\",\"no\":\"3391863708800601\",\"name\":\"\343\201\273\343\201\222\343\201\273\343\201\222\",\"balance\":0}"
2018-03-07 08:03:04.221 UTC [main] main -> INFO 00c Exiting.....
peer chaincode invoke
コマンドはすぐに結果が戻ってきますが、この時点ではまだpeerにproposalを投げただけです。実際にトランザクションがコミットされるまでには時間がかかりますので、 peer chaincode invoke
した直後にpeer chaincode query
を実行した場合、更新前のデータが表示される場合があります。RDB的にはMVCCのような挙動です。
コミットされたトランザクションは、queryすると取得することができます。
ubuntu@node0:~/fabric-payment-sample-docker/dev-solo$ docker exec -e "CORE_PEER_LOCALMSPID=${LOCALMSPID}" -e "CORE_PEER_MSPCONFIGPATH=${PEER_MSPCONFIGPATH}" -e "CORE_PEER_ADDRESS=${PEER_ADDRESS}" cli peer chaincode query -C ${CHANNEL_NAME} -n ${CHAINCODE_NAME} -c '{"Args":["listAccount"]}'
2018-03-04 08:04:18.109 UTC [msp] GetLocalMSP -> DEBU 001 Returning existing local MSP
2018-03-04 08:04:18.109 UTC [msp] GetDefaultSigningIdentity -> DEBU 002 Obtaining default signing identity
2018-03-04 08:04:18.109 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 003 Using default escc
2018-03-04 08:04:18.109 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 004 Using default vscc
2018-03-04 08:04:18.109 UTC [chaincodeCmd] getChaincodeSpec -> DEBU 005 java chaincode disabled
2018-03-04 08:04:18.110 UTC [msp/identity] Sign -> DEBU 006 Sign: plaintext: 0AB8070A7408031A0B0882BAFED40510...741A0D0A0B6C6973744163636F756E74
2018-03-04 08:04:18.110 UTC [msp/identity] Sign -> DEBU 007 Sign: digest: C0B00291DDB568DCB73CE29665073AFC43667F38E56B692EB285D7D5E074091F
Query Result: [{"model_type":"account","no":"3391863708800601","name":"ほげほげ","balance":0}]
chaincodeの修正
同じチャネルに同じ名前のchaincodeを再度instantiateすることはできません。Hyperledger/fabric 1.1の時点では、chaincodeをuninstallするコマンドも提供されていないようです(peer chaincdeコマンドのマニュアル)
登録済みのchaincodeのupgradeをすることは可能ですので、chaincodeを修正してupgradeしてみましょう。
-
chaincodeの修正
- とりあえずListAccountを起動すると、問答無用でエラーが発生するように変更する
${GOPATH}/src/github.com/nmatsui/fabric-payment-sample-chaincode/contracts/account.go// ListAccount : return a list of all accounts func (ac *AccountContract) ListAccount(APIstub shim.ChaincodeStubInterface, args []string) sc.Response { accountLogger.Error("ERROR!!!!") return shim.Error("ERROR!!!!") ... }
-
ローカルでビルドが成功することを確認
ubuntu@node0:~/go/src/github.com/nmatsui/fabric-payment-sample-chaincode$ go build --tags nopkcs11 fabric-payment.go
-
バージョンを上げてchaincodeをインストール
ubuntu@node0:~/go/src/github.com/nmatsui/fabric-payment-sample-chaincode$ CHAINCODE_VERSION="0.1.1" ubuntu@node0:~/go/src/github.com/nmatsui/fabric-payment-sample-chaincode$ docker exec -e "CORE_PEER_LOCALMSPID=${LOCALMSPID}" -e "CORE_PEER_MSPCONFIGPATH=${PEER_MSPCONFIGPATH}" -e "CORE_PEER_ADDRESS=${PEER_ADDRESS}" cli peer chaincode install -n ${CHAINCODE_NAME} -p ${CHAINCODE_SRC} -v ${CHAINCODE_VERSION}
-
-v
は内部的には単なる文字列として扱われているようなので、インストール済みのchaincodeのバージョンと重複しなければどのような文字列でもかまわないと思います。日時とかでも良いかもしれません。
-
-
インストールしたバージョン番号を指定してchaincodeをupgrade
-
peer chaincode upgrade
コマンドを用いてchaincodeをupgradeします。
ubuntu@node0:~/go/src/github.com/nmatsui/fabric-payment-sample-chaincode$ docker exec -e "CORE_PEER_LOCALMSPID=${LOCALMSPID}" -e "CORE_PEER_MSPCONFIGPATH=${PEER_MSPCONFIGPATH}" -e "CORE_PEER_ADDRESS=${PEER_ADDRESS}" cli peer chaincode upgrade -o ${ORDERER_ADDRESS} -C ${CHANNEL_NAME} -n ${CHAINCODE_NAME} -v ${CHAINCODE_VERSION} -c '{"Args":[""]}' -P "OR ('Org1MSP.member')"
- upgradeに成功すると、新たなバージョン番号でchaincodeコンテナが起動しているはずです。
ubuntu@node0:~/go/src/github.com/nmatsui/fabric-payment-sample-chaincode$ docker ps
-
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
d4aeb1cade49 dev-peer0.org1.example.com-fabric-payment-0.1.1-874aea1ca4ce200d18b066ef104cb747da2d4780a3a96664ef8c724d21c25ec5 "chaincode -peer.add…" About a minute ago Up About a minute dev-peer0.org1.example.com-fabric-payment-0.1.1
6e4dd2c7baff dev-peer0.org1.example.com-fabric-payment-0.1-5937be8eee46b8bca564d23f7316cb6fd253fb536b7aece7268323db087b8073 "chaincode -peer.add…" 44 minutes ago Up 44 minutes dev-peer0.org1.example.com-fabric-payment-0.1
41b3370d272c fabric-payment/api "npm start" About an hour ago Up About an hour 0.0.0.0:3000->3000/tcp api
070fe3e78f64 hyperledger/fabric-tools "/bin/bash" About an hour ago Up About an hour cli
23efcf53f8c1 hyperledger/fabric-peer "peer node start" About an hour ago Up About an hour 0.0.0.0:32772->7051/tcp, 0.0.0.0:32771->7053/tcp peer
b13b50dcffc8 hyperledger/fabric-couchdb "tini -- /docker-ent…" About an hour ago Up About an hour 4369/tcp, 9100/tcp, 0.0.0.0:32769->5984/tcp couchdb
bf9770e74b1e hyperledger/fabric-orderer "orderer" About an hour ago Up About an hour 0.0.0.0:32770->7050/tcp orderer
85d6c89d3596 hyperledger/fabric-ca "sh -c 'fabric-ca-se…" About an hour ago Up About an hour 0.0.0.0:32768->7054/tcp ca
```
-
更新したchaincodeを実行する
ubuntu@node0:~/go/src/github.com/nmatsui/fabric-payment-sample-chaincode$ docker exec -e "CORE_PEER_LOCALMSPID=${LOCALMSPID}" -e "CORE_PEER_MSPCONFIGPATH=${PEER_MSPCONFIGPATH}" -e "CORE_PEER_ADDRESS=${PEER_ADDRESS}" cli peer chaincode query -C ${CHANNEL_NAME} -n ${CHAINCODE_NAME} -c '{"Args":["listAccount"]}'
2018-03-07 08:32:21.665 UTC [msp] GetLocalMSP -> DEBU 001 Returning existing local MSP
2018-03-07 08:32:21.666 UTC [msp] GetDefaultSigningIdentity -> DEBU 002 Obtaining default signing identity
2018-03-07 08:32:21.666 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 003 Using default escc
2018-03-07 08:32:21.666 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 004 Using default vscc
2018-03-07 08:32:21.666 UTC [chaincodeCmd] getChaincodeSpec -> DEBU 005 java chaincode disabled
2018-03-07 08:32:21.667 UTC [msp/identity] Sign -> DEBU 006 Sign: plaintext: 0AB9070A7508031A0C0895C7FED40510...741A0D0A0B6C6973744163636F756E74
2018-03-07 08:32:21.667 UTC [msp/identity] Sign -> DEBU 007 Sign: digest: F7B0A9B235CE8CDAD556F79B9626C805BB59B95ACBB8C34E46BA99D6734CDAE8
Error: Error endorsing query: rpc error: code = Unknown desc = chaincode error (status: 500, message: ERROR!!!!) -
...
```
* 修正したように、エラーが発生していることが確認できます。
次回は
次回は、docker swarmクラスタ上でHyperledger/fabricを分散動作させてみます。