COSMOS 源码分析——auth&bank模块

摘要

本文主要分析auth模块,bank顺带一起分析,基于 cosmos 版本:v0.38.5-rc1。

x/auth模块负责指定应用程序的基本交易和帐户类型,SDK本身是不知道这些细节的。它包含ante处理程序,其中执行所有基本的交易有效性检查(签名、nonces、辅助字段),并公开帐户管理员(此管理员只有一个,后面会详细说明),允许其他模块读取、写入和修改帐户。

x/bank模块保存两个主要对象的状态,即账户余额和所有余额的总量。x/bank负责处理账户间的资产转账,并跟踪特殊情况下的伪转账,这些伪转账必须与特定类型的账户有不同的工作方式(特别是授权账户的委托/不授权)。此外,bank模块跟踪应用程序中使用的所有资产的总供应量并提供查询。

先看 module.go 文件。

上类图

在这里插入图片描述

AppModuleBasic 是所有功能模块的基类,是一个标准形式。AppModule是具有完整的功能且相互依赖的模块。

我这里只挑了几个重要的函数进行分析。先看注册路由的函数 RegisterRESTRoutes

func (AppModuleBasic) RegisterRESTRoutes(ctx context.CLIContext, rtr *mux.Router) {
   rest.RegisterRoutes(ctx, rtr, authtypes.StoreKey)
}

// RegisterRESTRoutes 内部注册了两条路由,都只支持get方法,一条 QueryAccountRequestHandlerFn 用来查询账户,一条 queryParamsHandler 用来查询参数。

func QueryAccountRequestHandlerFn(storeName string, cliCtx context.CLIContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
        // 获取请求参数
		vars := mux.Vars(r)
        // 那么 address 是什么呢?又是怎么生成的呢?后面会分析
		bech32addr := vars["address"]

        // 解析地址,并使用bech32将地址解码
        // Bech32是一种地址格式。 它由BIP173作为SegWit地址引入。 Bech32由42个符号组成,以bc1开头。 例如:bc1qa5ndt07z2lu7r2kl6zrffw362chj74vse76lq5
        // 内部接口以十六进制或base64编码形式编码二进制值
		addr, err := sdk.AccAddressFromBech32(bech32addr)
         // 将错误信息与http 500 的响应进行绑定,没有错误返回false,否则返回true
        // 查询参数时也会用到此函数
		if rest.CheckInternalServerError(w, err) {
			return
		}

        // 如果由http请求设置,则设置查询的高度
        // 若 heigth == ""(height是r中的一个参数),set height = 0
        // 如果解析高度出错,则返回false
        // 查询参数时也会用到此函数
		cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
		if !ok {
			return
		}

        // 初始化一个新的AccountRetriever实例
		accGetter := types.NewAccountRetriever(client.Codec)

        // 根据给定地址的查询帐户。返回帐户信息和高度。如果查询或解码失败,将返回一个错误。
		account, height, err := accGetter.GetAccountWithHeight(cliCtx, addr)
		if err != nil {
			// 根据错误类型更恰当地处理
			// 参考: https://github.com/cosmos/cosmos-sdk/issues/4923
			if err := accGetter.EnsureExists(cliCtx, addr); err != nil {
				cliCtx = cliCtx.WithHeight(height)
                // 对REST响应执行后期处理。返回给客户端的结果将包含两个字段,查询资源的高度和原始结果
                // 查询参数时也会用到此函数
				rest.PostProcessResponse(w, cliCtx, types.BaseAccount{})
				return
			}

            // 将错误信息和状态码写入响应中
			rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
			return
		}

		cliCtx = cliCtx.WithHeight(height)
		rest.PostProcessResponse(w, cliCtx, account)
	}
}

func queryParamsHandler(cliCtx context.CLIContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
		if !ok {
			return
		}

        // 根据提供的route和params调用tendermint的abci查询接口。如果查询成功,则返回结果和查询的高度;如果查询失败,则返回错误。
		route := fmt.Sprintf("custom/%s/%s", types.QuerierRoute, types.QueryParams)
		res, height, err := cliCtx.QueryWithData(route, nil)
		if rest.CheckInternalServerError(w, err) {
			return
		}

		cliCtx = cliCtx.WithHeight(height)
		rest.PostProcessResponse(w, cliCtx, res)
	}
}

