FLINTERS Engineer's Blog

FLINTERSのエンジニアによる技術ブログ

EOSを使った分散型アプリでブロックチェーン革命の波に乗る!

こんにちは。菅野です。

近頃はビットコインなどの暗号通貨の話題を目にすることが多くなっていますよね。
ブロックチェーンという言葉を聞いたことがないエンジニアはいないと思います。
もうブロックチェーンを使ったDApp1のひとつやふたつくらいは作っているのではないでしょうか。

ところでDAppとは何ですか?
ちなみに私は今年になって初めてそれを知りました。

分散型アプリケーションDApp

DAppとはDecentralized Applicationの略で、サービスを提供する管理サーバーがある普通のアプリとは違う、非中央集権型のアプリです。

なぜ今DAppなのかというと、高機能なスマートコントラクトを実装したEthereum等が登場したことに関係しています。スマートコントラクトを利用することで簡単にDAppを作成できて、お金のやり取りまで簡単に組み込めるためDAppの数が増えています。

プランニングポーカーアプリはついにブロックチェーンの時代へ

存在を知ったからには早速DAppを作ってみないといけませんね。

私は以前の記事でプランニングポーカーアプリを作りました。

labs.septeni.co.jp

ただ、このアプリにはとてつもなく重大な欠点があるのです!
それは、管理者である私がDBを直接弄って他人が出したカードを不正に改ざんすることにより利益を享受することが出来てしまうのです。
もしそんなことがあったら、いや、可能性があるだけでもこのアプリの信頼性は地の底まで落ちてしまいます。

自分の知らないところでポイントが変えられていたら困りますよね。
もしこのアプリがブロックチェーンを使ったDAppであったならば不正に改ざんすることは出来ないはず!
ああ、DAppだったらなぁ〜。

早速DAppにしましょう。

スマートコントラクトのプラットフォームEOS

DAppに使えるプラットフォームは色々あります。Ethereumも良いですが、今回はEOSを使います。

EOSといえばこの記事を書いている時点でレートが500円台の暗号通貨ですが、スマートコントラクトのプラットフォームとしての役割も持ちます。
というよりも、通貨としてのEOS自体がEOSのスマートコントラクトを使ったトークンです。

EOSのスマートコントラクトはWebAssemblyが使われており、公式のSDKとしてはC++のものがあります。
私はC++は完全に理解した程度なので、サードパーティ製のRustのSDKを使います。

GitHub - sagan-software/eosio-rust: EOSIO SDK for Rust – APIs for building smart contracts on EOSIO blockchains in Rust

SolidityよりRustの方が好みなのでEOSを使ったという側面もあります。

世に出回っているEOSのDAppは賭博アプリがほとんどのようなので、代わりに健全なプランニングポーカーアプリを作ってあげましょう。

Rustでコントラクト作成

sagan-software.github.io

ドキュメントに従ってRustのプロジェクトを作りましょう。
と、言いたいところですが、ドキュメントが古いようで内容の通りにやっても動きません。

現在の環境、Rust v1.41.0SDK eosio=0.3.0、eosio_cdt=0.3.0での作り方を説明します。 なお、Rust関連のツールの説明は省略します。

プロジェクト作成

コントラクトを作るプロジェクトですが、今の環境だとnightlyチャネルではなくstableでも動くのでnightlyを使う必要はありません。

cargo new ppap --lib

ppapはプロジェクト名です

Cargo.tomlは以下の内容です。

[package]
name = "ppap"
version = "0.1.0"
edition = "2018"
publish = false

[lib]
crate-type = ["cdylib"]
doc = false

[dependencies]
eosio = "0.3.0"
eosio_cdt = "0.3.0"

[profile.release]
lto = true

.cargo/configも以下のように設定しておきます。

[target.wasm32-unknown-unknown]
rustflags = [
  "-C", "link-args=-z stack-size=48000"
]
コントラクト実装

実装を書くsrc/lib.rsは以下のようになりました。

use eosio::*;
use eosio_cdt::*;

#[eosio::table("playercard")]
struct PlayerCard {
    #[eosio(primary_key)]
    player: eosio::AccountName,
    #[eosio(secondary_key)]
    master: eosio::AccountName,
    player_name: String,
    card: u64,
}

#[eosio::action]
fn join(player: eosio::AccountName, master: eosio::AccountName, player_name: String) {
    eosio_cdt::require_auth(player);

    let code = eosio_cdt::current_receiver();
    let table = PlayerCard::table(code, master);

    let card = table.find(player);
    assert!(card.is_none(), "already joined!");

    let card = PlayerCard {
        player,
        master,
        player_name,
        card: 0,
    };

    table.emplace(player, card).expect("join error");

    eosio_cdt::print!("join ", player, " to ", master);
}

