背景
分析 cosmos 的交易手续费的实现细节,以了解其实现方式用于TRON的手续费模型的实现参考。
在 cosmos 中,gas
用于跟踪执行期间的资源消耗。普通交易消耗的也是 gas
。
gas
通常在对存储进行读取和写入时使用,但如果需要执行昂贵的计算,也可以使用。
重点关注的两件事情:
- 如果计算、校验,即交易做了哪些操作,是否合法
- 每个操作的收费是如何定价的,包括:读取、存储、计算。
tx 会产生所有状态读取/写入、签名验证以及与 tx 大小成比例的成本的 gas 成本。运营商在启动节点时会设定最低 gas 价格。
需要消耗 gas的交易类型
每个交易在执行过程中都会消耗一定数量的Gas,该Gas用于跟踪执行过程中的资源消耗。 在Cosmos SDK应用程序中,交易可以是发送消息(Message)的操作,例如
- 发送代币
- 执行智能合约
当执行这些消息时,相关的Gas会被消耗,并且可能会生成相应的费用(Fees)。
请注意,Gas的消耗和费用的生成通常由应用程序开发者定义和管理,可以根据具体的应用逻辑和需求进行设置。
Cosmos SDK提供了Gas计量器(GasMeter)(主要就是通过个是来记录gas消耗)和相关的方法来追踪Gas的消耗和管理费用的生成。开发者可以在交易的执行逻辑中使用Gas计量器来测量Gas的消耗,并根据消耗的Gas数量来计算相应的费用。
因此,Gas的消耗和费用的生成是与交易(Transaction)密切相关的,并由应用程序开发者根据具体需求进行定义和管理。
交易收费
收费公式:fees = gas * gas-prices
,交易费用按共识计算的确切gas价格收取。
收费有两个主要目的:
- 确保块不会消耗太多资源
- 防止用户发起垃圾交易
普通交易的gas是如何计算的
通过对交易的长度进行计算,最终确认这笔交易所需要gas。而当发送到节点的交易低于全节点本地设置的 min-gas-prices
,交易将直接被丢弃,这可确保 mempool 不会被垃圾交易塞满。
对于数据读、写的操作,可以通过根据需要设置每个gas的消耗,以下是Cosmos官方的默认设定:
操作 | 作用 | gas |
---|---|---|
HasCost | 检查是否存在kay的 Gas 消耗 | 1000 |
DeleteCost | 删除kay的 Gas 消耗 | 1000 |
ReadCostFlat | 读取操作的固定 Gas 消耗 | 1000 |
ReadCostPerByte | 每字节读取操作的额外 Gas 消耗 | 3 |
WriteCostFlat | 写入操作的固定 Gas 消耗 | 2000 |
WriteCostPerByte | 每字节写入操作的额外 Gas 消耗 | 30 |
IterNextCostFlat | 迭代器的下一个操作的固定 Gas 消耗 | 30 |
1.写入收费
对数据写入的gas消耗需要计算 key 和 value 的大小,如下:
总消耗 = keyGas valueGas
代码语言:javascript复制key = WriteCostPerByte * len(key)
value = WriteCostPerByte * len(value)
2.签名收费
普通交易按照签名后的字节长度进行计费,每笔交易的gas有上限。
计算公式:
总消耗
= 原始交易byte大小
签名数据大小
* 每个字节的 Gas 消耗值
ConsumeGas
= byte
TxSizeCostPerByte
* cost
params.TxSizeCostPerByte
就是用来定义每个字节的额外 Gas 消耗值。通过将交易的大小乘以该值,可以得到交易大小对应的额外 Gas 消耗。
3.读取收费
对数据读取的gas消耗需要计算 key 和 value 的大小,如下:
总消耗 = keyGas valueGas
代码语言:javascript复制keyGas = ReadCostPerByte * len(key)
valueGas = ReadCostPerByte * len(value)
4.gas price
gas price 是动态的变动的,有三种方式:
- 提案进行修改,很少情况会通过这种方式修改
- 前一个区块负载进行调整
- 前一个区块负载以更高的速度进行调整
实现部分分析
gas 的消耗有两个功能跟踪:
- Main Gas Meter 主gas表 作用:用于跟踪每一笔交易的执行消耗。
- Block Gas Meter 作用:用于跟踪每一个区块的gas消耗。
Cosmos 通过抽像 Meter 数据结构,对gas的消耗进行跟踪。
1.Main Gas Meter 交易gas跟踪
作用:用于跟踪每一笔交易的执行消耗。
在 Cosmos SDK 中,gas
是简单的别名,由名为GasMeter
结构的一个字段uint64
// GasMeter interface to track gas consumption
type GasMeter interface {
GasConsumed() Gas
GasConsumedToLimit() Gas
GasRemaining() Gas
Limit() Gas
ConsumeGas(amount Gas, descriptor string)
RefundGas(amount Gas, descriptor string)
IsPastLimit() bool
IsOutOfGas() bool
String() string
}
GasConsumed()
返回 gas meter实例消耗的gas量。GasConsumedToLimit()
返回 gas meter 实例消耗的gas量或达到限制(如果达到限制)。GasRemaining()
返回 gas mete 中剩余的gas。Limit()
返回gas meter实例的限制。0
如果燃气表是无限大的。ConsumeGas(amount Gas, descriptor string)
消耗提供的数量gas
。 如果溢出,gas
它会对descriptor
消息感到恐慌(panics)。 如果燃气表不是无限的,消耗超过限制,它会gas
恐慌(panics)。RefundGas()
从消耗的gas中扣除给定的量。此功能可以将gas退还到交易或区块 gas 池,以便EVM兼容链可以完全支持go-ethereumStateDB
接口。IsPastLimit()
如果gas meter实例消耗的 gas 量严格高于限制,false
则返回true
。IsOutOfGas()
如果燃气表实例消耗的 gas 量高于或等于限制,false
则返回,否则返回true
。
2.读/写 操作的gas消耗跟踪
Cosmos 中对读 和 写的操作,记录到 gasMeter 中,先操作后,再进行记录,每一笔交易的gas 都有上限,实现逻辑如下
- 进行数据库读写
- 计算所需要的gas值
- 注意
gs.gasConfig.ReadCostPerByte
是一个常量值,见上文 key
和value
都需要计算 gas
// Implements KVStore.
func (gs *Store) Get(key []byte) (value []byte) {
gs.gasMeter.ConsumeGas(gs.gasConfig.ReadCostFlat, types.GasReadCostFlatDesc)
// parent 是 types.KVStore,即数据库接口
value = gs.parent.Get(key)
// TODO overflow-safe math?
// 对读的操作,记录到 gasMeter 中
gs.gasMeter.ConsumeGas(gs.gasConfig.ReadCostPerByte*types.Gas(len(key)), types.GasReadPerByteDesc)
gs.gasMeter.ConsumeGas(gs.gasConfig.ReadCostPerByte*types.Gas(len(value)), types.GasReadPerByteDesc)
return value
}
// Implements KVStore.
func (gs *Store) Set(key, value []byte) {
types.AssertValidKey(key)
types.AssertValidValue(value)
gs.gasMeter.ConsumeGas(gs.gasConfig.WriteCostFlat, types.GasWriteCostFlatDesc)
// TODO overflow-safe math?
gs.gasMeter.ConsumeGas(gs.gasConfig.WriteCostPerByte*types.Gas(len(key)), types.GasWritePerByteDesc)
gs.gasMeter.ConsumeGas(gs.gasConfig.WriteCostPerByte*types.Gas(len(value)), types.GasWritePerByteDesc)
gs.parent.Set(key, value)
}
3.签名gas消耗
对于签名部分,也是需要计算gas的消耗,总消耗
= 原始交易byte大小
签名数据大小
* 每个字节的 Gas 消耗值
x/auth/ante/basic.go
代码语言:javascript复制func (cgts ConsumeTxSizeGasDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) {
sigTx, ok := tx.(authsigning.SigVerifiableTx)
if !ok {
return ctx, errorsmod.Wrap(sdkerrors.ErrTxDecode, "invalid tx type")
}
params := cgts.ak.GetParams(ctx)
// 计算交易长度
// ctx: transaction 交易上下文
// 注意,此处跟踪原始交易 byte 长度
ctx.GasMeter().ConsumeGas(params.TxSizeCostPerByte*storetypes.Gas(len(ctx.TxBytes())), "txSize")
// simulate gas cost for signatures in simulate mode
// 在模拟模式下模拟签名的gas成本
if simulate {
// in simulate mode, each element should be a nil signature
// 在模拟模式下,每个元素都应是 nil 签名
sigs, err := sigTx.GetSignaturesV2()
if err != nil {
return ctx, err
} n := len(sigs) signers, err := sigTx.GetSigners() if err != nil { return sdk.Context{}, err }
for i, signer := range signers {
// if signature is already filled in, no need to simulate gas cost
// 如果签名已填写,则无需模拟gas成本
if i < n && !isIncompleteSignature(sigs[i].Data) {
continue
}
var pubkey cryptotypes.PubKey
acc := cgts.ak.GetAccount(ctx, signer)
// use placeholder simSecp256k1Pubkey
if sig is nil if acc == nil || acc.GetPubKey() == nil {
pubkey = simSecp256k1Pubkey
} else {
pubkey = acc.GetPubKey()
}
// use stdsignature to mock the size of a full signature
// 使用 stdsignature 模拟完整签名的大小
simSig := legacytx.StdSignature{ //nolint:staticcheck // SA1019: legacytx.StdSignature is deprecated
Signature: simSecp256k1Sig[:],
PubKey: pubkey,
}
sigBz := legacy.Cdc.MustMarshal(simSig)
// cost 为签名长度
cost := storetypes.Gas(len(sigBz) 6)
// If the pubkey is a multi-signature pubkey, then we estimate for the maximum
// number of signers.
// 如果公开密钥是多签名公开密钥,那么我们将估计最大的签名者数量。
if _, ok := pubkey.(*multisig.LegacyAminoPubKey); ok {
cost *= params.TxSigLimit
}
// 此处记录 签名后的 gas 消耗
ctx.GasMeter().ConsumeGas(params.TxSizeCostPerByte*cost, "txSize")
}
}
return next(ctx, tx, simulate)
}
总结
Cosmos 对普通交易的处理,基于对交易长度 * 预设gas 的方式进行计算,其中的实现方式以抽出 Meter 记录表的方式,在每一步关键操作位置计算并记录gas消息,可以考虑借鉴Cosmos。
参考链接
transaction 生命周期:Transaction Lifecycle | Cosmos SDK gas fee介绍:Gas and Fees | Cosmos SDK Gas & Fees:x/auth | Cosmos SDK GasKVStore:Store | Cosmos SDK