Rollup Bridge 介紹(七):Optimism 原生橋

NIC Lin
imToken
Published in
17 min readJun 22, 2022

本篇是 Rollup Bridge 介绍系列的第七篇,介紹 Optimism 的原生橋。

Photo by kyler trautner on Unsplash

這個系列一開始從跨 Rollup 橋開始介紹起,直到第七篇才開始回過頭來介紹 Rollup 本身提供的原生橋,還請讀者見諒。

Rollup 原生橋是去中心化跨 Rollup 橋例如 Hop Protocol 的基石,沒有 Rollup 原生橋來協助在 L1 與 L2 之間傳遞訊息,這種跨 Rollup 橋就沒辦法可信的將跨 Rollup 的轉帳訊息從源頭 Rollup 傳遞到目標 Rollup。沒辦法可信的傳遞訊息就表示使用者要仰賴、相信第三方來傳遞訊息,如此就和一般跨 L1 橋的安全性無異,即便這個第三方是一個多簽。

先備知識包含:

  • Ethereum Transaction
  • Solidity、contract call、msg.sender 的概念
  • Optimistic Rollup

Recap: L1 對 Rollup 的重要性

在介紹 Rollup 原生橋之前,先快速複習一下 Rollup 以及 L1 對它的重要性。

Rollup 把交易資料送到鏈上,但把交易執行搬到鏈下,執行完後再把結果(State Root)丟回鏈上。在 Optimistic Rollup 中,都先默認 State Root 是正確的,如果發現錯誤再透過挑戰機制移除錯誤的 State Root。在 ZK Rollup 中則是透過零知識證明來證明 State Root 是經過正確計算得來的。

但不管是 Optimistic Rollup 或是 ZK Rollup,這兩者都需要 L1 來幫他們確保一件事 :交易的順序。Rollup 把交易資料送到鏈上就決定了這些交易的排序,只要 L1 沒有被攻擊、沒有發生 re-org,交易的執行順序就不會改變,執行結果也就一定不會改變。因此 Rollup 的安全性也等同於 L1 的安全性。

交易(藍色區塊)送到 L1 後,大家都能夠以此交易順序自行算出執行結果 state 2

交易紀錄(或更精確地說交易順序)是最重要的,state(執行結果)是其次、是副產品。確定了交易順序,自然能確定執行結果。

接下我們將進入到 Optimism 的協議裡,先從最核心 — 確保交易順序的元件 — Canonical Transaction Chain 介紹,接著是交易資料送到鏈上的方式、訊息在 L1 與 Rollup 之間傳遞的方式、處理另一邊傳過來的訊息的方式,最後是實際透過原生橋傳送訊息的例子。

Canonical Transaction Chain(CTC)

CTC 這個合約就是用來儲存交易紀錄的。當交易一筆一筆提交到 CTC 上,就同時確定了交易彼此之間的順序,這個順序就決定了最新的狀態。Optimism 上的交易會由 Sequencer 打包然後提交到 CTC 裡。另外也有不經由 Sequencer 來送交易的管道,待會會提到。

註:Sequencer 透過 appendSequencerBatch 來打包交易送到 CTC。

另一個合約 State Commitment Chain 則是讓人對每一筆交易去 propose 交易執行後的狀態,例如交易前狀態是我有 10 元,你有 5 元,當一筆我給你 5 元的交易執行後,狀態會變成我有 5 元,你有 10 元。我可以提交一個假的狀態說交易執行後變成我有 15 元,你有 0 元,但大家都會發現這是錯的狀態而去發起挑戰,最後換上正確的狀態。

註:任何人抵押後都可以透過 appendStateBatch來 propose 交易執行後的狀態。

Bob propose 一個錯的 state,任何人都可以去發起挑戰,換上正確的 state

Sequencer and Censorship Resistance

目前 Rollup 幾乎都是採用中心化 Sequencer 的方式,由項目方先擔任唯一 Sequencer,未來再慢慢引入去中心化 Sequncer 的機制。但在這段過渡期我們只能相信 Sequencer 不會作惡,不會故意審查、屏蔽我們的交易嗎?

註:我們不需要相信 Sequencer 會把錢捲走,因為當他 propose 一個錯的狀態時,馬上就會被人發現是錯的狀態並去挑戰,所以安全性是不需要擔心的。但我們必須要擔心 Sequencer 會故意忽略特定使用者的交易(Censorship Attack)。

Force Inclusion

Rollup 必須要有機制讓使用者發現自己交易被屏蔽時,也能有手段強制讓他的交易進到 L1 的交易紀錄中,讓大家都看到他的交易,這個機制稱作 Force Inclusion。在前一段我們有提到 Optimism 在 L1 的 CTC 合約就是用來儲存交易紀錄的,那我們開放讓使用者能夠自己到 CTC 合約去插入交易不就可以讓使用者繞過惡意 Sequencer,來達成 Force Inclusion?沒錯!就是這麼做,但會有些地方需要注意。

