Rollup 的 Force Inclusion 機制介紹

NIC Lin
Taipei Ethereum Meetup
27 min readMay 10, 2024

--

May the Force Inclusion be with you. 這篇文章將會介紹 Rollup 的交易抗審查機制 — Force Inclusion,並以幾個著名 Rollup 的設計與實作為例。

Photo by Artur Tumasjan on Unsplash

先備知識:

  • 知道 Rollup 的運作機制以及為什麼 Rollup 會需要把交易上傳到 L1 上

交易的抗審查能力

交易抗審查(Censorship Resistance)的能力對一條區塊鏈來說非常重要,如果一條區塊鏈能夠任意審查使用者交易,那這條區塊鏈就和一個 Web2 伺服器沒有兩樣。Ethereum 目前的交易抗審查能力來自於它為數眾多的驗證者們,如果 Alice 想要審查 Bob 的交易、不讓他的交易上鏈,那 Alice 要不得嘗試買通網路中每一個驗證者,要不得 Spam 整個網路、不斷送出手續費比 Bob 交易高的垃圾交易來塞滿區塊。不管是哪一個方式,她的成本都會非常高。

註:在 Ethereum 目前的 PBS 架構中,審查的成本其實降低不少,可以參考配合 OFAC 審查 Tornado Cash 交易的區塊比例。當前的抗審查能力仰賴在 OFAC 及政府管轄範圍之外的獨立驗證者及 Relay。

但 Rollup 呢?Rollup 不需要一大堆的驗證者來確保它的安全性,即便 Rollup 只有一個中心化的角色(Sequencer,稱為排序者或排序器)來產出區塊,它也和 L1 一樣安全。但安全和抗審查能力是兩回事,即便一個 Rollup 和 Ethereum 一樣安全,卻只有一個中心化 Sequencer,那該 Sequencer 想要審查任何使用者的交易都行。

Sequencer 可以拒絕收入使用者交易,導致使用者無法使用也無法離開該 Rollup

Force Inclusion 機制

與其要求 Rollup 要有和 L1 一樣多的驗證者來確保抗審查能力,還不如直接利用 L1 的抗審查能力:Sequencer 也是要將交易資料打包送到 L1 的 Rollup 合約上,不如在 Rollup 合約裡加入一個機制讓使用者也可以插入交易到排序之中,這樣的機制就稱為 Force Inclusion。只要 Sequencer 沒辦法審查使用者的「L1 交易」,它就沒辦法阻止使用者透過 L1 強制插入 Rollup 交易,而 Rollup 的運作及安全性正是奠基於 L1 的抗審查能力。

Sequencer 無法審查使用者的 L1 交易,除非付出很高的成本

註:Force Inclusion 機制是要該 Rollup 有設計才會有,使用者並不是一定可以透過 L1 強制插入 Rollup 交易。如果 Rollup 沒有提供 Force Inclusion 機制,那使用者就只能祈禱 Sequencer 不會審查自己交易。

交易立即生效 vs. 延遲生效

如果我們允許透過 Force Inclusion 插入的交易可以直接寫入到 Rollup 的交易歷史中(也就是立即生效),那 Rollup 的狀態就會馬上被改變,例如 Bob 透過 Force Inclusion 機制插入一筆「他轉 1000 DAI 給 Carol」的交易,如果這筆交易立即生效,那最新的狀態中 Bob 的餘額就會少 1000 DAI 而 Carol 會多 1000 DAI(當然前提是 Bob 餘額超過 1000 DAI)。

如果 Bob 交易能直接被寫進交易歷史中,馬上生效,那 Rollup 的狀態就會馬上改變

