從理論到實踐:解析以太坊 Rollup 實現抗審查交易的機制

WEEX 唯客博客, 原文標題:《Rollup 的 Force Inclusion 機制介紹》 作者:NIC Lin,Taipei Ethereum Meetup 負責人   就在昨天發生了一起震驚無數人的事情:由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給自己)…

Previous:

Next: