Skip to Content
Technical Articles

SAP Cloud Platform Blockchainを使ったデモアプリのご紹介

はじめに

このブログは、SAP Advent Calendar 2019 の13日目の記事として投稿しています。

11月25日、27日~28日の3日間に渡って開催されました SAP Inside Track Tokyo 2019。私は「いまやっておくべき、ブロックチェーン。」という内容で登壇させていただきました。今回は、その中でご紹介したブロックチェーンを使ったデモアプリについて、詳しくご紹介したいと思います。

 

あなただけのチケットを発券しよう。

SAP Inside Track Tokyo 2019 に参加されていない方もいらっしゃると思いますので、改めてデモアプリの概要をご紹介します。イベントの電子チケット(QRコード)を発券するアプリを作りました。「あなただけのチケットを」がコンセプトで、好きな写真をベースに自分だけのチケットを作成することができます。

ブロックチェーンを使ったチケットアプリ

デモでは、ブロックチェーンを体験いただくため、チケットの譲渡とチケットの動きをトレースする様子をご紹介しました。具体的には、デモアプリから他の人が発券したチケットを受け取り、SAP Cloud Platform Blockchain サービスの REST API テスト画面からチケットの動き(トランザクション)を確認しました。

SCP Blockchain サービスの REST API テスト画面

こちらがデモアプリのアーキテクチャです。フロントエンドは Vue.js、サービスエンドポイントは Python 、バックエンドはブロックチェーンです。すべて SAP Cloud Platform Cloud Foundry 環境で動作しています。ブロックチェーンのデモアプリとして作ったのですが、Cloud Foundry 環境でのアプリ開発としても勉強になったことがたくさんあったので、今回ご紹介しようと思いました。

アーキテクチャ

 

フロントエンドアプリ

SAP Cloud Platform で画面を作ると言えば SAPUI5 ですが、フロンドエンド開発者にお馴染みの Angular、React、Vue といった JavaSript フレームワークを採用することもできます。

いずれも Web テクノロジには変わりないので、SAP WebIDE からMTAプロジェクトを作成し、その中に HTML5 モジュールを作成して、npm run build した結果を埋め込んでも当然動くと思います。が、せっかく Cloud Foundry なのでコマンド一発でデプロイしたい。そんなときに見つけたのが「VueSAP」です。

VueSAP は、SAP Cloud Platform 上で Vue を動かすためのプラグインです。今回のデモアプリは、このプラグインを使って開発しました。VueSAP を使ったプロジェクト作成の詳細については、こちらにまとめています。本ブログでは割愛しますので、興味のある方はぜひ読んでみてください。

 

アプリケーションルータ

SAP は、SAP Cloud Platform Cloud Foundry 上でのマイクロサービスアーキテクチャに基づくアプリケーション開発を標準化しています。それに伴い、Cloud Foundry 環境で動くアプリには必ずアプリケーションルータというものが必要になります。アプリケーションルータは、Cloud Foundry 環境で動くあらゆるアプリケーション、マイクロサービスの唯一のエントリポイントとなります。SAP WebIDE を使うと自動生成されるケースがほとんどで、あまり意識しないかもしれませんが、今回は自前で用意する必要があります。

アプリケーションルータはその名前のとおり、ルーティングが大きな役割です。こちらがデモアプリの xs-app.json です。このファイルでは、Vue アプリと REST API サービスへのルーティングとそれぞれの認証方式を定義しています。

{
  "authenticationMethod": "route",
  "logout": {
    "logoutEndpoint": "/logout",
    "logoutPage": "logout.html"
  },
  "routes": [
    {
      "source": "^/api/(.*)$",
      "target": "$1",
      "destination": "python",
      "authenticationType": "xsuaa",
      "csrfProtection": false
    },
    {
      "source": "^/(.*)$",
      "target": "$1",
      "destination": "myapp",
      "authenticationType": "xsuaa"
    }
  ]
}

 

アプリケーションルータはルーティングを実現するために、ルーティング先のアプリケーションの実際の宛先を知っておく必要があります。そこで、アプリケーションルータの manifest.yml にそれらを定義します。ここでは、環境変数の destinations として定義していますが、Destination サービスをバインドし、サービス側に宛先を設定しても同じことが実現できます。

