連載 [第4回] :
  Hyperledger Fabric再入門

fabric-samplesのfabcarを用いてchaincodeの中身を記述する

2020年11月17日(火)
西島 直

はじめに

前回は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をデプロイするときは以下の引数を用います。

$ ./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です。

$ tree chaincode -L 2
chaincode
 |---- README.md
 |---- abac
 |      |---- go
 |---- abstore
 |      |---- go
 |      |---- java
 |      |---- javascript
 |---- fabcar
 |      |---- external
 |      |---- go
 |      |---- java
 |      |---- javascript
 |      |---- typescript
 |---- marbles02
 |      |---- go
 |      |---- javascript
 |---- marbles02_private
 |      |---- collections_config.json
 |      |---- go
 |---- sacc
    |----go.mod
    |---- go.sum
    |---- sacc.go
    |---- sacc_test.go

fabcarディレクトリには、goやjava、javascript、typescriptで書かれたchaincodeがあります。今回はgoのchaincodeを見てみます。Goのchaincodeは151行のシンプルなプログラムです。概要を把握するために関数を折り畳んだものを下記に示します。

$ cat fabcar/go/fabcar.go

package main

import (
    "encoding/json"
    "fmt"
    "strconv"

    "github.com/hyperledger/fabric-contract-api-go/contractapi"
)

// SmartContract provides functions for managing a car
type SmartContract struct {
(中略)
}

// Car describes basic details of what makes up a car
type Car struct {
(中略)
}

// QueryResult structure used for handling result of query
type QueryResult struct {
(中略)
}

// InitLedger adds a base set of cars to the ledger
func (s *SmartContract) InitLedger(ctx contractapi.TransactionContextInterface) error {
(中略)
}

// CreateCar adds a new car to the world state with given details
func (s *SmartContract) CreateCar(ctx contractapi.TransactionContextInterface, carNumber string, make string, model string, colour string, owner string) error {
(中略)
}

// QueryCar returns the car stored in the world state with given id
func (s *SmartContract) QueryCar(ctx contractapi.TransactionContextInterface, carNumber string) (*Car, error) {
(中略)
}

// QueryAllCars returns all cars found in world state
func (s *SmartContract) QueryAllCars(ctx contractapi.TransactionContextInterface) ([]QueryResult, error) {
(中略)
}

// ChangeCarOwner updates the owner field of car with given id in world state
func (s *SmartContract) ChangeCarOwner(ctx contractapi.TransactionContextInterface, carNumber string, newOwner string) error {
(中略)
}

func main() {
(中略)
}

10行目でfabricのパッケージをインポートしています。そして53行目でmain関数29、34、39、44、49行目で関数を定義しています。ここで定義された関数は台帳を操作できます。例えば、34行目のCreateCar関数は台帳に新たな車を登録します。

29行目のInitLedger関数はchaincodeの初期化時に呼び出す関数です。Chaincodeはinstantiateした後に初期化する必要があります。前回のdeployCC.shの中で定義されているchaincodeInvokeInit()において、peerコマンドでInitLedger関数を呼び出しchaincodeを初期化しています。

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関数でも用いられ、値を変更するために利用されています

func (s *SmartContract) InitLedger(ctx contractapi.TransactionContextInterface) error {
    cars := []Car{
        Car{Make: "Toyota", Model: "Prius", Colour: "blue", Owner: "Tomoko"},
        Car{Make: "Ford", Model: "Mustang", Colour: "red", Owner: "Brad"},
        Car{Make: "Hyundai", Model: "Tucson", Colour: "green", Owner: "Jin Soo"},
        Car{Make: "Volkswagen", Model: "Passat", Colour: "yellow", Owner: "Max"},
        Car{Make: "Tesla", Model: "S", Colour: "black", Owner: "Adriana"},
        Car{Make: "Peugeot", Model: "205", Colour: "purple", Owner: "Michel"},
        Car{Make: "Chery", Model: "S22L", Colour: "white", Owner: "Aarav"},
        Car{Make: "Fiat", Model: "Punto", Colour: "violet", Owner: "Pari"},
        Car{Make: "Tata", Model: "Nano", Colour: "indigo", Owner: "Valeria"},
        Car{Make: "Holden", Model: "Barina", Colour: "brown", Owner: "Shotaro"},
    }

    for i, car := range cars {
        carAsBytes, _ := json.Marshal(car)
        err := ctx.GetStub().PutState("CAR"+strconv.Itoa(i), carAsBytes)

        if err != nil {
            return fmt.Errorf("Failed to put to world state. %s", err.Error())
        }
    }

    return nil
}

今度はQueryCar関数を見てみます。2行目でGetState関数を呼び出しています。この関数は引数にkeyをとり、その値を台帳から取得します。トランザクションの送受信やコミット途中などで反映されていない値もあるかもしれませんが、GetState関数を呼び出した時点で最新の台帳の値を取得します。

func (s *SmartContract) QueryCar(ctx contractapi.TransactionContextInterface, carNumber string) (*Car, error) {
    carAsBytes, err := ctx.GetStub().GetState(carNumber)

    if err != nil {
        return nil, fmt.Errorf("Failed to read from world state. %s", err.Error())
    }

    if carAsBytes == nil {
        return nil, fmt.Errorf("%s does not exist", carNumber)
    }

    car := new(Car)
    _ = json.Unmarshal(carAsBytes, car)

    return car, nil
}

