chainlink VRF讲解
VRF(可验证随机数)
VRF的三个函数
- 公私钥生成函数,即 G(r) → (PrivateKey, PublicKey);
- 随机数生成函数,即 G(PrivateKey, Seed) → (RandomNumber, Proof); //seed是一个随机性种子
- 验证函数,即 V(Proof, RandomNumber, PrivateKey, Seed) → (bool)。//验证是否是由自己生成的随机数
整个流程
- 预言机节点网络中,每个节点都生成一个公私钥对;
- 需求方使用合约发送VRF请求,接收消息的合约会生成日志事件记录消息。
- 预言机节点监听网络的event,发现请求后生成随机数及证明;
- 进行回调,释放事件;
其他随机数生成方案
一般链上生成随机数方式
1uint private _counter = 0;
function getRandomWithTen() external returns (uint) {
++_counter;
return uint(keccak256(abi.encode(
blockhash(1),
gasleft(),
block.number,
_counter //_counter在这里起到的妙用就是:在一个区块内,以保证用户发送的多笔交易生成的随机数是不同的
))) % 10;
}
存在问题
对于普通用户来说,这是安全的。但是对于矿工来说,这些信息都是可以看到的,它可以在出块的时候,调控 gasleft() 参数,通过节点计算出最终的要得奖的随机数。
解决方法
那么针对问题,我们肯定是要保证随机数生成的相关信息是不会别任何人知道的。这里直接引用了文档的介绍
Chainlink VRF(可验证随机函数)是一种可证明公平且可验证的随机数生成器(RNG),它使智能合约能够在不影响安全性或可用性的情况下访问随机值。对于每个请求, Chainlink VRF 生成一个或多个随机值以及如何确定这些值的加密证明。在任何 consumer 应用程序可以使用该证明之前,该证明将在链上发布和验证。此过程确保结果不会被任何单个实体篡改或操纵,包括预言机运营商、矿工、用户或智能合约开发者。
注意
在这个过程中,你所使用的区块链的底层矿工/验证者可以重写链的历史来将你发送的随机性请求合约放到不同的块,这将导致不同的 VRF 输出。请注意,这并不能让矿工提前确定随机值。它只能让他们获得一个新的随机值,这可能对他们有利,也可能不利。打个比方,他们只能重新掷骰子,而不能预先确定或预测骰子会落在哪一边。你必须为你提出的随机性请求选择适当的确认时间,也就是确认区块数。
具体操作流程
代码示例
大佬可以直接看着文档直接写,下面我以介绍实操为主,我是直接按照里面的流程走的,主要是初学者进行熟悉,以后文档都可以这样去看
-
首先导入两个库合约
// SPDX-License-Identifier: MIT pragma solidity 0.8.19; import {VRFConsumerBaseV2Plus} from "@chainlink/contracts/src/v0.8/vrf/dev/VRFConsumerBaseV2Plus.sol"; import {VRFV2PlusClient} from "@chainlink/contracts/src/v0.8/vrf/dev/libraries/VRFV2PlusClient.sol"; contract VRFD20 is VRFConsumerBaseV2Plus { }
VRFConsumerBaseV2Plus.sol 合约内容
// SPDX-License-Identifier: MIT pragma solidity ^0.8.4; import {IVRFCoordinatorV2Plus} from "./interfaces/IVRFCoordinatorV2Plus.sol"; import {IVRFMigratableConsumerV2Plus} from "./interfaces/IVRFMigratableConsumerV2Plus.sol"; import {ConfirmedOwner} from "../../shared/access/ConfirmedOwner.sol"; abstract contract VRFConsumerBaseV2Plus is IVRFMigratableConsumerV2Plus, ConfirmedOwner { error OnlyCoordinatorCanFulfill(address have, address want); error OnlyOwnerOrCoordinator(address have, address owner, address coordinator); error ZeroAddress(); // s_vrfCoordinator should be used by consumers to make requests to vrfCoordinator // so that coordinator reference is updated after migration IVRFCoordinatorV2Plus public s_vrfCoordinator; /** * @param _vrfCoordinator address of VRFCoordinator contract */ constructor(address _vrfCoordinator) ConfirmedOwner(msg.sender) { if (_vrfCoordinator == address(0)) { revert ZeroAddress(); } s_vrfCoordinator = IVRFCoordinatorV2Plus(_vrfCoordinator); } /** * @notice fulfillRandomness handles the VRF response. Your contract must * @notice implement it. See "SECURITY CONSIDERATIONS" above for important * @notice principles to keep in mind when implementing your fulfillRandomness * @notice method. * * @dev VRFConsumerBaseV2Plus expects its subcontracts to have a method with this * @dev signature, and will call it once it has verified the proof * @dev associated with the randomness. (It is triggered via a call to * @dev rawFulfillRandomness, below.) * * @param requestId The Id initially returned by requestRandomness * @param randomWords the VRF output expanded to the requested number of words */ // solhint-disable-next-line chainlink-solidity/prefix-internal-functions-with-underscore function fulfillRandomWords(uint256 requestId, uint256[] calldata randomWords) internal virtual; // rawFulfillRandomness is called by VRFCoordinator when it receives a valid VRF // proof. rawFulfillRandomness then calls fulfillRandomness, after validating // the origin of the call function rawFulfillRandomWords(uint256 requestId, uint256[] calldata randomWords) external { if (msg.sender != address(s_vrfCoordinator)) { revert OnlyCoordinatorCanFulfill(msg.sender, address(s_vrfCoordinator)); } fulfillRandomWords(requestId, randomWords); } /** * @inheritdoc IVRFMigratableConsumerV2Plus */ function setCoordinator(address _vrfCoordinator) external override onlyOwnerOrCoordinator { if (_vrfCoordinator == address(0)) { revert ZeroAddress(); } s_vrfCoordinator = IVRFCoordinatorV2Plus(_vrfCoordinator); emit CoordinatorSet(_vrfCoordinator); } modifier onlyOwnerOrCoordinator() { if (msg.sender != owner() && msg.sender != address(s_vrfCoordinator)) { revert OnlyOwnerOrCoordinator(msg.sender, owner(), address(s_vrfCoordinator)); } _; } }
这个合约定义了我们之后要使用的一些重要的函数,由我们定义的函数去继承
VRFV2PlusClient.sol 合约内容
// SPDX-License-Identifier: MIT pragma solidity ^0.8.4; // End consumer library. library VRFV2PlusClient { // extraArgs will evolve to support new features bytes4 public constant EXTRA_ARGS_V1_TAG = bytes4(keccak256("VRF ExtraArgsV1")); struct ExtraArgsV1 { bool nativePayment; } struct RandomWordsRequest { bytes32 keyHash; uint256 subId; uint16 requestConfirmations; uint32 callbackGasLimit; uint32 numWords; bytes extraArgs; } function _argsToBytes(ExtraArgsV1 memory extraArgs) internal pure returns (bytes memory bts) { return abi.encodeWithSelector(EXTRA_ARGS_V1_TAG, extraArgs); } }
这个合约定义了一些必要的参数(后面会讲),以及配置了是否使用原生代币(eth)功能的函数
-
补充我们现在的合约,引入一些必要的变量 和 映射
// SPDX-License-Identifier: MIT pragma solidity 0.8.19; import {VRFConsumerBaseV2Plus} from "@chainlink/contracts/src/v0.8/vrf/dev/VRFConsumerBaseV2Plus.sol"; import {VRFV2PlusClient} from "@chainlink/contracts/src/v0.8/vrf/dev/libraries/VRFV2PlusClient.sol"; contract VRFD20 is VRFConsumerBaseV2Plus { uint256 s_subscriptionId; address vrfCoordinator = 0x9DdfaCa8183c41ad55329BdeeD9F6A8d53168B1B; bytes32 s_keyHash = 0x787d74caea10b2b357790d5b5247c2f63d1d91572a9846f780606e4d953677ae; uint32 callbackGasLimit = 40000; uint16 requestConfirmations = 3; uint32 numWords = 1; //用来检查请求是否被响应过 mapping(uint256 => address) private s_rollers; mapping(address => uint256) private s_results; }
- s_subscriptionId :我们使用服务的订阅id
- vrfCoordinator : Chainlink VRF 协调器合约的地址,协调链上与链下
- s_keyHash : 定义了gas单价区间,通过用户需求选择决定,为了快速上链,支付更高的gas
- callbackGasLimit :函数能在回调请求中使用的最大 gas 上限
- requestConfirmations :等待的区块数量,节点等待的时间越长,随机值越安全
- numWords :要请求多少个随机值
-
初始化合约 引入事件,发起请求,存储requestId
// SPDX-License-Identifier: MIT pragma solidity 0.8.19; import {VRFConsumerBaseV2Plus} from "@chainlink/contracts/src/v0.8/vrf/dev/VRFConsumerBaseV2Plus.sol"; import {VRFV2PlusClient} from "@chainlink/contracts/src/v0.8/vrf/dev/libraries/VRFV2PlusClient.sol"; contract VRFD20 is VRFConsumerBaseV2Plus { uint256 s_subscriptionId; address vrfCoordinator = 0x9DdfaCa8183c41ad55329BdeeD9F6A8d53168B1B; bytes32 s_keyHash = 0x787d74caea10b2b357790d5b5247c2f63d1d91572a9846f780606e4d953677ae; uint32 callbackGasLimit = 40000; uint16 requestConfirmations = 3; uint32 numWords = 1; //用来检查请求是否被响应过 mapping(uint256 => address) private s_rollers; mapping(address => uint256) private s_results; // variables uint256 private constant ROLL_IN_PROGRESS = 42; // ... // constructor constructor(uint256 subscriptionId) VRFConsumerBaseV2Plus(vrfCoordinator) {//向父合约的构造函数传入参数 s_subscriptionId = subscriptionId; } // events event DiceRolled(uint256 indexed requestId, address indexed roller); // ... // ... // { constructor } // ... // rollDice function function rollDice(address roller) public onlyOwner returns (uint256 requestId) { require(s_results[roller] == 0, "Already rolled"); // Will revert if subscription is not set and funded. //s_vrfCoordinator 这个参数是在我们的父合约中 requestId = s_vrfCoordinator.requestRandomWords( VRFV2PlusClient.RandomWordsRequest({ keyHash: s_keyHash, subId: s_subscriptionId, requestConfirmations: requestConfirmations, callbackGasLimit: callbackGasLimit, numWords: numWords, // Set nativePayment to true to pay for VRF requests with Sepolia ETH instead of LINK extraArgs: VRFV2PlusClient._argsToBytes(VRFV2PlusClient.ExtraArgsV1({nativePayment: false})) }) ); s_rollers[requestId] = roller; s_results[roller] = ROLL_IN_PROGRESS; emit DiceRolled(requestId, roller); } }
- rollDice 函数作用
- 检查owner是否已经发过申请了,避免瞬间的多次点击造成请求都只在一个区块交易内,这样回返回多个相同的随机数
- 调用 s_vrfCoordinator 请求随机数
- 存储
requestId
和 owner地址
- rollDice 函数作用
-
返回随机数 fulfillRandomWords
// SPDX-License-Identifier: MIT pragma solidity 0.8.19; import {VRFConsumerBaseV2Plus} from "@chainlink/contracts/src/v0.8/vrf/dev/VRFConsumerBaseV2Plus.sol"; import {VRFV2PlusClient} from "@chainlink/contracts/src/v0.8/vrf/dev/libraries/VRFV2PlusClient.sol"; contract VRFD20 is VRFConsumerBaseV2Plus { uint256 s_subscriptionId; address vrfCoordinator = 0x9DdfaCa8183c41ad55329BdeeD9F6A8d53168B1B; bytes32 s_keyHash = 0x787d74caea10b2b357790d5b5247c2f63d1d91572a9846f780606e4d953677ae; uint32 callbackGasLimit = 40000; uint16 requestConfirmations = 3; uint32 numWords = 1; //用来检查请求是否被响应过 mapping(uint256 => address) private s_rollers; mapping(address => uint256) private s_results; // variables uint256 private constant ROLL_IN_PROGRESS = 42; // ... // constructor constructor(uint256 subscriptionId) VRFConsumerBaseV2Plus(vrfCoordinator) {//向父合约的构造函数传入参数 s_subscriptionId = subscriptionId; } ... // events event DiceRolled(uint256 indexed requestId, address indexed roller); event DiceLanded(uint256 indexed requestId, uint256 indexed result); // ... // ... // { constructor } // ... // rollDice function function rollDice(address roller) public onlyOwner returns (uint256 requestId) { require(s_results[roller] == 0, "Already rolled"); // Will revert if subscription is not set and funded. //s_vrfCoordinator 这个参数是在我们的父合约中 requestId = s_vrfCoordinator.requestRandomWords( VRFV2PlusClient.RandomWordsRequest({ keyHash: s_keyHash, subId: s_subscriptionId, requestConfirmations: requestConfirmations, callbackGasLimit: callbackGasLimit, numWords: numWords, // Set nativePayment to true to pay for VRF requests with Sepolia ETH instead of LINK extraArgs: VRFV2PlusClient._argsToBytes(VRFV2PlusClient.ExtraArgsV1({nativePayment: false})) }) ); s_rollers[requestId] = roller; s_results[roller] = ROLL_IN_PROGRESS; emit DiceRolled(requestId, roller); } function fulfillRandomWords(uint256 requestId, uint256[] calldata randomWords) internal override { // transform the result to a number between 1 and 20 inclusively uint256 d20Value = (randomWords[0] % 20) + 1; // assign the transformed value to the address in the s_results mapping variable s_results[s_rollers[requestId]] = d20Value; // emitting event to signal that dice landed emit DiceLanded(requestId, d20Value); } }
最后再次注意文档里面说的 该示例适用于 Sepolia 测试网,
申请订阅id
- 来到这个页面,点击 创建订阅
- 填写信息
- 按照要求支付
- 成功之后,会让你添加资金,其实就是给到这个合约,因为我们并没有使用原生代币支付,而是使用link去支付这个服务,测试币可以通过官方水龙头获取
- 之后需要我们添加消费合约,也就是我们刚刚创建的合约,先进行部署,再填写合约地址
- 添加成功之后就会看到我们的订阅记录
- 再remix中输入我们的订阅Id,调用函数之后,在测试网信息的返回事件里面可以看到我们的随机数的一个结果(因为回调返回的函数是内部函数,等于是代理合约向我们合约发送的消息,我们接收了,通过内部函数释放事件)
版权声明
本文仅代表作者观点,不代表区块链技术网立场。
本文系作者授权本站发表,未经许可,不得转载。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。