Cosmos-sdkのチュートリアルを触ってみる その5

Takuya Fujita
GBEC Tech Blog
Published in
21 min readJul 20, 2019

前回までは以下のリンクになります

前回までで3分の2ほどが終わりました。前回はCLIとRESTからチェーンの情報を参照したり、トランザクションを作成しました。なんとなくアプリとしてどう動くのか見えてきたかと思います。

今回は”AppModule Interface”からやっていきます!

https://cosmos.network/docs/tutorial/module.html

AppModule Interface

Cosmos SDKはモジュール用の標準インターフェイスを提供しています。このAppModule インターフェイスは、モジュールをアプリケーションチェーンの中に組み込めるようにするため、モジュールに対しModulebasicsManagerから使えるメソッドの提供を求めています。まず、そのインターフェイスを足場にして、メソッドをいくつか実装します。その後で、authとbankと共にnameserviceモジュールをアプリに組み込みます。

どうやら今まで作ってきたnameserviceモジュールをアプリチェーンの一部としてちゃんと機能させるためにはそのインターフェイスに沿った仕様にする必要があるみたいです。このAppModuleのインターフェイスの仕様を守れば、自分でもチェーンに対して追加機能などカスタマイズが可能になるのではないでしょうか。チェーン上でトランザクションに合わせて機械学習を走らせるモジュールとか作れたら面白そうですね。

module.goとgenesis.goという新しい2つのファイルを用意します。AppModuleインターフェイスをmodule.goに、最初のステートの管理に関しての関数をgenesis.goに実装します。あなたが作るAppModule構造体での最初のメソッドは、genesis.goで定義されたものへのパスを通すコールとなります。

とりあえず、module.goからやっていきましょう。場所は、./x/nameservice/module.goとなります。

package nameserviceimport (
"encoding/json"
"github.com/gorilla/mux"
"github.com.spf13/cobra"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/types/module"
"github.com/cosmos/cosmos-sdk/x/bank"
"github.com/cosmos/sdk-application-tutorial/x/nameservice/client/cli"
"github.com/cosmos/sdk-application-tutorial/x/nameservice/client/rest"
"github.com/cosmos/cosmos-sdk/client/context"
sdk "github.com/cosmos/cosmos-sdk/types"
abci "github.com/tendermint/tendermint/abci/types"
)
// type check to ensure the interface is properly implemented
var (
_ module.AppModule = AppModule{}
_ module.AppModuleBasic = AppModulebasic{}
)
// app module Basics object
type AppModulebasic struct{}
func (AppModuleBasic) Name() string {
return ModuleName
}
func (AppModuleBasic) RegisterCodec(cdc *codec.Codec) {
RegisterCodec(cdc)
}
func (AppModuleBasic) DefaultGenesis() json.RawMessage {
return ModuleCdc.MustMarshalJSON(DefaultGenesisState())
}
// Validation check of the Genesis
func (AppModuleBasic) ValidateGenesis(bz json.RawMessage) error {
var data GenesisState
err := ModuleCdc.UnmarshalJSON(bz, &data)
if err != nil {
return err
}
// Once json successfully marshalled, passed along to genesis.go
return ValidateGenesis(data)
}
// Register rest routes
func (AppModuleBasic) RegisterRESTRoutes(ctx context.CLIContext, rtr *mux.Router) {
rest.RegisterRoutes(ctx, rtr, StoreKey)
}
// Get the root query command of this module
func (AppModuleBasic) GetQueryCmd(cdc *codec.Codec) *cobra.Command {
return cli.GetQueryCmd(StoreKey, cdc)
}
// Get the root tx command of this module
func (AppModuleBasic) GetTxCmd(cdc *codec.Codec) *cobra.Command {
return cli.GetTxCmd(StoreKey, cdc)
}
type AppModule struct {
AppModuleBasic
keeper Keeper
coinKeeper bank.Keeper
}
// NewAppModule creates a new AppModule Object
func NewAppModule(k Keeper, bankKeeper bank.Keeper) AppModule {
return AppModule{
AppModuleBasic: AppModuleBasic{},
keeper: k,
coinKeeper: bankKeeper,
}
}
func (AppModule) Name() string {
return ModuleName
}
func (am AppModule) RegisterInvariants(ir sdk.InvariantRegistry) {}func (am AppModule) Route() string {
return RouterKey
}
func (am AppModule) Newhandler() sdk.Handler {
return NewHandler(am.keeper)
}
func (am AppModule) QuerierRoute() string {
return ModuleName
}
func (am AppModule) NewQuerierhandler() sdk.Querier {
return NewQuerier(am.keeper)
}
func (am AppModule) BeginBlock(_ sdk.Context, _ abci.RequestBeginBlock) sdk.Tags {
return sdk.EmptyTags()
}
func (am AppModule) EndBlock(sdk.Context, abci.RequestEndBlock) ([]abci.ValidatorUpdate, sdk.Tags) {
return []abci.ValidatorUpdate{}, sdk.EmptyTags()
}
func (am AppModule) InitGenesis(ctx sdk.Context, data json.RawMessage) []abci.ValidatorUpdate {
var genesisState GenesisState
Moduledc.MustUnmarshalJSON(data, &genesisState)
return InitGenesis(ctx, am.keeper, genesisState)
}
func (am AppModule) ExportGenesis(ctx sdk.Context) json.RawMessage {
gs := ExportGenesis(ctx, am.keeper)
return ModuleCdc.MustMarshalJSON(gs)
}

