以 EIP-3668 進行鏈下 / 跨鏈資料傳遞

ChiHaoLu
Taipei Ethereum Meetup
18 min readAug 19, 2022

--

本文主要介紹 EIP-3668 這個作為智能合約將鏈下資料無需信任地取回的一種標準。

Table of Cotents

  • Introduction
  • Cast an eye over EIP-3668
  • Overview with Example
  • System Design: Contract, Gateway and Client
  • Discussion
  • Closing

Introduction

EIP-3668: CCIP Read: Secure offchain data retrieval 由 ENS 的工程師 Nick Johnson 於 2020–07–19 提出。EIP-3668 作為智能合約將鏈下資料無需信任地取回的一種標準,可以讓合約與客戶端不用知道底層儲存空間(或 Layer2 合約)的每個細節就能 fetch 資料,進而達到將應用程式取得鏈下資料整個過程模組化的用途。

例如我們可能需要與存有 merkle tree 的空投資料庫互動時(tree 裡面會放著接收者們的相關資訊),或由 Layer1 查看 Layer2 上儲存的資訊時,就能夠使用 EIP-3668 這樣的方法來無需信任地取得鏈下/跨鏈資料。

Background & Roles

Durin 和 CCIP 在 EIP-3668 中時常交替使用,Durin 為 EIP-3668 的俗名,代表著這樣一個無須信任地取得鏈下資料的機制,過程中不需要額外的信任假設,是 EIP-3668 這個 standards 的原創名詞(討論提及)。之後皆改稱為 Cross-Chain Interoperability Protocol (CCIP),同時 chainlink 與 ethers.js 都有 CCIP 相關的實作與套件,內部程式碼和 EIP-3668 系統設計如出一轍。

在 EIP-3668 中的三個角色:

  • Client: 為一個 process,可以是前端 dapp 也可以是後端服務,是一個希望可以取得區塊鏈資訊或使用區塊鏈服務的使用者的操作介面或進程,client 必須知道怎麼使用 CCIP-read 來取得區塊鏈資料或達到其他目的。
  • Contract 為在任一區塊鏈上的智能合約。
  • Gateway 為可以回答 CCIP-read query 的服務,通常是 Client 向 gateway 發起 HTTP request。

Cast an eye over EIP-3668

Intro.

主要是讓合約可以查看 off-chain 資料再決定之後的步驟,而不是直接回傳資料給 client。如果 client 端支援 EIP-3668 並且實作了 OffchainLookup 的 revert catch,便可以利用原先合約呼叫回傳的 URLs 來對 gateway 進行 POST/GET request。最後 client 得到 gateway 的 response 後供原先合約驗證。

Motivation

EIP-3668 的發起動機是對 Ethereum 的應用開發者來說,如何最小化地儲存資料和把交易手續費降到最低是最為重要的事情,解決方案有很多種,例如:L2 Solution、IPFS、中心化 DB、Hashing、Recursive Hashing(Merkle Trees / Tries)。

開發者們大多會選擇特別為這些方案「客製」鏈下資料的取得方法,然而當鏈下的儲存空間滿了或者使用者需要「多個(種)儲存空間」時,特別再為每種方案設計一套資料取得方法就會變得過於累贅和不夠彈性。

Requirement

最主要的一個想法是把資料從當前的鏈上移動到其他地方,而 Ethereum 只需要驗證資料正確性。

舉例來說某個系統(e.g. Rollup)儲存了各種資料的 Merkle Tree,而像是 Merkle Root 存在 L1 鏈上就好。任何人和 L2 互動後都可以產生資料真的儲存在 L2 的 Proof(例如一個 Merkle Proof),並且到 Ethereum 這個 L1 上比對 Merkle Root 來驗證。

問題在於如何建立一個在多個不同的應用程式都適用的標準達到模組化的效果,並且把信任建立的門檻最小化。

最直觀的方法可能是建立一個相同標準的 witness data,然而這會出現兩個問題:一個是 witness data 的格式與系統建置的細節高度相關,例如 EVM-compatible 與否會影響 merkle proof 的 hash function 該如何實作;二是 client 想要 fetch data 時可能會遇到很大的困難(自己產生 witness data 的門檻頗高)。

