Chainlink基本原理
本文我們來(lái)從技術(shù)上簡(jiǎn)述一下Chainlink的基本原理。如果用最短的一句話解釋什么是Chainlink,可以說(shuō)Chainlink一個(gè)去中心化的預(yù)言機(jī)項(xiàng)目,所以為了理解Chainlink的工作原理,我們首先要明白什么是預(yù)言機(jī)。
預(yù)言機(jī)
預(yù)言機(jī)的英文為Oracle,和著名的數(shù)據(jù)庫(kù)服務(wù)提供商O(píng)racle(甲骨文)重名,但是兩者除了名字相同以為并沒(méi)有任何關(guān)系。Oracle這個(gè)單詞是什么意思,下面是我在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是一個(gè)提出建議或預(yù)言的人,他的建議或預(yù)言被認(rèn)為是直接來(lái)自于神。在現(xiàn)代用法中,任何好的信息來(lái)源都可以稱為oracle。
這樣就不難理解了,Oracle傳達(dá)了萬(wàn)能全知的神的旨意,而甲骨文最初就是用來(lái)占卜吉兇時(shí)的記錄,也在當(dāng)時(shí)也被認(rèn)為是神諭,傳達(dá)了神的意思。所以不管是“預(yù)言機(jī)”還是“甲骨文”都表達(dá)了“信息源”的意思。
計(jì)算機(jī)領(lǐng)域內(nèi)的預(yù)言機(jī)一詞,最早是圖靈提出的。圖靈在圖靈機(jī)(Turing Machine)的基礎(chǔ)上,加入了一個(gè)稱為預(yù)言者(oracle)的黑盒,組成了預(yù)言機(jī)(Oracle Machine)。所謂預(yù)言者,是一個(gè)可以回答特定問(wèn)題集合的實(shí)體。即它可以向圖靈機(jī)系統(tǒng)內(nèi)部輸入信息,幫助圖靈機(jī)完成運(yùn)算。以太坊的智能合約是“圖靈完備(Turing Complete)”的,某種意義上可以看做一個(gè)圖靈機(jī),所以以太坊的設(shè)計(jì)者借鑒這個(gè)概念,把向“圖靈完備的智能合約”這個(gè)圖靈機(jī)輸入信息的也被稱為預(yù)言機(jī)oracle。所以說(shuō)“預(yù)言機(jī)”這個(gè)名字并不是區(qū)塊鏈技術(shù)領(lǐng)域內(nèi)的獨(dú)創(chuàng)概念,它來(lái)源于非常早期的計(jì)算機(jī)抽象設(shè)計(jì),在密碼學(xué)等領(lǐng)域內(nèi)也都有類似的概念。
而在區(qū)塊鏈領(lǐng)域,預(yù)言機(jī)被認(rèn)為是可以為智能合約提供外部數(shù)據(jù)源的系統(tǒng)。從傳統(tǒng)技術(shù)架構(gòu)方面來(lái)看,預(yù)言機(jī)是連接智能合約與區(qū)塊鏈外部世界的中間件(middleware),是區(qū)塊鏈重要的基礎(chǔ)設(shè)施,它的作用是為區(qū)塊鏈上的智能合約(Smart Contract)提供數(shù)據(jù)信息的。
正如以太坊的定義,區(qū)塊鏈?zhǔn)且粋€(gè)交易驅(qū)動(dòng)的狀態(tài)機(jī)(a transaction-based state machine),它能做的事情非常簡(jiǎn)單,就是通過(guò)向區(qū)塊鏈提交事務(wù)/交易(transaction),來(lái)將區(qū)塊鏈從一個(gè)狀態(tài)轉(zhuǎn)變成另一個(gè)狀態(tài)。為了保持共識(shí),EVM的執(zhí)行過(guò)程必須完全確定,并且僅基于以太坊狀態(tài)和簽名交易的共享上下文。這產(chǎn)生了兩個(gè)特別重要的后果:一個(gè)是EVM和智能合約沒(méi)有內(nèi)在的隨機(jī)性來(lái)源;另一個(gè)是外部數(shù)據(jù)只能作為交易的數(shù)據(jù)載荷引入。用通俗的話講,區(qū)塊鏈沒(méi)有主動(dòng)獲取數(shù)據(jù)的能力,它能用的只有區(qū)塊鏈自己本身的數(shù)據(jù)。數(shù)據(jù)的缺失導(dǎo)致智能合約的應(yīng)用范圍非常少,目前大部分的應(yīng)用都是圍繞著token來(lái)展開(kāi)的。
區(qū)塊鏈的確定性的意思是,在任何節(jié)點(diǎn)上,只要連入到區(qū)塊鏈的分布式網(wǎng)絡(luò)中,它就可以同步所有的歷史區(qū)塊,回放出一套完全相同的賬本。換句話說(shuō):在沒(méi)有互聯(lián)網(wǎng)連接的情況下,給定完整的塊,節(jié)點(diǎn)必須能夠從頭開(kāi)始重新創(chuàng)建區(qū)塊鏈的最終狀態(tài)。如果賬本在形成過(guò)程中,依賴于某個(gè)外部的API調(diào)用結(jié)果,那在不同時(shí)間不同環(huán)境下回放的結(jié)果就會(huì)不一樣。這種情況是區(qū)塊鏈所不允許的,所以區(qū)塊鏈在設(shè)計(jì)之初就沒(méi)有網(wǎng)絡(luò)調(diào)用。
那么要實(shí)現(xiàn)向區(qū)塊鏈提供數(shù)據(jù),應(yīng)該怎么做呢?區(qū)塊鏈能留下的只有賬本,而區(qū)塊鏈所能輸入的只有交易。我們就從這兩個(gè)方面入手。
幾乎每一個(gè)合約系統(tǒng),都會(huì)有事件記錄的功能,比如以太坊中的EventLog功能。
下面我們通過(guò)一個(gè)例子,來(lái)介紹一下預(yù)言機(jī)的基本原理。我們?cè)谝蕴绘溕辖⒁粋€(gè)用戶合約,它需要獲取到某個(gè)城市的氣溫?cái)?shù)據(jù)。當(dāng)然,智能合約自己是無(wú)法獲取到這個(gè)發(fā)生于鏈下真實(shí)世界中的數(shù)據(jù)信息的,需要借助預(yù)言機(jī)來(lái)實(shí)現(xiàn)。智能合約將需要獲取天氣溫度的的城市寫(xiě)入到EventLog中,鏈下我們會(huì)啟動(dòng)一個(gè)進(jìn)程,監(jiān)聽(tīng)并訂閱這個(gè)事件日志,獲取到智能合約的請(qǐng)求之后,將指定城市的溫度,通過(guò)提交transaction的方式,調(diào)用合約中的回填方法,提交到智能合約中。
聲明:以下代碼僅供演示預(yù)言機(jī)原理,沒(méi)有做參數(shù)檢測(cè)和錯(cuò)誤處理,請(qǐng)不要在生產(chǎn)環(huán)境中使用。
消費(fèi)者合約:
contract WeatherOracle {
// 用戶存儲(chǔ)預(yù)言機(jī)提交的天氣數(shù)值
// 定義事件
event RequestTemperature (bytes city);
// 發(fā)出獲取請(qǐng)求,即發(fā)出一個(gè)事件日志
function requestTemperature (string memory _city) public {
emit RequestTemperature(bytes(_city));
}
// 預(yù)言機(jī)回調(diào)方法,預(yù)言機(jī)獲取到數(shù)據(jù)后通過(guò)這個(gè)方法將數(shù)據(jù)提交到鏈上
function updateWeather (uint256 _temperature) public {
temperature = _temperature;
}
}
上面的代碼非常簡(jiǎn)單,定義了一個(gè)變量用來(lái)存儲(chǔ)結(jié)果,一個(gè)方法用于發(fā)出請(qǐng)求,一個(gè)方法用于接收結(jié)果。
鏈下,我們啟動(dòng)一個(gè)進(jìn)程,以訂閱topic的方式獲取日志信息,之后通過(guò)構(gòu)建一個(gè)transaction,提交一個(gè)結(jié)果到合約中。
func SubscribeEventLog() {
topic := crypto.Keccak256([]byte(“RequestTemperature(bytes)”))
query := ethereum.FilterQuery{
Topics: [][]common.Hash{
{
common.BytesToHash(topic),
},
},
}
// 訂閱相關(guān)主題的日志事件
events := make(chan types.Log)
sub, err := EthClient.SubscribeFilterLogs(ctx, query, events)
// 加載合約的ABI文件
ta, err := abi.JSON(strings.NewReader(AbiJsonStr))
// 監(jiān)聽(tīng)事件訂閱
for {
select {
case err := 《-sub.Err():
log.Error(err)
break
case ev := 《-events:
// 獲取到訂閱的消息
ej, _ := ev.MarshalJSON()
log.Info(string(ej))
// 解析數(shù)據(jù)
var sampleEvent struct {
City []byte
}
err = ta.Unpack(&sampleEvent, “RequestTemperature”, ev.Data)
log.Info(string(sampleEvent.City))
// 構(gòu)建交易提交結(jié)果,需要提供私鑰用于簽署交易
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)
// 調(diào)用合約中的updateWeather方法,回填數(shù)據(jù)“29”
tx, err := instance.UpdateWeather(auth, big.NewInt(29))
log.Info(tx.Hash().Hex())
}
用一個(gè)圖來(lái)展示這個(gè)過(guò)程:
Chainlink
Chainlink是一個(gè)去中心化的預(yù)言機(jī)項(xiàng)目,它的作用就是以最安全的方式向區(qū)塊鏈提供現(xiàn)實(shí)世界中產(chǎn)生的數(shù)據(jù)。Chainlink在基本的預(yù)言機(jī)原理的實(shí)現(xiàn)方式之上,圍繞LINK token通過(guò)經(jīng)濟(jì)激勵(lì)建立了一個(gè)良性循環(huán)的生態(tài)系統(tǒng)。Chainlink預(yù)言機(jī)需要通過(guò)LINK token的轉(zhuǎn)賬來(lái)實(shí)現(xiàn)觸發(fā)。
LINK是以太坊網(wǎng)絡(luò)上的ERC677合約,關(guān)于各類ERC token的區(qū)別,請(qǐng)參考這篇文章。
在《精通以太坊(Matering Ethereum)》一書(shū)中,提出了三種預(yù)言機(jī)的設(shè)計(jì)模式,分別是
· 立即讀?。╥mmediate-read)
· 發(fā)布/訂閱(publish–subscribe)
· 請(qǐng)求/響應(yīng)(request–response)
而基于LINK ERC677 token完成的預(yù)言機(jī)功能,就屬于其中的請(qǐng)求/響應(yīng)模式。這是一種較為復(fù)雜的模式,上圖中展示的是一個(gè)不含有聚合過(guò)程的簡(jiǎn)單請(qǐng)求/相應(yīng)流程。
我們以Chainlink提供的TestnetConsumer合約中的一個(gè)requestEthereumPrice 方法為例來(lái)簡(jiǎn)單講一下請(qǐng)求響應(yīng)的流程。這個(gè)函數(shù)定義如下:
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);
}
它所實(shí)現(xiàn)的功能就是從指定的API(cryptocompare)獲取ETH/USD的交易價(jià)格。函數(shù)傳入的參數(shù)是指定的oracle地址和jobId。
將一些列的請(qǐng)求參數(shù)組好后,調(diào)用sendChainlinkRequestTo 方法將請(qǐng)求發(fā)出。sendChainlinkRequestTo是定義在Chainlink提供的庫(kù)中的一個(gè)接口方法,定義如下:
/**
* @notice 向指定的oracle地址創(chuàng)建一個(gè)請(qǐng)求
* @dev 創(chuàng)建并存儲(chǔ)一個(gè)請(qǐng)求ID, 增加本地的nonce值, 并使用`transferAndCall` 方法發(fā)送LINK,
* 創(chuàng)建到目標(biāo)oracle合約地址的請(qǐng)求
* 發(fā)出 ChainlinkRequested 事件。
* @param _oracle 發(fā)送請(qǐng)求至的oracle地址
* @param _req 完成初始化的Chainlink請(qǐng)求
* @param _payment 請(qǐng)求發(fā)送的LINK數(shù)量
* @return 請(qǐng)求 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;
}
其中l(wèi)ink.transferAndCall方法即是ERC677定義的token轉(zhuǎn)賬方法,與ERC20的transfer方法相比,它多了一個(gè)data字段,可以在轉(zhuǎn)賬的同時(shí)攜帶數(shù)據(jù)。這里就將之前打包好的請(qǐng)求數(shù)據(jù)放在了data字段,跟隨轉(zhuǎn)賬一起發(fā)送到了oracle合約。transferAndCall 方法定義如下:
/**
* @dev 將token和額外數(shù)據(jù)一起轉(zhuǎn)移給一個(gè)合約地址
* @param _to 轉(zhuǎn)移到的目的地址
* @param _value 轉(zhuǎn)移數(shù)量
* @param _data 傳遞給接收合約的額外數(shù)據(jù)
*/
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);是發(fā)出一個(gè)事件日志:
event Transfer(address indexed from, address indexed to, uint value, bytes data);
將這次轉(zhuǎn)賬的詳細(xì)信息(發(fā)送方、接收方、金額、數(shù)據(jù))記錄到日志中。
Oracle合約在收到轉(zhuǎn)賬之后,會(huì)觸發(fā)onTokenTransfer方法,該方法會(huì)檢查轉(zhuǎn)賬的有效性,并通過(guò)發(fā)出OracleRequest事件記錄更為詳細(xì)的數(shù)據(jù)信息:
event OracleRequest(
bytes32 indexed specId,
address requester,
bytes32 requestId,
uint256 payment,
address callbackAddr,
bytes4 callbackFunctionId,
uint256 cancelExpiration,
uint256 dataVersion,
bytes data
);
這個(gè)日志會(huì)在oracle合約的日志中找到,如圖中下方所示。鏈下的節(jié)點(diǎn)會(huì)訂閱該主題的日志,在獲取到記錄的日志信息之后,節(jié)點(diǎn)會(huì)解析出請(qǐng)求的具體信息,通過(guò)網(wǎng)絡(luò)的API調(diào)用,獲取到請(qǐng)求的結(jié)果。之后通過(guò)提交事務(wù)的方式,調(diào)用Oracle合約中的fulfillOracleRequest方法,將數(shù)據(jù)提交到鏈上。fulfillOracleRequest定義如下:
/**
* @notice 由Chainlink節(jié)點(diǎn)調(diào)用來(lái)完成請(qǐng)求
* @dev 提交的參數(shù)必須是`oracleRequest`方法所記錄的哈希參數(shù)
* 將會(huì)調(diào)用回調(diào)地址的回調(diào)函數(shù),`require`檢查時(shí)不會(huì)報(bào)錯(cuò),以便節(jié)點(diǎn)可以獲得報(bào)酬
* @param _requestId 請(qǐng)求ID必須與請(qǐng)求者所匹配
* @param _payment 為Oracle發(fā)放付款金額 (以wei為單位)
* @param _callbackAddress 完成方法的回調(diào)地址
* @param _callbackFunctionId 完成方法的回調(diào)函數(shù)
* @param _expiration 請(qǐng)求者可以取消之前節(jié)點(diǎn)應(yīng)響應(yīng)的到期時(shí)間
* @param _data 返回給消費(fèi)者合約的數(shù)據(jù)
* @return 外部調(diào)用成功的狀態(tài)值
*/
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
}
這個(gè)方法會(huì)在進(jìn)行一系列的檢驗(yàn)之后,會(huì)將結(jié)果通過(guò)之前記錄的回調(diào)地址與回調(diào)函數(shù),返回給消費(fèi)者合約:
_callbackAddress.call(_callbackFunctionId, _requestId, _data);
這樣一次請(qǐng)求就全部完成了。
總結(jié)
本文從預(yù)言機(jī)的概念開(kāi)始,通過(guò)一個(gè)簡(jiǎn)單的獲取ETH價(jià)格的例子,講解了請(qǐng)求/響應(yīng)模式的Chainlink預(yù)言機(jī)的基本過(guò)程,希望對(duì)你理解預(yù)言機(jī)與Chainlink的運(yùn)行原理有所幫助。
責(zé)任編輯;zl
評(píng)論
查看更多