为了避免网络滥用及回避由于图灵完备而带来的一些不可避免的问题(the halting problem),在以太坊中所有的程序执行都收费。Gas是基本的工作量成本单位,用于计量在以太坊区块链上执行操作所需的计算、存储资源和带宽,其目的是限制执行交易所需的工作量。各种操作的费用以gas为单位计算。任意的程序片段(包括合约创建、消息调用、分配资源以及访问账户storage、在虚拟机上执行操作等)都有一个普遍认同的gas成本。[1] Gas有两个作用[5]:
每一个交易都要指定一个 gas 上限:gasLimit。发送者通过在交易中指定gas price来购买gas,系统预先从发送者的账户余额中扣除gasLimit * gasPrice的交易费,即采用预付费机制。Gas price是指当你将交易发送到以太坊网络时,愿意支付的每单位gas的价格。[5]如果账户余额不足,交易会被视为无效交易。[1]之所以将其命名为 gasLimit,是因为剩余的 gas会在交易完成后被返还(与购买时同样价格)到发送者账户。每个矿工自己选择他们想要接受和拒绝的gas价格。交易者们则需要在降低 gas 价格和使交易能尽快被矿工打包间进行权衡。
通常来说,以太币(Ether)是用来购买 gas 的,未返还的部分就会移交到 beneficiary 的地址(即一般由矿工所控制的一个账户地址)。以太币最小的单位是 Wei(伟),所有货币值都以 Wei 的整数倍来记录。[1]
注意:Gas只存在于EVM中,用来给计算的工作量计数。发送方用ether支付交易费,然后将其转换为gas用于EVM核算,最后将剩余的gas转换为ether返还给发送方,未返还的同样转换为ether作为交易费付给矿工[5]。
block gas limit是一个块中所有交易可以消耗的最大gas量,并且限制了一个块中可以容纳多少个交易。如果矿工试图包含一个需要比block gas limit更多gas的交易,则该块将被网络拒绝。[5]
以太坊采用投票系统来设定block gas limit。网络上的矿工共同决定block gas limit。以太坊协议有一个内置的机制,矿工可以对block gas limit进行投票,从而增加或减少后续区块的容量。矿工有权将当前区块的gas限定值设定在最后区块的gas限定值的0.0975% (1/1024)内[3]。所以最终的gas限定值应该是矿工们设置的中间值。
EVM可执行的各种操作的相对gas cost经过精心设计,以最好地保护以太坊区块链不受攻击。操作进行的计算越多,gas成本越高[5]。
2016年,一名攻击者发现并利用了gas成本与实际资源成本不匹配的问题,证明了将gas成本与实际资源成本相匹配的重要性。 这个问题通过一个硬分叉(代号为“橘子口哨”,EIP 150)解决,它通过改变IO重型操作长期的gas费率来抵抗垃圾交易攻击,并增加了63/64规则。
三种情况下会收取执行费用(以gas来结算)[1]:
gas消耗计算还有以下特点[3]:
对于任何交易,都先收取21000 gas的基本费用(base fee)。这些费用可用于支付运行椭圆曲线算法(该算法旨在从签名中恢复发送者的地址)以及存储交易所花费的硬盘空间和带宽所需的费用。
交易可以包括无限量的“数据”。虚拟机中的某些操作码,可以让合约允许交易对这些数据的访问。数据的固定费用(intrinsic gas)计算:每个零字节4 gas,非零字节68 gas。
合约提供的消息数据是没有成本的。因为在消息调用期间不需要实际复制任何数据,调用数据可以简单地视为指向父合约内存的指针,该指针在子进程执行时不会改变。
某些操作码的计算时间极度依赖参数,gas成本是动态变化的。例如,EXP的的开销是指数级别的(ie. x^0 = 1 gas, x^1 … x^255 = 2 gas, x^256 … x^65535 = 3 gas, etc)。
如果操作码CALL(以及CALLCODE)的值不是零,会额外消耗9000 gas。这是因为任何值传输都会引起归档节点的历史存储显著增大。请注意,实际消耗是6700,在此基础上,以太坊强制增加了一个自动给予接收方的gas值,这个值最小是2300。这样做是为了让接受交易的钱包至少有足够的gas来记录交易。
对于一个账户的执行,内存的总费用和其内存索引(无论是读还是写)的范围成正比;这个内存范围是32字节的倍数,不足32字节以32字节计。这是实时(just-in-time)结算的;也就是说,任何对超出先前已索引的内存区域的访问,都会实时地结算为额外的内存使用费。
存储费用则有一个细微差别——激励存储的最小化使用。清除一个存储中的记录项或账户不仅不收费,而且还会返还一定gas作为奖励。[1] EVM中有两种操作会出现这种情况,具体可以参考SSTORE操作码的gas计算函数(core/vm/gas_table.go/gasSStore)[5]:
为了避免退款机制被利用,每笔交易的最高退款额被设定为gas总用量的50%(向下取整)[5]。这种退款机制会激励人们清理存储器。正因为缺乏这样的激励,许多合约并未有效使用存储空间,从而导致存储快速膨胀。这样既获得了存储收费的大部分好处,又不会失去合约一旦确立就可以永久存在的保证。延迟退款机制是必要的,因为可以防止拒绝服务攻击。攻击者发送一笔含有少量gas的交易,循环清理大量的存储,直到用光gas,这样消耗了大量的验证算力,但实际并没有真正清理存储也没有花费大量gas。50%的上限是为了确保:给定一个具有一定数量gas的交易,矿工依然可以根据gasLimit确定用于执行此交易的计算时间上限。[3]
当EVM需要完成一个交易时,它首先被给予一个等于交易中gas limit所指定数量的gas supply。执行的每个操作码都有一个gas成本,因此EVM的gas supply会随着程序向前一步步执行而逐渐减少。在每个操作之前,EVM检查是否有足够的gas来支付操作的执行费用。以太坊在操作执行前收取费用。如果没有足够的gas,EVM就会停止执行并将本次交易修改的状态回滚。[5]
如果EVM成功完成了执行,并且没有耗尽gas,则使用的gas将作为交易费支付给矿工,并根据交易中指定的gas价格转换为ether,即交易费= gas used * gas price。gas supply中剩余的gas将退还给发送方,同样是根据交易中指定的gas价格转换为ether。
如果交易在执行期间“耗尽gas”,操作将立即终止,抛出“out of gas(OOG)”异常。交易被恢复,对状态的所有更改都回滚。
虽然交易没成功执行,但发送方仍需支付交易费,因为到那时为止,矿工已经执行了计算工作,必须为此进行补偿。[5] 收取的交易费为发送方提供的全部gas,即gas limit * gas price。当一个合约发送消息给另一个合约,可以对这个消息引起的子执行设置一个gas限制。如果子执行耗尽了gas,则子执行被恢复,但gas仍然消耗。[3]
gas成本定义和指令gas成本的计算代码集中在core/vm/gas.go
和core/vm/gas_table.go
两个文件。
core/vm/gas.go
// Gas costs
const (
GasQuickStep uint64 = 2
GasFastestStep uint64 = 3
GasFastStep uint64 = 5
GasMidStep uint64 = 8
GasSlowStep uint64 = 10
GasExtStep uint64 = 20
)
// calcGas returns the actual gas cost of the call.
// calcGas返回实际用于调用的gas成本。。
//
// The cost of gas was changed during the homestead price change HF.
// As part of EIP 150 (TangerineWhistle), the returned gas is gas - base * 63 / 64.
// 在homestead价格变动中,gas成本发生了变化。 HF ??
// 作为 EIP 150 (TangerineWhistle 橘子口哨硬分叉)的一部分,返回的gas = (gas - base) * 63 / 64.
//
// EIP150是通过重新调整gas价格彻底解决DoS问题的硬分叉主要备选方案。
// “针对IO重型操作长期的gas费率改变以抵抗垃圾交易攻击” https://github.com/ethereum/EIPs/blob/master/EIPS/eip-150.md
// 添加规则:一个调用的子调用不能消耗超过父调用剩余gas的63/64。也就是说,如果调用者最初投入了数量为 a 的 gas, 在 10 层递归调用后,最内层的函数最多只有 (63/64)^10*a 的 gas.
// 有两个目的:(Rationale 片段)
// 1. 用一个更软的基于gas的限制("softer" gas-based restriction)取代最大调用栈深度的“硬限制”,这将使得深度调用需要的gas数量呈指数增长。
// 这将堆栈深度限制攻击这一个类别的攻击彻底从合约开发者需要担心的问题清单中剔除,从而提高了合约编程的安全性。
// 2. 把事实上的最大堆栈调用深度从1024减少到约300,在一定程度上缓解未来客户端受二次Dos攻击的可能。
func callGas(isEip150 bool, availableGas, base uint64, callCost *big.Int) (uint64, error) {
if isEip150 {
availableGas = availableGas - base
gas := availableGas - availableGas/64 // gas = (availableGas - base) * 63 / 64
// If the bit length exceeds 64 bit we know that the newly calculated "gas" for EIP150
// is smaller than the requested amount. Therefor we return the new gas instead
// of returning an error.
// 如果位长超过64位,我们知道新计算的EIP150的“gas”要比请求的量小。
// 因此,我们返回新的gas,而不是返回一个错误。
// 若callCost超过64位,条件直接成立,不用比较即返回gas;
// 若callCost不超过64位,新计算的gas肯定小于CallCost,仍返回gas。
if !callCost.IsUint64() || gas < callCost.Uint64() {
return gas, nil
}
}
if !callCost.IsUint64() {
return 0, errGasUintOverflow
}
return callCost.Uint64(), nil
}
core/vm/gas_table.go
代码量大,是各个指令对应gas计算函数的实现。对 gas 的计算都需要考虑三个方面:解释指令本身 、使用内存存储 、使用StateDB 存储 需要的 gas 。代码较多,以下截取一些比较复杂的计算函数:
// memoryGasCost calculates the quadratic gas for memory expansion. It does so
// only for the memory region that is expanded, not the total memory.
// memoryGasCost计算用于内存扩展的二次gas。它只对扩展的内存区域执行此操作,而不是对整个内存。
func memoryGasCost(mem *Memory, newMemSize uint64) (uint64, error) {
if newMemSize == 0 {
// 新内存大小为0,gas直接返回0
return 0, nil
}
// The maximum that will fit in a uint64 is max_word_count - 1. Anything above
// that will result in an overflow. Additionally, a newMemSize which results in
// a newMemSizeWords larger than 0xFFFFFFFF will cause the square operation to
// overflow. The constant 0x1FFFFFFFE0 is the highest number that can be used
// without overflowing the gas calculation.
// uint64的最大值是max_word_count - 1。超过这个数字就会导致溢出。此外,
// newMemSize若导致newMemSizeWords大于0xFFFFFFFF,将引发平方操作溢出。
// 0x1fffffffffe0是不会导致gas计算溢出的最大数字。
if newMemSize > 0x1FFFFFFFE0 {
return 0, errGasUintOverflow
}
newMemSizeWords := toWordSize(newMemSize)
newMemSize = newMemSizeWords * 32 // 按整数个字word计费,向上取整
if newMemSize > uint64(mem.Len()) {
// 若新内存大小大于原有内存大小
square := newMemSizeWords * newMemSizeWords // 平方
linCoef := newMemSizeWords * params.MemoryGas // 线性系数
quadCoef := square / params.QuadCoeffDiv // 平方系数
newTotalFee := linCoef + quadCoef // 新的总花费
fee := newTotalFee - mem.lastGasCost // 新扩展内存的花费
mem.lastGasCost = newTotalFee // 目前为止内存的总花费,用于下次扩展内存进行计算
return fee, nil
}
return 0, nil // 新内存大小没有原有内存大小大,gas花费为0
}
func gasSStore(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
var (
y, x = stack.Back(1), stack.Back(0) // 从栈里取得操作数
// 得到x地址的当前值
current = evm.StateDB.GetState(contract.Address(), common.BigToHash(x))
)
// The legacy gas metering only takes into consideration the current state
// Legacy rules should be applied if we are in Petersburg (removal of EIP-1283)
// OR Constantinople is not active
// 传统的gas计量只考虑当前的状态
// 如果我们处在Petersburg(在EIP-1283中被移除)或者不是Constantinople版本的规则中,就应该使用传统规则
if evm.chainRules.IsPetersburg || !evm.chainRules.IsConstantinople {
// This checks for 3 scenario's and calculates gas accordingly:
// 这里分别为三种场景计算相应的gas:
//
// 1. From a zero-value address to a non-zero value (NEW VALUE)
// 2. From a non-zero value address to a zero-value address (DELETE)
// 3. From a non-zero to a non-zero (CHANGE)
// 1. 0->非0 (新值)
// 2. 非0->0 (删除)
// 3. 非0->非0 (更新)
switch {
case current == (common.Hash{
}) && y.Sign() != 0: // 0 => non 0
return params.SstoreSetGas, nil
case current != (common.Hash{
}) && y.Sign() == 0: // non 0 => 0
evm.StateDB.AddRefund(params.SstoreRefundGas) // 清理内存给奖励
return params.SstoreClearGas, nil
default: // non 0 => non 0 (or 0 => 0)
return params.SstoreResetGas, nil
}
}
// The new gas metering is based on net gas costs (EIP-1283):
// 新的Gas计量以gas净成本(EIP-1283)计算:
//
// 1. If current value equals new value (this is a no-op), 200 gas is deducted.
// 2. If current value does not equal new value
// 2.1. If original value equals current value (this storage slot has not been changed by the current execution context)
// 2.1.1. If original value is 0, 20000 gas is deducted.
// 2.1.2. Otherwise, 5000 gas is deducted. If new value is 0, add 15000 gas to refund counter.
// 2.2. If original value does not equal current value (this storage slot is dirty), 200 gas is deducted. Apply both of the following clauses.
// 2.2.1. If original value is not 0
// 2.2.1.1. If current value is 0 (also means that new value is not 0), remove 15000 gas from refund counter. We can prove that refund counter will never go below 0.
// 2.2.1.2. If new value is 0 (also means that current value is not 0), add 15000 gas to refund counter.
// 2.2.2. If original value equals new value (this storage slot is reset)
// 2.2.2.1. If original value is 0, add 19800 gas to refund counter.
// 2.2.2.2. Otherwise, add 4800 gas to refund counter.
// 1. 如果当前值等于新值(这是空操作),扣除200 gas。
// 2. 如果当前值不等于新值
// 2.1. 如果原值等于当前值(此存储槽还未被当前执行上下文更改)
// 2.1.1. 如果原值是0 ,则为创建,扣除20000gas
// 2.1.2. 否则,扣5000gas。但如果新值是0,清理了内存,奖励15000gas,暂存退款计数器。
// 2.2. 如果原值不等于当前值(这个存储槽“脏”), 扣除200gas。下列两项条款均适用。
// 2.2.1. 如果原值不是0
// 2.2.1.1. 如果当前是0(也意味着新值非0),从退款计数器减去15000gas。我们可以证明计数器的值不会低于0.
// 2.2.1.2. 如果新值是0(也意味着当前值非0),奖励15000gas到退款计数器。
// 2.2.2. 如果原值等于新值(此存储槽被重置)
// 2.2.2.1. 如果原值是0,增加19800gas到退款计数器。清理内存,并且是重置,奖励15000+4800gas。
// 2.2.2.2. 否则,增加4800gas到退款计数器。只是重置,奖励4800gas。
// 对于存储来说,存储0可看做未使用的存储。将存储归0给奖励,复位也给奖励。
value := common.BigToHash(y) // 新值hash
if current == value {
// noop (1)
return params.NetSstoreNoopGas, nil
}
// 原值hash
original := evm.StateDB.GetCommittedState(contract.Address(), common.BigToHash(x))
if original == current {
// 2.1
if original == (common.Hash{
}) {
// create slot (2.1.1)
return params.NetSstoreInitGas, nil
}
if value == (common.Hash{
}) {
// delete slot (2.1.2b)
evm.StateDB.AddRefund(params.NetSstoreClearRefund)
}
return params.NetSstoreCleanGas, nil // write existing slot (2.1.2)
}
if original != (common.Hash{
}) {
if current == (common.Hash{
}) {
// recreate slot (2.2.1.1) 重新使用了存储,扣除奖励的gas
evm.StateDB.SubRefund(params.NetSstoreClearRefund)
} else if value == (common.Hash{
}) {
// delete slot (2.2.1.2)
evm.StateDB.AddRefund(params.NetSstoreClearRefund)
}
}
if original == value {
if original == (common.Hash{
}) {
// reset to original inexistent slot (2.2.2.1) 复位为未使用的存储
evm.StateDB.AddRefund(params.NetSstoreResetClearRefund)
} else {
// reset to original existing slot (2.2.2.2) 复位为已使用的存储
evm.StateDB.AddRefund(params.NetSstoreResetRefund)
}
}
return params.NetSstoreDirtyGas, nil
}
// 0. If *gasleft* is less than or equal to 2300, fail the current call.
// 1. If current value equals new value (this is a no-op), SSTORE_NOOP_GAS gas is deducted.
// 2. If current value does not equal new value:
// 2.1. If original value equals current value (this storage slot has not been changed by the current execution context):
// 2.1.1. If original value is 0, SSTORE_INIT_GAS gas is deducted.
// 2.1.2. Otherwise, SSTORE_CLEAN_GAS gas is deducted. If new value is 0, add SSTORE_CLEAR_REFUND to refund counter.
// 2.2. If original value does not equal current value (this storage slot is dirty), SSTORE_DIRTY_GAS gas is deducted. Apply both of the following clauses:
// 2.2.1. If original value is not 0:
// 2.2.1.1. If current value is 0 (also means that new value is not 0), subtract SSTORE_CLEAR_REFUND gas from refund counter. We can prove that refund counter will never go below 0.
// 2.2.1.2. If new value is 0 (also means that current value is not 0), add SSTORE_CLEAR_REFUND gas to refund counter.
// 2.2.2. If original value equals new value (this storage slot is reset):
// 2.2.2.1. If original value is 0, add SSTORE_INIT_REFUND to refund counter.
// 2.2.2.2. Otherwise, add SSTORE_CLEAN_REFUND gas to refund counter.
// 0. 如果*gasleft*不大于2300,则不允许进行SSTORE操作。具体参考:https://github.com/ethereum/EIPs/pull/1706/files https://learnblockchain.cn/docs/eips/eip-1706.html
// EIP-1283显著降低了写入合约存储的gas成本.这就产生了可重入攻击现有合约的危机,因为Solidity会向简单的交易调用提供2300gas的“津贴”。
// 若在低gasleft状态下不允许SSTORE,这个危机很容易缓解。而且不破坏向后兼容性和这个EIP的原始意图。
// 1. 如果当前值等于新值(这是空操作),扣除SSTORE_NOOP_GAS gas。
// 2. 如果当前值不等于新值
// 2.1. 如果原值等于当前值(此存储槽还未被当前执行上下文更改)
// 2.1.1. 如果原值是0 ,则为创建,扣除SSTORE_INIT_GAS gas
// 2.1.2. 否则,扣SSTORE_CLEAN_GAS gas。但如果新值是0,清理了内存,奖励SSTORE_CLEAR_REFUND gas,暂存退款计数器。
// 2.2. 如果原值不等于当前值(这个存储槽“脏”), 扣除SSTORE_DIRTY_GAS gas。下列两项条款均适用。
// 2.2.1. 如果原值不是0
// 2.2.1.1. 如果当前是0(也意味着新值非0),从退款计数器减去SSTORE_CLEAR_REFUND gas。我们可以证明计数器的值不会低于0.
// 2.2.1.2. 如果新值是0(也意味着当前值非0),奖励SSTORE_CLEAR_REFUND gas到退款计数器。
// 2.2.2. 如果原值等于新值(此存储槽被复位)
// 2.2.2.1. 如果原值是0,增加SSTORE_INIT_REFUND gas到退款计数器。清理内存,并且是复位,奖励两者相加。
// 2.2.2.2. 否则,增加SSTORE_CLEAN_REFUND gas到退款计数器。只是复位。
// 与gasSStore相比,gasSStoreEIP2200除增加了gasleft必须大于2300的限制(以太坊强制增加了一个自动给予接收方的gas值,
// 这个值最小是2300。这样做是为了让接受交易的钱包至少有足够的gas来记录交易)外,
// 还调整了各个情况下的gas成本。
func gasSStoreEIP2200(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
// If we fail the minimum gas availability invariant, fail (0)
// 0.如果*gasleft*不大于SstoreSentryGasEIP2200(2300),则不允许进行SSTORE操作。
if contract.Gas <= params.SstoreSentryGasEIP2200 {
return 0, errors.New("not enough gas for reentrancy sentry")
}
// Gas sentry honoured, do the actual gas calculation based on the stored value
// 可重入哨兵的Gas已经满足,进行基于存储值的实际gas计算。
var (
y, x = stack.Back(1), stack.Back(0)
current = evm.StateDB.GetState(contract.Address(), common.BigToHash(x))
)
value := common.BigToHash(y)
if current == value {
// noop (1)
return params.SstoreNoopGasEIP2200, nil
}
original := evm.StateDB.GetCommittedState(contract.Address(), common.BigToHash(x))
if original == current {
if original == (common.Hash{
}) {
// create slot (2.1.1)
return params.SstoreInitGasEIP2200, nil
}
if value == (common.Hash{
}) {
// delete slot (2.1.2b)
evm.StateDB.AddRefund(params.SstoreClearRefundEIP2200)
}
return params.SstoreCleanGasEIP2200, nil // write existing slot (2.1.2)
}
if original != (common.Hash{
}) {
if current == (common.Hash{
}) {
// recreate slot (2.2.1.1)
evm.StateDB.SubRefund(params.SstoreClearRefundEIP2200)
} else if value == (common.Hash{
}) {
// delete slot (2.2.1.2)
evm.StateDB.AddRefund(params.SstoreClearRefundEIP2200)
}
}
if original == value {
if original == (common.Hash{
}) {
// reset to original inexistent slot (2.2.2.1)
evm.StateDB.AddRefund(params.SstoreInitRefundEIP2200)
} else {
// reset to original existing slot (2.2.2.2)
evm.StateDB.AddRefund(params.SstoreCleanRefundEIP2200)
}
}
return params.SstoreDirtyGasEIP2200, nil // dirty update (2.2)
}
func gasCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
var (
gas uint64
transfersValue = stack.Back(2).Sign() != 0 // 要交易的值,不是0则为true
address = common.BigToAddress(stack.Back(1))
)
// EIP158旨在清除那些攻击者用来充斥泛滥以太坊网络的空账号(缺少code,balance,storage和nounce==0的账户),这些账号导致区块链网络处于"肿胀状态"。
// https://github.com/ethereum/EIPs/issues/158
if evm.chainRules.IsEIP158 {
if transfersValue && evm.StateDB.Empty(address) {
gas += params.CallNewAccountGas // 新账户
}
} else if !evm.StateDB.Exist(address) {
// 若不是IsEIP158,就不用关心交易值是否为0
gas += params.CallNewAccountGas // 若是新账户,直接加上调用新账户的gas
}
if transfersValue {
// 若交易值不是0,携带数据的交易
gas += params.CallValueTransferGas
}
memoryGas, err := memoryGasCost(mem, memorySize) // 内存扩展的gas成本
if err != nil {
return 0, err
}
var overflow bool
if gas, overflow = math.SafeAdd(gas, memoryGas); overflow {
return 0, errGasUintOverflow
}
// callGas返回实际用于调用的gas成本。并且保存在evm.callGasTemp中.
// 除去父合约在调用合约时花去的其他gas成本,单纯用于子合约执行的gas。也就是子合约可以使用的gas数量。
evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas, gas, stack.Back(0))
if err != nil {
return 0, err
}
if gas, overflow = math.SafeAdd(gas, evm.callGasTemp); overflow {
return 0, errGasUintOverflow
}
return gas, nil
}