如果此時 Sequencer 也在鏈下搜集交易,等著把下一批交易送到 Rollup 合約上,那就有可能被 Bob 強制插入並立即生效的交易給影響到。例如 Bob 其實事先送了一筆「他轉 1000 DAI 給 Alice」的交易到 Sequencer 那,Sequencer 當下驗證沒問題並承諾會收入,Alice 和 Bob 都可以向 Sequencer 查詢交易是否會被收入並且計算出最新狀態(Alice 多 1000 DAI,Bob 少 1000 DAI)。但等到 Sequencer 發現 Bob 搶先一步去強制插入交易後,它手上的交易的執行狀態都已改變,原本那筆「Bob 轉 1000 DAI 給 Alice」的交易已經變成會執行失敗,Alice 也拿不到 1000 DAI。

Sequencer 收入 Bob 的交易,Alice 因此相信自己會收到 1000 DAI
結果 Bob 直接在 L1 上強制收入另一筆衝突的交易,造成 Sequencer 手上的交易不能被收入,Alice 也拿不到 DAI

這可不是一個好的使用者體驗,因此 Rollup 一般都不會讓 Force Inclusion 交易立即生效,而是讓交易先進到一個「準備中」的狀態。Sequencer 可以在打包交易送上來時選擇是否要順便塞入這些「準備中」狀態的交易到批次的最後面,如果 Sequencer 一直都沒有想要處理這些「準備中」的狀態,那這些交易在過了一段時間後就可以被強制插入到交易歷史中。

Bob 想透過強制收入交易來騙 Alice,但實際上交易會先進到等待隊列
Sequencer 可以自己決定在什麼時候「順便收入」等待隊列中交易,所以 Bob 給 Alice DAI 的交易會先執行

如果 Sequencer 一直沒有收入等待隊列中的交易,在一段時間後使用者(或是任何人)就可以自己去強制收入。

Sequencer 拒絕收入 Bob 交易,所以 Bob 從 L1 將交易放進等待隊列中
Sequencer 還是可以持續拒絕收入等待隊列中的交易一段時間
但 Sequencer 無法永遠拒絕收入等待隊列中的交易,一段時間後任何人都可以觸發強制收入

接下來將依序介紹 Optimism、Arbitrum、StarkNet 及 zkSync 等四個較有名的 Rollup 的 Force Inclusion 機制實作。

Optimism 的 Force Inclusion 機制

首先先介紹 Optimism 的 Deposit 流程,這個 Deposit 不單是指把存錢進 Optimism 的意思,而像是更一般的「把給 L2 的訊息」存進去 L2。L2 節點在接收到新存入的訊息後就會將訊息轉換成一筆 L2 交易去執行,送到訊息指定的接收方。

使用者從 L1 Deposit 給 L2 的訊息

L1CrossDomainMessenger 合約

當一個使用者要把 ETH 或 ERC-20 代幣存進 Optimism 時,他會(透過前端網頁)和 L1 上的 L1StandardBridge 合約互動,指定要存多少數量以及由哪個地址接收等等。接著 L1StandardBridge 會將訊息傳遞至下一層的 L1CrossDomainMessenger 合約,這個合約主要是作為一般通用的 L1 與 L2 之間互相通訊的合約,L1StandardBridge 便是使用這個通用的通訊合約來和 L2 上的 L2StandardBridge 溝通,決定誰可以從 L2 鑄造代幣或是誰可以從 L1 提領代幣。如果開發者需要開發一個 L1 與 L2 之間互通、同步狀態的合約,那他就可以搭建在 L1CrossDomainMessenger 合約之上。

使用者的訊息透過 CrossChainMessenger 合約從 L1 傳遞到 L2

OptimismPortal 合約

L1CrossDomainMessenger 合約會再將訊息送至最底層的 OptimismPortal 合約,OptimismPortal 合約處理完後會 emit 一個 TransactionDeposited event,event 參數包含「送訊息的人」、「接收訊息的人」,以及相關的執行參數。

接著 L2 的 Optimism 節點會監聽 OptimismPortal 合約所 emit 的 TransactionDeposited event,並把 event 裡的參數轉換為一筆 L2 的交易,這個 L2 交易的發起者就會是 TransactionDeposited event 裡的「送訊息的人」、交易的接收者就會是 event 裡的「接收訊息的人」,其他執行參數也是由 event 參數而來。