我們讓使用者可以自己把交易插入 CTC 的交易紀錄中

如果我們讓使用者可以直接插入交易到隊列中,就有可能被利用來攻擊網路的節點,包含 Sequencer。想像你送一筆交易給 Sequencer 請他打包這筆交易,這筆交易是從你朋友 Bob 身上轉 100 DAI 給你。Sequencer 收到這筆交易並檢查狀態,確認 Bob 身上真的有 100 DAI 且他也允許你這麼做,所以 Sequencer 告訴你交易成功,這筆交易會被收入,於是你開心地繼續準備接下去的交易…

Sequencer confirm 你的交易

但這時 Bob 的壞朋友 Alice 發現這筆交易,於是她直接在 L1 上插入她的交易,這筆交易是她從 Bob 身上轉走 100 DAI 的合法交易(假設 Bob 是個慷慨的人)。因為 Sequencer 還沒把你的交易打包送到 L1,這表示 Alice 已經插入 L1 的交易會發生在你的交易之前。

Alice 直接在 L1 將交易插入 CTC 裡

因此當 Sequencer 發現 Alice 強制插入了這筆交易,Sequencer 必須要重新模擬一次他原本打包到一半的交易們,這時候他就會發現你的交易已經失效了(因為 Bob 的 DAI 已經被 Alice 轉走),於是就會產生一連串骨牌效應導致你原本在準備的後續交易也全都失敗或無效了。這樣的攻擊除了會造成使用者的麻煩,也導致即便是誠實的 Sequencer 也沒辦法確定交易的執行結果。

Sequencer 發現 Alice 的交易排在他本地端準備的交易之前,所以必須重新模擬交易執行
Alice 的交易導致其他交易失敗或無效

Enqueue

所以雖然我們允許使用者可以繞過 Sequencer,自行插入交易,但我們不能讓使用者插入的交易可以即時生效!所以這些交易會先被放在 CTC 的一個 queue 裡(把它當作一個交易的候補隊伍),交易被放進這個 queue 裡代表不會馬上生效,需要等待 Sequeuncer 指示。交易放進 queue 裡讓 Sequencer 先看見並有時間準備,在下一包交易中 Sequencer 就可以指定要額外處理 queue 裡的交易。如此一來使用者有手段可以 Force Inclusion,Sequencer 也可以不被影響。

註:使用者可以透過 enqueue 來將交易放進 queue 裡。Sequencer 在 appendSequencerBatch 時可以指定要額外處理 queue 裡的交易

Alice 的交易不會直接生效,而是先放進 CTC 的 queue 裡
Sequencer 送出下一包的交易時再順便指定要處理幾筆 CTC queue 裡的交易

Where is the Force!?

你可能已經發現:如果還是讓 Sequencer 自己決定要不要額外處理 queue 裡的交易,那 Sequencer 不就還是可以故意不處理交易嗎?沒錯!所以會需要為 queue 的交易加上限制,例如規定 Sequencer 一定要在交易被放進 queue 後的 XX 時間內處理,否則無法再收入交易,或是 Sequencer 每一包交易都要包含 XX 筆 queue 裡的交易。如此一來就能確保交易真的能夠被 “Force” Inclusion。

如果 Sequencer 還是能自己決定要不要處理 CTC queue 裡的交易,就沒辦法 Force Inclusion
其中一個方式:CTC 規定每一包交易一定要處理 XX 筆 queue 裡的交易

但目前 Optimism 還沒有提供這個功能,之前版本的 CTC 原本有提供 Force Inclusion 的功能( appendQueueBatch ),但被拿掉了。這表示我們目前必須得相信 Sequencer 不會審查我們的交易。

以上是對 Optimism 的核心元件 — CTC 的介紹,接下來將介紹搭建在這之上的訊息跨鏈功能。有了訊息跨鏈,我們才能有代幣跨鏈。

Cross-chain Message

這裡的 Cross-chain 指的是 L1與 L2 之間的 Cross-chain。

L1 -> L2 Message

L1 到 L2 的跨鏈訊息的核心和 Force Inclusion 一樣,都是透過上面提到的 enqueue 方式來發送。

enqueue 裡帶的資料很簡單:targetdata,你可以把它比擬做 L1 交易要填的 todata,只是是在 L2 的環境執行而已。但你不需自己去觸發 enqueue 來發送 L1->L2 訊息,Optimism 提供了一套合約來做跨鏈訊息:CrossDomainMessenger 合約。CrossDomainMessenger 合約在 L1 及 L2 各有一個,你可以把它們想像成是 L1 及 L2 之間通訊的傳送門。

