chainlink源码分析四

VRF源码解析

Posted by Emo on October 12, 2022

chainlink源码分析

EMO’s Blog

介绍

这个服务是链下节点为用户提供可验证的 VRF 随机数。涉及两个合约,Consumer 和 Coordinator ,其中 Consumer 比较简单,核心的方法都是在 Coordinator 合约上的,会主要针对这个合约讲解。

而使用 VRF 服务,有两种方式

  • VRF Direct funding 和使用 AnyAPI 的操作差不多,每次 request 都需要先进入到 LInkToken 里面去交钱。
  • Subscription Method 订阅制度,通过在 Coordinator 上创建一个订阅账户,并通过 Linktoken 给订阅账户预充一笔钱,这样每次使用服务就不用去 LinkToken 里面走一圈了,直接在 Coordinator 上扣就行了。而且可以将多个 Consumer 绑定到同一个订阅账户里,这样多个 Consumer 就可以公用一个订阅账户的预充钱去请求 VRF 服务了。对于资产管理方便了很多。

前者比较简单,我们会重点分析 Subscription Method 。

角色

在 chainlink 的 VRF 设计当中,主要有四种角色。

  • Consumer 实际请求和消费 VRF 随机数的合约
  • Subscriber VRF服务的订阅账户,其中需要预充钱,同时会挂靠多个 Consumer, 所有 Consumer 的消费都是由 Subscriber 账户统一支付的。
  • Coordinator 协调和管理订阅服务和 VRF 服务的地方,用户和Oracle之间的交互都是由Coordinator作为中间人来进行的。扣费,转发VRF请求,随机数验证,随机数callback都发生在这里。
    • 订阅板块 资金管理方便,预充钱省gas 用户可以创建订阅账户,用户往这个订阅账户里面预充钱,这样在使用 VRF 服务的时候,就不用走 LinkToken 转钱了,而是直接在订阅账户里面扣钱就行。同时一个订阅账户可以挂靠很多个消费者,这些消费者共用这个订阅账户里的资金,这样就不用为多个合约分别进行资金管理了,而是通过同一个订阅账户来管理资金。
    • VRF协调板块
      • 接受请求,发出 VRF Event
      • 接收链下节点提交的 VRF 随机数,并做验证
      • 费用处理
      • 通过回调函数,将 VRF 随机数返回给消费者
    • Oracle管理板块 Oracle 通过 Coordinator 来接任务,所以他们在 Coordinator上也是有 balances 的,他们每完成一次任务,Coordinator 都会发钱到他们的 balance 里。Oracle 随时可以将他们 balance 里的钱取成 Link 代币到他们自己的账户里面。
  • Oracle 提供 VRF 服务的 Oracle。需要在 Coordinator 中注册。收益由 Coordinator 记录,可以随时去 Coordinator 中将收益取出来。

核心功能清单

  1. 创建订阅
  2. 取消订阅
  3. 给订阅账户充钱
  4. 添加Consumer到订阅账户
  5. Oracle 加入
  6. Oracle 退出
  7. Oracle 取钱
  8. 请求随机数
  9. 随机数验证后回填

Consumer 代码解析

这一部分,主要是一个规则协议,用户需要实现两个方法

  • requestRandomness 用来申请随机数的方法
  • fulfillRandomness 用来消费随机数的方法

requestRandomness

不通过订阅制度,直接选择交钱请求随机数时才会使用到这个方法。 使用订阅制度则不需要这个方法,只需要实现好自己的 callback 方法就好。

  function requestRandomness(bytes32 _keyHash, uint256 _fee) internal returns (bytes32 requestId) {
    // 给 LinkToken 合约转钱,并调用 Coordinator 的 onTokentransfer 方法
    LINK.transferAndCall(vrfCoordinator, _fee, abi.encode(_keyHash, USER_SEED_PLACEHOLDER));
    // This is the seed passed to VRFCoordinator. The oracle will mix this with
    // the hash of the block containing this request to obtain the seed/input
    // which is finally passed to the VRF cryptographic machinery.
    // 传递给 Corrdinator 的 seed,Coordinator 还会将 block hash 组合进去
    uint256 vRFSeed = makeVRFInputSeed(_keyHash, USER_SEED_PLACEHOLDER, address(this), nonces[_keyHash]);
    // nonces[_keyHash] must stay in sync with
    // VRFCoordinator.nonces[_keyHash][this], which was incremented by the above
    // successful LINK.transferAndCall (in VRFCoordinator.randomnessRequest).
    // This provides protection against the user repeating their input seed,
    // which would result in a predictable/duplicate output, if multiple such
    // requests appeared in the same block.
    nonces[_keyHash] = nonces[_keyHash] + 1;
    return makeRequestId(_keyHash, vRFSeed);
  }