L2 節點會將 OptimismPortal emit 的 TransactionDeposited event 轉換成一筆 L2 交易

例如這一筆是某個使用者透過 L1StandardBridge 合約 Deposit 0.01 ETH 的交易,這個訊息及 ETH 一路傳到 OptimismPortal 合約(地址是 0xbEb5…06Ed),然後幾分鐘後轉換成 L2 交易:訊息發起者是 L1CrossDomainMessenger 合約;接收者是 L2 上的 L2CrossDomainMessenger 合約;附帶 0.01 ETH。

Force Inclusion

當使用者想要強制收入他的交易進 Optimism 執行時,他並不是想要從 L1 送訊息至 L2,所以他不會去使用 L1CrossDomainMessenger 合約。他要達到的效果是讓一筆原本「他從他的 L2 地址在 L2 上送出並要執行的交易」能順利執行,所以他會直接去和 OptimismPortal 合約互動,而且是以他 L2 地址去呼叫 OptimismPortal 合約,如此到時候 TransactionDeposited event 轉換成的 L2 交易的「交易發起者才會是他的 L2 地址」,才能重現原本那筆交易。

從 TransactionDeposited event 轉換而成的 L2 交易中,發起人會是 Bob 自己;接收人是 Uniswap 合約;而且會附帶指定的 ETH,就像 Bob 自己發起 L2 交易一樣
用 L2 地址呼叫 OptimismPortal 合約的 depositTransaction 函式,並將原本 L2 交易的參數一一填入

我做了一個簡單的 Force Inclusion 交易:以我 L2 的地址(0xeDc1…6909)轉錢給我自己,並附帶一個 “force inclusion” 的文字訊息。這是我透過 OptimismPortal 合約執行 depositTransaction 函式的 L1 交易,可以看到在 emit 的 TransactionDeposited event 中,fromto 都是我自己,剩下的 opaqueData 欄位裡的值(000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000015BE000000000000C35000666F72636520696E636C7573696F6E)則是編碼了「呼叫 depositTransaction 的人附帶了多少 ETH」、「L2 交易發起者要附帶多少 ETH 給 L2 接收者」、「L2 交易 Gas Limit」及「給 L2 接收者的 Data」等等資訊

這筆 L1 交易 emit 的 TransactionDeposited event

opaqueData 值的這幾個資訊解碼後分別會得到:

  • 「呼叫 depositTransaction 的人附帶了多少 ETH」:0,因為我沒有要從 L1 Deposit ETH 進到 L2
  • 「L2 交易發起者要附帶多少 ETH 給 L2 接收者」:5566(wei),因為我要轉錢給我自己
  • 「L2 交易 Gas Limit」:50000
  • 「給 L2 接收者的 Data」:0x666f72636520696e636c7573696f6e,也就是 “force inclusion” 這個字串的 hex 編碼

接著沒多久就出現轉換後的 L2 交易:一筆我轉錢給自己的 L2 交易,金額是 5566,Data 是 “force inclusion” 字串。而且可以注意到在圖中倒數第二行的 Other AttributesTxn Type(交易類型)是系統交易 126(System),表示不是我自己在 L2 發起的交易,是由 L1 TransactionDeposited event 轉換而來。

轉換而成的 L2 交易

如果使用者要呼叫其他合約、帶不同 Data,那一樣就是將參數一一填入 depositTransaction 函式,只要記得是要用 L2 地址來去 L1 上執行,如此到時候 L2 交易發起者才會是該 L2 地址。

Sequencer Window

前面提到的 Optimism L2 節點將 TransactionDeposited event 轉換成 L2 交易,其實這個 Optimism 節點指的是 Sequencer 節點,畢竟這攸關交易排序,所以只有 Sequencer 可以決定何時要轉換成 L2 交易。在監聽到 TransactionDeposited event 時,Sequencer 並不一定會馬上將 event 轉換成 L2 交易,而是可以有一段時間決定要何時轉換,這段時間稱為 Sequencer Window,目前 Optimism 主網上的 Sequencer Window 為 24 小時,也就是當使用者從 L1 Deposit 一筆錢或一個訊息,或是 Force Include 一筆交易時,最糟情況會是 24 小時後才被正式收入進 L2 交易歷史中,不過至少這勝過交易永遠沒辦法被收入的結果。