L1 Messenger 將傳送訊息給 L2 Messenger,訊息先放進 queue 裡,系統再於 L2 解讀出接收人和訊息

如果你要送一個 L1->L2 訊息,你要透過 L1 CrossDomainMessenger 合約的 sendMessage 函式,指定你訊息要送給誰(target )及內容( data)。L1 CrossDomainMessenger 合約會封裝你的訊息,夾帶在它自己傳給 L2 CrossDomainMessenger 合約的訊息裡,如下圖。L2 CrossDomainMessenger 合約收到 L1 CrossDomainMessenger 合約送來的訊息後,會拆封對方封裝的訊息,去呼叫 target 並帶上 data

L1 Messenger 把使用者訊息(橘色外框)封裝在自己的訊息(綠色外框)裡,等到 L2 Messenger 收到訊息後再拆封並去呼叫使用者指定的 target 合約

注意這邊是由 L2CrossDomainMessenger 去觸發 target,不是你,因為你在 L1 不在 L2。

跨鏈訊息的 msg.sender

這邊你可能會覺得奇怪,如果在 L2 上是由 L2 CrossDomainMessenger 合約來呼叫 Bob 的 L2 合約,那 Bob 的 L2 合約要怎麼知道這訊息的來源?有可能是 Alice 在 L1 送的訊息,或是別人在 L1 送錯訊息,或是其實根本是 Carol 在 L2 去呼叫 Bob 的合約,它要怎麼分辨?

