剖析Cosmos SDK(十三):构建模块(查询服务、BeginBlocker&EndBlocker、Keeper)

煤油灯科技2022-07-28 10:00

查询服务

Protobuf 查询服务处理查询。查询服务特定于定义它们的模块,并且只处理在所述模块中定义的查询。它们是从BaseApp 的 Query方法调用的。

查询者类型

Cosmos SDK中定义的查询器类型将被弃用,以支持gRPC服务。它指定了查询器函数的典型结构:

package types

import (
	abci "github.com/tendermint/tendermint/abci/types"
)

// Querier defines a function type that a module querier must implement to handle
// custom client queries.
type Querier = func(ctx Context, path []string, req abci.RequestQuery) ([]byte, error)

分解:

  • Context包含处理查询所需的所有必要信息,以及最新状态的分支。它主要是由keeper用来访问状态的。
  • 路径是一个字符串数组,包含查询的类型,也可以包含查询参数。
  • req本身主要用于检索参数,如果这些参数太大,不适合放在路径中。这是用req的Data字段完成的。
  • 结果以[]字节的形式返回给BaseApp,使用应用程序的编解码器进行编解码。

模块的查询服务的实现

gRPC服务

在定义Protobuf查询服务时,每个模块都会生成一个查询服务器接口,其中包含所有的服务方法:

type QueryServer interface {
	QueryBalance(context.Context, *QueryBalanceParams) (*types.Coin, error)
	QueryAllBalances(context.Context, *QueryAllBalancesParams) (*QueryAllBalancesResponse, error)
}

这些自定义查询方法应该由模块的keeper实现,通常在./keeper/grpc_query.go中。这些方法的第一个参数是通用context.Context,而查询器方法通常需要sdk.Context的实例才能从存储中读取。因此,Cosmos SDK提供了一个函数sdk.UnwrapSDKContext来从提供的context.Context中检索sdk.Context。

这是bank模块的示例实现:

package keeper

import (
	"context"

	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"

	"github.com/cosmos/cosmos-sdk/store/prefix"
	sdk "github.com/cosmos/cosmos-sdk/types"
	"github.com/cosmos/cosmos-sdk/types/query"
	"github.com/cosmos/cosmos-sdk/x/bank/types"
)

var _ types.QueryServer = BaseKeeper{}

// Balance implements the Query/Balance gRPC method
func (k BaseKeeper) Balance(ctx context.Context, req *types.QueryBalanceRequest) (*types.QueryBalanceResponse, error) {
	if req == nil {
		return nil, status.Error(codes.InvalidArgument, "empty request")
	}

	if req.Address == "" {
		return nil, status.Error(codes.InvalidArgument, "address cannot be empty")
	}

	if req.Denom == "" {
		return nil, status.Error(codes.InvalidArgument, "invalid denom")
	}

	sdkCtx := sdk.UnwrapSDKContext(ctx)
	address, err := sdk.AccAddressFromBech32(req.Address)
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "invalid address: %s", err.Error())
	}

	balance := k.GetBalance(sdkCtx, address, req.Denom)

	return &types.QueryBalanceResponse{Balance: &balance}, nil
}

// AllBalances implements the Query/AllBalances gRPC method
func (k BaseKeeper) AllBalances(ctx context.Context, req *types.QueryAllBalancesRequest) (*types.QueryAllBalancesResponse, error) {
	if req == nil {
		return nil, status.Error(codes.InvalidArgument, "empty request")
	}

	if req.Address == "" {
		return nil, status.Error(codes.InvalidArgument, "address cannot be empty")
	}

	addr, err := sdk.AccAddressFromBech32(req.Address)
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "invalid address: %s", err.Error())
	}

	sdkCtx := sdk.UnwrapSDKContext(ctx)

	balances := sdk.NewCoins()
	accountStore := k.getAccountStore(sdkCtx, addr)

	pageRes, err := query.Paginate(accountStore, req.Pagination, func(key, value []byte) error {
		var amount sdk.Int
		if err := amount.Unmarshal(value); err != nil {
			return err
		}
		balances = append(balances, sdk.NewCoin(string(key), amount))
		return nil
	})
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "paginate: %v", err)
	}

	return &types.QueryAllBalancesResponse{Balances: balances, Pagination: pageRes}, nil
}

