对于 Web3 利用来说,获取加密资产的价格数据是一个很常见的要求,许多协定都须要依赖于高质量且及时更新的数据来经营 DeFi 利用并且保障其安全性。除此之外,智能合约开发者有的时候也须要获取加密资产的历史数据。
在这篇文章中,咱们将演示如何从 Chainlink Price Feeds 中取得历史价格数据,并且在链上验证取得的后果,你能够在这里查看代码。
取得历史价格数据的需要
在过来的几年中,咱们见证了 DeFi 爆炸式增长,这些 DeFi 协定的一个独特的需要是它们须要十分平安,精确和值得信赖的数据。Chainlink Price Feeds 曾经成为了在 DeFi 生态中最常被应用的价格预言机,并且集成进来数十个百亿美元级别的协定,比方 Aave,Synthetix 和 Trader Joe。
Price Feeds 最常见的应用场景是从既定的资产对中获取最新的价格数据。然而,有的时候 DeFi 协定或者 dApp 也会有查看某个资产在某个工夫点的的历史价格。相干案例很多,比方一个金融产品会比拟不同时间段的资产价格,比方加密资产保险就会应用历史数据来计算出来市场稳定率,而后动静调整保证金。
历史价格数据能够通过很多市场数据的 API 被取得,而后通过 Chainlink Any API 的性能在链上获取。然而这个解决方案有安全性的顾虑:就是数据源和传递数据的预言机可能是中心化的,并且没有方法验证这个数据是否是精确的。就像实时价格数据一样,历史价格数据也须要去中心化的形式取得,同时也须要足够多的数据源,这就要求不能应用繁多的数据源,交易所或者 API。
Chainlink Price Feeds 通过多个数据提供商,提供去中心化的,高质量的价格数据解决了问题。当须要历史价格数据的时候,Price Feeds 能够保证价格数据是精确的,同时某个工夫点的价格数据起源是整个市场,而不是繁多交易所。
当然,还有一些其余解决方案来获取 Chainlink Price Feed 的数据,比方 Reputation.link’s API 或者是 Graph 的一个 subGraph。只管这些都是无效的解决方案,然而它们还是依赖于繁多 API 或者是链下数据是正确的这个前提条件。
在这个解决方案中,咱们将展现通过应用 Chainlink 语言来进行必要的链下计算,而后从 Chainlink Price Feeds 中取得历史数据。通过应用 Chainlink Price Feeds 这个入口,用户合约能够通过信赖最小化的形式在链上取得历史价格数据。
Price Feed 合约简介
从用户合约的视角来看,Chainlink Price Feeds 智能合约大体上能够分为两个类型:代理合约和聚合合约。
代理合约代表一个价格对(像是 ETH/USD),同时也是用户要交互的合约。这个合约有多个函数,能够依据特定的参数获取 round 数据,比如说获取某个交易对最新的 round 数据或者在某个 round 中获取价格数据。
代理合约中有所有的聚合合约的信息,同时也晓得当初正在应用哪一个聚合合约。能够通过 aggregator
或者给 phaseAggregator
传入正确 phase ID 来取得这些信息。在上面的例子中,咱们能够看到 Kovan ETH/USD 这个代理合约中,第二个聚合合约正在被应用(phase ID = 2)。
取得 Kovan ETH/USD 中第二个聚合合约
取得 ETH/USD 代理合约中以后应用的聚合合约
这些聚合合约在实现的时候稍有不同,这取决于它们是在哪个版本上开发的(FluxAggregator,legacy Aggregator 等等),然而它们都存储了聚合当前的 round 数据,同时也都有一个函数让预言机能够提交价格数据。
Chainlink Price Feeds 合约架构
解决方案概述
Chainlink Price Feeds 的价格数据是存在链上的,同时开发者能够执行 getLatestRoundData
函数来获取某一个交易对最新的价格数据。然而,从 Price Feeds 中获取历史数据却并不简略,过程比较复杂。
Chainlink Price Feeds 存储聚合当前的价格数据,每一个数据都会有一个独特的 round ID。当价格对的稳定超过个性的稳定阈值,或者超过心跳工夫下限,一个新的 round 就会产生。如果开发者晓得历史价格数据的 round ID,那么能够很容易通过 getHistoricaPrice 这个函数找到这个历史价格。然而,roundID 和工夫戳,区块,或者任何其余能够被用来决定工夫的变量都没有间接的分割。
另外要留神的是,Chainlink Price Feed 有很多版本的聚合合约,比方 FluxAggregator 和应用 Off-Chain Reporting 的 OffchainAggregator 合约。一个交易对的喂价有可能在以前的一段时间里应用的是 FluxAggregator,而后换成兼容 OCR 的 OffchainAggregator 来更新价格。所以在找到对应工夫的 round ID 的时候,须要分辨不同的聚合合约版本。
另外因为 round ID 在面向用户的代理合约和底层的聚合合约中,是被有目的地分成不同的 phase,所以代理合约中的 round ID 比聚合合约中的要大很多。代理合约中的 round ID 总是须要自增 1,而底层的聚合合约在每次部署的时候 round ID 都是从新从 1 开始计数。通过上面的办法,能够由聚合合约的 round ID 计算出代理合约的 round ID,首先 phase 是基于代理合约部署的聚合合约的程序(第一次,第二次,第三次等等),originalRoundId 就是聚合合约部署的 round。你能够通过调用代理合约的 phaseAggregators
这个 getter 办法来取得 phase 参数。
external proxy round ID = uint80(uint256(phase) << 64 | originalRoundId);
最初,并不是每一个 round 都会有价格数据。有一些 round(次要是在测试网)可能没有价格或者工夫戳数据,次要是因为过后连贯超时或者一些环境问题。
因为这些复杂性,在链上取得一个可验证且精确的历史数据是很简单的,比方你须要做一个循环,就要去遍历大量的 round 数据,或者在链上存储一个 round 和工夫戳的 mapping,然而这些操作都十分贵。
除了可能给智能合约提供链上数据和工夫,Chainlink 去中心化预言机网络还提供了一个通用的框架来做链下运算,这样用户就不必存储大量的链上数据,也不必对未知数量的 round 做循环了。链上合约还能够通过一个运行着内部适配器(external adapter)的预言机来取得某个工夫点的特定交易对价格。预言机把 round ID 返回到链上,而后用户合约能够马上应用这个数据,并通过历史数据 API 验证链上数据来验证数据的准确性,验证形式是通过 round ID 取得价格数据,而后与返回的价格数据比拟。
这个解决方案所基于的概念是:预言机会解决任何区块链本身所不能解决的数据,或者区块链因为容量限度和效率不能或不应该解决的问题。除此以外,通过这种形式取得历史价格数据,还有很多劣势:
- 不须要在链上存储大量的数据,或者对链上数据进行大量的循环查看。
- 应用链上的函数获取历史数据,和从链下获得的数据比拟,杜绝歹意预言机提供的谬误数据的危险。
- 内部适配器是无状态的,不会像 Chainlink 节点运营商一样存储数据并且提供一个通用的办法来获取历史数据。
- 内部适配器提供的解决方案不依赖于内部 API 或者其余零碎,它间接与链上数据交互。
在智能合约中应用内部适配器
怎么应用 Chainlink 预言机取得加密资产历史价格数据
创立一个历史价格数据申请
在初始化一个历史价格数据的申请之前,用户合约须要给一个预言机提交一个 API 申请,这个预言机在本人的工作中须要经营一个自定义的历史价格数据的内部适配器。在发送申请的参数中,用户合约须要传入无效的代理合约地址和工夫戳(Unix 工夫)以返回价格数据。
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);
}
历史价格数据内部适配器
一旦 Chainlink 节点收到这个申请,它会将输出信息发送给历史价格内部适配器,这个内部适配器将会找工夫戳所对应的 round ID,以及这个 round ID 之前和之后的 round ID。这两个额定的 round ID 在验证环节中会被用到,咱们须要通过这些信息来验证价格数据的工夫是最靠近所查找的工夫戳的,在这个 round 中 updatedAt 这个工夫戳肯定要比要搜寻的工夫戳参数要小。
一旦内部适配器接管到代理合约的地址和要查找的工夫戳参数之后,它会进行以下的计算:
- 对传入的地址和工夫戳进行验证
- 确定哪一个聚合合约蕴含要寻找的工夫戳的 round
- 在上一步找到的聚合合约中,获取存储在聚合合约中的 round ID 列表(应用 eth_getlogs)
- 在返回的 round ID 列表中,进行二分查找,找到蕴含工夫戳的 round ID。这个搜寻的工夫复杂度是 O(logN),相比之下,线性搜寻的的工夫复杂度更高是 O(N)
- 如果找到的 round 是空或者有效(因为超时的起因等等),这个算法会分辨出在二分查找中的空值,而后把这些 round 排除掉,再在新的列表中进行新的二分查找。
- 当被查找的 round ID 被找到之后,内部适配器会应用这个聚合合约的 round ID 以及它后面和前面的 round ID,算出这三个 round ID 在代理合约中对应的 round ID,而后在适配器中返回
roundAnswer
,earlierRoundAnswer
和laterRoundAnswer
这三个值。 - 解决历史价格数据的申请的预言机会获取上一步的三个值,而后在通过 multi-variable response 的性能链上返回给链上的用户合约。
{
"jobRunID": "534ea675a9524e8e834585b00368b178",
"data": {
"roundAnswer": "36893488147419111519",
"earlierRoundAnswer": "36893488147419111518",
"laterRoundAnswer": "36893488147419111520"
},
"result": null,
"statusCode": 200
}
在链上验证后果
应用中心化数据源或者预言机来取得历史价格数据,会给智能合约带来潜在的平安危险。然而,在这个例子中,对于预言机返回的 round ID,咱们能够利用现存的历史价格数据函数在链上验证 round ID,这样就能够用信赖最小化的形式验证获取到的历史价格数据。在这种形式中,数据还是从链上获取的,只不过 round ID 是由内部的预言机计算出来的。
咱们能够通过以下的形式来验证 round 数据,而后应用这个数据来获取到最终的历史数据:
首先,咱们须要验证三个 round(answerRound, previousRound, nextRound)中蕴含的无效的返回 round 数据
//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;
下一步,咱们验证这些 round 数据以及它们蕴含的工夫戳。
- 保障 round 的程序是正确的
- 保障三个 round 中蕴含的工夫戳工夫程序是对的
-
如果这些 round 中有任何的距离(比方 previousRound = 1625097600 而 answerRound = 1625097605),要确保在这两个 ID 之间没有任何无效的 round ID。所以在这个例子中,previousRound = 1625097600 并且 answerRound = 1625097605,这个合约须要保障 1625097601, 1625097602, 1625097603 和 1625097604 这些 round 不会返回无效数据
//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) 的价格数据,就是某个交易对在某个工夫点上通过验证的历史价格数据。
总结
Chainlink Price Feeds 提供一种办法让 Solidity 智能合约取得高质量的价格数据。除此之外,Chainlink 的预言机框架能够实现链下计算,容许开发者能够通过信赖最小化的模式,取得平安可验证的历史价格数据。
您能够关注 Chainlink 预言机并且私信退出开发者社区,有大量对于智能合约的学习材料以及对于区块链的话题!