fulfillRandomWords

不管是通过订阅还是直接直接转钱哪种方式,都需要实现这里来接收并使用随机数。

  // 这个帮你写好了,回传先调这里,然后再导向你自己写的 fulfillRandomWords 方法
  function rawFulfillRandomness(bytes32 requestId, uint256 randomness) external {
    require(msg.sender == vrfCoordinator, "Only VRFCoordinator can fulfill");
    fulfillRandomness(requestId, randomness);
  }
  
  // 自己实现这个方法去决定怎么使用这个随机数
  function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal virtual; 

Coordinator 状态变量

  // 一个订阅账户最多只能挂靠100个消费者
  uint16 public constant MAX_CONSUMERS = 100;
  
  // LinkToken 合约
  LinkTokenInterface public immutable LINK;
  // 一个链上的 datafeed 合约,用于访问 eth 市场价,并以此计算消耗的 gas 的实际价值
  AggregatorV3Interface public immutable LINK_ETH_FEED;
  // 可以获取 blockhash 值的合约
  BlockhashStoreInterface public immutable BLOCKHASH_STORE;
  
  // (consumer, subID) -> nonce 
  // 记录每一个 consumer 在 subId 指定的订阅账户上的 nonce 值
  mapping(address => mapping(uint64 => uint64)) /* consumer */ /* subId */ /* nonce */
    private s_consumers;

  // 记录订阅者的配置信息
  mapping(uint64 => SubscriptionConfig) /* subId */ /* subscriptionConfig */
    private s_subscriptionConfigs;
  struct SubscriptionConfig {
    address owner; // 拥有者可以 充值/接受退款/取消订阅
    address requestedOwner; // 这个是在订阅所有权转移的流程中会用到
    address[] consumers; // 这个订阅里绑定的 consumers
  }

  // 记录订阅者的账户信息
  mapping(uint64 => Subscription) /* subId */ /* subscription */
    private s_subscriptions;
  struct Subscription {
    // There are only 1e9*1e18 = 1e27 juels in existence, so the balance can fit in uint96 (2^96 ~ 7e28)
    uint96 balance; // Common link balance used for all consumer requests.
    uint64 reqCount; // 这个订阅账户一共发了多少个请求,会用来对你的消费等级做评估,不同等级收费不一样。发送的请求越多,对你的收费就会越低,有点像会员等级制度
  }

  // 用来记录最新订阅者的id的,主要是用在新增订阅者的时候可以快速计算其id
  uint64 private s_currentSubId;
  
  // 所有订阅账户预充的总的link币数量
  uint96 private s_totalBalance;

  // 200s内需要执行fulfill
  uint16 public constant MAX_REQUEST_CONFIRMATIONS = 200;
  // 一次最多请求500个随机数
  uint32 public constant MAX_NUM_WORDS = 500;
  // 额外检查需要支付的 gas fee
  uint256 private constant GAS_FOR_CALL_EXACT_CHECK = 5_000;

  // keyhash 到 oracle 地址的映射
  mapping(bytes32 => address) /* keyHash */ /* oracle */
    private s_provingKeys;
  
  // 所有的 keyhash
  bytes32[] private s_provingKeyHashes;
  
  // oracle 地址账户拥有多少钱,他们到时候可以提走的
  mapping(address => uint96) /* oracle */ /* LINK balance */
    private s_withdrawableTokens;
  
  // 每个请求对应的请求信息的hash,主要是在oracle提交结果的时候,做request的匹配性验证
  mapping(uint256 => bytes32) /* requestID */ /* commitment */
    private s_requestCommitments;

  // 每单位link值多少wei
  int256 private s_fallbackWeiPerUnitLink;
  
  // 配置信息
  Config private s_config;
  struct Config {
    uint16 minimumRequestConfirmations;
    uint32 maxGasLimit;
    // Reentrancy protection.
    bool reentrancyLock;
    // 这个时间用来判断我们通过 eth data feed 服务获取到的 link->eth 价格是不是一个很久的数据
    // 如果是很久的,我们会使用默认的 s_fallbackWeiPerUnitLink 来作为转换标准
    uint32 stalenessSeconds;
    // Gas to cover oracle payment after we calculate the payment.
    // We make it configurable in case those operations are repriced.
    // 这个组要是用来计算用户需要为产生的gas费交多少link的时候,我们发生实际的预充账户转钱,必定发生在计算费用之后
    // 所以转钱这几个步骤产生的gas费用,我们就只能预估了。这个就是预估值。
    uint32 gasAfterPaymentCalculation;
  }

  // 费用配置
  // 做消费等级判断的,一个订阅账户消费的request越多,它的消费等级就越高,单词收费就越少
  FeeConfig private s_feeConfig;
  struct FeeConfig {
    uint32 fulfillmentFlatFeeLinkPPMTier1;
    uint32 fulfillmentFlatFeeLinkPPMTier2;
    uint32 fulfillmentFlatFeeLinkPPMTier3;
    uint32 fulfillmentFlatFeeLinkPPMTier4;
    uint32 fulfillmentFlatFeeLinkPPMTier5;
    uint24 reqsForTier2;
    uint24 reqsForTier3;
    uint24 reqsForTier4;
    uint24 reqsForTier5;
  }

