對(duì)于 Web3 應(yīng)用來說,獲取加密資產(chǎn)的價(jià)格數(shù)據(jù)是一個(gè)很常見的要求,許多協(xié)議都需要依賴于高質(zhì)量且及時(shí)更新的數(shù)據(jù)來運(yùn)營(yíng)DeFi 應(yīng)用并且保證其安全性。除此之外,智能合約開發(fā)者有的時(shí)候也需要獲取加密資產(chǎn)的歷史數(shù)據(jù)。
在這篇文章中,我們將演示如何從 Chainlink Price Feeds 中獲得歷史價(jià)格數(shù)據(jù),并且在鏈上驗(yàn)證獲得的結(jié)果,你可以在這里查看代碼。
獲得歷史價(jià)格數(shù)據(jù)的需求
在過去的幾年中,我們見證了 DeFi 爆炸式增長(zhǎng),這些 DeFi 協(xié)議的一個(gè)共同的需求是它們需要非常安全,準(zhǔn)確和值得信任的數(shù)據(jù)。Chainlink Price Feeds 已經(jīng)成為了在 DeFi 生態(tài)中最常被使用的價(jià)格預(yù)言機(jī),并且集成進(jìn)來數(shù)十個(gè)百億美元級(jí)別的協(xié)議,比如 Aave,Synthetix 和 Trader Joe。
Price Feeds 最常見的使用場(chǎng)景是從既定的資產(chǎn)對(duì)中獲取最新的價(jià)格數(shù)據(jù)。然而,有的時(shí)候 DeFi 協(xié)議或者 dApp 也會(huì)有查看某個(gè)資產(chǎn)在某個(gè)時(shí)間點(diǎn)的的歷史價(jià)格。相關(guān)案例很多,比如一個(gè)金融產(chǎn)品會(huì)比較不同時(shí)間段的資產(chǎn)價(jià)格,比如加密資產(chǎn)保險(xiǎn)就會(huì)使用歷史數(shù)據(jù)來計(jì)算出來市場(chǎng)波動(dòng)率,然后動(dòng)態(tài)調(diào)整保證金。
歷史價(jià)格數(shù)據(jù)可以通過很多市場(chǎng)數(shù)據(jù)的 API 被獲得,然后通過 Chainlink Any API 的功能在鏈上獲取。然而這個(gè)解決方案有安全性的顧慮:就是數(shù)據(jù)源和傳遞數(shù)據(jù)的預(yù)言機(jī)可能是中心化的,并且沒有辦法驗(yàn)證這個(gè)數(shù)據(jù)是否是準(zhǔn)確的。就像實(shí)時(shí)價(jià)格數(shù)據(jù)一樣,歷史價(jià)格數(shù)據(jù)也需要去中心化的方式獲得,同時(shí)也需要足夠多的數(shù)據(jù)源,這就要求不能使用單一的數(shù)據(jù)源,交易所或者 API。
Chainlink Price Feeds 通過多個(gè)數(shù)據(jù)提供商,提供去中心化的,高質(zhì)量的價(jià)格數(shù)據(jù)解決了問題。當(dāng)需要?dú)v史價(jià)格數(shù)據(jù)的時(shí)候,Price Feeds 可以保證價(jià)格數(shù)據(jù)是準(zhǔn)確的,同時(shí)某個(gè)時(shí)間點(diǎn)的價(jià)格數(shù)據(jù)來源是整個(gè)市場(chǎng),而不是單一交易所。
當(dāng)然,還有一些其他解決方案來獲取 Chainlink Price Feed 的數(shù)據(jù),比如 Reputation.link’s API 或者是 Graph 的一個(gè) subGraph。盡管這些都是有效的解決方案,但是它們還是依賴于單一 API 或者是鏈下數(shù)據(jù)是正確的這個(gè)前提條件。
在這個(gè)解決方案中,我們將展示通過使用 Chainlink 語言來進(jìn)行必要的鏈下計(jì)算,然后從 Chainlink Price Feeds 中獲得歷史數(shù)據(jù)。通過使用 Chainlink Price Feeds 這個(gè)入口,用戶合約可以通過信任最小化的方式在鏈上獲得歷史價(jià)格數(shù)據(jù)。
Price Feed 合約簡(jiǎn)介
從用戶合約的視角來看,Chainlink Price Feeds 智能合約大體上可以分為兩個(gè)類型:代理合約和聚合合約。
代理合約代表一個(gè)價(jià)格對(duì)(像是 ETH/USD),同時(shí)也是用戶要交互的合約。這個(gè)合約有多個(gè)函數(shù),可以根據(jù)特定的參數(shù)獲取 round 數(shù)據(jù),比如說獲取某個(gè)交易對(duì)最新的 round 數(shù)據(jù)或者在某個(gè) round 中獲取價(jià)格數(shù)據(jù)。
代理合約中有所有的聚合合約的信息,同時(shí)也知道現(xiàn)在正在使用哪一個(gè)聚合合約??梢酝ㄟ^ aggregator
或者給 phaseAggregator
傳入正確 phase ID 來獲得這些信息。在下面的例子中,我們可以看到 Kovan ETH/USD 這個(gè)代理合約中,第二個(gè)聚合合約正在被使用(phase ID = 2)。
獲得 Kovan ETH/USD 中第二個(gè)聚合合約
獲得 ETH/USD 代理合約中當(dāng)前使用的聚合合約
這些聚合合約在實(shí)現(xiàn)的時(shí)候稍有不同,這取決于它們是在哪個(gè)版本上開發(fā)的(FluxAggregator,legacy Aggregator 等等),但是它們都存儲(chǔ)了聚合以后的 round 數(shù)據(jù),同時(shí)也都有一個(gè)函數(shù)讓預(yù)言機(jī)可以提交價(jià)格數(shù)據(jù)。
Chainlink Price Feeds 合約架構(gòu)
解決方案概述
Chainlink Price Feeds 的價(jià)格數(shù)據(jù)是存在鏈上的,同時(shí)開發(fā)者可以執(zhí)行 getLatestRoundData
函數(shù)來獲取某一個(gè)交易對(duì)最新的價(jià)格數(shù)據(jù)。然而,從 Price Feeds 中獲取歷史數(shù)據(jù)卻并不簡(jiǎn)單,過程比較復(fù)雜。
Chainlink Price Feeds 存儲(chǔ)聚合以后的價(jià)格數(shù)據(jù),每一個(gè)數(shù)據(jù)都會(huì)有一個(gè)獨(dú)特的 round ID。當(dāng)價(jià)格對(duì)的波動(dòng)超過特性的波動(dòng)閾值,或者超過心跳時(shí)間上限,一個(gè)新的 round 就會(huì)產(chǎn)生。如果開發(fā)者知道歷史價(jià)格數(shù)據(jù)的 round ID,那么可以很容易通過 getHistoricaPrice 這個(gè)函數(shù)找到這個(gè)歷史價(jià)格。然而,roundID 和時(shí)間戳,區(qū)塊,或者任何其他可以被用來決定時(shí)間的變量都沒有直接的聯(lián)系。
另外要注意的是,Chainlink Price Feed 有很多版本的聚合合約,比如 FluxAggregator 和使用 Off-Chain Reporting 的 OffchainAggregator 合約。一個(gè)交易對(duì)的喂價(jià)有可能在以前的一段時(shí)間里使用的是 FluxAggregator,然后換成兼容 OCR 的 OffchainAggregator 來更新價(jià)格。所以在找到對(duì)應(yīng)時(shí)間的 round ID 的時(shí)候,需要分辨不同的聚合合約版本。
另外因?yàn)?round ID 在面向用戶的代理合約和底層的聚合合約中,是被有目的地分成不同的 phase,所以代理合約中的 round ID 比聚合合約中的要大很多。代理合約中的 round ID 總是需要自增 1,而底層的聚合合約在每次部署的時(shí)候 round ID 都是重新從 1 開始計(jì)數(shù)。通過下面的方法,可以由聚合合約的 round ID 計(jì)算出代理合約的 round ID,首先 phase 是基于代理合約部署的聚合合約的順序(第一次,第二次,第三次等等),originalRoundId 就是聚合合約部署的 round。你可以通過調(diào)用代理合約的 phaseAggregators
這個(gè) getter 方法來獲得 phase 參數(shù)。
external proxy round ID = uint80(uint256(phase) << 64 | originalRoundId);
最后,并不是每一個(gè) round 都會(huì)有價(jià)格數(shù)據(jù)。有一些 round(主要是在測(cè)試網(wǎng))可能沒有價(jià)格或者時(shí)間戳數(shù)據(jù),主要是因?yàn)楫?dāng)時(shí)連接超時(shí)或者一些環(huán)境問題。
因?yàn)檫@些復(fù)雜性,在鏈上獲得一個(gè)可驗(yàn)證且準(zhǔn)確的歷史數(shù)據(jù)是很復(fù)雜的,比如你需要做一個(gè)循環(huán),就要去遍歷大量的 round 數(shù)據(jù),或者在鏈上存儲(chǔ)一個(gè) round 和時(shí)間戳的 mapping,但是這些操作都非常貴。
除了能夠給智能合約提供鏈上數(shù)據(jù)和時(shí)間,Chainlink 去中心化預(yù)言機(jī)網(wǎng)絡(luò)還提供了一個(gè)通用的框架來做鏈下運(yùn)算,這樣用戶就不用存儲(chǔ)大量的鏈上數(shù)據(jù),也不用對(duì)未知數(shù)量的 round 做循環(huán)了。鏈上合約還可以通過一個(gè)運(yùn)行著外部適配器(external adapter)的預(yù)言機(jī)來獲得某個(gè)時(shí)間點(diǎn)的特定交易對(duì)價(jià)格。預(yù)言機(jī)把 round ID 返回到鏈上,然后用戶合約可以馬上使用這個(gè)數(shù)據(jù),并通過歷史數(shù)據(jù) API 驗(yàn)證鏈上數(shù)據(jù)來驗(yàn)證數(shù)據(jù)的準(zhǔn)確性,驗(yàn)證方式是通過 round ID 獲得價(jià)格數(shù)據(jù),然后與返回的價(jià)格數(shù)據(jù)比較。
這個(gè)解決方案所基于的概念是:預(yù)言機(jī)會(huì)處理任何區(qū)塊鏈自身所不能處理的數(shù)據(jù),或者區(qū)塊鏈因?yàn)槿萘肯拗坪托什荒芑虿粦?yīng)該處理的問題。除此以外,通過這種方式獲得歷史價(jià)格數(shù)據(jù),還有很多優(yōu)勢(shì):
- 不需要在鏈上存儲(chǔ)大量的數(shù)據(jù),或者對(duì)鏈上數(shù)據(jù)進(jìn)行大量的循環(huán)檢查。
- 使用鏈上的函數(shù)獲取歷史數(shù)據(jù),和從鏈下取得的數(shù)據(jù)比較,杜絕惡意預(yù)言機(jī)提供的錯(cuò)誤數(shù)據(jù)的風(fēng)險(xiǎn)。
- 外部適配器是無狀態(tài)的,不會(huì)像 Chainlink 節(jié)點(diǎn)運(yùn)營(yíng)商一樣存儲(chǔ)數(shù)據(jù)并且提供一個(gè)通用的方法來獲取歷史數(shù)據(jù)。
- 外部適配器提供的解決方案不依賴于外部 API 或者其他系統(tǒng),它直接與鏈上數(shù)據(jù)交互。
在智能合約中使用外部適配器
怎樣使用 Chainlink 預(yù)言機(jī)獲得加密資產(chǎn)歷史價(jià)格數(shù)據(jù)
創(chuàng)建一個(gè)歷史價(jià)格數(shù)據(jù)請(qǐng)求
在初始化一個(gè)歷史價(jià)格數(shù)據(jù)的請(qǐng)求之前,用戶合約需要給一個(gè)預(yù)言機(jī)提交一個(gè) API 請(qǐng)求,這個(gè)預(yù)言機(jī)在自己的任務(wù)中需要運(yùn)營(yíng)一個(gè)自定義的歷史價(jià)格數(shù)據(jù)的外部適配器。在發(fā)送請(qǐng)求的參數(shù)中,用戶合約需要傳入有效的代理合約地址和時(shí)間戳(Unix 時(shí)間)以返回價(jià)格數(shù)據(jù)。
function getHistoricalPrice(address _proxyAddress, uint _unixTime) public returns (bytes32
requestId)
{
Chainlink.Request memory request = buildChainlinkRequest(jobId, address(this),
this.singleResponseFulfill.selector);
// Set the URL to perform the GET request on
request.add("proxyAddress", addressToString(_proxyAddress));
request.add("unixDateTime", uint2str(_unixTime));
//set the timestamp being searched, we will use it for verification after
searchTimestamp = _unixTime;
//reset any previous values
answerRound = 0;
previousRound = 0;
nextRound = 0;
nextPrice = 0;
nextPriceTimestamp = 0;
previousPrice = 0;
previousPriceTimestamp = 0;
priceAnswer = 0;
priceTimestamp = 0;
// Sends the request
return sendChainlinkRequestTo(oracle, request, fee);
}
歷史價(jià)格數(shù)據(jù)外部適配器
一旦 Chainlink 節(jié)點(diǎn)收到這個(gè)請(qǐng)求,它會(huì)將輸入信息發(fā)送給歷史價(jià)格外部適配器,這個(gè)外部適配器將會(huì)找時(shí)間戳所對(duì)應(yīng)的 round ID,以及這個(gè) round ID 之前和之后的 round ID。這兩個(gè)額外的 round ID 在驗(yàn)證環(huán)節(jié)中會(huì)被用到,我們需要通過這些信息來驗(yàn)證價(jià)格數(shù)據(jù)的時(shí)間是最接近所查找的時(shí)間戳的,在這個(gè) round 中 updatedAt 這個(gè)時(shí)間戳一定要比要搜索的時(shí)間戳參數(shù)要小。
一旦外部適配器接收到代理合約的地址和要查找的時(shí)間戳參數(shù)之后,它會(huì)進(jìn)行以下的計(jì)算:
- 對(duì)傳入的地址和時(shí)間戳進(jìn)行驗(yàn)證
- 確定哪一個(gè)聚合合約包含要尋找的時(shí)間戳的 round
- 在上一步找到的聚合合約中,獲取存儲(chǔ)在聚合合約中的 round ID 列表(使用 eth_getlogs)
- 在返回的 round ID 列表中,進(jìn)行二分查找,找到包含時(shí)間戳的round ID。這個(gè)搜索的時(shí)間復(fù)雜度是 O(logN),相比之下,線性搜索的的時(shí)間復(fù)雜度更高是 O(N)
- 如果找到的 round 是空或者無效(因?yàn)槌瑫r(shí)的原因等等),這個(gè)算法會(huì)分辨出在二分查找中的空值,然后把這些 round 排除掉,再在新的列表中進(jìn)行新的二分查找。
- 當(dāng)被查找的 round ID 被找到之后,外部適配器會(huì)使用這個(gè)聚合合約的 round ID 以及它前面和后面的 round ID,算出這三個(gè) round ID 在代理合約中對(duì)應(yīng)的 round ID,然后在適配器中返回
roundAnswer
,earlierRoundAnswer
和laterRoundAnswer
這三個(gè)值。 - 處理歷史價(jià)格數(shù)據(jù)的請(qǐng)求的預(yù)言機(jī)會(huì)獲取上一步的三個(gè)值,然后在通過 multi-variable response 的功能鏈上返回給鏈上的用戶合約。
{
"jobRunID": "534ea675a9524e8e834585b00368b178",
"data": {
"roundAnswer": "36893488147419111519",
"earlierRoundAnswer": "36893488147419111518",
"laterRoundAnswer": "36893488147419111520"
},
"result": null,
"statusCode": 200
}
在鏈上驗(yàn)證結(jié)果
使用中心化數(shù)據(jù)源或者預(yù)言機(jī)來獲得歷史價(jià)格數(shù)據(jù),會(huì)給智能合約帶來潛在的安全風(fēng)險(xiǎn)。然而,在這個(gè)例子中,對(duì)于預(yù)言機(jī)返回的 round ID,我們可以利用現(xiàn)存的歷史價(jià)格數(shù)據(jù)函數(shù)在鏈上驗(yàn)證 round ID,這樣就可以用信任最小化的方式驗(yàn)證獲取到的歷史價(jià)格數(shù)據(jù)。在這種方式中,數(shù)據(jù)還是從鏈上獲取的,只不過 round ID 是由外部的預(yù)言機(jī)計(jì)算出來的。
我們可以通過以下的方式來驗(yàn)證 round 數(shù)據(jù),然后使用這個(gè)數(shù)據(jù)來獲取到最終的歷史數(shù)據(jù):
首先,我們需要驗(yàn)證三個(gè) round(answerRound, previousRound, nextRound) 中包含的有效的返回 round 數(shù)據(jù)
//verify the responses
//first get back the responses for each round
(
uint80 id,
int price,
uint startedAt,
uint timeStamp,
uint80 answeredInRound
) = priceFeed.getRoundData(_answerRound);
require(timeStamp > 0, "Round not complete");
priceAnswer = price;
priceTimestamp = timeStamp;
(
id,
price,
startedAt,
timeStamp,
answeredInRound
) = priceFeed.getRoundData(_previousRound);
require(timeStamp > 0, "Round not complete");
previousPrice = price;
previousPriceTimestamp = timeStamp;
(
id,
price,
startedAt,
timeStamp,
answeredInRound
) = priceFeed.getRoundData(_previousRound);
require(timeStamp > 0, "Round not complete");
nextPrice = price;
nextPriceTimestamp = timeStamp;
下一步,我們驗(yàn)證這些 round 數(shù)據(jù)以及它們包含的時(shí)間戳。
- 保證 round 的順序是正確的
- 保證三個(gè) round 中包含的時(shí)間戳?xí)r間順序是對(duì)的
- 如果這些 round 中有任何的間隔(比如 previousRound = 1625097600 而 answerRound = 1625097605),要確保在這兩個(gè) ID 之間沒有任何有效的 round ID。所以在這個(gè)例子中,previousRound = 1625097600 并且 answerRound = 1625097605,這個(gè)合約需要保證 1625097601, 1625097602, 1625097603 和 1625097604 這些 round 不會(huì)返回有效數(shù)據(jù)
//first, make sure order of rounds is correct
require(previousPriceTimestamp < timeStamp, "Previous price timetamp must be < answer
timestamp");
require(timeStamp < nextPriceTimestamp, "Answer timetamp must be < next round
timestamp");
//next, make sure prev round is before timestamp that was searched, and next round is
after
require(previousPriceTimestamp < searchTimestamp, "Previous price timetamp must be <
search timestamp");
require(searchTimestamp < nextPriceTimestamp, "Search timetamp must be < next round
timestamp");
require(priceTimestamp <= searchTimestamp, "Answer timetamp must be less than or equal to
searchTimestamp timestamp");
//check if gaps in round numbers, and if so, ensure there's no valid data in between
if (answerRound - previousRound > 1) {
for (uint80 i= previousRound; i<answerRound; i++) {
(uint80 id,
int price,
uint startedAt,
uint timeStamp,
uint80 answeredInRound
) = priceFeed.getRoundData(i);
require(timeStamp == 0, "Missing Round Data");
}
}
if (nextRound - answerRound > 1) {
for (uint80 i= answerRound; i<nextRound; i++) {
(uint80 id,
int price,
uint startedAt,
uint timeStamp,
uint80 answeredInRound
) = priceFeed.getRoundData(i);
require(timeStamp == 0, "Missing Round Data");
}
}
如果上述的檢查都可以通過,那么返回的 round (answerRound) 的價(jià)格數(shù)據(jù),就是某個(gè)交易對(duì)在某個(gè)時(shí)間點(diǎn)上經(jīng)過驗(yàn)證的歷史價(jià)格數(shù)據(jù)。
總結(jié)
Chainlink Price Feeds 提供一種方法讓 Solidity 智能合約獲得高質(zhì)量的價(jià)格數(shù)據(jù)。除此之外,Chainlink 的預(yù)言機(jī)框架可以實(shí)現(xiàn)鏈下計(jì)算,允許開發(fā)者可以通過信任最小化的模式,獲得安全可驗(yàn)證的歷史價(jià)格數(shù)據(jù)。文章來源:http://www.zghlxwxcb.cn/news/detail-423405.html
您可以關(guān)注 Chainlink 預(yù)言機(jī)并且私信加入開發(fā)者社區(qū),有大量關(guān)于智能合約的學(xué)習(xí)資料以及關(guān)于區(qū)塊鏈的話題!文章來源地址http://www.zghlxwxcb.cn/news/detail-423405.html
到了這里,關(guān)于如何通過 Chainlink Price Feeds獲得加密資產(chǎn)的歷史價(jià)格的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!