RegisterRESTRoutes函数是实现的父类模块:types/module/module.go,此模块的RegisterRESTRoutes函数在main.gosimapp/simcli/main.go)中被registerRoutes()调用。直接上图,下图顺序指的是函数调用顺序。
在这里插入图片描述

GetTxCmd()GetQueryCmd()分别实现auth的交易命令和查询命令,都是以cmd方式使用的。GetTxCmd()添加了三个功能命令:多个交易签名(GetMultiSignCommand)、单个交易签名(GetSignCommand)、查询签名的验证者(GetValidateSignaturesCommand),这三个功能命令都是从给定的文件中读取数据。GetQueryCmd()添加了两个功能命令:查询账户(GetAccountCmd)、查询证据参数(QueryParamsCmd)。这样说,可能他们之间的相互关系还不是很明确,上图,就不贴源码了。
在这里插入图片描述

来看RegisterInterfaceTypes函数,此函数将protoNameAccountI接口关联,并创建其具体实现的注册表。

func RegisterInterfaces(registry types.InterfaceRegistry) {
   registry.RegisterInterface(
      "cosmos_sdk.auth.v1.AccountI",
       // AccountI是一个接口,用于存储币在一个给定的地址内的状态。它定义一个序列号Sequence用于重播保护,一个帐户AccountNumber用于先前删除的帐户的重播保护,以及用于身份验证的公钥PubKey。
      (*AccountI)(nil),
       // 定义了基本帐户类型。它包含基本帐户功能的所有必要字段。任何自定义帐户类型都应该扩展该类型以实现其他功能(例如vesting)。这里就涉及到了上文提到的 `address`,`address`就是通过secp256k1曲线推导出来的经bech32编码后的值
      &BaseAccount{},
       // 为在池中持有币的模块定义帐户
      &ModuleAccount{},
   )
}

实际上module.go中的函数大部分是为了调用 client/cli中的函数,比如广播交易(broadcast.go)、编码/解码交易(encode.godecode.go)、发送交易(tx.go)、查询交易(query.go)、交易签名(tx_multisign.gotx_sign.go)、查询签名验证者(validate_sigs.go)。

细看就能发现,其实auth还提供了一个wrapper的功能。定位到代码:auth/client/rest.go

func RegisterTxRoutes(cliCtx context.CLIContext, r *mux.Router) {
   r.HandleFunc("/txs/{hash}", QueryTxRequestHandlerFn(cliCtx)).Methods("GET")
   r.HandleFunc("/txs", QueryTxsRequestHandlerFn(cliCtx)).Methods("GET")
   r.HandleFunc("/txs", BroadcastTxRequest(cliCtx)).Methods("POST")
   r.HandleFunc("/txs/encode", EncodeTxRequestHandlerFn(cliCtx)).Methods("POST")
   r.HandleFunc("/txs/decode", DecodeTxRequestHandlerFn(cliCtx)).Methods("POST")
}
// 拿 BroadcastTxRequest 举例,发送交易时,首先请求 'txs' 这个 API,这个 API 会调用 client/broadcast.go.BroadcastTx(),这个函数根据不同的广播类型(Sync、Async、Commit),调用不同的广播函数,然后再调用 tendermint 的广播函数 tendermint/rpc/client/mock/client.go.BroadcastTxCommit(),然后这个函数会调用 cosmos 中的检查、验证交易的函数 cosmos-sdk/baseapp/abci.go.CheckTx()
再来看 types/account.go账户文件

Account Interface

为了将帐户写入存储,需要使用帐户管理员——keeper/account.goAccountKeeper,它实现了bank/types/expected_keepers.go中的AccountKeeper。管理员账户只有一个,且是公开的。

type AccountI interface {
	GetAddress() sdk.AccAddress
	SetAddress(sdk.AccAddress) error // errors if already set.

	GetPubKey() crypto.PubKey // can return nil.
	SetPubKey(crypto.PubKey) error

	GetAccountNumber() uint64
	SetAccountNumber(uint64) error

	GetSequence() uint64
	SetSequence(uint64) error

	// Ensure that account implements stringer
	String() string
}

AccountKeeper Interface