Coordinator 方法分析

构造函数

  // 主要为了注册,Corrdindator 会用到的三个外部合约的地址
  constructor(
    address link, // LinkToken合约,实际发生Link代币交易的地方
    address blockhashStore,
    address linkEthFeed // 一个 eth 的 data feed 合约
  ) ConfirmedOwner(msg.sender) {
    LINK = LinkTokenInterface(link);
    LINK_ETH_FEED = AggregatorV3Interface(linkEthFeed);
    BLOCKHASH_STORE = BlockhashStoreInterface(blockhashStore);
  }

Oracle相关函数

注册Oracle

/**
 * @notice Registers a proving key to an oracle.
 * @param oracle address of the oracle
 * @param publicProvingKey key that oracle can use to submit vrf fulfillments
 */
function registerProvingKey(address oracle, uint256[2] calldata publicProvingKey) external onlyOwner { // 只有 owner 才能操作
    bytes32 kh = hashOfKey(publicProvingKey);
    if (s_provingKeys[kh] != address(0)) { // 检查是否注册过
      revert ProvingKeyAlreadyRegistered(kh);
    }
    s_provingKeys[kh] = oracle; // 存储注册信息
    s_provingKeyHashes.push(kh); // 将 key 放到列表里
    emit ProvingKeyRegistered(kh, oracle); // 抛出 event
}

// 生成 key hash
function hashOfKey(uint256[2] memory publicKey) public pure returns (bytes32) {
    return keccak256(abi.encode(publicKey));
}

取消注册 Oracle

  /**
    * @notice Deregisters a proving key to an oracle.
    * @param publicProvingKey key that oracle can use to submit vrf fulfillments
    */
  function deregisterProvingKey(uint256[2] calldata publicProvingKey) external onlyOwner { // 只有 owner 才能操作
    bytes32 kh = hashOfKey(publicProvingKey);
    address oracle = s_provingKeys[kh];
    if (oracle == address(0)) { // 检查是否存在
      revert NoSuchProvingKey(kh);
    }
    delete s_provingKeys[kh]; // 删除注册信息
    
    // 从 key 列表里删除
    for (uint256 i = 0; i < s_provingKeyHashes.length; i++) {
      if (s_provingKeyHashes[i] == kh) {
        bytes32 last = s_provingKeyHashes[s_provingKeyHashes.length - 1];
        // Copy last element and overwrite kh to be deleted with it
        s_provingKeyHashes[i] = last;
        s_provingKeyHashes.pop();
      }
    }
    emit ProvingKeyDeregistered(kh, oracle); // 抛出事件
  }

subscription相关

注册订阅

这里会在 Coordinator 上面创建一个订阅账户。大概看一下,就是对 subscription 相关的状态变量进行操作。

function createSubscription() external override nonReentrant returns (uint64) {
    s_currentSubId++;
    uint64 currentSubId = s_currentSubId;
    address[] memory consumers = new address[](0);
    s_subscriptions[currentSubId] = Subscription({balance: 0, reqCount: 0});
    s_subscriptionConfigs[currentSubId] = SubscriptionConfig({
      owner: msg.sender, // 这个账号的 owner 就是交易发起人,这个 owner 也会作为订阅账号注销后,退钱的账户
      requestedOwner: address(0), // 发生订阅账户所有权转移后会使用到
      consumers: consumers
    });

    emit SubscriptionCreated(currentSubId, msg.sender);
    return currentSubId;
  }

更换订阅账户的 Owner

只有原本的 Owner 才有权限发起这个操作进行订阅账户的所有权转移

  function requestSubscriptionOwnerTransfer(uint64 subId, address newOwner)
    external
    override
    onlySubOwner(subId)
    nonReentrant
  {
    // Proposing to address(0) would never be claimable so don't need to check.
    if (s_subscriptionConfigs[subId].requestedOwner != newOwner) {
      s_subscriptionConfigs[subId].requestedOwner = newOwner;
      emit SubscriptionOwnerTransferRequested(subId, msg.sender, newOwner);
    }
  }

