1 问题的提出

在联盟链里,有需求是普通的转账ether可以收取交易gas,发布或调用智能合约不需要gas费用。在私链环境下,如果智能合约调用是私链官方者的行为,则希望智能合约不收取gas费用。所谓的普通转账,就是在web3里面通过eth.sendTransaction({from:a,to:b,value:c)这种方式发起的交易。

2 虚拟机EVM中对交易处理及收取gas的机制

在源码 core/state_transition.go中,执行交易的函数是

func ApplyMessage(evm *vm.EVM, msg Message, gp *GasPool) ([]byte, uint64, bool, error) {
	return NewStateTransition(evm, msg, gp).TransitionDb()
}

这个函数首先通过NewStateTransaction函数新建一个StateTransaction对象,然后再通过这个对象来执行TransactionDb()函数来真正处理交易。首先看NewStateTransaction函数,根据传递进来的EVM对象evm和Message对象msg来初始化一个StateTransaction对象。EVM对象就是虚拟机本身,交易包含的额外信息都存在Message对象中。

/ NewStateTransition initialises and returns a new state transition object.
func NewStateTransition(evm *vm.EVM, msg Message, gp *GasPool) *StateTransition {
	return &StateTransition{
		gp:       gp,
		evm:      evm,
		msg:      msg,
		gasPrice: msg.GasPrice(),
		value:    msg.Value(),
		data:     msg.Data(),
		state:    evm.StateDB,
	}
}

再来看TransactionDb()交易执行函数:

func (st *StateTransition) TransitionDb() (ret []byte, usedGas uint64, failed bool, err error) {
	if err = st.preCheck(); err != nil {
		return
	}
	msg := st.msg
	sender := vm.AccountRef(msg.From())
	homestead := st.evm.ChainConfig().IsHomestead(st.evm.BlockNumber)
	contractCreation := msg.To() == nil

	// Pay intrinsic gas
	gas, err := IntrinsicGas(st.data, contractCreation, homestead)
	if err != nil {
		return nil, 0, false, err
	}
	if err = st.useGas(gas); err != nil {
		return nil, 0, false, err
	}

	var (
		evm = st.evm
		// vm errors do not effect consensus and are therefor
		// not assigned to err, except for insufficient balance
		// error.
		vmerr error
	)
	if contractCreation {
		ret, _, st.gas, vmerr = evm.Create(sender, st.data, st.gas, st.value)
	} else {
		// Increment the nonce for the next transaction
		st.state.SetNonce(msg.From(), st.state.GetNonce(sender.Address())+1)
		ret, st.gas, vmerr = evm.Call(sender, st.to(), st.data, st.gas, st.value)
	}
	if vmerr != nil {
		log.Debug("VM returned with error", "err", vmerr)
		// The only possible consensus-error would be if there wasn't
		// sufficient balance to make the transfer happen. The first
		// balance transfer may never fail.
		if vmerr == vm.ErrInsufficientBalance {
			return nil, 0, false, vmerr
		}
	}
	st.refundGas()
	st.state.AddBalance(st.evm.Coinbase, new(big.Int).Mul(new(big.Int).SetUint64(st.gasUsed()), st.gasPrice))

	return ret, st.gasUsed(), vmerr != nil, err
}

首先是进行交易的preCheck()函数,preCheck()函数先检查Nonce,然后再通过buyGas购买gas。

func (st *StateTransition) preCheck() error {
	// Make sure this transaction's nonce is correct.
	if st.msg.CheckNonce() {
		nonce := st.state.GetNonce(st.msg.From())
		if nonce < st.msg.Nonce() {
			return ErrNonceTooHigh
		} else if nonce > st.msg.Nonce() {
			return ErrNonceTooLow
		}
	}
	return st.buyGas()
}

进入到buyGas内部:

func (st *StateTransition) buyGas() error {
	mgval := new(big.Int).Mul(new(big.Int).SetUint64(st.msg.Gas()), st.gasPrice)
	if st.state.GetBalance(st.msg.From()).Cmp(mgval) < 0 {
		return errInsufficientBalanceForGas
	}
	if err := st.gp.SubGas(st.msg.Gas()); err != nil {
		return err
	}
	st.gas += st.msg.Gas()

	st.initialGas = st.msg.Gas()
	st.state.SubBalance(st.msg.From(), mgval)
	return nil
}

这里先根据gasPrice和数量gas计算gas费用,mgVal=gasPrice*gas,(注意这里的gas就是给交易设置的gasLimit,而不是真正用到的gas数量gasUsed。以太坊普通的以太坊转账需要消耗的gasUsed一般是21000。而gasLimit是你为交易设定的gas上限。)。然后判断当前转出方st.msg.From()的账户余额和mgVal,如果余额不足则会抛出 errInsufficientBalanceForGas错误。然后从转出账户扣除数量为mgval的费用。注意,mgval比实际真实消耗的gas费用要高,这里扣除多了,到时候会补偿回来。st.gas += st.msg.Gas(),此时st.gas=gasLimit

再回到TransitionDb()函数,执行完preChek(),做了一些变量赋值。如果是发布合约,msg.To()就会为nil。

    msg := st.msg
    sender := vm.AccountRef(msg.From())
    homestead := st.evm.ChainConfig().IsHomestead(st.evm.BlockNumber)
    contractCreation := msg.To() == nil

然后通过IntrinsicGas来计算交易要消耗的真实gas数量gasUsed。IntrinsicGas根据交易code的字节数多少来计算,合约越复杂,携带的数据量越多,需要的gas数量越多。然后再经过过st.useGas函数从st.gas扣除gasUsed,此时st.gas=gasLimit-gasUsed。

接着真正在虚拟机内执行交易了。

    if contractCreation {
		ret, _, st.gas, vmerr = evm.Create(sender, st.data, st.gas, st.value)
	} else {
		// Increment the nonce for the next transaction
		st.state.SetNonce(msg.From(), st.state.GetNonce(sender.Address())+1)
		ret, st.gas, vmerr = evm.Call(sender, st.to(), st.data, st.gas, st.value)
	}
	if vmerr != nil {
		log.Debug("VM returned with error", "err", vmerr)
		// The only possible consensus-error would be if there wasn't
		// sufficient balance to make the transfer happen. The first
		// balance transfer may never fail.
		if vmerr == vm.ErrInsufficientBalance {
			return nil, 0, false, vmerr
		}
	}

如果是创建合约,则调用evm.Create,否则走evm.Call路径。这里有个问题,调用合约的交易和普通以太坊转账交易也会走evm.call路径,那么如何区别这俩种情况?evm.Call总有代码段

    if !evm.StateDB.Exist(addr) {
		precompiles := PrecompiledContractsHomestead
		if evm.ChainConfig().IsByzantium(evm.BlockNumber) {
			precompiles = PrecompiledContractsByzantium
		}
		if precompiles[addr] == nil && evm.ChainConfig().IsEIP158(evm.BlockNumber) && value.Sign() == 0 {
			// Calling a non existing account, don't do antything, but ping the tracer
			if evm.vmConfig.Debug && evm.depth == 0 {
				evm.vmConfig.Tracer.CaptureStart(caller.Address(), addr, false, input, gas, value)
				evm.vmConfig.Tracer.CaptureEnd(ret, 0, 0, nil)
			}
			return nil, gas, nil
		}
		evm.StateDB.CreateAccount(addr)
	}

可以通过判断交易的目的地址st.to()执行evm.StateDB.Exist(addr)来判断,如果是合约地址,则evm.StateDB.Exist(addr)返回true,如果是用户账户地址,则返回false,这个时候需要为普通以太坊转账交易临时新建一个账户。

继续回到TransitionDb(),在evm中执行完交易后,最后还剩3行代码

        st.refundGas()
	st.state.AddBalance(st.evm.Coinbase, new(big.Int).Mul(new(big.Int).SetUint64(st.gasUsed()), st.gasPrice))

	return ret, st.gasUsed(), vmerr != nil, err

st.refundGas()给发起者补偿gas,前面说过了,from账户在buyGas阶段被扣除了gasLimit*gasPrice的gas,而实际上交易消耗的真实gas是gasUsed*gasPrice,所以需要将(gasLimit-gasUsed)*gasPrice的多扣部分返还给from账户。

交易的真实gas消耗是gasUsed*gasPrice,这部分通过st.AddBalance给了evm.Coinbase,即区块记账账户。

3 如何取消智能合约手续费

上一节中解读了交易的gas扣除与奖励机制,源码绕了一大圈,感觉有点懵。其实想取消gas费用,直接操作gasPrice就行了。由于gasPrice对交易的过滤机制是在交易处理ApplyMessage函数之前就完成了,所以在这里可以取巧。可以在NewStateTransition中传递gasPrice时将之设为0即可。改造后的NewSateTransaction函数:

// NewStateTransition initialises and returns a new state transition object.
func NewStateTransition(evm *vm.EVM, msg Message, gp *GasPool) *StateTransition {
	gasPrice:= msg.GasPrice()
	var addr = common.Address{}
	if msg.To() != nil{
		addr = *msg.To()
	}
	if  msg.To() == nil || evm.StateDB.Exist(addr){
		log.Info("contract create or call")
		gasPrice = big.NewInt(0)
	}
	return &StateTransition{
		gp:       gp,
		evm:      evm,
		msg:      msg,
		gasPrice: gasPrice,//msg.GasPrice(),
		value:    msg.Value(),
		data:     msg.Data(),
		state:    evm.StateDB,
	}
}

这里判断交易目的地址msg.To(),如果目的地址为空,说明是合约发布的交易。如果msg.To()存在evm.StateDB中,则说明是调用智能合约的交易,这俩种情况都将gasPrice设置为0。

4 实验验证

新建一条私有链,里面新建3个账户。eth.accounts[0]是矿工,先用eth.accounts[0]往eth.accounts[1]转100 ether,查询eth.accounts[1]余额:

> eth.sendTransaction({from:eth.coinbase,to:eth.accounts[1],value:web3.toWei(100,"ether")})
INFO [08-11|16:08:47.447] Submitted transaction                    fullhash=0x94d130d04050b9368e4c124c97728ab539a6ff084eae5b761b148ddbaf478afe recipient=0xC004Fdeb4daC9827c695C672dAa2aFB0Ed2D0779
"0x94d130d04050b9368e4c124c97728ab539a6ff084eae5b761b148ddbaf478afe"
> miner.start()
INFO [08-11|16:09:01.725] Updated mining threads                   threads=0
INFO [08-11|16:09:01.725] Transaction pool price threshold updated price=12000000000000
null
INFO [08-11|16:09:01.725] Starting mining operation 
> INFO [08-11|16:09:01.726] Commit new mining work                   number=1 txs=1 uncles=0 elapsed=898.694µs
> miner.stopINFO [08-11|16:09:04.990] Successfully sealed new block            number=1 hash=15e49e…f61ead
INFO [08-11|16:09:04.991] 
Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