一種以太坊 Layer-2 的通用橋

買賣虛擬貨幣
隨著走向成熟的以太坊 Layer-2 解決方案多了起來,ENS 也要能為整個生態系統提供服務,同時讓 ENS 使用者能夠獲得 Layer-2 解決方案給他們帶來的效率提升。自 Vitalik 的一篇帖子提出了一種可能的方法之後,ENS 團隊和廣大的 ENS 和 L2 社羣也一直在開發一種通用的 “Layer-2 橋”,讓包括 ENS 在內的應用,能夠以免信任的方式在多個鏈下信源處檢索資料,進而使跨平臺的互操作性成為可能。在 10 月 27 號最新的一次工作會議上,我演示了這個想法的一個初步實現。本文中我會詳細講解這種解決方案。目標概要來說,Layer-2 和其它相關係統的工作原理都是減少與以太坊互動的需要,它們將原本需要在鏈上儲存和訪問的 狀態 移到了別的地方,同時,保證在以太坊區塊鏈上有足夠多的資訊能驗證資料的正確性。舉個例子,在 Rollup 這種常見的方案中,(Rollup 的)狀態會儲存在另外一個系統中,只有 witness 資料例如默克爾根會儲存在以太坊區塊鏈上(譯者注:作者此處的舉例不夠完整,witness 還包括使用者交易的原始資料)。有了這些 witness 資料和 Layer-2 解決方案的訪問權,一個參與者就可以構建出對任意保護在 Layer-2 系統中的資料的有效性證明,並且可以由以太坊來驗證。這個定義比大多數人所認為的 “Layer-2” 要更加廣泛 —— 它還包括了其它一些減少鏈上資料儲存的工具,比如使用賬戶餘額默克爾樹的空投(airdrop),以及會觸發事件但並不在鏈上儲存餘額的代幣。對於 ENS 和其它應用來說,關鍵問題在於,在一個存在許多互不相容的 Layer-2 方案的世界裡,如何能以信任最小化的方式 —— 也就是不引入任何新的信任假設 —— 從某個系統中檢索資料,且不需要變成所有 Layer-2 方案的客戶端、自己來儲存可能有用的資料 。
一個幼稚的方法是,要求所有的系統都使用同樣的 witness 資料格式。但這一點是不可能的,兩個原因:第一,witness 資料的格式和型別都高度依賴於相關係統的實現細節,ZK Rollup 和 Optimistic Rollup 使用的元件必定不同;第二,客戶端仍然無法實際獲得資料。實用的方法必須滿足下列條件:· 客戶端不需要為它們可能與之互動的每一個系統提供顯式支援。· 客戶端必須能夠驗證返回的資料是有效的,最好無需引入除相關 L2 方案自帶假設以外的信任模型。· 解決方案不會要求接入的 L2 平臺產生結構性的變更。· 第三方必須能夠為 L2 平臺開發介面,無需平臺維護者的支援和參與。
解決方案概覽我們提議的方案的核心是一種標準化的工具,讓客戶端能夠從一個外部系統 —— 一個閘道器服務 —— 處檢索資料;以及一種標準化的方法,來驗證返回的資料是正確的。相應地,這裡有兩個主要的組成部分:第一個,是一個放在以太坊 Layer-1 上的智慧合約,向客戶端提供一個發現閘道器並驗證閘道器響應正確性的工具;第二個,是一個閘道器服務,理解如何與給定的 L2 系統互動、以及如何為合約的用途而格式化資料。

在該模型下,獲得資料的過程分三步:

1. 向合約發出查詢資料的請求。合約並不直接返回所需的結果,而是返回兩個值:一個 閘道器 URL,以及一個 calldata 字首。

2. 向該閘道器傳送一個 HTTP POST 請求,請求與第一步中相同的資料。閘道器返回一個不透明值(opaque value),resolver(解析器) calldata。驗證該 解析器 calldata 的起始位就是第一步中得到的 calldata 字首。

3. 查詢合約,或者與之互動,提供第二步中得到的 解析器 calldata ,合約驗證該資料的有效性,如果有效的話,返回 結果 或者執行交易。

