Optimistic Rollup 的挑戰機制(一):Optimism OVM 1.0

NIC Lin
Taipei Ethereum Meetup
12 min readApr 22, 2022
https://medium.com/ethereum-optimism/ovm-deep-dive-a300d1085f52

本文將介紹 Optimistic Rollup 之一 Optimism 挑戰機制的演進。預備知識包含

  • 區塊鏈客戶端是什麼,例如 Geth
  • State Transition Function 的意思
  • EVM 大致的運作方式,opcodes
  • Merkle Tree、Merkle Proof

首先先快速複習一下什麼是 Optimistic Rollup(以下簡稱 OR),如果你已經熟悉 OR 可以直接跳過這一段,到下一段的「分開 Data 和 Execution」

什麼是 Optimistic Rollup?

什麼是 Rollup?

可以先看這篇了解 L2 的演進及 Rollup 介紹

另外這一個投影片有非常詳盡的 L2 演化介紹:

那什麼是 Optimistic Rollup?

交易都會送到 L1 上,所以任何人都可以自己從初始狀態(Initial State),將所有交易依序帶進去執行 State Transition Function,得到最新的狀態。

我們平常都相信 Operator 是誠實的、送到 L1 宣稱的狀態是正確的狀態,但當 Operator 送了一個假的狀態上去,宣稱在這個狀態裡所有錢都是他的時候,我們可以知道 Operator 在說謊,也知道正確的狀態是長怎樣。因此我們可以請 L1 合約當仲裁者,並提供證據給合約,證明正確的狀態該長怎樣。

「平常都相信 Operator 是誠實的」就是 Optimistic 的精神。

而 L1 合約要怎麼做出仲裁呢?這就是我們今天的主題:挑戰機制。

分開 Data 和 Execution

平常 L1 不會管你 L2 執行了什麼交易、做了什麼事,它就是保管 Operator 收集並送上來的每一筆交易:它不知道 L2 的最新狀態長怎樣,但它知道 L2 所有交易的順序。

就像每個人都可以在鏈下自己透過初始狀態、交易和 State Transition Function 算出最新狀態,L1 合約在有需要(即有人申請挑戰)的時候也能做一樣的事情:算出最新狀態(因為它也有初始狀態和所有歷史交易),而這個狀態就會是正確的狀態,因此它就可以做出決定,看是 Operator 說謊還是挑戰者在胡鬧。

那這樣 L1 不就會需要知道 L2 的 State Transition Function?沒錯,或是反過來,L2 配合 L1:如果 L1 是 EVM,L2 就用 Solidity 來寫、用 EVM 來執行 State Transition Function,這樣 L1 合約就可以不費吹灰之力重演 L2 的 State Transition Function。

OVM 1.0:EVM compatible

在進入 Optimism 第一代之前,先介紹一下過去在 L1 模擬 L2 執行環境的嘗試:EVM inside EVM。

EVM inside EVM

用 Solidity 模擬出 EVM 執行,聽起來挺酷的吧,而且真的可行。最初 EVM-in-EVM 是想說可以在 EVM 裡去模擬執行其他 EVM 鏈的交易。

不過其實「其他 EVM 鏈」這個東西也可以換成一個 L2 或甚至是更 general 的、不一定要是一條鏈、一筆交易,它可以單純是用 EVM 來做運算的一個計算過程(例如用 EVM 來做加減乘除)。或乾脆不用 EVM,其他 VM 也可以,只要你能在 EVM 上模擬出那個 VM 的 opcode。這就是 Truebit 這個協議在做的事。

以下是 EVM-in-EVM 的實作範例:

不過 EVM-in-EVM 這個做法 Gas 消耗會高很多:ADD opcode 在 EVM 只消耗 3 gas,但你用 Solidity 模擬出來的 ADD 的 gas 消耗會是數百倍,因為你還要模擬出 Stack!其他更複雜的 opcode 還會包含模擬出 memory 和 storage。

所以 EVM-in-EVM 不是拿來重新模擬執行一筆完整的 L1 交易用的,而是用來執行一小部分的 opcode。以 Truebit 為例,只有在被挑戰的時候,雙方才會來回比對尋找各自的執行是在哪一個步驟(哪一個 opcode)開始分歧的,找到了那個 opcode 後再請鏈上合約執行一遍,決定誰才是贏家。

註:接下來會以 Interactive Proving 來代稱這種雙方來回比對找出分歧的步驟,並以特定步驟的執行結果決定勝負的模式。

OVM 1.0 的挑戰機制:一筆交易定勝負

即便有 Truebit 這個前輩示範如何透過 Interactive Proving 來解決挑戰,OVM 1.0 還是決定走上自己的路:他們想要在一筆 L1 交易內執行完所有步驟,決定勝負。如此他們就可以不需設計 Interactive Proving 這個複雜的遊戲,而且也不需要等待雙方來回交互的時間,畢竟你不能要求每個人要在十個區塊內就做出下一步、要求被挑戰後雙方要馬上連續在線十小時來解決挑戰,太沒人性了( Interactive Prooving 通常會耗時很久)。

剛剛有說到 EVM-in-EVM 的方式太耗 gas 了,如果 OVM 1.0 用這個方式要在一筆 L1 交易執行完驗證是不可能的,除非你的 L2 交易都很簡單。因此 OVM 1.0 不用 EVM-in-EVM 的做法,而是直接把 L2 合約直接部署在 L1,並直接執行該筆交易(不過當然是發生挑戰的時候才會這樣做,平常 L1 不會知道 L2 發生什麼事),也就是不用模擬了,在有需要的時候直接執行一次。

