学习以太坊Gas机制

Gas

基本概念

为了避免网络滥用及回避由于图灵完备而带来的一些不可避免的问题(the halting problem),在以太坊中所有的程序执行都收费。Gas是基本的工作量成本单位,用于计量在以太坊区块链上执行操作所需的计算、存储资源和带宽,其目的是限制执行交易所需的工作量。各种操作的费用以gas为单位计算。任意的程序片段(包括合约创建、消息调用、分配资源以及访问账户storage、在虚拟机上执行操作等)都有一个普遍认同的gas成本。[1] Gas有两个作用[5]:

  • 以太坊(不稳定的)价格和矿工工作报酬之间的缓冲
  • 对拒绝服务(DoS)攻击的防御.

每一个交易都要指定一个 gas 上限:gasLimit。发送者通过在交易中指定gas price来购买gas,系统预先从发送者的账户余额中扣除gasLimit * gasPrice的交易费,即采用预付费机制。Gas price是指当你将交易发送到以太坊网络时,愿意支付的每单位gas的价格。[5]如果账户余额不足,交易会被视为无效交易。[1]之所以将其命名为 gasLimit,是因为剩余的 gas会在交易完成后被返还(与购买时同样价格)到发送者账户。每个矿工自己选择他们想要接受和拒绝的gas价格。交易者们则需要在降低 gas 价格和使交易能尽快被矿工打包间进行权衡。
通常来说,以太币(Ether)是用来购买 gas 的,未返还的部分就会移交到 beneficiary 的地址(即一般由矿工所控制的一个账户地址)。以太币最小的单位是 Wei(伟),所有货币值都以 Wei 的整数倍来记录。[1]
ether-wei
注意:Gas只存在于EVM中,用来给计算的工作量计数。发送方用ether支付交易费,然后将其转换为gas用于EVM核算,最后将剩余的gas转换为ether返还给发送方,未返还的同样转换为ether作为交易费付给矿工[5]。

block gas limit

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限定值应该是矿工们设置的中间值。

Gas成本的确定

EVM可执行的各种操作的相对gas cost经过精心设计,以最好地保护以太坊区块链不受攻击。操作进行的计算越多,gas成本越高[5]。
2016年,一名攻击者发现并利用了gas成本与实际资源成本不匹配的问题,证明了将gas成本与实际资源成本相匹配的重要性。 这个问题通过一个硬分叉(代号为“橘子口哨”,EIP 150)解决,它通过改变IO重型操作长期的gas费率来抵抗垃圾交易攻击,并增加了63/64规则。

Gas收费情况

三种情况下会收取执行费用(以gas来结算)[1]:

  1. 最普遍的情况就是计算操作费用。
  2. 执行一个低级别的消息调用或者合约创建可能需要扣除 gas,这也就是执行 CREATE,CALL和CALLCODE 的费用的一部分。
  3. 内存使用的增加也会消耗一定的 gas。
    gas cost

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]:

  1. 删除一份合约(自毁)将会得到奖励。
  2. 将一个存储地址的非零值改为零(SSTORE[x] = 0)可以获得退款。

为了避免退款机制被利用,每笔交易的最高退款额被设定为gas总用量的50%(向下取整)[5]。这种退款机制会激励人们清理存储器。正因为缺乏这样的激励,许多合约并未有效使用存储空间,从而导致存储快速膨胀。这样既获得了存储收费的大部分好处,又不会失去合约一旦确立就可以永久存在的保证。延迟退款机制是必要的,因为可以防止拒绝服务攻击。攻击者发送一笔含有少量gas的交易,循环清理大量的存储,直到用光gas,这样消耗了大量的验证算力,但实际并没有真正清理存储也没有花费大量gas。50%的上限是为了确保:给定一个具有一定数量gas的交易,矿工依然可以根据gasLimit确定用于执行此交易的计算时间上限。[3]

gas在执行过程中的使用

当EVM需要完成一个交易时,它首先被给予一个等于交易中gas limit所指定数量的gas supply。执行的每个操作码都有一个gas成本,因此EVM的gas supply会随着程序向前一步步执行而逐渐减少。在每个操作之前,EVM检查是否有足够的gas来支付操作的执行费用。以太坊在操作执行前收取费用。如果没有足够的gas,EVM就会停止执行并将本次交易修改的状态回滚。[5]
gas and fee

  • 如果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相关源代码(geth)

gas成本定义和指令gas成本的计算代码集中在core/vm/gas.gocore/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(), 
  • 5
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值