May the Force Inclusion be with you. 這篇文章將會介紹 Rollup 的交易抗審查機制 — Force Inclusion,並以幾個著名 Rollup 的設計與實作為例。
先備知識:
交易抗審查(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 想要審查任何使用者的交易都行。
與其要求 Rollup 要有和 L1 一樣多的驗證者來確保抗審查能力,還不如直接利用 L1 的抗審查能力:Sequencer 也是要將交易資料打包送到 L1 的 Rollup 合約上,不如在 Rollup 合約裡加入一個機制讓使用者也可以插入交易到排序之中,這樣的機制就稱為 Force Inclusion。只要 Sequencer 沒辦法審查使用者的「L1 交易」,它就沒辦法阻止使用者透過 L1 強制插入 Rollup 交易,而 Rollup 的運作及安全性正是奠基於 L1 的抗審查能力。
註:Force Inclusion 機制是要該 Rollup 有設計才會有,使用者並不是一定可以透過 L1 強制插入 Rollup 交易。如果 Rollup 沒有提供 Force Inclusion 機制,那使用者就只能祈禱 Sequencer 不會審查自己交易。
如果我們允許透過 Force Inclusion 插入的交易可以直接寫入到 Rollup 的交易歷史中(也就是立即生效),那 Rollup 的狀態就會馬上被改變,例如 Bob 透過 Force Inclusion 機制插入一筆「他轉 1000 DAI 給 Carol」的交易,如果這筆交易立即生效,那最新的狀態中 Bob 的餘額就會少 1000 DAI 而 Carol 會多 1000 DAI(當然前提是 Bob 餘額超過 1000 DAI)。
如果此時 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。
這可不是一個好的使用者體驗,因此 Rollup 一般都不會讓 Force Inclusion 交易立即生效,而是讓交易先進到一個「準備中」的狀態。Sequencer 可以在打包交易送上來時選擇是否要順便塞入這些「準備中」狀態的交易到批次的最後面,如果 Sequencer 一直都沒有想要處理這些「準備中」的狀態,那這些交易在過了一段時間後就可以被強制插入到交易歷史中。
如果 Sequencer 一直沒有收入等待隊列中的交易,在一段時間後使用者(或是任何人)就可以自己去強制收入。
接下來將依序介紹 Optimism、Arbitrum、StarkNet 及 zkSync 等四個較有名的 Rollup 的 Force Inclusion 機制實作。
首先先介紹 Optimism 的 Deposit 流程,這個 Deposit 不單是指把存錢進 Optimism 的意思,而像是更一般的「把給 L2 的訊息」存進去 L2。L2 節點在接收到新存入的訊息後就會將訊息轉換成一筆 L2 交易去執行,送到訊息指定的接收方。
當一個使用者要把 ETH 或 ERC-20 代幣存進 Optimism 時,他會(透過前端網頁)和 L1 上的 L1StandardBridge 合約互動,指定要存多少數量以及由哪個地址接收等等。接著 L1StandardBridge 會將訊息傳遞至下一層的 L1CrossDomainMessenger 合約,這個合約主要是作為一般通用的 L1 與 L2 之間互相通訊的合約,L1StandardBridge 便是使用這個通用的通訊合約來和 L2 上的 L2StandardBridge 溝通,決定誰可以從 L2 鑄造代幣或是誰可以從 L1 提領代幣。如果開發者需要開發一個 L1 與 L2 之間互通、同步狀態的合約,那他就可以搭建在 L1CrossDomainMessenger 合約之上。
L1CrossDomainMessenger 合約會再將訊息送至最底層的 OptimismPortal 合約,OptimismPortal 合約處理完後會 emit 一個 TransactionDeposited event,event 參數包含「送訊息的人」、「接收訊息的人」,以及相關的執行參數。
接著 L2 的 Optimism 節點會監聽 OptimismPortal 合約所 emit 的 TransactionDeposited event,並把 event 裡的參數轉換為一筆 L2 的交易,這個 L2 交易的發起者就會是 TransactionDeposited event 裡的「送訊息的人」、交易的接收者就會是 event 裡的「接收訊息的人」,其他執行參數也是由 event 參數而來。
例如這一筆是某個使用者透過 L1StandardBridge 合約 Deposit 0.01 ETH 的交易,這個訊息及 ETH 一路傳到 OptimismPortal 合約(地址是 0xbEb5…06Ed),然後幾分鐘後轉換成 L2 交易:訊息發起者是 L1CrossDomainMessenger 合約;接收者是 L2 上的 L2CrossDomainMessenger 合約;附帶 0.01 ETH。
當使用者想要強制收入他的交易進 Optimism 執行時,他並不是想要從 L1 送訊息至 L2,所以他不會去使用 L1CrossDomainMessenger 合約。他要達到的效果是讓一筆原本「他從他的 L2 地址在 L2 上送出並要執行的交易」能順利執行,所以他會直接去和 OptimismPortal 合約互動,而且是以他 L2 地址去呼叫 OptimismPortal 合約,如此到時候 TransactionDeposited event 轉換成的 L2 交易的「交易發起者才會是他的 L2 地址」,才能重現原本那筆交易。
我做了一個簡單的 Force Inclusion 交易:以我 L2 的地址(0xeDc1…6909)轉錢給我自己,並附帶一個 “force inclusion” 的文字訊息。這是我透過 OptimismPortal 合約執行 depositTransaction 函式的 L1 交易,可以看到在 emit 的 TransactionDeposited event 中,from 和 to 都是我自己,剩下的 opaqueData 欄位裡的值(000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000015BE000000000000C35000666F72636520696E636C7573696F6E)則是編碼了「呼叫 depositTransaction 的人附帶了多少 ETH」、「L2 交易發起者要附帶多少 ETH 給 L2 接收者」、「L2 交易 Gas Limit」及「給 L2 接收者的 Data」等等資訊。
將 opaqueData 值的這幾個資訊解碼後分別會得到:
接著沒多久就出現轉換後的 L2 交易:一筆我轉錢給自己的 L2 交易,金額是 5566,Data 是 “force inclusion” 字串。而且可以注意到在圖中倒數第二行的 Other Attributes 中 Txn Type(交易類型)是系統交易 126(System),表示不是我自己在 L2 發起的交易,是由 L1 TransactionDeposited event 轉換而來。
如果使用者要呼叫其他合約、帶不同 Data,那一樣就是將參數一一填入 depositTransaction 函式,只要記得是要用 L2 地址來去 L1 上執行,如此到時候 L2 交易發起者才會是該 L2 地址。
前面提到的 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 交易歷史中,不過至少這勝過交易永遠沒辦法被收入的結果。
在 Optimism 中 L1 的 Deposit 操作會 emit 一個 TransactionDeposited event,剩下的就是等待 Sequencer 收入這個 Deposit 操作;但在 Arbitrum 中 L1 的操作(存錢或傳訊息給 L2 等等)會被存在 L1 合約的一個 Queue 裡,而不是單純只 emit event。而 Sequencer 會被給予一段時間來將這個 Queue 裡的 L1 操作放進交易歷史中,如果時間到了 Sequencer 都沒有作為,那任何人都可以去替 Sequencer 完成。
L1 操作都要經由稱為 Delayed Inbox 的合約,顧名思義這裡的操作都會延遲生效;另一個合約則是 Sequencer Inbox,是給 Sequencer 上傳 L2 交易的介面。Sequencer 上傳的交易會直接寫進交易歷史中,而每次 Sequencer 上傳時都可以選擇要順便從 Delayed Inbox 拿多少個 L1 操作一起寫進交易歷史中,讓這些 L1 操作生效。
如果讀者直接參考 Arbitrum 官方關於 Sequencer 及 Force Inclusion 的章節,會看到裡面提到了 Force Inclusion 大致如何運作,以及一些參數名稱和函式名稱:使用者先去 Inbox 合約呼叫 sendUnsignedTransaction 函式,如果 Sequencer 沒在約 24 小時內收入,那使用者就可以去呼叫 Sequencer Inbox 合約的 forceInclusion 函式。就這樣,連連結也沒有附在裡面,只能自己去看合約程式碼裡相對應的函式。
當找到 sendUnsignedTransaction 函式後,你發現竟然要自己填 nonce 值還有 maxFeePerGas 值。是哪個地址的 nonce?是哪個網路上的 maxFeePerGas 值?要怎麼填比較好?沒有文件紀錄,連 Natpsec 都沒有。然後你還順便發現一堆看似相似的函式:sendL1FundedUnsignedTransaction、sendUnsignedTransactionToFork、sendContractTransaction、sendL1FundedContractTransaction,一樣沒有文件告訴你這些函式的區別、該怎麼用、參數該怎麼填,連 Natpsec 都沒有。
你抱著姑且一試的心態來試填參數並送出交易,想用 trial and error 的方式看能不能找出正確的用法,但發現這些函式全都會把你的 L1 地址做 Address Aliasing,導致最終 L2 上的交易發起人根本是不一樣的地址,於是你的 L2 地址一動也不動。
後來偶然點開 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)是什麼用途、怎麼用、什麼時候用。
很遺憾地,StarkNet 目前還沒有 Force Inclusion 機制。只有兩篇在官方論壇上討論到 Censorship 及 Force Inclusion 的文章。
原本是因為 StarkNet 的零知識證明系統沒辦法證明一筆失敗的交易,所以不能允許 Force Inclusion。因為如果有人惡意(或無意) Force Include 一筆失敗、無法被證明的交易,那 StarkNet 就會直接卡住:因為交易被強制收入後,Prover 就必須證明該筆失敗交易,但它卻沒辦法證明。
而 StarkNet 預期在 v0.15.0 版引入證明失敗交易的功能,之後應該就可以進一步實現 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 中拿出交易寫入交易歷史中。
如果你透過 zkSync 的官方橋去 Deposit ETH,像是這筆交易,它便是去呼叫 MailBox 合約的 requestL2Transaction 函式,它會將這個 Deposit ETH 的 L2 交易放進優先佇列 中並 emit 一個 NewPriorityRequest event。因為合約把 L2 交易資料編碼成一串 bytes 字串所以不易讀,改成看這筆 L1 交易的參數的話,會看到參數中 L2 的接收方也是交易的發起人(因為是 Deposit 給自己),所以過一陣子這筆 L2 交易被 Sequeuncer 從優先佇列拿出並收入進交易歷史中時,它會在 L2 上被轉換成一筆自己轉帳給自己的交易,而轉帳的金額就是交易發起人在 L1 的 Deposit ETH 交易所帶上的 ETH 金額。
接著我直接照本宣科呼叫 requestL2Transaction 函式,送了一筆自己呼叫自己的交易:沒有帶任何 ETH,call data 帶入「force inclusion」字串的 HEX 編碼。 接著它被轉換成 L2 上一筆自己呼叫自己的交易,call data 裡是「force inclusion」字串:0x666f72636520696e636c7573696f6e。
透過 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 機制。
TEM Medium 目前正在進行有獎徵稿!詳情請參考:
Special thanks to Chih-Cheng Liang and Kimi Wu for reviewing and improving this post
Rollup 的 Force Inclusion 機制介紹 was originally published in Taipei Ethereum Meetup on Medium, where people are continuing the conversation by highlighting and responding to this story.
【免责声明】市场有风险,投资需谨慎。本文不构成投资建议,用户应考虑本文中的任何意见、观点或结论是否符合其特定状况。据此投资,责任自负。