IOST 次世代ブロックチェーンの旗手 ~Storage & LuckyBet~

Yuya Sugano
28 min readAug 10, 2019

--

IOSTはスケーラビリティの問題に対応するシャーディングや非中央集権性を確保するPoB(Proof-of-Believability)の実装という特徴を持つブロックチェーンです。Ethereum/EOS/TRONに続くDappsのパブリックなブロックチェーン基盤で、ローンチから1か月後にはEthereumのトランザクション数を超えました。既にインキュベーター、金融やゲームなど様々な分野のプレイヤーからなるコミュニティを形成しています。前回の記事ではEDSやPoBなどのアーキテクチャをまとめました。この記事では追加のチュートリアルを取り扱います。

Unleashing the power of blockchain

目次です。メインは太字の2つです(『IOSTとは』と『ローカルノードを起動』は再掲)。

  • IOSTとは
  • ローカルノードを起動
  • サンプルコード(Storage)
  • サンプルコード(LuckyBet)

IOSTとは

これまでのブロックチェーンの基盤にはTPS(Transaction Per Second)の低さに起因する問題がありました。IOSTは最大で 100,000 TPS までスケールすることを目指す次世代のブロックチェーンです。[1]

IOST is an ultra-fast, decentralised blockchain network based on the next-generation consensus algorithm “Proof of Believability” (PoB). Led by a team of proven founders and backed by world-class investors, our mission is to be the underlying architecture for online services that meets the security and scalability needs of a decentralized economy.

スケーラビリティの問題に対応するEDS(Efficient Distributed Sharding)という技術や、PoS(Proof-of-Stake)で問題となるコンセンサスの中央集権化に対応するPoB(Proof-of-Believability)という仕組みで注目を集めています。

EthereumのスマートコントラクトによるDApps(特にCryptoKittiesはEthereumのトランザクション全体の10%を占めると言われる)では、取引量の多さからネットワークが逼迫し、15 TPSの処理を巡ってガスコストが高騰する問題が発生しました。[2]

IOSTはライヴネットで 8000 TPS をベンチマークしており、Javascriptによるスマートコントラクトの開発サポートなど、オンラインサービスを実装するモダンなブロックチェーン基盤として利用しやすいように設計されています。IOSTでは以下の機能や特徴をビルトインで持っています。

  • スマートコントラクト実装(Dapps)
  • プライベートトランザクション
  • 非中央集権の分散型ストレージ
  • 分散型マーケットプレースのユーザ評価追跡
  • IoTノードと呼ばれる軽量なノード
  • ブロックチェーンからの適切な報酬分配

前回、前々回とIOSTの概要やPoBについて説明してきました。実際にJavascriptを使用したコーディングについて公式にあるサンプルコードを追加してみました。[3] [4]

同様にローカルノードを走らせてチュートリアルにある『Storage』と『LuckyBet』のサンプルを触ってみたいと思います。

ローカルノードを起動

ローカルノード(プライベート)を構築します。Dockerで公式イメージが提供されているのでそのまま使用します。

AWS(Amazon Web Services)マーケットプレイスにDocker環境およびiServerやiWalletが乗ったイメージがあるのでDocker環境がない方はAWSを利用しても良いかもしれません。[5]

IOST AMI runs iServer and iWallet natively

使用したDockerの環境です。

$ docker version
Client:
Version: 18.09.7
API version: 1.39
Go version: go1.10.8
Git commit: 2d0083d
Built: Thu Jun 27 17:56:17 2019
OS/Arch: linux/amd64
Experimental: falseServer: Docker Engine - Community
Engine:
Version: 18.09.7
API version: 1.39 (minimum version 1.12)
Go version: go1.10.8
Git commit: 2d0083d
Built: Thu Jun 27 17:23:02 2019
OS/Arch: linux/amd64
Experimental: false

公式のイメージは iostio/iost-node です。 docker pull iostio/iost-nodeとして最新のタグのイメージを取得してください。

$ docker pull iostio/iost-node
$ docker images
iostio/iost-node latest 66d694bc2c7b 4 weeks ago 216MB
centos latest 9f38484d220f 4 months ago 202MB

素直にDockerイメージを走らせます。Genesisファイルの作成とブロックの生成が始まることを確認してください。

$ docker run -p 30000-30003:30000-30003 iostio/iost-node
Info 2019-07-13 01:02:27.453 main.go:74 Config Information:
acc:
id: producer000
seckey: 1rA******
algorithm: ed25519
genesis: /var/lib/iserver/genesis
vm:
jspath: vm/v8vm/v8/libjs/
loglevel: ""
db:
ldbpath: /var/lib/iserver/storage/
snapshot:
enable: false
filepath: /var/lib/iserver/storage/snapshot.tar.gz
...debug:
listenaddr: 0.0.0.0:30003
version:
netname: debugnet
protocolversion: "1.0"
docker run -p 30000–30003:30000–30003 iostio/iost-node

詳細についてはこちらの記事が参考になります。[6]

サンプルコード(Storage)

前回のように adminアカウントをインポートし、Dockerイメージをバックグラウンドで走らせておきます(前回は — rm -d のオプションを付けずにDockerイメージを起動しています)。

$ docker run -it --rm -d -p 30000-30003:30000-30003 iostio/iost-node
$ iwallet account import admin 2yquS3ySrGWPEKywCPzX4RTJugqRh7kJSo5aehsLYPEWkUxBWA39oMrZ7ZxuM4fgyXYs2cPwh5n8aNNpH5x2VyK1
import account admin done

ストレージはIOSTに標準のキーバリューストアで、スマートコントラクトでの値の保存に使用できます。ここでご紹介するストレージは他のコントラクトのストレージを呼ぶことはできないローカルストレージ操作のオブジェクトです。※他のスマートコントラクトのストレージを呼ぶものはグローバルストレージとして提供されています

以下の公式チュートリアルを参考に進めます。[7]

storage.get でストレージから値を読み込み、 storage.putでローカルのストレージへキーバリューを保存できます。簡単ですね。

put(key, value, payer=contractOwner)

IOSTのコントラクトはJavascriptでコーディングできます。 storage.js という名前のファイルを作成してください。 value1 というキーに対して値を操作するプログラムを記述していきます。コントラクトデプロイ時に呼び出される init 関数で初期値である TEST を投入します。

class Storage {
init() {
storage.put("value1", "TEST")
}

read() {
return storage.get("value1")
}
change(someone) {
storage.put("value1", someone)
}
}
module.exports = Storage;

コンパイルして JSONで記述されたABI(Application Binary Interface)ファイルを出力します。 storage.js.abi というファイルが同じディレクトリに作成されていることを確認してください。

$ iwallet compile storage.js
Successfully generated abi file as: storage.js.abi

以下のような内容となっています。

{
"lang": "javascript",
"version": "1.0.0",
"abi": [
{
"name": "read",
"args": [],
"amountLimit": [],
"description": ""
},
{
"name": "change",
"args": [
"string"
],
"amountLimit": [],
"description": ""
}
]
}

publish を使用してローカルのブロックチェーンへコントラクトをデプロイします。前回記載したとおりメインネットではないため、 chain_id1020 を指定する必要があります。 1024 はメインネット用のIDで、iWalletのデフォルト値です。

$ iwallet --server localhost:30002 --account admin --chain_id=1020 publish storage.js storage.js.abi

スマートコントラクトのデプロイに成功すると、トランザクションハッシュとコントラクトに割り当てられたIDが返却されます(以下、太字)。

$ iwallet --server localhost:30002 --account admin --chain_id=1020 publish storage.js storage.js.abi
Connecting to server localhost:30002 ...
Sending transaction...
Transaction:
{
"time": "1565182787389606032",
"expiration": "1565182877389606032",
"gasRatio": 1,
"gasLimit": 1000000,
"delay": "0",
"chainId": 1020,
"actions": [
{
...
Transaction has been sent.
The transaction hash is: BFeSn74qX7yt3LMtCW2fRcsQf5GVs8fsYz5Qd3aLXEyV
Checking transaction receipt...
Transaction receipt:
{
"txHash": "BFeSn74qX7yt3LMtCW2fRcsQf5GVs8fsYz5Qd3aLXEyV",
"gasUsage": 254860,
"ramUsage": {
"ContractBFeSn74qX7yt3LMtCW2fRcsQf5GVs8fsYz5Qd3aLXEyV": "117",
"admin": "654"
},
"statusCode": "SUCCESS",
"message": "",
"returns": [
"[\"ContractBFeSn74qX7yt3LMtCW2fRcsQf5GVs8fsYz5Qd3aLXEyV\"]"
],
"receipts": [
]
}
SUCCESS! Transaction has been irreversible
The contract id is: ContractBFeSn74qX7yt3LMtCW2fRcsQf5GVs8fsYz5Qd3aLXEyV
  • Transaction hash: BFeSn74qX7yt3LMtCW2fRcsQf5GVs8fsYz5Qd3aLXEyV
  • Contract id: ContractBFeSn74qX7yt3LMtCW2fRcsQf5GVs8fsYz5Qd3aLXEyV

デプロイしたコントラクトを呼び出してみましょう。 TESTという文字列で初期化しています。結果が想定通りであることを確認してください。

$ iwallet --server localhost:30002 --account admin --chain_id=1020 call "ContractBFeSn74qX7yt3LMtCW2fRcsQf5GVs8fsYz5Qd3aLXEyV" "read" '[]'
...
Transaction receipt:
{
"txHash": "H91GHmyx4DfV98fUDRQBajLme62VroS2WBWJDXCeQJau",
"gasUsage": 33791,
"ramUsage": {
},
"statusCode": "SUCCESS",
"message": "",
"returns": [
"[\"TEST\"]"
]
,
"receipts": [
]
}

トランザクションハッシュから同じ内容をいつでも再現できます。

$ iwallet receipt BFeSn74qX7yt3LMtCW2fRcsQf5GVs8fsYz5Qd3aLXEyV

定義した changeメソッドを使ってストレージを更新してみます。 HELLO という文字列をバリューへ設定しました。

$ iwallet --server localhost:30002 --account admin --chain_id=1020 call "ContractBFeSn74qX7yt3LMtCW2fRcsQf5GVs8fsYz5Qd3aLXEyV" "change" '["HELLO"]'
...
Transaction receipt:
{
"txHash": "HPACxW2U9oaRrE9fcFUCanod2TkbSSp6HxQ46CN9tKZR",
"gasUsage": 33841,
"ramUsage": {
"ContractBFeSn74qX7yt3LMtCW2fRcsQf5GVs8fsYz5Qd3aLXEyV": "1"
},
"statusCode": "SUCCESS",
"message": "",
"returns": [
"[\"\"]"
]
,
"receipts": [
]
}

再度ストレージの確認を行ってみます。値が更新されていることを確認できました。

$ iwallet --server localhost:30002 --account admin --chain_id=1020 call "ContractBFeSn74qX7yt3LMtCW2fRcsQf5GVs8fsYz5Qd3aLXEyV" "read" '[]'
...
Transaction receipt:
{
"txHash": "HGEgY6MZ81oZwCaz5FjjGe3qFjhNS87qJ6kJF6wwinu5",
"gasUsage": 33801,
"ramUsage": {
},
"statusCode": "SUCCESS",
"message": "",
"returns": [
"[\"HELLO\"]"
]
,
"receipts": [
]
}

ストレージは以上です。

サンプルコード(LuckyBet)

LuckyBetは公式サイトの中でも実践的なアプリケーションで、賭けを題材としたサンプルゲームです。コードの全てを解説はしませんが、構造や関数、またテストのやり方を紹介していきます。

ゲームのルールや実行についてはサイトに記載があり、コード自体はgithub上にアップロードされています。[8]

ゲームのルールは以下の通りです。いま賭けへの参加人数を20人とします。

  1. IOSTアカウントを使って0から9の数字のいずれかに1–5 IOSTを賭けることができます。
  2. 20ベット集まると、ラッキーナンバーが開示され、数字を当てた人はベット総額の95%を分配によって獲得できます。残りの5%はトランザクション手数料として徴収されます。
  3. ラッキーナンバーはブロック高を10で除算した余り(剰余)です。直前のラッキーナンバーのブロックが少なくとも16ブロック以上離れているか、ブロックの親ハッシュを除算した余り(剰余)が0である必要があります。そうでない場合はラッキーナンバーは開示されません。

contract/lucky_bet.js で使用される変数や関数の説明をします。

  • 最大ユーザ数
const maxUserNumber = 20;

最大ユーザ数までベットされないと賭けの各ラウンドが終わらないため、テストする際には1や2など小さいユーザ数に変更してテストします。

  • コンストラクタ
init() {
storage.put("user_number", JSON.stringify(0));
storage.put("total_coins", JSON.stringify(0));
storage.put("last_lucky_block", JSON.stringify(-1));
storage.put("round", JSON.stringify(1));
this.clearUserValue()
}
clearUserValue() {
for (let i = 0; i < 10; i ++) {
storage.mapPut('table', i.toString(), JSON.stringify([]))
}
}

ストレージを使用して、参加しているユーザ数、ベットされている賭け金、 last_lucky_block 、現在のラウンド数を初期化しています。 clearUserValue はラッキーナンバーである0から9の数字に対するユーザのベット情報を保管するストレージマップを初期化します。

  • 賭け関数
bet(account, luckyNumber, coins, nonce) {
if (coins < 1 || coins > 5) {
console.log(coins);
throw new Error("bet coins should be >=1 and <= 5");
}
if (luckyNumber < 0 || luckyNumber > 9) {
throw new Error("lucky number should be >=0 and <= 9");
}
...

ユーザアカウントの賭け金と対象番号(0~9の数字でした)を table というストレージマップに保管していきます。

let table = JSON.parse(storage.mapGet('table', luckyNumber.toString()));
table.push({ account:account, coins : coins, nonce : nonce});
storage.mapPut('table', luckyNumber.toString(), JSON.stringify(table));

このストレージマップはこういう形をしているはずです。

storage.mapPut(‘table’, luckyNumber.toString(), JSON.stringify(table))

IOSTにおけるストレージマップはストレージオブジェクトで使用可能なタイプで、 (key, field, value) の組み合わせで値を格納することができます。[9]

賭けが十分に集まったところでラッキーナンバーの判定や報酬の計算を行います。まずストレージから last_lucky_block の値を取得していることが分かると思います(コントラクトをデプロイした段階では-1という初期値でした)。

現在のブロック高と前回のラッキーナンバーの出たブロックの差分が16以上離れている(つまり16ブロックに相当する時間が経過している)か、ブロックの親ハッシュを除算した余り(剰余)が0である場合に、現在のブロックをラッキーナンバーの出たブロック last_lucky_block としてストレージへ記録します。

if (userNumber >= maxUserNumber) {
const bi = JSON.parse(blockchain.blockInfo());
const bn = bi.number;
const ph = bi.parent_hash;
const lastLuckyBlock = JSON.parse(storage.get("last_lucky_block"));
if ( lastLuckyBlock < 0 || bn - lastLuckyBlock >= 16 || bn > lastLuckyBlock && ph[ph.length-1] % 16 === 0) {
storage.put("last_lucky_block", JSON.stringify(bn));
const round = JSON.parse(storage.get("round"));
this.getReward(bn % 10, round, bn, userNumber);
storage.put("user_number", JSON.stringify(0));
storage.put("total_coins", JSON.stringify(0));
this.clearUserValue();
storage.put("round", JSON.stringify(round + 1));
}
}

getReward 関数にラッキーナンバーを渡して、本ラウンドを閉じ、ラウンド番号を1加えて終了です。 getReward では報酬計算や分配を行います。

本筋から逸れますが blockchain.blockInfo() がどういった情報を返すか気になったので以下のコントラクトを作成して、テストしてみました。

class Info {
init() {
const bi = JSON.parse(blockchain.blockInfo());
const bn = bi.number;
const ph = bi.parent_hash;
storage.put("bi", JSON.stringify(bi));
storage.put("bn", JSON.stringify(bn));
storage.put("ph", JSON.stringify(ph));
}
read(something) {
return storage.get(something)
}
}
module.exports = Info

ブロック番号、親ハッシュ、time、ウィットネスが値として取得できるようです(以下、太字)。

$ iwallet --server localhost:30002 --account admin --chain_id=1020 call "Contract9V4KR8gUbWK1TbuZqGHmMDREnF8nBBgtcJDot1oBAzB5" "read" '["bi"]'Transaction receipt:
{
"txHash": "26K5c77QSpsFXSeX3T9DSWvdB5RvR7qW9bwfFTXDDMHz",
"gasUsage": 35506,
"ramUsage": {
},
"statusCode": "SUCCESS",
"message": "",
"returns": [
"[\"{\\\"number\\\":4428,\\\"parent_hash\\\":\\\"8tbm19qPBasvsUefiKbcRX7Pcg1t4TNV7winTt8baG9n\\\",\\\"time\\\":1565409232500296000,\\\"witness\\\":\\\"6sNQa7PV2SFzqCBtQUcQYJGGoU7XaB6R4xuCQVXNZe6b\\\"}\"]"
]
,
"receipts": [
]
}
  • 報酬関数
getReward(ln, round, height, total_number) {
const y = new Int64(100);
const x = new Int64(95);
const totalCoins = JSON.parse(storage.get("total_coins"));
const _tc = new Int64(totalCoins);
const tc = _tc.multi(x).div(y);
...

賭け金総額の95%を計算しています。ラッキーナンバーを当てたグループに分配される分のコインです。

const winTable = JSON.parse(storage.mapGet('table', ln.toString()));if (winTable !== undefined && winTable !== null) {
winTable.forEach(function (record) {
totalVal += record.coins;
kNum++;
});
}

winTable にラッキーナンバーを当てたグループのレコードを格納します。 totalVal にこのグループの賭け金総額が、 kNum にはレコード数が保存されるようになっています。これれは後の報酬分配の際に使用します。

次に報酬分配を行います。ラッキーナンバーである0から9まででループして、 table のストレージマップを走査していきます。

for (let i = 0; i < 10; i ++) {
let table = [];
if(i === ln) {
table = winTable;
table.forEach(function (record) {
const reward = (tc.multi(record.coins).div(totalVal));
blockchain.withdraw(record.account, reward, "");
record.reward = reward.toString();
result.records.push(record)
})
}
else {
table = JSON.parse(storage.mapGet('table', i.toString()));
table.forEach(function (record) {
result.records.push(record)
})
}
}

太字の箇所で報酬計算と支払いを行っています。あるIOSTアカウントの報酬は賭け金総額とラッキーナンバーを当てたグループ内での対象IOSTアカウントの賭け金比率で計算できます。

IOSTアカウントの報酬 = 賭け金総額 * (IOSTアカウントの賭け金 / ラッキーナンバーを当てたグループの賭け金総額)

result のJSONは各ラウンドの結果をストレージへ保存するために使用されます。以下のような構造となるはずです。

let result = {
number: 4428,
user_number: 20,
k_number: 5,
total_coins : 15,
records : [{ account:account, coins : coins, nonce : nonce}, { account:account, coins : coins, nonce : nonce}, { account:account, coins : coins, nonce : nonce}]
};

テストのやり方

デモは luckbet.py を実行することで確認できます。 chain_id がデフォルトなので、ローカルノード上へデプロイして確認する場合は変更する必要があります。

22~23行目のコマンドプレフィックス内にローカルサーバと chain_id の指定を加えます。

command_prefix = f'iwallet --server localhost:30002 --expiration {DEFAULT_EXPIRATION} --gas_limit {DEFAULT_GASLIMIT} --gas_ratio {DEFAULT_GASRATIO} --amount_limit "iost:3000000|ram:10000" --chain_id=1020'

44~46行目の create_account 関数内にも同様の追加を施してください。

def create_account(creator, account_name, initial_ram, initial_gas_pledge, initial_balance, verbose=False):
call(f'{command_prefix} --server localhost:30002 --account {creator} account create {account_name} '
+ f'--initial_ram {initial_ram} --initial_gas_pledge {initial_gas_pledge} --initial_balance {initial_balance} --chain_id=1020', verbose)

pythonのコードを実行します。

$ python luckbet.py

以上、追加サンプルのご紹介でした。

まとめ

  • IOST はスマートコントラクトが動作するパブリックブロックチェーンとしてローンチされ既に大きなコミュニティを形成している
  • アカウント操作やコントラクトのデプロイにはiWalletのクライアントが必要となる
  • ローカルノードはAWSマーケットプレイスから、もしくはDockerイメージとしても提供されている
  • IOSTに標準のキーバリューストアであるストレージオブジェクトを使用してスマートコントラクトで値の保存が行える

--

--

Yuya Sugano
Yuya Sugano

Written by Yuya Sugano

Cloud Architect and Blockchain Enthusiast, techflare.blog, Vinyl DJ, Backpacker. ブロックチェーン・クラウド(AWS/Azure)関連の記事をパブリッシュ。バックパッカーとしてユーラシア大陸を陸路横断するなど旅が趣味。

No responses yet