Contract-Design

如果 Metamask Gas 計算幾乎完美,為什麼我們仍然可以擺脫 Gas 錯誤?

  • January 3, 2019

如果這個問題看起來有點幼稚,請原諒我。我寫了一個智能合約(用於證明存在)並在 Ganache 中模擬它。在其中,我有邏輯將數據儲存在索引和結構之間創建的映射中,如下所示:

/// mapping of document and index
mapping (uint => Proof) idToProof;

// Struct 
struct Proof {
   uint16   id;
   uint     proofdate;
   address  creator; 
   string   title;
   string   proofhash;
   string   ipfshash;

}

當執行與映射中的儲存數據相關的程式碼行時,我的合約遇到了 gas 異常(在下面突出顯示):

function _saveTheProof(uint _proofTimeStamp) private {    
    Proof memory newProof = Proof(proofId, proofTimeStamp, proofCreator, proofTitle, proofHash, proofIpfsHash); 
    idToProof[proofId] = newProof; // <-- this line
    newProofCreated(proofId, proofCreator, proofTitle, proofHash, proofIpfsHash, proofRemarks, proofTags, _proofTimeStamp);
} 

當我刪除此程式碼時,我的 dapp 執行良好。我真的很困惑這裡有什麼問題,為什麼 Metamask 計算的 Gas 不足以執行這個邏輯。如果 Metamask 計算的 Gas 需求幾乎是正確的,那麼為什麼我們最終會出現 Out of Gas 錯誤。

請在這裡幫助糾正我的概念,或者如果我的程式碼在這裡受到指責。

氣體估算不是一門精確的科學。在某些情況下,氣體估計可能是錯誤的,或者無法正確估計。

某些此類情況取決於行為。如果合約具有由不受 tx 控制的變數(例如,塊雜湊)觸發的不同程式碼路徑,那麼估計和實際執行可能需要不同數量的 gas。例如,如果 blockhash 是奇數,你可能會消耗 30000 gas,如果是偶數,你可能會消耗 50000。如果估計gas時blockhash是奇數,但即使它被開採,tx也會失敗。

另一個常見的場景與儲存有關。如果您的交易釋放了儲存空間,您將獲得“gas 退款”。因此,如果一筆交易消耗了 50000 gas,並獲得了 2000 gas 退款,那麼根據最終成本估算 gas 將返回 48000。但是,交易仍然需要 50000 的氣體限制才能成功,因為它需要在應用任何退款之前完成計算。如果不手動增加氣體限制,這將導致交易失敗。

在處理數組和映射時,可能會出現類似的錯誤。至少在前一段時間,像元遮罩這樣的系統無法正確處理數組遍歷估計,我不確定是否仍然如此。

嘗試設置一個非常高的 gas 限制,並查看失敗交易和成功交易之間的跟踪差異,看看你是否遇到了任何可能導致 gas 退款的操作,或者某種可變邏輯或數組遍歷。

Metamask 的氣體計算一點也不完美。問題的核心在於eth_EstimateGas簡單地執行合約並獲得usedGas價值的功能。

這就是它的作用/internal/ethapi/api.go

// Create a helper to check if a gas allowance results in an executable transaction
executable := func(gas uint64) bool {
   args.Gas = hexutil.Uint64(gas)

   _, _, failed, err := s.doCall(ctx, args, rpc.PendingBlockNumber, vm.Config{}, 0)
   if err != nil || failed {
       return false
   }
   return true
}
// Execute the binary search and hone in on an executable gas limit
for lo+1 < hi {
   mid := (hi + lo) / 2
   if !executable(mid) {
       lo = mid
   } else {
       hi = mid
   }
}

此功能使用二分搜尋來找到最佳汽油價格,但是這種啟發式方法完全沒有用。為什麼?因為gas退款。如果您的契約返回儲存,則會發生負氣體使用。例如,如果您花費 100,000 gas 並清理大量儲存空間,您可能會收到 40,000 gas 的退款。但是 eth_EstimateGas 將返回比 100,000 低得多的值,因為 eth_EstimateGas 認為正確的 gas 限制是 60,000。由於退款僅在 Call() 結束時處理,因此您必須提供最大 gas 限制才能存活到結束。

退款在ApplyMessage函式內部處理,eth_EstimateGas無法知道是否有退款。這就是為什麼,無論你使用二分搜尋還是 Black Scholes 模型來估計 gas 限制,無論如何你都會弄錯。

如果你想解決這個問題,只需從ApplyMessage函式中獲取合約消耗的真實gas即可。修改原始獲取源以返回此變數:

st.gasUsed()

這就是我在自己的自定義修改中的做法geth

func (st *StateTransition) TransitionDb() (ret []byte, usedGas,maxUsedGas 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,0, false, err
   }
   if err = st.useGas(gas); err != nil {
       return nil, 0,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,0, false, vmerr
       }
   }
   maxUsedGas=st.gasUsed()
   st.refundGas()
   st.state.AddBalance(st.evm.Coinbase, new(big.Int).Mul(new(big.Int).SetUint64(st.gasUsed()), st.gasPrice))

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

顯然,您必須將附加的返回值向上傳播到所有呼叫 ApplyMessage 的函式,但這並不是一項大工作。

引用自:https://ethereum.stackexchange.com/questions/56287