如何在本地单元测试 Chainlink 的可验证随机函数?

How to locally unit-test Chainlink's Verifiable Random Function?

上下文

在尝试设置测试 this Chainlink VRF random number contract 的基本自托管单元测试环境(和 CI)时,我在如何模拟任何相关 blockchains/testnets 方面遇到了一些小困难本地。

比如我找到了this repository that tests Chainlinks VRF。但是,对于默认部署,它 suggests/requires 免费 KOVAN_RPC_URL 例如来自 Infura 的站点,甚至对于“本地部署”,它 suggests/requires 免费 MAINNET_RPC_URL 来自例如炼金术的网站。

Attempt/baseline

我采用了 waffle 框架的单元测试环境,描述为:

文件结构

src____AmIRichAlready.sol
   |____RandomNumberConsumer.sol
   |
test____AmIRichAlready.test.ts
   |____mocha.opts
package.json
tsconfig.json
waffle.json
yarn.lock

文件内容

AmIRichAlready.sol

pragma solidity ^0.6.2;

interface IERC20 {
    function balanceOf(address account) external view returns (uint256);
}

contract AmIRichAlready {
    IERC20 private tokenContract;
    uint public richness = 1000000 * 10 ** 18;

    constructor (IERC20 _tokenContract) public {
        tokenContract = _tokenContract;
    }

    function check() public view returns (bool) {
        uint balance = tokenContract.balanceOf(msg.sender);
        return balance > richness;
    }

    // IS THIS NEEDED???
    function setRichness(uint256 _richness) public {
      richness = _richness;
    }
}

RandomNumberConsumer.sol 文件内容已经在 stackexange 中 over here

AmIRichAlready.test.ts

import {expect, use} from 'chai';
import {Contract, utils, Wallet} from 'ethers';
import {deployContract, deployMockContract, MockProvider, solidity} from 'ethereum-waffle';

import IERC20 from '../build/IERC20.json';
import AmIRichAlready from '../build/AmIRichAlready.json';

use(solidity);

describe('Am I Rich Already', () => {
  let mockERC20: Contract;
  let contract: Contract;
  let vrfContract: Contract;
  let wallet: Wallet;

  beforeEach(async () => {
    [wallet] = new MockProvider().getWallets();
    mockERC20 = await deployMockContract(wallet, IERC20.abi);
    contract = await deployContract(wallet, AmIRichAlready, [mockERC20.address]);
    vrfContract = await deployContract(wallet, RandomNumberConsumer);
  });

  it('checks if contract called balanceOf with certain wallet on the ERC20 token', async () => {
    await mockERC20.mock.balanceOf
      .withArgs(wallet.address)
      .returns(utils.parseEther('999999'));
    await contract.check();
    expect('balanceOf').to.be.calledOnContractWith(mockERC20, [wallet.address]);
  });

  it('returns false if the wallet has less than 1000000 coins', async () => {
    await mockERC20.mock.balanceOf
      .withArgs(wallet.address)
      .returns(utils.parseEther('999999'));
    expect(await contract.check()).to.be.equal(false);
  });

  it('returns true if the wallet has at least 1000000 coins', async () => {
    await mockERC20.mock.balanceOf
      .withArgs(wallet.address)
      .returns(utils.parseEther('1000000'));
    expect(await contract.check()).to.be.equal(false);
  });
});

mocha.opts

-r ts-node/register/transpile-only
--timeout 50000
--no-warnings
test/**/*.test.{js,ts}

package.json

{
  "name": "example-dynamic-mocking-and-testing-calls",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "test": "export NODE_ENV=test && mocha",
    "build": "waffle",
    "lint": "eslint '{src,test}/**/*.ts'",
    "lint:fix": "eslint --fix '{src,test}/**/*.ts'"
  },
  "devDependencies": {
    "@openzeppelin/contracts": "^4.3.1",
    "@types/chai": "^4.2.3",
    "@types/mocha": "^5.2.7",
    "@typescript-eslint/eslint-plugin": "^2.30.0",
    "@typescript-eslint/parser": "^2.30.0",
    "chai": "^4.3.4",
    "eslint": "^6.8.0",
    "eslint-plugin-import": "^2.20.2",
    "ethereum-waffle": "^3.4.0",
    "ethers": "^5.0.17",
    "mocha": "^7.2.0",
    "ts-node": "^8.9.1",
    "typescript": "^3.8.3"
  }
}

tsconfig.json

