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]