因為負責理解如何與 L2 互動的是閘道器服務,所以這樣一種簡單的協議就可以讓客戶端從鏈下獲得資料,並且不需要讓客戶端理解任何與 L2 相關的東西。為了使用這套系統,每一個應用都需要為自己意向互動的 L2 實現並部署一個閘道器服務和一個驗證合約。在大部分使用,這些閘道器可以是非常通用的,降低了在不同應用間重複勞動的負擔。

重要的是,這三個步驟的流程在呼叫者處可以完全抽象掉;一個理解這個協議的庫就可以讓整個流程看起來跟一個常規的 web3 合約呼叫一般無二,也就是說,不僅應用不需要知道自己在跟哪個 L2 互動,它們甚至完全不知道自己是在跟 L2 互動!

閘道器返回錯誤或者誤導性結果的能力受到協議本身的限制。合約所實現的驗證邏輯保證了任何無效的結果都會在第三步被發現,同時,合約在第一步中返回的字首,在第二步中得到驗證;這些都放置了閘道器用對某一次查詢有效的答案來回應另一次查詢。

工作案例

我們可以用一個預載入了一組餘額的 ERC20 token 合約,以及一個本身是簡單靜態默克爾樹的 “Layer-2” 來演示這條系統在實踐中是如何運作的:

contract PreloadedToken is ERC20 {
  mapping(address=>uint) preload;
  function claimableBalance(address addr) external view returns(uint) {
    return preload[addr];
  }
  function claim(address addr) external {
    if(preload[addr] > 0) {
      _mint(addr, preload[addr]);
      preload[addr] = 0;
    }
  }
}

這個簡單的解決方案有一個顯而易見的問題:部署者必須在部署時將所有餘額填充到 preload 對映中,這是一種非常昂貴的操作。他們會更願意把資料儲存在鏈下,然後讓能夠證明自己擁有餘額的使用者來提取自己的數額。用默克爾樹很容易就能實現這一點:

