共计 4604 个字符,预计需要花费 12 分钟才能阅读完成。
智能合约中使用更安全的随机数(代码实战篇)
Chainlink 最近推出一款革命性的产品,VRF—Verifiable Random Function 可验证随机数,给智能合约带来了真正安全的随机数。本文我们就来介绍一下如何在智能合约中使用 VRF 吧。
我们先简要介绍一下 Chainlink VFR 的工作流程。
- 首先,智能合约应用,也就是我们的 Dapp,需要先发起一个获取随机数的请求,这个请求需要给定一个合约地址,这个合约称为 VRFCoordinator 合约。
- 与 VRFCoordinator 合约所关联的 Chainlink 链下节点,会(通过椭圆曲线数字签名算法)生成一个随机数,以及一个证明。
- Chainlink 节点将上面生成的随机数和证明发送到 VRFCoordinator 合约中。
- VRFCoordinator 合约收到随机数和证明后,会对通过证明来验证所生成随机数的合法性。
- 随机数验证成功后,会将随机数发送回用户的智能合约应用
整个过程中有两次的交易提交确认的过程,用户合约需要支付 LINK 给 VRF 合约作为交易费用。
下面我们就通过写一个猜数字的小游戏,来学习如何使用 Chainlink VRF。
首先,新建一个 truffle 项目,安装 Chainlink 开发包
mkdir vrf; cd vrf
truffle init
npm install @chainlink/contracts --save
在 contracts
目录下新建一个合约文件MyVrfContract.sol
。引入 vrf 的库文件:
pragma solidity 0.6.2;
import "@chainlink/contracts/src/v0.6/VRFConsumerBase.sol";
VRFConsumerBase 是我们需要继承的基类,使用 Chainlink VRF 的很多方法都在这个合约里定义了,因此我们来简单介绍一下这个合约。
abstract contract VRFConsumerBase is VRFRequestIDBase {
...
function fulfillRandomness(bytes32 requestId, uint256 randomness)
external virtual;
function requestRandomness(bytes32 _keyHash, uint256 _fee, uint256 _seed)
public returns (bytes32 requestId)
{...}
...
}
上面列出了 VRFConsumerBase
合约的两个基本方法,一个是 requestRandomness
方法,它是用来发起一个 VRF 请求的方法,在调用的时候呢,需要传入三个参数:
-
_keyHash
: 节点的公钥哈希。因为随机数及其证明的生成,是通过椭圆曲线密码学来完成的,所以需要一对公钥和私钥。公钥是节点公开的密钥,目前可用的 VRF 节点公钥及 VRF 节点的其他相关信息,可用在 Chainlink 官方文档上查到。 -
_fee
: 用户发起一次 VRF 请求所需要花费的费用,这个费用也可以在节点公布的相关信息中查阅到。如果费用没有达到节点的最低要求,那么 VRF 请求无法完成。费用是以 LINK token 形式支付,所以用户合约需要持有足够的 LINK token。 -
_seed
: 用户提供的种子,用于生成随机数。用户需要给出高质量的种子。这里我们需要解释一下 VRF 的特点,VRF 是通过种子与节点私钥做椭圆曲线中的计算得来的,同一个种子对应的随机数也是相同的,所以用户需要每次都给出不一样的且不可预测的种子。这很重要。因为任何可以左右用户种子的因素,都可以与链下的节点勾结作恶,生成他们想要的随机数,从而损害用户的利益。区块链中,我们很容易就找到这么一个随机源就是区块哈希,但是区块哈希是可以被矿工控制的(虽然很难),所以建议不能仅使用区块链哈希,还需要与其他随机源一起使用生成种子。比如下面就是一个例子。
function makeRequest(uint256 userProvidedSeed)
public returns (bytes32 requestId)
{uint256 seed = uint256(keccak256(abi.encode(userProvidedSeed, blockhash(block.number)))); // Hash user seed and block hash
return requestRandomness(keyHash, fee, seed);
}
VRFConsumerBase
合约中的另外一个重要的方法是fulfillRandomness
,这是一个虚函数,需要在继承的类中加以实现。这个函数的主要作用就是接受节点生成的随机数。用户合约在复写这个函数的时候就可以添加一些自己的业务逻辑代码。
另外,VRFConsumerBase
合约中海油一个构造函数,需要调用者提供_vrfCoordinator 合约的地址和所在网络中 LINK token 的地址。
constructor(address _vrfCoordinator, address _link) public {
vrfCoordinator = _vrfCoordinator;
LINK = LinkTokenInterface(_link);
}
在我们的用户合约中,需要通过构造函数,来进行一些初始化的工作。初始化的内容包括向基类,也就是 VRFConsumerBase 类的构造函数进行赋值初始化,还包括初始化本合约所用到的一些常量参数。
constructor(address _vrfCoordinator, address _link)
VRFConsumerBase(_vrfCoordinator, _link) public {// 其他一些初始化参数}
下面就需要发起 VRF 请求来获取随机数,调用一下基类里的 requestRandomness
方法就可以。
bytes32 internal constant keyHash = 0xced103054e349b8dfb51352f0f8fa9b5d20dde3d06f9f43cb2b85bc64b238205;
uint256 internal constant fee = 10 ** 18;
function getRandomness(uint256 userProvidedSeed)
public returns (bytes32 requestId) {require(LINK.balanceOf(address(this)) > fee, "Not enough LINK - fill contract with faucet");
uint256 seed = uint256(keccak256(abi.encode(userProvidedSeed, blockhash(block.number)))); // Hash user seed and blockhash
bytes32 _requestId = requestRandomness(keyHash, fee, seed);
return _requestId;
}
在这个请求函数中,我们把用户合约和区块哈希共同作为种子。keyHash 和 fee 就作为常量直接写在合约里,也可以写到 setter 方法中或者构造函数中。
接下来就是复写接收方法。
uint256 public random;
function fulfillRandomness(bytes32 requestId, uint256 randomness)
external override {random = randomness.mod(100).add(1);
}
这里我们就直接赋值给我们的一个公开变量,方便我们查看结果。节点生成的随机数是一个非常大的一个数,我们需要对原始的随机数做一个模运算,以方面我们后面的处理。加一是为了避免得到 0 值。
下面我们贴一下完整的代码:
pragma solidity 0.6.2;
import "@chainlink/contracts/src/v0.6/VRFConsumerBase.sol";
// import this if you are using remix
// import "https://raw.githubusercontent.com/smartcontractkit/chainlink/develop/evm-contracts/src/v0.6/VRFConsumerBase.sol";
contract MyVRFContract is VRFConsumerBase {constructor(address _vrfCoordinator, address _link)
VRFConsumerBase(_vrfCoordinator, _link) public { }
bytes32 internal constant keyHash = 0xced103054e349b8dfb51352f0f8fa9b5d20dde3d06f9f43cb2b85bc64b238205;
uint256 internal constant fee = 10 ** 18;
function getRandomness(uint256 userProvidedSeed) public returns (bytes32 requestId) {require(LINK.balanceOf(address(this)) > fee, "Not enough LINK - fill contract with faucet");
uint256 seed = uint256(keccak256(abi.encode(userProvidedSeed, blockhash(block.number))));
bytes32 _requestId = requestRandomness(keyHash, fee, seed);
return _requestId;
}
uint256 public random;
function fulfillRandomness(bytes32 requestId, uint256 randomness) external override {random = randomness.mod(100).add(1);
}
}
好了,这样一个简单的用户合约写完了,我们就可以写根据之前的方法测试一下啦。由于步骤都是类似的,这里就不在赘述了。可以参考这里进行测试。
总结,虽然 VRF 内涵了非常复杂深奥的密码学知识,但是得益于 Chainlink 工程师们的良好设计,已经把复杂性极大程度的掩盖了,作为 Dapp 开发者,我们在使用起来非常的容易上手,只需要注意提供好种子就可以了。快来尝试一下吧!
参考文档:
https://blog.chain.link/verifiable-random-functions-vrf-random-number-generation-rng-feature/
https://docs.chain.link/docs/chainlink-vrf
https://docs.chain.link/docs/vrf-contracts
https://learnblockchain.cn/article/1056
https://www.trufflesuite.com/docs/truffle/quickstart
欢迎加入 Chainlink 开发者社区