chainlink源码分析三

AnyAPI源码解析

Posted by Emo on October 11, 2022

chainlink源码分析

EMO’s Blog

介绍

basic request model 用于实现 链下 api 访问服务 。这种模型其实只是提供了一个链上链下交互的桥梁,提供了成熟的框架帮助开发者快速开发。其实这里并没有解决 去中心化的链下节点返回的结果的可信性 问题。因为 api 的访问是单点的,实际上并没有经过去中心化的共识机制来提高 api 返回结果的可信性。所以这里我们可以通过权威的中心化 oracle 节点来保障可信性。

模型

流程

ChainlinkClient.sol 解析

常量

  uint256 internal constant LINK_DIVISIBILITY = 10**18;
  uint256 private constant AMOUNT_OVERRIDE = 0;
  address private constant SENDER_OVERRIDE = address(0);
  uint256 private constant ORACLE_ARGS_VERSION = 1;
  uint256 private constant OPERATOR_ARGS_VERSION = 2;
  bytes32 private constant ENS_TOKEN_SUBNAME = keccak256("link");
  bytes32 private constant ENS_ORACLE_SUBNAME = keccak256("oracle");
  address private constant LINK_TOKEN_POINTER = 0xC89bD4E1632D3A43CB03AAAd5262cbe4038Bc571

变量

  // 用于通过 ens 更新 s_link 和 s_oracle
  ENSInterface private s_ens;
  bytes32 private s_ensNode;

  // LinkToken 合约,我们和这个合约交互
  LinkTokenInterface private s_link;

  // 帮助我们执行请求的 oracle
  OperatorInterface private s_oracle;
  
  // 发出去的请求的数量,用于计数和给 nonce 赋值并生成 requestId
  uint256 private s_requestCount = 1;

  // 发出去的请求的缓存,主要为了进行防御性验证,确保调用自己回调函数的地址是合法的
  mapping(bytes32 => address) private s_pendingRequests;
  • 我们需要通过 s_link 来知道需要请求发送给谁
  • 也需要通过 s_oracle 来告知需要哪个 oracle 来执行我们的请求
  • 至于 s_requestCount 则是用来记录我们发出的请求的数量。同时也会在发出请求的时候作为 nonce ,应该是为了防重放。而 requestId 的生成也是根据 nonce 生成的。
  • s_pendingRequests 是用来做许可校验的。我们知道,我们请求的结果需要 oracle 通过回调我们的函数来告知我们。这种提供回调的函数,在 chainlink 中被称为 fulfillment functions 。所以当有人对我们的回调函数进行调用的时候,我们需要对调用者进行身份验证。通过调用者提供的 requestId 进行查找 s_pendingRequests 中对应的地址,只有当调用者和查询到的地址匹配时,才能通过验证。
  • 最后,s_enss_ensNode 用于通过 ens 更新 s_links_oracle。后面会详细讲。

事件

  // 当请求发出的时候 emit
  event ChainlinkRequested(bytes32 indexed id);
  // 当 fulfillment functions 被 oracle 回调的时候抛出
  event ChainlinkFulfilled(bytes32 indexed id);
  // 当申请取消请求的时候 emit
  event ChainlinkCancelled(bytes32 indexed id);

event 用于给 web3 应用使用。

方法分析

配置变量相关方法

| 方法名 | 描述 | | —- | —- | | setChainlinkOracle | 设置 s_oracle 的值 | | chainlinkOracleAddress | 获取 s_oracle 的值 | | setChainlinkToken | 设置 s_link 的值 | | chainlinkTokenAddress | 获取 s_link 的值 | | useChainlinkWithENS | 设置 s_ens 和 s_ensNode 的值,并根据 ENS_TOKEN_SUBNAME 解析并更新 s_link | | updateChainlinkOracleWithENS | 通过 s_ens 和 s_ensNode 并根据 ENS_ORACLE_SUBNAME 值解析并更新 s_oracle 的值 |