這個就是 CrossDomainMessenger 要協助解決的問題:當 Bob L1 合約透過 L1 CrossDomainMessenger 合約送 L1->L2 訊息時,L1 CrossDomainMessenger 合約會把「sender 是 Bob L1 合約」這個資訊附在 relayMessage 訊息裡,如此 L2 CrossDomainMessenger 合約收到訊息時就可以知道原本發起 L1->L2 訊息的人( sender是 Bob L1 合約。想像剛剛圖中橘色外框的信封裡除了 targetdata 外,L1 CrossDomainMessenger 合約另外在信封上加上 sender 的資訊。

L1 Messenger 為使用者訊息(橘色外框)附加上發送人資訊

L2 CrossDomainMessenger 合約在呼叫 Bob L2 合約之前會先sender 紀錄起來再去觸發 Bob L2 合約,如此一來 Bob L2 合約就可以透過查詢 L2 CrossDomainMessenger 合約的 xDomainMessageSender 變數來查詢 L1->L2 訊息的發起人是誰,如果不是 Bob L1 合約就終止交易,確保只有 Bob L1 合約可以成功送訊息給 Bob L2 合約。

CrossDomainMessenger 合約紀錄相關資訊,確保訊息接收方能驗證訊息來源

如果是 Carol 在 L2 去呼叫 Bob 合約,想偽裝成 L1->L2 訊息,則 Bob 合約只要檢查來呼叫它的人(msg.sender)是 L2 CrossDomainMessenger 合約就好,因為 L2 CrossDomainMessenger 合約沒辦法在 L2 被操控,它只會接收從 L1 CrossDomainMessenger 合約送來的訊息 。

CrossDomainMessenger 合約及 Bob 的 L2 合約都會檢查 caller,所以 Carol 沒辦法偽裝成 Bob 的 L1 合約

L2 -> L1 Message

L2->L1 訊息的流程基本上和 L1->L2 訊息一樣,你可以看到 L1 和 L2 的 CrossDomainMessenger 合約長得很像:sendMessagerelayMessagexDomainMessageSender…。主要的差別就在 L2 -> L1 訊息要等待挑戰期過後才能完成 relay,且要附上 Merkle Proof 證明那筆 L2 ->L1 訊息確實存在(L2 CrossDomainMessenger 在收到 L2->L1 訊息時會把訊息存起來,所以 Merkle Proof 會是用來證明 L2 CrossDomainMessenger 合約的 storage 裡確實有這個 L2->L1 訊息)。

L1->L2 訊息和 L2->L1 訊息流程其實差不多:在一端 send,在另一端 relay

以上是訊息在 L1 及 L2 之間傳遞的方式,接下來會以實際的例子帶過使用方法。

代幣跨鏈只是訊息跨鏈的其中一種

不管是 L1<->L1 還是 L1 <-> L2 的代幣跨鏈,代幣都沒辦法真的從一條鏈移動到另一條鏈。常見的代幣跨鏈方法是在 A 鏈上鎖住代幣,並在 B 鏈上鑄造出新的代幣(反過來則是先在 B 鏈上燒毀代幣,並在 A 鏈上解鎖代幣)。如果我們往後退一步來看,「A 鏈通知 B 鏈去鑄造出新的代幣」這一個動作其實就是在 relay 一個跨鏈訊息:「我這裡鎖住他的代幣了,請在另一邊鑄造出新的代幣給他」、「我在這裡燒毀他的代幣了,請在另一邊解鎖他的代幣」。真正在移動的是訊息,不是代幣。

Optimism 的官方代幣橋

Optimism 官方有提供 L1StandardBridge、L2StandardBridge 合約來提供代幣跨鏈,但你也可以像 Synthetix 一樣自己寫一組代幣橋合約,畢竟核心是訊息的跨鏈,只要透過 CrossDomainMessenger 合約就能完成各種訊息的跨鏈,包含代幣,不需完全仰賴官方的代幣橋(但當然你自己寫的和官方的代幣橋相比下可信度會有差)。

L1StandardBridge.depositERC20

以官方橋的跨 ERC20 代幣為例,首先 L1StandardBridge 合約在創建時要知道 L2StandardBridge 的地址,因為要告訴 L2StandardBridge 代幣已經鎖住,可以放心鑄造對應的代幣了。

使用者呼叫 L1StandardBridge 合約的 depositERC20 函式來啟動代幣跨鏈,函式裡要指定 L1 代幣地址、L2 代幣地址及其他一些資訊,函式裡面會把代幣轉到 L1StandardBridge 合約身上,並送出跨鏈訊息到 L2StandardBridge 合約,並觸發 L2StandardBridge 合約的 finalizeDeposit 函式。

L2StandardBridge 的 finalizeDeposit 函式裡會檢查跨鏈訊息裡提供的 L1 代幣地址是不是符合 L2 代幣合約所記錄的 L1 地址。這表示 L2 代幣合約要紀錄自己對應的 L1 代幣合約。例如 Tether 如果部署代幣合約到 Optimism 上,則它在部署時就要紀錄自己對應的 L1 USDT 地址。如果它 L1 代幣地址不小心填成一個 L1 上的垃圾代幣,那任何持有那個垃圾代幣的人都可以透過代幣橋把 L1 垃圾幣換成 L2 的 USDT。

如果檢查通過,finalizeDeposit 就會鑄造出 L2 代幣給接收者;如果檢查沒有通過,則 L2StandardBridge 會封裝一個 L2->L1 訊息 finalizeERC20Withdrawal,這個訊息會指示 L1StandardBridge 把原先的 L1 代幣解鎖還給對方(不過要等挑戰期結束才能 relay,完成解鎖),也就是原路退還代幣。

L2StandardBridge 檢查通過則 mint 代幣(4a),沒通過則送訊息回去 L1 解鎖代幣(4b)

L2StandardBridge.withdraw

代幣從 L2 回到 L1 的流程基本上就和 L1 到 L2 相反,先燒毀 L2 代幣,再接著送出一個 L2–>L1 訊息,基本上就是上一段提到的 finalizeETHWithdrawal,指示 L1StandardBridge 把 L1 代幣解鎖還給對方。

由 L2StandardBridge 指示 L1StandardBridge 解鎖代幣給使用者

以上就是 Optimism 官方代幣橋的運作方式。如果你要部署代幣合約到 Optimism 上並藉由 官方代幣橋來做代幣跨鏈,請記得一些重要的值例如 L2StandardBridge 地址及 L1 代幣地址不要填錯。

注意事項

L1 -> L2 訊息指定的 gas limit

當你在送出 L1->L2 訊息時,和 L1 交易一樣,你需要順便指定這個訊息在 L2 上執行時的 gas limit。但在那個當下沒辦法知道 L2 實際執行會耗費多少 gas 或是執行結果,這樣要怎麼收手續費?

Optimism 採用的方式是讓使用者在 L1 送出訊息時,就預付一筆手續費,但有一個免費的限額:只要指定的 gas limit 小於 enqueueL2GasPrepaid,Optimism 就不收錢(目前 enqueueL2GasPrepaid 的值是 192w gas)。超過的話,合約裡會用 while loop 去消耗 gas(意即讓使用者用消耗的 L1 gas fee 來代付 L2 gas fee)。

但最重要需要注意的是:gas limit 請一定要指定足夠,合約裡有一個防呆檢查:檢查一個最低值 MIN_ROLLUP_TX_GAS(目前是 10w gas),但如果你的訊息真的到 L2 花超過預期的 gas,那這個訊息就會失敗,而且沒有辦法補救。

瞭解更多

風險提示:本文內容均不構成任何形式的投資意見或建議。 imToken 對本文所提及的第三方服務和產品不做任何保證和承諾,亦不承擔任何責任。數字資產投資有風險,請謹慎評估該等投資風險,咨詢相關專業人士後自行作出決定。

--

--