これ以外にもAppModuleの実装を見たい場合は、stakingモジュールなどのSDKの他のモジュールのmodule.goを調べてみましょう。

https://github.com/cosmos/cosmos-sdk/blob/master/x/staking/genesis.go

ここまで来てチュートリアルの解説がかなり雑になっている気がする。。
書いてる人も疲れて来たのかな笑

前回実装したCLIとRESTの部分をimportしており、RESTやQuery、トランザクションに関するメソッドをStoreKeyを使って実行させてることがわかりますね。

Genesisに関しての言及が多いのもわかります。次にgenesis.goファイルを作っていきます。

Genesis

AppModuleインターフェイスはチェーンのGenesisStateの初期化と取り出しに使うための多くの関数を含んでいます。チェーンを開始したり、停止したり、またデータを取り出したりする時に、各モジュールからModulebasicManagerがこれらの関数を呼びだすとのことです。

./x/nameservice/genesis.goに新しいファイルを作っていきましょう。

package nameserviceimport (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
abci "github.com/tendermint/tendermint/abci/types"
)
type GenesisState struct {
WhoisRecords []Whois 'json:"whois_records"'
}
func NewGenesisState(whoIsRecords []Whois) GenesisState {
return GenesisState{WhoisRecords: nil}
}
func ValidateGenesis(data GenesisState) error {
for _, record := range data.WhoisRecords {
if record.Owner == nil {
return fmt.Errorf("Invalid WhoisRecord: Value: %s. Error: Missing Owner", record.Value)
}
if record.Value == "" {
return fmt.Errorf("Invalid WhoisRecord: Owner: %s. Error: Missing Value", record.Owner)
}
if record.Price == nil {
return fmt.Errorf("Invalid WhoisRecord: Value: %s. Error: Missing Price", record.Value)
}
}
return nil
}
func DefaultGenesisState() GenesisState {
return GenesisState{
WhoisRecords: []Whois{}
}
}
func InitGenesis(ctx sdk.Context, keeper Keeper, data GenesisState) []abci.ValidatorUpdate {
for _, record := range data.WhoisRecords {
keeper.SetWhois(ctx, record.Value, record)
}
return []abci.ValidatorUpdate{}
}
func ExportGenesis(ctx sdk.Context, k Keeper) GenesisState {
var records []Whois
iterator := k.GetNamesIterator(ctx)
for ; iterator.Valid(); iterator.Next() {
name := string(iterator.Key())
var whois Whois
whois = k.GetWhois(ctx, name)
records = append(records, whois)
}
return GenesisState{WhoisRecords: records}
}

それぞれの関数を簡単に説明します。

ValidateGenesis() : チェーン立ち上げの最初に設定されるステートにちゃんと値が入っているかを検証する。

DefaultGenesisState() : テスト用に使うもの。チェーン起動時に用意する最小のステート。中身が空っぽなのがわかります。

InitGenesis() : チェーン起動時に呼び出される関数で、nameserviceモジュールのkeeperに対して起動時のステートを入力するものです。

ExportGenesis() : チェーン停止後に呼び出される関数で、アプリ上にあるステートをGenesis Stateの構造体の形にし、他のモジュールがもつデータと一緒に(通貨保持量とか?)genesis.jsonという形で出力するもの。

一番最初のステートの設定だけでなく、チェーンを停止させた後にステートを出力する関数まで用意されてました。ハードスプーンとかする時に使うのかな〜と思いましたが、”チェーン停止後に呼び出す関数”とのことなので何か障害などの緊急時に使う関数かと思われます。取り出したステートを使ってそれを起動時のステートとして別のチェーンを立ち上げることもできそうですね。

Import your modules and finish your application

これでNameserviceモジュールの準備はでき他ので、authとbankという二つのモジュールと一緒に./app.goファイルに組み込むことができるようになりました。
Nameserviceモジュールをインポートに追加するところからやっていきましょう!