Arbitrum 的 Force Inclusion 機制

在 Optimism 中 L1 的 Deposit 操作會 emit 一個 TransactionDeposited event,剩下的就是等待 Sequencer 收入這個 Deposit 操作;但在 Arbitrum 中 L1 的操作(存錢或傳訊息給 L2 等等)會被存在 L1 合約的一個 Queue 裡,而不是單純只 emit event。而 Sequencer 會被給予一段時間來將這個 Queue 裡的 L1 操作放進交易歷史中,如果時間到了 Sequencer 都沒有作為,那任何人都可以去替 Sequencer 完成。

Arbitrum 會在 L1 合約維護一個 Queue,如果 Sequencer 沒有主動收入 Queue 裡的交易,時間到了任何人都可以強制收入 Queue 裡的交易進交易歷史中

L1 操作都要經由稱為 Delayed Inbox 的合約,顧名思義這裡的操作都會延遲生效;另一個合約則是 Sequencer Inbox,是給 Sequencer 上傳 L2 交易的介面。Sequencer 上傳的交易會直接寫進交易歷史中,而每次 Sequencer 上傳時都可以選擇要順便從 Delayed Inbox 拿多少個 L1 操作一起寫進交易歷史中,讓這些 L1 操作生效。

SequencerInbox 裡是交易歷史,一般只有 Sequencer 可以直接寫入新交易;DelayedInbox 裡則是等待被收入的交易
Sequencer 寫入新交易時可以順便從 DelayedInbox 拿出交易一起寫入

複雜的設計以及凡善可陳的文件

如果讀者直接參考 Arbitrum 官方關於 Sequencer 及 Force Inclusion 的章節,會看到裡面提到了 Force Inclusion 大致如何運作,以及一些參數名稱和函式名稱:使用者先去 Inbox 合約呼叫 sendUnsignedTransaction 函式,如果 Sequencer 沒在約 24 小時內收入,那使用者就可以去呼叫 Sequencer Inbox 合約的 forceInclusion 函式。就這樣,連連結也沒有附在裡面,只能自己去看合約程式碼裡相對應的函式。

當找到 sendUnsignedTransaction 函式後,你發現竟然要自己填 nonce 值還有 maxFeePerGas 值。是哪個地址的 nonce?是哪個網路上的 maxFeePerGas 值?要怎麼填比較好?沒有文件紀錄,連 Natpsec 都沒有。然後你還順便發現一堆看似相似的函式:sendL1FundedUnsignedTransactionsendUnsignedTransactionToForksendContractTransactionsendL1FundedContractTransaction,一樣沒有文件告訴你這些函式的區別、該怎麼用、參數該怎麼填,連 Natpsec 都沒有。

WTF? src

你抱著姑且一試的心態來試填參數並送出交易,想用 trial and error 的方式看能不能找出正確的用法,但發現這些函式全都會把你的 L1 地址做 Address Aliasing,導致最終 L2 上的交易發起人根本是不一樣的地址,於是你的 L2 地址一動也不動。

sendL2Message

後來偶然點開 Google 搜尋頁面中的某個連結才發現原來 Arbitrum 自己有一個 Tutorial 程式庫,裡面有腳本示範怎麼從 L1 送 L2 交易(也就是 Force Inclusion 的意思),然後它戳的函式完全不是上面提到的任何一個,而是一個叫 sendL2Message函式,而且 message 參數要帶入的竟然是簽完名的 Arbitrum L2 交易???誰會知道要「送給 L2 的訊息」竟然會是一筆「簽完名的 L2 交易」??而且再一次,沒有任何文件及 Natspec 解釋什麼時候用及如何使用這個函式。