type AccountKeeper interface {
   // 返回一个带有下一个帐号的新帐号。没有将新帐户保存到存储区。
   NewAccount(sdk.Context, types.AccountI) types.AccountI
   // 返回一个带有下一个帐号和指定地址的新帐号。没有将新帐户保存到存储区。
   NewAccountWithAddress(ctx sdk.Context, addr sdk.AccAddress) types.AccountI

   // 根据给定的地址从存储区查询账号
   GetAccount(ctx sdk.Context, addr sdk.AccAddress) types.AccountI
   GetAllAccounts(ctx sdk.Context) []types.AccountI
   SetAccount(ctx sdk.Context, acc types.AccountI)

   // 遍历所有帐户,调用所提供的函数。当迭代返回false时停止迭代。
   IterateAccounts(ctx sdk.Context, process func(types.AccountI) bool)

   ValidatePermissions(macc types.ModuleAccountI) error

   GetModuleAddress(moduleName string) sdk.AccAddress
   GetModuleAddressAndPermissions(moduleName string) (addr sdk.AccAddress, permissions []string)
   GetModuleAccountAndPermissions(ctx sdk.Context, moduleName string) (types.ModuleAccountI, []string)
   GetModuleAccount(ctx sdk.Context, moduleName string) types.ModuleAccountI
   SetModuleAccount(ctx sdk.Context, macc types.ModuleAccountI)
}

看到这里,我就来说说怎么创建模块账户的?看图说话。
在这里插入图9描述

Base Account

基本帐户是最简单和最常见的帐户类型,它直接在结构中存储所有必需的字段。Account Interface用来管理账户类型的字段,AccountKeeper Interface用来管理账号

type BaseAccount struct {
   Address       github_com_cosmos_cosmos_sdk_types.AccAddress 
   PubKey        []byte   // PubKey遵循在 tendermint的crypto包中定义的Pubkey接口
    					// Pubkey并非以其原始格式进行操作。它使用[Amino]和[bech32]进行2次编码.在SDK里面,`Pubkey`首先调用`Bytes()`方法(这里面提供amino编码),然后使用bech32的ConvertAndEncode 方法
   AccountNumber uint64   // 是在创建帐户时分配的唯一编号,用于踢出空帐户
    					// 如果没有设置全局的 account number,那他的初始化值就是0
   Sequence      uint64   
}
// `"account_number"` 和 `"sequence"` 字段可以直接从区块链或本地缓存中查询

实际上account包含SDK区块链唯一标识的外部用户的身份验证信息,包括用于重播保护的公钥、地址、帐号标记和序列号。为了提高效率,由于也必须提取账户余额来支付费用,因此账户结构也将用户的余额存储为sdk.Coinstypes/coin.go)。帐户在外部作为接口公开,在内部存储为基本帐户"base account"或授权帐户"vesting account"

