BBc-1で仮想通貨実装(アカウント型)
## 想定読者
- BBc-1がなんだかは知っているが、イマイチ使い方がわからないという人
- 公式チュートリアルはやったが、で通貨的なのってどうやって作るの?という人
環境
- macOS High Sierra(10.13.3)
- Python 3.6.2
- BBc-1 v1.0.0
- Flask v1.0.2
BBc-1のインストール手順はこちら
https://github.com/beyond-blockchain/bbc1/blob/develop/docs/BBc1_core_tutorial_installation_ja.md
Flaskのインストール手順はこちら
http://flask.pocoo.org/
設計
BBc-1ではアセットとトランザクションを分けて管理します。
アセットとは資産という意味ですが、ここでは管理したいファイルだと考えましょう。
トランザクションとはアセットの変更を記録する履歴、証拠です。
今回はアカウント型実装なので、台帳データのファイルをアセットと定義して実装していきます。
以下は、アセットに相当する台帳データのファイルです。JSON形式になっています。
<>で囲まれている箇所は実際にはデータが来ます。
{
'token_info':{
'name': <トークン名>,
'amount': <トークン発行量>,
'issue_date': <発行日>,
'issuer': <発行者>,
'expiration_date': <有効期限>
},
'users': {
<userのid>: <トークン保有量>
}
}
- ユーザー作成
- トークン作成
- トークンの送信
- トークンの換金(発展編)
- 残高確認
- 全トークン情報取得
- 改ざん検知・復旧(発展編)
といった機能の実装を通してBBc-1でのアプリケーション作成のイメージを持ってもらえたらいいなと考えています。
実装・ロジック
core起動
まずはcoreの起動から始めます。
# bbc1/bbc1/core以下にて
python bbc_core.py -w ./.bbc1 --no_nodekey
-w で./.bbc1を指定しています。
これがこのあとbbc1のcoreが使用するDBやアセットファイルの格納場所となります。
Finderで確認したい場合は、.で始まるファイルやディレクトリは隠れているので「Command+Shift+i」で見られるようになります。
また、--no_nodekeyとすることでnodekeyなしで起動できます。
今回は特に必要ないのでこのままいきます。
APIサーバー作成
無事起動が確認できたらアプリケーション側を作り込んでいきます。
今回はFlaskでAPIサーバーを作成します。
以下がディレクトリ構造です。
sample_bbc1
├─ config
│ └─ config.json #後で生成される
└─ app.py
まずはどこでもいいので作業用ディレクトリを作って適当な名前をつけてください。
こちらでは"sample_bbc1"という名前にしています。
次に、sample_bbc1/以下に'app.py'を作ります。
# coding=utf-8
from flask import Flask, jsonify, request
import requests
import binascii
import json
import datetime
import ast
from bbc1.core import bbclib, bbc_app
from bbc1.core.message_key_types import KeyType
from bbc1.core.bbc_error import *
asset_group_id = bbclib.get_new_id("default_group", include_timestamp=False)
app = Flask(__name__)
# 以後コードはここより上に書いてください
if __name__ == '__main__':
app.run(host='localhost', port=5000, debug=True)
asset_group_idとは、複数アセットをひとつのまとまりとして扱うものです。
今回は、全て同じasset_groupに属するものとしてグローバル変数としています。
次に、'config'ディレクトリを作成します。
このあと作るアセットやユーザー管理のファイルはここに格納していきます。
ドメイン作成
最初に、ドメイン作成をするAPIから用意していきます。
BBc-1におけるドメインとはユーザーやアセット管理の単位です。
単純に、組織を指すと考えてください。
app.pyに以下記述を書き足します。
@app.route('/domain/new', methods=['POST'])
def create_domain():
# パラメータからドメイン名を受け取る
domain_name = request.form['domain']
# ドメイン名を元にidを生成
domain_id = bbclib.get_new_id(domain_name)
domain_id_str = binascii.b2a_hex(domain_id).decode()
# 初期ユーザーとしてadminを作成する
user_id = bbclib.get_new_id("admin")
keypair = bbclib.KeyPair()
keypair.generate()
# adminのidと公開鍵ペアをcoreに登録
client = bbc_app.BBcAppClient()
client.set_domain_id(domain_id)
client.set_user_id(user_id)
client.set_keypair(keypair)
client.register_to_core()
client.domain_setup(domain_id)
client.unregister_from_core()
# 登録した情報は今後も使うのでconfig.jsonとして、configディレクトリに書き出す
domain_cfg = {
domain_id_str: {
"users": {
"admin": {
"user_id": binascii.b2a_hex(user_id).decode(),
"public_key": binascii.b2a_hex(keypair.public_key).decode(),
"private_key": binascii.b2a_hex(keypair.private_key).decode()
}
},
"assets": {}
}
}
with open("./config/config.json", mode='w') as f:
f.write(json.dumps(domain_cfg, indent=4))
return json.dumps(domain_cfg, indent=4)
BBc-1では、coreと通信するためのクライアントクラスが用意されており、BBcAppClientクラスが該当します。
このクラスをインスタンス化してユーザーの作成とドメインの作成をおこなっています。
ここで、Flaskを起動してみます。
python app.py
* Serving Flask app "app" (lazy loading)
* Environment: production
WARNING: Do not use the development server in a production environment.
Use a production WSGI server instead.
* Debug mode: on
* Running on http://localhost:5000/ (Press CTRL+C to quit)
* Restarting with stat
こんな感じで起動できていればOKです。
monkey.patch_all()で警告が出ていてもここでは気にすることはありません。
それでは、早速ドメイン作成をしてみましょう。
curl -X POST localhost:5000/domain/new -d 'domain=sample_domain'
{
"b20f62a6596595f18e433e9003cf12c5d4ca7c7c040d5c849dd756555d6fa61e": {
"users": {
"admin": {
"user_id": "1057a77f8cc0c9af3e36c806337e0ecd6c2a4d98b8245bf1c979b452e37fc19c",
"public_key": "04d3abe6334ed5bfa165efbb2766752eaa1ff3ba22f766e0ef63e015cf18dc5470fcfacaba0ed03d94d3956d84072ff6775fe5287bbbf08cfb743e0d7654475d42",
"private_key": "507602a39767b093e300c5a610eec74fe5f87d69aa6377bb24449f1d701e2e07"
}
},
"assets": {}
}
}
こんな感じのJSON文字列が返ってきていれば成功です。
ここでは、"b20f62a6596595f18e433e9003cf12c5d4ca7c7c040d5c849dd756555d6fa61e"というドメインが作られたことになります。
/bbc1/core/.bbc1/(coreを起動しているところ)を確認して、同名のディレクトリが生成されていれば問題ないです。
アセットやトランザクションデータ(デフォルトではSQLite)はこのディレクトリ内に作られていきます。
ユーザー作成
ドメインが作成できたら、次はユーザー作成です。
以下コードをapp.pyに追記してください。
@app.route('/user/new', methods=['POST'])
def new_user():
user_name = request.form['user_name']
with open("./config/config.json", mode='r') as f:
domain_cfg = json.load(f)
domain_id_str = [i for i in domain_cfg.keys()][0]
users = domain_cfg[domain_id_str]["users"]
if user_name in users:
return "同じ名前のユーザーが既に存在します"
user_id = bbclib.get_new_id(user_name)
keypair = bbclib.KeyPair()
keypair.generate()
# クライアントの作成
client = bbc_app.BBcAppClient()
client.set_domain_id(bytes(binascii.a2b_hex(domain_id_str)))
client.set_user_id(user_id)
client.set_keypair(keypair)
client.register_to_core()
user_value = {
"user_id": binascii.b2a_hex(user_id).decode(),
"public_key": binascii.b2a_hex(keypair.public_key).decode(),
"private_key": binascii.b2a_hex(keypair.private_key).decode()
}
domain_cfg[domain_id_str]["users"][user_name] = user_value
with open("./config/config.json", mode='w') as f:
f.write(json.dumps(domain_cfg, indent=4))
return json.dumps(domain_cfg, indent=4)
flaskを再起動してcurlで確認してみましょう。
curl -X POST localhost:5000/user/new -d 'user_name=sample_user1'
{
"b20f62a6596595f18e433e9003cf12c5d4ca7c7c040d5c849dd756555d6fa61e": {
"users": {
"admin": {
"user_id": "1057a77f8cc0c9af3e36c806337e0ecd6c2a4d98b8245bf1c979b452e37fc19c",
"public_key": "04d3abe6334ed5bfa165efbb2766752eaa1ff3ba22f766e0ef63e015cf18dc5470fcfacaba0ed03d94d3956d84072ff6775fe5287bbbf08cfb743e0d7654475d42",
"private_key": "507602a39767b093e300c5a610eec74fe5f87d69aa6377bb24449f1d701e2e07"
},
"sample_user1": {
"user_id": "b4df0246f424ee691d4893c1b574b8d9f23c2107e3985fe38f063d0ecc276bdc",
"public_key": "048fb0ba9b6010ad19ebb6a4a63aac2292a60e28733c56e64e20fcee007612dbd9654c8ffaeeaa6e8945194bafede91cadf7b7aa82dbdb02ff1cd549e139609d42",
"private_key": "b34d828fb5f9a72c967c5a4d922fdd2a874cd850ec60e60130135c0c3355f763"
}
},
"assets": {}
}
}
ユーザーのキーが追加されていればOKです。
ここでクライアントの作成を行っていますが、これは次回以降も必要なので関数化しましょう。
def setup_client(domain_cfg, domain_id_str=None, user_name=None):
private_key = bytes(binascii.a2b_hex(domain_cfg[domain_id_str]["users"][user_name]["private_key"]))
public_key = bytes(binascii.a2b_hex(domain_cfg[domain_id_str]["users"][user_name]["public_key"]))
keypair = bbclib.KeyPair(privkey=private_key, pubkey=public_key)
user_id = bytes(binascii.a2b_hex(domain_cfg[domain_id_str]["users"][user_name]["user_id"]))
client = bbc_app.BBcAppClient()
client.set_domain_id(bytes(binascii.a2b_hex(domain_id_str)))
client.set_user_id(user_id)
client.set_keypair(keypair)
client.register_to_core()
return client, keypair
トークン作成
そして、いよいよトークンの作成です。
トークンを作成にするにあたっては、アセットを保存する必要があります。
今後も使うので関数として定義します。
def store(client=None, asset=None, txid=None, body=None, keypair=None):
txobj = bbclib.make_transaction(relation_num=1, witness=True)
# txidが存在する=関連するアセットやトランザクションが既に存在する(更新系)
if txid:
query_id = client.search_transaction(txid)
response = client.callback.sync_by_queryid(query_id)
if response[KeyType.status] < ESUCCESS:
print("ERROR: ", response[KeyType.reason].decode())
return Exception(response[KeyType.reason].decode())
prev_tx = bbclib.BBcTransaction(deserialize=response[KeyType.transaction_data])
bbclib.add_relation_pointer(transaction=txobj, relation_idx=0, ref_transaction_id=prev_tx.transaction_id)
bbclib.add_relation_asset(txobj, relation_idx=0, asset_group_id=asset_group_id, user_id=client.user_id, asset_body=body, asset_file=asset)
txobj.witness.add_witness(client.user_id)
sig = txobj.sign(private_key=keypair.private_key, public_key=keypair.public_key)
txobj.witness.add_signature(user_id=client.user_id, signature=sig)
txobj.digest()
query_id = client.insert_transaction(txobj)
assert query_id
# ここの10はタイムアウト時間(秒)
response_data = client.callback.sync_by_queryid(query_id, 10)
if response_data[KeyType.status] < ESUCCESS:
print("ERROR: ", response_data[KeyType.reason].decode())
return Exception(response_data[KeyType.reason].decode())
return response_data[KeyType.transaction_id], txobj.relations[0].asset.asset_id
そして、トークン作成用のAPIです。
@app.route('/token/new', methods=['POST'])
def make_token():
token_name = request.form['token_name']
user_name = request.form['user_name']
amount = int(request.form['amount'])
exp_date = datetime.datetime.strptime(request.form['exp_date'], "%Y/%m/%d").strftime('%Y/%m/%d')
with open("./config/config.json", mode='r') as f:
domain_cfg = json.load(f)
domain_id_str = [i for i in domain_cfg.keys()][0]
if token_name in domain_cfg[domain_id_str]["assets"]:
return "Error. Same token_name already exists."
client, keypair = setup_client(domain_cfg, domain_id_str=domain_id_str, user_name=user_name)
user_id = domain_cfg[domain_id_str]["users"][user_name]["user_id"]
asset_data = {
"token_info": {
"token_name": token_name,
"issue_amount": amount,
"expiration_date": exp_date,
"issuer": user_id,
"issue_date": datetime.datetime.now().strftime("%Y/%m/%d")
},
"users": {
user_id: amount
}
}
txid, asset_id = store(client=client, asset=str(asset_data).encode('utf-8'), body=str(asset_data).encode('utf-8'), keypair=keypair)
domain_cfg[domain_id_str]["assets"][token_name] = {
"transaction_id": binascii.b2a_hex(txid).decode(),
"asset_id": binascii.b2a_hex(asset_id).decode()
}
with open("./config/config.json", mode='w') as f:
f.write(json.dumps(domain_cfg, indent=4))
return binascii.b2a_hex(asset_id).decode()
これも、curlで実行します。
curl -X POST localhost:5000/token/new -d 'token_name=tokenA&amount=1000&user_name=sample_user1&exp_date=2018/12/31'
謎の文字列が返ってきていると思いますが、これがasset_idです。
なお、トークン名はtokenA, 発行量は1000, 発行者はsample_user1, 有効期限は2018/12/31としました。
アセットがきちんと保存されているか見に行きましょう。
.bbc1//になにかディレクトリが出来ています。
そのディレクトリの中に先ほどのasset_id名のファイルが作られているのでエディタで中身を確認します。
{'token_info': {'token_name': 'tokenA', 'issue_amount': 1000, 'expiration_date': '2018/12/31', 'issuer': 'b4df0246f424ee691d4893c1b574b8d9f23c2107e3985fe38f063d0ecc276bdc', 'issue_date': '2018/09/12'}, 'users': {'b4df0246f424ee691d4893c1b574b8d9f23c2107e3985fe38f063d0ecc276bdc': 1000}}
こんな感じになっていれば正しくアセットが保存されています。
発展編で必要なのでtokenBも作成しましょう。
curl -X POST localhost:5000/token/new -d 'token_name=tokenB&amount=1000&user_name=sample_user1&exp_date=2018/12/31'
トークン送信
次に、作ったトークンを送ってみます。
ここでasset_idからトランザクションを検索するために、search_by_asset_idという関数を定義しています。
ここではアセットファイルの取得のために使っています。
def search_by_asset_id(client, asset_id):
query_id = client.search_transaction_with_condition(asset_id=asset_id)
response_data = client.callback.sync_by_queryid(query_id, 10)
if response_data[KeyType.status] < ESUCCESS:
print("ERROR: ", response_data[KeyType.reason].decode())
return "error"
asset_files = response_data[KeyType.all_asset_files]
asset_file = ast.literal_eval(asset_files[asset_id].decode('utf-8'))
return asset_file
@app.route('/token/update', methods=['POST'])
def send_token():
token_name = request.form['token_name']
sender = request.form['sender']
receiver = request.form['receiver']
amount = int(request.form['amount'])
with open("./config/config.json", mode='r') as f:
domain_cfg = json.load(f)
domain_id_str = [i for i in domain_cfg.keys()][0]
client, keypair = setup_client(domain_cfg, domain_id_str=domain_id_str, user_name=sender)
asset_id = bytes(binascii.a2b_hex(domain_cfg[domain_id_str]["assets"][token_name]["asset_id"]))
asset_file = search_by_asset_id(client, asset_id)
receiver_id = domain_cfg[domain_id_str]["users"][receiver]["user_id"]
sender_id = domain_cfg[domain_id_str]["users"][sender]["user_id"]
# 送り主から送金額を引き、送り相手に足す
asset_file["users"][sender_id] -= amount
receiver_amount = asset_file["users"].setdefault(receiver_id, 0)
asset_file["users"][receiver_id] = receiver_amount + amount
txid, asset_id = store(client=client, asset=str(asset_file).encode('utf-8'), body=str(asset_file).encode('utf-8'), keypair=keypair)
domain_cfg[domain_id_str]["assets"][token_name] = {
"transaction_id": binascii.b2a_hex(txid).decode(),
"asset_id": binascii.b2a_hex(asset_id).decode()
}
with open("./config/config.json", mode='w') as f:
f.write(json.dumps(domain_cfg, indent=4))
return binascii.b2a_hex(asset_id).decode()
sample_user1からadminにtokenAを10だけ送ってみます。
curl -X POST localhost:5000/token/update -d 'token_name=tokenA&amount=10&sender=sample_user1&receiver=admin'
>> 3ddda9c87652794c979e2480f836c469e03da6c9a252168ee8407535f795b2d7
成功していればasset_idが返ってくるので、.bbc1//以下の同名ファイルの中をエディタで確認します。
{'token_info': {'token_name': 'tokenA', 'issue_amount': 1000, 'expiration_date': '2018/12/31', 'issuer': 'b4df0246f424ee691d4893c1b574b8d9f23c2107e3985fe38f063d0ecc276bdc', 'issue_date': '2018/09/12'}, 'users': {'b4df0246f424ee691d4893c1b574b8d9f23c2107e3985fe38f063d0ecc276bdc': 990, '1057a77f8cc0c9af3e36c806337e0ecd6c2a4d98b8245bf1c979b452e37fc19c': 10}}
1000発行したうち、10送ったので990になっていれば成功です。
途中、再度アセットを保存する際のコードがありました。
txid, asset_id = store(client=client, asset=str(asset_file).encode('utf-8'), body=str(asset_file).encode('utf-8'), keypair=keypair)
ここでbodyにassetと同じデータを送っていますが、body部にはバイト型であればなんでも入れられる便利スペースとなっています。
したがって、ここには任意の文字列を格納しておくことも可能です。
msg = "トークンの送信".encode('utf-8')
txid, asset_id = store(client=client, asset=str(asset_file).encode('utf-8'), body=msg, keypair=keypair)
としても構いません。
トークン保有量取得
ここまででPOST系のAPIは一通り終わったので、GET系の実装に移ります。
ユーザー名からそのユーザーの全トークンの残高を取得するAPIを作ります。
@app.route('/user/<user_name>', methods=['GET'])
def query_user_balance(user_name=None):
with open("./config/config.json", mode='r') as f:
domain_cfg = json.load(f)
domain_id_str = [i for i in domain_cfg.keys()][0]
user_id = domain_cfg[domain_id_str]["users"][user_name]["user_id"]
client, _ = setup_client(domain_cfg, domain_id_str=domain_id_str, user_name=user_name)
asset_ids = [bytes(binascii.a2b_hex(domain_cfg[domain_id_str]["assets"][asset]["asset_id"])) for asset in domain_cfg[domain_id_str]["assets"].keys()]
asset_files = [search_by_asset_id(client, asset_id) for asset_id in asset_ids]
user_balance = dict()
for asset in asset_files:
user_balance[asset["token_info"]["token_name"]] = asset["users"].setdefault(user_id, 0)
return jsonify({'status': 'success', 'msg': user_balance})
curlでGETします。
curl -X GET localhost:5000/user/sample_user1
{
"msg": {
"tokenA": 990,
"tokenB": 1000
},
"status": "success"
}
tokenAを990, tokenBを1000持っていることがわかりました。成功です。
トークン一覧取得
次に、トークンの情報の一覧が欲しいので作成します。
@app.route('/token/all', methods=['GET'])
def query_all_tokens():
with open("./config/config.json", mode='r') as f:
domain_cfg = json.load(f)
domain_id_str = [i for i in domain_cfg.keys()][0]
client, _ = setup_client(domain_cfg, domain_id_str=domain_id_str, user_name="admin")
asset_ids = [bytes(binascii.a2b_hex(domain_cfg[domain_id_str]["assets"][asset]["asset_id"])) for asset in domain_cfg[domain_id_str]["assets"].keys()]
asset_files = [search_by_asset_id(client, asset_id) for asset_id in asset_ids]
all_tokens = [token["token_info"] for token in asset_files]
return jsonify({'status': 'success', 'msg': all_tokens})
早速curlで検証します。
curl -X GET localhost:5000/token/all
{
"alter": null,
"msg": [
{
"expiration_date": "2018/12/31",
"issue_amount": 1000,
"issue_date": "2018/09/12",
"issuer": "b4df0246f424ee691d4893c1b574b8d9f23c2107e3985fe38f063d0ecc276bdc",
"token_name": "tokenA"
},
{
"expiration_date": "2020/01/01",
"issue_amount": 1000,
"issue_date": "2018/09/12",
"issuer": "b4df0246f424ee691d4893c1b574b8d9f23c2107e3985fe38f063d0ecc276bdc",
"token_name": "tokenB"
}
],
"status": "success"
}
きちんとtokenA, tokenBの情報が取得できていますね。
発展編
アトミックなトランザクション
ここまでは一度のトランザクションで、一つのアセット(トークン)しか操作しませんでしたが、実運用では複数アセットを取り扱うことは十分考えられます。
その際、トランザクションをアトミックなものにしたいと思うので、その方法について解説していきます。
まず現状のstore関数では一つのアセットしか想定していないので、変更していきます。
def store(client=None, assets=None, txids=None, body=None, keypair=None):
#relationをアセットの数だけ用意する
txobj = bbclib.make_transaction(relation_num=len(assets), witness=True)
# txidsが存在する=関連するアセットやトランザクションが既に存在する
if txids:
for i, txid in enumerate(txids):
query_id = client.search_transaction(txid)
response = client.callback.sync_by_queryid(query_id)
if response[KeyType.status] < ESUCCESS:
print("ERROR: ", response_data[KeyType.reason].decode())
return None
# 一つ前の関連トランザクション
prev_tx = bbclib.BBcTransaction(deserialize=response[KeyType.transaction_data])
bbclib.add_relation_pointer(transaction=txobj, relation_idx=i, ref_transaction_id=prev_tx.transaction_id)
for i, asset in enumerate(assets):
bbclib.add_relation_asset(txobj, relation_idx=i, asset_group_id=asset_group_id, user_id=client.user_id, asset_body=body, asset_file=asset)
txobj.witness.add_witness(client.user_id)
sig = txobj.sign(private_key=keypair.private_key, public_key=keypair.public_key)
txobj.witness.add_signature(user_id=client.user_id, signature=sig)
txobj.digest()
query_id = client.insert_transaction(txobj)
assert query_id
response_data = client.callback.sync_by_queryid(query_id, 10)
if response_data[KeyType.status] < ESUCCESS:
print("ERROR: ", response_data[KeyType.reason].decode())
return None
return response_data[KeyType.transaction_id], [txobj.relation.asset.asset_id for txobj.relation in txobj.relations]
引数のassetがassetsになっており、Pythonなのでわかりづらいですが、リストを期待しています。
txidsも同様です。
渡されたtxidsでそれぞれ検索をしてトランザクションの存在を確認してref_transaction_idに入れてポインタとしています。
BBc-1ではトランザクション内にポインタとして過去のトランザクションや関連するトランザクションのidをいれることが出来ます。
こうすることで関連トランザクションを数珠つなぎにたどることができるようになります。
例えばポインタとして一つ前のトランザクションを入れていけば、あるアセットがどのような変更履歴を持つのか辿ることができます。
トークン変換
では、この新しいstore関数を使って、複数アセットの変更を1トランザクションにまとめてみます。
トークンを変換するAPIを用意します。
@app.route('/token/exchange', methods=['POST'])
def exchange_token():
token1 = request.form['token1']
token2 = request.form['token2']
user = request.form['user']
amount = int(request.form['amount'])
with open("./config/config.json", mode='r') as f:
domain_cfg = json.load(f)
domain_id_str = [i for i in domain_cfg.keys()][0]
client, keypair = setup_client(domain_cfg, domain_id_str=domain_id_str, user_name=user)
token1_id = bytes(binascii.a2b_hex(domain_cfg[domain_id_str]["assets"][token1]["asset_id"]))
token2_id = bytes(binascii.a2b_hex(domain_cfg[domain_id_str]["assets"][token2]["asset_id"]))
txids = [bytes(binascii.a2b_hex(domain_cfg[domain_id_str]["assets"][token]["transaction_id"])) for token in [token1, token2]]
token_ids = [token1_id, token2_id]
assets = [search_by_asset_id(client, token_id) for token_id in token_ids]
user_id = domain_cfg[domain_id_str]["users"][user]["user_id"]
assets[0]["users"][user_id] -= amount
token2_amount = assets[1]["users"].setdefault(user_id, 0)
assets[1]["users"][user_id] = token2_amount + amount
asset_files = [str(asset).encode('utf-8') for asset in assets]
txid, asset_ids = store(client=client, assets=asset_files, txids=txids, body=str("トークン交換").encode('utf-8'), keypair=keypair)
domain_cfg[domain_id_str]["assets"][token1] = {
"transaction_id": binascii.b2a_hex(txid).decode(),
"asset_id": binascii.b2a_hex(asset_ids[0]).decode()
}
domain_cfg[domain_id_str]["assets"][token2] = {
"transaction_id": binascii.b2a_hex(txid).decode(),
"asset_id": binascii.b2a_hex(asset_ids[1]).decode()
}
with open("./config/config.json", mode='w') as f:
f.write(json.dumps(domain_cfg, indent=4))
return binascii.b2a_hex(txid).decode()
store呼び出し時に渡す引数assetsにasset_filesというアセットファイルデータのリストを渡しています。
curlで検証します。
curl -X POST localhost:5000/token/exchange -d 'token1=tokenA&token2=tokenB&user=sample_user1&amount=10'
トークンが交換されているか確認します。
curl -X GET localhost:5000/user/sample_user1
{
"msg": {
"tokenA": 980,
"tokenB": 1010
},
"status": "success"
}
きちんと変換されていることが確認できたらOKです。
改ざん検知・修復
最後に、BBc-1の改ざん検知機能と修復機能について説明します。
BBc-1にはトランザクションとアセットの改ざん検知機能が備わっています。
ここでいう「改ざん」とは、署名検証に失敗する改ざんのことです。
coreはトランザクションを検索するたびに署名を検証するようになっています。
ここでいう署名とは、BBc-1のトランザクションデータのダイジェスト値であるtransaction_idだと考えてください。
詳しいことは公式のprogramminge guide
https://github.com/beyond-blockchain/bbc1/blob/develop/docs/BBc1_programming_guide_v1.0_ja.md
または、How BBc1 Worksのダイジェスト計算(P46あたり)と改ざんからの修復(P55)を参照してください。
https://github.com/beyond-blockchain/bbc1/blob/develop/docs/How_BBc1_works_v1.0.2_ja.pdf
改ざん検知自体は検索(client.search_transaction_with_condition(asset_group_id=asset_group_id)等)を実行するたびに自動で行っています。
なので、こちらで行なうのは、検索に対するcoreからのレスポンスデータの確認を行い、改ざんから修復するか否かを決定することです。
以下は解説のためのソースコードを表示しているので、まだapp.pyに記述する必要はありません。
query_id = client.search_transaction(txid)
response = client.callback.sync_by_queryid(query_id)
if response[KeyType.status] < ESUCCESS:
print(response[KeyType.reason].decode())
if KeyType.compromised_transactions in response:
print("トランザクションの改竄を検知")
if KeyType.compromised_asset_files in response_data:
print("アセットファイルの改竄を検知")
sync_by_queryidで返ってきたレスポンス内のキーにKeyType.compromised_transactionsが存在していれば、トランザクションの改ざんが検知されたということです。
同様に、KeyType.compromised_asset_filesが存在していれば、アセットの改ざんが検知されたということになります。
改ざん検知を確認できたら、次におこないたいのは改ざんからの復旧です。
詳しい説明は公式ドキュメントに任せるとして、基本的に復旧の方法は「他のDBやcoreに問い合わせて、正しいものがあればそのデータを送ってもらう」というものです。
修復の仕方はシンプルで、BBc-1のクライアントのrequest_to_repair_asset関数にasset_group_idとasset_idを渡せばアセットの復旧は可能です。
トランザクションも同様の手順で復旧可能なのですが、v1.0.1時点で、response[KeyType.compromised_transactions]で返ってくるものは、トランザクションのデータ本体で、transaction_idはわかりません。(2018/9/18現在 解決案をプルリクしました)
とりあえず今回は、アセットの改ざん検知と復旧機能を実装していきましょう。
では、検索をおこなっている部分で改ざん検知をする処理を追加していきましょう。
該当箇所は、search_by_asset_id、store、exchange_tokenの3箇所です。
改ざん検知後に復旧する関数としてrepairを定義します。
repair関数の戻り値altersは辞書型で、改ざん検知をしたasset_idと復旧できたか否かを返してくれます。
ただし、現状アセットファイルは一箇所でしか管理していないので改ざんを検知できたとしても修復することは不可能です。
def repair(client=None, compromised_data=None, count=1000, alters=dict()):
if not "assets" in alters:
alters = {"assets":{}}
for comp_ast in compromised_data:
client.request_to_repair_asset(asset_group_id, comp_ast)
comp_ast_str = binascii.b2a_hex(comp_ast).decode()
alters["assets"][comp_ast_str] = True
# NOTE: request_to_repair_transaction()とrequest_to_repair_asset()には一切の応答メッセージはないため、
# 再度searchメソッドを呼んで復旧が完了したかを確認している
query_id = client.search_transaction_with_condition(asset_id=comp_ast, count=count)
res = client.callback.sync_by_queryid(query_id)
if res[KeyType.status] < ESUCCESS:
print("アセットファイルを復旧できませんでした")
if KeyType.reason in res:
print("ERROR: ", res[KeyType.reason].decode())
raise Exception("アセットファイルが改ざんされ、復旧できなかったので取引中止")
alters["assets"][comp_ast_str] = False
# レスポンス内に改ざんアセットファイルが存在していて、そのアセットが復旧しようとしたものだった場合、復旧失敗
if KeyType.compromised_asset_files in res and comp_ast in res[KeyType.compromised_asset_files]:
print("アセットファイルを復旧できませんでした")
alters["assets"][comp_ast_str] = False
raise Exception("アセットファイルが改ざんされ、復旧できなかったので取引中止")
return alters
def search_by_asset_id(client, asset_id, alters=dict()):
query_id = client.search_transaction_with_condition(asset_id=asset_id)
response_data = client.callback.sync_by_queryid(query_id, 10)
if response_data[KeyType.status] < ESUCCESS:
print("ERROR: ", response_data[KeyType.reason].decode())
return "error"
if KeyType.compromised_asset_files in response_data:
print("アセットファイルの改竄を検知")
alters = repair(client=client, compromised_data=response_data[KeyType.compromised_asset_files], count=1, alters=dict())
txdata_array = response_data[KeyType.transactions]
txs = list()
for tx in txdata_array:
txs.append(bbclib.BBcTransaction(deserialize=tx))
asset_files = response_data[KeyType.all_asset_files]
asset_file = ast.literal_eval(asset_files[asset_id].decode('utf-8'))
return asset_file, alters
def store(client=None, assets=None, txids=None, body=None, keypair=None, alters=dict()):
txobj = bbclib.make_transaction(relation_num=len(assets), witness=True)
# txidsが存在する=関連するアセットやトランザクションが既に存在する(更新系)
if txids:
for i, txid in enumerate(txids):
query_id = client.search_transaction(txid)
response = client.callback.sync_by_queryid(query_id)
if response[KeyType.status] < ESUCCESS:
print(response[KeyType.reason].decode())
if KeyType.compromised_asset_files in response:
print("アセットファイルの改竄を検知")
alters = repair(client=client, compromised_data=response_data[KeyType.compromised_asset_files], count=1)
prev_tx = bbclib.BBcTransaction(deserialize=response[KeyType.transaction_data])
bbclib.add_relation_pointer(transaction=txobj, relation_idx=i, ref_transaction_id=prev_tx.transaction_id)
for i, asset in enumerate(assets):
bbclib.add_relation_asset(txobj, relation_idx=i, asset_group_id=asset_group_id, user_id=client.user_id, asset_body=body, asset_file=asset)
txobj.witness.add_witness(client.user_id)
sig = txobj.sign(private_key=keypair.private_key, public_key=keypair.public_key)
txobj.witness.add_signature(user_id=client.user_id, signature=sig)
txobj.digest()
query_id = client.insert_transaction(txobj)
assert query_id
response_data = client.callback.sync_by_queryid(query_id, 10)
if response_data[KeyType.status] < ESUCCESS:
print("ERROR: ", response_data[KeyType.reason].decode())
return None
return response_data[KeyType.transaction_id], [txobj.relation.asset.asset_id for txobj.relation in txobj.relations], alters
@app.route('/token/exchange', methods=['POST'])
def exchange_token():
token1 = request.form['token1']
token2 = request.form['token2']
user = request.form['user']
amount = int(request.form['amount'])
with open("./config/config.json", mode='r') as f:
domain_cfg = json.load(f)
domain_id_str = [i for i in domain_cfg.keys()][0]
client, keypair = setup_client(domain_cfg, domain_id_str=domain_id_str, user_name=user)
token1_id = bytes(binascii.a2b_hex(domain_cfg[domain_id_str]["assets"][token1]["asset_id"]))
token2_id = bytes(binascii.a2b_hex(domain_cfg[domain_id_str]["assets"][token2]["asset_id"]))
token_ids = [token1_id, token2_id]
assets = list()
alters = None
for token_id in token_ids:
query_id = client.search_transaction_with_condition(asset_id=token_id, count=1)
res = client.callback.sync_by_queryid(query_id, 10)
if res[KeyType.status] < ESUCCESS:
print("ERROR: ", res[KeyType.reason].decode())
if KeyType.compromised_asset_files in res:
print("アセットファイルの改竄を検知")
alters = repair(client=client, compromised_data=res[KeyType.compromised_asset_files], count=1, alters=dict())
txdata_array = res[KeyType.transactions]
txs = list()
for tx in txdata_array:
txs.append(bbclib.BBcTransaction(deserialize=tx))
asset_files = res[KeyType.all_asset_files]
asset_files = ast.literal_eval(asset_files[token_id].decode('utf-8'))
assets.append(asset_files)
user_id = domain_cfg[domain_id_str]["users"][user]["user_id"]
assets[0]["users"][user_id] -= amount
token2_amount = assets[1]["users"].setdefault(user_id, 0)
assets[1]["users"][user_id] =+ token2_amount
asset_files = [str(asset).encode('utf-8') for asset in assets]
txid, asset_ids, alter_data = store(client=client, assets=asset_files, body=str("トークン変換").encode('utf-8'), keypair=keypair)
domain_cfg[domain_id_str]["assets"][token1] = {
"transaction_id": binascii.b2a_hex(txid).decode(),
"asset_id": binascii.b2a_hex(asset_ids[0]).decode()
}
domain_cfg[domain_id_str]["assets"][token2] = {
"transaction_id": binascii.b2a_hex(txid).decode(),
"asset_id": binascii.b2a_hex(asset_ids[1]).decode()
}
with open("./config/config.json", mode='w') as f:
f.write(json.dumps(domain_cfg, indent=4))
return jsonify({'txid': binascii.b2a_hex(txid).decode(), 'alters': alters })
これで改ざん検知とその復旧、複数アセットのトランザクション登録ができるようになりました。
それでは、実際にアセットを改ざんしてみましょう。
coreが起動しているディレクトリの'.bbc1/domain_id/asset_group_id/'にアセットファイルがあります。
tokenAの最新アセットのidをconfig.json(.bbc1直下ではなく、Flaskのある方の)確認し、asset_idと一致するファイルを見つけます。
そうしたら、そのファイルをテキストエディタで編集してみましょう。
編集が終わったら試しにtokenAからtokenBに1トークン変換してみましょう。
curl -X POST localhost:5000/token/update -d 'token_name=tokenA&amount=10&sender=sample_user1&receiver=admin'
"Exception: アセットファイルが改ざんされ、復旧できなかったので取引中止"というエラーが返ってきます。
修復はできませんが、改ざんを検知できたのでOKです。
このままだと、storeやsearch_by_asset_idなどの戻り値が変更されているので動かない関数があります。
では、直していきましょう。
@app.route('/token/new', methods=['POST'])
def make_token():
token_name = request.form['token_name']
user_name = request.form['user_name']
amount = int(request.form['amount'])
exp_date = datetime.datetime.strptime(request.form['exp_date'], "%Y/%m/%d").strftime('%Y/%m/%d')
with open("./config/config.json", mode='r') as f:
domain_cfg = json.load(f)
domain_id_str = [i for i in domain_cfg.keys()][0]
if token_name in domain_cfg[domain_id_str]["assets"]:
return "Error. Same token_name already exists."
client, keypair = setup_client(domain_cfg, domain_id_str=domain_id_str, user_name=user_name)
user_id = domain_cfg[domain_id_str]["users"][user_name]["user_id"]
asset_data = {
"token_info": {
"token_name": token_name,
"issue_amount": amount,
"expiration_date": exp_date,
"issuer": user_id,
"issue_date": datetime.datetime.now().strftime("%Y/%m/%d")
},
"users": {
user_id: amount
}
}
txid, asset_ids, alters = store(client=client, assets=[str(asset_data).encode('utf-8')], body=str(asset_data).encode('utf-8'), keypair=keypair)
domain_cfg[domain_id_str]["assets"][token_name] = {
"transaction_id": binascii.b2a_hex(txid).decode(),
"asset_id": binascii.b2a_hex(asset_ids[0]).decode()
}
with open("./config/config.json", mode='w') as f:
f.write(json.dumps(domain_cfg, indent=4))
return jsonify({'status': 'success', 'asset_id': binascii.b2a_hex(asset_ids[0]).decode(), 'alters': alters})
@app.route('/token/update', methods=['POST'])
def send_token():
token_name = request.form['token_name']
sender = request.form['sender']
receiver = request.form['receiver']
amount = int(request.form['amount'])
with open("./config/config.json", mode='r') as f:
domain_cfg = json.load(f)
domain_id_str = [i for i in domain_cfg.keys()][0]
client, keypair = setup_client(domain_cfg, domain_id_str=domain_id_str, user_name=sender)
asset_id = bytes(binascii.a2b_hex(domain_cfg[domain_id_str]["assets"][token_name]["asset_id"]))
asset_file, _ = search_by_asset_id(client, asset_id)
receiver_id = domain_cfg[domain_id_str]["users"][receiver]["user_id"]
sender_id = domain_cfg[domain_id_str]["users"][sender]["user_id"]
# 送り主から送金額を引き、送り相手に足す
asset_file["users"][sender_id] -= amount
receiver_amount = asset_file["users"].setdefault(receiver_id, 0)
asset_file["users"][receiver_id] = receiver_amount + amount
txid, asset_ids, alters = store(client=client, assets=[str(asset_file).encode('utf-8')], body=str(asset_file).encode('utf-8'), keypair=keypair)
domain_cfg[domain_id_str]["assets"][token_name] = {
"transaction_id": binascii.b2a_hex(txid).decode(),
"asset_id": binascii.b2a_hex(asset_ids[0]).decode()
}
with open("./config/config.json", mode='w') as f:
f.write(json.dumps(domain_cfg, indent=4))
return jsonify({'status': 'success', 'asset_id': binascii.b2a_hex(asset_ids[0]).decode(), 'alters': alters})
@app.route('/user/<user_name>', methods=['GET'])
def query_user_balance(user_name=None):
with open("./config/config.json", mode='r') as f:
domain_cfg = json.load(f)
domain_id_str = [i for i in domain_cfg.keys()][0]
user_id = domain_cfg[domain_id_str]["users"][user_name]["user_id"]
client, _ = setup_client(domain_cfg, domain_id_str=domain_id_str, user_name=user_name)
asset_ids = [bytes(binascii.a2b_hex(domain_cfg[domain_id_str]["assets"][asset]["asset_id"])) for asset in domain_cfg[domain_id_str]["assets"].keys()]
asset_files = list()
for asset_id in asset_ids:
asset_file, _ = search_by_asset_id(client, asset_id)
asset_files.append(asset_file)
user_balance = dict()
for asset in asset_files:
user_balance[asset["token_info"]["token_name"]] = asset["users"].setdefault(user_id, 0)
return jsonify({'status': 'success', 'msg': user_balance})
これにて終了です。
お疲れ様でした。
全ソースコード
# coding=utf-8
from flask import Flask, jsonify, request
import requests
import binascii
import json
import datetime
import ast
from bbc1.core import bbclib, bbc_app
from bbc1.core.message_key_types import KeyType
from bbc1.core.bbc_error import *
asset_group_id = bbclib.get_new_id("default_group", include_timestamp=False)
app = Flask(__name__)
def repair(client=None, compromised_data=None, count=1000, alters=dict()):
if not "assets" in alters:
alters = {"assets":{}}
for comp_ast in compromised_data:
client.request_to_repair_asset(asset_group_id, comp_ast)
comp_ast_str = binascii.b2a_hex(comp_ast).decode()
alters["assets"][comp_ast_str] = True
# NOTE: request_to_repair_transaction()とrequest_to_repair_asset()には一切の応答メッセージはないため、
# 再度searchメソッドを呼んで復旧が完了したかを確認している
query_id = client.search_transaction_with_condition(asset_id=comp_ast, count=count)
res = client.callback.sync_by_queryid(query_id)
if res[KeyType.status] < ESUCCESS:
print("アセットファイルを復旧できませんでした")
if KeyType.reason in res:
print("ERROR: ", res[KeyType.reason].decode())
raise Exception("アセットファイルが改ざんされ、復旧できなかったので取引中止")
alters["assets"][comp_ast_str] = False
# レスポンス内に改ざんアセットファイルが存在していて、そのアセットが復旧しようとしたものだった場合、復旧失敗
if KeyType.compromised_asset_files in res and comp_ast in res[KeyType.compromised_asset_files]:
print("アセットファイルを復旧できませんでした")
alters["assets"][comp_ast_str] = False
raise Exception("アセットファイルが改ざんされ、復旧できなかったので取引中止")
return alters
def setup_client(domain_cfg, domain_id_str=None, user_name=None):
private_key = bytes(binascii.a2b_hex(domain_cfg[domain_id_str]["users"][user_name]["private_key"]))
public_key = bytes(binascii.a2b_hex(domain_cfg[domain_id_str]["users"][user_name]["public_key"]))
keypair = bbclib.KeyPair(privkey=private_key, pubkey=public_key)
user_id = bytes(binascii.a2b_hex(domain_cfg[domain_id_str]["users"][user_name]["user_id"]))
client = bbc_app.BBcAppClient()
client.set_domain_id(bytes(binascii.a2b_hex(domain_id_str)))
client.set_user_id(user_id)
client.set_keypair(keypair)
client.register_to_core()
return client, keypair
def store(client=None, assets=None, txids=None, body=None, keypair=None, alters=dict()):
txobj = bbclib.make_transaction(relation_num=len(assets), witness=True)
# txidsが存在する=関連するアセットやトランザクションが既に存在する(更新系)
if txids:
for i, txid in enumerate(txids):
query_id = client.search_transaction(txid)
response = client.callback.sync_by_queryid(query_id)
if response[KeyType.status] < ESUCCESS:
print(response[KeyType.reason].decode())
if KeyType.compromised_asset_files in response:
print("アセットファイルの改竄を検知")
alters = repair(client=client, compromised_data=response[KeyType.compromised_asset_files], count=1)
prev_tx = bbclib.BBcTransaction(deserialize=response[KeyType.transaction_data])
bbclib.add_relation_pointer(transaction=txobj, relation_idx=i, ref_transaction_id=prev_tx.transaction_id)
for i, asset in enumerate(assets):
bbclib.add_relation_asset(txobj, relation_idx=i, asset_group_id=asset_group_id, user_id=client.user_id, asset_body=body, asset_file=asset)
txobj.witness.add_witness(client.user_id)
sig = txobj.sign(private_key=keypair.private_key, public_key=keypair.public_key)
txobj.witness.add_signature(user_id=client.user_id, signature=sig)
txobj.digest()
query_id = client.insert_transaction(txobj)
assert query_id
response_data = client.callback.sync_by_queryid(query_id, 10)
if response_data[KeyType.status] < ESUCCESS:
print("ERROR: ", response_data[KeyType.reason].decode())
return None
return response_data[KeyType.transaction_id], [txobj.relation.asset.asset_id for txobj.relation in txobj.relations], alters
def search_by_asset_id(client, asset_id, alters=dict()):
query_id = client.search_transaction_with_condition(asset_id=asset_id)
response_data = client.callback.sync_by_queryid(query_id, 10)
if response_data[KeyType.status] < ESUCCESS:
print("ERROR: ", response_data[KeyType.reason].decode())
return "error"
if KeyType.compromised_asset_files in response_data:
print("アセットファイルの改竄を検知")
alters = repair(client=client, compromised_data=response_data[KeyType.compromised_asset_files], count=1, alters=dict())
txdata_array = response_data[KeyType.transactions]
txs = list()
for tx in txdata_array:
txs.append(bbclib.BBcTransaction(deserialize=tx))
asset_file = ast.literal_eval(response_data[KeyType.all_asset_files][asset_id].decode('utf-8'))
return asset_file, alters
@app.route('/domain/new', methods=['POST'])
def create_domain():
# パラメータからドメイン名を受け取る
domain_name = request.form['domain']
# ドメイン名を元にidを生成
domain_id = bbclib.get_new_id(domain_name)
domain_id_str = binascii.b2a_hex(domain_id).decode()
# 初期ユーザーとしてadminを作成する
user_id = bbclib.get_new_id("admin")
keypair = bbclib.KeyPair()
keypair.generate()
# adminのidと公開鍵ペアをcoreに登録
client = bbc_app.BBcAppClient()
client.set_domain_id(domain_id)
client.set_user_id(user_id)
client.set_keypair(keypair)
client.register_to_core()
client.domain_setup(domain_id)
client.unregister_from_core()
# 登録した情報は今後も使うのでconfig.jsonとして、configディレクトリに書き出す
domain_cfg = {
domain_id_str: {
"users": {
"admin": {
"user_id": binascii.b2a_hex(user_id).decode(),
"public_key": binascii.b2a_hex(keypair.public_key).decode(),
"private_key": binascii.b2a_hex(keypair.private_key).decode()
}
},
"assets": {}
}
}
with open("./config/config.json", mode='w') as f:
f.write(json.dumps(domain_cfg, indent=4))
return json.dumps(domain_cfg, indent=4)
@app.route('/user/new', methods=['POST'])
def new_user():
user_name = request.form['user_name']
with open("./config/config.json", mode='r') as f:
domain_cfg = json.load(f)
domain_id_str = [i for i in domain_cfg.keys()][0]
users = domain_cfg[domain_id_str]["users"]
if user_name in users:
return "同じ名前のユーザーが既に存在します"
user_id = bbclib.get_new_id(user_name)
keypair = bbclib.KeyPair()
keypair.generate()
# クライアントの作成
client = bbc_app.BBcAppClient()
client.set_domain_id(bytes(binascii.a2b_hex(domain_id_str)))
client.set_user_id(user_id)
client.set_keypair(keypair)
client.register_to_core()
user_value = {
"user_id": binascii.b2a_hex(user_id).decode(),
"public_key": binascii.b2a_hex(keypair.public_key).decode(),
"private_key": binascii.b2a_hex(keypair.private_key).decode()
}
domain_cfg[domain_id_str]["users"][user_name] = user_value
with open("./config/config.json", mode='w') as f:
f.write(json.dumps(domain_cfg, indent=4))
return json.dumps(domain_cfg, indent=4)
@app.route('/token/new', methods=['POST'])
def make_token():
token_name = request.form['token_name']
user_name = request.form['user_name']
amount = int(request.form['amount'])
exp_date = datetime.datetime.strptime(request.form['exp_date'], "%Y/%m/%d").strftime('%Y/%m/%d')
with open("./config/config.json", mode='r') as f:
domain_cfg = json.load(f)
domain_id_str = [i for i in domain_cfg.keys()][0]
if token_name in domain_cfg[domain_id_str]["assets"]:
return "Error. Same token_name already exists."
client, keypair = setup_client(domain_cfg, domain_id_str=domain_id_str, user_name=user_name)
user_id = domain_cfg[domain_id_str]["users"][user_name]["user_id"]
asset_data = {
"token_info": {
"token_name": token_name,
"issue_amount": amount,
"expiration_date": exp_date,
"issuer": user_id,
"issue_date": datetime.datetime.now().strftime("%Y/%m/%d")
},
"users": {
user_id: amount
}
}
txid, asset_ids, alters = store(client=client, assets=[str(asset_data).encode('utf-8')], body=str(asset_data).encode('utf-8'), keypair=keypair)
domain_cfg[domain_id_str]["assets"][token_name] = {
"transaction_id": binascii.b2a_hex(txid).decode(),
"asset_id": binascii.b2a_hex(asset_ids[0]).decode()
}
with open("./config/config.json", mode='w') as f:
f.write(json.dumps(domain_cfg, indent=4))
return jsonify({'status': 'success', 'asset_id': binascii.b2a_hex(asset_ids[0]).decode(), 'alters': alters})
@app.route('/token/update', methods=['POST'])
def send_token():
token_name = request.form['token_name']
sender = request.form['sender']
receiver = request.form['receiver']
amount = int(request.form['amount'])
with open("./config/config.json", mode='r') as f:
domain_cfg = json.load(f)
domain_id_str = [i for i in domain_cfg.keys()][0]
client, keypair = setup_client(domain_cfg, domain_id_str=domain_id_str, user_name=sender)
asset_id = bytes(binascii.a2b_hex(domain_cfg[domain_id_str]["assets"][token_name]["asset_id"]))
asset_file, _ = search_by_asset_id(client, asset_id)
receiver_id = domain_cfg[domain_id_str]["users"][receiver]["user_id"]
sender_id = domain_cfg[domain_id_str]["users"][sender]["user_id"]
# 送り主から送金額を引き、送り相手に足す
asset_file["users"][sender_id] -= amount
receiver_amount = asset_file["users"].setdefault(receiver_id, 0)
asset_file["users"][receiver_id] = receiver_amount + amount
txid, asset_ids, alters = store(client=client, assets=[str(asset_file).encode('utf-8')], body=str(asset_file).encode('utf-8'), keypair=keypair)
domain_cfg[domain_id_str]["assets"][token_name] = {
"transaction_id": binascii.b2a_hex(txid).decode(),
"asset_id": binascii.b2a_hex(asset_ids[0]).decode()
}
with open("./config/config.json", mode='w') as f:
f.write(json.dumps(domain_cfg, indent=4))
return jsonify({'status': 'success', 'asset_id': binascii.b2a_hex(asset_ids[0]).decode(), 'alters': alters})
@app.route('/user/<user_name>', methods=['GET'])
def query_user_balance(user_name=None):
with open("./config/config.json", mode='r') as f:
domain_cfg = json.load(f)
domain_id_str = [i for i in domain_cfg.keys()][0]
user_id = domain_cfg[domain_id_str]["users"][user_name]["user_id"]
client, _ = setup_client(domain_cfg, domain_id_str=domain_id_str, user_name=user_name)
asset_ids = [bytes(binascii.a2b_hex(domain_cfg[domain_id_str]["assets"][asset]["asset_id"])) for asset in domain_cfg[domain_id_str]["assets"].keys()]
asset_files = list()
for asset_id in asset_ids:
asset_file, _ = search_by_asset_id(client, asset_id)
asset_files.append(asset_file)
user_balance = dict()
for asset in asset_files:
user_balance[asset["token_info"]["token_name"]] = asset["users"].setdefault(user_id, 0)
return jsonify({'status': 'success', 'msg': user_balance})
@app.route('/token/all', methods=['GET'])
def query_all_tokens():
with open("./config/config.json", mode='r') as f:
domain_cfg = json.load(f)
domain_id_str = [i for i in domain_cfg.keys()][0]
client, _ = setup_client(domain_cfg, domain_id_str=domain_id_str, user_name="admin")
asset_ids = [bytes(binascii.a2b_hex(domain_cfg[domain_id_str]["assets"][asset]["asset_id"])) for asset in domain_cfg[domain_id_str]["assets"].keys()]
asset_files = [search_by_asset_id(client, asset_id) for asset_id in asset_ids]
all_tokens = [token["token_info"] for token in asset_files]
return jsonify({'status': 'success', 'msg': all_tokens})
@app.route('/token/exchange', methods=['POST'])
def exchange_token():
token1 = request.form['token1']
token2 = request.form['token2']
user = request.form['user']
amount = int(request.form['amount'])
with open("./config/config.json", mode='r') as f:
domain_cfg = json.load(f)
domain_id_str = [i for i in domain_cfg.keys()][0]
client, keypair = setup_client(domain_cfg, domain_id_str=domain_id_str, user_name=user)
token1_id = bytes(binascii.a2b_hex(domain_cfg[domain_id_str]["assets"][token1]["asset_id"]))
token2_id = bytes(binascii.a2b_hex(domain_cfg[domain_id_str]["assets"][token2]["asset_id"]))
token_ids = [token1_id, token2_id]
assets = list()
alters = None
for token_id in token_ids:
query_id = client.search_transaction_with_condition(asset_id=token_id, count=1)
res = client.callback.sync_by_queryid(query_id, 10)
if res[KeyType.status] < ESUCCESS:
print("ERROR: ", res[KeyType.reason].decode())
if KeyType.compromised_asset_files in res:
print("アセットファイルの改竄を検知")
alters = repair(client=client, compromised_data=res[KeyType.compromised_asset_files], count=1, alters=dict())
txdata_array = res[KeyType.transactions]
txs = list()
for tx in txdata_array:
txs.append(bbclib.BBcTransaction(deserialize=tx))
asset_files = res[KeyType.all_asset_files]
asset_files = ast.literal_eval(asset_files[token_id].decode('utf-8'))
assets.append(asset_files)
user_id = domain_cfg[domain_id_str]["users"][user]["user_id"]
assets[0]["users"][user_id] -= amount
token2_amount = assets[1]["users"].setdefault(user_id, 0)
assets[1]["users"][user_id] =+ token2_amount
asset_files = [str(asset).encode('utf-8') for asset in assets]
txid, asset_ids, alter_data = store(client=client, assets=asset_files, body=str("兌換").encode('utf-8'), keypair=keypair)
domain_cfg[domain_id_str]["assets"][token1] = {
"transaction_id": binascii.b2a_hex(txid).decode(),
"asset_id": binascii.b2a_hex(asset_ids[0]).decode()
}
domain_cfg[domain_id_str]["assets"][token2] = {
"transaction_id": binascii.b2a_hex(txid).decode(),
"asset_id": binascii.b2a_hex(asset_ids[1]).decode()
}
with open("./config/config.json", mode='w') as f:
f.write(json.dumps(domain_cfg, indent=4))
return jsonify({'txid': binascii.b2a_hex(txid).decode(), 'alters': alters })
if __name__ == '__main__':
app.run(host='localhost', port=5000, debug=True)