rounded

在區塊鏈領域中,預言機是一種能夠爲鏈上智能合約提供外部信息的系統。作爲連接智能合約和區塊鏈以外世界的中間件,預言機扮演着極其關鍵的基礎設施角色,它的主要功能是爲區塊鏈中的智能合約提供數據。

例如,如果我們在以太坊網絡上創建一個智能合約,而這個合約需要訪問原油某天的交易量數據。然而智能合約本身無法獲取這種鏈下的現實世界數據,因此需要通過預言機來實現。在這種情況下,智能合約會將所需日期的原油交易量寫入事件日誌,然後,鏈下會啓動一個進程來監控並訂閱這個事件日誌,當監聽到交易中的請求時,該進程會通過提交鏈上交易,調用合約的相關方法,把指定日期的原油交易量信息上傳到智能合約中。

數據源自 https://defillama.com/oracles

Chainlink

在區塊鏈中,市場佔有率最大的莫過於 Chainlink 預言機。Chainlink 是一個去中心化的預言機項目,它的作用就是以最安全的方式向區塊鏈提供現實世界中產生的數據。Chainlink 在基本的預言機原理的實現方式之上,圍繞 LINK token 通過經濟激勵建立了一個良性循環的生態系統。Chainlink 預言機需要通過 LINK token 的轉賬來實現觸發。而 LINK 則是以太坊網絡上的 ERC677 合約。而基於 LINK ERC677 token 完成的預言機功能,屬於其中的請求 / 響應模式。

ERC677 代幣中的 transferAndCall

預言機實質上是提供服務的一方,ChainLink 在設計預言機框架的時候首先想到的是預言機的用戶如何向提供服務的預言機支付服務費用。但由於標準的同質化 Token 合約 ERC20 無法滿足支付後提供服務這樣的一個需求,因此 ChainLink 自己提出了一個適用於預言機服務場景的標準——ERC677。

從上面的代碼中可以看到,ERC677 其實只是在標準 ERC20 的基礎上增加了一個 transferAndCall 方法。該方法將支付和服務請求合二爲一,滿足了預言機業務場景的需求。

當用戶進行 transferAndCall 進行轉賬時,除了 ERC20 的轉賬以外,還會判斷 to 地址是否爲一個合約地址,如果是,則調用該 to 地址的 onTokenTransfer 方法。(這裏 ERC677Receiver 裏面只有一個方法:onTokenTransfer)

我們也可以去到 Etherscan 上查看 LINK 代幣的合約源碼:https://etherscan.io/address/0x514910771af9ca656af840dff83e8264ecf986ca#code

可以看到 LINK Token 在實現的時候除了多對 _to 地址進行了校驗以外,都是實實在在繼承了 ERC677 的 transferAndCall 方法。注意:在請求預言機服務之前,要先確定該預言機是否可信,因爲預言機爲消費者提供服務之前需要先付款。(人人都能提供預言機服務)

預言機可信度劃分

鏈上 oracle 請求

下面來看看 oracle 合約的 onTokenTransfer 方法是如何實現的:

當預言機的消費者使用 transferAndCall 方法支付費用並請求預言機的服務,這裏這個 to 地址就是被請求的預言機的地址了。預言機中的 onTokenTransfer 方法首先會校驗轉賬是否爲 LINK 代幣(onlyLINK),其實就是判斷 msg.sender 是否爲 Link 代幣合約的地址。然後會判斷 _data 的長度有沒有超過最大限度。最後會判斷_data 中是不是以“oracleRequest”開頭的 function selector。當然這裏的 function selector 可以根據預言機所提供的服務進行定製,不一定非要是“oracleRequest”,具體看這個預言機對外暴露什麼樣的接口了。

當這些 modifier 都判斷通過後,再檢查當前的函數調用者和轉賬金額是否跟 _data 中的相同。這一些列的安全檢查都通過後,才通過一個 delegatecall 來 call 當前這個 oracle 合約。當然,因爲已經檢查過 _data 中的 function selector 了,所以其實是 call 的 oracleRequest 方法。

首先,將 oracle 請求者和他發送過來的 nonce 拼接然後進行哈希,作爲本次請求的 requestId,並通過查檢查 commitments 映射看是否是唯一的 id。檢查沒問題的話,就設置一個過期時間,並將 requestId 添加到 commitments 中去,並將 _payment、_callbackAddress、_callbackFunctionId 和 expiration 進行拼接作爲 value。最重要的是,發出一個 OracleRequest 事件,該事件中包含了請求數據 _data,是一種 Concise Binary Object Representation(CBOR) 數據。該編碼格式輕量簡潔,可以簡單理解爲二進制形式 JSON 格式。這個數據可以是各種各樣的形式,看鏈下節點是如何設計的了。

例如:一個 Chainlink: ETH/USD Aggregator,有一筆交易包含了 OracleRequest 事件:

OracleRequest 事件示例

