背景

分析 cosmos 的交易手续费的实现细节,以了解其实现手续费模型用于实现参考。
在 cosmos 中,gas 用于跟踪执行期间的资源消耗。普通交易消耗的也是 gas
gas 通常在对存储进行读取写入时使用,但如果需要执行昂贵的计算,也可以使用。

重点关注的两件事情:

  1. 如果计算、校验,即交易做了哪些操作,是否合法
  2. 每个操作的收费是如何定价的,包括:读取、存储、计算。

tx 会产生所有状态读取/写入、签名验证以及与 tx 大小成比例的成本的 gas 成本。运营商在启动节点时会设定最低 gas 价格。

需要消耗 gas的交易类型

每个交易在执行过程中都会消耗一定数量的Gas,该Gas用于跟踪执行过程中的资源消耗。
在Cosmos SDK应用程序中,交易可以是发送消息(Message)的操作,例如

  1. 发送代币
  2. 执行智能合约

当执行这些消息时,相关的Gas会被消耗,并且可能会生成相应的费用(Fees)。

请注意,Gas的消耗和费用的生成通常由应用程序开发者定义和管理,可以根据具体的应用逻辑和需求进行设置。

Cosmos SDK提供了Gas计量器(GasMeter)(主要就是通过个是来记录gas消耗)和相关的方法来追踪Gas的消耗和管理费用的生成。开发者可以在交易的执行逻辑中使用Gas计量器来测量Gas的消耗,并根据消耗的Gas数量来计算相应的费用。

因此,Gas的消耗和费用的生成是与交易(Transaction)密切相关的,并由应用程序开发者根据具体需求进行定义和管理。

交易收费

收费公式:fees = gas * gas-prices,交易费用按共识计算的确切gas价格收取。

收费有两个主要目的:

  1. 确保块不会消耗太多资源
  2. 防止用户发起垃圾交易

普通交易的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

Text
1
2
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

Text
1
2
keyGas = ReadCostPerByte * len(key)
valueGas = ReadCostPerByte * len(value)

4.gas price

gas price 是动态的变动的,有三种方式:

  1. 提案进行修改,很少情况会通过这种方式修改
  2. 前一个区块负载进行调整
  3. 前一个区块负载以更高的速度进行调整

实现部分分析

gas 的消耗有两个功能跟踪:

  1. Main Gas Meter 主gas表
    作用:用于跟踪每一笔交易的执行消耗。
  2. Block Gas Meter
    作用:用于跟踪每一个区块的gas消耗。

Cosmos 通过抽像 Meter 数据结构,对gas的消耗进行跟踪。

1.Main Gas Meter 交易gas跟踪

作用:用于跟踪每一笔交易的执行消耗。

在 Cosmos SDK 中,gas是简单的别名,由名为GasMeter 结构的一个字段uint64

1
2
3
4
5
6
7
8
9
10
11
12
// 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-ethereum StateDB接口。
  • IsPastLimit() 如果gas meter实例消耗的 gas 量严格高于限制, false 则返回 true
  • IsOutOfGas() 如果燃气表实例消耗的 gas 量高于或等于限制, false 则返回,否则返回 true

2.读/写 操作的gas消耗跟踪

Cosmos 中对读 和 写的操作,记录到 gasMeter 中,先操作后,再进行记录,每一笔交易的gas 都有上限,实现逻辑如下

  1. 进行数据库读写
  2. 计算所需要的gas值
  3. 注意 gs.gasConfig.ReadCostPerByte 是一个常量值,见上文
  4. keyvalue 都需要计算 gas
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
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