於是在思考解決方法的時候,提出者希望可以滿足以下的需求。但這邊的設計需求不代表實作 EIP-3668 的系統一定要完全符合,可以根據各種情況 trade-off 各種特性:

Data Availability

  • 將資料儲存在 Layer2 並且可以達到鏈上資料可取得性

Trust

  • 不需要其他的信任假設

Client developer-friendly

  • 如果資料是存在 L2 的話,Clients 不必知道 request L2 的資料的細節步驟
  • Client 不需要特別為了不同的生態系或 Gateway 系統建置屬於它們的 interface
  • Clients 必須可以驗證回傳的資料是正確的

Scalability

  • 希望這個方法不只限於 EVM-compatible 的區塊鏈
  • 這個方法必須避免因為不同的 L2 方案而進行結構性的調整
  • 任意第三方可以自行建立對 L2 生態系的 interface 而不須成為系統地開發者或者維護者
  • Layer1 Contract 不需要去 commit 或者更新任何資料

Overview

在 EIP-3668 的設計上主要由三個 CCIP(Cross-Chain Interoperability Protocol,跨鏈協同工作通訊標準/協議) Read 請求組成。這邊為了更好的講述流程,於是提供一個 Client 查看 Contract 上 Balance 的例子:

第一步:Client 向合約發起請求:

  • 當 Client 向合約呼叫 operation 時:例如呼叫 balanceOf(addr) 後,便接收到 revert

這個 revert 包含特定的 error 資訊:一個 URL 和一些未來兩個步驟會需要資訊:revert OffchainData(sender, urls, callData, callbackFunction, extraData),待會在 Contract Interface 會詳細介紹這個 error type。

第二步:Client 以合約回傳的 URL 找到 gateway 發起請求:

  • Client 會把 callData 附帶在請求中向 Gateway 發起:HTTP request (sender, callData)
  • Gateway 去查詢資料儲存處或相關動作之後,會回傳一個資訊:Response (result)。這邊我們先把重點放在合約部分,這個 request 如何實作待下文解釋。

第三步:Client 以前兩步取得的資料向合約發起請求(read function)或交易(write function):

  • Client 再次以第一步回傳的 extraData 和第二步的 result 向合約發起交易,也就是調用callbackFunction(result, extraData)
  • callbackFunction 中會解碼接受到的資料並且驗證這個 response,回傳相對應的結果或對狀態進行改變,當然也有可能是回傳一個新的(其他) error 然後重新回到第一步

這邊的 responseextraData 概念上便是 proof information。

接下來分別介紹三個角色的實作介紹與設計重點。

Contract Interface

當現在有一個 Durin-enabled(CCIP-enabled) contract,無論是戳合約中的哪個函式,revert 的 error 都必須是同一個格式(介面標準):

1. sender: 合約地址;用於辨別回傳這個 error 的到底是用戶去呼叫的合約,還是混雜在 nested call 中的其他合約。

2. urls: 儲存可以使用 gateways 服務的 URL List;這些 URLs 指向的 gateways 需要實作 CCIP read protocol 並且可以計算 query 的 answer

  • 可為空陣列,當需要 client 自行使用 URLs 時。
  • 理論上要使用哪個 URLs 取決於 client,但合約回傳時必須以一個 priority queue 的方式回傳,最重要的在最前面。
  • 關於 URL 的格式下文 Gateway Interface 部分會講述。

3. callData: 為將來要呼叫 gateway 的附帶 data,這個值通常是 ABI-encoded。

4. callbackFunction: 合約未來要被呼叫的 callback function 的 4-byte function selector:bytes4(keccak256("<callbackFxName()>"))

5. extraData:作為未來 callback 需要的資料。這個值要被 client 一直保留著並在最後呼叫 callback function 時傳遞,過程中也不可以被修改。

在合約中必須實作 callback 方法以便對 gateway 回傳來的資料進行解碼和驗證,基本上就是針對其回傳訊息依條件去執行相對應的函式。函式名稱可以自訂,但 signature 的參數部分必須包含 (bytes response, bytes extraData)

在 nested call 的情況下也可以在 callbackFunction 中實作 revert OffchainLookup 詳細內容可看這裡

當 client 成功地呼叫了 gateway,在 OffchainLookup error 中指定的 callback function 就會被依序執行,將 response 改為 gateway 回傳的值,以及將 extraData 設為合約中 OffchainLookup error 回傳的 extraData