// SpendableBalances implements a gRPC query handler for retrieving an account's
// spendable balances.
func (k BaseKeeper) SpendableBalances(ctx context.Context, req *types.QuerySpendableBalancesRequest) (*types.QuerySpendableBalancesResponse, error) {
	if req == nil {
		return nil, status.Error(codes.InvalidArgument, "empty request")
	}

	addr, err := sdk.AccAddressFromBech32(req.Address)
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "invalid address: %s", err.Error())
	}

	sdkCtx := sdk.UnwrapSDKContext(ctx)

	balances := sdk.NewCoins()
	accountStore := k.getAccountStore(sdkCtx, addr)
	zeroAmt := sdk.ZeroInt()

	pageRes, err := query.Paginate(accountStore, req.Pagination, func(key, value []byte) error {
		balances = append(balances, sdk.NewCoin(string(key), zeroAmt))
		return nil
	})
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "paginate: %v", err)
	}

	result := sdk.NewCoins()
	spendable := k.SpendableCoins(sdkCtx, addr)

	for _, c := range balances {
		result = append(result, sdk.NewCoin(c.Denom, spendable.AmountOf(c.Denom)))
	}

	return &types.QuerySpendableBalancesResponse{Balances: result, Pagination: pageRes}, nil
}

// TotalSupply implements the Query/TotalSupply gRPC method
func (k BaseKeeper) TotalSupply(ctx context.Context, req *types.QueryTotalSupplyRequest) (*types.QueryTotalSupplyResponse, error) {
	sdkCtx := sdk.UnwrapSDKContext(ctx)
	totalSupply, pageRes, err := k.GetPaginatedTotalSupply(sdkCtx, req.Pagination)
	if err != nil {
		return nil, status.Error(codes.Internal, err.Error())
	}

	return &types.QueryTotalSupplyResponse{Supply: totalSupply, Pagination: pageRes}, nil
}

// SupplyOf implements the Query/SupplyOf gRPC method
func (k BaseKeeper) SupplyOf(c context.Context, req *types.QuerySupplyOfRequest) (*types.QuerySupplyOfResponse, error) {
	if req == nil {
		return nil, status.Error(codes.InvalidArgument, "empty request")
	}

	if req.Denom == "" {
		return nil, status.Error(codes.InvalidArgument, "invalid denom")
	}

	ctx := sdk.UnwrapSDKContext(c)
	supply := k.GetSupply(ctx, req.Denom)

	return &types.QuerySupplyOfResponse{Amount: sdk.NewCoin(req.Denom, supply.Amount)}, nil
}

// Params implements the gRPC service handler for querying x/bank parameters.
func (k BaseKeeper) Params(ctx context.Context, req *types.QueryParamsRequest) (*types.QueryParamsResponse, error) {
	if req == nil {
		return nil, status.Errorf(codes.InvalidArgument, "empty request")
	}

	sdkCtx := sdk.UnwrapSDKContext(ctx)
	params := k.GetParams(sdkCtx)

	return &types.QueryParamsResponse{Params: params}, nil
}

// DenomsMetadata implements Query/DenomsMetadata gRPC method.
func (k BaseKeeper) DenomsMetadata(c context.Context, req *types.QueryDenomsMetadataRequest) (*types.QueryDenomsMetadataResponse, error) {
	if req == nil {
		return nil, status.Errorf(codes.InvalidArgument, "empty request")
	}

	ctx := sdk.UnwrapSDKContext(c)
	store := prefix.NewStore(ctx.KVStore(k.storeKey), types.DenomMetadataPrefix)

	metadatas := []types.Metadata{}
	pageRes, err := query.Paginate(store, req.Pagination, func(_, value []byte) error {
		var metadata types.Metadata
		k.cdc.MustUnmarshal(value, &metadata)

		metadatas = append(metadatas, metadata)
		return nil
	})
	if err != nil {
		return nil, status.Error(codes.Internal, err.Error())
	}

	return &types.QueryDenomsMetadataResponse{
		Metadatas:  metadatas,
		Pagination: pageRes,
	}, nil
}

