Cosmos gas计费规则详解

Cosmos gas收取规则

Cosmos 手续费组成

type StdFee struct {
	Amount sdk.Coins `json:"amount"`
	Gas    uint64    `json:"gas"`
}

由上述结构可以看出,手续费由两部分组成:

  • Amount: 提供的金额
  • Gas: 上述金额所关联的gas数量

gasPrice = Amount/Gas;

当gasPrice 符合validator 交易池的规则时(即大于等于限定的最小gasPrice),允许该交易进入交易池。

  • Note:本篇文章只讨论gas对交易有效性的影响;其余交易规则概不涉及

当交易在执行时,会使用交易费中提供的 Gas字段作为limit,每个交易累计所有动作消耗的gas数量,当累计消耗的gas数量超过 limit时,交易执行失败。

gas消耗表

在Cosmos sdk中,硬编码了每种操作所需要消耗的gas数量.

func KVGasConfig() GasConfig {
	return GasConfig{
		HasCost:          1000,		// 查询操做,消耗的gas数量
		DeleteCost:       1000,		// 删除操作,消耗的gas数量
		ReadCostFlat:     1000,		// 读取操作固定收取的费用
		ReadCostPerByte:  3,		// 每读取1byte,消耗的gas数量
		WriteCostFlat:    2000,		// 写入操作固定收取的费用
		WriteCostPerByte: 30,		// 每写入1byte, 消耗的gas数量
		IterNextCostFlat: 30,		// 每个迭代操作,消耗的gas数量
	}
}

对数据库操作的封装

gaskv.store 使用了装饰器模式,在原始的KVStore的基础上提供了gas消耗的统计操作。

type Store struct {
	gasMeter  types.GasMeter		// gas计数器
	gasConfig types.GasConfig		// gas消耗表,每个操作对应的gas消耗
	parent    types.KVStore			// 实际的底层存储
}

在对实际的底层存储进行操作前,先 依据操作的类型,在gasConfig中找出对应操作需要消耗的gas数量,使用gasMeter 进行gas计数:当提供的gas消耗完,或者超过Uint64的最大值时,程序Panic;或者提供交易提供的gas可以满足消耗.

下面是gaskv.store提供的几种功能,以及gas消耗:

  • Get() 操作:从数据库中读取一个值

    • 收取两种gas费用: 1. 收取固定的读取费用 ReadCostFlat; 2. 依据读取的数据量,每个字节收取固定的费用ReadCostPerByte.
  • Set() 操作:向数据库中写入一个值

    • 收取两种gas费用: 1. 收取固定的写入费用 WriteCostFlat; 2. 依据写入的value的字节数量,每个字节收取固定的费用WriteCostPerByte.
  • Has() 操作:判断一个值是否存在于数据库

    • 收取固定的gas费用: HasCost
  • Delete() 操作:从数据库中删除一个值

    • 收取固定的gas费用: DeleteCost
  • Iterator() 迭代操作: 对数据库本身提供的迭代器使用了装饰器模式进行了封装gasIterator

    • consumeSeekGas : 每次迭代时,会使用该方法,在当前的数据有效的情况下,收取两种gas费:1. 收取固定费用IterNextCostFlat; 2. 依据当前数据值,每个字节收取固定的费用ReadCostPerByte
    type gasIterator struct {
    	gasMeter  types.GasMeter
    	gasConfig types.GasConfig
    	parent    types.Iterator
    }
    

    当一个有效的迭代器创建后,会使用consumeSeekGas收取一笔手续费.

gas计数器

Cosmos sdk 提供了一个接口GasMeter,定义了gas计数器的所提供的所有功能

type GasMeter interface {
	GasConsumed() Gas                           // 已消耗的gas数量
	GasConsumedToLimit() Gas                    // 返回两值中的最小值;gasLimit,gas累计消耗。
	Limit() Gas                                 // 提供的gas数量
	ConsumeGas(amount Gas, descriptor string)   // 对gas的消耗进行累计;如果超过uint64,或者limit时,直接Panic
	IsPastLimit() bool                          // gas消耗是否超过limit
	IsOutOfGas() bool                           // 提供的gas是否已消耗完毕
}

分别是:gas消耗的统计,交易提供的gas 数量,累计交易每个操作的gas消耗,以及gas是否完全被消耗等判断.

sdk 提供了两种计数器的实现:

  • basicGasMeter: 提供了真实的gas消耗统计
  • infiniteGasMeter: 提供了无限可供消耗的gas(为uint64 最大值),可以在交易模拟执行中进行使用。

basicGasMeter

type basicGasMeter struct {
	limit    Gas
	consumed Gas
}

依据交易提供的gas数量创建该结构,将交易提供的gas赋值给limit, 使用consumed统计每个操作累计消耗的gas数量.

分别提供了如下几个方法:

  • GasConsumed: 交易当前累计消耗的gas数量
  • Limit: 交易提供的gas 数量
  • GasConsumedToLimit: 获取两个值中最小的一个,返回。
  • ConsumeGas(amount, descriptor): 提供了两个参数:当前操作需要消耗的gas数量,当前操作的描述;
    • 但累计的consumed消耗超过提供的limit或者超过uint64时,panic报错。
  • IsPastLimit: 判断gas消耗是否超出提供,即:consumed > limit
  • IsOutOfGas: 判断提供的gas是否被全部消耗,即:consumed >= limit

infiniteGasMeter

type infiniteGasMeter struct {
	consumed Gas
}

由上述结构可以看出,它只统计累计的gas消耗了;默认提供了几乎无限的gas(uint64最大值);

Cosmos 中对这两种实现的使用如下:

func SetGasMeter(simulate bool, ctx sdk.Context, gasLimit uint64) sdk.Context {
	// In various cases such as simulation and during the genesis block, we do not
	// meter any gas utilization.
	if simulate || ctx.BlockHeight() == 0 {
		return ctx.WithGasMeter(sdk.NewInfiniteGasMeter())
	}

	return ctx.WithGasMeter(sdk.NewGasMeter(gasLimit))
}

如果交易为模拟执行或者创世块,则提供无限gas供应模式; 否则提供正常的gas计费模式.

本文由 CoinEx Chain团队 路之遥 写作,转载无需授权