他にもQueryAllCars関数の5行目で使われているGetStaeByRange()があります。これはKeyの範囲の値を取得する便利な関数です。

func (s *SmartContract) QueryAllCars(ctx contractapi.TransactionContextInterface) ([]QueryResult, error) {
    startKey := ""
    endKey := ""

    resultsIterator, err := ctx.GetStub().GetStateByRange(startKey, endKey)

    if err != nil {
        return nil, err
    }
    defer resultsIterator.Close()

    results := []QueryResult{}

    for resultsIterator.HasNext() {
        queryResponse, err := resultsIterator.Next()

        if err != nil {
            return nil, err
        }

        car := new(Car)
        _ = json.Unmarshal(queryResponse.Value, car)

        queryResult := QueryResult{Key: queryResponse.Key, Record: car}
        results = append(results, queryResult)
    }

    return results, nil
}

他にもGetStateValidationParameter()やGetStateByRangeWithPagination()、GetStateByPartialCompositeKey(), GetStateByPartialCompositeKeyWithPagination()など便利な関数がfabricのライブラリとして用意されています。

また、GetQueryResult()、GetQueryResultWithPaginationなどリッチクエリと呼ばれる関数があります。これらの関数を利用するためにはstateDBにCouchDBを指定する必要があります。

fabcar chaincodeの改造

fabcarに新しい関数を追加してみたいと思います。今のfabcarはOwnerしか変更できません。そこで車の色を変更した場合に台帳を変更するための関数を追加します。以下のコードをmain関数の上に追記してください。

// ChangeCarColour updates the Colour field of car with given id in world state
func (s *SmartContract) ChangeCarColour(ctx contractapi.TransactionContextInterface, carNumber string, newColour string) error {
    car, err := s.QueryCar(ctx, carNumber)

    if err != nil {
        return err
    }

    car.Colour = newColour

    carAsBytes, _ := json.Marshal(car)

    return ctx.GetStub().PutState(carNumber, carAsBytes)
}

基本的にはChangeCarOwner関数と同じです。違いは引数と9行目をcar.Colourに引数のnewColourを代入するように変更しました。

この関数を実行してみたと思います。まずはnetwork.shを用いてHyperledger Fabricの環境を構築し、改造したchaincodeをインストールします。

$ ./network.sh up createChannel
Creating channel 'mychannel'.
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'
Bringing up network
LOCAL_VERSION=2.2.1
(中略)
Anchor peers updated for org 'Org2MSP' on channel 'mychannel'
Channel successfully joined
$ ./network.sh deployCC -ccn fabcar -ccv 1 -cci initLedger -ccl go -ccp ../chaincode/fabcar/go
deploying chaincode on channel 'mychannel'
executing with the following
- CHANNEL_NAME: mychannel
- CC_NAME: fabcar
(中略)
2020-10-27 07:53:47.017 JST [chaincodeCmd] chaincodeInvokeOrQuery -> INFO 001 Chaincode invoke successful. result: status:200 
Invoke transaction successful on peer0.org1 peer0.org2 on channel 'mychannel'

Chiancodeのインストールが完了しました。初期化用の関数により台帳に登録された値を取得してみます。

Peerコマンドを実行するためには、adminの証明書が入っているMSPディレクトリやTLSの証明書のディレクトリの指定など、いくつかの環境変数を設定する必要があります。

export PATH=$PATH:../bin
export CORE_PEER_TLS_ENABLED=true
export CORE_PEER_LOCALMSPID=Org1MSP
export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt
export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp
export CORE_PEER_ADDRESS=localhost:7051
export FABRIC_CFG_PATH=../config
export ORDERER_CA=${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem
export PEER0_ORG1_CA=${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt
export PEER0_ORG2_CA=${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt

次に、peerコマンドでchiancodeのqueryAllCars関数を呼び出してみます。

$ peer chaincode query -C mychannel -n fabcar -c '{"Args":["queryAllCars"]}'
[{"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に変えてみます。

$ 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"]}'
2020-10-27 08:37:42.208 JST [chaincodeCmd] chaincodeInvokeOrQuery -> INFO 001 Chaincode invoke successful. result: status:200

変更した結果をみます。車の色がRedに変わりました。

$ peer chaincode query -C mychannel -n fabcar -c '{"function":"QueryCar", "Args":["CAR0"]}'
{"make":"Toyota","model":"Prius","colour":"Red","owner":"Tomoko"}

まとめ

今回はfabric-samplesにあるfabcarを用いたchaincodeを解説しました。Hyperledger FabricのchaincodeはgoやNode.js, javaで書くことができ、chaincodeで実現できる表現は多彩です。chaincode特有のコードはそこまで多くありません。

次回は、SDKを用いてchaincodeの操作を行なってみたいと思います。

株式会社日立製作所

研究開発グループ デジタルテクノロジーイノベーションセンタ OSSテクノロジーラボラトリ員
OSSの評価・検証・機能開発、upstream活動、社内外へのOSS普及に従事。
Linux KernelやKVM、OpenStackでOSSコミュニティの参加した経験を持つ。
現在はHyperledgerコミュニティに参加しブロックチェーンの普及に勤めている。

連載バックナンバー

Think ITメルマガ会員登録受付中

Think ITでは、技術情報が詰まったメールマガジン「Think IT Weekly」の配信サービスを提供しています。メルマガ会員登録を済ませれば、メルマガだけでなく、さまざまな限定特典を入手できるようになります。

Think ITメルマガ会員のサービス内容を見る

他にもこの記事が読まれています