{
  "compilerOptions": {
    "declaration": true,
    "esModuleInterop": true,
    "lib": [
      "ES2018"
    ],
    "module": "CommonJS",
    "moduleResolution": "node",
    "outDir": "dist",
    "resolveJsonModule": true,
    "skipLibCheck": true,
    "strict": true,
    "target": "ES2018"
  }

  // custom test in vrfContract
  it('Tests if a random number is returned', async () => {
    expect(await vrfContract.getRandomNumber()).to.be.equal(7);
  });
}

waffle.json

{
  "compilerType": "solcjs",
  "compilerVersion": "0.6.2",
  "sourceDirectory": "./src",
  "outputDirectory": "./build"
}

yarn.lock文件内容有点大,而且是自动生成的,所以你可以在同一个版本库中找到它on the Waffle framework repository. Similarly, the package.json can be found here

命令

也可以使用指定的文件结构 here 简单地克隆 repo,并使用以下命令 运行 测试:

git clone git@github.com:a-t-2/chainlink.git
git clone git@github.com:a-t-2/test_vrf3.git
cd test_vrf3
sudo apt install npm
npm install
npm audit fix
npm install --save-dev ethereum-waffle
npm install @openzeppelin/contracts -D
npm i chai -D
npm i mocha -D
rm -r build
npx waffle
npx mocha
npm test

测试输出

这将测试 AmIRichAlready.sol 文件并输出:

  Am I Rich Already
    ✓ checks if contract called balanceOf with certain wallet on the ERC20 token (249ms)
    ✓ returns false if the wallet has less than 1000000 coins (190ms)
    ✓ returns true if the wallet has at least 1000000 coins (159ms)
    Tests if a random number is returned:
     Error: cannot estimate gas; transaction may fail or may require manual gas limit (error={"name":"RuntimeError","results":{"0x0a0b028de6cf6e8446853a300061305501136cefa5f5eb3e96afd95dbd73dd92":{"error":"revert","program_counter":609,"return":"0x"}},"hashes":["0x0a0b028de6cf6e8446853a300061305501136cefa5f5eb3e96afd95dbd73dd92"],"message":"VM Exception while processing transaction: revert"}, tx={"data":"0xdbdff2c1","to":{},"from":"0x17ec8597ff92C3F44523bDc65BF0f1bE632917ff","gasPrice":{"type":"BigNumber","hex":"0x77359400"},"type":0,"nonce":{},"gasLimit":{},"chainId":{}}, code=UNPREDICTABLE_GAS_LIMIT, version=abstract-signer/5.4.1)
      at Logger.makeError (node_modules/@ethersproject/logger/src.ts/index.ts:225:28)
      at Logger.throwError (node_modules/@ethersproject/logger/src.ts/index.ts:237:20)
      at /home/name/git/trucol/tested/new_test/test_vrf3/node_modules/@ethersproject/abstract-signer/src.ts/index.ts:301:31
      at process._tickCallback (internal/process/next_tick.js:68:7)



  3 passing (4s)

问题

如果提供了足够的“gas”,我需要哪一组文件、文件结构和命令来自动测试getRandomNumber()合约returns是否为整数,否则会出现错误?

要在本地进行测试,您需要使用 mocks which can simulate having an oracle network. Because you're working locally, a Chainlink node doesn't know about your local blockchain, so you can't actually do proper VRF requests. Note you can try deploy a local Chainlink node and a local blockchain and have them talk, but it isn't fully supported yet so you may get mixed results. Anyway, as per the hardhat starter kit that you linked, you can set the defaultNetwork to be 'hardhat' in the hardhat.config.js file, then when you deploy and run the integration tests (yarn test-integration), it will use mocks to mock up the VRF node, and to test the requesting of a random number. See the test here, and the mock contracts and linktoken get deployed here

我也遇到过这个问题,通过mock解决了。这是我的 MockVRFCoordinator:

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol";

contract MockVRFCoordinator {
    uint256 internal counter = 0;

    function requestRandomWords(
        bytes32,
        uint64,
        uint16,
        uint32,
        uint32
    ) external returns (uint256 requestId) {
        VRFConsumerBaseV2 consumer = VRFConsumerBaseV2(msg.sender);
        uint256[] memory randomWords = new uint256[](1);
        randomWords[0] = counter;
        consumer.rawFulfillRandomWords(requestId, randomWords);
        counter += 1;
    }
}

你可以在我的repository

中查看测试示例