該事件可以看出,是 0xF79D6aFBb6dA890132F9D7c355e3015f15F3406F 這個 ETH/USD 價格聚合器向 oracle:0x7e94a8a23687d8c7058ba5625db2ce358bcbd244 發出的價格數據請求。如果 oracle 返回請求數據的話,可以從這裏面知道返回的合約地址:0xF79D6aFBb6dA890132F9D7c355e3015f15F3406F,需要調用的方法 ID:6A9705B4,以及過期時間:1618185924。

鏈下節點回應

3.1 鏈下調用 fulfillOracleRequest

首先進行檢查:

  • onlyAuthorizedNode:函數調用者 (msg.sender) 必須是合約的 owner 或在授權的列表內;

  • isValidRequest:依舊去 commitments 映射中檢查是否存在該 requestId;

  • 將 payment、callbackAddress、_callbackFunctionId 和 expiration 進行拼接,檢查是否是該 requestId 在 commitments 映射中對應的值。

如果這些檢查都通過了的話,那麼將這次的請求的花費累加到 withdrawableTokens 中,記錄可以取款的數額。之後將該 _requestId 從 commitments 映射中刪除。最後計算一下剩餘的 gas 量,看是否大於 MINIMUM_CONSUMER_GAS_LIMIT,即回調發出請求的合約的回調函數執行最小需要的 gas 量。

如果上述檢查都通過了,那麼可以用 call 的形式正式調用請求者合約的回調函數。

迴應 request 應該儘量迅速,因此這裏推薦使用 ZAN 的節點服務(https://zan.top/home/node-service?chInfo=ch_WZ)來提高響應速度。可以在節點服務控制檯找到獲取對應的 RPC 鏈接以提高鏈下發送交易的速度。

3.2 回調函數

之前我們從 oracleRequest 中知道了回調函數的 id 是 6A9705B4,查詢得到該方法爲「 chainlinkCallback(bytes32,int256 」

validateChainlinkCallback 是一個可以自定義的函數,這裏有一個 modifier:

在 pendingRequests 裏面檢查該 _requestId 對應請求的 oracle 是否匹配。併發出事件 ChainlinkFulfilled:

如果校驗都通過了的話,那麼就可以對 responds 做進一步的處理了,這裏是對 answers 映射進行更新。那麼如果是價格預言機的話,則是將回應的價格數據賦給 currentPrice 做相應的價格更新:

以上是通用預言機服務的完整流程。

我們以 Chainlink 提供的「 TestnetConsumer 」合約中的一個「 requestEthereumPrice 」方法爲例來簡單講一下價格預言機請求響應的流程。這個函數定義如下:

它所實現的功能就是從指定的 API(cryptocompare) 獲取 ETH/USD 的交易價格。函數傳入的參數是指定的 oracle 地址和 jobId。將一些列的請求參數組好後,調用「 sendChainlinkRequestTo 」方法將請求發出。「 sendChainlinkRequestTo 」是定義在 Chainlink 提供的庫中的一個接口方法,定義如下:

Oracle 合約在收到轉賬之後,會觸發「 onTokenTransfer 」方法,該方法會檢查轉賬的有效性,並通過發出「 OracleRequest 」事件記錄更爲詳細的數據信息。

這個日誌會在 oracle 合約的日誌中找到。鏈下的節點會訂閱該主題的日誌,在獲取到記錄的日誌信息之後,節點會解析出請求的具體信息,通過網絡的 API 調用,獲取到請求的結果。之後通過提交事務的方式,調用 Oracle 合約中的「 fulfillOracleRequest 」方法,將數據提交到鏈上。

這個方法會在進行一系列的檢驗之後,會將結果通過之前記錄的回調地址與回調函數,返回給消費者合約。

那我作爲開發者我只想要用已有的幣對價格,而不需要自己指定這些 url 可不可以呢?

答案是可以。第一種使用方式,官方給的示例代碼是這樣的:


首先,每個交易對都有一個單獨的 Price Feed,也叫 Aggregator,其實就是一個個 AggregatorProxy,像下面這樣:

具體這個 interface 實現比較簡單,可以參考 AAVE/ETH 這個 pair:https://etherscan.io/address/0x6Df09E975c830ECae5bd4eD9d90f3A95a4f88012#code

總共有 5 個查詢方法:

  • decimals():返回的價格數據的精度位數,一般爲 8 或 18

  • description():一般爲交易對名稱,比如 ETH / USD

  • version():主要用來標識 Proxy 所指向的 Aggregator 類型

  • getRoundData(_roundId):根據 round ID 獲取當時的價格數據

  • latestRoundData():獲取最新的價格數據

大部分應用場景下,合約可能只需要讀取最新價格,即調用最後一個方法,其返回參數中,answer 就是最新價格。

另外,大部分應用讀取 token 的價格都是統一以 USD 爲計價單位的,若如此,你會發現,以 USD 爲計價單位的 Pair,精度位數都是統一爲 8 位的,所以一般情況下也無需根據不同 token 處理不同精度的問題。


本文由 ZAN Team(X 賬號 @zan_team) 的 XiG(X 賬號 @SHXiGi)撰寫。