Cosmos-sdkのチュートリアルを触ってみる その2
前回の続きを書いていきます。前回のその1は以下からどうぞ〜
続きからの”Start your application”をやっていきます。
やっと実装ですね。今回はただブロックチェーンを学ぶというより、オブジェクト指向や、モジュールの通信の仕方などが学べて楽しいはずです!長くなっていますが、一緒に頑張っていきましょう!
Start your application
まず、nameserviceのフォルダ直下にapp.goというファイルを作ります。これはこれから作るアプリチェーンの心臓部です。ここでsdk.ModuleBasicManagerというものを使って、様々なモジュールの初期化を行います。前回の記事であげたそれぞれの必要なモジュールのことです。
app.goでは、トランザクションが発生した時にアプリチェーンをどう動作させるのかを決めていきます。まずは、うまくトランザクションを受け取れるように設定していきます。
必要な依存先パッケージを読み込んでいきます。
package app
import (
"encoding/json"
"os"
"github.com/tendermint/tendermint/libs/log"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/x/auth"
"github.com/cosmos/cosmos-sdk/x/auth/genaccounts"
"github.com/cosmos/cosmos-sdk/x/bank"
distr "github.com/cosmos/cosmos-sdk/x/distribution"
"github.com/cosmos/cosmos-sdk/x/params"
"github.com/cosmos/cosmos-sdk/x/staking"
"github.com/cosmos/sdk-application-tutorial/x/nameservice"
bam "github.com/cosmos/cosmos-sdk/baseapp"
sdk "github.com/cosmos/cosmos-sdk/types"
abci "github.com/tendermint/tendermint/abci/types"
cmn "github.com/tendermint/tendermint/libs/common"
dbm "github.com/tendermint/tendermint/libs/db"
)
よく見るとtendermintとcosmos-sdk、両方のリポジトリからimportしてきてます。ほぼ同じものだと認識してましたが、厳密には違うみたい。
いくつか抜き出して簡単に解説
・log : log取るお仕事をしてくれるやつ
・auth: Cosmos SDKのための認証モジュール
・dbm: Tendermintのデータベースを動かすやつ
・baseapp: これから説明する〜
importしてるもののうちいくつかはTendermintのものです。TendermintはABCIというインターフェイスを介して、ブロックチェーンのネットワークから、チェーンのアプリケーション部に対してトランザクションを渡してあげる役割を担っています。図にすると以下のような感じ。
何がなんでも綺麗な図を用意したくなかったのが伝わってきます。
ABCIのインターフェイスに関しては、CosmosSDK側が用意してるので、baseappモジュールのフォームに従って実装していけば良いです。大分楽にしてくれているみたいです。
baseappモジュールがやってくれること一覧は以下のようになります。
・Tendermintのコンセンサスエンジンから受け取ったトランザクションをデコードする
・トランザクションからmessageを抽出して、異常がないかの基本的なチェック
・messageを正しいモジュールに伝えるようにルーティングしてくれる。baseapp自体はあなたがどういうモジュールを使いたいのか把握していないから、app.goの中でちゃんとどのモジュールを使うのかを宣言してあげる必要があります。(これからチュートリアルの中で扱います)
・ABCIが伝えるmessageが”DeliverTx”かどうかを確認する。
・”Beginblock”と”Endblock”の設定を手助けする。これら二つはそれぞれのブロックの初めと終わりに実行されるように定義できるmessageのことです。
・ステートの初期化を手助けする。
・クエリの設定を手助けする。
ブロックチェーン上のデータのやりとりや、データの保存に関して設定するモジュールであることがわかります。めちゃくちゃ重要なところですね。
nameServiceAppという新しい構造体、typeを作成し、そこでbaseappのモジュールを使用することを宣言してあげましょう。
const appName = "nameservice"
var (
// default home directories for the application CLI
DefaultCLIHome = os.ExpandEnv("$HOME/.nscli")
// DefaultNodeHome sets the folder where the application data and configuration will be stored
DefaultNodeHome = os.ExpandEnv("$HOME/.nsd")
// ModuleBasicManager is in charge of setting up basic module elements
ModuleBasics sdk.ModuleBasicManager
)
type nameServiceApp struct {
*bam.BaseApp
}
なんかコマンドラインと、ノードの場所を設定したりもしてますね。
コンストラクタの設定も加えます。
func NewNameServiceApp(logger log.Logger, db dbm.DB) *nameServiceApp {
// First define the top level codec that will be shared by the different modules. Note: Codec will be explained later
cdc := MakeCodec()
// BaseApp handles interactions with Tendermint through the ABCI protocol
bApp := bam.NewBaseApp(appName, logger, db, auth.DefaultTxDecoder(cdc))
var app = &nameServiceApp{
BaseApp: bApp,
cdc: cdc,
}
return app
}
logモジュールとdbモジュールを使っているのがわかります。cdcについては後で説明をするとのこと。bAppという部分がABCIを介してtendermint部分との伝達をやってくれるみたいです。とりあえずここもcdcとbAppを決めて設定してあげてるだけみたいですね。これでアプリの骨組みだけならできました。
次に、アプリ内でのメッセージのやりとり、ユーザー間の相互作用について決め、それらを管理するためのステートについての設定を行なっていきましょう。
次のセクションからnameserviceのモジュールを実装していきます。
また後でapp.goの実装に戻ってきます。
Types
アプリとして保持することになるユーザーネームのメタデータの構造を決めていきます。これらの構造のことをWhoisと呼ぶことにします。
nameserviceのフォルダに./x/nameservice/types/types.goという階層でtype.goファイルを作ってあげてください。Cosmos SDKを用いたアプリでは、xというファイル内にモジュールを置く習慣があるらしいです。
・Whois
ユーザーネームのメタデータ構造を作ります。
それぞれのユーザーネームは三つの情報を持つことになります。
Value : string型のユーザーネーム
Owner: ユーザーネームの所有者のアドレス
Price: そのユーザーネームを購入する際に支払う必要のある額
ではこれらの構造を具体的にコードに落としていきましょう。
package types
import (
"fmt"
"strings"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// Whois is a struct that contains all the metadata of a name
type Whois struct {
Value string `json:"value"`
Owner sdk.AccAddress `json:"owner"`
Price sdk.Coins `json:"price"`
}
sdkのモジュールを読み込んで、OwnerのアドレスとPriceの価格の型設定に使っていることがわかります。そこらへんの細かいところは既にsdkで用意してくれているみたいですね。
前回の記事で書いたように、もしまだそのユーザーネームの所有者がいない場合は、最低限の額をチェーンに支払ってネームを取得する仕組みにする予定でした。その部分も記述していきます。
// Initial Starting Price for a name that was never previously owned
var MinNamePrice = sdk.Coins{sdk.NewInt64Coin("nametoken", 1)}
// Returns a new Whois with the minprice as the price
func NewWhois() Whois {
return Whois{
Price: MinNamePrice,
}
}
// implement fmt.Stringer
func (w Whois) String() string {
return strings.TrimSpace(fmt.Sprintf(`Owner: %s
Value: %s
Price: %s`, w.Owner, w.Value, w.Price))
}
チュートリアル側は全然説明してくれてないですが、MinNamePriceの設定でしれっと”nametoken”という通貨が発生してますね。これでネームの売買をしていくみたいです。1 nametokenがまだ所有者がいないネームを買うための額に設定されていますね。String関数で、それぞれのネームの情報を参照できるようにしているのがわかります。
Key
./x/nameservice/types/key.goと、先ほどのtype.goと同じフォルダにkey.goを作りましょう。”このkey.goファイルに置いて、モジュールの作成を通してあなたが使う鍵を設定していきます。ここで鍵を設定 することでDRYのコードを書くのに役立ちますよ”とのこと。
DRYってなんじゃ?乾くんか?とか思ったのですが、調べたら「Don’t repeat yourself」の略でした。簡単に言うと、「コンピュータ領域において、重複を避けろ」という考え方らしいです。恥ずかしながら知りませんでした。
どうやってDRYを実現するのか具体的に想像できませんが、とりあえず先に進みましょう。後でDRYの設計の意図がわかったらまた触れます。
package types
const (
// module name
ModuleName = "nameservice"
// StoreKey to be used when creating the KVStore
StoreKey = ModuleName
)
key.goファイルはこれだけです。
本当にこれが役に立つのかな?ModuleNameに”nameservice”を設定して、StoreKeyにぶち込んでますね。「KVStoreを作る時に使えるのがStoreKey」とコメントに書いてありますが、KVStoreは多分key-valu storeのことだと思うので、先ほど設定したユーザーネームを管理するDBに”nameservice”モジュールからしかアクセスできない。みたいな設定なのかもしれません。いきあたりばったりで喋ってるけど大丈夫かな。間違ってたら後で修正します。
The Keeper
Cosmos SDKのモジュールにおけるメインコア部分らしいですこのKeeper。前回の記事のファイル構成を見るに、上で実装したtype.goやkey.goよりも一つ上の階層にあるファイルであることがわかります。./x/nameservice/keeper.goでkey.goファイルを作りましょう。nameserviceのモジュールのファイルの下にファイルをおきます。アプリの名前もnameserviceで、その中のモジュールの名前もnameserviceになっているのでややこしいです。ご注意を。
このKeeperは、ユーザーネームを保持しているストアとこのモジュールのやり取りに、nameservice外の他のモジュール(前回あげたauth,bank,staking等)のKeeperと通信をするためのものであると考えられます。これがないとモジュール間のやり取りができなくなってしまう重要な部分です。
package nameservice
import (
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/x/bank"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// 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
storeKey sdk.StoreKey // Unexposed key to access store from sdk.Context
cdc *codec.Codec // The wire codec for binary encoding/decoding.
}
途中です。cosmos-sdkのパッケージを3つインポートしました。ぞれぞれについて説明します。
codec : Cosmosでのエンコードフォーマット?を動かすツールらしいです。モジュール間においてメッセージを送るためにエンコードしたり、デコードしたりすると思われます。Amino?という形式のエンコードプロトコルを利用しているみたいです。
bank: アカウントとコインの移動に関して管理する
types: SDKの中で共通して使う型を準備してくれてます。
次はKeeperの構造体(struct)の中の説明。
bank.Keeper :これは、bankモジュールから参照してきたKeeperです。
nameserviceモジュールのKeeperからbankモジュールのKeeperを参照していることになるのでちょっとややこしいですね。ここで宣言することで、bankモジュール内の関数を呼び出すことが可能になります。SDKでは、”object capabilities approach”を使って、アプリのステートに接続するみたいです。
正直ちょっとよくわからない。。ソフトウェア工学の圧倒的知識の足りなさを実感します。object capabilityのwikiを以下に貼っておきます。難しいことを英語で説明されても難しすぎるよ。。
一応、自分で調べたCapability-based securityについての日本語wikiも貼っておきます。
どうやらあるオブジェクトに対して、セキュリティを保つために相互作用を行うアクセス権をどう設定するかの考え方らしいです。口に出して全部読んでもよくわからない。難しい。
とにかくSDK内のそれぞれのモジュール間でセキュアな権限を持ち、相互通信が可能になっていると考えていいってことかな。欠陥のあるモジュールや悪意のあるモジュールが変に作用する必要のない他の箇所に悪さをしないようになってるらしいです。
*codec.Codec : Aminoのバイナリー形式でエンコード、デコードを行うためのコーデックへのポインタ
自分のためにポインタとコーデックのwikiを
先ほどのcodecモジュールの説明で言ったように、モジュール間でメッセージを送りあうために形式を変換してることはわかります。今まで、自分がいかに雰囲気だけでブロックチェーンを理解した気でいたのかを思い知らされています。
sdk.StoreKey : KVStoreにアクセスするための鍵。KVStoreはさっき用意したWhois、(ユーザーネームの構造体)、つまりアプリチェーンのステートを保持しているところです。
・Getter and Setters
では、チェーンにストアされるデータが相互に作用する関数をKeeperに加えましょう。まずは、ユーザーネームを登録する関数です。Whoisの構造体を新たに登録するということですね。先ほどのコードの下に記述します。
// Sets the entire Whois metadata struct for a name
func (k Keeper) SetWhois(ctx sdk.Context, name string, whois Whois) {
if whois.Owner.Empty() {
return
}
store := ctx.KVStore(k.storeKey)
store.Set([]byte(name), k.cdc.MustMarshalBinaryBare(whois))
}
golangにおいて、関数名の後に引数を書くのは理解してましたが、関数名SetWhoisの前の (k Keeper) がどういうものなのか理解できてないので調査。話がそれてすみません。エンジニアとして弱い自分を認める。
ああ、関数じゃなくてメソッドの表記なのか完全に理解した。オブジェクト指向をかねて説明する自信ないので説明省きます。
SetWhoisメソッドの中で、引数としてsdk.Contextを使っているのが確認できます。このオブジェクトは、blockHeightやChainIDなどの重要なステート情報にアクセスすることができる関数を持っているらしいです。
k.storeKey、つまりKeeperが持っているKVStoreへのアクセスするための鍵を使って、storeを作っています。これでこのSetWhoisメソッド内からKVStoreに指示ができるわけです。
そしてstote.SetメソッドでnameとwhoisをCosmos SDK内のAminoという通信規格?に変換して渡していることがわかります。
このstore.Setメソッドに渡すWhois構造体には、すでに所有者の情報を記して渡しているようです。
if文では、引数として渡しているwhoisのOwnerの部分にちゃんと値が入っているかを確認しています。これは、所有者のいないユーザーネームが存在してしまうことを避けるための仕組みのようです。
個人的にはこのSetWhoisメソッドの引数自体に所有者のaddressも入れて、メソッド内でWhois作ってKVStoreに投げればいいのでは?と思ったのですが、何か理由があるのでしょうね。
次に、ユーザーネームからそのWhois構造体を探して、それを返すメソッドを記述します。
// Gets the entire Whois metadata struct for a name
func (k Keeper) GetWhois(ctx sdk.Context, name string) Whois {
store := ctx.KVStore(k.storeKey)
if !store.Has([]byte(name)) {
return NewWhois()
}
bz := store.Get([]byte(name))
var whois Whois
k.cdc.MustUnmarshalBinaryBare(bz, &whois)
return whois
}
先ほどと同様にstoreKeyを使って、KVStoreに接続しています。わかってきた楽しくなってきた。
store.Getメソッドで、指定したユーザーネームが入っているWhoisのデータがbzに返ってきていますが、これもAminoの規格でバイナリの状態なので、読める形にデコードしてあげましょう。それがk.cdc.MustUnmarshalBinarybare(bx, &whois)の所になっています。
よく見ると、SetWhoisメソッドではAmino規格に変換している所がMustmarshalBinaryBareだったのですが、今回はMust”Un”marshalBinaryBareになってます。逆の処理を行なっているわけですね。
if文のところでは、そもそもそのユーザーネームを持っているWhoisがあるのかをKVStore側に問い合わせています。あればtrue、なければfalseが返ってくるみたいですね。なかった場合は、minimumPriceを設定したWhoisを返す形になっています。もしまだ使われてないユーザーネームならば、これから新しく作るか?みたいな流れの処理に繋がるのかと思われます。推測ですけど。
GetWhoisメソッドのように、ユーザーネームからWhoisの情報を参照する関数を以下に用意しました。また、Whois内の情報の更新を行うためのメソッドは、SetWhoisとGetWhoisのメソッドを使うことによって記述を楽にしています。
// ResolveName - returns the string that the name resolves to
func (k Keeper) ResolveName(ctx sdk.Context, name string) string {
return k.GetWhois(ctx, name).Value
}
// SetName - sets the value string that a name resolves to
func (k Keeper) SetName(ctx sdk.Context, name string, value string) {
whois := k.GetWhois(ctx, name)
whois.Value = value
k.SetWhois(ctx, name, whois)
}
// HasOwner - returns whether or not the name already has an owner
func (k Keeper) HasOwner(ctx sdk.Context, name string) bool {
return !k.GetWhois(ctx, name).Owner.Empty()
}
// GetOwner - get the current owner of a name
func (k Keeper) GetOwner(ctx sdk.Context, name string) sdk.AccAddress {
return k.GetWhois(ctx, name).Owner
}
// SetOwner - sets the current owner of a name
func (k Keeper) SetOwner(ctx sdk.Context, name string, owner sdk.AccAddress) {
whois := k.GetWhois(ctx, name)
whois.Owner = owner
k.SetWhois(ctx, name, whois)
}
// GetPrice - gets the current price of a name. If price doesn't exist yet, set to 1nametoken.
func (k Keeper) GetPrice(ctx sdk.Context, name string) sdk.Coins {
return k.GetWhois(ctx, name).Price
}
// SetPrice - sets the current price of a name
func (k Keeper) SetPrice(ctx sdk.Context, name string, price sdk.Coins) {
whois := k.GetWhois(ctx, name)
whois.Price = price
k.SetWhois(ctx, name, whois)
}
二つ目のSetNameメソッドが、ユーザーネームの情報を更新できる?仕様になっているように思えて、頭を悩ませていたのですが、Whois内のvalueの設定を読み返すと、ただのユーザーネームのstringというわけではなく、「今後、IPアドレス、DNS Zone file, ブロックチェーンアドレスなどのように、特定のフォーマットに適した値に修正されることがある。」との記述があったので、あくまでもKVStoreではKeyの部分が変更することのできないstring型(僕ら人間にもわかる文字列として)のユーザーネームとして存在しており、その中のvalueは利用用途に合わせて変更、変換可能な値として用意していると理解しました。
その他の関数も、コードを読みながらどうやってコメントの意図通りに実装しているのか確認してみてください。ここまで読んだ強いあなたならわかるはずです!!
sdkモジュールには、全てのkey,valueペアの情報を返してくれるIteratorメソッドが準備されており、それをこのKeeper内で取得するためのメソッドが以下になります。
// Get an iterator over all names in which the keys are the names and the values are the whois
func (k Keeper) GetNamesIterator(ctx sdk.Context) sdk.Iterator {
store := ctx.KVStore(k.storeKey)
return sdk.KVStorePrefixIterator(store, []byte{})
}
最後のコードは、Keeperのコンストラクタになります。
Keeperの最初に記述でも良かったような。
// NewKeeper creates new instances of the nameservice Keeper
func NewKeeper(coinKeeper bank.Keeper, storeKey sdk.StoreKey, cdc *codec.Codec) Keeper {
return Keeper{
coinKeeper: coinKeeper,
storeKey: storeKey,
cdc: cdc,
}
}
今回はここで終わりです。なかなかに長かった!!これで1/3くらいのはずです。
まだまだ先は長いですが、おぼろげにCosmos-SDKが見えてきたのではないかと思います。個人的にはブロックチェーンというよりは、golangのオブジェクト指向や、ポインタ、コーデック、Object-capabilityなど確認できてとても勉強になりました。
パズルを解くみたいで楽しいですね。ここまでついてきてるあなたはそこそこの変態だと思います。
一緒に自分で書いたブロックチェーンを立ち上げるまで頑張って行きましょう!!
次は、MsgsとHandlersを実装して、ユーザー自身がどうやってストアに作用するのかの部分を実装していきます!!
前回同様、時間が経つと飽きてて描かなくなるので、明後日月曜日7月15日までに続きのその3を公開することにします。お楽しみに!!
その3は以下のリンクです!
お知らせ
■HashHubでは入居者募集中です!
HashHubは、ブロックチェーン業界で働いている人のためのコワーキングスペースを運営しています。ご利用をご検討の方は、下記のWEBサイトからお問い合わせください。また、最新情報はTwitterで発信中です。
HashHub:https://hashhub.tokyo/
Twitter:https://twitter.com/HashHub_Tokyo
■ブロックチェーンエンジニア集中講座開講中!
HashHubではブロックチェーンエンジニアを育成するための短期集中講座を開講しています。お申込み、詳細は下記のページをご覧ください。
ブロックチェーンエンジニア集中講座:https://www.blockchain-edu.jp/