contract PreloadedToken is ERC20 {
  bytes32 merkleRoot;
  mapping(address=>bool) claimed;
  function claimableBalanceWithProof(address addr, uint balance, bytes proof) external view returns(uint) {
    require(verifyProof(keccak256(addr, balance), proof));
    if(!claimed[addr]) {
      return balance;
    }
    return 0;
  }
  function claimWithProof(address addr, uint balance, bytes proof) external {
    require(verifyProof(keccak256(addr, balance), proof);
    if(claimed[addr]) {
      return;
    }
    _mint(addr, balance);
    claimed[addr] = true;
  }
}

(為了簡化,我們省略掉了 verifyProof (驗證證明功能)的實現)

這個方法非常有效,合約的作者也不再需要花費大量的 eth 來預載入所有餘額,一個默克爾根就足夠了,而且呼叫者想申領餘額的時候,可以自己支付證明 token 所有權的開銷。

不過,現在呼叫者必須理解生成證明的具體流程,並且知道要到哪兒去獲取餘額清單來生成自己賬戶的證明。如果我們可以把第一個方案的介面(方便),與第二個方案的效率結合起來,那就完美了。這就是我們的方案。

首先,我們加入了匹配初始 claim 的簽名和 claimbleBalance 的方法:

string gateway;
  function claimableBalance(address addr) external view returns(bytes prefix, string url) {
    return (abi.encodeWithSelector(claimableBalanceWithProof.selector, addr), gateway);
  }
  function claim(address addr) external view returns(bytes prefix, string url) {
    return (abi.encodeWithSelector(claimWithProof.selector, addr), gateway);

這些函式的呼叫者可以得到兩個值:第一個值是一個後續 callback 的字首;第二個值是一個閘道器服務的 URL。該字首保證了兩件事:callback 會用相關的 proof 函式來響應,並且其第一個引數會是所提供的地址。這防止了閘道器用給另一個地址的資料來響應請求。
接下來,我們需要實現一個閘道器服務來,可以滿足客戶端的查詢請求。以 claim1 為例,很直接就能實現:

const args = tokenInterface.decodeFunctionData("claim", data);
const balance = balances[args.addr];
const proof = merkleTree.getProof(addr, balance);
return merkleInterface.encodeFunctionData("claimWithProof", [args.addr, balance, proof]);

(再一次,為了簡潔,我們假設已經有了包括 getProof 函式在內的合適實現)
這裡的閘道器服務只需要為客戶端所傳送的 claim 呼叫解碼函式呼叫資料,組裝一個證明 —— 或者,在一個實際的 L2 方案中,參考 L2 來組裝出一個證明 —— 然後將結果編碼放在對 claimWithProof 的呼叫中,返回給客戶端。

最後,客戶端驗證返回的 calldata 是否以合約所斷言的字首開始,如果是,則使用交易傳送 calldata 給合約。

claimableBalance 的實現也差不多,只是客戶端使用 calldata 來呼叫合約,將返回值作為呼叫的最終結果。

安全考慮和信任模型

假設客戶端信任了原始合約 —— 我們的意思是,期望該合約會以特定的方式執行,而這可以透過檢查它釋出的原始碼來驗證 —— 那麼這個系統就不會引入任何新的信任假設。雖然閘道器的響應是一個外部流程,但其不良行為的範圍僅限於拒絕服務。

首先,如果我們信任合約,我們同樣也會信任它來制定一個閘道器 URL 來回應我們的查詢請求。其次,我們也可以信任它來實現充分的驗證、保證閘道器的響應是準確的,既可以透過在第一步中指定 calldata 字首、也可以透過在最後一步中驗證閘道器的響應來保證。

因此,一個嘗試用不正確的值來響應的閘道器 —— 無論是提交了不正確的資料,還是不正確的證明 —— 都會被執行驗證步驟的合約發現。一個嘗試正確響應、但使用非使用者所發出請求的對應結果來響應的閘道器,會在使用者的 calldata 字首檢查中發現。客戶端可以透過檢查合約的行為來保證這些 —— 或者依賴於某些人對合約的檢查 —— 都可以在開始互動前實現。

閘道器可以完全拒絕響應,也就是拒絕服務,而且這種情況確實可能因為閘道器惡意或者故障而發生。因為這一點,我們提議,任意最終規範,都應該讓使用者易於 fork 服務,並提供自己的閘道器;就像現在使用者能夠 fork dApp 的前端一樣。

ENS 應用

ENS 使用這套系統也會相對直接一些。解析器可以實現本文所述的協議,用於解析任何的資料欄位,然後每一個希望支援 ENS 資料的儲存和檢索的 L2 都可以部署新的解析器實現和相應的閘道器。希望使用 L2 的使用者只需儲存自己的記錄到合適的 L2 中,並在以太坊上傳送一筆一次性的交易來指定相關的解析器地址,來使用自己的域名。

為了讓這個方案更通用,ENS 也應該改進,以支援某種形式的萬用字元解析(wildcard resolution),使得搜尋域名失敗時會向解析器諮詢該域名的父域名 —— 如果 “foo.example.eth” 不存在,那客戶端就會在解析器內搜尋 “example.eth”。這一功能使得其它系統可以儲存 ENS 的整個子樹,而不僅僅是單個域名的記錄。

未解決的問題

· 雖然某些應用(比如 ENS )可以從合約指定閘道器 URL 所創造的額外間接層中獲益,另一些應用,比如上文所示的 token 合約,最好把這些編碼為該合約 ABI 的一部分來,使得使用者更容易 fork。一個終極的解決方案最好能支援兩種選擇,且不會強加不必要的負擔。

· 目前,客戶端無法分別出一個返回無效 calldata(例如提供一個無效的證明)的閘道器和一個無論如何都會回滾的呼叫。需要作出一些規定來區分這兩種情況 —— 舉個例子,如果證明資料的驗證不透過的話,要求合約使用一個特定的回滾理由。

· 它需要一個比 “以太坊 L2 通用橋” 更吸引人的名字。

自己試試

我文章所有 demo 的原始碼都可以在這裡找到。

免責聲明:

  1. 本文版權歸原作者所有,僅代表作者本人觀點,不代表鏈報觀點或立場。
  2. 如發現文章、圖片等侵權行爲,侵權責任將由作者本人承擔。
  3. 鏈報僅提供相關項目信息,不構成任何投資建議

推荐阅读

;