摘要
本文主要分析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.go
(simapp/simcli/main.go
)中被registerRoutes()
调用。直接上图,下图顺序指的是函数调用顺序。
GetTxCmd()
和GetQueryCmd()
分别实现auth
的交易命令和查询命令,都是以cmd
方式使用的。GetTxCmd()
添加了三个功能命令:多个交易签名(GetMultiSignCommand
)、单个交易签名(GetSignCommand
)、查询签名的验证者(GetValidateSignaturesCommand
),这三个功能命令都是从给定的文件中读取数据。GetQueryCmd()
添加了两个功能命令:查询账户(GetAccountCmd
)、查询证据参数(QueryParamsCmd
)。这样说,可能他们之间的相互关系还不是很明确,上图,就不贴源码了。
来看RegisterInterfaceTypes
函数,此函数将protoName
与AccountI
接口关联,并创建其具体实现的注册表。
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.go
,decode.go
)、发送交易(tx.go
)、查询交易(query.go
)、交易签名(tx_multisign.go
、tx_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.go
的AccountKeeper
,它实现了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)
}
看到这里,我就来说说怎么创建模块账户的?看图说话。
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.Coins
(types/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
公开的类型还有StdFee
(auth/types/stdtx.go
)、StdSignature
、StdTx
、StdSignDoc
StdFee
是Amount
(支付任意数量的费用)和Gas
(gas的最高限额)(用Amount除以Gas
得到“gas price
”)的组合。fees = gas * gas-prices。每当对储存进行读写操作的时候会消耗gas
,gas
在Tx
执行之前无法被精确计算出来,只能进行估算。验证者通过将给定的或计算出的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
模块提供rest
和cmd
Ⅱ. ante
:对交易进行签名认证
III. keeper
:账户管理员,实现对其他账户的读、写
IV. types
:定义了公开的账户
V. vesting
:实现了对某个模块的币的归属
总之auth
就是为应用程序提供账户并对交易进行签名认证。再上个图,图中的顺序代表函数调用顺序,加深一下印象:
实际上,cosmos-SDK
并没有实现创建一个普通账户,也就是说,需要我们自己去实现具体逻辑。