目录结构
cosmos 版本:v0.38.5-rc1。
先看看staking
模块的目录结构:
目录结构和其他模块基本是一致的,只不过少了 alias.go 这个文件,说明staking
模块并没有被其他模块所调用。那么staking
模块主要用来做什么呢?
rest&cmd
先看 client 目录,cli/tx.go、cli/query.go、rest/query.go、rest/tx.go这几个文件会明确告诉我们,staking
对外提供了那些功能,不过 cli/query.go、rest/query.go 这两文件是不同的方式(一个是命令,一个是RPC路由)实现了相同功能(查询验证人、委托人、历史信息、参数等)。
cli/tx.go
查询功能就不多做描述了,先看 cli/tx.go 文件。通过NewTxCmd
方法基本就知道 tx.go 实现的功能了。
// NewTxCmd returns a root CLI command handler for all x/staking transaction commands.
func NewTxCmd() *cobra.Command {
stakingTxCmd := &cobra.Command{
Use: types.ModuleName,
Short: "Staking transaction subcommands",
DisableFlagParsing: true,
SuggestionsMinimumDistance: 2,
RunE: client.ValidateCmd,
}
stakingTxCmd.AddCommand(
NewCreateValidatorCmd(), // 创建验证人,通过命令接收所需的参数,构造对象后广播出去,下同。
NewEditValidatorCmd(), // 编辑验证人
NewDelegateCmd(), // 创建委托
NewRedelegateCmd(), // 重新创建委托
NewUnbondCmd(), // 接触委托
)
return stakingTxCmd
}
根据提供的默认参数可以知道创建验证人需要的代价:
var (
defaultTokens = sdk.TokensFromConsensusPower(100) // 抵押币:100 *(10**6)
defaultAmount = defaultTokens.String() + sdk.DefaultBondDenom
defaultCommissionRate = "0.1" // 服务费率
defaultCommissionMaxRate = "0.2" // 最大服务费率
defaultCommissionMaxChangeRate = "0.01" // 最大可变服务费率
defaultMinSelfDelegation = "1" // 最小抵押币
)
既然是默认参数,就允许被修改,在实际运行过程中,按需修改。不过,在使用create-validator
命令创建验证者时,可以通过"commission-rate"、“commission-max-rate”、"commission-max-change-rate"等参数来修改,不必修改代码。如下图(下文出现的idd
命令都是cosmos自带的例子(simapp/simd/main.go),不过文件名在打包时改为了idd
):
rest/tx.go
这个文件主要实现了委托相关的功能:
func registerTxHandlers(clientCtx client.Context, r *mux.Router) {
// 创建委托,参数以json的形式传递,下同。
r.HandleFunc(
"/staking/delegators/{delegatorAddr}/delegations",
newPostDelegationsHandlerFn(clientCtx),
).Methods("POST")
// 解除委托
r.HandleFunc(
"/staking/delegators/{delegatorAddr}/unbonding_delegations",
newPostUnbondingDelegationsHandlerFn(clientCtx),
).Methods("POST")
// 重建委托
r.HandleFunc(
"/staking/delegators/{delegatorAddr}/redelegations",
newPostRedelegationsHandlerFn(clientCtx),
).Methods("POST")
}
这三个API仅仅只是封装了签名所需的参数,并没有实现广播。想要广播上链,就要使用私钥对返回的参数进行签名,并调用txs
API进行广播上链。这里提供使用命令来签名的例子,毕竟是工具自带的签名方法,不需要在下写代码了:
idd tx sign unsigned.json --from test --offline --chain-id chain_test --sequence 0 --account-number 7 > signedTx.json
# 待签名的字符串必须放在文件里:unsigned.json
# test: 预置公私钥的名字,也就是预置账号(初始化程序时,需要有创世交易生成验证人,预置账号参与签名)
# chain_test:初始化的网关名
# sequence:预置账号被使用的次数
# account-number: 预置账号的编号
# idd query bank test: 这个命令可以查 test 的 sequence 和 account-number
handler.go
cli/tx.go、rest/tx.go 实现的功能经广播后,最后都会走到这里来:
func NewHandler(k keeper.Keeper) sdk.Handler {
return func(ctx sdk.Context, msg sdk.Msg) (*sdk.Result, error) {
ctx = ctx.WithEventManager(sdk.NewEventManager())
// 请求参数中的msg.type决定了最终走哪个函数
// handle函数会调用其他模块(auth、bank)和本模块的keeper,实现对验证人、委托人的增改查
switch msg := msg.(type) {
case *types.MsgCreateValidator:
return handleMsgCreateValidator(ctx, msg, k)
case *types.MsgEditValidator:
return handleMsgEditValidator(ctx, msg, k)
case *types.MsgDelegate:
return handleMsgDelegate(ctx, msg, k)
case *types.MsgBeginRedelegate:
return handleMsgBeginRedelegate(ctx, msg, k)
case *types.MsgUndelegate:
return handleMsgUndelegate(ctx, msg, k)
default:
return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized %s message type: %T", types.ModuleName, msg)
}
}
}
staking
模块对外提供的功能到这里似乎将完了,但是还有几个文件还没有说。
abci.go
Begin-Block
每个abci begin块调用,历史信息(验证人和块头部信息)将根据HistoricalEntries参数被存储和删除:
func BeginBlocker(ctx sdk.Context, k keeper.Keeper) {
defer telemetry.ModuleMeasureSince(types.ModuleName, time.Now(), telemetry.MetricKeyBeginBlocker)
// 如果HistoricalEntries参数为0,则BeginBlock执行一个no-op操作。
// 否则,最新的历史信息将存储在key为 historicalInfoKey|height 中,而任何比(height - HistoricalEntries)早的历史信息将被删除。
// 在大多数情况下,这会导致每个块删除单个历史信息。但是,如果参数 HistoricalEntries 更改为较低的值,则存储中有多个条目被删除。
k.TrackHistoricalInfo(ctx)
}
End-Block
每个abci end块调用,执行指定的更改验证人集合、更新队列(解除验证人和委托)和重新委托的操作:
func EndBlocker(ctx sdk.Context, k keeper.Keeper) []abci.ValidatorUpdate {
defer telemetry.ModuleMeasureSince(types.ModuleName, time.Now(), telemetry.MetricKeyEndBlocker)
// 1. 更改验证人集合
// 在此过程中,每个块末尾运行的状态转换将更新staking模块的验证人集合。
// 作为此过程的一部分,任何更新的验证人也会返回到Tendermint,以包含在Tendermint验证人集合中,该验证人集合负责在共识层验证Tendermint消息。操作如下:
// a. 新的验证人将放置在 params.MaxValidators 的顶部,根据ValidatorsByPower的索引恢复使用
// b. 将前一个验证人与新的验证器人进行比较:
// 1. 丢失的验证人开始解除绑定,它们的 tokens 从 BondedPool 转移到 NotBondedPool ModuleAccount 模块账户
// 2. 新的验证人会立即绑定,它们的 tokens 会从 NotBondedPool 转移到 BondedPool ModuleAccount
// 在所有情况下,任何离开或进入绑定验证人集合的验证人或更改余额并停留在绑定验证人集合的验证人都会产生一条更新消息,该消息将被传递回Tendermint。
// 2.1. 解除验证人
// 当一个验证人被踢出绑定验证人集合时(通过惩罚,或者没有足够的绑定币),它开始解除绑定过程,
// 同时它的所有委托开始解除绑定(同时仍然被委托给这个验证人)。此时,验证人被称为非绑定验证人,在非绑定期结束后,它最终成为“非绑定验证人”。
// 验证人队列的每个块都将被检查是否有到期解除绑定的验证人(即完成时间<=当前时间)。
// 此时,任何没有保留任何委托的验证人都将从状态中删除。对于所有其他到期解除绑定验证人,仍然有剩余的委托,validator.Status 将从 sdk.Unbonding 切换到 sdk.Unbonded。
// 2.2 解除委托
// 完成所有到期的 UnbondingDelegations.Entries 的解除,通过以下步骤处理 UnbondingDelegations 队列:
// a. 将剩余的币转移到委托人的账户
// b. 从 UnbondingDelegation.Entries 删除到期的委托
// c. 如没有剩余的委托,就将 UnbondingDelegation 对象从 db 中删除
// 3. 重新委托
// 完成所有到期的 Redelegation.Entries 的解除,通过以下步骤处理 Redelegations 队列:
// a. 删除到期的委托从 Redelegation.Entries 中
// b. 如没有剩余的委托,就将 Redelegation 对象从 db 中删除
return k.BlockValidatorUpdates(ctx)
}
genesis.go
下面这四个函数就是genesis.go实现的主要功能:
// 将委托参数、验证人信息、委托人信息等生成json导出
ExportGenesis(ctx sdk.Context, keeper keeper.Keeper) *types.GenesisState
// 初始化验证人信息(此验证人是节点的第一个验证人)、委托信息、委托参数
InitGenesis(ctx sdk.Context, keeper keeper.Keeper, accountKeeper types.AccountKeeper,bankKeeper types.BankKeeper, data *types.GenesisState) (res []abci.ValidatorUpdate)
// 校验验证人信息、委托信息
ValidateGenesis(data *types.GenesisState) error
// 将最初的验证人集合写入json串中
WriteValidators(ctx sdk.Context, keeper keeper.Keeper) (vals []tmtypes.GenesisValidator)
last but not least
看到这,相信对staking
实现的功能有了一个直观的了解。
- 在打包块开始的时候,删除过期的验证人和块头部信息
- 在打包块结束的时候,更新验证人、委托人
- 在初始化节点时,创建第一个验证人并委托(第一个验证人是通过创世交易(idd gentx)生成,通过收集(idd collect-gentxs)写入创世文件,然后在节点启动时,通过 InitGenesis 方法创建)
- 在节点运行过程中,创建、委托新的验证人,更新旧验证人