#[eosio::action]
fn leave(player: eosio::AccountName, master: eosio::AccountName) {
    let code = eosio_cdt::current_receiver();
    let table = PlayerCard::table(code, master);
    let cursor = table.find(player).expect("record not found!");

    cursor.erase().expect("leave error");

    eosio_cdt::print!("leave ", player, " from ", master);
}

#[eosio::action]
fn bet(player: eosio::AccountName, master: eosio::AccountName, card: u64) {
    eosio_cdt::require_auth(player);

    let code = eosio_cdt::current_receiver();
    let table = PlayerCard::table(code, master);
    let cursor = table.find(player).expect("record not found!");

    let mut player_card = cursor.get().expect("read error");
    player_card.card = card;

    cursor.modify(Payer::Same, player_card).expect("write error");

    eosio_cdt::print!("player ", player, " bet ", card);
}

eosio_cdt::abi!(join, leave, bet);

プレイヤーごとのカードの値を持つplayercardテーブルをデータとして持ち、相手を指定してポーカーに参加、退出、カードを出すの3つのactionを定義しています。 後でWebAssemblyにすることを考えても割と適当なコードです。

ビルド

WebAssemblyのwasmをビルドします。ビルドするにはターゲットとしてwasm32-unknown-unknownを追加する必要があります。nightlyは指定しません。

rustup target add wasm32-unknown-unknown

ビルドすると大きなwasmが出来上がるので、ドキュメント通りにダイエットします。ツールの導入はドキュメントにあります。

cargo build --release --target=wasm32-unknown-unknown
wasm-gc target/wasm32-unknown-unknown/release/ppap.wasm ppap_gc.wasm
wasm-opt ppap_gc.wasm --output ppap_gc_opt.wasm -Oz

356K37K30Kとダイエットに成功しました。なお、デバッグビルドすると2.5Mになりました。

abi.json

EOSのスマートコントラクトにはwasmだけではなくABI(Application Binary Interface)というものも必要です。
C++だと自動生成されるようですが、RustのSDKだと将来的に自動生成されるようになるようです。

まさかのjson手書きをすることになります。
ppap.abi.json

{
  "version": "eosio::abi/1.0",
  "structs": [
    {
      "name": "join",
      "base": "",
      "fields": [
        {
          "name": "player",
          "type": "name"
        },
        {
          "name": "master",
          "type": "name"
        },
        {
          "name": "player_name",
          "type": "string"
        }
      ]
    },
    {
      "name": "leave",
      "base": "",
      "fields": [
        {
          "name": "player",
          "type": "name"
        },
        {
          "name": "master",
          "type": "name"
        }
      ]
    },
    {
      "name": "bet",
      "base": "",
      "fields": [
        {
          "name": "player",
          "type": "name"
        },
        {
          "name": "master",
          "type": "name"
        },
        {
          "name": "card",
          "type": "uint64"
        }
      ]
    },
    {
      "name": "playercard",
      "base": "",
      "fields": [
        {
          "name": "player",
          "type": "name"
        },
        {
          "name": "master",
          "type": "name"
        },
        {
          "name": "player_name",
          "type": "string"
        },
        {
          "name": "card",
          "type": "uint64"
        }
      ]
    }
  ],
  "actions": [
    {
      "name": "join",
      "type": "join"
    },
    {
      "name": "leave",
      "type": "leave"
    },
    {
      "name": "bet",
      "type": "bet"
    }
  ],
  "tables": [
    {
      "name": "playercard",
      "index_type": "i64",
      "key_names": ["master"],
      "key_types": ["i64"],
      "type": "playercard"
    }
  ]
}

コントラクトをデプロイ

ローカルネットワーク作成

出来上がったスマートコントラクトをテストするための環境を作ります。

まずはeosをインストールします。

github.com

ドキュメントにはdockerでやるように書かれていますが、そのimageが古いので直接入れます。
Macならhomebrewで入れられます。

brew tap eosio/eosio
brew install eosio

インストールしたら必要なものを起動します。

ウォレットの鍵管理のデーモンのkeosd。

keosd

EOSのノードサーバーnodeos。

nodeos -e -p eosio \
--plugin eosio::producer_plugin \
--plugin eosio::chain_api_plugin \
--plugin eosio::http_plugin \
--plugin eosio::history_plugin \
--plugin eosio::history_api_plugin \
--access-control-allow-origin='*' \
--contracts-console \
--http-validate-host=false \
--verbose-http-errors \
--filter-on='*'
ウォレットとアカウント作成

