はじめに
前回はHyperledger Fabricのアーキテクチャとchaincodeのデプロイ方法を解説しました。Chaincodeを開始するには複数の参加者が署名を行うことで、そのchaincodeの透明性を担保します。
今回は、fabric-samplesでデプロイした自動車の所有者を管理するchaincodeの中身を解説します。
[注意]デフォルトの変更
最新のfabric-samplesでは、network.shによるchaincodeのインストールにおいて、デフォルトのchaincodeがfabcarからasset-transfer-basicに変更になりました。V2.0.0ブランチに切り替えるか、最新版でfabcarをインストールするには、network.shでchaincodeをデプロイするときは以下の引数を用います。
1 | $ ./network.sh deployCC -ccn fabcar -ccv 1 -cci initLedger -ccl go -ccp ../chaincode/fabcar/go |
Chaincode
Chaincodeは、GoやNode.js,Javaで記述されたfabric用のインターフェースを実装したプログラムです。Dockerコンテナとして動作し、トランザクションを受けたpeerが呼び出します。Chaincodeは台帳を操作できる唯一の方法で、トランザクションを元に台帳の更新や参照を実行します。同じchannelには複数のchaincodeをインストールできます。異なるchannelのchaincodeを呼び出せますが、可能なのは参照のみです。
GoやNode.js, Javaのプログラムを書いた経験があればchaincodeを記述するのは難しくありません。ただしchaincodeはsmart contractと呼ばれるように契約と同等のものです。Chaincodeにバグがあると契約書に不備があることと同じなので、複雑なchaincodeを記述する場合は細心の注意が必要です。
Linux FoundationのAccordプロジェクトは、標準化された方法でsmart contractを作成するためのツールを提供しています。このプロジェクトはHyperledgerやR3等の技術的な団体以外に、複数の法律事務所も参加しています。まだまだ発足したばかりですが、将来が楽しみなプロジェクトです。
fabcar chaincodeの概要
fabric-samples直下のchaincodeディレクトには、いくつかのchaincodeがあります。test-networkで動かしたchaincodeはfabcarです。
19 | |---- marbles02_private |
20 | | |---- collections_config.json |
fabcarディレクトリには、goやjava、javascript、typescriptで書かれたchaincodeがあります。今回はgoのchaincodeを見てみます。Goのchaincodeは151行のシンプルなプログラムです。概要を把握するために関数を折り畳んだものを下記に示します。
01 | $ cat fabcar/go/fabcar.go |
10 | "github.com/hyperledger/fabric-contract-api-go/contractapi" |
13 | // SmartContract provides functions for managing a car |
14 | type SmartContract struct { |
18 | // Car describes basic details of what makes up a car |
23 | // QueryResult structure used for handling result of query |
24 | type QueryResult struct { |
28 | // InitLedger adds a base set of cars to the ledger |
29 | func (s *SmartContract) InitLedger(ctx contractapi.TransactionContextInterface) error { |
33 | // CreateCar adds a new car to the world state with given details |
34 | func (s *SmartContract) CreateCar(ctx contractapi.TransactionContextInterface, carNumber string, make string, model string, colour string, owner string) error { |
38 | // QueryCar returns the car stored in the world state with given id |
39 | func (s *SmartContract) QueryCar(ctx contractapi.TransactionContextInterface, carNumber string) (*Car, error) { |
43 | // QueryAllCars returns all cars found in world state |
44 | func (s *SmartContract) QueryAllCars(ctx contractapi.TransactionContextInterface) ([]QueryResult, error) { |
48 | // ChangeCarOwner updates the owner field of car with given id in world state |
49 | func (s *SmartContract) ChangeCarOwner(ctx contractapi.TransactionContextInterface, carNumber string, newOwner string) error { |
10行目でfabricのパッケージをインポートしています。そして53行目でmain関数29、34、39、44、49行目で関数を定義しています。ここで定義された関数は台帳を操作できます。例えば、34行目のCreateCar関数は台帳に新たな車を登録します。
29行目のInitLedger関数はchaincodeの初期化時に呼び出す関数です。Chaincodeはinstantiateした後に初期化する必要があります。前回のdeployCC.shの中で定義されているchaincodeInvokeInit()において、peerコマンドでInitLedger関数を呼び出しchaincodeを初期化しています。
1 | peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile $ORDERER_CA -C $CHANNEL_NAME -n ${CC_NAME} $PEER_CONN_PARMS --isInit -c '{"function":"InitLedger","Args":[]}' |
初期化はinstantiate後に必要な手順であり、chaincodeをバージョンアップする際にもinstantiate後に行う必要があります。そのため初期化用の関数で値を初期化すると、バージョンアップ時にも値が初期化されてしまいます。そのため本番環境では初期化用の関数では、値の初期化を行うのは避けたほうが良いです。
初期化で呼ばれたInitLedgerは以下のようになっています。2行目でcarsのオブジェクトを作成し、17行目でそれらの値はPutState関数を用いてトランザクションの書き込みセットとして格納します。Chiancodeを実行した時点では台帳に書き込みません。このトランザクションは他組織のpeerで検証され、ordererに送信されてブロックとして纏められます。その後、各組織のpeerに送られて検証を行い、問題がなければ台帳に書き込まれます。
最初の引数であるCAR+数字がkeyで2番目の引数はvalueです。Hyperledger Fabricでは、台帳の中をKey-Valueで管理しています。
PutState関数は、CreateCar関数やChangeCarOwner関数でも用いられ、値を変更するために利用されています
。
01 | func (s *SmartContract) InitLedger(ctx contractapi.TransactionContextInterface) error { |
03 | Car{Make: "Toyota", Model: "Prius", Colour: "blue", Owner: "Tomoko"}, |
04 | Car{Make: "Ford", Model: "Mustang", Colour: "red", Owner: "Brad"}, |
05 | Car{Make: "Hyundai", Model: "Tucson", Colour: "green", Owner: "Jin Soo"}, |
06 | Car{Make: "Volkswagen", Model: "Passat", Colour: "yellow", Owner: "Max"}, |
07 | Car{Make: "Tesla", Model: "S", Colour: "black", Owner: "Adriana"}, |
08 | Car{Make: "Peugeot", Model: "205", Colour: "purple", Owner: "Michel"}, |
09 | Car{Make: "Chery", Model: "S22L", Colour: "white", Owner: "Aarav"}, |
10 | Car{Make: "Fiat", Model: "Punto", Colour: "violet", Owner: "Pari"}, |
11 | Car{Make: "Tata", Model: "Nano", Colour: "indigo", Owner: "Valeria"}, |
12 | Car{Make: "Holden", Model: "Barina", Colour: "brown", Owner: "Shotaro"}, |
15 | for i, car := range cars { |
16 | carAsBytes, _ := json.Marshal(car) |
17 | err := ctx.GetStub().PutState("CAR"+strconv.Itoa(i), carAsBytes) |
20 | return fmt.Errorf("Failed to put to world state. %s", err.Error()) |
今度はQueryCar関数を見てみます。2行目でGetState関数を呼び出しています。この関数は引数にkeyをとり、その値を台帳から取得します。トランザクションの送受信やコミット途中などで反映されていない値もあるかもしれませんが、GetState関数を呼び出した時点で最新の台帳の値を取得します。
01 | func (s *SmartContract) QueryCar(ctx contractapi.TransactionContextInterface, carNumber string) (*Car, error) { |
02 | carAsBytes, err := ctx.GetStub().GetState(carNumber) |
05 | return nil, fmt.Errorf("Failed to read from world state. %s", err.Error()) |
08 | if carAsBytes == nil { |
09 | return nil, fmt.Errorf("%s does not exist", carNumber) |
13 | _ = json.Unmarshal(carAsBytes, car) |
他にもQueryAllCars関数の5行目で使われているGetStaeByRange()があります。これはKeyの範囲の値を取得する便利な関数です。
01 | func (s *SmartContract) QueryAllCars(ctx contractapi.TransactionContextInterface) ([]QueryResult, error) { |
05 | resultsIterator, err := ctx.GetStub().GetStateByRange(startKey, endKey) |
10 | defer resultsIterator.Close() |
12 | results := []QueryResult{} |
14 | for resultsIterator.HasNext() { |
15 | queryResponse, err := resultsIterator.Next() |
22 | _ = json.Unmarshal(queryResponse.Value, car) |
24 | queryResult := QueryResult{Key: queryResponse.Key, Record: car} |
25 | results = append(results, queryResult) |
他にもGetStateValidationParameter()やGetStateByRangeWithPagination()、GetStateByPartialCompositeKey(), GetStateByPartialCompositeKeyWithPagination()など便利な関数がfabricのライブラリとして用意されています。
また、GetQueryResult()、GetQueryResultWithPaginationなどリッチクエリと呼ばれる関数があります。これらの関数を利用するためにはstateDBにCouchDBを指定する必要があります。
fabcar chaincodeの改造
fabcarに新しい関数を追加してみたいと思います。今のfabcarはOwnerしか変更できません。そこで車の色を変更した場合に台帳を変更するための関数を追加します。以下のコードをmain関数の上に追記してください。
01 | // ChangeCarColour updates the Colour field of car with given id in world state |
02 | func (s *SmartContract) ChangeCarColour(ctx contractapi.TransactionContextInterface, carNumber string, newColour string) error { |
03 | car, err := s.QueryCar(ctx, carNumber) |
09 | car.Colour = newColour |
11 | carAsBytes, _ := json.Marshal(car) |
13 | return ctx.GetStub().PutState(carNumber, carAsBytes) |
基本的にはChangeCarOwner関数と同じです。違いは引数と9行目をcar.Colourに引数のnewColourを代入するように変更しました。
この関数を実行してみたと思います。まずはnetwork.shを用いてHyperledger Fabricの環境を構築し、改造したchaincodeをインストールします。
01 | $ ./network.sh up createChannel |
02 | Creating channel 'mychannel'. |
03 | If network is not up, starting nodes with CLI timeout of '5' tries and CLI delay of '3' seconds and using database 'leveldb with crypto from 'cryptogen' |
07 | Anchor peers updated for org 'Org2MSP' on channel 'mychannel' |
08 | Channel successfully joined |
09 | $ ./network.sh deployCC -ccn fabcar -ccv 1 -cci initLedger -ccl go -ccp ../chaincode/fabcar/go |
10 | deploying chaincode on channel 'mychannel' |
11 | executing with the following |
12 | - CHANNEL_NAME: mychannel |
15 | 2020-10-27 07:53:47.017 JST [chaincodeCmd] chaincodeInvokeOrQuery -> INFO 001 Chaincode invoke successful. result: status:200 |
16 | Invoke transaction successful on peer0.org1 peer0.org2 on channel 'mychannel' |
Chiancodeのインストールが完了しました。初期化用の関数により台帳に登録された値を取得してみます。
Peerコマンドを実行するためには、adminの証明書が入っているMSPディレクトリやTLSの証明書のディレクトリの指定など、いくつかの環境変数を設定する必要があります。
01 | export PATH=$PATH:../bin |
02 | export CORE_PEER_TLS_ENABLED=true |
03 | export CORE_PEER_LOCALMSPID=Org1MSP |
04 | export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt |
05 | export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp |
06 | export CORE_PEER_ADDRESS=localhost:7051 |
07 | export FABRIC_CFG_PATH=../config |
08 | export ORDERER_CA=${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem |
09 | export PEER0_ORG1_CA=${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt |
10 | export PEER0_ORG2_CA=${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt |
次に、peerコマンドでchiancodeのqueryAllCars関数を呼び出してみます。
1 | $ peer chaincode query -C mychannel -n fabcar -c '{"Args":["queryAllCars"]}' |
2 | [{"Key":"CAR0","Record":{"make":"Toyota","model":"Prius","colour":"blue","owner":"Tomoko"}},{"Key":"CAR1","Record":{"make":"Ford","model":"Mustang","colour":"red","owner":"Brad"}},{"Key":"CAR2","Record":{"make":"Hyundai","model":"Tucson","colour":"green","owner":"Jin Soo"}},{"Key":"CAR3","Record":{"make":"Volkswagen","model":"Passat","colour":"yellow","owner":"Max"}},{"Key":"CAR4","Record":{"make":"Tesla","model":"S","colour":"black","owner":"Adriana"}},{"Key":"CAR5","Record":{"make":"Peugeot","model":"205","colour":"purple","owner":"Michel"}},{"Key":"CAR6","Record":{"make":"Chery","model":"S22L","colour":"white","owner":"Aarav"}},{"Key":"CAR7","Record":{"make":"Fiat","model":"Punto","colour":"violet","owner":"Pari"}},{"Key":"CAR8","Record":{"make":"Tata","model":"Nano","colour":"indigo","owner":"Valeria"}},{"Key":"CAR9","Record":{"make":"Holden","model":"Barina","colour":"brown","owner":"Shotaro"}}] |
初期化時に呼ばれた関数により、10個のレコードが台帳に入っていることを確認できました。次に、新たに追加したChangeCarColour関数を用いて、CAR0に入っているTomokoさん所有のメーカーがToyotaでモデルがPriusの自動車の色をblueからredに変えてみます。
1 | $ peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile $ORDERER_CA -C mychannel -n fabcar --peerAddresses localhost:7051 --tlsRootCertFiles $PEER0_ORG1_CA --peerAddresses localhost:9051 --tlsRootCertFiles $PEER0_ORG2_CA -c '{"function":"ChangeCarColour","Args":["CAR0", "Red"]}' |
2 | 2020-10-27 08:37:42.208 JST [chaincodeCmd] chaincodeInvokeOrQuery -> INFO 001 Chaincode invoke successful. result: status:200 |
変更した結果をみます。車の色がRedに変わりました。
1 | $ peer chaincode query -C mychannel -n fabcar -c '{"function":"QueryCar", "Args":["CAR0"]}' |
2 | {"make":"Toyota","model":"Prius","colour":"Red","owner":"Tomoko"} |
まとめ
今回はfabric-samplesにあるfabcarを用いたchaincodeを解説しました。Hyperledger FabricのchaincodeはgoやNode.js, javaで書くことができ、chaincodeで実現できる表現は多彩です。chaincode特有のコードはそこまで多くありません。
次回は、SDKを用いてchaincodeの操作を行なってみたいと思います。