所有樂觀的 L2 方案都是圍繞著執行結果及其分歧來打造的:從 plasma 到 rollup,其關鍵都在於 “樂觀性執行( optimistic execution )”—— 樂觀性執行的意思是:任何人(或群體)都能宣稱 “嘿,Layer 1,這些交易的執行結果是 X,不用再執行驗證了!”;如果結果不為 X ,(我們假設)會有其他群體願意支付主鏈的執行成本,來證明結果 X 是錯誤的。
理想情況下我們不需要在主鏈上執行 transaction,這也是為什麼樂觀性執行能很大程度上提高鏈的吞吐量。然而考慮到安全性,一旦出現錯誤的情況(如上圖 tx2),我們也需要有 transaction 回溯機制!
Unipig 的自定義程式碼基本上就是 Unipig 編碼形式的 execute_L2_tx(),你也可以稱之為 execute_uniswap_tx() !
總的來說,其實我們需要的是 Unipig 編碼實現 execute_EVM_tx() —— 一個能夠讓我們在 L1 transaction 中,巢狀執行任何以太坊 L2 transaction 的函式(以實現 fraud proof “錯誤性證明” 功能)。但是理想很豐滿,現實很骨感,要讓以太坊 transaction 巢狀執行本來就非常困難,更何況有些 L2 transaction 根本不適合 L1 !
為什麼很難構建 EVM 中的 EVM
在我們深入解釋我們所提出的獨特解決方案之前,我們先想想 ——為什麼這會成為一個問題 ?難道 EVM 不是執行 EVM transaction 的完美環境嗎?它可是 EVM 呢!
天真的想法:將 L2 的智慧合約重新部署到 L1
EVM 的核心定義了一組計算機指令,以及定義了在 transaction 中每個指令對應地該執行什麼。智慧合約就像是個巨大又醜陋的指令集合——舉個簡單的例子,下圖是 Solidity 的 SafeMath.sol 庫在部署之前的部分編譯:
如果我們想要在 L1 上執行 L2 transaction,直觀的做法就是獲取 L2 使用的程式碼(智慧合約),然後放到 L1;簡言之,就是直接在 L1 上部署對應的 L2 智慧合約!
然而不能這樣做,因為:不同的鏈,不同的結果
這個方法可能適用於某些情況。比如邏輯非常簡單的智慧合約—— SafeMath 庫,它只執行加、減等數學運算;如果我們將 L2 的 SafeMath 合約部署到 L1 上,則它在 L1 上也能正確執行!畢竟加就是加、減就是減,跟在哪條鏈執行無關。
但對於其他智慧合約來說,事情就變得複雜了。舉個簡單的例子,下面的智慧合約執行後會返回“當前的以太坊(區塊)的時間戳 + 42”:
contract TimeShifter {
function getShiftedTime() returns(uint) {
return block.timestamp + 42;
}
}
(在錯誤性挑戰中)把這個合約重新部署到 L1 上之後,還能返回相同的值嗎?
明顯不行!(重部署在當前塊 “之後的區塊”,返回結果肯定不同。)即使是在同一條 L1 上,如果將智慧合約重部署在不同的兩個區塊,返回值也不一樣 —— 因為重部署的合約會獲取 L1 的時間戳,而正確執行 execute_l2_tx 則應該返回 L2 的時間戳。
如果你深入思考,你會發現這個問題幾乎會發生在所有智慧合約上。比如對於某個 ERC 20 智慧合約來說,你將合約重部署在 L1 上之後,你要怎麼設定 L2 上的餘額呢?諸如此類,不可勝數。
解決之道:OVM
過去曾出現過兩種解決 “ EVM 中的 EVM ” 問題的辦法:要麼是對 EVM 進行分叉,要麼是硬著頭皮用 Solidity 重新實現整個 EVM ;OVM 是一種全新的方法,對於當前的以太坊 1.0 有著更好的效能和靈活性,而且不需要分叉!
容器化:執行管理器
OVM 能夠解決問題的最重要原因是,它引入了一個全新的智慧合約(Execution Manager,執行管理器)—— 作為 OVM 智慧合約的虛擬容器。執行管理器會虛擬化所有可能導致 L1、L2 出現不同結果的執行,包括:
· 智慧合約儲存內容
· 交易內容 —— 如區塊高度、時間戳、tx.origin (Solidity 的一個全域性變數,它遍歷整個呼叫棧並返回最初傳送呼叫(或事務)的帳戶的地址),等等。
· 跨合約資訊的路由
基本上,對於可能導致 L1 、 L2 出現不同結果的 EVM 功能, 執行管理器都提供了保證其結果一致的函式。
舉例來說,我們構造一個容器來解決上述提到的時間戳不一致的問題:
contract TimestampManager {
uint storage ovmTimestamp;
function setOvmTimestamp(number: uint) {
ovmTimestamp = number;
}
function getOvmTimestamp() public returns(uint) {
return ovmTimestamp;
}
}
現在我們重部署上面的合約,這回我們使用虛擬容器:
contract OvmTimeShifter {
function getShiftedTime() returns(uint) {
return timestampManager.getOvmTimestamp() + 42;
}
}
如此一來,我們就能夠在驗證 fraud proof 的時候,設定 L1 容器中的 “虛擬區塊高度”,來保證正確的返回值!
這就是 " EVM 中的 EVM " —— OVM 的核心概念:虛擬化所有可能在不同鏈上返回不同結果的 EVM 元件。具體點來說,約有 15 條以太坊指令需要被虛擬化,你可以從以下入口檢視真正的執行管理器長啥樣。
安全性:容器純度檢查
當然我們還需要稍微修改上面的合約,才能真正呼叫 timestamp 容器而不是拿到錯誤的 block.timestamp。
雖然我們解決了結果差異性的問題,但這隻作用於該智慧合約而已。因此,為了保障 L2 的安全性,我們需要確保 L2 上的所有合約都使用了 timestamp 容器,沒有錯誤使用 block.timestamp 的漏網之智慧合約。
OVM 提供了 “容器純度檢查” 的服務 —— 檢查目標智慧合約 “是否只透過執行管理器來呼叫虛擬化指令”,而不允許像是 block.timestamp 這樣的操作!不論有沒有其他智慧合約呼叫了目標合約,只要(目標)合約未透過檢查,就無法部署到 OVM。這樣就能保證 L2 的安全性。
開發體驗:轉譯器
要讓智慧合約只透過執行管理器來呼叫某些指令,還有一個問題就是開發體驗 —— 如果開發者需要遍歷整份智慧合約,然後把所有 block.timestamp 替換為 getOvmTimestamp(),這種費力不討好的活肯定沒人願意做。
為了解決這個問題,我們搭了一個轉譯器 —— 輸入普通 EVM 位元組碼,然後轉譯器會輸出使用上述容器的 OVM 位元組碼。對於使用轉譯器的開發者來說,完全不需要和 OVM 直接打交道 —— 只需要在 Waffle 、Truffle 等你喜歡的測試套件中加入我們的 solc-transpiler 包。
展望
我們認為 OVM 的出現代表著以太坊 L2 的飛躍,因為它不同於變著招 使用 以太坊,它就是以太坊本身的進步。只要加上幾行程式碼,就能夠實現快速且低成本的 Solidity 智慧合約遷移,這也是當前關於以太坊擴充套件方面最令我們興奮的 topic 。如果你想要自行體驗一把,可以關注我們最近的 OVM 測試——在標準的以太坊工具中(如 Graph 和 Burner 錢包),實時執行部分的 Synthetix 複雜交易合約(見此處)。