上面的方法总体比较简单,就是些 getter 和 setter 函数,简单来看一下吧

// s_oracle 的 setter 和 getter
function setChainlinkOracle(address oracleAddress) internal {
    s_oracle = OperatorInterface(oracleAddress);
}
function chainlinkOracleAddress() internal view returns (address) {
    return address(s_oracle);
}

// s_link 的 setter 和 getter
function setChainlinkToken(address linkAddress) internal {
    s_link = LinkTokenInterface(linkAddress);
}
function chainlinkTokenAddress() internal view returns (address) {
    return address(s_link);
}

// 获取 s_requestCount
function getNextRequestCount() internal view returns (uint256) {
    return s_requestCount;
  }

除此之外就是 s_ens 和 s_ensNode 相关的要稍微复杂一点点,但其本质也是为了对 s_oracle 和 s_link 进行设置,只不过是通过 ens 而已。这样的好处是,当 oracle 升级后,我们能很方便的切换过去。

/**
* @notice Sets the stored oracle and LINK token contracts with the addresses resolved by ENS
* @dev Accounts for subnodes having different resolvers
* @param ensAddress The address of the ENS contract
* @param node The ENS node hash
*/
function useChainlinkWithENS(address ensAddress, bytes32 node) internal {
    s_ens = ENSInterface(ensAddress); // 设置 s_ens
    s_ensNode = node; // 设置 s_ensNode
    bytes32 linkSubnode = keccak256(abi.encodePacked(s_ensNode, ENS_TOKEN_SUBNAME)); // 生成 hash 值
    ENSResolver_Chainlink resolver = ENSResolver_Chainlink(s_ens.resolver(linkSubnode)); // 根据 hash 值去 s_ens 解析
    setChainlinkToken(resolver.addr(linkSubnode)); // 获取解析出来的地址值并设置到 s_link
    updateChainlinkOracleWithENS(); // 同样的流程去设置 s_oracle
}

/**
* @notice Sets the stored oracle contract with the address resolved by ENS
* @dev This may be called on its own as long as `useChainlinkWithENS` has been called previously
*/
function updateChainlinkOracleWithENS() internal {
    // 我们将这部分从上面单独分出来,是因为 
    // s_oracle 中对应的 oracle 可能时常会更新,
    // 所以我们可能需要时常单独对 s_oracle 进行更新。
    bytes32 oracleSubnode = keccak256(abi.encodePacked(s_ensNode, ENS_ORACLE_SUBNAME));
    ENSResolver_Chainlink resolver = ENSResolver_Chainlink(s_ens.resolver(oracleSubnode));
    setChainlinkOracle(resolver.addr(oracleSubnode));
}

核心功能方法

| 方法名 | 描述 | | —- | —- | | buildChainlinkRequest | 构建 Request | | buildOperatorRequest | 构建 Request,需要 oracle 是一个 Operator Contract | | sendChianlinkRequest | 发送 request 并指定执行者为 s_oracle 记录的 oracle | | sendOperatorRequest | 和上面差不多,只不过这个也是需要 oracle 为 Operator Contract | | sendChainlinkRequestTo | 发送 request 并指定特定的 oracle | | sendOperatorRequestTo | 和上面差不多,只不过这个也是需要 oracle 为 Operator Contract | | _rawRequest | 实际最底层的执行函数,会将编码好的请求等,通过调用 s_link 对应的 LinkToken 合约当中的 transferAndCall 来将请求发送出去| | cancelChainlinkRequest | 取消 request |

发起 request

根据 oracle 的不同类型,我们可能会发起两种不同的 request 流程。整体如下图