---
#application settings
applications:

#approuter settings
- name: approuter                             
  memory: 128M                                
  routes:                                     
    - route: approuter-sXXXXXXXXXXtrial.cfapps.eu10.hana.ondemand.com
  env:                                        # env variables 
    TENANT_HOST_PATTERN: 'approuter-(.*).cfapps.eu10.hana.ondemand.com' 
    SAP_JWT_TRUST_ACL: '[{"clientid":"*","identityzone":"*"}]'
    destinations: >
      [
        {
          "name": "myapp",
          "url": "https://myapp-sXXXXXXXXXXtrial.cfapps.eu10.hana.ondemand.com/",
          "forwardAuthToken": true
        },
        {
          "name": "python",
          "url": "https://myapi-sXXXXXXXXXXtrial.cfapps.eu10.hana.ondemand.com/",
          "forwardAuthToken": true
        }
      ]
  services:              # binded services 
    - my-xsuaa           # instantie to uaa service 
    

 

認証は、 xsuaa サービスを利用します。バインドサービスに xsuaa サービスインスタンスの名前を指定します。アプリケーションルータをデプロイする前に、ここで指定した名前で xsuaa サービスインスタンスを作成しておきましょう。

xsuaa サービスインスタンス作成

実装できたら、以下のコマンドでデプロイします。

$ npm run build && cf push

 

Vue アプリ

Vue アプリは、特別な実装がほとんど必要ありません。ここでのポイントは、xsuaa サービスで認証をパスした後、アプリケーション側でどうやってカレントユーザ情報を取得するのか、という一点だけです。

カレントユーザは、以下のようにして取得できます。

async created() {
  // axiosでユーザ情報を取得
  let response = await this.$http.get('/uaa/userinfo');
  var userinfo = response.data;

  // 例えば、Vuexにカレントユーザをストアしておく
  this.$store.dispatch('setCurrentUser', userinfo);

},

 

結果として、このような情報を得ることができます。

{
    "user_id":"XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
    "user_name":"kaori@example.com",
    "given_name":"Kaori",
    "family_name":"Sukenobe",
    "email":"kaori@example.com",
    "email_verified":true,
    "previous_logon_time":null,
    "sub":"XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
    "name":"Kaori Sukenobe"
}

 

REST API サービス

SAP Cloud Platform Functions では、(まだ) Python がサポートされていません。Functions が使えたらそれがベストですが、画像処理で Python を使いたかったのと自分のお勉強も兼ねて Python で REST API サービスを開発することにしました。

こちらがアプリケーションのフォルダ構成です。まず、プロジェクトフォルダを作成して必要なファイル、フォルダを準備します。

まず、manifest.yaml です。host には、デプロイされている他のアプリケーションと競合しないような一意の名前を指定します。また、REST API も認証をかけるので xsuaa サービスをバインドします。さらに、バックエンドのブロックチェーンサービスを呼び出すため、ブロックチェーンのサービスインスタンスもバインドしておきます。

---
applications:
- name: my-api
  host: ksukenobe-api
  path: .
  memory: 512M
  command: python server.py
  env:
    # Blockchain service
    CHAINCODE_ID: 'chaincode id'
    # QR Generator configuration
    QR_VERSION: 5
    ERR_CORRECT_VAL: 'H'
    CONTRAST_VAL: '1.0'
    BRIGHTNESS_VAL: '1.0'
    IS_COLOR: true
    IS_PIXELATE: false
  services:
    - my-xsuaa
    - tickettransfer.dev

 

runtime.txt には、アプリケーションを実行する Python バージョンを定義します。

python-3.6.x

 

依存関係をベンダリングします。requirements.txt に必要なパッケージを定義して、以下のコマンドを実行します。

$ pip download -d vendor -r requirements.txt --platform manylinux1_x86_64 --only-binary=:all:

 

ここまでの土台が整ったら、あとは実装あるのみです。Python Flask で REST API サービスを作りました。デコレータを使って認証チェックを行っています。ブロックチェーンの呼び出し部は、アクセストークンを取得してブロックチェーンの API を叩く、といった一般的な OAuth 2.0 の認証フローとなっています。

uaa_service = env.get_service(name='my-xsuaa').credentials
baas_service = env.get_service(name='tickettransfer.dev').credentials

def xsuaa_token(f):
  @wraps(f)
  def decorated_view(*args, **kwargs):
    if 'authorization' not in request.headers:
      error_message = {
        'error': 'authorization header not found'
      }
      return make_response(jsonify(error_message), 403)

    access_token = request.headers.get('authorization')[7:]
    security_context = xssec.create_security_context(access_token, uaa_service)
    isAuthorized = security_context.check_scope('openid')
    if not isAuthorized:
      error_message = {
        'error': 'not authorized token'
      }
      return make_response(jsonify(error_message), 403)

    return f(*args, **kwargs)
  return decorated_view

@app.route('/assets/tickets', methods=['get'])
@xsuaa_token
def get_my_tickets():
  '''
    自分が所有するチケットを取得します.
  '''
  owner = request.args.get('owner')
  print('get_my_ticket is called: owner -> {}'.format(owner))

  oauth_token_headers = {'Authorization': 'Bearer {}'.format(
                          get_oauth_token(
                            baas_service['oAuth']['url'] + '/oauth/token?grant_type=client_credentials', 
                            baas_service['oAuth']['clientId'], 
                            baas_service['oAuth']['clientSecret'])
                          )}
  response = requests.get(
                  baas_service['serviceUrl'] + '/chaincodes/' + CHAINCODE_ID + '/latest/assets/tickets?owner={}'.format(owner),
                  headers=oauth_token_headers)

  return jsonify(response.json()), response.status_code

 

ブロックチェーン

最後に、ブロックチェーンの部分です。Hyperledger Fabric のノードやチャンネルの作り方については、4日目にK.yoshimuraさんが投稿されている【ブロックチェーン】ゼロ知識の人がSAP Cloud Platform上のブロックチェーンサービスを体験する方法に詳しくまとめられています。まだご覧になってない方はぜひチェックしてみてください!

ブロックチェーン開発のメインは、スマートコントラクトです。このブログでは、スマートコントラクト開発をメインにご紹介したいと思います。スマートコントラクトは、契約上のルールや条件を実装したものです。Hyperledger Fabric では、これをチェーンコードと呼びます。

チェーンコードに必要なもの

 

コードで見る、ブロックチェーン。

では、実際のソースコードを一部ご紹介します。全貌はご紹介しきれないので、あくまで参考としてご覧いただければと思います。

まず、必要なパッケージをインポートします。shim パッケージと peer パッケージが必須です。それ以外は、適宜必要なものをインポートします。

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/hyperledger/fabric/core/chaincode/shim"
	"github.com/hyperledger/fabric/protos/peer"
)

 

次に、関数を実行するための構造体とブロックチェーンで扱うデータモデルの構造体を定義します。タグは、JSONに変換するときに使われます。

type SimpleAsset struct {
}

type Ticket struct {
	ObjectType  string      `json:"docType"`
	ID          string      `json:"id"`
	Event       Event       `json:"event"`
	Owner       string      `json:"owner"`
	Status      string      `json:"status"`
}

 

いよいよここからは、実際の処理を書いていきます。チェーンコードは、インタフェースとして Init と Invoke の二つを定義する必要があります。

関数 役割
Init Instantiate、Upgradeのときに呼び出される。
Invoke 台帳操作(更新・参照)のときに呼び出される。
func main() {
	if err := shim.Start(new(SimpleAsset)); err != nil {
		fmt.Printf("Error starting SimpleAsset chaincode: %s", err)
	}
}

// Instantiate, Upgradeで呼び出される
func (t *SimpleAsset) Init(stub shim.ChaincodeStubInterface) peer.Response {
	fmt.Println("Init() is called.")
	return Success(http.StatusNoContent, "OK", nil)
}

