アルゴリズムとかオーダーとか

仕事で勉強したことなどをまとめてます

Transactionの消費gasは最大50%offまで!

Hi-Etherのslackで興味深い質問がありました。

storageのデータを削除するときには gasがrefundされるという認識だったのですが、arrayの要素を全削除するときにgasが大量にかかるのはどうしてでしょうか?教えていただきたいです。

確かに、以前の記事でstorage上からデータを削除(non zero から zeroに変更)した場合は、refundGas=15000、 used gas=5000で最終的に10000gasが戻ってくると思っていました。が、実際に100件の値を持つuintの配列を生成して削除してみると削除時に270000gasほど消費されてしまいました。

今回は、このrefundGasとtransactionで消費されるgasの計算方法について調べたことをまとめます。

1. refundGasとused gasは別々に計上される

まず、そもそもとしてgas costを計算するときに、gas costとrefund gasは別々に計上されます。
例えばstorageにデータを格納するときのgas costの処理はgo-ethereumでは次のようになっています。

func gasSStore(gt params.GasTable, evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
	var (
		y, x = stack.Back(1), stack.Back(0)
		val  = evm.StateDB.GetState(contract.Address(), common.BigToHash(x))
	)
	// This checks for 3 scenario's and calculates gas accordingly
	// 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)
	if common.EmptyHash(val) && !common.EmptyHash(common.BigToHash(y)) {
		// 0 => non 0
		return params.SstoreSetGas, nil
	} else if !common.EmptyHash(val) && common.EmptyHash(common.BigToHash(y)) {
		evm.StateDB.AddRefund(params.SstoreRefundGas)

		return params.SstoreClearGas, nil
	} else {
		// non 0 => non 0 (or 0 => 0)
		return params.SstoreResetGas, nil
	}
}

gas_table.go#L118-L138
上記の通り、DELETEの処理ではevm.StateDB.AddRefundでrefundGasを計上し、sstoreのcostとしてはparams.SstoreClearGasを返しています。

2. transactionで消費されるgasの総量

transactionで実際に消費されるgasの総量はrefundも含めて再計算されます。
実際に再計算しているソースは以下の通りです。

        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
}

func (st *StateTransition) refundGas() {
	// Apply refund counter, capped to half of the used gas.
	refund := st.gasUsed() / 2
	if refund > st.state.GetRefund() {
		refund = st.state.GetRefund()
	}
	st.gas += refund

	// Return ETH for remaining gas, exchanged at the original rate.
	sender := st.from()

	remaining := new(big.Int).Mul(new(big.Int).SetUint64(st.gas), st.gasPrice)
	st.state.AddBalance(sender.Address(), remaining)

	// Also return remaining gas to the block gas counter so it is
	// available for the next transaction.
	st.gp.AddGas(st.gas)
}

// gasUsed returns the amount of gas used up by the state transition.
func (st *StateTransition) gasUsed() uint64 {
	return st.initialGas - st.gas
}

state_transaction.go#L248-L276

st.gasは消費された残りのgas量です。そして、refoundGas()の中で前述した別々に計上されたrefundGasを元にrefund量を再計算しています。ソースからわかる通り、refundされる最大値は使用したgasの1/2までです。

ということで、例えばstorageから100個の値を削除する場合に消費されるgas量は次のように計算されます。

  1. refundGas = 15,000 * 100 = 1,500,000gas
  2. gas cost = 5,000 * 100 = 500,000gas
  3. 有効なrefundGas量 = min(gas cost/ 2, refundGas) = 250,000gas
  4. 最終的なcost =gas cost - refundGasの最大値 = 250,000gas

なので実際にはそこそこ大量のgasが消費されることになります。

まとめ

gas代については結構表示されてる額をみてそんなもんかぁって鵜呑みにしちゃってた部分があるのでまた1つ賢くなれました。
transactionが発行時にgasが戻ってくるという設計だといろいろ問題があるから、実際にはgasが消費されるように設計されているんだろうなぁってなんとなく思ってはいたので、具体的にどういう計算がされて実際にいくらまでrefundGasでcostが削減されているのかが理解できました。
普段スルーしていた部分を質問してくれて非常に助かりました!この場を借りてご質問してくださった方に感謝を述べます。ありがとござます!