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

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

預言機詳解系列之 Chainlink(上)

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

Chainlink

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

ERC677 代幣中的transferAndCall

預言機詳解系列之 Chainlink(上)

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

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

預言機詳解系列之 Chainlink(上)

當使用者進行transferAndCall 進行轉帳時,除了ERC20 的轉帳以外,也會判斷to 位址是否為一個合約位址,如果是,則呼叫該to 位址的onTokenTransfer 方法。 (這裡ERC677Receiver 裡面只有一個方法:onTokenTransfer)

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

預言機詳解系列之 Chainlink(上)

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

預言機詳解系列之 Chainlink(上)

預言機可信度劃分

鏈上oracle 請求

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

預言機詳解系列之 Chainlink(上)

當預言機的消費者使用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 方法。

預言機詳解系列之 Chainlink(上)

首先,將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 事件:

預言機詳解系列之 Chainlink(上)

 OracleRequest 事件範例

事件可以看出,是0xF79D6aFBb6dA890132F9D7c355e3015f15F3406F 這個ETH/USD 價格聚合器向oracle:0x7e94a8a23687d8c7058ba562523558如果oracle 回傳請求資料的話,可以從這裡面知道回傳的合約位址:0xF79D6aFBb6dA890132F9D7c355e3015f15F3406F,需要呼叫的方法ID:6A9705B4,以及過期時間:1618185924。

鏈下節點回應

3.1 鏈下呼叫fulfillOracleRequest

預言機詳解系列之 Chainlink(上)

首先進行檢查:

  • 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 連結以提高鏈下發送交易的速度。

預言機詳解系列之 Chainlink(上)

3.2 回呼函數

之前我們從oracleRequest 中知道了回呼函數的id 是6A9705B4,查詢得到該方法為“ chainlinkCallback(bytes32,int256 ”

預言機詳解系列之 Chainlink(上)

預言機詳解系列之 Chainlink(上)

validateChainlinkCallback 是一個可以自訂的函數,這裡有一個modifier:

預言機詳解系列之 Chainlink(上)

在pendingRequests 裡面檢查該_requestId 對應請求的oracle 是否符合。並發出事件ChainlinkFulfilled:

預言機詳解系列之 Chainlink(上)

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

預言機詳解系列之 Chainlink(上)

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

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

預言機詳解系列之 Chainlink(上)

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

預言機詳解系列之 Chainlink(上)

Oracle 合約在收到轉帳之後,會觸發「 onTokenTransfer 」方法,該方法會檢查轉帳的有效性,並透過發出「 OracleRequest 」事件記錄更為詳細的資料資訊。

這個日誌會在oracle 合約的日誌中找到。鏈下的節點會訂閱該主題的日誌,在取得到記錄的日誌資訊之後,節點會解析出請求的具體信息,透過網路的API 調用,獲取到請求的結果。之後透過提交交易的方式,呼叫Oracle 合約中的「 fulfillOracleRequest 」方法,將資料提交到鏈上。

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

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

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

預言機詳解系列之 Chainlink(上)

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

預言機詳解系列之 Chainlink(上)

具體這個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 處理不同精確度的問題。