来看ante/ante.go文件,此文件就定义了一个函数NewAnteHandler——返回一个AnteHandler,它检查和递增序列号,检查签名和帐号,并从第一个签名者扣除费用。顺序调用的 decorator。这里用到了责任链设计模式。
// AnteHandler在处理交易的内部消息之前对交易进行身份验证。if newCtx.IsZero(),则使用ctx。
func NewAnteHandler(
   ak AccountKeeper, bankKeeper types.BankKeeper, ibcKeeper ibckeeper.Keeper,
   sigGasConsumer SignatureVerificationGasConsumer,
) sdk.AnteHandler {
   return sdk.ChainAnteDecorators(
     // 在Context中设置GasMeter,并用一个defer子句包装下一个AnteHandler,以便从AnteHandler链中的任何后续OutOfGas恐慌中恢复,从而返回一个包含提供的gas和使用的gas信息的错误。
	// 两个约定:1.必须是链中的第一个decorator,2.Tx必须实现GasTx接口
      NewSetUpContextDecorator(), // outermost AnteDecorator. SetUpContext must be called first
       
    // 将检查交易的费用是否至少大于本地验证者的最低gasFee(在验证者配置中定义)。
    // 如果费用太低,decorator会返回错误,并且mempool会拒绝tx。
    // 注意,这仅用于当ctx.CheckTx = true时。
    // 如果费用足够高或没有CheckTx,就会调用下一个AnteHandler。
    // 约定:Tx必须实现了FeeTx,才能使用MempoolFeeDecorator
      NewMempoolFeeDecorator(),
       
    // 将调用tx.ValidateBasic(auth/types/stdtx.go)并返回任何非nil错误。
    // 注意,ValidateBasicDecorator的decorator不会在ReCheckTx上执行,因为它不依赖于应用程序状态。
      NewValidateBasicDecorator(),
       
    // 根据传入的参数对memo(用于存储的标志)进行验证,如果memo太大,decorator返回错误。
    // 约定:Tx必须实现TxWithMemo接口。
      NewValidateMemoDecorator(ak),
       
    // 检查参数并按照tx的大小比例消费gas。
    // 注意,由于任何给定的签名帐户都可能需要从state取回,所以gas成本将略微超出估计。
	// 两个约定:1.If simulate=true,签名必须填写完整或空,2.交易的签名必须表示为 types.StdSignature。否则simulate模块将会错误地估计gas成本
      NewConsumeGasForTxSizeDecorator(ak),
       
    // 在context中为任何尚未设置公钥的签名者(也就是validator)设置公钥,
	// 在运行任何其他sigverify装饰器之前,必须在context中为所有签名者设置pubkey。
      NewSetPubKeyDecorator(ak), // SetPubKeyDecorator must be called before all signature verification decorators
       
    // 检查参数,如果给定参数的tx中有太多签名,则返回错误,使用此decorator来设置tx中签名数量的参数化限制。
    // 约定:Tx必须实现SigVerifiableTx接口
      NewValidateSigCountDecorator(ak),
       
    // 扣除第一个交易签名者的费用,如果第一个签名者没有资金支付费用,返回资金不足错误。
	// 约定:Tx必须实现FeeTx接口
    // 会调用 bankKeeper.SendCoinsFromAccountToModule :进行扣费,SendCoinsFromAccountToModule会调用GetModuleAccount,若模块账户不存在,就会panic。给账户进行扣费时,并不会比较余额是否足够,只会校验扣费之后的余额是否非法
      NewDeductFeeDecorator(ak, bankKeeper),
       
    // 为每个签名消耗参数定义的gas
	// 两个约定:1.在这个decorator运行之前,在context中为所有签名者设置pubkey,2.Tx必须实现SigVerifiableTx接口
      NewSigGasConsumeDecorator(ak, sigGasConsumer),
       
    // 验证tx的所有签名,如果有无效的则返回一个错误。注意,重新检查时,不会执行SigVerificationDecorator的decorator。
	// 两个约定:1.在这个decorator运行之前,在context中为所有签署人设置pubkey,2.Tx必须实现SigVerifiableTx接口
      NewSigVerificationDecorator(ak),
       
    // 处理所有签名者的递增序列。使用IncrementSequenceDecorator decorator防止重播攻击。
	// 注意,没有必要在RecheckTX上执行IncrementSequenceDecorator,因为CheckTx已经增加序列号。由于CheckTx和DeliverTx状态是分开管理的,因此,除非客户手动管理和跟踪序列号,否则无法以可靠的方式正确处理来自同一帐户的后续和连续的txs组织。建议在tx中使用多个消息。
      NewIncrementSequenceDecorator(ak),
       
     // 处理包含应用程序特定数据包类型的消息,包括MsgPacket、MsgAcknowledgement、MsgTimeout。MsgUpdateClients也在这里被用来执行原子的multimsg交易。
      ibcante.NewProofVerificationDecorator(ibcKeeper.ClientKeeper, ibcKeeper.ChannelKeeper), // innermost AnteDecorator
   )
}

实际上auth模块目前没有自己的交易处理程序,但是公开了特殊的AnteHandler,用于对交易执行基本的有效性检查,这样就可以将其抛出内存池。注意,ante处理程序在CheckTx上调用,但在DeliverTx上也调用,因为Tendermint提议者目前有能力在他们提议的失败的块交易中包含这些交易。

讲完了auth的路由、账户、验证器,再来看看auth中的公开类型

除了账户(特别是在state中指定的)外,auth公开的类型还有StdFeeauth/types/stdtx.go)、StdSignatureStdTxStdSignDoc