次はウォレットを作成します。

EOSのcliクライアントのcleosを使います。
下記のコマンドでウォレットが作成され、コンソールにウォレットのパスワードが出力されるので覚えておきましょう。時間経過などでウォレットのロックが掛かったときに解除するのに必要です。

cleos wallet create --to-console

次に秘密鍵と公開鍵を作成します。
下記のコマンドで作成されてコンソールに出力されます。

cleos create key --to-console

必要なものが揃ったら下記のコマンドで鍵をウォレットにインポートします。秘密鍵を聞かれるので先程のものを入力します。

cleos wallet import

そして最後にアカウント作成をするのですが、このままでは作成することが出来ません。
なぜかと言うと、EOSではアカウントは別のアカウントに作ってもらうものなのですが、作成を頼めるアカウントがいません。

そこでアカウントを作成できる権限がある秘密鍵をこっそり教えてしまいます!すばり、5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3です!
なお、この鍵は開発者はみんな知っている開発用の鍵なので気をつけてください。

…というわけで、下記コマンドでウォレットに追加します。

cleos wallet import --private-key 5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3

これでアカウントが自由に作れるようになったので作成を行います。コントラクト公開用のppapとプランニングポーカーを楽しむユーザーアカウント1,2,3を作成します。

cleos create account eosio ppap <作成した公開鍵>
cleos create account eosio test1 <作成した公開鍵>
cleos create account eosio test2 <作成した公開鍵>
cleos create account eosio test3 <作成した公開鍵>

全部同じ鍵で作るのはそもそも変ですが、お試しなので大目に見てください。

デプロイ

やっとスマートコントラクトをデプロイ出来る状態になりました。

下記のコマンドでppapアカウントでabiとwasmをデプロイします。

cleos set abi ppap ppap.abi.json
cleos set code ppap ppap_gc_opt.wasm

うまく行ったでしょうか?
本当に動くのか試してみましょう。

test1に対してtest2が"俺"という名前でポーカーに参加するというのを試してみます。
下記コマンドでppapのjoinアクションを実行できます。

cleos push action ppap join '["test2", "test1", "俺"]' -p test2@active

データの部分にはRustのコードの引数に対応する値を指定します。-pは署名の指定で、誰がどのようなロールでアクションを実行したのかを証明するものです。joinアクションではplayerは署名したアカウント以外を指定するとエラーになるように作っています。つまり、正しく署名できる本人しかjoinアクションが出来ません。

さて、実行結果はどうなったでしょうか。

>> join test2 to test1

コンソールにはコントラクト内で記述したコンソール出力の内容が出力されているので動いていそうです。

本当にデータが保存されたのでしょうか?テーブルの内容を確認してみます。
今回はppapコントラクトのtest1アカウントのplayercardテーブルを見に行けば目的のデータがあるはずです。

cleos get table ppap test1 playercard
{
  "rows": [{
      "player": "test2",
      "master": "test1",
      "player_name": "",
      "card": 0
    }
  ],
  "more": false,
  "next_key": ""
}

ありました!

UIを作る

アプリっぽくするためにWebのフロントエンドを作ります。

developers.eos.io

eosjsというJSのクライアントがあるのでこれでUIを作れます。

過程は省略しますが、とりあえず出来ました。

f:id:zakknak:20200209082225p:plain
これが公平なプランニングポーカー

我ながらすごく雑な作りで感心します。

f:id:zakknak:20200209091839p:plain

nodeosは http://localhost:8888 で動いているのでhostにはそれを指定します。

実際に遊んでみましょう。

test3が居るtest1のところにtest2としてお邪魔してみます。

f:id:zakknak:20200209091504g:plain

既にお気づきかと思いますが、いちいち保存されたデータをフェッチして来なければいけません。
ここらへんをどうにかしたくはある。
どうにかするライブラリもあるようですが、中身は定期的にポーリングしてるだけのようでした。

いろいろ突っ込みどころがある感じですが、プランニングポーカーのDApp化に成功(?)しました。
アプリはScala.js+Akka Stream(web socket)FirebaseEOSでDAppと進化を果たしました。多分。

おわり

とりあえず簡単なDAppを作ることが出来ました。
動くのを確認できただけで実用的なものを作るためにはもっと色々やることがありますが、なんとなく雰囲気はつかめたので今回はここまでで終わりです。

最後まで読んでいただきありがとうございます。


  1. なおダップは、dapp, Dapp, dApp, DAppという表記ブレの激しさに定評がある