// DenomMetadata implements Query/DenomMetadata gRPC method.
func (k BaseKeeper) DenomMetadata(c context.Context, req *types.QueryDenomMetadataRequest) (*types.QueryDenomMetadataResponse, error) {
	if req == nil {
		return nil, status.Errorf(codes.InvalidArgument, "empty request")
	}

	if req.Denom == "" {
		return nil, status.Error(codes.InvalidArgument, "invalid denom")
	}

	ctx := sdk.UnwrapSDKContext(c)

	metadata, found := k.GetDenomMetaData(ctx, req.Denom)
	if !found {
		return nil, status.Errorf(codes.NotFound, "client metadata for denom %s", req.Denom)
	}

	return &types.QueryDenomMetadataResponse{
		Metadata: metadata,
	}, nil
}

func (k BaseKeeper) DenomOwners(
	goCtx context.Context,
	req *types.QueryDenomOwnersRequest,
) (*types.QueryDenomOwnersResponse, error) {
	if req == nil {
		return nil, status.Errorf(codes.InvalidArgument, "empty request")
	}

	if req.Denom == "" {
		return nil, status.Error(codes.InvalidArgument, "empty denom")
	}

	ctx := sdk.UnwrapSDKContext(goCtx)
	denomPrefixStore := k.getDenomAddressPrefixStore(ctx, req.Denom)

	var denomOwners []*types.DenomOwner
	pageRes, err := query.FilteredPaginate(
		denomPrefixStore,
		req.Pagination,
		func(key []byte, value []byte, accumulate bool) (bool, error) {
			if accumulate {
				address, _, err := types.AddressAndDenomFromBalancesStore(key)
				if err != nil {
					return false, err
				}

				denomOwners = append(
					denomOwners,
					&types.DenomOwner{
						Address: address.String(),
						Balance: k.GetBalance(ctx, address, req.Denom),
					},
				)
			}

			return true, nil
		},
	)
	if err != nil {
		return nil, status.Error(codes.Internal, err.Error())
	}

	return &types.QueryDenomOwnersResponse{DenomOwners: denomOwners, Pagination: pageRes}, nil
}

遗留查询器

模块遗留查询器通常在模块文件夹内的./keeper/querier.go文件中实现。模块管理器用于通过NewQuerier()方法将模块的查询器添加到应用程序的queryRouter。通常,管理器的NewQuerier() 方法只是调用keeper/querier.go中定义的NewQuerier()方法,如下所示:

func NewQuerier(keeper Keeper) sdk.Querier {
	return func(ctx sdk.Context, path []string, req abci.RequestQuery) ([]byte, error) {
		switch path[0] {
		case QueryType1:
			return queryType1(ctx, path[1:], req, keeper)

		case QueryType2:
			return queryType2(ctx, path[1:], req, keeper)

		default:
			return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unknown %s query endpoint: %s", types.ModuleName, path[0])
		}
	}
}

这个简单的开关返回特定于接收到的查询类型的查询器函数。在查询生命周期的这一点上,路径的第一个元素 (path[0]) 包含查询的类型。以下元素为空或包含处理查询所需的参数。

查询器函数本身非常简单。他们通常使用keeper从状态中获取一个或多个值。然后,他们使用编解码器对值进行编组并返回作为结果获得的[]byte。


BeginBlocker&EndBlocker

BeginBlocker和EndBlocker是模块开发人员可以在其模块中实现的可选方法。它们将分别在每个区块的开始和结束时触发,当从底层共识引擎接收到BeginBlock和EndBlock ABCI消息时。

BeginBlocker和EndBlocker是模块开发人员向其模块添加自动执行逻辑的一种方式。这是一个应该小心使用的强大工具,因为复杂的自动功能会减慢甚至停止链条。

需要时,BeginBlocker和EndBlocker被实现为AppModule接口的一部分。module.go中实现的接口的BeginBlock和EndBlock方法一般分别遵循BeginBlocker和EndBlocker方法,这些方法通常在abci.go中实现。