StdFeeAmount(支付任意数量的费用)和Gas(gas的最高限额)(用Amount除以Gas得到“gas price”)的组合。fees = gas * gas-prices。每当对储存进行读写操作的时候会消耗gasgasTx执行之前无法被精确计算出来,只能进行估算。验证者通过将给定的或计算出的gas-prices与他们本地的min-gas-prices进行比较,来决定是否在其区块中写入该Tx。如果gas-prices不够高,该Tx将被拒绝,因此鼓励用户支付更多fee。

type StdFee struct {
  Amount sdk.Coins
  Gas    uint64
}

StdSignature是可选公钥和作为字节数组的加密签名的组合。SDK不知道特定的密钥或签名格式,但支持PubKey接口支持的任何格式。

type StdSignature struct {
  PubKey    []byte
  Signature []byte
}

StdTx是实现sdk.Tx接口,并可能足够通用,以服务于许多Cosmos SDK的区块链。

type StdTx struct {
  Msgs        []sdk.Msg
  Fee         StdFee  
  Signatures  []StdSignature
  Memo        string
}

StdSignDoc是一种要进行签名的防止重播结构,它确保任何提交的交易(只是对特定字节字符串的签名)在特定的区块链上只能执行一次。

type StdSignDoc struct {
  AccountNumber uint64			// 这笔交易的来源
  ChainID       string
  Fee           json.RawMessage // json.RawMessage是为了兼容性
  Memo          string
  Msgs          []json.RawMessage
  Sequence      uint64		  // 用于防止重播攻击的用户已发出的交易数
}
再来看看被Cosmos Hub使用的归属账户——vesting account
VestingAccount interface

VestingAccount定义了一种账户类型,它通过一个归属时间表(开始时间ST,结束时间ET,余额x)对币进行归属。需要使用归属帐户类型的应用程序必须通过RegisterCodec类型注册新的Vesting包。

type VestingAccount interface {
   types.AccountI

   // 返回一组不可使用的币(即锁定的)
   // 要获得可用于支付的硬币,首先必须提取总余额,并从总余额中减去锁定的币。
   // 注意,可支出余额可以是负数。
   LockedCoins(blockTime time.Time) sdk.Coins

   // 当从归属账户进行授权时,执行内部归属账单。
   // 当前块时间、委托金额和所有币的余额作为参数,这些币的面值存在于账户的原始归属余额中。
   // 通过为授权的数量、自由授权的数量设置适当的值来跟踪所需的授权数量,并减少基础币的总数量。
   TrackDelegation(blockTime time.Time, balance, amount sdk.Coins)

   // 当归属帐户执行解除委托时,执行必要的内部归属账单
   // 通过设置授权授予所需的值来跟踪未授权的数量、委派的授权需要减少多少、以及基础硬币需要增加多少。
   // 注意:未委托(债券退款)的金额可能会超过委托的授权(债券)金额,这是由于未委托截断债券退款的方式,如果未委托的令牌是非完整的,这可能会略微增加验证者的汇率(令牌/股份)。
   // 约定:必须对帐户的币和取消委托的币进行排序。
   TrackUndelegation(amount sdk.Coins)

   GetVestedCoins(blockTime time.Time) sdk.Coins
   GetVestingCoins(blockTime time.Time) sdk.Coins

   GetStartTime() int64
   GetEndTime() int64

   GetOriginalVesting() sdk.Coins
   GetDelegatedFree() sdk.Coins
   GetDelegatedVesting() sdk.Coins
}

归属账户可以初始化一些归属和非归属的币,非归属币将立即转让。当前规范不允许在生成之后使用正常消息创建归属帐户。所有归属账户必须在创世中创建,或者作为手动网络升级的一部分。当前的规范只允许无条件的授权(例如,没有可能达到ET和币授予失败)。

看到这里,我就来总结一下auth模块的主要功能

先看一下auth包结构:
在这里插入图片描述

Ⅰ. client:为auth模块提供restcmd

Ⅱ. ante:对交易进行签名认证

III. keeper:账户管理员,实现对其他账户的读、写

IV. types:定义了公开的账户

V. vesting:实现了对某个模块的币的归属

总之auth就是为应用程序提供账户并对交易进行签名认证。再上个图,图中的顺序代表函数调用顺序,加深一下印象:
在这里插入图片描述

实际上,cosmos-SDK并没有实现创建一个普通账户,也就是说,需要我们自己去实现具体逻辑。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值