Dappsの運用方法を考える - Cosmos-SDKを試す - SaaSベンチャーで働くエンタープライズ部長のブログ

SaaSベンチャーで働くエンタープライズ部長のブログ

SaaSベンチャーでエンジニア→プロダクトマネージャー→エンタープライズ部長として働いています。

Dappsの運用方法を考える - Cosmos-SDKを試す

f:id:naomasabit:20190206005906p:plain

Dappsの運用方法を考える

レイヤー2ソリューションやinteroperabilityが重要視されており、実ビジネスでの利用が徐々に考えられています。

まだ整理中なので誤っているかもしれないのですが、Dappsの運用方法を整理してみました。

運用方法 Public以外で利用するツール
オフチェーンで運用して、Publicに重要な時だけ書き込む(取引所型) RDBなど
パーミッションドチェーン(Private / Consortium)で運用して、PublicとPrivateのInteroperabilityを確立する private chain + / parity-bridge, Cosmos(Tendermint)、Polkadot
PrivateとPublicの間にトラストできる仕組みを設ける private chain + 透明性のある仕組み・開示されている仕組みなどをオフチェーンで作る
パーミッションレスなサイドチェーンで運用して、たまにPublicに書き込む Plasma / Cross Chain

この分類で完全にダブりがないわけではないと思いますが(特に4番目のものとか)、一旦自身の整理として書きました。

オフチェーン+Public Chain以外の場合、パーミッションドチェーン(Private / Consortium)であったり、サイドチェーンであったりを運用することになります。となると、それぞれのチェーンの用意はもちろん必要です。パーミッションドチェーン(Private / Consortium)でも、書き込む権限やブロードキャストできる権限はノード参加者に限るとしても読み込み権限は多くに解放する、エンジニアでなくてもわかりやすいようにデータを確認できるよう、エクスプローラーを立てるといったこともありえそうです。

パーミッションドチェーンとの両立をしたInteroperabilityが現時点では割と面白いのではないかと思っており、Cosmos-SDKを使ってみます。

Cosmos-SDKとは

The Cosmos-SDK is a framework for building multi-asset Proof-of-Stake (PoS) blockchains, like the Cosmos Hub, as well as Proof-Of-Authority (PoA) blockchains.

Cosmos-SDKは、Cosmos-Hubのような複数通貨のPoS / PoAブロックチェーンを作るためのフレームワークです。

Cosmos-HubはZoneと呼ばれるチェーンと、Cosmos-HubをIBCというプロトコルで通信することで各Zoneのトークンの合計数を記録できます

https://cosmos.network/docs/intro/

Cosmosはアーキテクチャとして、ConsensusとNetworkingはTendermint Coreで作成しており、アプリケーションレイヤーをCosmos-SDKで利用しています。

                ^  +-------------------------------+  ^
                |  |                               |  |   Built with Cosmos SDK
                |  |  State-machine = Application  |  |
                |  |                               |  v
                |  +-------------------------------+
                |  |                               |  ^
Blockchain node |  |           Consensus           |  |
                |  |                               |  |
                |  +-------------------------------+  |   Tendermint Core
                |  |                               |  |
                |  |           Networking          |  |
                |  |                               |  |
                v  +-------------------------------+  v

ABCI(Application Block Chain Interface)というプロトコル(Tendermintの基盤プロトコル)でアプリケーションレイヤーからTendermintに通信をして情報を取得します。

+---------------------+
|                     |
|     Application     |
|                     |
+--------+---+--------+
         ^   |
         |   | ABCI
         |   v
+--------+---+--------+
|                     |
|                     |
|     Tendermint      |
|                     |
|                     |
+---------------------+

スマートコントラクトについて

Cosmos-SDKではEthereum形式のスマートコントラクトはサポートしておらず、go言語で独自ブロックチェーンを作るという方向性の設計になっています。後述しますが、Msgと呼ばれる形式の構造体によって任意の情報を定義したり、受け取った時のアクションを設定したりできます。

