ENS 如何實現互操作性?了解以太坊Layer 2 通用橋


對於ENS 等應用來說,如何能以信任最小化的方式從某個系統中檢索數據,且不需要所有Layer 2 方案的客戶端存儲全部數據?

原文標題:《引介| 一種以太坊Layer 2 的通用橋》
撰文:Nick Johnson
翻譯:阿劍

隨著走向成熟的以太坊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 系統交互、以及如何為合約的用途而格式化數據。

在該模型下,獲得數據的過程分三步:

引介| 一種以太坊Layer 2 的通用橋

  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 的源代碼都可以在 這裡 找到。

來源鏈接:medium.com

.



Source link