// 台帳操作時に呼び出される
func (t *SimpleAsset) Invoke(stub shim.ChaincodeStubInterface) peer.Response {
	fn, args := stub.GetFunctionAndParameters()

	var result string
	var err error

	// 関数名で処理をディスパッチする
	if fn == "init" {
		return t.Init(stub)
	} else if fn == "initTickets" {
		return t.initTickets(stub, args)
	} else if fn == "editTicket" {
		return t.editTicket(stub, args)
	} else if fn == "deleteTicket" {
		return t.deleteTicket(stub, args)
	} else if fn == "getHistory" {
		return t.getHistory(stub, args)
	} else if fn == "getAllAssets" {
		return t.getAllAssets(stub)
	} else if fn == "getTicketByOwner" {
		return t.getTicketByOwner(stub, args)
	} else if fn == "getTicketById" {
		return t.getTicketByID(stub, args)
	}
	if err != nil {
		fmt.Println("error")
	}
	return shim.Success([]byte(result))
}

 

次に、実際の処理を行うサブ関数を実装します。台帳操作に必要なAPIが用意されているのでそれを使います。

// チケットIDをキーにデータを取得する
func (t *SimpleAsset) getTicketByID(stub shim.ChaincodeStubInterface, args []string) peer.Response {
	fmt.Println("----- getTicketByID() start. ")

	if len(args) < 1 {
		return Error(http.StatusBadRequest, "Incorrect number of arguments. Expecting 1")
	}
	id := args[0]

	// キーを指定してステートを読み取るAPIを実行
	jsonAsBytes, err := stub.GetState(id)
	if err != nil {
		return Error(http.StatusInternalServerError, "Failed to get ticket: "+id)
	} else if jsonAsBytes == nil {
		return Error(http.StatusNotFound, "Ticket is not found. key: "+id)
	}

	fmt.Println("----- getTicketByID() end. ")
	return Success(http.StatusOK, "OK", jsonAsBytes)
}

// チケットのトランザクションを取得する
func (t *SimpleAsset) getHistory(stub shim.ChaincodeStubInterface, args []string) peer.Response {
	fmt.Println("----- getHistory() start.")
	if len(args) < 1 {
		return Error(http.StatusBadRequest, "Incorrect number of arguments. Expecting 1")
	}
	id := args[0]
	// ステートの変更履歴を取得するAPIを実行
	resultsIterator, err := stub.GetHistoryForKey(id)
	if err != nil {
		fmt.Printf(err.Error())
		return Error(http.StatusInternalServerError, err.Error())
	}
	defer resultsIterator.Close()
	var buffer bytes.Buffer
	buffer.WriteString("[")

	bArrayMemberAlreadyWritten := false
	for resultsIterator.HasNext() {
		response, err := resultsIterator.Next()
		if err != nil {
			return Error(http.StatusInternalServerError, err.Error())
		}
		if bArrayMemberAlreadyWritten == true {
			buffer.WriteString(",")
		}
		buffer.WriteString("{\"TxId\":")
		buffer.WriteString("\"")
		buffer.WriteString(response.TxId)
		buffer.WriteString("\"")

		buffer.WriteString(", \"Value\":")
		if response.IsDelete {
			buffer.WriteString("null")
		} else {
			buffer.WriteString(string(response.Value))
		}

		buffer.WriteString(", \"Timestamp\":")
		buffer.WriteString("\"")
		buffer.WriteString(time.Unix(response.Timestamp.Seconds, int64(response.Timestamp.Nanos)).String())
		buffer.WriteString("\"")

		buffer.WriteString(", \"IsDelete\":")
		buffer.WriteString("\"")
		buffer.WriteString(strconv.FormatBool(response.IsDelete))
		buffer.WriteString("\"")

		buffer.WriteString("}")
		bArrayMemberAlreadyWritten = true
	}
	buffer.WriteString("]")
	fmt.Printf("- getHistory returning:\n%s\n", buffer.String())
	fmt.Println("----- getHistory() end.")
	return Success(http.StatusOK, "OK", buffer.Bytes())
}

 

ソースコードの実装が完了したら、それぞれの処理をどの API にマッピングするかを定義します。operationId に、Invoke 関数で使われる 関数名 (fn) を指定します。

# OpenAPI.yaml file
swagger: "2.0"
info:
  version: "1.0.0"
  title: "tickettransfer"

consumes:
  - application/x-www-form-urlencoded
produces:
  - application/json

parameters:
  ticketId:
    name: ticket_id
    in: path
    required: true
    type: string