./app.goファイルに関しては、最初の方で骨組みとして書いたものがあるはずです。以下のコードと見比べながら抜けているところを補足していく形でもいいですが、結構importのモジュールの順番が変わってるので最初から書いた方がわかりやすいと思います。

package appimport (
"encoding/json"
"os"
abci "github.com/tendermint/tendermint/abci/types"
cmn "github.com/tendermint/tendermint/libs/common"
dbm "github.com/tendermint/tendermint/libs/db"
"github.com/tendermint/tendermint/libs/log"
tmtypes "github.com/tendermint/tendermint/types"
bam "github.com/cosmos/cosmos-sdk/baseapp"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/types/module"
"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/genutil"
"github.com/cosmos/cosmos-sdk/x/params"
"github.com/cosmos/cosmos-sdk/x/slashing"
"github.com/cosmos/cosmos-sdk/x/staking"
"github.com/cosmos/sdk-application-tutorial/x/nameservice"
)
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 = module.NewBasicManager(
genaccounts.AppModuleBasic{},
genutil.AppModuleBasic{},
auth.AppModuleBasic{},
bank.AppModuleBasic{},
params.AppModuleBasic{},
nameservice.AppModule{},
staking.AppModulebasic{},
distr.AppModuleBasic{},
slashing.AppModuleBasic{},
)
)

色々とimportさせた後に、ModuleBasicsでこのアプリで扱うモジュールを設定しているのがわかります。頑張って作ったnameserviceもnameservice.AppModule{}として中に入っていることが確認できます。できますが、一つだけBasicが入ってなくてちょっと寂しい笑

次に、storeに接続するためのkeyと、Keepersの設定を、nameServiceApp構造体に加えていきます。

type nameServiceApp struct {
*bam.BaseApp
cdc *codec.Codec
// Keys to access the substores
keyMain *sdk.KVStoreKey
keyAccount *sdk.KVStoreKey
keyFeeCollection *sdk.KVStoreKey
keyStaking *sdk.KVStoreKey
tkeyStaking *sdk.TransientStoreKey
keyDistr *sdk.KVStoreKey
tkeyDistr *sdk.TransientStoreKey
keyNS *sdk.KVStoreKey
keyParams *sdk.KVStoreKey
tkeyParams *sdk.TransientStoreKey
keySlashing *sdk.KVStoreKey
// Keepers
accountKeeper auth.AccountKeeper
bankKeeper bank,Keeper
stakingKeeper staking.Keeper
slashingKeeper slashing.Keeper
distrKeeper distr.Keeper
feeCollectionKeeper auth.FeeCollectionKeeper
paramsKeeper params.Keeper
nsKeeper nameservice.Keeper
// Module Manager
mm *module.Manager
}
// NewNameServiceApp is a constructor function for nameServiceApp
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,
keyMain: sdk.NewKVStoreKey(bam.MainStoreKey),
keyAccount: sdk.NewKVStoreKey(auth.StoreKey),
keyFeeCollection: sdk.NewKVStoreKey(auth.FeeStoreKey),
keyStaking: sdk.NewKVStoreKey(staking.StoreKey),
tkeyStaking: sdk.NewTransientStoreKey(staking.TStoreKey),
keyDistr: sdk.NewKVStoreKey(distr.StoreKey),
tkeyDistr: sdk.NewTransientStoreKey(distr.TStoreKey),
keyNS: sdk.NewKVStoreKey(params.StoreKey),
keyPrams: sdk.NewKVStoreKey(params.StoreKey),
tkeyParams: sdk.NewTransientStoreKey(params.TStoreKey),
keySlashing: sdk.NewKVStoreKey(slashing.StoreKey),
}
}

改めて、他のモジュールにもKeeperが会ってそれぞれに情報を保持しているStoreがあることを思い知らされます。チュートリアルが終わったら、それぞれのモジュールの中がどうなっていて、一モジュールとして機能しているのかとか見ていくのも面白そうですね。

中途半端ですが、長くなりそうなので、今回はここまでとします!!お疲れ様でした。

次回はアプリケーションに対して、それぞれのモジュールのKeeperに接続できるように設定を行なっていきます。

その6は7月22日の24時までに投稿します!終わらせたい!!

お知らせ

■HashHubでは入居者募集中です!
HashHubは、ブロックチェーン業界で働いている人のためのコワーキングスペースを運営しています。ご利用をご検討の方は、下記のWEBサイトからお問い合わせください。また、最新情報はTwitterで発信中です。

HashHub:https://hashhub.tokyo/
Twitter:https://twitter.com/HashHub_Tokyo

■ブロックチェーンエンジニア集中講座開講中!
HashHubではブロックチェーンエンジニアを育成するための短期集中講座を開講しています。お申込み、詳細は下記のページをご覧ください。

ブロックチェーンエンジニア集中講座:https://www.blockchain-edu.jp/

--

--