一个是对于 oracle 是 operator contract 的,现在一般推荐使用这种

  // 回调的合约的地址,只能是自己,不能是别的合约
  function buildOperatorRequest(bytes32 specId, bytes4 callbackFunctionSignature)
    internal
    view
    returns (Chainlink.Request memory) 
  {   // 不用传入回调 address,默认本合约的地址就是回调地址
    Chainlink.Request memory req;
    return req.initialize(specId, address(this), callbackFunctionSignature);
  }

  function sendOperatorRequest(Chainlink.Request memory req, uint256 payment) internal returns (bytes32) {
    // 调用 sendOperatorRequestTo ,使用 s_oracle
    return sendOperatorRequestTo(address(s_oracle), req, payment);
  }

  function sendOperatorRequestTo(
    address oracleAddress,
    Chainlink.Request memory req,
    uint256 payment
  ) internal returns (bytes32 requestId) {
    uint256 nonce = s_requestCount; // nonce 的获取
    s_requestCount = nonce + 1; // 更新

    // 对 request 相关信息进行编码
    bytes memory encodedRequest = abi.encodeWithSelector(
      // 可以看到这里指定的 Operator
      OperatorInterface.operatorRequest.selector,
      SENDER_OVERRIDE, // Sender value - overridden by onTokenTransfer by the requesting contract's address
      AMOUNT_OVERRIDE, // Amount value - overridden by onTokenTransfer by the actual amount of LINK sent
      req.id,
      req.callbackFunctionId,
      nonce,
      OPERATOR_ARGS_VERSION,
      req.buf.buf // 里面包含了 request 执行的 api 相关的信息
    );
    // 调用 _rawRequest
    return _rawRequest(oracleAddress, nonce, payment, encodedRequest);
  }

  function _rawRequest( // 发送 request 的底层 private 方法
    address oracleAddress, // oracle 的地址
    uint256 nonce, // 发送的 request 的计数
    uint256 payment, // 会支付的 link token 的数量
    bytes memory encodedRequest // encode 后的 request
  ) private returns (bytes32 requestId) {
    requestId = keccak256(abi.encodePacked(this, nonce)); // 根据 nonce 生成 requestId
    s_pendingRequests[requestId] = oracleAddress; // 将 request 存起来,应该是为了在 oracle 回调的时候做身份验证
    emit ChainlinkRequested(requestId); // 抛出 event
    
    // 调用 LinkToken 的方法
    require(s_link.transferAndCall(oracleAddress, payment, encodedRequest), "unable to transferAndCall to oracle");
  }

另一套方法和上面的基本差不多,我们简单来看一下吧

  // 区别点在于,这里要求传入 oracle 回调的地址
  // 也就是说本地址发起的 request ,
  // 回调的时候可能会去调用别的合约的 fulfillment function
  function buildChainlinkRequest(
    bytes32 specId,
    address callbackAddr,
    bytes4 callbackFunctionSignature // oracle 回调的方法的签名
  ) internal pure returns (Chainlink.Request memory) { // 构建 request
    Chainlink.Request memory req;
    return req.initialize(specId, callbackAddr, callbackFunctionSignature);
  }

  function sendChainlinkRequest(Chainlink.Request memory req, uint256 payment) internal returns (bytes32) {
    return sendChainlinkRequestTo(address(s_oracle), req, payment);
  }

  function sendChainlinkRequestTo(
    address oracleAddress, // 帮你执行 request 的 oracle 的地址
    Chainlink.Request memory req, // 需要执行的 request
    uint256 payment // 支付的 link 币数量
  ) internal returns (bytes32 requestId) { // 返回请求 id
    uint256 nonce = s_requestCount; // 跟以太坊的账户的 nonce 一样,为了避免重放
    s_requestCount = nonce + 1;
    bytes memory encodedRequest = abi.encodeWithSelector(
      // 这里指明了是 oracaleRequest
      ChainlinkRequestInterface.oracleRequest.selector,
      SENDER_OVERRIDE, // Sender value - overridden by onTokenTransfer by the requesting contract's address
      AMOUNT_OVERRIDE, // Amount value - overridden by onTokenTransfer by the actual amount of LINK sent
      req.id,
      address(this),
      req.callbackFunctionId,
      nonce,
      ORACLE_ARGS_VERSION,
      req.buf.buf
    );

    // 这个就和上面是一样的了
    return _rawRequest(oracleAddress, nonce, payment, encodedRequest);
  }