paths:  
  /assets/tickets/{ticket_id}:
    get:
      operationId: getTicketById
      parameters:
      - $ref: '#/parameters/ticketId'
      responses:
        200:
          description: "OK"
        404:
          description: "Not Found"

  /assets/tickets/{ticket_id}/history:
    get:
      operationId: getHistory
      parameters:
      - $ref: '#/parameters/ticketId'
      responses:
        200:
          description: "OK"

 

柔軟な検索に対応しよう。

ステート DB は KVS (Key-Value Store) 形式となるので、複雑な検索が不得意です。デモアプリではチケット ID がキーになっているため、それ以外の項目でチケットを検索できません。したがって、自分が所有するチケットを取得できない(面倒な処理が必要となる)のです。

この解決策として、ステート DB にインデックスを設定する方法があります。Hyperledger Fabric では、ステートDB に LevelDB か CouchDB を使用することができますが、この方法は CouchDB の使用が大前提となります。SAP Cloud Platform Blockchain サービスでは、デフォルトで CouchDB が使用されているため、インデックスを設定して多彩な検索をすることができます。Hyperledger Fabric ではこれをリッチクエリと呼んでいます。

では、早速インデックスを作っていきましょう。JSON形式で、インデックスを定義します。

// indexOwner.json
{
    "index" : {
        "fields" : [
            "docType","owner"
        ]
    },
    "ddoc" : "indexOwnerDoc", 
    "name" : "indexOwner",
    "type":"json"
}

 

作成したインデックスは、このようなフォルダ構成でソースコードに追加します。

こちらが、インデックスを利用した検索処理です。非常にシンプルに検索できます。

インデックスは、適切に設定してこそ効果があります。例えば、正規表現によるマッチングで検索すると、インデックスは使われるものの、結局全レコードスキャンされてしまうことがあります。また、CouchDB の制限によりインデックスが使用されない検索条件もありますので注意が必要です。

// 自分が所有するチケットを取得する
func (t *SimpleAsset) getTicketByOwner(stub shim.ChaincodeStubInterface, args []string) peer.Response {
	fmt.Println("----- getTicketByOwner() start. ")

	if len(args) < 1 {
		return Error(http.StatusBadRequest, "Incorrect number of arguments. Expecting 1")
	}

	owner := strings.ToLower(args[0])
	queryString := fmt.Sprintf("{\"selector\":{\"docType\":\"ticket\",\"owner\":\"%s\"}}", owner)

	queryResults, err := getQueryResultForQueryString(stub, queryString)
	if err != nil {
		return Error(http.StatusInternalServerError, err.Error())
	}
	fmt.Println("----- getTicketByOwner() end. ")
	return Success(http.StatusOK, "OK", queryResults)
}

// 内部関数:リッチクエリを使ってデータを取得する
func getQueryResultForQueryString(stub shim.ChaincodeStubInterface, queryString string) ([]byte, error) {

	fmt.Printf("- getQueryResultForQueryString queryString:\n%s\n", queryString)
	resultsIterator, err := stub.GetQueryResult(queryString)
	if err != nil {
		return nil, err
	}
	defer resultsIterator.Close()

	buffer, err := constructQueryResponseFromIterator(resultsIterator)
	if err != nil {
		return nil, err
	}

	fmt.Printf("- getQueryResultForQueryString queryResult:\n%s\n", buffer.String())

	return buffer.Bytes(), nil
}

 

チェーンコードをテストしよう。

ここで少しテストに触れたいと思います。いくらテストとはいえ「ブロックチェーン」ですので、テストデータも半永久的に残ってしまいます。ひとりの開発者として、変なデータを後世に残すのは心苦しいですし、ディスク容量も圧迫してしまいます。なので、実際にチャンネルにインストールする前に、最低限のテストをローカルでやっておくのがお作法になっています。

Hyperledger Fabric では、チェーンコードの単体テスト用のモックオブジェクトである MockStub を使用できます。これと Go 言語が標準で提供している testing パッケージを使って、お手軽に単体テストする方法をご紹介します。

まず、テストしたいパッケージディレクトリの中に「*_test.go」で終わる名前のテストファイルを作成し、テストコードを書きます。

package main

