Cosmos 普通交易手续费调

2023-10-23 14:54:19 浏览数 (1)

背景

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

重点关注的两件事情:

  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

代码语言: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 是动态的变动的,有三种方式:

  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

代码语言:javascript复制
// 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
代码语言:javascript复制
// 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

0 人点赞