在联盟链里,有需求是普通的转账ether可以收取交易gas,发布或调用智能合约不需要gas费用。在私链环境下,如果智能合约调用是私链官方者的行为,则希望智能合约不收取gas费用。所谓的普通转账,就是在web3里面通过eth.sendTransaction({from:a,to:b,value:c)这种方式发起的交易。
在源码 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,即区块记账账户。
上一节中解读了交易的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。
新建一条私有链,里面新建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]