本文會從技術上簡述一下 Chainlink 的基本原理。如果用最短的一句話解釋什麼是 Chainlink,可以說 Chainlink 是一個去中心化的預言機項目,所以為了理解 Chainlink 的工作原理,我們首先要明白什麼是預言機。本文參考自 Tales From the Crypto 的 Medium 文章《Building your first Ethereum Oracle》,由專欄作者 以太坊愛好者 編譯、撰寫及整理。
預言機的英文為 Oracle,和著名的數據庫服務提供商 Oracle(甲骨文)同名,但是兩者除了名字相同以外並沒有任何關係。Oracle 這個單詞是什麼意思,下面是我在 vocabulary.com 上查到的 Oracle 的含義:
Back in ancient times, an oracle was someone who offered advice or a prophecy thought to have come directly from a divine source. In modern usage, any good source of information can be called an oracle.
中文的大概意思是:在古代,oracle 是一個提出建議或預言的人,他的建議或預言被認為是直接來自於神。在現代用法中,任何好的訊息來源都可以稱為 oracle。
這樣就不難理解了,Oracle 傳達了萬能全知的神的旨意,而甲骨文最初就是用來占卜吉凶時的記錄,也在當時也被認為是神諭,傳達了神的意思。說以不管是「預言機」還是「甲骨文」都表達了「訊息源」的意思。
延伸閱讀:文組也該知道的區塊鏈技術知識 <9> :淺談預言機 Oracle,區塊鏈與現實世界的橋樑
延伸閱讀:落地應用痛點|為什麼預言機(Oracle)是區塊鏈落地應用的「剛性需求」?
電腦科學領域的預言機
電腦領域內的預言機一詞,最早是圖靈提出的。
圖靈在圖靈機(Turing Machine)的基礎上,加入了一個稱為預言者(oracle)的黑盒,組成了預言機(Oracle Machine)。所謂預言者,是一個可以回答特定問題集合的實體。即它可以向圖靈機系統內部輸入訊息,幫助圖靈機完成運算。
以太坊的智能合約是「圖靈完備(Turing Complete)」的,某種意義上可以看做一個圖靈機,所以以太坊的設計者借鑑這個概念,把向「圖靈完備的智能合約」這個圖靈機輸入訊息的也被稱為預言機 oracle。
所以說「預言機」這個名字並不是區塊鏈技術領域內的獨創概念,它來源於非常早期的電腦抽象設計,在密碼學等領域內也都有類似的概念。
而在區塊鏈領域,預言機被認為是可以為智能合約提供外部數據源的系統。從傳統技術架構方面來看,預言機是連接智能合約與區塊鏈外部世界的中間件(middleware),是區塊鏈重要的基礎設施,它的作用是為區塊鏈上的智能合約(Smart Contract)提供數據信息。
正如以太坊的定義,區塊鏈是一個交易驅動的狀態機(a transaction-based state machine),它能做的事情非常簡單,就是通過向區塊鏈提交事務/交易(transaction),來將區塊鏈從一個狀態轉變成另一個狀態。
為了保持共識,EVM 的執行過程必須完全確定,並且僅基於以太坊狀態和簽名交易的共享上下文。
這產生了兩個特別重要的後果:一個是 EVM 和智能合約沒有內在的隨機性來源;另一個是外部數據只能作為交易的數據載荷引入。
延伸閱讀:從網路看區塊鏈《三》40年前「互聯網巨頭」的協議戰爭,對我們有什麼啟示?
延伸閱讀:文組也該知道的區塊鏈技術知識《2》:一次搞懂「以太坊智慧合約」如何運作
用通俗的話講,區塊鏈沒有主動獲取數據的能力,它能用的只有區塊鏈自己本身的數據。數據的缺失導致智能合約的應用範圍非常少,目前大部分的應用都是圍繞著 token 來展開的。
區塊鏈的確定性的意思是,在任何節點上,只要連入到區塊鏈的分佈式網路中,它就可以同步所有的歷史區塊,回放出一套完全相同的帳本。
換句話說:在沒有互聯網連接的情況下,給定完整的塊,節點必須能夠從頭開始重新創建區塊鏈的最終狀態。
如果帳本在形成過程中,依賴於某個外部的 API 調用結果,那在不同時間不同環境下回放的結果就會不一樣。這種情況是區塊鏈所不允許的,所以區塊鏈在設計之初就沒有網路調用。
那麼要實現向區塊鏈提供數據,應該怎麼做呢?區塊鏈能留下的只有帳本,而區塊鏈所能輸入的只有交易。我們就從這兩個方面入手。
幾乎每一個合約系統,都會有事件記錄的功能,比如以太坊中的 EventLog 功能。
下面我們通過一個例子,來介紹一下預言機的基本原理。我們在以太坊鏈上建立一個用戶合約,它需要獲取到某個城市的氣溫數據。
當然,智能合約自己是無法獲取到這個發生於鏈下真實世界中的數據訊息的,需要藉助預言機來實現。
智能合約將需要獲取天氣溫度的的城市寫入到 EventLog 中,鏈下我們會啟動一個進程,監聽並訂閱這個事件日誌,獲取到智能合約的請求之後,將指定城市的溫度,通過提交 transaction 的方式,調用合約中的回填方法,提交到智能合約中。
延伸閱讀:深度探討|推特創辦人Jack Dorsey的權力下放計畫:BlueSky 的背後究竟想要改變什麼?
延伸閱讀:台灣交易所「比特之星」驚傳跑路!公司現已登記解散,12小時成交量僅剩17,952
聲明:以下程式碼僅供演示預言機原理,沒有做參數檢測和錯誤處理,請不要在生產環境中使用。
消費者合約:
contract WeatherOracle {
// 用戶存儲預言機提交的天氣數值
uint256 public temperature;
// 定義事件
event RequestTemperature (bytes city);
// 發出獲取請求,即發出一個事件日誌
function requestTemperature (string memory _city) public {
emit RequestTemperature(bytes(_city));
}
// 預言機回調方法,預言機獲取到數據後通過這個方法將數據提交到鏈上
function updateWeather (uint256 _temperature) public {
temperature = _temperature;
}
}
上面的程式碼非常簡單,定義了一個變量用來存儲結果,一個方法用於發出請求,一個方法用於接收結果。
鏈下,我們啟動一個進程,以訂閱 topic 的方式獲取日誌訊息,之後通過構建一個 transaction,提交一個結果到合約中。
func SubscribeEventLog() {
topic := crypto.Keccak256([]byte("RequestTemperature(bytes)"))
query := ethereum.FilterQuery{
Topics: [][]common.Hash{
{
common.BytesToHash(topic),
},
},
}
// 訂閱相關主題的日誌事件
events := make(chan types.Log)
sub, err := EthClient.SubscribeFilterLogs(ctx, query, events)
// 加載合約的ABI文件
ta, err := abi.JSON(strings.NewReader(AbiJsonStr))
// 監聽事件訂閱
for {
select {
case err := <-sub.Err():
log.Error(err)
break
case ev := <-events:
// 獲取到訂閱的消息
ej, _ := ev.MarshalJSON()
log.Info(string(ej))
// 解析數據
var sampleEvent struct {
City []byte
}
err = ta.Unpack(&sampleEvent, "RequestTemperature", ev.Data)
log.Info(string(sampleEvent.City))
// 構建交易提交結果,需要提供私鑰用於簽署交易
CallContract("b7b502b...164b42c")
}
}
}
func CallContract(keyStr string) {
addr := PrivateKeyToAddress(keyStr)
nonce, err := EthClient.PendingNonceAt(ctx, addr)
gasPrice, err := EthClient.SuggestGasPrice(ctx)
privateKey, err := crypto.HexToECDSA(keyStr)
auth := bind.NewKeyedTransactor(privateKey)
auth.Nonce = big.NewInt(int64(nonce))
auth.Value = big.NewInt(0)
auth.GasLimit = uint64(300000)
auth.GasPrice = gasPrice
instance, err := event.NewEvent(common.HexToAddress("0x8A421906e9562AA1c71e5a32De1cf75161C5A463"), EthClient)
// 調用合約中的updateWeather方法,回填數據"29"
tx, err := instance.UpdateWeather(auth, big.NewInt(29))
log.Info(tx.Hash().Hex())
}
用一個圖來展示這個過程:
Chainlink
Chainlink 是一個去中心化的預言機項目,它的作用就是以最安全的方式向區塊鏈提供現實世界中產生的數據。Chainlink 在基本的預言機原理的實現方式之上,圍繞 LINK token 通過經濟激勵建立了一個良性循環的生態系統。Chainlink 預言機需要通過 LINK token 的轉帳來實現觸發。
LINK 是以太坊網路上的 ERC677 合約,關於各類 ERC token 的區別,請參考這篇文章。
在《精通以太坊(Matering Ethereum)》一書中,提出了三種預言機的設計模式,分別是:
- 立即讀取(immediate-read)
- 發布/訂閱(publish–subscribe)
- 請求/響應(request–response)
而基於 LINK ERC677 token 完成的預言機功能,就屬於其中的請求/響應模式。這是一種較為複雜的模式,上圖中展示的是一個不含有聚合過程的簡單請求/相應流程。
我們以Chainlink提供的 TestnetConsumer
合約中的一個 requestEthereumPrice
方法為例來簡單講一下請求響應的流程。這個函數定義如下:
function requestEthereumPrice(address _oracle, string _jobId)
public
onlyOwner
{
Chainlink.Request memory req = buildChainlinkRequest(stringToBytes32(_jobId), this, this.fulfillEthereumPrice.selector);
req.add("get", "https://min-api.cryptocompare.com/data/price?fsym=ETH&tsyms=USD");
req.add("path", "USD");
req.addInt("times", 100);
sendChainlinkRequestTo(_oracle, req, ORACLE_PAYMENT);
}
它所實現的功能就是從指定的 API (cryptocompare) 獲取 ETH/USD 的交易價格。函數傳入的參數是指定的 oracle 地址和 jobId。將一些列的請求參數組好後,調用 sendChainlinkRequestTo
方法將請求發出。sendChainlinkRequestTo
是定義在 Chainlink 提供的庫中的一個接口方法,定義如下:
/**
* @notice 向指定的oracle地址創建一個請求
* @dev 創建並存儲一個請求ID, 增加本地的nonce值, 並使用`transferAndCall` 方法發送LINK,
* 創建到目標oracle合約地址的請求
* 發出 ChainlinkRequested 事件.
* @param _oracle 發送請求至的oracle地址
* @param _req 完成初始化的Chainlink請求
* @param _payment 請求發送的LINK數量
* @return 請求 ID
*/
function sendChainlinkRequestTo(address _oracle, Chainlink.Request memory _req, uint256 _payment)
internal
returns (bytes32 requestId)
{
requestId = keccak256(abi.encodePacked(this, requests));
_req.nonce = requests;
pendingRequests[requestId] = _oracle;
emit ChainlinkRequested(requestId);
require(link.transferAndCall(_oracle, _payment, encodeRequest(_req)), "unable to transferAndCall to oracle");
requests += 1;
return requestId;
}
其中 link.transferAndCall
方法即是 ERC677 定義的 token 轉帳方法,與 ERC20 的 transfer
方法相比,它多了一個 data 字段,可以在轉帳的同時攜帶數據。這裡就將之前打包好的請求數據放在了 data 字段,跟隨轉帳一起發送到了 oracle 合約。transferAndCall
方法定義如下:
/**
* @dev 將token和額外數據一起轉移給一個合約地址
* @param _to 轉移到的目的地址
* @param _value 轉移數量
* @param _data 傳遞給接收合約的額外數據
*/
function transferAndCall(address _to, uint _value, bytes _data)
public
returns (bool success)
{
super.transfer(_to, _value);
Transfer(msg.sender, _to, _value, _data);
if (isContract(_to)) {
contractFallback(_to, _value, _data);
}
return true;
}
其中的 Transfer(msg.sender, _to, _value, _data);
是發出一個事件日誌:
event Transfer(address indexed from, address indexed to, uint value, bytes data);
將這次轉帳的詳細信息(發送方、接收方、金額、數據)記錄到日誌中。
Oracle 合約在收到轉帳之後,會觸發onTokenTransfer
方法,該方法會檢查轉帳的有效性,並通過發出OracleRequest
事件記錄更為詳細的數據訊息:
event OracleRequest(
bytes32 indexed specId,
address requester,
bytes32 requestId,
uint256 payment,
address callbackAddr,
bytes4 callbackFunctionId,
uint256 cancelExpiration,
uint256 dataVersion,
bytes data
);
這個日誌會在 oracle 合約的日誌中找到,如圖中下方所示。鏈下的節點會訂閱該主題的日誌,在獲取到記錄的日誌訊息之後,節點會解析出請求的具體訊息,通過網路的 API 調用,獲取到請求的結果。之後通過提交事務的方式,調用 Oracle 合約中的 fulfillOracleRequest
方法,將數據提交到鏈上。fulfillOracleRequest
定義如下:
/**
* @notice 由Chainlink節點調用來完成請求
* @dev 提交的參數必須是`oracleRequest`方法所記錄的哈希參數
* 將會調用回調地址的回調函數,`require`檢查時不會報錯,以便節點可以獲得報酬
* @param _requestId 請求ID必須與請求者所匹配
* @param _payment 為Oracle發放付款金額 (以wei為單位)
* @param _callbackAddress 完成方法的回調地址
* @param _callbackFunctionId 完成方法的回調函數
* @param _expiration 請求者可以取消之前節點應響應的到期時間
* @param _data 返回給消費者合約的數據
* @return 外部調用成功的狀態值
*/
function fulfillOracleRequest(
bytes32 _requestId,
uint256 _payment,
address _callbackAddress,
bytes4 _callbackFunctionId,
uint256 _expiration,
bytes32 _data
)
external
onlyAuthorizedNode
isValidRequest(_requestId)
returns (bool)
{
bytes32 paramsHash = keccak256(
abi.encodePacked(
_payment,
_callbackAddress,
_callbackFunctionId,
_expiration
)
);
require(commitments[_requestId] == paramsHash, "Params do not match request ID");
withdrawableTokens = withdrawableTokens.add(_payment);
delete commitments[_requestId];
require(gasleft() >= MINIMUM_CONSUMER_GAS_LIMIT, "Must provide consumer enough gas");
// All updates to the oracle's fulfillment should come before calling the
// callback(addr+functionId) as it is untrusted.
// See: https://solidity.readthedocs.io/en/develop/security-considerations.html#use-the-checks-effects-interactions-pattern
return _callbackAddress.call(_callbackFunctionId, _requestId, _data); // solhint-disable-line avoid-low-level-calls
}
這個方法會在進行一系列的檢驗之後,會將結果通過之前記錄的回調地址與回調函數,返回給消費者合約:
_callbackAddress.call(_callbackFunctionId, _requestId, _data);
這樣一次請求就全部完成了。
總結
本文從預言機的概念開始,通過一個簡單的獲取 ETH 價格的例子,講解了請求 / 響應模式的 Chainlink 預言機的基本過程,希望對你理解預言機與 Chainlink 的運行原理有所幫助。
?相關報導?
ethfans|從歷年數據看以太坊的發展:自2018 年 DeFi 增長30 倍;各大 ICO 項目資金告急
區塊鏈的關鍵最後一哩路!預言機如何連接「虛擬世界」與「現實世界」?
週末專題|如何實現真正的去中心化預言機?—— 以 Chainlink 為例延伸發想
讓動區 Telegram 新聞頻道再次強大!!立即加入獲得第一手區塊鏈、加密貨幣新聞報導。