取消 request

如果一个请求还没有被履行,也就是还没有被回调通知结果,允许取消这个请求,并将其消耗的 Link 代币退回。

需要自行追踪 oracle 发出的 event 里面标注的 request 过期时间。如果超过过期时间都没有收到返回的结果,就可以调用 cancelChainlinkRequest 这个方法去 oracle 合约中取消这个 request 并接收退款。

  /**
   * @notice Allows a request to be cancelled if it has not been fulfilled
   * @dev Requires keeping track of the expiration value emitted from the oracle contract.
   * Deletes the request from the `pendingRequests` mapping.
   * Emits ChainlinkCancelled event.
   * @param requestId The request ID
   * @param payment The amount of LINK sent for the request
   * @param callbackFunc The callback function specified for the request
   * @param expiration The time of the expiration for the request
   */
  function cancelChainlinkRequest(
    bytes32 requestId,
    uint256 payment,
    bytes4 callbackFunc,
    uint256 expiration
  ) internal {
    OperatorInterface requested = OperatorInterface(s_pendingRequests[requestId]);
    delete s_pendingRequests[requestId]; // 从 s_pending 中清除
    emit ChainlinkCancelled(requestId); // 抛出 请求取消 event
    requested.cancelOracleRequest(requestId, payment, callbackFunc, expiration); // 调用 operator oracle 中的请求取消相关函数
  }

具体的取消请求操作发生在 oracle 合约当中,之后再细说。

身份验证相关函数

| 方法名 | 描述 | | —- | —- | | validateChainlinkCallback | 用于对进行回调的 oracle 进行身份验证,搭配着修饰器 recordChainlinkFulfillment | | addChianlinkExternalRequest | 将在其他合约中发起的 request 填充注册到自己的 s_pendingRequests 中 |

我们之间提到过 s_pendingRequest 这个变量。他是一个 key-value 类型,其中的 key 是 requestId ,而 value 是 oracleAddress 。每一个 request 都会在未来产生一次回调,我们在这里将每个发起的 request 都注册起来的目的是,当这个回调发生的时候,我们可以对调用者 oracle 进行身份验证,是否匹配 s_pendingRequest 中指定 requestId 所对应的 oracleAddress

整个身份验证的核心,是通过一个 recordChainlinkFulfillment 修饰器完成的。代码如下

  /**
   * @dev Reverts if the sender is not the oracle of the request.
   * Emits ChainlinkFulfilled event.
   * @param requestId The request ID for fulfillment
   */
  // 身份验证,所有用于回调的函数在调用前都需要进行这个验证。
  // 可以用这个 modifier 修饰回调函数。
  // 也可以直接在函数里面调用 validateChainlinkCallback 进行验证
  modifier recordChainlinkFulfillment(bytes32 requestId) { // 确认访问者是否是记录中的 request 对应的地址
    require(msg.sender == s_pendingRequests[requestId], "Source must be the oracle of the request");
    delete s_pendingRequests[requestId];
    emit ChainlinkFulfilled(requestId);
    _;
  }

还有另外一个 modifier ,用来检测这个 request 是否在 s_pendingRequests 中存在

  modifier notPendingRequest(bytes32 requestId) { // 确认 request 是否在 pending 中
    require(s_pendingRequests[requestId] == address(0), "Request is already pending");
    _;
  }

每一个 Fulfillment Function (也就是用于接受回调的函数) 都需要进行这个验证。我们在函数构建的时候添加这个修饰器,也可以通过在方面里面引入 validateChainlinkCallback 这个函数来进行身份验证。这个函数是一个组合了 recordChainlinkFulfillment 修饰器的空函数。