发起转移后,其实只是记录了一下 requestedOwner 。要彻底完成转移,还需要 requestedOwner 自己去接收所有权,通过 acceptSubscriptionOwnerTransfer 函数

  function acceptSubscriptionOwnerTransfer(uint64 subId) external override nonReentrant {
    if (s_subscriptionConfigs[subId].owner == address(0)) {
      revert InvalidSubscription();
    }
    // 发起接收所有权,必须是记录中的 requestedOwner
    if (s_subscriptionConfigs[subId].requestedOwner != msg.sender) {
      revert MustBeRequestedOwner(s_subscriptionConfigs[subId].requestedOwner);
    }

    // 这里才会完成所有权转移,修改 owner 字段
    address oldOwner = s_subscriptionConfigs[subId].owner;
    s_subscriptionConfigs[subId].owner = msg.sender;
    s_subscriptionConfigs[subId].requestedOwner = address(0);
    emit SubscriptionOwnerTransferred(subId, oldOwner, msg.sender);
  }

取消订阅

有两个入口,一个是 Coordinator 合约发起删除订阅 ,一个是 订阅账户自己发起删除订阅 。主要就是将注册信息删除,需要把 subscription 和其中的 consumers 相关的信息删除,还要将订阅账户里面剩余的钱通过 LinkToken 合约退还给 Owner 。

  // Coordinator 合约删除订阅账户
  function ownerCancelSubscription(uint64 subId) external onlyOwner {
    // 检查订阅是否存在
    if (s_subscriptionConfigs[subId].owner == address(0)) {
      revert InvalidSubscription();
    }
    cancelSubscriptionHelper(subId, s_subscriptionConfigs[subId].owner);
  }

  // 订阅账户自己发起删除
  function cancelSubscription(uint64 subId, address to) external override onlySubOwner(subId) nonReentrant {
    // 该订阅账户是否还有正在操作中的 request
    if (pendingRequestExists(subId)) {
      revert PendingRequestExists();
    }
    cancelSubscriptionHelper(subId, to);
  }
  
  // 实际进行删除操作的方法
  function cancelSubscriptionHelper(uint64 subId, address to) private nonReentrant {
    SubscriptionConfig memory subConfig = s_subscriptionConfigs[subId];
    Subscription memory sub = s_subscriptions[subId];
    uint96 balance = sub.balance;
    // Note bounded by MAX_CONSUMERS;
    // If no consumers, does nothing.
    for (uint256 i = 0; i < subConfig.consumers.length; i++) {
      // 先把该订阅中的 consumers 都删了
      delete s_consumers[subConfig.consumers[i]][subId];
    }
    // 在删除订阅
    delete s_subscriptionConfigs[subId];
    delete s_subscriptions[subId];
    
    // 退钱
    s_totalBalance -= balance;
    if (!LINK.transfer(to, uint256(balance))) {
      revert InsufficientBalance();
    }

    // 抛出事件
    emit SubscriptionCanceled(subId, to, balance);
  }

订阅账户是否还存在进行中的请求

这个函数主要是在取消订阅的时候使用。如果该订阅账户还存在进行中的任务,则暂时还无法取消订阅

  function pendingRequestExists(uint64 subId) public view override returns (bool) {
    SubscriptionConfig memory subConfig = s_subscriptionConfigs[subId];
    for (uint256 i = 0; i < subConfig.consumers.length; i++) {
      for (uint256 j = 0; j < s_provingKeyHashes.length; j++) {
        (uint256 reqId, ) = computeRequestId(
          s_provingKeyHashes[j],
          subConfig.consumers[i],
          subId,
          s_consumers[subConfig.consumers[i]][subId]
        );
        if (s_requestCommitments[reqId] != 0) {
          return true;
        }
      }
    }
    return false;
  }

消费者账号管理

添加消费者到订阅账户

只有订阅账户的 owner 才有权限添加 consumer 进来

  function addConsumer(uint64 subId, address consumer) external override onlySubOwner(subId) nonReentrant {
    // Already maxed, cannot add any more consumers.
    // 看看容量还够不够
    if (s_subscriptionConfigs[subId].consumers.length == MAX_CONSUMERS) {
      revert TooManyConsumers();
    }

    // 是否已经存在了
    if (s_consumers[consumer][subId] != 0) {
      // Idempotence - do nothing if already added.
      // Ensures uniqueness in s_subscriptions[subId].consumers.
      return;
    }
    
    // 添加
    // Initialize the nonce to 1, indicating the consumer is allocated.
    s_consumers[consumer][subId] = 1;
    s_subscriptionConfigs[subId].consumers.push(consumer);

    emit SubscriptionConsumerAdded(subId, consumer);
  }

把消费者从订阅账户移除

