はじめに
Hyperledger Composerとは、Hyperledger Fabric向けのブロックチェーン・アプリケーションの開発フレームワークです。Hyperledger Composerを用いることで、効率よくアプリケーションを開発することができると言われています。
今回、Hyperledger Composerを使ってブロックチェーン・ベースのオブジェクトストレージを作ってみました。ブロックチェーンの上にオブジェクトストレージを作成してしまうと、ブロックチェーンの性質上高いパフォーマンスが期待できないことが予想されます。一方で、以下のような利点が得られると考えられます。
- トランザクションの履歴が残るため、あるデータが改ざんされた場合、誰がいつ改ざんしたか調べることができる
- データの履歴が残るため、以前のデータを読み出すことができる。
- 悪意のある人にシステムを乗っ取られても、ブロックチェーンの性質上トランザクションやデータの履歴を削除できない
ブロックチェーン・ベースのオブジェクトストレージのアーキテクチャ
ブロックチェーン・ベースのオブジェクトストレージのアーキテクチャは以下のようになっています。REST APIのリクエストをNode.jsで作成されたサーバが受け取ると、composer-clientライブラリを呼び出します。Hyperledger Fabricのブロックチェーンではオブジェクトストレージのチェーンコード(以下、オブジェクトストレージネットワークと呼ぶことにします)が実行され、呼び出されたリクエストに応じてトランザクションやクエリを実行します。Hyperledger ComposerはComposer Rest Serverと呼ばれる手軽にRESTサーバを開発できる機能を提供していますが、OpenStack SwiftやAmazon S3のREST APIのようなパス・スタイル形式のAPIを実装したかったため、今回は自前でRESTサーバを開発しました。
では、オブジェクトストレージネットワークとNode.jsのRESTサーバについてそれぞれ概要をご紹介したいと思います。
オブジェクトストレージネットワークの概要
Hyperledger Composerでチェーンコードを開発するためには、Model File (.cto), Script File (.js), Access Control (.acl)の三つのファイルを作成する必要があります。Model Fileはブロックチェーンネットワークの参加者、そこで扱う資産、およびトランザクションのデータモデルを定義します。Script Fileには、トランザクションの処理の内容を定義します。Access Controlはブロックチェーンネットワークの参加者に対する権限を設定します。
まずはModel Fileの内容についてご紹介します。OpenStack SwiftやAmazon S3などのオブジェクトストレージに保存されるデータはオブジェクトと呼ばれます。オブジェクトはコンテナと呼ばれる入れ物で管理され、コンテナの中に複数のオブジェクトを保存することができます。同じコンテナの中にあるオブジェクトの名前は一意である必要があります。また、あるアカウントは複数のコンテナを持つことができ、同じアカウントが所有するコンテナの名前は一意である必要があります。以上の性質から、以下のようなModel Fileを作成しました。
namespace org.acme.objectstore
participant Account identified by accountId{
o String accountId
}
asset Container identified by accountContainerId{
o String accountContainerId
o String containerId
--> Account account
}
asset Object identified by accountContainerObjectId{
o String accountContainerObjectId
o String objectId
o String value
--> Container container
}
transaction CreateAccount{
o String accountId
}
transaction CreateContainer{
--> Account account
o String containerId
}
transaction CreateObject{
--> Container container
o String objectId
o String value
}
transaction DeleteContainer{
o String accountContainerId
}
transaction DeleteObject{
o String accountContainerObjectId
}
ContainerのaccountContainerIdには、<accountId>/<containerId>
のようにアカウント名とコンテナ名をスラッシュ/
を挟んで連結した文字列を保存します。このaccountContaierIdをContainerの一意なキーに設定することで、異なるアカウントが同じ名前のコンテナを所有することができます。同様に、ObjectのaccountContainerObjectIdには<accountId>/<containerId>/<objectId>
のようにアカウント名、コンテナ名とオブジェクト名をスラッシュを挟んで連結した文字列を保存し、これをObjectの一意なキーに設定することで、異なるコンテナに同じ名前のオブジェクトを作成できるようにします。
次に、Script Fileの内容をご紹介します。Script Fileには、Model Fileで定義したトランザクションの処理の内容を書いていきます。
'use strict';
NS = 'org.acme.objectstore';
/**
* Create Account
* @param {org.acme.objectstore.CreateAccount} tx The create account transaction instance.
* @transaction
*/
function createAccount(tx) {
var factory = getFactory();
var account = factory.newResource(NS, 'Account', tx.accountId);
return getParticipantRegistry(NS + '.Account')
.then(function (participantRegistry){
return participantRegistry.add(account);
});
}
/**
* Create Container
* @param {org.acme.objectstore.CreateContainer} tx The create container transaction instance.
* @transaction
*/
function createContainer(tx) {
var accountContainerId = tx.account.accountId + "/" + tx.containerId;
var factory = getFactory();
var container = factory.newResource(NS, 'Container', accountContainerId);
container.containerId = tx.containerId;
container.account = tx.account;
return getAssetRegistry(NS + '.Container')
.then(function(assetRegistry){
return assetRegistry.add(container);
});
}
/**
* Create Object
* @param {org.acme.objectstore.CreateObject} tx The create object transaction instance.
* @transaction
*/
function createObject(tx) {
var accountContainerObjectId = tx.container.accountContainerId + "/" + tx.objectId;
var factory = getFactory();
var object = factory.newResource(NS, 'Object', accountContainerObjectId);
object.objectId = tx.objectId;
object.value = tx.value;
object.container = tx.container;
return getAssetRegistry(NS + '.Object')
.then(function (assetRegistry){
return assetRegistry.add(object);
});
}
/**
* Delete Container
* @param {org.acme.objectstore.DeleteContainer} tx The delete container transaction instance.
* @transaction
*/
function deleteContainer(tx) {
var accountContainerId = tx.accountContainerId;
var factory = getFactory();
var container = factory.newResource(NS, 'Container', accountContainerId);
return getAssetRegistry(NS + '.Container')
.then(function (assetRegistry){
return assetRegistry.remove(container);
});
}
/**
* Delete Object
* @param {org.acme.objectstore.DeleteObject} tx The delete object transaction instance.
* @transaction
*/
function deleteObject(tx) {
var accountContainerObjectId = tx.accountContainerObjectId;
var factory = getFactory();
var object = factory.newResource(NS, 'Object', accountContainerObjectId);
return getAssetRegistry(NS + '.Object')
.then(function (assetRegistry){
return assetRegistry.remove(object);
});
}
最後に、Access Controlのご紹介です。自分が所有するコンテナやオブジェクトに対してどんな操作も可能なように定義します。また、管理者は全てのリソースに対してアクセスできるようにしておきます。
rule EverybodyCanCreateContainers {
description: "Allow all participants to submit transactions"
participant: "org.acme.objectstore.Account"
operation: CREATE
resource: "org.acme.objectstore.CreateContainer"
action: ALLOW
}
rule EverybodyCanCreateObjects {
description: "Allow all participants to submit transactions"
participant: "org.acme.objectstore.Account"
operation: CREATE
resource: "org.acme.objectstore.CreateObject"
action: ALLOW
}
rule OwnerHasFullAccessToTheirContainers {
description: "Allow all participants full access to their assets"
participant(p): "org.acme.objectstore.Account"
operation: ALL
resource(r): "org.acme.objectstore.Container"
condition: (r.owner.getIdentifier() === p.getIdentifier())
action: ALLOW
}
rule OwnerHasFullAccessToTheirObjects {
description: "Allow all participants full access to their assets"
participant(p): "org.acme.objectstore.Account"
operation: ALL
resource(r): "org.acme.objectstore.Object"
condition: (r.owner.getIdentifier() === p.getIdentifier())
action: ALLOW
}
rule SystemACL {
description: "System ACL to permit all access"
participant: "org.hyperledger.composer.system.Participant"
operation: ALL
resource: "org.hyperledger.composer.system.**"
action: ALLOW
}
rule NetworkAdminACL {
description: "NetworkAdmin ACL to permit all access"
participant: "org.hyperledger.composer.system.Participant"
operation: ALL
resource: "org.acme.objectstore.*"
action: ALLOW
}
以上でオブジェクトストレージネットワークは完成です!
RESTサーバの概要
次いで、RESTサーバの概要をご紹介します。今回、RESTサーバはNode.js Expressで作成してみました。Express Generatorで作成した雛形に、変更を加えていきます。
REST APIでテキストデータを扱えるよう、app.js
にはテキストデータを扱えるようにapp.use(bodyParser.text({type: 'text/html'}));
の一行を追加します。routes/index.js
にはREST APIを定義し、処理の内容に応じてトランザクションやクエリを呼び出します。
var express = require('express');
var router = express.Router();
const BusinessNetworkConnection = require('composer-client').BusinessNetworkConnection;
businessNetworkConnection = new BusinessNetworkConnection();
businessNetworkConnection.connect('hlfv1', 'object-storage-network', 'PeerAdmin', 'adminpw')
.then((result) => {
businessNetworkDefinition = result;
});
/* GET home page. */
router.get('/', function(req, res, next) {
res.render('index', { title: 'Express' });
});
router.put('/:accountid', function(req, res, next){
console.log("account creation request received");
let serializer = global.businessNetworkDefinition.getSerializer();
let resource = serializer.fromJSON({
'$class': 'org.acme.objectstore.CreateAccount',
'accountId': req.params.accountid
});
businessNetworkConnection.submitTransaction(resource)
.then(function (){
res.send('Account ' + req.params.accountid + ' is created');
}).catch(function (error){
res.send(error);
});
});
router.get('/:accountid', function(req, res, next){
console.log("listing containers for " + req.params.accountid);
var selectStatement = businessNetworkConnection.buildQuery('SELECT org.acme.objectstore.Container WHERE (account == _$class)');
businessNetworkConnection.query(selectStatement, { "class": "resource:org.acme.objectstore.Account#" + req.params.accountid })
.then(function(results) {
res.send(String(results));
}).catch(function (error){
res.send(error);
});
});
router.put('/:accountid/:containerid', function(req, res, next){
console.log("container creation request received");
let serializer = businessNetworkDefinition.getSerializer();
let account_id = "resource:org.acme.objectstore.Account#" + req.params.accountid;
let resource = serializer.fromJSON({
'$class': 'org.acme.objectstore.CreateContainer',
'account': account_id,
'containerId': req.params.containerid
});
businessNetworkConnection.submitTransaction(resource)
.then(function(){
res.send('Container ' + req.params.containerid + ' is created');
}).catch(function (error){
res.send(error);
});
});
router.get('/:accountid/:containerid', function(req, res, next){
console.log("listing objects for " + req.params.accountid + " in " + req.params.containerid);
var selectStatement = businessNetworkConnection.buildQuery('SELECT org.acme.objectstore.Object WHERE (container == _$class)');
var accountContainerId = req.params.accountid + "/" + req.params.containerid;
businessNetworkConnection.query(selectStatement, { "class": "resource:org.acme.objectstore.Container#" + accountContainerId })
.then(function(results) {
res.send(String(results));
}).catch(function (error){
res.send(error);
});
});
router.put('/:accountid/:containerid/:objectid', function(req, res, next){
console.log("object creation request received with id=" + req.params.objectid + " and value=" + req.body);
let serializer = businessNetworkDefinition.getSerializer();
let account_id = "resource:org.acme.objectstore.Account#" + req.params.accountid;
let account_container_id = "resource:org.acme.objectstore.Container#" + req.params.accountid + "/" + req.params.containerid;
let resource = serializer.fromJSON({
'$class': 'org.acme.objectstore.CreateObject',
'account': account_id,
'container': account_container_id,
'objectId': req.params.objectid,
'value': req.body
});
businessNetworkConnection.submitTransaction(resource)
.then(function(){
res.send('Object ' + req.params.objectid + ' is created');
}).catch(function(error){
res.send("failed to create the object");
});
});
router.get('/:accountid/:containerid/:objectid', function(req, res, next){
console.log("showing value of object " + req.params.objectid + " for " + req.params.accountid + " in " + req.params.containerid);
var selectStatement = businessNetworkConnection.buildQuery('SELECT org.acme.objectstore.Object WHERE (accountContainerObjectId == _$id)');
var accountContainerObjectId = req.params.accountid + "/" + req.params.containerid + "/" + req.params.objectid;
businessNetworkConnection.query(selectStatement, { "id": accountContainerObjectId })
.then(function(results) {
res.send(String(results[0].value));
}).catch(function (error){
res.send(error);
});
});
module.exports = router;
オブジェクトストレージを動かしてみた
では、作成したオブジェクトストレージネットワークとRESTサーバを起動してテストしてみましょう。オブジェクトストレージネットワークのコードはこちら、RESTサーバのコードはこちらに公開しています。
まず、Hyperledger Fabricを起動してオブジェクトストレージネットワークをデプロイします。Hyperledger Fabricの起動方法はこちらをご覧ください。オブジェクトストレージネットワークのコードをgit clone
したら、まずはコードをビルドして.bna
ファイルを作成します。
$ cd object-storage-network
$ npm install
.bna
ファイルができたらHyperledger Fabricにデプロイします。
$ cd dist
$ composer network deploy -a object-storage-network.bna -p hlfv1 -i PeerAdmin -s adminpw
以上で、オブジェクトストレージネットワークを起動することができました!
次に、RESTサーバを起動します。RESTサーバのコードをgit clone
したら以下の手順で起動します。
$ cd object-storage-app
$ npm install
$ npm start
では、オブジェクトストレージをテストしてみましょう。まずはアカウントを作成します。
$ curl -X PUT http://localhost:3000/testuser
Account testuser is created
続いて、このユーザが所有するコンテナを二つ作成します。
$ curl -X PUT http://localhost:3000/testuser/container0
Container container0 is created
$ curl -X PUT http://localhost:3000/testuser/container1
Container container1 is created
ここで一旦、このアカウントが所有するコンテナの一覧を取得してみます。オブジェクトストレージネットワークに対するクエリの結果を文字列に変換してそのまま表示しているため、見栄えはイマイチですがコンテナの一覧を取得できたことがわかります。
$ curl -X GET http://localhost:3000/testuser
Resource {id=org.acme.objectstore.Container#testuser/container0},Resource {id=org.acme.objectstore.Container#testuser/container1}
では、一方のコンテナにオブジェクトを二つ作成してみましょう。
$ curl -X PUT http://localhost:3000/testuser/container0/obj0 -H "Content-Type: text/html; charset=UTF-8" -d "hello"
Object obj0 is created
$ curl -X PUT http://localhost:3000/testuser/container0/obj1 -H "Content-Type: text/html; charset=UTF-8" -d "hello hello"
Object obj1 is created
このコンテナの中に保存されているオブジェクトの一覧を取得してみます。
$ curl -X GET http://localhost:3000/testuser/container0
Resource {id=org.acme.objectstore.Object#testuser/container0/obj0},Resource {id=org.acme.objectstore.Object#testuser/container0/obj1}
オブジェクトが二つ保存されていることが確認できました。ついで、オブジェクトのデータを読み出してみます。
$ curl -X GET http://localhost:3000/testuser/container0/obj0
hello
$ curl -X GET http://localhost:3000/testuser/container0/obj1
hello hello
無事にデータを取り出すことができました!
おわりに
Hyperledger Composerを使うことで、簡単にブロックチェーン・ベースのオブジェクトストレージを開発することができました。普段、Webアプリケーションを開発している方はJavaScriptに慣れていると思われるため、Hyperledger Composerを使うとブロックチェーンのチェーンコードも同じ言語で記述できて嬉しいですよね!
今後はこのブロックチェーン・ベースのオブジェクトストレージの開発を継続して、OpenStack Swiftkと互換性のあるようなAPIを提供できるようにしたいと思います。