比較 require(), assert() 和 revert(),及其運作方式。
利用 solidity 來撰寫 ethereum 智能合約的時候,很常會需要做一些狀態檢查。比如很多dapp常用到的 ownable:
在 transferOwnership 的時候合約需要檢查交易發起人是否為 owner , 若不是則執行 throw。throw 會使整個交易終止,並讓合約狀態回到交易執行前的狀態,並取走“所有”交易手續費,及 msg.sender 會被抽走 gasLimit * gasPrice 這麼多 ether。
補充:若是一筆沒有遇到 throw 的正常交易, 則會被抽走 gasUsed * gasPrice 這麼多 ether。
有參與過 ICO 的人應該會知道,ICO 很常會也一些奇怪的投標規則。像是會限制投標時間,或是 gasPrice,或是個人投標上限等等。你會發現如果你沒有遵守這些規則,那你發出去的交易會失敗,還會被抽走所有的交易手續費,而這通常都不少錢,因為很多投標需要大量的 gas,致使 gasLimit 不能設太低。像我只是搞錯了投標時間,提早發出了投標交易,就被抽走這麼多手續費,實在是叫我情何以堪。所以需要不同的狀態檢查機制,能夠分辨是簡單的錯誤,或是較嚴重的系統錯誤,兩者會顯示不同的錯誤,抽不同的手續費。
補充:在還沒更新前,還是可以透過 return 的寫法來避免抽取過多的手續費。不過這樣會合約會需要寫比較多東西。另外,因為智能合約為 ethereum 上所有節點所執行, return 如果不是由其他智能合約承接的話,無法直接從交易上看出回傳值,導致很難從交易本身直接看出交易成功或失敗。
在 0.4.10 版的 solidity,新增了 require(), assert(), revert() 想要解決上段提到的問題。預計 require() 用來檢查較不嚴重的錯誤,可以退回為使用到的 gas。而 assert() 用來檢查較嚴重的錯誤,會像以前一樣拿走所有 gasLimit 的手續費。寫法基本上都相同,只是處理方式不一樣。而 revert 跟 require 基本上相同,但是 revert 沒有包括狀態檢查。
solidity 的智能合約完成後,會編譯成 ethereum byte code 在 ethereum virtual machine (EVM) 上執行。在 0.4.10 版之前,throw 這個關鍵字,會被編譯成 0xfe 開頭的 opcode ,如果你有把黃皮書背熟的話,就會知道這個 opcode 是沒有被定義的,所以當 evm 執行到這個 opcode 會回傳 invalid opcode error。
而在 0.4.10 版之後,新增了 require(), assert(), revert() 三個函式。編譯器會把 require() 以及 revert() 編譯成 0xfd。把 assert() 編譯成 0xfe。 throw 處理方式跟 require相同,會編譯成 0xfd。這些更新只在 solidity 跟 solidity compiler,與 ethereum protocol 無關。當然,新產生的 0xfd opcode, evm 也不認得。
要讓 evm 認得新的 opcode,以便執行相對應的動作,會要更改到 ethereum protocol, 需要硬分岔來更新。在還未更新之前,這些變更都還是會跟之前相同的結果。而這項更新會在 即將到來的 Byzantium 硬分叉裡。新增 REVERT opcode ,就是 0xfd 。
當 evm 執行到 REVERT 指令,會把這個交易更新到的合約狀態都回復到交易前,且會退還剩餘的手續費。另外允許回傳一些資訊,讓錯誤訊息不再只有 execution error。可能會像是這樣:
補充: solidity 目前還沒有 error message 的寫法,相關討論在 (連結)。
最後,關於 assert 以及 require 等如何選用並沒有硬性規定,在合約撰寫的時候可以自行決定。不過,建議像是狀態檢查,input 檢查這些不影響合約狀態的可以用 require()。而 revert() 跟 require 相同,但當合約較複雜的時候,你會發現用 revert 會比較好讀。assert ()用在較安全性上的檢查,像是 overflow 等等。