作者:NIC Lin,Taipei Ethereum Meetup負責人
原文標題:《Rollup的Force Inclusion機制介紹》
就在昨天發生了一起震驚無數人的事情:由Metamask母公司Consensys推出的以太坊二層Linea主動停機了,官方稱這麼做的目的是為了降低Velocore黑客攻擊事件的影響。而這不由得讓人想起之前BSC鏈(BNB Chain)為了降低駭客攻擊的損失,在官方主動協調下停機一事。每當人們談論起這種事情,都會對Web3倡議的去中心化價值感到懷疑。
當然,上述事件發生的核心原因,更多在於基礎設施本身的不完善,即不夠去中心化:如果一條鏈足夠去中心化,那麼就不該說停就停。由於以太坊二層的獨特構造,大多數Layer2都依賴於中心化的Sequencer,雖然近些年去中心化排序器的論調越來越多,但考慮到二層的存在目的及其結構,我們大可以認為,Layer2的排序器大機率不會有多去中心化,最後可能還比不上BSC鏈的去中心化程度。如果事實真的如此,那我們該怎麼辦?
其實對於二樓而言,排序器不去中心化帶來的最直接危害,在於抗審查性和活性。如果處理交易的實體(Sequencer)很少,那麼它在是否為你服務這件事上就掌握了絕對權力:想拒絕你就拒絕你,而你可能沒有辦法。如何解決Layer2的抗審查問題,顯然是一個重要的議題。
在過去的數年中,各大以太坊二層針對抗審查問題提出了各種各樣的解決方案,例如Loopring和Degate以及StarkEx的強制提款與逃生艙功能、Arbitrum及其他OP Rollup的Force Inclusion功能,這些方法都可以在一定條件下對Sequencer產生製衡,以防止其無端拒絕任意用戶的交易請求。
在今天的文章中,來自台北以太坊協會的NIC Lin現身說法,親自實驗了4個主流Rollup的抗審查交易功能,從工作流程和操作方法等方面深入的分析了Force Inclusion的機制設計,這對於以太坊社群和手握巨額資產的大戶而言尤其具有參考價值。
交易審查與Force Inclusion
交易抗審查性(Censorship Resistance)對一條區塊鏈來說非常重要,如果區塊鏈能夠任意審查並拒絕用戶發起的交易,那就和一個Web2伺服器沒有兩樣。以太坊目前的交易抗審查能力來自於它為數眾多的Validator,如果有人想審查Bob的交易、不讓他的交易上鍊,要么就嘗試買通網絡中大部分Validator,要不就Spam整個網路,不斷送出手續費比Bob更高的垃圾交易來搶佔區塊空間。不管是哪一種方式,成本都會非常高。
註:在Ethereum目前的PBS架構中,審查交易的成本會降低不少,可以參考配合OFAC審查Tornado Cash交易的區塊比例。目前的抗審查能力仰賴在OFAC及政府管轄範圍之外的獨立驗證者及Relay。
但Rollup呢? Rollup不需要一大堆的Validator來確保安全性,即便Rollup只有一個中心化的角色(Sequencer)來產出區塊,它也和L1一樣安全。但安全和抗審查能力是兩回事,即便一個Rollup和以太坊一樣安全,但在只有一個中心化Sequencer的情況下,想審查任何用戶的交易都行。
Sequencer可以拒絕處理用戶的交易,導致用戶資金被扣留無法離開該Rollup
Force Inclusion機制
與其要求Rollup有大量的去中心化的Sequencer,不如直接利用L1的抗審查能力:
原本Sequencer就是要將交易資料打包送到L1的Rollup合約中,不如在合約裡加入一個設計,讓用戶可以自行把交易插入到Rollup合約,這個機制就稱為「Force Inclusion」。只要Sequencer沒辦法在L1層級審查用戶,它就沒辦法阻止用戶在L1強制插入交易。這樣一來,Rollup就可以繼承L1的抗審查能力。
Sequencer無法審查使用者的L1交易,除非付出很高的成本
強制交易該怎麼生效?
如果允許透過Force Inclusion把交易直接寫入Rollup合約中(也就是立即生效),那麼Rollup的狀態就會馬上改變,例如Bob透過Force Inclusion機制插入一筆「轉1000 DAI給Carol」的交易,如果交易立即生效,那麼最新的狀態中Bob的餘額會少1000 DAI,Carol會多1000 DAI。
如果Force Inclusion能直接把交易寫進Rollup合約中並馬上生效,那狀態就會馬上改變
如果此時Sequencer也在鏈下收集交易,並把下一批交易送到Rollup合約上,就有可能被Bob強制插入並立即生效的交易給影響到。這種問題要極力避免,因此Rollup一般不會讓Force Inclusion交易立即生效,而是先讓用戶把交易插入到L1上的等待隊列中,進入「準備中」狀態。
Sequencer在把鏈下交易打包送上Rollup合約時,選擇是否在交易序列裡塞入前述交易,如果Sequencer一直無視這些處於「準備中」狀態的交易,等窗口期結束後,用戶可以把這些交易強制插入到Rollup合約中。
Sequencer可以決定什麼時候「順便收入」等待隊列中的交易
Sequencer還是可以拒絕處理等待佇列中的交易
如果Sequencer長期拒絕,一段時間後任何人都可以透過Force Inclusion功能把交易強行插入到Rollup合約中
接下來我們將依序介紹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合約互動,指定要存多少金額以及由哪個L2地址接收這些資產。
L1StandardBridge合約會將訊息傳遞至下一層的L1CrossDomainMessenger合約,這個合約主要作為L1與L2之間互相通訊的組件,L1StandardBridge便透過這個通用的通訊組件和L2上的L2StandardBridge交流,決定誰可以在L2鑄造代幣,或是誰可以從L1解鎖代幣。
如果開發者需要開發一個在L1與L2之間互通、同步狀態的合約,那麼他就可以搭建在L1CrossDomainMessenger合約之上。
使用者的訊息透過CrossDomainMessenger合約從L1傳遞到L2 註:本文的部分圖片中將CrossDomainMessager寫成了CrossChainMessager
OptimismPortal合約
L1CrossDomainMessenger合約會再將訊息送至最底層的OptimismPortal合約,OptimismPortal合約處理完後會拋出一個名為TransactionDeposited的事件,參數包含“發送訊息的人”、“收訊息的人”,以及相關的執行參數。
接著L2的Optimism節點會監聽OptimismPortal合約拋出的Transaction Deposited事件,並把event裡的參數轉換為一筆L2交易,這個交易的發起者會是Transaction Deposited事件參數裡指明的“發訊息的人”,交易接收者就是事件參數裡“接收訊息的人”,其他交易參數也是由上述事件中的參數而來。
L2節點會將OptimismPortalemit的Transaction Deposited事件參數轉換成一筆L2交易
例如,這是某個用戶透過L1StandardBridge合約存款0.01ETH的交易,這個消息及ETH一路傳到OptimismPortal合約(地址是0xbEb5…06Ed),然後幾分鐘後被轉換成L2交易:
訊息發起者是L1CrossDomainMessenger合約;接收者是L2上的L2CrossDomainMessenger合約;訊息內容是L1StandardBridge收到了BoB的0.01ETH存款。這之後還會觸發一些流程,例如為L2StandardBridge增發0.01枚ETH,再由後者轉給Bob。
具體怎麼觸發
當你想把交易強制收納進Optimism的Rollup合約中時,你要達到的效果是讓一筆「從你的L2地址在L2上發起並要執行的交易」能順利執行,這時你應該用自己的L2地址把訊息直接提交給OptimismPortal合約(注意OptimismPortal合約其實在L1上,但OP的地址格式和L1地址格式一致,你直接用和L2帳戶相同地址的L1帳戶調用上述合約即可)。
之後該合約拋出的Transaction Deposited事件轉化的L2交易的“發起者”,才會是你的L2帳戶,此時交易格式和正常的L2交易一致。
從Transaction Deposited事件轉換而成的L2交易中,發起人會是Bob自己;接收人是Uniswap合約;而且會附帶指定的ETH,就像Bob自己發起L2交易一樣
如果要呼叫Optimism的Force Inclusion功能,你要直接呼叫OptimismPortal合約的depositTransaction函數,將你想要在L2執行的交易的參數填入
我做了一個簡單的Force Inclusion實驗,這項交易想達成這樣一件事:在L2上用我的地址自轉帳(0xeDc1…6909),並附帶一個「force inclusion」的文字訊息。
這是我透過OptimismPortal合約執行depositTransaction函數的L1交易,可以看到在其拋出的Transaction Deposited事件中,from和to都是我自己
剩下的opaque Data一欄的值則編碼了「呼叫deposit Transaction函數的人附帶了多少ETH」、「L2交易發起者要把多少ETH發給接收者」、「L2交易GasLimit」及「給L2接收者的Data」等等訊息。
將上述訊息解碼後分別會得到:
「呼叫deposit Transaction的人附加了多少ETH」:0,因為我並不是從L1存ETH到L2;
「L2交易發起者要把多少ETH發給接收者」:5566(wei)
「L2交易的GasLimit」:50000
「給L2接收者的Data」:0x666f72636520696e636c7573696f6e,也就是「force inclusion」這個字串的16進位編碼
接著沒多久就出現轉換後的L2交易:一筆我轉錢給自己的L2交易,金額是5566 wei,Data是「force inclusion」字符串。而且可以注意到,在圖中倒數第二行的Other Attributes中的TxnType(交易類型),顯示是系統交易126(System),表示這筆交易不是我自己在L2發起的,是由L1交易的Deposited事件轉換而來。
轉換而成的L2交易
如果你要透過Force Inclusion呼叫L2合約、發送不同的Data,那無非就是將參數一一填入前面的deposit Transaction函數,只是要記得,要用和自己L2帳戶相同的L1地址去調用deposit Transaction函數,這樣當Deposited Event轉換為L2交易時,發起者就是你的L2帳戶。
SequencerWindow
前面提到的Optimism L2節點將Transaction Deposited事件轉換成L2交易,其實這個Optimism節點指的是Sequencer,畢竟這關係到交易排序,所以只有Sequencer可以決定何時要將前述事件轉換成L2交易。
當監聽到TransactionDeposited事件時,Sequencer不一定會馬上將event轉換成L2交易,可以有一段延遲,這段時間的最大值稱為SequencerWindow。
目前Optimism主網上的Sequencer Window為24小時,也就是當用戶從L1存入一筆錢或Force Inclusion一筆交易,最糟情況是24小時後才被收入到L2交易歷史中。
Arbitrum的Force Inclusion機制
在Optimism中L1的Deposit操作會拋出一個Transaction Deposited事件,剩下的就是等待Sequencer收錄上述操作;但在Arbitrum中發生於L1的操作(存錢或傳訊息給L2等)會被存在L1上的一個佇列裡,而不是單純拋出個事件。
Sequencer會被給予一段時間將上述隊列裡的交易納入L2交易歷史,如果時間到了Sequencer都沒有作為,那任何人都可以去替Sequencer完成。
Arbitrum會在L1合約維護一個Queue,如果Sequencer沒有主動處理Queue裡的交易,時間到了任何人都可以把Queue裡的交易強制收錄到L2交易歷史中
Arbitrum的設計中,L1上發生的如存款等操作都要經由Delayed Inbox合約,顧名思義這裡的操作都會延遲生效;另一個合約則是Sequencer Inbox,是Sequencer把L2交易上傳到L1時的直接場所。每次Sequencer上傳L2交易時,都可以順便從Delayed Inbox取出一些待處理的交易一併寫進交易歷史中。
Sequencer寫入新交易時可以順便從DelayedInbox拿出交易一起寫入複雜的設計以及凡善可陳的參考資料
如果讀者直接參考Arbitrum官方關於Sequencer及Force Inclusion的章節,會看到裡面提到了Force Inclusion大致如何運作,以及一些參數名稱和函數名稱:
使用者先去DelayedInbox合約呼叫sendUnsignedTransaction函數,如果Sequencer沒在約24小時內收錄,那用戶可以呼叫SequencerInbox合約的forceInclusion函數。然後Arbitrum官方也沒把函數的連結附加在官網文檔裡,只能自己去看合約程式碼裡相對應的函數。
當找到sendUnsignedTransaction函數後,你發現竟然要自己填nonce值還有maxFeePerGas值。是哪個網址的nonce?是哪個網路上的maxFeePerGas?怎麼填比較好?沒有文件參考,連Natpsec都沒有。然後你還會在Arbitrum合約裡發現一堆看著相似的函數:
sendL1FundedUnsignedTransaction、sendUnsignedTransactionToFork、sendContractTransaction、sendL1FundedContractTransaction,一樣沒有檔案告訴你這些函數的差別、該怎麼用、參數該怎麼填,連Natpsec都沒有。
你抱著姑且一試的心態來試填參數並送出交易,想用試錯的方式看能不能找出正確的用法,但發現這些函數全都會把你的L1地址做AddressAliasing,導致最終在L2上發起交易時的Sender根本是不一樣的位址,於是你的L2位址一動也不動。
sendL2Message
後來偶然點開Google搜索,才發現原來Arbitrum自己有一個Tutorial程式庫,裡面有腳本示範怎麼從L1發送L2交易(也就是Force Inclusion的意思),然後它列舉的函數完全不是上面提到的任何一個,而是一個叫sendL2Message的函數,而且message參數要帶入的竟然是用L2帳戶簽完名的交易?
誰會知道要「透過Force Inclusion送給L2的消息」竟然會是一筆「簽完名的L2交易」?而且沒有任何檔案及Natspec解釋什麼時候用及如何使用這個函數。
結論:要手動產生一個Arbitrum的強制交易比較麻煩,建議就照著官方Tutorial跑Arbitrum SDK唄。 Arbitrum不像其他Rollup有清楚的開發者文件及程式碼附註,許多函數的用途和參數缺乏說明,導致開發者得花費比預期多更多的時間來接入和使用。我也在Arbitrum Discord上詢問Arbitrum的人,但並沒有得到令人滿意的答案。
在Discord上詢問,對方也只會叫我去看sendL2Message,沒有想要解釋其他函數的功能(甚至是Force Inclusion文件裡提到的sendUnsignedTransaction)是什麼用途、怎麼用、什麼時候用。
StarkNet的ForceInclusion機制
很遺憾地,StarkNet目前還沒有ForceInclusion機制。只有兩篇在官方論壇上討論到Censorship及ForceInclusion的文章。
無法證明失敗的交易
上述原因其實是因為,StarkNet的零知識證明系統沒辦法證明一筆失敗的交易,所以不能允許Force Inclusion。因為如果有人惡意(或無意)Force Include一筆失敗的、無法被證明的交易,那麼StarkNet就會直接卡住:因為交易被強制收入後,Prover就必須證明該筆失敗交易,但它卻沒辦法證明。
而StarkNet預期在v0.15.0版引入證明失敗交易的功能,之後應該可以進一步實現Force Inclusion機制。
zkSync的ForceInclusion機制
zkSync的L1->L2訊息傳送以及Force Inclusion機制,都是透過MailBox合約的requestL2Transaction函數進行,使用者指定L2位址、calldata、附加的ETH數量、L2GasLimit值等,requestL2Transaction會將這些參數組合成一個L2交易,然後放進優先隊列(PriorityQueue)中,Sequencer會在交易打包上傳到L1時(透過commitBatches函數),說明要順便從優先隊列中拿出多少筆交易一起收錄進L2交易記錄中。
zkSync在Force Inclusion形式上和Optimism很像,都是以發起者的L2位址(與L1位址一致)去呼叫相關函數,並填入資料(被呼叫者、calldata等等),而不是像Arbitrum一樣是填一筆簽完名的L2交易;但在設計上則是和Arbitrum一樣,都是在L1維護一個隊列Queue,並由Sequencer從Queue中拿出用戶直接提交的待處理交易,並寫入交易歷史中。
如果你透過zkSync的官方橋接Deposit ETH,像是這筆交易,它便是去呼叫MailBox合約的requestL2Transaction函數,它會將這個Deposit ETH的L2交易放進優先隊列中拋出一個NewPriorityRequest事件。因為合約把L2交易資料編碼成一串bytes字符串所以不易讀,改成看這筆L1交易的參數的話,會看到參數中L2的接收方也是交易的發起人(因為是Deposit給自己),所以過一陣子這筆L2交易被Sequeuncer從優先隊列拿出,並收錄進交易歷史時,它會在L2上被轉換成一筆自己轉給自己的交易,而轉帳的金額就是交易發起人在L1的Deposit ETH交易中帶的ETH金額。
L1Deposit交易中,交易發起者和接收者都是0xeDc1…6909,金額是0.03ETH,calldata為空
L2上會出現一筆0xeDc1…6909自己轉帳給自己的交易,交易類型(TxnType)是255,也就是係統交易
接著我直接像之前實驗OP的強制交易功能一樣,呼叫zkSync的requestL2Transaction函數,發了一筆自轉帳:沒有帶任何ETH,calldata帶入「force inclusion」字符串的HEX編碼。
接著它被轉換成L2上一筆自己轉自己的交易,calldata裡是「force inclusion」的十六進位字串:0x666f72636520696e636c7573696f6e。
當Sequencer把交易從PriorityQueue拿出來並寫入交易歷史中,在L2上就會轉換成相對應的L2交易
透過requestL2Transaction函數,使用者可以用和L2位址一樣的L1帳戶,在L1提交資料,指定L2接收方、附帶的ETH金額以及calldata。如果使用者要call其他合約、帶不同Data,那一樣就是將參數一一填入requestL2Transaction函數。
還沒有讓使用者強制收錄的功能
雖然L2交易放到優先佇列後,會順便計算出這筆L2交易被Sequencer收錄的等待期限,但目前zkSync設計中並沒有讓使用者能強制執行的Force Inclusion函數,等於是只做半套。也就是雖然有“收錄等待期限”,但實際上還是“看Sequencer要不要收入”:Sequencer可以等到過期後才收入,也可以永遠不再收入優先隊列中任何交易。
未來zkSync應該要加入相關函數,讓使用者可以在收入有效期過了但都還沒被Sequeuncer收錄時,能強制把交易包含進L2交易歷史,如此才是真正有效的Force Inclusion機制。
總結
L1靠為數眾多的驗證者們來確保網路的“安全性”及“抗審查能力”,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稱為DelayedInbox;zkSync稱為PriorityQueue
但zkSync送出L2交易的方式和Optimism比較像,都是以L2地址去L1發送訊息,如此轉換為L2交易後,其發起人才會是該L2地址。 Optimism送L2交易的函數稱為depositTransaction;zkSync稱為requestL2Transaction。而Arbitrum則是產生一筆完整的L2交易並簽名,然後透過sendL2Message函數送出,Arbitrum在L2上會透過簽名還原簽署者來作為L2交易的發起人。
StarkNet目前還沒有Force Inclusion機制;zkSync則是像做了半套的Force Inclusion,—有PriorityQueue且每個Queue裡的L2交易都有收錄有效期限,但這個有效期限目前只是裝飾用,實際上Sequencer可以選擇完全不收入任何PriorityQueue裡的L2交易