OffchainLookup中需要合約地址作為 sender 一起包進 error msg 的原因是如果我們有多個 Layer1 合約對單個 Gateway service 的設計,或者多對多的設計時。

只要 Layer1 contracts 們達到模組化,傳遞合約的 address 作為 sender 給 gateway 就可以達到更高效率的作用,gateway 在設計上就不用針對每個合約都進行特別處理,因為在相同的 interface 中就可以讓這個 gateway 服務多個合約。

設計重點

  1. 當前 client 還沒辦法辨別 gateway 是傳了不合法的 calldata 導致 client 進行 callback function 呼叫時發生錯誤。抑或者是 callback function 中的其它 revert error,所以 contract 裡面最好針對各種錯誤有明確的 revert reason,例如提供錯誤的 proof 時回傳 proof data failed 之類的訊息。
  2. 如果 gateway 是使用 sign message 的方式簽章傳遞的 response 則在 callback function 中可以使用 ecrecover 重新計算出 signer address。
  3. 如果 gateway 是使用 merkle proof 當作 response,則 contract 中需要儲存 merkle_root 以及在 callback function 中實作 merkleProofVerify()
  4. Gateway URLs 是 priority array,因此在提供或調用這個 array 時都需要從第一個元素開始依序嘗試

Gateway Interface

Client 呼叫的合約有可能回傳各種形式的 URLs,以下只定義資料回傳的形式(JSON 格式內容)。原文提供的例子為 OffchainLookup 回傳的內容,URL 部分由全小寫且 0x 開頭的十六進位的 sender,以及 0x 開頭的十六進位 callData 兩者組成:

則 URL query 的目的地為: https://example.com/gateway/0xaabbccddeeaabbccddeeaabbccddeeaabbccddee/0x00112233.json.

使用 GET request 可以最小化 gateway 與 client 的實作和呼叫過程,在某些情況下 gateway 可以單純是一個 HTTP Server 或者是由文字檔組成的 IPFS static instance。為了符合更複雜的 CCIP 使用情況,也可以使用 POST data 這個選項。

設計重點

  1. 使用 CCIP-read server framework 可以直接建立一個 CCIP-read gateway 的 server 框架。
  2. 無論是向 static files、IPFS、DataBase 還是 Layer2 Solution fetch 資料,Gateway 可能需要經過 sign 或者 merkle proof 等方式將 result 傳回給 Client。
  3. 常見 HTTP Status Code
  • 404: Gateway 不支持這個 sender address(完全錯誤)
  • 400: callData 是一個不合法的格式(長度錯誤)
  • 500: HTTP Server 遇到內部的錯誤

Client Lookup Protocol

上述已經介紹了整個系統三個角色中的兩個,現在要來探討最後一個 client 的角度。

一個支援 CCIP read 的客戶端在發起 contract call 時必須符合以下的步驟:

  1. 將 Lookup Protocol 建置在一個遍歷(例如:for 迴圈)裡面
  2. try 完 contract call 以後失敗則 catch error 並嘗試 HTTP Request
  3. 若成功下一次遍歷就會再 try contract call

而 Lookup Protocol 的實作過程(包在遍歷之中)如下:

a. 建置交易參數並呼叫合約函式: call(to, calldata)

  • 成功:return
  • 失敗:error.msg 非 OffchainLookup -> throw error
  • 失敗:error.msg 為 OffchainLookup -> catch 後進入下一步

b. 將 error.msg 中的 data 們傳遞 HTTP GET request 到目標 URL

c. Check 回傳的 HTTP Status Code:

  • 200–299: return result
  • 400–499: return error
  • 500–599: try another URL

綜合以上可以接著來看以下的 pseudocode 範例:

設計重點

  1. 如果我們想要預先檢查交易(通常是 write function)是否會成功時,需要利用 Client Lookup Protocol 提到的步驟,只是在最後一步替換使用 transaction 和 call (eg, eth_estimateGas or eth_call)。
  2. 在 client 成功使用 dapp 戳到 Layer1 合約之後,要如何取得 revert 和 error message 是一個實作點。已知 ethers.js 會在 exception 發生時將 error info 包成 exception 物件讓 client catch,而 EIP-3668 也有提供一個支援的 provider 套件,讓 client 可以不用自己去 handle 這個 error,並自行與 gateway 互動(發起 HTTP Request)。
  3. Client 必須處理 HTTP status code 的各種情況,包含 handle HTTP 4xx 和 5xx 的 exception 處理,這些情況可能擁有 application/json 這樣的 content type,需要注意不能將他們當作普通的 JSON 直接 parse 掉了。
  4. 這個 protocol 需要有一個遍歷的機制來重複 request,通常這樣一個 single contract call 之後嘗試 Gateway URLs 的次數最少為四次
  5. 需要實作 JSON RPC call 時可以參考 Ethereum JSON-RPC specification

Discussion

Trust Construction

當我們假設 client 信任 contract 通常代表這個合約已經被 verify 和公開原始碼了,然而這邊雖然 gateway 屬於外部進程,我們仍然可以在信任 contract 的情況下間接信任 gateway(相信這個 contract 回傳的 gateway URL 可以妥善回覆 client 的 query)。

另外我們也需要相信 contract 具有足夠能力可以驗證 gateway response 是正確的,只要以上部分可受公允,基本上就可以避免安全疑慮。

使用 revert 來傳遞 call information

如果使用一個函式回傳值來告訴 Client 該做什麼,那 client 就必須實作 function 的 ABI,或者合約提供其他 flag 或 signal 的方式告訴使用者現在該去 gateway 了。

也就是說,如果今天是使用普通 function 來進行 signal 以及傳遞資訊的作用,那 client 呼叫不同函式時,都必須要針對其的 fxName()offchainFxName() 進行處理。使用 revert 並且把需要傳遞的資訊放在 revert data 之中,只要宣告一個 error type 就可以避免上述每個 function 都要實作的情況,以此達到模組化的功效。

此外在 Nested call 的情況中,以 error msg 的形式可以更好的在每一層合約之中傳遞資訊,如果是以 function 包住必要參數作為回傳值的話就需要特別再去設計這個回傳的「內容」(例如 bytes 型態的訊息)。此時 client 就必須再去解讀這個回傳值是OffchainLookup 還是想知道的回傳值。

extraData 參數的存在意義

extraData 促使 Layer1 上的合約函式資訊能夠被傳遞,因為 Layer1 合約可能會被多個 Client 進行呼叫並回傳 revert 給每個人,如果沒有這個參數存在,合約可能無法判斷當前這個 callback 該怎麼處理。

Gateway Response Data / Client Extra Data Validation

設計上 extraData 作為主要判斷 Gateway Response 的依據,舉例來說 contract 索取的資訊是基於某個資料(查詢某個地址的 balance),那在 extraData 終究必須擁有這個資料,以便之後 callback 時可以驗證 gateway 有沒有提供錯誤的 response。

同時合約也要實作足夠的驗證機制來驗證 Gateway 的 response 是否合法。這個驗證必須要是無法被偽造的,例如是否存在於 Layer2 合約的 Merkle proof,或驗證一個可信任的 signer 的 sign message。

HTTP requests and Privacy Problems

CCIP read 包含著 HTTP request 以及 address 的互動,因此可能會出現我們能把 IP 和 address 掛鉤以此辨別出使用者的隱私問題。

Closing

這兩、三個禮拜幾乎都在忙著研究 EIP-3668 並嘗試自己做出一個跨鏈橋的互動場景,除了以上提到的內容之外其實 EIP-3668 還有非常非常多地方可以加以著墨,例如使用 MT, SMT 來降低複雜度,或者使用 Off-Chain Calculating 來讓鏈下的單位負擔產樹的成本,使用 SNARKs 的證明而非上述的 sign messge 或 merkle proof 等。

這篇文章非常感謝 Nic 老師以及 Cyan 老師給予超級有幫助的建議!

Reference

ethers.js with CCIP-read

Synchronization Link Tree

0x2b83c71A59b926137D3E1f37EF20394d0495d72d

--

--

ChiHaoLu
Taipei Ethereum Meetup

Multitasking Master & Mr.MurMur | Blockchain Dev. @ imToken Labs | chihaolu.me | Advisory Services - https://forms.gle/mVGKQwPQEUP37fLYA