精华内容
下载资源
问答
  • 学习以太坊Gas机制

    千次阅读 2020-04-27 23:37:46
    以太坊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(), 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
    }
    

    参考文献

    1. Ethereum Yellow Paper
      ETHEREUM: A SECURE DECENTRALISED GENERALISED TRANSACTION LEDGER
      https://ethereum.github.io/yellowpaper/paper.pdf
    2. Ethereum White Paper
      A Next-Generation Smart Contract and Decentralized Application Platform
      https://github.com/ethereum/wiki/wiki/White-Paper
    3. Design Rationale
      https://github.com/ethereum/wiki/wiki/Design-Rationale
    4. Notes on the EVM
      https://github.com/CoinCulture/evm-tools/blob/master/analysis/guide.md
    5. ethereumbook
      The Ethereum Virtual Machine
      https://github.com/ethereumbook/ethereumbook/blob/develop/13evm.asciidoc
    6. The Ethereum Virtual Machine
      https://solidity.readthedocs.io/en/v0.4.24/introduction-to-smart-contracts.html
    7. Ethereum EVM Illustrated
      24/introduction-to-smart-contracts.html
    8. Ethereum EVM Illustrated
      https://github.com/takenobu-hs/ethereum-evm-illustrated
    展开全文
  • 18 以太坊的共识机制

    2021-01-08 04:26:18
    以太坊的共识机制 以太坊把出块时间降低到十几秒,但是这样也带来了很多问题,较频繁的出现分叉,这对于共识协议来说有什么影响呢? 在比特币中,只有最长合法链的中才是合法链, GHOST协议:假如一条区块链没有成为...
  • 1 以太坊的事件机制 以太坊go-ethereum源码中发送事件除了用常规的通道以外,还用了封装的Feed结构来执行事件的订阅和发送。以太坊中使用了大量的Feed来处理事件。使用Feed订阅事件的步骤是: 定义一个通道ch:ch=...

    1 以太坊的事件机制

    以太坊go-ethereum源码中发送事件除了用常规的通道以外,还用了封装的Feed结构来执行事件的订阅和发送。以太坊中使用了大量的Feed来处理事件。使用Feed订阅事件的步骤是:

    • 定义一个通道ch:ch=make(someType)
    • 定义一个Feed对象feed
    • Feed订阅通道ch:feed.Subscribe(ch)
    • 使用feed发送数据给通道:feed.Send(someTypeData)
    • ch接收数据:ret<-ch

    一个feed可以订阅多个通道,当使用feed发送数据后,所有的通道都将接收到数据。下文将解读Feed的源码,在进入Feed源码解读之前我们先介绍一下go中的reflect包中的SelectCase。

    2 使用reflect.SelectCase来监听多个通道

    对于多个通道ch1,ch2,ch3,使用传统的Select方式来监听:

    package main
     
    import (
    	"fmt"
    	"strconv"
    )
     
    func main() {
    	var chs1 = make(chan int)
    	var chs2 = make(chan float64)
    	var chs3 = make(chan string)
    	var ch4close = make(chan int)
    	defer close(ch4close)
     
    	go func(c chan int, ch4close chan int) {
    		for i := 0; i < 5; i++ {
    			c <- i
    		}
    		close(c)
    		ch4close <- 1
    	}(chs1, ch4close)
     
    	go func(c chan float64, ch4close chan int) {
    		for i := 0; i < 5; i++ {
    			c <- float64(i) + 0.1
    		}
    		close(c)
    		ch4close <- 1
    	}(chs2, ch4close)
     
    	go func(c chan string, ch4close chan int) {
    		for i := 0; i < 5; i++ {
    			c <- "string:" + strconv.Itoa(i)
    		}
    		close(c)
    		ch4close <- 1
    	}(chs3, ch4close)
     
    	done := 0
    	finished := 0
    	for finished < 3 {
    		select {
    		case v, ok := <-chs1:
    			if ok {
    				done = done + 1
    				fmt.Println(0, v)
    			}
    		case v, ok := <-chs2:
    			if ok {
    				done = done + 1
    				fmt.Println(1, v)
    			}
    		case v, ok := <-chs3:
    			if ok {
    				done = done + 1
    				fmt.Println(2, v)
    			}
    		case _, ok := <- ch4close:
    			if ok {
    				finished = finished+1
    			}
    		}
    	}
    	fmt.Println("Done", done)
    }
    

    使用reflect的方式来监听:

    package main
     
    import (
    	"fmt"
    	"reflect"
    	"strconv"
    )
     
    func main() {
    	var chs1 = make(chan int)
    	var chs2 = make(chan float64)
    	var chs3 = make(chan string)
    	var ch4close = make(chan int)
    	defer close(ch4close)
     
    	go func(c chan int, ch4close chan int) {
    		for i := 0; i < 5; i++ {
    			c <- i
    		}
    		close(c)
    		ch4close <- 1
    	}(chs1, ch4close)
     
    	go func(c chan float64, ch4close chan int) {
    		for i := 0; i < 5; i++ {
    			c <- float64(i) + 0.1
    		}
    		close(c)
    		ch4close <- 1
    	}(chs2, ch4close)
     
    	go func(c chan string, ch4close chan int) {
    		for i := 0; i < 5; i++ {
    			c <- "string:" + strconv.Itoa(i)
    		}
    		close(c)
    		ch4close <- 1
    	}(chs3, ch4close)
     
     
    	var selectCase = make([]reflect.SelectCase, 4)
    	selectCase[0].Dir = reflect.SelectRecv
    	selectCase[0].Chan = reflect.ValueOf(chs1)
     
    	selectCase[1].Dir = reflect.SelectRecv
    	selectCase[1].Chan = reflect.ValueOf(chs2)
     
    	selectCase[2].Dir = reflect.SelectRecv
    	selectCase[2].Chan = reflect.ValueOf(chs3)
     
    	selectCase[3].Dir = reflect.SelectRecv
    	selectCase[3].Chan = reflect.ValueOf(ch4close)
     
    	done := 0
    	finished := 0
    	for finished < len(selectCase)-1 {
    		chosen, recv, recvOk := reflect.Select(selectCase)
     
    		if recvOk {
    			done = done+1
    			switch chosen {
    			case 0:
    				fmt.Println(chosen, recv.Int())
    			case 1:
    				fmt.Println(chosen, recv.Float())
    			case 2:
    				fmt.Println(chosen, recv.String())
    			case 3:
    				finished = finished+1
    				done = done-1
    				// fmt.Println("finished\t", finished)
    			}
    		}
    	}
    	fmt.Println("Done", done)
     
    }

    这里构建了一个reflect.SelectCase数组selectCase,将要监听的通道添加到数组中。监听时只要使用reflect.Select(selectCase)就可以监听所有通道的消息。当通道数多的时候,用SelectCase的方式将会更简洁优雅。

    3 Feed源码解读

    Feed结构的源码在event/feed.go中。

    Feed结构

    type Feed struct {
    	once      sync.Once        // ensures that init only runs once
    	sendLock  chan struct{}    // sendLock has a one-element buffer and is empty when held.It protects sendCases.
    	removeSub chan interface{} // interrupts Send
    	sendCases caseList         // the active set of select cases used by Send
    
    	// The inbox holds newly subscribed channels until they are added to sendCases.
    	mu     sync.Mutex
    	inbox  caseList
    	etype  reflect.Type
    	closed bool
    }
    
    type caseList []reflect.SelectCase

    Feed结构核心的是inbox成员,它是一个SelectCase的数组,保存了该Feed订阅的所有通道。sendCase是所有活跃的通道数组。sendLock通道用来作为锁来保护sendCase。

    初始化函数

    func (f *Feed) init() {
    	f.removeSub = make(chan interface{})
    	f.sendLock = make(chan struct{}, 1)
    	f.sendLock <- struct{}{}
    	f.sendCases = caseList{{Chan: reflect.ValueOf(f.removeSub), Dir: reflect.SelectRecv}}
    }

    这里sendLock被设置成有容量为1的缓冲通道。并且给sendLock先写入了一个值。sendCases预先加入了removeSub通道作为第一个通道。

    通道订阅函数

    
    //这个通道需要有足够的缓冲空间以避免阻塞其它订阅者。速度慢的订阅者不会被丢弃
    func (f *Feed) Subscribe(channel interface{}) Subscription {
    	f.once.Do(f.init)
    
    	chanval := reflect.ValueOf(channel)
    	chantyp := chanval.Type()
    	if chantyp.Kind() != reflect.Chan || chantyp.ChanDir()&reflect.SendDir == 0 {
    		panic(errBadChannel)
    	}
    	sub := &feedSub{feed: f, channel: chanval, err: make(chan error, 1)}
    
    	f.mu.Lock()
    	defer f.mu.Unlock()
    	if !f.typecheck(chantyp.Elem()) {
    		panic(feedTypeError{op: "Subscribe", got: chantyp, want: reflect.ChanOf(reflect.SendDir, f.etype)})
    	}
    	// Add the select case to the inbox.
    	// The next Send will add it to f.sendCases.
    	cas := reflect.SelectCase{Dir: reflect.SelectSend, Chan: chanval}
    	f.inbox = append(f.inbox, cas)
    	return sub
    }

    这个函数做的事情很简单,就是根据通道ch构造一个SelectCase对象,然后将其加入到inbox数组中。这样就完成了通道的订阅。

    发送函数

    // Send delivers to all subscribed channels simultaneously.
    // It returns the number of subscribers that the value was sent to.
    func (f *Feed) Send(value interface{}) (nsent int) {
    	rvalue := reflect.ValueOf(value)
    
    	f.once.Do(f.init)//重新初始化,onece.Do保证只会执行一次
    	<-f.sendLock    //读sendLock通道,若sendLock为空则会堵塞
    
    	// Add new cases from the inbox after taking the send lock.
    	f.mu.Lock()    //访问公共变量加锁
    	f.sendCases = append(f.sendCases, f.inbox...)//将inbox注入到sendCase
    	f.inbox = nil 
    
    	if !f.typecheck(rvalue.Type()) {
    		f.sendLock <- struct{}{}    //出错了,退出前先写sendLock以免下次send操作堵塞
    		panic(feedTypeError{op: "Send", got: rvalue.Type(), want: f.etype})
    	}
    	f.mu.Unlock()
    
    	// 给所有通道设置要发送的数据
    	for i := firstSubSendCase; i < len(f.sendCases); i++ {
    		f.sendCases[i].Send = rvalue
    	}
    
    	// Send until all channels except removeSub have been chosen. 'cases' tracks a prefix
    	// of sendCases. When a send succeeds, the corresponding case moves to the end of
    	// 'cases' and it shrinks by one element.
    	cases := f.sendCases
    	
    	for {
    		// Fast path: try sending without blocking before adding to the select set.
    		// This should usually succeed if subscribers are fast enough and have free
    		// buffer space.
    		for i := firstSubSendCase; i < len(cases); i++ {
               //首先使用TrySend进行发送,这是一种非阻塞操作。当订阅者足够快时一般能够立即成功
    			if cases[i].Chan.TrySend(rvalue) {
    				nsent++
    				cases = cases.deactivate(i)//发送成功,后移该通道
    				i--
    			}
    		}
    		if len(cases) == firstSubSendCase {//所有通道发送完成,退出
    			break
    		}
    		// Select on all the receivers, waiting for them to unblock.
    		chosen, recv, _ := reflect.Select(cases)//等待通道返回
    		//<-f.removeSub
    		if chosen == 0  {
    			index := f.sendCases.find(recv.Interface())
    			f.sendCases = f.sendCases.delete(index)
    			if index >= 0 && index < len(cases) {
    				// Shrink 'cases' too because the removed case was still active.
    				cases = f.sendCases[:len(cases)-1]
    			}
    		} else {
    			cases = cases.deactivate(chosen)
    			nsent++
    		}
    	}
    
    	// Forget about the sent value and hand off the send lock.
    	for i := firstSubSendCase; i < len(f.sendCases); i++ {
    		f.sendCases[i].Send = reflect.Value{}
    	}
    	f.sendLock <- struct{}{}//返回时写入sendLock,为下次发送做准备
    	return nsent
    }

    send函数使用通道的trySend方法来发送,在正常情况下能够立即发送成功,但是当接收通道堵塞的时候,则需要用Select方法这种堵塞的方式等待通道发送成功。在最后返回时,写入sendLock,为下次发送做准备。

    4 send函数存在的问题及优化

    我们看到send函数使用了sendLock通道,它是一个容量为1的通道。在send函数最开始,读出sendLock通道,如果这个时候sendLock为空,则send函数就会堵塞。所以在send函数最后,写入了sendLock通道,这样下次发送去读sendLock时就不会堵塞。看起来好像没有问题,但是理想很丰满,显示有时候会骨感。这里存在的问题就是chosen, recv, _ := reflect.Select(cases)这行代码可能会堵塞,导致for循环一值退不出,send函数发生堵塞,导致sendLock不会被写入。从而导致了死锁。下次send发送就会被堵塞。

    这里使用sendLock是为了保护公共的sendCase数据,解决思路是去掉sendCase,不适用全局的sendCase,而使用局部变量。这样就不用考虑同步的问题了。改造后的send函数:

    func (f *Feed) Send(value interface{}) (nsent int) {
    	rvalue := reflect.ValueOf(value)
    
    	f.once.Do(f.init)
    	//<-f.sendLock
    
    	sendCases := caseList{{Chan: reflect.ValueOf(f.removeSub), Dir: reflect.SelectRecv}}
    	sendCases = append(sendCases, f.inbox...)
    
    	// Set the sent value on all channels.
    	for i := firstSubSendCase; i < len(sendCases); i++ {
    		sendCases[i].Send = rvalue
    	}
    
    	// Send until all channels except removeSub have been chosen. 'cases' tracks a prefix
    	// of sendCases. When a send succeeds, the corresponding case moves to the end of
    	// 'cases' and it shrinks by one element.
    	cases := sendCases
    	//LOOP:
    	for {
    		// Fast path: try sending without blocking before adding to the select set.
    		// This should usually succeed if subscribers are fast enough and have free
    		// buffer space.
    		for i := firstSubSendCase; i < len(cases); i++ {
    			if cases[i].Chan.TrySend(rvalue) {
    				nsent++
    				cases = cases.deactivate(i)
    				i--
    			}
    		}
    		if len(cases) == firstSubSendCase {
    			break
    		}
    		// Select on all the receivers, waiting for them to unblock.
    		chosen, recv, _ := reflect.Select(cases)
    		//<-f.removeSub
    		if chosen == 0  {
    			index := f.sendCases.find(recv.Interface())
    			f.sendCases = f.sendCases.delete(index)
    			if index >= 0 && index < len(cases) {
    				// Shrink 'cases' too because the removed case was still active.
    				cases = f.sendCases[:len(cases)-1]
    			}
    		} else {
    			cases = cases.deactivate(chosen)
    			nsent++
    		}
    	}
    
    	// Forget about the sent value and hand off the send lock.
    	for i := firstSubSendCase; i < len(f.sendCases); i++ {
    		f.sendCases[i].Send = reflect.Value{}
    	}
    	//f.sendLock <- struct{}{}
    	return nsent
    }

    某次send可能会堵塞,但是不会影响下次send发送。

    5 go-ethereum源码中使用send的坑

     我们看core/blockchain.go中的发送函数PostChainEvents():

    // PostChainEvents iterates over the events generated by a chain insertion and
    // posts them into the event feed.
    // TODO: Should not expose PostChainEvents. The chain events should be posted in WriteBlock.
    func (bc *BlockChain) PostChainEvents(events []interface{}, logs []*types.Log) {
    	log.Info("lzj-log PostChainEvents", "events len",len(events))
    	// post event logs for further processing
    	if logs != nil {
    		bc.logsFeed.Send(logs)
    	}
    	for _, event := range events {
    		switch ev := event.(type) {
    		case ChainEvent:
    			log.Info("lzj-log send ChainEvent")
    			bc.chainFeed.Send(ev)
    
    		case ChainHeadEvent:
    			log.Info("lzj-log send ChainHeadEvent")
    			bc.chainHeadFeed.Send(ev)
    
    		case ChainSideEvent:
    			log.Info("lzj-log send ChainSideEvent")
    			bc.chainSideFeed.Send(ev)
    		}
    	}
    }

    这个函数是在for循环中先后发送了ChainEvent、ChainHeadEvent和ChainSideEvent事件。在insert函数中调用了这个 函数。但是这里有个问题,如果前一个事件发送堵塞了,后面的事件发送就不会执行。需要把Send函数放到单独的协程中去。改成这样可以防止堵塞的问题:

    // PostChainEvents iterates over the events generated by a chain insertion and
    // posts them into the event feed.
    // TODO: Should not expose PostChainEvents. The chain events should be posted in WriteBlock.
    func (bc *BlockChain) PostChainEvents(events []interface{}, logs []*types.Log) {
    	log.Info("lzj-log PostChainEvents", "events len",len(events))
    	// post event logs for further processing
    	if logs != nil {
    		bc.logsFeed.Send(logs)
    	}
    	for _, event := range events {
    		switch ev := event.(type) {
    		case ChainEvent:
    			log.Info("lzj-log send ChainEvent")
    			go bc.chainFeed.Send(ev)
    
    		case ChainHeadEvent:
    			log.Info("lzj-log send ChainHeadEvent")
    			go bc.chainHeadFeed.Send(ev)
    
    		case ChainSideEvent:
    			log.Info("lzj-log send ChainSideEvent")
    			go bc.chainSideFeed.Send(ev)
    		}
    	}
    }

    在go里面使用通道要发非常小心,因为很容易引起堵塞从而达不到自己期望的结果。

    展开全文
  • 以太坊交易收发机制

    万次阅读 2019-04-15 07:34:35
    交易的主要数据结构 序号 数据 描述 备注 1 AccountNonce 发送者的发起的交易总... 以太坊地址 | 3 Price 此次交易的 gas price | 4 GasLimit 本交易允许消...

    交易的主要数据结构

      序号 数据 描述 备注
      1 AccountNonce 发送者的发起的交易总数量 |
      2 Recipient 交易接受者的地址 以太坊地址 |
      3 Price 此次交易的 gas price |
      4 GasLimit 本交易允许消耗的最大 Gas 数 |
      5 Gas 要转换的 gas 数 |
      6 Amount 此次交易转移的以太币数量 |
      7 V 签名数据 |
      8 R 签名数据 |
      9 S 签名数据 |
      10 Payload 其他数据 |

    交易收发相关协程

    交易数据验证流程

    交易入池流程

    展开全文
  • 狗年吉祥,开工利是,我们继续研究以太坊源码。从本篇文章开始,我们会深入到以太坊核心源码中去,进而分析与研究以太坊的核心技术。 关键字:拜占庭,挖矿,矿工,分叉,源码分析,uncle叔块,agent,worker,...
  • 以太坊RLP机制分析

    2018-07-31 17:22:00
    RLP,即 Recursive Length Prefix, 递归长度前缀编码,是以太坊数据序列化的主要方法, 具有较好的数据处理效率,尤其是将长度和类型统一作为前缀,实际上 RLP 是基于 ASCII 编码的一种结构化扩充,既能表示长度还能...
  • 以太坊RPC机制与API实例

    千次阅读 2018-07-11 09:34:52
    原文地址:https://www.cnblogs.com/Evsward/p/eth-rpc.html上一篇文章介绍了以太坊的基础知识,我们了解了web3.js的调用方式是通过以太坊RPC技术,本篇文章旨在研究如何开发、编译、运行与使用以太坊RPC接口。...
  • 以太坊共识机制

    2018-04-25 23:01:00
    以太坊常用的三种共识机制(算法):PoW(Proof of Work),工作量证明机制;PoS(Proof of Stake):股权证明机制;DPoS(Delegated Proof of Stake),授权股权证明机制。这些共识机制都能在现实生活中找到对应的经济模型,吸引...
  • 以太坊启动机制

    千次阅读 2017-12-26 17:51:04
    // Start create a live P2P node and starts running it. //创建一个有生命的p2p 节点 并且运行它 func (n *Node) Start() error { //获取写锁 n.lock.Lock() defer n.lock.Unlock() // 如果节点已经在运行,...
  • 以太坊的共识机制

    千次阅读 2018-08-13 14:50:49
    在开始之前,我们补充一点基础知识。   第一个概念是哈希。简单理解,哈希是一个函数。...几乎任何加密货币都会用到哈希算法,以太坊采用的哈希算法是ethash算法。   第二个补充知识是,以太坊的区块结构。...
  • 以太坊:RLP机制分析

    万次阅读 2019-04-13 08:36:55
    RLP,即 Recursive Length Prefix, 递归长度前缀编码,是以太坊数据序列化的主要方法, 具有较好的数据处理效率,尤其是将长度和类型统一作为前缀,实际上 RLP 是基于 ASCII 编码的一种结构化扩充,既能表示长度还能...
  • 上一篇文章(【刘文彬】探路以太坊)介绍了以太坊的基础知识,我们了解了web3.js的调用方式是通过以太坊RPC技术,本篇文章旨在研究如何开发、编译、运行与使用以太坊RPC接口。 关键字:以太坊,RPC,JSON-RPC,...
  • 以太坊挖矿奖励机制研究 1. 目前ETH的奖励机制 目前ETH的区块分为两种,普通区块和叔块: 普通区块的奖励: 固定奖励3ETH 区块内包含所有程序的Gas花费 如果这个普通区块包含了叔块(最多两个叔块),则包含一个叔块...
  • 比特币以太坊 区块链以太坊挖矿原理 关于比特币和以太坊,熟悉区块链的朋友相比都不陌生。比特币是一种加密数字货币,可从别人手中购买或通过挖矿获得。通常是加入比特币矿池进行挖,2020年比特币第3次减产,之后...
  • 然而从现在的以太坊1.0转向以太坊2.0是一个巨大的工程,其共识机制将由POW转向POS。伴随着共识机制的转变,以太坊挖矿的方式也将发生巨变。 为什么呢?因为以太坊2.0的挖矿将不再需要强算力作为保证,那个时候我们...
  • 以太坊中除了基于运算能力的POW(Ethash)外,还有基于权利证明的POA共识机制,Clique是以太坊的POA共识算法的实现,这里主要对POA的Clique相关源码做一个解读分析。 Clique的初始化在 Ethereum.StartMining中,如果...
  • 以太坊的RPC机制

    2018-11-13 01:18:47
    1 go语言的RPC机制  RPC(Remote Procedure Call,远程过程调用)是一种通过网络从远程计算机程序上请求服 务,而不需要了解底层网络细节的应用程序通信协议。RPC协议构建于TCP或UDP,或者是 HTTP 之上,允许...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 970
精华内容 388
关键字:

以太坊机制