只有订阅账户的 owner 才有权限将 consumer 移除

  function removeConsumer(uint64 subId, address consumer) external override onlySubOwner(subId) nonReentrant {
    // 看consumer存不存在
    if (s_consumers[consumer][subId] == 0) {
      revert InvalidConsumer(subId, consumer);
    }
    // Note bounded by MAX_CONSUMERS
    address[] memory consumers = s_subscriptionConfigs[subId].consumers;
    uint256 lastConsumerIndex = consumers.length - 1;
    
    // 遍历寻找到要移除的consumer
    // 将最后一个consumer复制过来覆盖要删除的consumer
    // 将最后一个移除
    for (uint256 i = 0; i < consumers.length; i++) {
      if (consumers[i] == consumer) {
        address last = consumers[lastConsumerIndex];
        // Storage write to preserve last element
        s_subscriptionConfigs[subId].consumers[i] = last;
        // Storage remove last element
        s_subscriptionConfigs[subId].consumers.pop();
        break;
      }
    }
    delete s_consumers[consumer][subId];
    emit SubscriptionConsumerRemoved(subId, consumer);
  }

配置相关

设置Config

对 s_config ,s_feeConfig 还有 s_fallbackWeiPerUnitLink 进行更改

  /**
   * @notice Sets the configuration of the vrfv2 coordinator
   * @param minimumRequestConfirmations global min for request confirmations
   * @param maxGasLimit global max for request gas limit
   * @param stalenessSeconds if the eth/link feed is more stale then this, use the fallback price
   * @param gasAfterPaymentCalculation gas used in doing accounting after completing the gas measurement
   * @param fallbackWeiPerUnitLink fallback eth/link price in the case of a stale feed
   * @param feeConfig fee tier configuration
   */
  function setConfig(
    uint16 minimumRequestConfirmations,
    uint32 maxGasLimit,
    uint32 stalenessSeconds,
    uint32 gasAfterPaymentCalculation,
    int256 fallbackWeiPerUnitLink,
    FeeConfig memory feeConfig
  ) external onlyOwner {
    if (minimumRequestConfirmations > MAX_REQUEST_CONFIRMATIONS) {
      revert InvalidRequestConfirmations(
        minimumRequestConfirmations,
        minimumRequestConfirmations,
        MAX_REQUEST_CONFIRMATIONS
      );
    }
    if (fallbackWeiPerUnitLink <= 0) {
      revert InvalidLinkWeiPrice(fallbackWeiPerUnitLink);
    }
    s_config = Config({
      minimumRequestConfirmations: minimumRequestConfirmations,
      maxGasLimit: maxGasLimit,
      stalenessSeconds: stalenessSeconds,
      gasAfterPaymentCalculation: gasAfterPaymentCalculation,
      reentrancyLock: false
    });
    s_feeConfig = feeConfig;
    s_fallbackWeiPerUnitLink = fallbackWeiPerUnitLink;
    emit ConfigSet(
      minimumRequestConfirmations,
      maxGasLimit,
      stalenessSeconds,
      gasAfterPaymentCalculation,
      fallbackWeiPerUnitLink,
      s_feeConfig
    );
  }

Getter 函数

大概看看就好了,就是获取一下状态变量

  // 获取 config
  function getConfig()
    external
    view
    returns (
      uint16 minimumRequestConfirmations,
      uint32 maxGasLimit,
      uint32 stalenessSeconds,
      uint32 gasAfterPaymentCalculation
    )
  {
    return (
      s_config.minimumRequestConfirmations,
      s_config.maxGasLimit,
      s_config.stalenessSeconds,
      s_config.gasAfterPaymentCalculation
    );
  }

  // 获取 fee config
  function getFeeConfig()
    external
    view
    returns (
      uint32 fulfillmentFlatFeeLinkPPMTier1,
      uint32 fulfillmentFlatFeeLinkPPMTier2,
      uint32 fulfillmentFlatFeeLinkPPMTier3,
      uint32 fulfillmentFlatFeeLinkPPMTier4,
      uint32 fulfillmentFlatFeeLinkPPMTier5,
      uint24 reqsForTier2,
      uint24 reqsForTier3,
      uint24 reqsForTier4,
      uint24 reqsForTier5
    )
  {
    return (
      s_feeConfig.fulfillmentFlatFeeLinkPPMTier1,
      s_feeConfig.fulfillmentFlatFeeLinkPPMTier2,
      s_feeConfig.fulfillmentFlatFeeLinkPPMTier3,
      s_feeConfig.fulfillmentFlatFeeLinkPPMTier4,
      s_feeConfig.fulfillmentFlatFeeLinkPPMTier5,
      s_feeConfig.reqsForTier2,
      s_feeConfig.reqsForTier3,
      s_feeConfig.reqsForTier4,
      s_feeConfig.reqsForTier5
    );
  }

  function getTotalBalance() external view returns (uint256) {
    return s_totalBalance;
  }

  function getFallbackWeiPerUnitLink() external view returns (int256) {
    return s_fallbackWeiPerUnitLink;
  }

  /**
   * @inheritdoc VRFCoordinatorV2Interface
   */
  function getRequestConfig()
    external
    view
    override
    returns (
      uint16,
      uint32,
      bytes32[] memory
    )
  {
    return (s_config.minimumRequestConfirmations, s_config.maxGasLimit, s_provingKeyHashes);
  }

