ReactとHyperledger Irohaを使ってデジタル通貨の超簡易決済処理を実装する。
※ 前提として、デジタル通貨なんて大げさな言葉を使っていますが、今回 は、Hyperledger Irohaを利用して発行した独自の通貨を指すものとします
注意事項!!
この中で利用している公開鍵及び秘密鍵の情報は全て開発用として生成しています。絶対に本番環境では利用しないようにしてください!
【参考にさせていただいた書籍】
https://www.ohmsha.co.jp/book/9784274224737/
【今回作成したアプリのソースコード】
【システムの概要図(ポンチ絵レベルです。。)】
iroha_front_appとAPIサーバーは、ReactやExpress、Hyperledger Irohaの公式ドキュメントの参照の他、たくさんの方々のブログ記事などを参考にさせていただき、作成してみました。
irohaの3つのノードと3つのDB(PostgreSQL)のコンテナについては、docker-composeを利用して構築しています。今回は超簡易的に作成したこともあり商用アプリに比べたら本当に小規模ですが3つのノードを立ててブロックチェーンネットワークを構成しています。また、ブロックチェーンネットワークは仮想マシン上に構築しました。
iroha_front_appは、クライアントアプリで主にReactとTypeScriptを使って作成しています。画面デザインについては、Muiコンポーネントを利用いたしました。(本当に使いやすくて便利!しかもある程度は整えられる!)
APIサーバーは、主にExpressを使って作成しています。APIが定義してあります。APIの内容については、上記のソースコード内にswagger用のyamlファイルがあるのでそちらをswagger editorなどで見ていただくと分かりやすいと思います。
次に今回採用したブロックチェーン「Hyperledger Iroha」について簡単にご紹介させていただきます。このHyperledger Irohaは、カンボジアのCBDCであるバコンにも採用されているもので、オリジナルソースコードを開発したのは日本のITベンダーソラミツ株式会社です!!
会社のHPのURLはこちらになります。
それではまず、Hyperledger Irohaの概要と3つの基本的な概念から。
Hyperledger Iroha
hyperledgerプロジェクトで4番目に採択されたブロックチェーンで、オリジナルコードについては、ソラミツ株式会社が開発した。ターミナルからの操作についてはコマンドでの操作となるが、対話式のインターフェースなのでCLIでもとても操作しやすい。
Hyperledger Irohaの基本的な概念
①ドメイン:
グループ化や区分けを実現するための抽象化する概念のこと
②アセット:
アカウントが取引や蓄積を行う資産の種類のこと
Hyperledger Iroha上では複数のアセットを取り扱うことが可能
③アカウント:
アセットをやりとりすることができる。
アカウントIDは、「アカウント名@ドメイン」と表現される。
権限とロール
Hyperledger Irohaには、53もの権限が存在し、大きく命令タイプと問い合わせタイプに分類できる。また、権限設定をまとめたものがロールとなる。
APIも命令タイプのものと問い合わせタイプのものに分類できる。
World State View
Hyperledger Irohaでは、全てのデータをブロックチェーン内に格納するわけではなく、PostgreaSQLのDB内に格納されるブロックチェーン外で保持しなければならない情報があり、これをWorld State Viewという。16個のテーブルが存在し、Hyperledger Irohaで構成されるブロックチェーンの最新の情報を保持する。テーブルの一覧についてはこちらを参照。
ジェネシスブロック
ビットコインやイーサリアムと同様にHyperledger Irohaでも一番最初のブロックとしてジェネシスブロックを用意する必要があるが、いわゆるパブリックブロックチェーンタイプのジェネシスブロックとは少しだけ必要な設定が異なっている。Hyperledger Irohaは、コンソーシアム型のブロックチェーンなのでジェネシスブロックにはあらかじめノードとなるpeerの情報を記載しておく必要がある。(IPアドレス(もしくはFQDN)と公開鍵)
ジェネシスブロックの例は、こちらを参照。
ブロックチェーンを最初に動かす準備としてあらかじめ鍵ペアを作成し、公開鍵をジェネシスブロックに登録するのはコンソーシアム型ブロックチェーンならではの設定であると思う。ちなみに今回、ノード用の鍵とアカウント用の鍵の生成にはエドワーズ曲線デジタル署名アルゴリズム(ED25519)という署名アルゴリズムを利用している。
鍵生成用のソースコードは、「KeyCreate」というモジュールにまとめており、アカウント生成時はAPIを介して鍵を生成する。
/**
* キーペア作成コンポーネント
* @param アカウントID
* @param ドメイン
* @returns 生成したアカウントの公開鍵
*/
const Keycreate = function (account, domain) {
// fsモジュールをインスタンス化
const fs = require('fs');
// 公開鍵を格納(初期化)
let public_key = ''
// 秘密鍵を格納(初期化)
let private_key = ''
// 設定ファイルの読み込み(環境によって変化する。)
const ConfigFile = require('config');
// キーペアのディレクトリ(環境によって変化する。)
const KEY_DIR = ConfigFile.config.dev_key_dir;
// ed25519オブジェクト作成
let ed25519 = require('ed25519.js')
// キーペア作成
let keys = ed25519.createKeyPair()
// public key セット
let pub = keys.publicKey
// private key セット
let priv = keys.privateKey
for (var i = 0; i < 32; i++) {
// 配列を文字列に変換
public_key = public_key + pub[i].toString(16).padStart(2, '0')
}
for (var i = 0; i < 32; i++) {
// 配列を文字列に変換
private_key = private_key + priv[i].toString(16).padStart(2, '0')
}
// console.log('public Key :', public_key)
// console.log('private Key:', private_key)
//公開鍵をファイルに書き出し
fs.writeFile(KEY_DIR + account + '@' + domain + '.pub', public_key , function (err) {
if (err) {
throw err
}
})
//秘密鍵をファイルに書き出し
fs.writeFile(KEY_DIR + account + '@' + domain + '.priv', private_key , function (err) {
if (err) {
throw err
}
})
return public_key;
};
module.exports = { Keycreate };
アカウントの鍵は、Metamaskのイメージで個人でしっかり管理する必要がありますが、ノード用の鍵も出てくるとなると管理する対象の鍵が増えるので運用面で課題がありそうですね。。本当に秘密鍵の管理をどうにかしないと商用利用した時もすぐにセキュリティインシデントにつながりそうな感じがします。今回は、開発用ということもあり、鍵ペアのファイルなどは全て公開していますが、本番ではいかに秘密鍵を守るかを考えないといけなそうです。ここらへんのノウハウは、ぜひ暗号資産交換所の担当者様やdouble jumptokyo社が発表したNSuiteの担当者様とお話ししてみたいと思いました。(もちろん、簡単に公開できるものではないと思いますが開発していて改めて秘密鍵の管理の難しさを感じました。)
N Suiteの公式HPは、下記の通りです。
その他、HSMベンダーで有名なThaless社とフレセッツ社(今は買収されてHashPort社)が公表している暗号資産向けの秘密鍵管理ソリューションなんかもとても参考になりそうだと感じました。
オフラインで管理することがとても重要であることは暗号資産が流行った時からずっと言われていることですが、個人で管理すると絶対に紛失しそうですよね。。
APIサーバー
APIサーバー用のファイルserver.jsには、6種類のAPIとテスト用のAPIが定義してあり、後はサーバーを起動させるための設定が記載しています。
ブロックチェーンとDBのそれぞれで処理を実施する必要があり、例えばブロックチェーン上の処理を行い、ブロック高を取得したその情報をDBに挿入するといった風な流れです。今回、この部分を自分で実装したことでブロックチェーンとDBを連動させてシステムを構築する基本的な流れをイメージすることができたことはすごく大きなことだと感じています。上記にも記載いたしましたが、yamlファイルがあるのでswagger editorでご覧いただくことも可能です。
/**
* Hyperledger Iroha用のAPIサーバー設定ファイル
*/
// Webサーバーの起動
const express = require('express');
const app = express();
// ポート番号
const portNo = 3001;
// 接続するデータベース名
const database1 = 'reidai';
const database2 = 'iroha_default';
// 起動
app.listen(portNo, () => {
console.log('起動しました', `http://localhost:${portNo}`)
});
// 外部プロセス呼び出し用に使用する。
let exec = require('child_process').exec;
// 暗号化用のモジュールを読み込む
const crypto = require('crypto');
// DB接続用のモジュールを読みこむ
const pgHelper = require('./server/db/pgHelper');
// 鍵生成用のモジュールを読み込む
const Keycreate = require('./server/key/KeyCreate');
// 鍵取得用のモジュールを読み込む
const GetPrivKey = require('./server/key/GetPrivKey');
// APIの定義
/**
* テスト用API
*/
app.get('/api/test', (req, res) => {
// SQL文
const query = req.query.query;
const values = req.query.values;
// DBの実行
pgHelper.execute(database1, query, values, (err, docs) => {
if (err) {
console.log(err.toString());
res.status(500).send("テスト用API実行失敗");
return;
}
console.log('取得結果:', docs.rows);
res.json({ roles: docs.rows });
});
});
/**
* キーペアを生成し、公開鍵を取得するためのAPI
*/
app.get('/api/publickey', (req, res) => {
// 公開鍵用の変数
let publicKey ='';
try {
// 公開鍵を取得する。
publicKey = Keycreate.Keycreate();
res.json({ publicKey: publicKey });
} catch(err) {
console.log('exec error: ' + err);
res.status(500).send("公開鍵取得中にエラーが発生しました。");
}
});
/**
* 新規会員情報を挿入するためのAPI
*/
app.get('/api/input', (req, res) => {
// パラメータから値を取得する。
let domain = req.query.domain;
let accountId = req.query.accountId;
let name = req.query.name;
let kana = req.query.kana;
let tel = req.query.tel;
let addr = req.query.adds;
let bd = req.query.bd;
let ed = req.query.ed;
let password = req.query.password;
// パスワードのハッシュ値を取得する。
let passHash = crypto.createHash('sha256').update(password).digest('hex');
// ブロック高用の変数を用意する。
let block = 0;
// 公開鍵を取得する。
let publicKey = Keycreate.Keycreate(accountId, domain);
// アカウント作成用のコマンドを作成
let COMMAND = ['node ./server/iroha/call/CreateAccountCall.js', domain, accountId, publicKey];
COMMAND = COMMAND.join(' ');
console.log('Execute COMMAND=', COMMAND);
// コマンドを実行する。
exec( COMMAND , function(error, stdout, stderr) {
if (error !== null) {
console.log('exec error: ' + error);
res.status(500).send("トランザクション作成中にエラーが発生しました");
return
}
console.log(stdout)
//ブロック位置を取得
if (stdout.match(/height: (\d+),/) !== null){
block = stdout.match(/height: (\d+),/)[1];
console.log("block:", block);
} else {
//キーファイルより公開鍵を取得
block = (2^64)+1
}
// 実行するSQL
const query = 'INSERT INTO kaiin_info (id,name,kana,addr,tel,bd,ed,block,password) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)';
// パラメータ用の配列を作成する。
const values = [ accountId + '@' + domain, name, kana, addr, tel, bd, ed, block, passHash ];
// DBの実行
pgHelper.execute(database1, query, values, (err, docs) => {
if (err) {
console.log(err.toString());
res.status(501).send("DB接続中にエラーが発生しました");
return;
}
// res.json({ roles: docs.rows });
});
});
});
/**
* チャージ処理用API
*/
app.get('/api/charge', (req, res) => {
// パラメータから値を取得する。
const prepay = req.query.prepay;
const counter = req.query.counter;
const total = req.query.total;
const accountId = req.query.accountId;
const domain = req.query.domain;
// メッセージ
const msg = "charge";
// アカウントの秘密鍵を取得する。
const privateKey = GetPrivKey.GetPrivKey(accountId, domain);
// アカウント作成用のコマンドを作成
let COMMAND = ['node ./server/iroha/call/ChargeAssetCall.js', prepay, counter, total, domain, accountId + '@' + domain, privateKey];
COMMAND = COMMAND.join(' ');
console.log('Execute COMMAND=', COMMAND);
// ブロック高用の変数
let block = 0;
// コマンドを実行する。
exec( COMMAND , function(error, stdout, stderr) {
if (error !== null) {
console.log('exec error: ' + error);
res.status(500).send("トランザクション作成中に発生しました。");
return
}
console.log(stdout)
//ブロック位置を取得
if (stdout.match(/height: (\d+),/) !== null){
block = stdout.match(/height: (\d+),/)[1];
console.log("block:", block);
} else {
//キーファイルより公開鍵を取得
block = (2^64)+1
}
// 実行するSQL
const query = 'INSERT INTO shiharai_info (id,prepay,ticket,total,shisetsu,ninzu,usetime,job) VALUES ($1,$2,$3,$4,$5,$6,$7,$8)';
// パラメータ用の配列を作成する。
const values = [ accountId + '@' + domain, prepay, counter, total, '-', 0, 0, msg ];
// DBの実行
pgHelper.execute(database1, query, values, (err, docs) => {
if (err) {
console.log(err.toString());
res.status(501).send("DB接続中にエラーが発生しました。");
return;
}
// res.json({ roles: docs.rows });
});
});
});
/**
* 支払処理用API
*/
app.get('/api/pay', (req, res) => {
// パラメータから値を取得する。
const prepay = req.query.prepay;
const counter = req.query.counter;
const total = req.query.total;
const accountId = req.query.accountId;
const domain = req.query.domain;
const room = req.query.room;
const people = req.query.people;
const usetime = req.query.usetime;
// メッセージ
const msg = "pay";
// アカウントの秘密鍵を取得する。
const privateKey = GetPrivKey.GetPrivKey(accountId, domain);
// アセット送金用のコマンドを作成
let COMMAND = ['node ./server/iroha/call/PayAssetCall.js', prepay, counter, total, domain, accountId + '@' + domain, privateKey, msg];
COMMAND = COMMAND.join(' ');
console.log('Execute COMMAND=', COMMAND);
// ブロック高用の変数
let block = 0;
// コマンドを実行する。
exec( COMMAND , function(error, stdout, stderr) {
if (error !== null) {
console.log('exec error: ' + error);
res.status(500).send("トランザクション作成中にエラーが発生しました");
return
}
console.log(stdout)
//ブロック位置を取得
if (stdout.match(/height: (\d+),/) !== null){
block = stdout.match(/height: (\d+),/)[1];
console.log("block:", block);
} else {
//キーファイルより公開鍵を取得
block = (2^64)+1
}
// 実行するSQL
const query = 'INSERT INTO shiharai_info (id,prepay,ticket,total,shisetsu,ninzu,usetime,job) VALUES ($1,$2,$3,$4,$5,$6,$7,$8)';
// パラメータ用の配列を作成する。
const values = [ accountId + '@' + domain, prepay, counter, total, room, people, usetime, msg ];
// DBの実行
pgHelper.execute(database1, query, values, (err, docs) => {
if (err) {
console.log(err.toString());
res.status(500).send("DB接続中にエラーが発生しました");
return;
}
console.log('実行結果:', docs);
// res.json({ roles: docs.rows });
});
});
});
/**
* 取引履歴照会用API
*/
app.get('/api/getTxHistory', (req, res) => {
// パラメータから値を取得する。
const accountId = req.query.accountId;
const domain = req.query.domain;
// 実行するSQL
const query = 'select no, id, prepay, ticket, total, shisetsu, ninzu, usetime, job from shiharai_info where id = $1';
// パラメータ用の配列を作成する。
const values = [ accountId + '@' + domain ];
// DBの実行
pgHelper.execute(database1, query, values, (err, docs) => {
if (err) {
console.log(err.toString());
res.status(501).send("DB接続中にエラーが発生しました");
return;
}
console.log('実行結果:', docs.rows);
res.status(200).send(docs.rows);
});
});
/**
* IDとパスワードを値を検証するAPI
*/
app.post('/api/login', (req, res) => {
// パラメータから値を取得する。
const accountId = req.query.accountId;
const domain = req.query.domain;
const password = req.query.password;
// パスワードのハッシュ値を取得する。
const passHash = crypto.createHash('sha256').update(password).digest('hex');
// 実行するSQL
const query = 'select * from kaiin_info where id = $1 and password = $2';
// パラメータ用の配列を作成する。
const values = [ accountId + '@' + domain, passHash ];
// DBの実行
pgHelper.execute(database1, query, values, (err, docs) => {
if (err) {
console.log(err.toString());
res.status(500).send("DB接続中にエラーが発生しました");
return;
}
// console.log('実行結果:', docs.rows);
res.status(200).send(docs.rows);
});
});
// 静的ファイルを自動的に返すようルーティングする。
app.use('/input', express.static('./build'));
app.use('/pay', express.static('./build'));
app.use('/charge', express.static('./build'));
app.use('/login', express.static('./build'));
app.use('/txHistory', express.static('./build'));
app.use('/', express.static('./build'));
iroha_front_app
Reactとtypescriptを中心に開発したクライアントアプリになります。
画面数は、ログイン画面を含めて7画面のアプリケーションになります。
基本的にブロックチェーンやDBに関連する処理は、APIサーバーにリクエストを投げるだけになっています。
useContextを利用して、ログイン済みの時のルーティングと未ログインの時のルーティングを切り替えるようにしています。画面のレイアウトなどは基本的にMuiの公式サイトをベースに構築しました。
return (
<AuthOperationContext.Provider value={{login, logout}}>
<AuthUserContext.Provider value={authUser}>
<Router>
<Box sx={{ flexGrow: 1 }}>
<AppBar position="static">
<Toolbar variant="dense">
<Typography variant="h5" color="inherit" component="div" sx={{ flexGrow: 1 }}>
{title}
</Typography>
{ isAuthenticated ? <Button color="inherit" onClick={logout}>ログアウト</Button> : <></> }
</Toolbar>
</AppBar>
</Box>
<div className={classes.root}>
{ !isAuthenticated ? <UnAuthRoute /> : <PrivateRoute /> }
</div>
</Router>
</AuthUserContext.Provider>
</AuthOperationContext.Provider>
)
では、デモを含めながら各画面について紹介していきたいと思います。
【ログイン画面】
すでにアカウントを作成済みであれば、IDとパスワードを入力すればログインできます。
【新規会員登録画面】
アカウントがない場合や秘密鍵を紛失した時はこちらからアカウントを作成することができます。
【ホーム画面】
ログイン後に最初に遷移する画面で、チャージか支払いか取引履歴を照会することができます。
【チャージ画面】
デジタル通貨をチャージするための画面です。
【支払い画面】
チャージしたデジタル通貨を使って、支払いを行う画面です。今回は、機能などはかなり限定的になっているので機能拡張は今後の課題です。
【取引履歴照会画面】
最後に取引履歴を照会する画面です。情報自体は、DBに登録されたものを引っ張ってきていますが、ブロックチェーン上にもブロックが生成されており、デジタル通貨も動いているので不正にDBの情報を改ざんしたとしてもブロックチェーン上との情報を照合すれば不正を見抜けると思います。
ちなみに支払いやチャージのタイミングでブロックが生成されますが内容は次の通りです。
最後に
一旦こっちはお休みして、solidityによるマルチシグウォレットの実装などを頑張ってみたいと思います。
すごい長文になってしまいましたが、少しでもブロックチェーンを利用したアプリの開発イメージを共有することができれば幸いです。また、間違いや助言等ありましたら、ご指摘いただけますと幸いです。
読んでいただきありがとうございました。