如何在智能合约中安全地生成一个真正的随机数?

Posted 云灬沙

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何在智能合约中安全地生成一个真正的随机数?相关的知识,希望对你有一定的参考价值。

随机数和区块链一直很难达到“一致”(区块链要求确定性,而随机数正相反)。

原因是:交易被矿工出块后,需要网络上的多个节点来确认才算真实有效。就要求每个节点验证时都必须得出相同的结果。如果函数是随机的(每次运行的结果不一样),则每个节点将得出不同的结果,从而导致交易得不到确认。

到目前为止,已有的方法都不算是真正的随机,或存在操控的可能。

设计的原则[1]

[1]翻译自https://ethereum.stackexchange.com/questions/191/how-can-i-securely-generate-a-random-number-in-my-smart-contract

  • 注意的点

  1. 用户可以做出任何给用户带来优势的决定和行为。 如:
    1. 使用blockhash、时间戳或其他矿工定义的值。 要明确一点,矿工可以有选择的是否发布一个块,所以他们有机会选择有利的区块。
    2. 任何用户提交的随机数。 即使用户预先提交了一个号码,他们也可以选择是否透露号码。
  2. 一切合约上的数据都是公开的
    1. 这意味着,直到进入抽奖后已经关闭之前,随机号码都不应该被生成。
  3. EVM不会快过物理计算机
    1. 合约生成的任何数字可以在该块结束之前知道。 在生成数字和使用数字之间留出时间。
  • 一个技术方案

完全去中心的彩票方案(也许扩展不好)

  1. 赌场为一个随机数字预留了奖励

  2. 每个用户生成自己的秘密随机数N

  3. 用户计算N和地址的Hash :bytes32 hash = sha3(N,msg.sender)

    注意: 2 和 3 应该在离线的安全环境下执行

  4. 发送上一步生产的 hash 给合约(尽管可能hash数据量比N大)

  5. 其他的用户继续按同样的方法提交各自的hash, 知道回合结束。

    需要所有的提交完成之后,才进行开奖环节。

  6. 每个用户向合约提交之前生成的随机数N。

  7. 合约根据hash值验证随机数N,使用同样的方法sha3(N,msg.sender),无法通过验证的N,可以没收罚金。

  8. 可以考虑所有有效的N在一起生成一个最终的随机数。

  9. 用这个随机数来决定谁可以获奖。

使用Chainlink VRF在以太坊上生成随机数[2]

[2]摘自https://cloud.tencent.com/developer/article/1634665

VRF 事件发生的顺序:

1.你的智能合约通过交易向VRF请求一个随机数。

2.VRF会生成该随机数字并进行验证。

3.VRF准备响应1 的请求。

4.VRF通过另一笔交易将随机数字发送回你的智能合约。

如何实现随机性

1.创建一个名为RandomGenerator的新合约,在合约里调用VRF并接收结果。

引入 Chainlink提供的VRFConsumerBase的合约,这是一个抽象合约,它定义了一个获取和消耗VRF的最少实现(后面也会列出VRFConsumerBase的代码),我们定义“ RandomGenerator.sol”文件开头:

pragma solidity ^0.6.2;

import "https://raw.githubusercontent.com/smartcontractkit/chainlink/develop/evm-contracts/src/v0.6/VRFConsumerBase.sol";

contract RandomGenerator is VRFConsumerBase {

    constructor(address _vrfCoordinator, address _link) VRFConsumerBase(_vrfCoordinator, _link) public {
    }

}

VRFConsumerBase的合约的源码如下:

pragma solidity 0.6.2;

import "./vendor/SafeMath.sol";
import "./interfaces/LinkTokenInterface.sol";
import "./VRFRequestIDBase.sol";

abstract contract VRFConsumerBase is VRFRequestIDBase {

  using SafeMath for uint256;

  function fulfillRandomness(bytes32 requestId, uint256 randomness)
    external virtual;


  function requestRandomness(bytes32 _keyHash, uint256 _fee, uint256 _seed)
    public returns (bytes32 requestId)
  {
    LINK.transferAndCall(vrfCoordinator, _fee, abi.encode(_keyHash, _seed));
    // This is the seed actually passed to the VRF in VRFCoordinator
    uint256 vRFSeed  = makeVRFInputSeed(_keyHash, _seed, 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)
    nonces[_keyHash] = nonces[_keyHash].add(1); 
    return makeRequestId(_keyHash, vRFSeed);
  }

  LinkTokenInterface internal LINK;
  address internal vrfCoordinator;

  // Nonces for each VRF key from which randomness has been requested.
  //
  // Must stay in sync with VRFCoordinator[_keyHash][this]
  mapping(bytes32 /* keyHash */ => uint256 /* nonce */) public nonces;
  constructor(address _vrfCoordinator, address _link) public {
    vrfCoordinator = _vrfCoordinator;
    LINK = LinkTokenInterface(_link);
  }
}