Fund相关

recoverFunds 总账户维护

  /**
   * @notice Recover link sent with transfer instead of transferAndCall.
   * @param to address to send link to
   */
  function recoverFunds(address to) external onlyOwner {
    // 该合约实际在 linkToken 当中拥有的 link 代币数量
    uint256 externalBalance = LINK.balanceOf(address(this));
    // 订阅者,oracle 等账户在该合约中记录的拥有的 link 代币数量
    uint256 internalBalance = uint256(s_totalBalance);
    // 一般来说上面两个值是维持相等的
    
    // 如果实际拥有的 token 合约记录的总 token 少,说明出问题了
    if (internalBalance > externalBalance) {
      // 抛出异常
      revert BalanceInvariantViolated(internalBalance, externalBalance);
    }

订阅账户充钱

这个充钱的流程会走 LinkToken 合约,用户通过其中的 transferAndCall 函数来调用 Coordinator 合约中的 onTokenTransfer 方法。整体比较简单,就是用户通过 LinkToken 合约给 Coordinator 转了多少钱,我就给他的账户里面增加相应的金额即可。

  function onTokenTransfer(
    address, /* sender */
    uint256 amount,
    bytes calldata data
  ) external override nonReentrant {
    if (msg.sender != address(LINK)) {
      revert OnlyCallableFromLink();
    }
    if (data.length != 32) {
      revert InvalidCalldata();
    }
    uint64 subId = abi.decode(data, (uint64));
    if (s_subscriptionConfigs[subId].owner == address(0)) {
      revert InvalidSubscription();
    }
    // We do not check that the msg.sender is the subscription owner,
    // anyone can fund a subscription.
    uint256 oldBalance = s_subscriptions[subId].balance;
    s_subscriptions[subId].balance += uint96(amount);
    s_totalBalance += uint96(amount);
    emit SubscriptionFunded(subId, oldBalance, oldBalance + amount);
  }

Oracle取走收益

就是从 coordinator 记录的 oracle 的 balance 里提走指定数量的 link 币。代码很简单,看看就好

  function oracleWithdraw(address recipient, uint96 amount) external nonReentrant {
    // 钱够不够
    if (s_withdrawableTokens[msg.sender] < amount) {
      revert InsufficientBalance();
    }

    // 扣掉取走的钱
    s_withdrawableTokens[msg.sender] -= amount;
    s_totalBalance -= amount;
    
    // 去 LinkToken 合约转账
    if (!LINK.transfer(recipient, amount)) {
      revert InsufficientBalance();
    }
  }

VRF业务流程相关

请求随机数

没有修饰器做权限限制,所有外部合约都可以调这个方法。

  /**
   * @inheritdoc VRFCoordinatorV2Interface
   */
  function requestRandomWords(
    bytes32 keyHash, // 指明哪一个 oracle 来做
    uint64 subId, // 指明自己对应的订阅账户
    uint16 requestConfirmations, // 
    uint32 callbackGasLimit, // 回调的gas消费上限
    uint32 numWords // 请求几个随机数
  ) external override nonReentrant returns (uint256) {
    // 订阅账户是否存在
    if (s_subscriptionConfigs[subId].owner == address(0)) {
      revert InvalidSubscription();
    }
    
    // 检查这个消费者是否真的在这个订阅账户里面
    uint64 currentNonce = s_consumers[msg.sender][subId];
    if (currentNonce == 0) {
      revert InvalidConsumer(subId, msg.sender);
    }
    // Input validation using the config storage word.
    // 判断传入的 requestConfirmations 是否符合要求
    if (
      requestConfirmations < s_config.minimumRequestConfirmations || requestConfirmations > MAX_REQUEST_CONFIRMATIONS
    ) {
      revert InvalidRequestConfirmations(
        requestConfirmations,
        s_config.minimumRequestConfirmations,
        MAX_REQUEST_CONFIRMATIONS
      );
    }
    
    // 检查传入的 gaslimit 是否超出设定的规范
    if (callbackGasLimit > s_config.maxGasLimit) {
      revert GasLimitTooBig(callbackGasLimit, s_config.maxGasLimit);
    }

    // 检查传入的 numWords 是否超出设定的规范
    if (numWords > MAX_NUM_WORDS) {
      revert NumWordsTooBig(numWords, MAX_NUM_WORDS);
    }

    // 这里面没有对 keyHash 做验证,所以用户是可以指定一个不存在的 keyHash ,但是他们不会被 fulfill
    // Note we do not check whether the keyHash is valid to save gas.
    // The consequence for users is that they can send requests
    // for invalid keyHashes which will simply not be fulfilled.
    uint64 nonce = currentNonce + 1;
    // 计算 requestId 和 preSeed ,这里只是 preSeed
    // 最终的 Seed ,还会往 preSeed 里面填充一个 BlockHash。
    (uint256 requestId, uint256 preSeed) = computeRequestId(keyHash, msg.sender, subId, nonce);

    // 记录这次请求的内容的hash,留待做匹配验证
    s_requestCommitments[requestId] = keccak256(
      abi.encode(requestId, block.number, subId, callbackGasLimit, numWords, msg.sender)
    );

    // 抛出随机数请求的event,链下对应节点捕获到了后,会进入随机数生成的阶段
    emit RandomWordsRequested(
      keyHash,
      requestId,
      preSeed,
      subId,
      requestConfirmations,
      callbackGasLimit,
      numWords,
      msg.sender
    );

    // 更新 consumer 的 nonce
    s_consumers[msg.sender][subId] = nonce;

    return requestId;
  }

  // 计算 requestId 和 preSeed,随便看看就好
  function computeRequestId(
    bytes32 keyHash,
    address sender,
    uint64 subId,
    uint64 nonce
  ) private pure returns (uint256, uint256) {
    uint256 preSeed = uint256(keccak256(abi.encode(keyHash, sender, subId, nonce)));
    return (uint256(keccak256(abi.encode(keyHash, preSeed))), preSeed);
  }

返回随机数

  /*
   * @notice Fulfill a randomness request
   * @param proof contains the proof and randomness
   * @param rc request commitment pre-image, committed to at request time
   * @return payment amount billed to the subscription
   * @dev simulated offchain to determine if sufficient balance is present to fulfill the request
   */
  function fulfillRandomWords(Proof memory proof, RequestCommitment memory rc) external nonReentrant returns (uint96) {
    uint256 startGas = gasleft();

    // 对传进来的结果进行验证,该函数的详细分析在下面
    (bytes32 keyHash, uint256 requestId, uint256 randomness) = 
    getRandomnessFromProof(proof, rc);

    // 用随机数构建指定数量的 randomWords
    uint256[] memory randomWords = new uint256[](rc.numWords);
    for (uint256 i = 0; i < rc.numWords; i++) {
      randomWords[i] = uint256(keccak256(abi.encode(randomness, i)));
    }

    // 删除 request 的备案
    delete s_requestCommitments[requestId];
    // 对 Consumer 进行回调,通知随机数结果
    VRFConsumerBaseV2 v;
    bytes memory resp = abi.encodeWithSelector(v.rawFulfillRandomWords.selector, requestId, randomWords);
    // Call with explicitly the amount of callback gas requested
    // Important to not let them exhaust the gas budget and avoid oracle payment.
    // Do not allow any non-view/non-pure coordinator functions to be called
    // during the consumers callback code via reentrancyLock.
    // Note that callWithExactGas will revert if we do not have sufficient gas
    // to give the callee their requested amount.
    s_config.reentrancyLock = true;
    // 以指定的 gaslimit 进行回调
    bool success = callWithExactGas(rc.callbackGasLimit, rc.sender, resp);
    s_config.reentrancyLock = false;

    // 记录该订阅账号总共请求了多少次服务
    // Increment the req count for fee tier selection.
    uint64 reqCount = s_subscriptions[rc.subId].reqCount;
    s_subscriptions[rc.subId].reqCount += 1;

    // 这部分是在计算,consumer 需要支付多少钱,主要是由两部分组成 
    // (给oracle的雇佣费 + oracle发起回调交易产生的 gas fee (会换算成link))
    uint96 payment = calculatePaymentAmount(
      startGas,
      s_config.gasAfterPaymentCalculation,
      getFeeTier(reqCount),
      tx.gasprice
    ); // 后面有对该函数的详细解析
    // 账户余额不够,抛出异常
    if (s_subscriptions[rc.subId].balance < payment) {
      revert InsufficientBalance();
    }
    
    /*
      s_config.gasAfterPaymentCalculation 指的是后续操作会消耗的gas费用,这里只能预估了,因为其必须发生在计算完费用之后。
    */

    // 订阅账户扣钱
    // 提供服务的 oracle 账户加钱
    s_subscriptions[rc.subId].balance -= payment;
    s_withdrawableTokens[s_provingKeys[keyHash]] += payment;
    // Include payment in the event for tracking costs.
    emit RandomWordsFulfilled(requestId, randomness, payment, success);
    return payment;
  }

随机数验证

  function getRandomnessFromProof(Proof memory proof, RequestCommitment memory rc)
    private
    view
    returns (
      bytes32 keyHash,
      uint256 requestId,
      uint256 randomness
    )
  {
    // 计算 keyHash
    keyHash = hashOfKey(proof.pk);
    // 检查是不是注册了的oracle
    address oracle = s_provingKeys[keyHash];
    if (oracle == address(0)) {
      revert NoSuchProvingKey(keyHash);
    }
    // 计算 requestId
    requestId = uint256(keccak256(abi.encode(keyHash, proof.seed)));
    // 检查这个 request 有没有备案
    bytes32 commitment = s_requestCommitments[requestId];
    if (commitment == 0) {
      revert NoCorrespondingRequest();
    }
    // 通过备案的hash,检查信息是否一致
    if (
      commitment != keccak256(abi.encode(requestId, rc.blockNum, rc.subId, rc.callbackGasLimit, rc.numWords, rc.sender))
    ) {
      revert IncorrectCommitment();
    }

    // 获取指定高度的 blockhash
    bytes32 blockHash = blockhash(rc.blockNum);
    if (blockHash == bytes32(0)) {
      // 通过 BLOCKHASH_STORE (这是另一个合约) 获取 blcokhash
      blockHash = BLOCKHASH_STORE.getBlockhash(rc.blockNum);
      if (blockHash == bytes32(0)) {
        revert BlockhashNotInStore(rc.blockNum);
      }
    }

    // The seed actually used by the VRF machinery, mixing in the blockhash
    // 通过 preSeed (proof.seed 是 preSeed) 和 blockhash 组合成最终 seed
    uint256 actualSeed = uint256(keccak256(abi.encodePacked(proof.seed, blockHash)));
    
    // 验证vrf随机数 (公钥+seed来验证)
    randomness = VRF.randomValueFromVRFProof(proof, actualSeed); // Reverts on failure
  }

费用计算

涉及两个函数

  • getFeeTier 根据订阅账户记录的发起的请求数量,有不同的收费级别。这个收费级别的设置是在 FeeConfig s_feeConfig 中设置的。
    function getFeeTier(uint64 reqCount) public view returns (uint32) {
      FeeConfig memory fc = s_feeConfig;
      if (0 <= reqCount && reqCount <= fc.reqsForTier2) {
        return fc.fulfillmentFlatFeeLinkPPMTier1;
      }
      if (fc.reqsForTier2 < reqCount && reqCount <= fc.reqsForTier3) {
        return fc.fulfillmentFlatFeeLinkPPMTier2;
      }
      if (fc.reqsForTier3 < reqCount && reqCount <= fc.reqsForTier4) {
        return fc.fulfillmentFlatFeeLinkPPMTier3;
      }
      if (fc.reqsForTier4 < reqCount && reqCount <= fc.reqsForTier5) {
        return fc.fulfillmentFlatFeeLinkPPMTier4;
      }
      return fc.fulfillmentFlatFeeLinkPPMTier5;
    }
    
  • calculatePaymentAmount
    function calculatePaymentAmount(
      uint256 startGas, // 这里传入回调开始的时候剩余的初始 gas
      uint256 gasAfterPaymentCalculation, // 帐户付款这些相关的操作需要支付的 gas
      uint32 fulfillmentFlatFeeLinkPPM, // 回调收费
      uint256 weiPerUnitGas // gas 到 wei 价格
    ) internal view returns (uint96) {
      // 获取 wei 到 link 的价格
      int256 weiPerUnitLink;
      weiPerUnitLink = getFeedData();
      if (weiPerUnitLink <= 0) {
        revert InvalidLinkWeiPrice(weiPerUnitLink);
      }
      // (1e18 juels/link) (wei/gas * gas) / (wei/link) = juels
      //  startGas - gasleft() 计算的时目前消耗的 gas 数量
      // gasAfterPaymentCalculation是在设置的后面发生balance钱转移的操作会消耗的gas数量
      // 整个计算是为了计算目前消耗的gas值多少link币,这个钱要用户给的
      uint256 paymentNoFee = (1e18 * weiPerUnitGas * (gasAfterPaymentCalculation + startGas - gasleft())) /
        uint256(weiPerUnitLink);
    
      // 使用回调服务的雇佣费
      uint256 fee = 1e12 * uint256(fulfillmentFlatFeeLinkPPM);
      if (paymentNoFee > (1e27 - fee)) {
        revert PaymentTooLarge(); // Payment + fee cannot be more than all of the link in existence.
      }
    
      // 换算成link的gas费 + 雇佣费 = 这次服务总的费用
      return uint96(paymentNoFee + fee);
    }
    

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