abci.go中BeginBlocker和EndBlocker的实际实现与Msg服务的实现非常相似:

  • 他们通常使用keeper和ctx来检索有关最新状态的信息。
  • 如果需要,他们使用keeper和ctx来触发状态转换。
  • 如果需要,他们可以通过ctx的EventManager发出事件。

EndBlocker的一个特点是它可以以[]abci.ValidatorUpdates的形式将验证器更新返回到底层共识引擎。这是实现自定义验证器更改的首选方式。

开发人员可以通过模块的管理器SetOrderBeginBlocker/SetOrderEndBlocker方法定义每个应用程序模块的BeginBlocker/EndBlocker函数之间的执行顺序。

请参阅分发模块中BeginBlocker的示例实现:

// BeginBlocker sets the proposer for determining distribution during endblock
// and distribute rewards for the previous block
func BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock, k keeper.Keeper) {
	defer telemetry.ModuleMeasureSince(types.ModuleName, time.Now(), telemetry.MetricKeyBeginBlocker)

	// determine the total power signing the block
	var previousTotalPower, sumPreviousPrecommitPower int64
	for _, voteInfo := range req.LastCommitInfo.GetVotes() {
		previousTotalPower += voteInfo.Validator.Power
		if voteInfo.SignedLastBlock {
			sumPreviousPrecommitPower += voteInfo.Validator.Power
		}
	}

	// TODO this is Tendermint-dependent
	// ref https://github.com/cosmos/cosmos-sdk/issues/3095
	if ctx.BlockHeight() > 1 {
		previousProposer := k.GetPreviousProposerConsAddr(ctx)
		k.AllocateTokens(ctx, sumPreviousPrecommitPower, previousTotalPower, previousProposer, req.LastCommitInfo.GetVotes())
	}

	// record the proposer for when we payout on the next block
	consAddr := sdk.ConsAddress(req.Header.ProposerAddress)
	k.SetPreviousProposerConsAddr(ctx, consAddr)
}

以及来自staking模块的EndBlocker的示例实现:

// Called every block, update validator set
func EndBlocker(ctx sdk.Context, k keeper.Keeper) []abci.ValidatorUpdate {
	defer telemetry.ModuleMeasureSince(types.ModuleName, time.Now(), telemetry.MetricKeyEndBlocker)

	return k.BlockValidatorUpdates(ctx)
}


Keeper

Keepers指的是Cosmos SDK抽象,其作用是管理对各种模块定义的状态子集的访问。Keeper是特定于模块的,即模块定义的状态子集只能由该模块中定义的keeper访问。如果一个模块需要访问另一个模块定义的状态子集,则需要将对第二个模块的内部保持器的引用传递给第一个模块。这是在模块保持器实例化期间在app.go中完成的。

动机

Cosmos SDK是一个框架,它使开发者很容易从头开始建立复杂的去中心化的应用程序,主要是通过将模块组合在一起。随着Cosmos SDK的开源模块生态系统的扩大,由于开发者的疏忽或恶意,其中一些模块包含漏洞的可能性会越来越大。

Cosmos SDK采用了一种基于对象能力的方法来帮助开发者更好地保护他们的应用,使其免受不必要的模块间交互的影响,而keeper是这种方法的核心。从字面上看,Keeper可以被认为是模块的存储空间的守门人。在一个模块中定义的每个存储空间(通常是一个IAVL存储空间)都有一个存储密钥(storeKey),它赋予了对它的无限访问权。模块的keeper持有这个storeKey(否则应该保持不公开),并定义了对存储空间的读写方法。

对象能力方法的核心思想是只揭示完成工作所需的内容。在实践中,这意味着不是通过访问控制列表来处理模块的权限,而是向模块保持者传递一个他们需要访问的其他模块保持者的具体实例的引用(这在应用程序的构造函数中完成)。因此,一个模块只能通过其他模块的keeper实例所公开的方法与另一个模块中定义的状态子集进行交互。这是一个很好的方法,开发者可以控制自己的模块与外部开发者开发的模块的交互。

类型定义

守护程序通常在位于模块文件夹中的/keeper/keeper.go文件中实现。按照约定,模块的类型保持器简称为Keeper,通常遵循以下结构:

type Keeper struct {
    // External keepers, if any

    // Store key(s)

    // codec
}

例如,这里是staking模块中keeper的类型定义:

// keeper of the staking store
type Keeper struct {
	storeKey   storetypes.StoreKey
	cdc        codec.BinaryCodec
	authKeeper types.AccountKeeper
	bankKeeper types.BankKeeper
	hooks      types.StakingHooks
	paramstore paramtypes.Subspace
}

不同的参数:

  • 一个预期的keeper是一个模块的外部keeper,是该模块的内部keeper所需要的。外部keeper被列在内部keeper的类型定义中,作为接口。这些接口本身被定义在模块文件夹根部的expected_keepers.go文件中。在这种情况下,接口被用来减少依赖关系的数量,同时也便于模块本身的维护。
  • storeKeys授予对模块所管理的multistore空间的访问。它们应该始终保持不向外部模块开放。
  • cdc是用于从[]byte到/到marshall结构的编解码器。根据要求,cdc可以是codec.BinaryCodec、codec.JSONCodec或codec.Codec中的任何一种。只要它们实现了这些接口,它就可以是原编码或氨基编码解码器。

当然,也可以为同一个模块定义不同类型的内部keeper(例如,只读keeper)。每种类型的keeper都有自己的构造函数,从应用程序的构造函数中调用。这是保持器被实例化的地方,也是开发者确保将模块keeper的正确实例传递给需要它们的其他模块的地方。

实现方法

Keeper主要为其模块所管理的商店提供getter和setter方法。这些方法应该尽可能简单,并严格限制在获取或设置请求的值上,因为当keeper的方法被调用时,应该已经通过消息和Msg服务器的ValidateBasic()方法进行了有效性检查。

通常情况下,getter方法会有以下签名:

func (k Keeper) Get(ctx sdk.Context, key string) returnType

而该方法将经历以下步骤:

  • 使用storeKey从ctx中检索适当的存储。这是通过ctx的KVStore(storeKey sdk.StoreKey)方法完成的。然后,为了方便和安全,最好使用prefix.Store,只访问所需的有限的存储子集。
  • 如果它存在,使用存储空间的Get(key []byte)方法获得存储在位置[]byte(key)的[]字节值。
  • 使用编解码器cdc将检索到的值从[]byte解压缩到returnType。返回该值。

类似地,一个setter方法将有如下签名:

func (k Keeper) Set(ctx sdk.Context, key string, value valueType)

而该方法将经历以下步骤:

  • 使用storeKey从ctx中检索适当的存储。这是通过ctx的KVStore(storeKey sdk.StoreKey)方法完成的。为了方便和安全,最好使用prefix.Store,只访问所需的有限的存储子集。
  • 使用编解码器cdc将值转为[]byte。
  • 使用存储空间的Set(key []byte, value []byte)方法在存储空间的位置key处设置编码后的值。

KVStore模块还提供了一个Iterator()方法,它返回一个Iterator对象来迭代一个域的键。

这是来自 auth 模块的例子,用来迭代账户:

// IterateAccounts iterates over all the stored accounts and performs a callback function.
// Stops iteration when callback returns true.
func (ak AccountKeeper) IterateAccounts(ctx sdk.Context, cb func(account types.AccountI) (stop bool)) {
	store := ctx.KVStore(ak.key)
	iterator := sdk.KVStorePrefixIterator(store, types.AddressStoreKeyPrefix)

	defer iterator.Close()
	for ; iterator.Valid(); iterator.Next() {
		account := ak.decodeAccount(iterator.Value())

		if cb(account) {
			break
		}
	}
}


原文链接:https://docs.cosmos.network/main/building-modules/query-services.html

https://docs.cosmos.network/main/building-modules/beginblock-endblock.html

https://docs.cosmos.network/main/building-modules/keeper.html


---煤油灯科技victorlamp.com翻译整理---

你可能也喜欢这些文章

科技互联网,科技,资讯,动态,洞察,量子,计算,AI,人工智能,机器人,区块链,Web3,分布式,操作系统,OS,芯片,视频,深度,论文