import (
	"fmt"
	"testing"

	"github.com/hyperledger/fabric/core/chaincode/shim"
)

func TestInit(t *testing.T) {
	simpleCC := new(SimpleAsset)
	mockStub := shim.NewMockStub("mockstub", simpleCC)

	mockStub.MockTransactionStart("tx0")
	response := simpleCC.Init(mockStub)
	mockStub.MockTransactionEnd("tx0")
	if s := response.GetStatus(); s != 200 {
		fmt.Println("Init test failed")
		t.FailNow()
	}
}

func TestNewTicket(t *testing.T) {
	simpleCC := new(SimpleAsset)
	mockStub := shim.NewMockStub("mockstub", simpleCC)

	fmt.Println("--- 初期データを登録する")
	mockStub.MockTransactionStart("tx1")
	response := simpleCC.initLedger(mockStub)
	mockStub.MockTransactionEnd("tx1")
	fmt.Println("Status: " + fmt.Sprint(response.GetStatus()))
	fmt.Println("Payload: " + string(response.GetPayload()))
	fmt.Println("Message: " + response.GetMessage())

	fmt.Println("--- チケットを登録する")
	mockStub.MockTransactionStart("tx2")
	initTickets := `[
			{
				"docType": "ticket",
				"id" : "t-98a692bd-f8b3-44a4-aab6-7fc8bba8a40c",
				"performanceid" : "p1",
				"owner" : "98f3e258-bcbe-4fac-a454-511d22770f7b",
				"status" : "RESERVED"
			},
			{
				"docType": "ticket",
				"id" : "t-baa1d5ec-7d5b-4e72-a0cd-6ea6ed34936b",
				"performanceid" : "p3",
				"owner" : "1c55ca5a-0abb-4162-9537-cf9366a402a0",
				"status" : "RESERVED"
			}
	]`
	args := []string{initTickets}
	response = simpleCC.initTickets(mockStub, args)
	mockStub.MockTransactionEnd("tx2")
	fmt.Println("Status: " + fmt.Sprint(response.GetStatus()))
	fmt.Println("Payload: " + string(response.GetPayload()))
	fmt.Println("Message: " + response.GetMessage())

	fmt.Println("--- 全アセットを取得する")
	mockStub.MockTransactionStart("tx3")
	response = simpleCC.getAllAssets(mockStub)
	mockStub.MockTransactionEnd("tx3")
	fmt.Println("Status: " + fmt.Sprint(response.GetStatus()))
	fmt.Println("Payload: " + string(response.GetPayload()))
	fmt.Println("Message: " + response.GetMessage())

}

 

テストコードが書けたら、go test コマンドでテストを実行できます。私は Visual Studio Code でテストしていました。

チェーンコードのテスト

この方法を使えばほとんどの機能がテストできますが、一部例外があります。今回ご紹介した機能では、CouchDB の使用を前提とする GetQueryResult や Key に対する Value の変更履歴を取得する GetHistoryForKey などはテストできません。これらの機能は実際のチャンネルにインストールした後でテストすることになります。

 

さいごに

ブロックチェーンを使ったデモアプリのご紹介、いかがでしたか?

かなりのボリュームになってしまい、反省しています…。が、この機会を逃すと二度と書かないかもしれない!と思い、勢いで執筆しました。

ブロックチェーンは、開発者にとって身近な存在になりました。実際のソースコードを見ると、それがより実感できると思います。とはいえ、ブロックチェーンに関する情報はまだまだ少ないのが現状です。「ベストプラクティスはありますか?」と聞かれることがよくあります。私の実感としては、まだ確立されていないと思っています。技術的に未成熟、ベストプラクティスは未確立、社会的にも未踏領域なのが、ブロックチェーンです。難しいこともたくさんありますが、だからこそ面白く、やりがいのあるテクノロジではないでしょうか。今回登壇が決まってから、ドタバタで作ったデモアプリですが、ブロックチェーンはじめてみようかな?と思っている方の参考になれば幸いです。

さて、明日は monstconcierge さんの「CF CLIか何か」です。Cloud Foundry 好きの私にはタマラナイ内容になりそうで、今から楽しみです。

Be the first to leave a comment
You must be Logged on to comment or reply to a post.