ChainLink 采用了分布式价格预言机的设计来为用户提供服务

作者:ZAN Team

封面:Chainlink

分布式价格预言机

当消费者请求预言机服务时,预言机可能会因为各种各样的原因无法及时响应,从而造成单点故障。因此,ChainLink 采用了分布式价格预言机的设计来为用户提供服务。例如,一个提供 BTC 美元价格的服务,聚合了 31 个价格预言机为用户提供服务。

图片

该聚合器的合约源码可以在 Etherscan 上查看:https://etherscan.io/address/0xae74faa92cb67a95ebcab07358bc222e33a34da7#readContract

其中,通过调用合约中的 transmitters 方法即可查看该聚合器包含的所有链下预言机。

function transmitters() external view    returns(address[] memory){      return s_transmitters;  }
图片

每一个链下预言机可以通过调用 transmit 方法来提供价格数据,以响应聚合器中用户的请求。这些链下预言机是一些 EOA 账户,他们不仅为 BTC/USD 聚合器提供价格数据,还可能为其他聚合器提供价格数据,例如 ETH/USD。

图片

链上合约:

struct Transmission {    int192 answer; // 192 bits ought to be enough for anyone    uint64 timestamp; }
enum Role {    // No oracle role has been set for address a    Unset,    // Signing address for the s_oracles[a].index'th oracle. I.e., report    // signatures from this oracle should ecrecover back to address a.    Signer,    // Transmission address for the s_oracles[a].index'th oracle. I.e., if a    // report is received by OffchainAggregator.transmit in which msg.sender is    // a, it is attributed to the s_oracles[a].index'th oracle.    Transmitter  }
struct Oracle {    uint8 index; // Index of oracle in s_signers/s_transmitters    Role role;   // Role of the address which mapped to this struct  }
struct HotVars {    // Provides 128 bits of security against 2nd pre-image attacks, but only    // 64 bits against collisions. This is acceptable, since a malicious owner has    // easier way of messing up the protocol than to find hash collisions.    bytes16 latestConfigDigest; // 提供抗弱碰撞性摘要    uint40 latestEpochAndRound; // 32 most sig bits for epoch, 8 least sig bits for round(round 是 leader 的标志)    uint8 threshold; // 敌手数量    uint32 latestAggregatorRoundId; // 链上最新的 roundId,链下 reporting 不会使用  }
struct ReportData {    HotVars hotVars; // Only read from storage once    bytes observers; // ith element is the index of the ith observer    int192[] observations; // ith element is the ith observation    bytes vs; // jth element is the v component of the jth signature    bytes32 rawReportContext;}
function transmit(    // NOTE: If these parameters are changed, expectedMsgDataLength and/or    // TRANSMIT_MSGDATA_CONSTANT_LENGTH_COMPONENT need to be changed accordingly    bytes calldata _report,    bytes32[] calldata _rs, bytes32[] calldata _ss, bytes32 _rawVs // signatures  )    external  {    uint256 initialGas = gasleft(); // This line must come first    // Make sure the transmit message-length matches the inputs.     require(msg.data.length == expectedMsgDataLength(_report, _rs, _ss),      "transmit message too long");    ReportData memory r;    {      r.hotVars = s_hotVars; // 读取上一轮的相关参数
      bytes32 rawObservers;      (r.rawReportContext, rawObservers, r.observations) = abi.decode(        _report, (bytes32, bytes32, int192[])      );
      // rawReportContext consists of:      // 11-byte zero padding      // 16-byte configDigest      // 4-byte epoch      // 1-byte round            bytes16 configDigest = bytes16(r.rawReportContext << 88);      require(        r.hotVars.latestConfigDigest == configDigest,        "configDigest mismatch"      );  // 检查是否存在 hash 碰撞
      uint40 epochAndRound = uint40(uint256(r.rawReportContext)); // 截取 uint256 的后 40 位            // direct numerical comparison works here, because      //      //   ((e,r) <= (e',r')) implies (epochAndRound <= epochAndRound')      //      // because alphabetic ordering implies e <= e', and if e = e', then r<=r',      // so e*256+r <= e'*256+r', because r, r' < 256      require(r.hotVars.latestEpochAndRound < epochAndRound, "stale report");            require(_rs.length > r.hotVars.threshold, "not enough signatures"); // 检查签名数量      require(_rs.length <= maxNumOracles, "too many signatures");      require(_ss.length == _rs.length, "signatures out of registration");      require(r.observations.length <= maxNumOracles,              "num observations out of bounds");      require(r.observations.length > 2 * r.hotVars.threshold,              "too few values to trust median");            // Copy signature parities in bytes32 _rawVs to bytes r.v      r.vs = new bytes(_rs.length);      for (uint8 i = 0; i < _rs.length; i++) {        r.vs[i] = _rawVs[i];      }
      // Copy observer identities in bytes32 rawObservers to bytes r.observers      r.observers = new bytes(r.observations.length);      bool[maxNumOracles] memory seen;      for (uint8 i = 0; i < r.observations.length; i++) {        uint8 observerIdx = uint8(rawObservers[i]);        require(!seen[observerIdx], "observer index repeated");        seen[observerIdx] = true;        r.observers[i] = rawObservers[i];      }            Oracle memory transmitter = s_oracles[msg.sender];      require( // Check that sender is authorized to report        transmitter.role == Role.Transmitter &&        msg.sender == s_transmitters[transmitter.index],        "unauthorized transmitter"      );      // record epochAndRound here, so that we don't have to carry the local      // variable in transmit. The change is reverted if something fails later.      r.hotVars.latestEpochAndRound = epochAndRound;    }
    { // Verify signatures attached to report      bytes32 h = keccak256(_report);      bool[maxNumOracles] memory signed;            Oracle memory o;      for (uint i = 0; i < _rs.length; i++) {        // v 取出来的值或者是 00 或 01。要使用时,我们先要将其转为整型,再加上 27,所以我们将得到 27 或 28。在调用函数时 v 将填入 27 或 28        address signer = ecrecover(h, uint8(r.vs[i])+27, _rs[i], _ss[i]);        o = s_oracles[signer];        require(o.role == Role.Signer, "address not authorized to sign");        require(!signed[o.index], "non-unique signature");        signed[o.index] = true;      }     }          { // Check the report contents, and record the result       for (uint i = 0; i < r.observations.length - 1; i++) {         bool inOrder = r.observations[i] <= r.observations[i+1];         require(inOrder, "observations not sorted");       }              int192 median = r.observations[r.observations.length/2];       require(minAnswer <= median && median <= maxAnswer, "median is out of min-max range");       r.hotVars.latestAggregatorRoundId++;       s_transmissions[r.hotVars.latestAggregatorRoundId] =         Transmission(median, uint64(block.timestamp));              emit NewTransmission(         r.hotVars.latestAggregatorRoundId,         median,         msg.sender,         r.observations,         r.observers,         r.rawReportContext       );       // Emit these for backwards compatability with offchain consumers       // that only support legacy events       emit NewRound(         r.hotVars.latestAggregatorRoundId,         address(0x0), // use zero address since we don't have anybody "starting" the round here         block.timestamp       );       emit AnswerUpdated(         median,         r.hotVars.latestAggregatorRoundId,         block.timestamp       );              validateAnswer(r.hotVars.latestAggregatorRoundId, median);     }     s_hotVars = r.hotVars;     assert(initialGas < maxUint32);     reimburseAndRewardOracles(uint32(initialGas), r.observers);   }

1. 首先,读取当前合约状态,并进行一系列的检查:

图片

2. 这些都通过后,可以进行一些准备工作了:

图片

3. 接下来是使用 ecrecover() 对对每一个签名数据进行验签,校验 hash 值是对 _report 做的 hash。同时还要检查签名者的角色是否是 Signer,且要检查签名的重复性。

4. 最后,检查观察值是否按照顺序排列好。再从排好顺序的观察值中选取中位数 median,并确保 median 不超过上下两个阈值。一切都没问题后,在 s_transmissions 中记录下本次预言机的 answer。此外,还要对 answer 进行校验:

function validateAnswer(uint32 _aggregatorRoundId, int256 _answer) private {    ValidatorConfig memory vc = s_validatorConfig;        if (address(vc.validator) == address(0)) {      return;    }
    uint32 prevAggregatorRoundId = _aggregatorRoundId - 1;    int256 prevAggregatorRoundAnswer = s_transmissions[prevAggregatorRoundId].answer;    require(      callWithExactGasEvenIfTargetIsNoContract(        vc.gasLimit,        address(vc.validator),        abi.encodeWithSignature(          "validate(uint256,int256,uint256,int256)",          uint256(prevAggregatorRoundId),          prevAggregatorRoundAnswer,          uint256(_aggregatorRoundId),          _answer        )      ),      "insufficient gas"    );  }

这里经过一系列(中间有 Proxy 合约)的 call 最终调用了 UniswapAnchoredView  合约 (Compound 使用的价格预言机) 的 validate 方法:

    function validate(uint256/* previousRoundId */,            int256 /* previousAnswer */,            uint256 /* currentRoundId */,            int256 currentAnswer) external override returns (bool valid) {
        // NOTE: We don't do any access control on msg.sender here. The access control is done in getTokenConfigByReporter,        // which will REVERT if an unauthorized address is passed.        TokenConfig memory config = getTokenConfigByReporter(msg.sender);        // 获取预言机 report 中的价格        uint256 reportedPrice = convertReportedPrice(config, currentAnswer);        // 获取 UniswapV2 价格预言机计算得到的价格        uint256 anchorPrice = calculateAnchorPriceFromEthPrice(config);
        PriceData memory priceData = prices[config.symbolHash];        if (priceData.failoverActive) {            require(anchorPrice < 2**248, "Anchor price too large");            prices[config.symbolHash].price = uint248(anchorPrice);            emit PriceUpdated(config.symbolHash, anchorPrice);        } else if (isWithinAnchor(reportedPrice, anchorPrice)) {            require(reportedPrice < 2**248, "Reported price too large");            prices[config.symbolHash].price = uint248(reportedPrice);            emit PriceUpdated(config.symbolHash, reportedPrice);            valid = true;        } else {            emit PriceGuarded(config.symbolHash, reportedPrice, anchorPrice);        }    }

关键是比较了两边预言机给的价格的偏差是否在一个范围内:

function isWithinAnchor(uint reporterPrice, uint anchorPriceinternal view returns (bool) {        if (reporterPrice > 0) {            uint anchorRatio = mul(anchorPrice, 100e16) / reporterPrice;            return anchorRatio <= upperBoundAnchorRatio && anchorRatio >= lowerBoundAnchorRatio;        }        return false;    }

Feed Registry

前面的使用方式虽然已经很简单,但如果需要不同 token 的价格就得对每个 token 执行 setPriceFeed,治理成本其实有点高,对某些场景来说就不太灵活。这时候,就可以考虑使用 Feed Registry  的方式来接入。

Feed Registry 可以简单理解为 PriceFeeds  的聚合器,已经聚合了多个 priceFeed,有了它,使用者就无需自己去设置 priceFeed 了,可直接通过 Feed Registry 读取价格数据,如下图:

图片

喂价机制

首先,Price Feed 的价格是通过多个层级的数据聚合得到的。实际上有三个数据聚合层:数据源聚合、节点运营商聚合、预言机网络聚合

图片

最原始的价格数据主要来源于币安、火币、Coinbase  等中心化交易平台,以及 Uniswap、Sushi  等去中心化交易平台。存在一些专门做数据聚合的服务商(比如 amberdata、CoinGecko),会从这些交易平台收集原始的价格数据,并对这些数据源进行加工整合,比如根据交易量、流动性和时差等进行加权计算。 

这就是第一个层面的聚合,对数据源的聚合。拥有可靠的价格数据源的关键是要有全面的市场覆盖,才能保证一个价格点能代表所有交易环境的精确聚合,而不是单个交易所或少数交易所的价格,以防止数据被人为操纵和出现价格偏差。

第二层则是 Chainlink Node Operators  所做的聚合。每个 Chainlink Node Operator 主要负责运行用于在区块链上获取和广播外部市场数据的 Chainlink 核心软件。Node Operators 会从多个独立的数据聚合服务商获取价格数据,并获取它们之间的中值,剔除掉异常值和 API 停机时间。

最后一层则是整个预言机网络的聚合,其聚合的方式有多种,但最常见的聚合方式是当响应节点数量达到预设值时对数据取中值。比如总共有 31 个节点,预设值为 21,即收到了 21 个节点的响应后,就取这些节点的价格数据的中值作为最终的价格。不过,并非每一轮的价格结果都会更新到链上,只有满足两个触发参数之一的时候才会更新:偏差阈值(Deviation Threshold)和心跳阈值(Heartbeat Threshold)。而且,不同 PriceFeed 的这两个参数的值可能会不一样。

总而言之,Chainlink 价格预言机接入方便,且安全性还是比较高的,但因为其价格更新机制存在偏差阈值,导致价格更新比较慢,短则几分钟或几十分钟更新一次,长则可能达 24 小时才更新一次,因此,一般只适用于对价格更新不太敏感的应用。这也是 Chainlink 价格预言机的局限性,并无法适用所有场景的应用。

免责声明:作为区块链信息平台,本站所发布文章仅代表作者及嘉宾个人观点,与 Web3Caff 立场无关。本文内容仅用于信息分享,均不构成任何投资建议及要约,并请您遵守所在国家或地区的相关法律法规。