function validateChainlinkCallback(bytes32 requestId)
    internal
    recordChainlinkFulfillment(requestId)
  // solhint-disable-next-line no-empty-blocks
  {

  }

当本合约发起请求的时候,我们会将发起的请求的信息注册到 s_pendingRequests 中。但是如果是外部的一个合约发起的请求,需要回调本合约的 fullfillment function 时,这个请求是没有注册到本合约的 s_pendingRequests 中的,我们就需要通过函数 addChainlinkExternalRequest 来将其注册。

function addChainlinkExternalRequest(
    address oracleAddress, 
    bytes32 requestId
    ) internal notPendingRequest(requestId) {
    s_pendingRequests[requestId] = oracleAddress;
  }

LinkToken

LinkToken 代币合约在 LinkToken源码解析 当中有详细的讲解。这里只简单的说一下。他主要是根据 ERC677 来开发的。ERC677 相比较于 ERC20 只是添加了一个 transferAndCall 方法。如果你看了上面的讲解,就可以看到, ChainLinkClient 中发起 request 的最后一步就是调用 LinkToken 合约当中的 transferAndCall 函数。其作用是在进行代币转账后,还会去调用特定的函数。在 LinkToken 当中,他就会根据传入的 oracle 地址去调用其 onTokenTransfer 方法。

Oracle

接收到 request

我们知道,LinkToken 在收到 request 并扣费后,会将通过调用 Oracle 的 onTokenTansfer 方法将 request 转发过来。

  function onTokenTransfer(
    address _sender,
    uint256 _amount,
    bytes _data
  )
    public
    onlyLINK
    validRequestLength(_data)
    permittedFunctionsForLINK(_data)
  {
    assembly { // solhint-disable-line no-inline-assembly
      mstore(add(_data, 36), _sender) // ensure correct sender is passed
      mstore(add(_data, 68), _amount) // ensure correct amount is passed
    }
    // solhint-disable-next-line avoid-low-level-calls
    require(address(this).delegatecall(_data), "Unable to create request"); // calls oracleRequest
  }

在上面经过三个修饰器验证后才能进入方法内部

  • onlyLINK
  • validRequestLength
  • permittedFunctionsForLINK

进入内部后会重写 _sender 和 _amount 。而后通过 delegatecall 调用指定的本地函数。 我们来看看,传进来的 _data ,也就是 encode 后的 request 具体有哪些信息,我们知道会有两种类型的 request ,我们先来看看其中一种。

bytes memory encodedRequest = abi.encodeWithSelector(
  // 这里指明了是 oracaleRequest 这个方法
  ChainlinkRequestInterface.oracleRequest.selector,
  // 后面是调用这个方法时会传入的参数
  SENDER_OVERRIDE, // Sender value - overridden by onTokenTransfer by the requesting contract's address
  AMOUNT_OVERRIDE, // Amount value - overridden by onTokenTransfer by the actual amount of LINK sent
  req.id,
  address(this),
  req.callbackFunctionId,
  nonce,
  ORACLE_ARGS_VERSION,
  req.buf.buf
);

通过上面我们,知道了 delegatecall 实际调用的是这个合约里的 oracleRequest 方法

  function oracleRequest(
    address _sender,
    uint256 _payment,
    bytes32 _specId,
    address _callbackAddress,
    bytes4 _callbackFunctionId,
    uint256 _nonce,
    uint256 _dataVersion,
    bytes _data
  )
    external
    onlyLINK
    checkCallbackAddress(_callbackAddress)
  {
    // 生成并检测 request id 的唯一性
    bytes32 requestId = keccak256(abi.encodePacked(_sender, _nonce));
    require(commitments[requestId] == 0, "Must use a unique ID");
    // solhint-disable-next-line not-rely-on-time
    uint256 expiration = now.add(EXPIRY_TIME); // 用于取消 request

    // 缓存 request id,极其对应信息的 hash
    commitments[requestId] = keccak256(
      abi.encodePacked(
        _payment,
        _callbackAddress,
        _callbackFunctionId,
        expiration
      )
    );

    // 抛出 event ,链下的 oracle 节点会监测这个 event 并执行
    // _data 的格式是由链下节点自己去解析的
    emit OracleRequest(
      _specId,
      _sender,
      requestId,
      _payment,
      _callbackAddress,
      _callbackFunctionId,
      expiration,
      _dataVersion,
      _data);
  }