結論:要手動產生一個 Arbitrum 的強制收入交易比較麻煩,建議就照著官方 Tutorial 跑 Arbitrum SDK 唄。Arbitrum 不像其他 Rollup 有清楚的開發者文件及程式碼附註,許多函式的用途和參數缺乏說明,導致開發者得花費比預期多更多的時間來接入和使用。我也在 Arbitrum Discord 上詢問 Arbitrum 的人,但並沒有得到令人滿意的答案。

註:在 Discord 上詢問,對方也只會叫我去看 sendL2Message,沒有想要解釋其他函式(甚至是 Force Inclusion 文件裡提到的 sendUnsignedTransaction)是什麼用途、怎麼用、什麼時候用。

JUST DO IT. src

StarkNet 的 Force Inclusion 機制

很遺憾地,StarkNet 目前還沒有 Force Inclusion 機制。只有兩篇在官方論壇上討論到 CensorshipForce Inclusion 的文章。

無法證明失敗的交易

原本是因為 StarkNet 的零知識證明系統沒辦法證明一筆失敗的交易,所以不能允許 Force Inclusion。因為如果有人惡意(或無意) Force Include 一筆失敗、無法被證明的交易,那 StarkNet 就會直接卡住:因為交易被強制收入後,Prover 就必須證明該筆失敗交易,但它卻沒辦法證明。

而 StarkNet 預期在 v0.15.0 版引入證明失敗交易的功能,之後應該就可以進一步實現 Force Inclusion 機制。

zkSync 的 Force Inclusion 機制

zkSync 的 L1->L2 訊息傳送以及 Force Inclusion 機制都是透過 MailBox 合約的 requestL2Transaction 函式進行,使用者指定要呼叫的 L2 地址、call data、帶上的 ETH 數量、L2 Gas Limit 值等,requestL2Transaction 會將這些參數組合成一個 L2 交易然後放進優先佇列(Priority Queue) 中,Sequencer 會在交易打包上傳到 L1 時(commitBatches 函式)指定要順便從優先佇列中拿出多少筆交易一起收入

zkSync 在 Force Inclusion 介面上和 Optimism 很像,都是以使用者的 L2 地址去呼叫,並填入相關資料(被呼叫者、call data 等等),而不是像 Arbitrum 一樣是填一筆簽完名的 L2 交易;但在設計上則是和 Arbitrum 一樣都是在 L1 維護一個實體的 Queue,並由 Sequencer 從 Queue 中拿出交易寫入交易歷史中。

使用者透過 requestL2Transaction 插入 L2 交易到優先佇列中,Sequencer 在 commitBatches 時可以從優先佇列中順便拿出交易

如果你透過 zkSync 的官方橋去 Deposit ETH,像是這筆交易,它便是去呼叫 MailBox 合約的 requestL2Transaction 函式,它會將這個 Deposit ETH 的 L2 交易放進優先佇列 中並 emit 一個 NewPriorityRequest event。因為合約把 L2 交易資料編碼成一串 bytes 字串所以不易讀,改成看這筆 L1 交易的參數的話,會看到參數中 L2 的接收方也是交易的發起人(因為是 Deposit 給自己),所以過一陣子這筆 L2 交易被 Sequeuncer 從優先佇列拿出並收入進交易歷史中時,它會在 L2 上被轉換成一筆自己轉帳給自己的交易,而轉帳的金額就是交易發起人在 L1 的 Deposit ETH 交易所帶上的 ETH 金額。

L1 Deposit 交易中,交易發起者和接收者都是 0xeDc1…6909,金額是 0.03 ETH,call data 為空
L2 上會出現一筆 0xeDc1…6909 自己轉帳給自己的交易,交易類型(Txn Type)是 255,也就是系統交易

接著我直接照本宣科呼叫 requestL2Transaction 函式,送了一筆自己呼叫自己的交易:沒有帶任何 ETH,call data 帶入「force inclusion」字串的 HEX 編碼。 接著它被轉換成 L2 上一筆自己呼叫自己的交易,call data 裡是「force inclusion」字串:0x666f72636520696e636c7573696f6e