github.com

Cosmos-SDKチュートリアルアプリの実装概要

では早速チュートリアルを実行しながら内容を理解してみます。

cosmos.network

チュートリアルアプリnameserviceは名前(ドメインネームなどを想定していそう)を管理したり売買したりするようなプラットフォームです。チュートリアルでは以下のプログラムを実装する作りになっています。

  • app.go
  • handler.go
  • keeper.go
  • msgs.go
  • querier.go
  • codec.go

app(baseapp)

cosmos-sdkbaseappと呼ばれる基本フレームワークをもとに動いています。 "./app.go"を作って必要なロジックを書いていきます。チュートリアルでは、nameServiceAppという構造体を作って、さらに BaseAppを見るようにしています。

bam "github.com/cosmos/cosmos-sdk/baseapp"
 
type nameServiceApp struct {
    *bam.BaseApp

BaseAppはtendermintのABCIを実装しており、自力でABCIプロトコルを書く必要はありません。

Keeper

KeeperはCosmos-SDKbank package内にあります。コインの足し引きなどを計算する番人としてのmodelです。チュートリアルのプログラムを以下などを見ると、Name(名前)、owner(所有者)、price(値段)を管理するモデルを書いてラップしています。

// Keeper maintains the link to data storage and exposes getter/setter methods for the various parts of the state machine
type Keeper struct {
    coinKeeper bank.Keeper

    namesStoreKey  sdk.StoreKey // Unexposed key to access name store from sdk.Context
    ownersStoreKey sdk.StoreKey // Unexposed key to access owners store from sdk.Context
    pricesStoreKey sdk.StoreKey // Unexposed key to access prices store from sdk.Context

    cdc *codec.Codec // The wire codec for binary encoding/decoding.
}

// ResolveName - returns the string that the name resolves to
func (k Keeper) ResolveName(ctx sdk.Context, name string) string {
    store := ctx.KVStore(k.namesStoreKey)
    bz := store.Get([]byte(name))
    return string(bz)
}

// SetName - sets the value string that a name resolves to
func (k Keeper) SetName(ctx sdk.Context, name string, value string) {
    store := ctx.KVStore(k.namesStoreKey)
    store.Set([]byte(name), []byte(value))
}

// HasOwner - returns whether or not the name already has an owner
func (k Keeper) HasOwner(ctx sdk.Context, name string) bool {
    store := ctx.KVStore(k.ownersStoreKey)
    bz := store.Get([]byte(name))
    return bz != nil
}

// GetOwner - get the current owner of a name
func (k Keeper) GetOwner(ctx sdk.Context, name string) sdk.AccAddress {
    store := ctx.KVStore(k.ownersStoreKey)
    bz := store.Get([]byte(name))
    return bz
}

Msg

Msgは送信されるTxにWrapされるもので、Cosmos-SDKを利用していると、DeveloperはTxの細かい設定は気にせずMsg開発にだけ集中できるとのことです。

チュートリアルで書かれているMsgの定義例は以下などです。Nameについて項目を設定しています。

// MsgSetName defines a SetName message
type MsgSetName struct {
    Name string
    Value  string
    Owner  sdk.AccAddress
}

Handler

HandlerはMsgを受け取った時にどのようにするかという対応です。 以下はMsgSetNameを受けとった時のロジックです。

// Handle MsgSetName
func handleMsgSetName(ctx sdk.Context, keeper Keeper, msg MsgSetName) sdk.Result {
    if !msg.Owner.Equals(keeper.GetOwner(ctx, msg.Name)) { // Checks if the the msg sender is the same as the current owner
        return sdk.ErrUnauthorized("Incorrect Owner").Result() // If not, throw an error
    }
    keeper.SetName(ctx, msg.Name, msg.Value) // If so, set the name to the value specified in the msg.
    return sdk.Result{}                        // return
}

Querier

クエリ処理をするロジックです。queryResolve、QueryWhoisの2つを用意しており、名前解決するクエリと所有者を返すクエリを持っています。これらは以下のようにswitchして実行funcを定義します。

// NewQuerier is the module level router for state queries
func NewQuerier(keeper Keeper) sdk.Querier {
    return func(ctx sdk.Context, path []string, req abci.RequestQuery) (res []byte, err sdk.Error) {
        switch path[0] {
        case QueryResolve:
            return queryResolve(ctx, path[1:], req, keeper)
        case QueryWhois:
            return queryWhois(ctx, path[1:], req, keeper)
        default:
            return nil, sdk.ErrUnknownRequest("unknown nameservice query endpoint")
        }
    }
}

後ほどでてきますが、以下などが使用例です。

$ nscli query nameservice resolve jack.id --chain-id testchain

codec

codecは独自のmodelを設定したら登録する必要があります。

// RegisterCodec registers concrete types on the Amino codec
func RegisterCodec(cdc *codec.Codec) {
    cdc.RegisterConcrete(MsgSetName{}, "nameservice/SetName", nil)
    cdc.RegisterConcrete(MsgBuyName{}, "nameservice/BuyName", nil)
}

Cosmos-SDKチュートリアルアプリの実行

チュートリアルアプリを実行してみます。Cosmosはgoで書かれています。 go versionはgo1.11です。

$ cd ~/$GOPATH/src/github.com/
$ git clone https://github.com/cosmos/sdk-application-tutorial.git
# export go111MODULE=OFF #GO111MODULEをoffにしておかないとうまく動きません
$ make get_tools && make get_vendor_deps && make install
  • make get_toolsではdepというgolangの依存関係管理ツールのインストール
  • make get_vendor_depsでは依存関係のあるツールをインストール
  • make installではnsdとnscliをインストール

します。ちなみに、nscliとnsdは、cobraというコマンドライブラリでコマンドを作成しています。

nsはnamespaceの略で、namespacecliとnamespacedeamonのことを指しています。

nscliのコマンド

$ nscli help
nameservice Client

Usage:
  nscli [command]

Available Commands:
  status      Query remote node for status
  config      Create or query a Gaia CLI configuration file
  query       Querying subcommands
  tx          Transactions subcommands
              
  rest-server Start LCD (light-client daemon), a local REST server
              
  keys        Add or view local private keys
              
  version     Print the app version
  help        Help about any command

Flags:
      --chain-id string   Chain ID of tendermint node
  -e, --encoding string   Binary encoding (hex|b64|btc) (default "hex")
  -h, --help              help for nscli
      --home string       directory for config and data (default "/Users/nao/.nscli")
  -o, --output string     Output format (text|json) (default "text")
      --trace             print out full stack trace on errors

Use "nscli [command] --help" for more information about a command.

nsdのコマンド

$ nsd help
nameservice App Daemon (server)

Usage:
  nsd [command]

Available Commands:
  init                Initialize genesis config, priv-validator file, and p2p-node file
  add-genesis-account Adds an account to the genesis file
  start               Run the full node
  unsafe-reset-all    Resets the blockchain database, removes address book files, and resets priv_validator.json to the genesis state
                      
  tendermint          Tendermint subcommands
  export              Export state to JSON
                      
  version             Print the app version
  help                Help about any command

Flags:
  -h, --help               help for nsd
      --home string        directory for config and data (default "/Users/nao/.nsd")
      --log_level string   Log level (default "main:info,state:info,*:error")
      --trace              print out full stack trace on errors

Use "nsd [command] --help" for more information about a command.

なお、nsdからtendermintのサブコマンドも呼べて、以下のようになります。

$ nsd tendermint help
Tendermint subcommands

Usage:
  nsd tendermint [command]

Available Commands:
  show-node-id   Show this node's ID
  show-validator Show this node's tendermint validator info
  show-address   Shows this node's tendermint validator consensus address
  version        Print tendermint libraries' version

Flags:
  -h, --help   help for tendermint

Global Flags:
      --home string        directory for config and data (default "/Users/nao/.nsd")
      --log_level string   Log level (default "main:info,state:info,*:error")
      --trace              print out full stack trace on errors

Use "nsd tendermint [command] --help" for more information about a command.

testchainのセットアップ

test chain 初期化

$ nsd init --chain-id testchain
Initialized nsd configuration and bootstrapping files in /Users/nao/.nsd...

jack とaliceのkeyを作成

$ nscli keys add jack
Enter a passphrase to encrypt your key to disk:
Repeat the passphrase:

NAME:   TYPE:   ADDRESS:                        PUBKEY:
jack    local cosmos1r70ad4fmkcsfs6r5wzll7rlnft4edpn3j6asyk   cosmospub1addwnpepqgf3ejr92547hm2gzdh7a9u0p52v87cyjcp6jualcsf8a7j5jdjlwu5nuj4

**Important** write this seed phrase in a safe place.
It is the only way to recover your account if you ever forget your password.

abandon fragile coin better wrestle later walnut talent control naive fashion toddler tooth clean drift intact victory cupboard drip fantasy keen deer magic text


$ nscli keys add alice
Enter a passphrase to encrypt your key to disk:
Repeat the passphrase:

NAME:   TYPE:   ADDRESS:                        PUBKEY:
alice   local cosmos1npzzr724p57sxjzmtvppgru987zu6qewkxv7pt   cosmospub1addwnpepqw2gd6jjxguk97xzgls2666crfr8kfewnxk8jg69wctsd94vadhnqmj8c3j

**Important** write this seed phrase in a safe place.
It is the only way to recover your account if you ever forget your password.

popular lazy scheme suit black property slight wet waste light then minor flavor liquid lobster weather fun height sure category blast furnace alter cost

jackとaliceの初期アカウントを作成する

# 公式通りにやると大文字が使えないと怒られるので、小文字にする error message (ERROR: denom cannot contain upper case characters: jackCoin
$ nsd add-genesis-account $(nscli keys show jack --address) 1000mycoin,1000jackcoin
$ nsd add-genesis-account $(nscli keys show alice --address) 1000mycoin,1000alicecoin

ルノード起動

$ nsd start
I[2019-02-05|01:12:05.554] Starting ABCI with Tendermint                module=main 
E[2019-02-05|01:12:05.690] Couldn't connect to any seeds                module=p2p 
I[2019-02-05|01:12:10.657] Executed block                               module=state height=0 validTxs=0 invalidTxs=0
I[2019-02-05|01:12:10.671] Committed state                              module=state height=0 txs=0 appHash=6233E01F84FEEC8F11D24EE17E50E4EBB6B3E32969060A136A741ADF41F2E307

jackの残高確認

$ nscli query account $(nscli keys show jack --address) \
>     --indent --chain-id testchain
 
Account:
    Address:       cosmos1r70ad4fmkcsfs6r5wzll7rlnft4edpn3j6asyk
    Pubkey:        
  Coins:         1000jackcoin,1000mycoin
  AccountNumber: 0
  Sequence:      0

jackが独自ID(jack.id)というドメイン名を購入

トランザクションを作ります。jackのパスワードを求められます。

$ nscli tx nameservice buy-name jack.id 5mycoin \
>     --from     $(nscli keys show jack --address) \
>     --chain-id testchain
Password to sign with 'jack':
Committed at block 28 (tx hash: 42177F9986922F4FD7230A1BBBFA59FE0BD6B45ABE37FAC01200E190DFB1C933, response: {Code:0 Data:[] Log:Msg 0:  Info: GasWanted:200000 GasUsed:21872 Tags:[{Key:[97 99 116 105 111 110] Value:[98 117 121 95 110 97 109 101] XXX_NoUnkeyedLiteral:{} XXX_unrecognized:[] XXX_sizecache:0}] Codespace: XXX_NoUnkeyedLiteral:{} XXX_unrecognized:[] XXX_sizecache:0})

ノードログを見ると、トランザクションが発行されています。

$ nsd start
...
I[2019-02-05|02:08:43.495] Executed block                               module=state height=28 validTxs=1 invalidTxs=0
I[2019-02-05|02:08:43.496] Committed state                              module=state height=28 txs=1 appHash=C559CAF524A6FAA38F324327FECB18888149E9B497F34D58CEF96B4079A0ED47

jackの残高を見ると、残高が減っています。

$ nscli query account $(nscli keys show jack --address) \
>     --indent --chain-id testchain
Account:
    Address:       cosmos1r70ad4fmkcsfs6r5wzll7rlnft4edpn3j6asyk
    Pubkey:        cosmospub1addwnpepqgf3ejr92547hm2gzdh7a9u0p52v87cyjcp6jualcsf8a7j5jdjlwu5nuj4
  Coins:         1000jackcoin,995mycoin
  AccountNumber: 0
  Sequence:      1

jack.idに8.8.8.8という値をつけてみます。

# idに値を設定

$ nscli tx nameservice set-name jack.id 8.8.8.8 \
>     --from     $(nscli keys show jack --address) \
>     --chain-id testchain
Password to sign with 'jack':
Committed at block 161 (tx hash: 8A8C8B5B63AA473BB4BB5D4332B9EBAB9BEAF84E8E3BA121AF7A3D87C50D4F84, response: {Code:0 Data:[] Log:Msg 0:  Info: GasWanted:200000 GasUsed:10603 Tags:[{Key:[97 99 116 105 111 110] Value:[115 101 116 95 110 97 109 101] XXX_NoUnkeyedLiteral:{} XXX_unrecognized:[] XXX_sizecache:0}] Codespace: XXX_NoUnkeyedLiteral:{} XXX_unrecognized:[] XXX_sizecache:0})

# 確認

$ nscli query nameservice resolve jack.id --chain-id testchain
8.8.8.8

resolveコマンドは上で設定していたコマンドですね。

jackがかったドメインの所有権を確認する

$ nscli query nameservice whois jack.id --chain-id testchain
{
  "value": "8.8.8.8",
  "owner": "cosmos1r70ad4fmkcsfs6r5wzll7rlnft4edpn3j6asyk", # jackのアドレス
  "price": [
    {
      "denom": "mycoin",
      "amount": "5"
    }
  ]
}

aliceがjackからドメインjack.idを購入する

$ nscli tx nameservice buy-name jack.id 10mycoin \
>     --from     $(nscli keys show alice --address) \
>     --chain-id testchain
Password to sign with 'alice':
Committed at block 175 (tx hash: D4A26EDFBCB094ABA83EE982E20BD29F270DC1B67A7DEADA20F6556FCAA98338, response: {Code:0 Data:[] Log:Msg 0:  Info: GasWanted:200000 GasUsed:31994 Tags:[{Key:[97 99 116 105 111 110] Value:[98 117 121 95 110 97 109 101] XXX_NoUnkeyedLiteral:{} XXX_unrecognized:[] XXX_sizecache:0}] Codespace: XXX_NoUnkeyedLiteral:{} XXX_unrecognized:[] XXX_sizecache:0})

whoisコマンドで見ると、jackからaliceに所有権が移転しています。

$ nscli query nameservice whois jack.id --chain-id testchain
{
  "value": "8.8.8.8",
  "owner": "cosmos1npzzr724p57sxjzmtvppgru987zu6qewkxv7pt", # aliceのアドレス
  "price": [
    {
      "denom": "mycoin",
      "amount": "10"
    }
  ]
}

感想

ビジネスロジックをチェーンに追加して開発するという事はしやすい気がしました。あとは、Cosmosでのチェーンでパーミッション機能設定がどうあるかなどはまだ調べていないので、引き続き追っていきたいと思います。

また、cosmosにはオープンソースのexplorerもあるのですが、ローカルのデータとうまくまだ繋げられていないので別途こちらもみてみたいと思います。

https://github.com/cosmos/explorer