任务完成后回调

当链下 oracle 节点完成任务后,会通过调用链上对应的 oracle 合约的 fulfillOracleRequest 去提交结果

  /**
   * @notice Called by the Chainlink node to fulfill requests
   * @dev Given params must hash back to the commitment stored from `oracleRequest`.
   * Will call the callback address' callback function without bubbling up error
   * checking in a `require` so that the node can get paid.
   * @param _requestId The fulfillment request ID that must match the requester's
   * @param _payment The payment amount that will be released for the oracle (specified in wei)
   * @param _callbackAddress The callback address to call for fulfillment
   * @param _callbackFunctionId The callback function ID to use for fulfillment
   * @param _expiration The expiration that the node should respond by before the requester can cancel
   * @param _data The data to return to the consuming contract
   * @return Status if the external call was successful
   */
  function fulfillOracleRequest(
    bytes32 _requestId,
    uint256 _payment,
    address _callbackAddress,
    bytes4 _callbackFunctionId,
    uint256 _expiration,
    bytes32 _data
  )
    external
    onlyAuthorizedNode // 验证返回结果的节点是不是授权节点
    isValidRequest(_requestId) // 检查 request id 是不是 commitments 记录中的
    returns (bool)
  {
    // 比较参数和 request id 之前的记录是否一致
    bytes32 paramsHash = keccak256(
      abi.encodePacked(
        _payment,
        _callbackAddress,
        _callbackFunctionId,
        _expiration
      )
    );
    require(commitments[_requestId] == paramsHash, "Params do not match request ID");
    
    withdrawableTokens = withdrawableTokens.add(_payment); // 记录一下获取到了多少 token 了
    delete commitments[_requestId]; // 将 request 记录删除
    require(gasleft() >= MINIMUM_CONSUMER_GAS_LIMIT, "Must provide consumer enough gas"); // 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
    // 回调 client 提供的回调地址和对调函数。
    return _callbackAddress.call(_callbackFunctionId, _requestId, _data); // solhint-disable-line avoid-low-level-calls
  }

取消任务

ChainLinkClient 可以通过调用 oracle 的 cancelOracleRequest 方法来取消逾期未返回结果的 request。

  /**
   * @notice Allows requesters to cancel requests sent to this oracle contract. Will transfer the LINK
   * sent for the request back to the requester's address.
   * @dev Given params must hash to a commitment stored on the contract in order for the request to be valid
   * Emits CancelOracleRequest event.
   * @param _requestId The request ID
   * @param _payment The amount of payment given (specified in wei)
   * @param _callbackFunc The requester's specified callback address
   * @param _expiration The time of the expiration for the request
   */
  function cancelOracleRequest(
    bytes32 _requestId,
    uint256 _payment,
    bytes4 _callbackFunc,
    uint256 _expiration
  ) external {
    // 比较传入的参数和记录的信息是否相等
    bytes32 paramsHash = keccak256(
      abi.encodePacked(
        _payment,
        msg.sender,
        _callbackFunc,
        _expiration)
    );
    require(paramsHash == commitments[_requestId], "Params do not match request ID");
    
    // 检查是否真的过期了
    // solhint-disable-next-line not-rely-on-time
    require(_expiration <= now, "Request is not expired");

    // 删除 request 记录并抛出事件
    delete commitments[_requestId];
    emit CancelOracleRequest(_requestId); // 这会是这个合约节点的一个污点
    
    // 退钱
    assert(LinkToken.transfer(msg.sender, _payment));
  }

{ % if page.mermaid % } { % endif % }