VRFConsumerBase 仍在后期测试中,因此还没有产品软件包对外提供。这就是为什么使用Github的HTTP URL进行导入的原因。

VRFConsumerBase抽象合约有两个参数,分别代表协调器(coordinator)和LINK ERC20 代币合约的地址。

第 2 步: 重载函数

VRFConsumerBase 中有两个对VRF流程至关重要的函数。

第一个是 requestRandomness ,这个函数已经实现了,我们不需要重载。这个函数是用来对VRF进行初始请求调用

另一个是 fulfillRandomness, 这是VRF在生成数字后,用来回调的函数。我们需要重载它,以便在获取随机数后执行相应的操作。

在我们合约的实现里,仅仅是把随机数存储在一个名为randomNumber的状态变量中,以便我们可以在结束时查询它。代码像这样:

pragma solidity ^0.6.2;

import "https://raw.githubusercontent.com/smartcontractkit/chainlink/develop/evm-contracts/src/v0.6/VRFConsumerBase.sol";

contract RandomGenerator is VRFConsumerBase {

    bytes32 public reqId;
    uint256 public randomNumber;

    constructor(address _vrfCoordinator, address _link) VRFConsumerBase(_vrfCoordinator, _link) public {
    }

    function fulfillRandomness(bytes32 requestId, uint256 randomness) external override {
        reqId = requestId;
        randomNumber = randomness;
    }
}

我们在fulfillRandomness函数上添加了override[6] 修饰符以实现重载,在实现中,使用reqIdrandomNumber 来保存接收变量的值。

第 3 步: 生成随机数

正如在前面 第1步提到的,函数调用需要传递一些地址和其他值作为参数。在部署智能合约并调用构造函数时,它需要VRF协调器(coordinator)合约地址和网络上LINK 代币合约地址。在Ropsten测试网上,合约地址如下:

VRF coordinator: 0xf720CF1B963e0e7bE9F58fd471EFa67e7bF00cfb

LINK 代币: 0x20fE562d797A42Dcb3399062AE9546cd06f63280

当调用 requestRandomness函数时,我们需要传递几个参数:生成随机数的key hash,生成随机数的费用fee(使用LINK代币)和生成随机性的种子seed(最后一个由我们提供)。requestRandomness函数签名如下:

function requestRandomness(bytes32 _keyHash, uint256 _fee, uint256 _seed) public returns (bytes32 requestId)

在 Ropsten 网络上,参数值如下:

•Key hash值: 0xced103054e349b8dfb51352f0f8fa9b5d20dde3d06f9f43cb2b85bc64b238205•费用 Fee (1 LINK): 1000000000000000000•种子 Seed: [我们想要的任意值]

因此我们的调用代码如下

// 设置ropsten key hash
bytes32 keyHash = "0xced103054e349b8dfb51352f0f8fa9b5d20dde3d06f9f43cb2b85bc64b238205";// // 设置 ropsten LINK 费用
fee = 1000000000000000000;
// 设置种子
seed = 123456789;
// 请求随机数
bytes32 reqId = rand.requestRandomness(keyHash, fee, seed);

当结果返回时,随机值将存储并且可以通过以下方法获取:

rand.randomNumber;

以上是关于如何在智能合约中安全地生成一个真正的随机数?的主要内容,如果未能解决你的问题,请参考以下文章

第157篇 合约安全-随机数

第157篇 合约安全-随机数

盘古开源解析:智能合约在Filecoin中真正价值

计算机程序可以产生真正的随机数吗?不是random伪随机

智能合约实战 solidity 语法学习 12 [ virtual 重写override 抽象abstract 接口interface 库library 销毁selfdestruct] 附代码(代码片

在java语言中如何随机地生成一个字符串