假設你今天在 L2 透過 Uniswap 換了一大筆錢,但 Operator 宣稱這筆交易的結果是他獲得了這筆錢,這時候你去 L1 挑戰這筆交易的執行結果,挑戰機制就會在 L1 部署原本的 Uniswap 合約(包含在這筆交易所有執行過的合約),並重新執行一次你的那筆交易。

如果你平常有在寫 Solidity,你可能就已經皺起你的眉頭了:可是在 L1 執行的那筆交易,人事時地物都不一樣了(除了合約代碼一樣),這怎麼可能還原的出原本的執行結果!?而這就要帶出這個挑戰機制最大的工程難度了:你必須要對合約編譯完的 opcode 動手腳,你要把原本的 CALLERSLOADSSTOREBLOCKTIME 等等會因 時空背景不同 而產生不一樣結果的 opcode 全都換成你客製過的另一坨 opcode,讓這一坨客製 opcode 在 L1 執行時能夠做一些特別處理,來達成原本被換掉的那個 opcode 的作用。

以 SLOAD 為例

L1 和 L2 的 state 是不一樣的,所以直接在 L1 上執行 SLOAD 和原本在 L2 執行 SLOAD 會是不一樣的結果。

所以 OVM Compiler 會把 SLOAD 換成一坨客製化 opcode,我們把這一坨叫做 ovmSLOADovmSLOAD 要做的就是在 L1 還原當時 L2 SLOAD 會讀取出來的值。

那這個值要怎麼來?L1 又不知道 L2 的 state?答案是:挑戰雙方其中一方要自己去提供這個值(下圖中綠色的 Supplied state),而且是預先提供好,沒提供就去執行 L2 交易的話交易會失敗。也就是說你會先送一筆 L1 交易去提供、準備好該筆 L2 交易會用到的所有 state(看交易裡用到幾次 SLOADBLOCKTIME 等等的),然後才送下一筆 L1 交易去執行 L2 交易。

簡化過的流程,ExecutionManager.ovmSLOAD() 會去 Supplied state 裡抓回原本 SLOAD 該讀出來的值

但要是你提供一個錯誤的、對你有利的 state,是不是可以影響執行結果?答案是不行,你提供 state 時要順便附上 Merkle Proof,證明這個 state。例如:你提供 state 說地址 0x1234 在 L2 Block 9999 時的餘額是 20 ETH,你就要提供 Merkle Proof 證明 0x1234 在 L2 Block 9999 的 state tree 裡的餘額真的是 20 ETH。

我上面這一段的解釋還是很粗淺,有興趣更深入了解的人可以閱讀以下兩篇(第二篇細節更多):

但這樣做不是沒有代價的,而且這個代價可不小。

Optimism 1.0 沈重的包袱

首先上面講到的客製 opcode 表示你不能隨便拿正常 Solidity Compiler 編譯出來的結果直接部署到 Optimism 上,你必須要用 Optimism 改過的 Solidity Compiler 來編譯,而且 Optimism 系統會檢查、拒絕部署不合規則的合約代碼(Safety Checker)。

這裡有 OVM 和 EVM 不相容地方的更詳細介紹:OVM/EVM incompatibilities and difference

另外客戶端例如 Geth 也需要改寫,因為 Optimism 引入了一些不同於 L1 的規則:ETH 變成一個 ERC20 的系統合約、所有地址全都是合約(抽象帳戶)、一個區塊只放一筆交易(否則 gas 消耗會導致挑戰機制沒辦法在一筆 L1 交易內完成)。

這些改變都代表開發者在 L1 熟悉的開發工具沒辦法直接在 Optimism 上使用,需要工具改寫來支援。

再來是合約大小的問題:L1 目前的部署合約大小限制是 24KB 左右,許多協議的合約都必須要靠刪減代碼、拆分合約才能符合這個限制。而 Optimism 的客製 opcode 會讓合約大小平均上升 15% 以上,這代表你 Optimism 的大小實際限制會是 20.8 KB 左右或更少,因為你的合約要在膨脹 15% 後還能符合 L1 小於 24KB 的要求。

最後是近期揭露關於 OVM 1.0 的漏洞。
第一個漏洞的起因在於 Optimism 團隊必須改寫 Geth,再加上上面說到 Optimism 引入的不同於 L1 的規則,導致改寫後的 SELFDESTRUCT opcode 在執行時沒有把狀態清理乾淨,造成能無限鑄造 ETH 的漏洞。詳細可以看底下這篇(揭露的白帽最後獲得了兩百萬美元的賞金):

第二個漏洞來自於一名白帽發現 Optimism 節點在 L2 執行交易時的 gas 消耗、gas 記帳規則和 L1 上的挑戰機制合約寫的不一樣,而且 Optimism 節點執行交易時只有執行節點本身的代碼,而沒有把交易丟進挑戰機制實際去模擬一遍並比對結果(因為他們相信節點本身代碼的執行和挑戰機制寫的是一樣的)。
所以攻擊者可以送一筆交易使其在 L2 節點執行結果得到 A(節點不會懷疑這個結果),但當他到 L1 挑戰時算出來的結果卻會是 B,導致狀態必須回滾,完成雙花攻擊。

裡面有更多種攻擊手段,但原理都是一樣的,詳情可以參考:

From EVM Compatible to EVM Equivalence

以上是 OVM 1.0 的挑戰機制介紹,它的特色是 EVM Compatible,意思就是大部分 EVM 的代碼、工具都可以沿用,但請預期會有些許不一樣的地方。

OVM 2.0 則是對整個協議做了大改,包含挑戰機制,目的就是為了變成 EVM equivalence,這個意思就是所有 EVM 的代碼、工具都可以直接沿用(連 Geth 也是)!

下一篇將介紹 OVM 2.0 的挑戰機制。

Special thanks to Cyan Ho and Kimi Wu for reviewing and improving this post

--

--