1. 程式人生 > >geth原始碼修改:取消呼叫智慧合約的gas費用

geth原始碼修改:取消呼叫智慧合約的gas費用

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]