當 Sequencer 把交易從 Priority Queue 拿出來並寫進交易歷史中,在 L2 上就會轉換成相對應的 L2 交易

透過 requestL2Transaction 函式,使用者可以以 L2 地址在 L1 請求 L2 交易,指定 L2 接收方、帶上的 ETH 金額以及 call data。如果使用者要呼叫其他合約、帶不同 Data,那一樣就是將參數一一填入 requestL2Transaction 函式,只要記得是要用 L2 地址來去 L1 上執行,如此到時候 L2 交易發起者才會是該 L2 地址。

還沒有讓使用者強制收入的功能

雖然 L2 交易放到優先佇列中會順便計算出這筆 L2 交易要被 Sequencer 收入的期限,但目前 zkSync 設計中並沒有讓使用者能自己執行的 Force Inclusion 相關函式,等於是做半套。也就是雖然有「收入有效期限」,但實際上還是「看 Sequencer 要不要收入」:Sequencer 可以等到過期後才收入,也可以永遠不再收入優先佇列中任何交易。未來 zkSync 應該要加入相關函式,讓使用者可以在收入有效期過了但都還沒被 Sequeuncer 收入時,能強制收入,如此才是真正有效的 Force Inclusion 機制。

總結

  • L1 靠為數眾多的驗證者們來確保網路的「安全性」及「抗審查能力」,而 Rollup 則是靠「L1 的抗審查能力」來獲得「和 L1 一樣的安全性」,但不包含 Rollup 的抗審查能力
  • 相反地,Rollup 因為都是由少數或甚至單一的 Sequencer 來寫入交易,抗審查能力反而更弱。因此 Rollup 需要有 Force Inclusion 機制來讓使用者可以繞過 Sequencer,將交易寫入歷史中,避免被 Sequencer 審查而無法使用也無法離開該 Rollup
  • Force Inclusion 讓使用者可以強制將交易寫入歷史中,但在設計上需在「交易是否能立即插入歷史、立即生效」上做選擇。如果允許交易立即生效,那就會造成 Sequencer 的麻煩和使用體驗上的困難,因為 L2 上等待被收入的交易都可能會被 L1 強制收入的交易所影響
  • 因此目前 Rollup 的 Force Inclusion 機制都會先讓 L1 上插入的交易先進到一個等待狀態中,並讓 Sequencer 有一段時間窗口來反應、來選擇要不要收入這些等待中的交易
  • zkSync 和 Arbitrum 都是在 L1 維護一個實體的 Queue,用來管理使用者從 L1 送出的 L2 交易或給 L2 的訊息。Arbitrum 稱為 Delayed Inbox;zkSync 稱為 Priority Queue
  • 但 zkSync 送出 L2 交易的方式和 Optimism 比較像,都是以 L2 地址去 L1 上發送,如此 L2 交易的發起人才會是該 L2 地址。Optimism 送 L2 交易的函式稱為 depositTransaction;zkSync 稱為 requestL2Transaction。而 Arbitrum 則是製作一筆完整的 L2 交易並簽名,然後透過 sendL2Message 函式送出,Arbitrum 在 L2 上會透過簽名還原簽章者來作為 L2 交易的發起人
  • StarkNet 目前還沒有 Force Inclusion 機制;zkSync 則是像做了半套的 Force Inclusion — 有 Priority Queue 且每個 Queue 裡的 L2 交易都有收入有效期限,但這個有效期限目前只是裝飾用,實際上 Sequencer 可以選擇完全不再收入任何 Priority Queue 裡的 L2 交易

參考資料與推薦延伸閱讀

Optimism Force Inclusion

Arbitrum Force Inclusion

StarkNet Force Inclusion

zkSync Force Inclusion

TEM Medium 2024 有獎徵稿

TEM Medium 目前正在進行有獎徵稿!詳情請參考:

Special thanks to Chih-Cheng Liang and Kimi Wu for reviewing and improving this post

--

--