在链上创建随机数是一项复杂的任务。事实上,有一些方法可以做到这一点,但总的来说,强烈建议在链下进行,因为几乎所有用于熵的输入都是公开的,或者在某种程度上可以被操纵。

幸运的是,这个挑战要求我们猜测链上创建的一个“随机”数字。这是怎么回事?

猜随机数字挑战智能合约代码

合约的第一行是一个uint8变量,answer。记住uint8变量最多包含256个可能的整数:0到255。

这个变量在构造函数中被分配给两个输入的 keccak256 哈希:包含我们部署交易的那个区块的前一个区块的blockhash(block.blockhash(block.number - 1), of type bytes32)和我们的区块被挖的时间戳(now, of type uint256)。

请记住,这个合约使用的是编译器版本^0.4.21,从那以后,一些语法发生了变化:block.blockhash()现在是blockhash(),now现在是block.timestamp。

正如我们在这行中看到的,keccak256函数(一个 bytes32 固定大小的字节数组)随后被显式转换为uint8并赋值给我们的变量。

这看起来很随机,对吧?我们应该如何猜出0到255之间的一个数字,它来自于对某个区块的哈希函数和时间戳。

其实很简单。因为区块链上的所有东西都是公开的。

我们的目标是在函数下guess,我们必须调用它并发送一个uint8 + 1 以太(我们已经在部署上发送了一个),然后如果我们的uint8等于answer变量,合约将发送我们2个以太,耗尽余额,因此isComplete()函数将返回到 true。

有多种与合约交互的方式,但我决定通过另一个合约来实现。这不是最简单的方法,在这种情况下,甚至没有必要,但绝对是我们可以利用的方法。

以下是我为解决这个问题所编写的代码:

// SPDX-License-Identifier: No Licensepragma solidity ^0.8.0;interface IGuessTheRandomNumberChallenge { function guess(uint8) external payable;}contract GuessTheRandomNumberSolver { IGuessTheRandomNumberChallenge public _interface; bytes32 public previousBlockHash = 0x66bcdb5e320c9e0c04a9fdeaa15de33a4c8a040db342f4f955fa54f170dba9ce; uint public previousTimestamp = 1641520092; constructor(address _interfaceAddress) { require(_interfaceAddress != address(0), 'Address can not be Zero'); _interface = IGuessTheRandomNumberChallenge(_interfaceAddress); } function solve() public payable { uint8 answer = uint8(uint256(keccak256(abi.encodePacked(previousBlockHash, previousTimestamp)))); _interface.guess{value: 1 ether}(answer); } function getBalance() public view returns(uint){ return address(this).balance; } function withdraw() public { payable(msg.sender).transfer(address(this).balance); } receive() external payable {}}

编译器版本之后,首先看到的是一个接口。我们可以使用它们通过代码与其他合约交互。它基本上是一个带有一些规则的简单合约:

它们不能从其他合约继承,但可以从其他接口继承。所有声明的函数必须是外部的。它们不能声明构造函数。它们不能声明状态变量。它们不能声明修饰符。

因为我们只需要调用' guess '函数,所以它是我们在接口中声明的唯一一个函数。

然后,在我们的GuessTheRandomNumberSolver合约中,我们将声明一个_interface变量,并通过构造函数分配挑战的地址(在CTE中部署它时获得的的地址)。

这就是我们现在在已部署的挑战中调用函数所需要的一切,我们继续收集信息,以重新创建与它一起部署的random number。

这些都可以在etherscan中找到,我们只需要寻找我们挑战的地址。

Blockhash(block.number - 1):要得到这个,可以转到内部Internal Txns标签,然后单击显示Contract Creation的同一行上的区块号码。在我的例子中,区块是#11766860:

现在,我们可以看到很多关于那个区块的信息,但我们需要访问前一个,所以继续寻找它。在我的例子中,它是#11766859。

下面我们可以看到hash。这是我们需要的第一个信息。

Block.timestamp:回到我们的区块,你会在第二行看到时间戳。这是一种人类可读的格式,我们需要Unix Timestamp格式。那是什么? 它是自1970年1月1日以来所经过的秒数。这是衡量时间的标准方法。

为了将这个人类可读的时间戳转换为Unix时间,我使用了一个非常方便的站点epochconverter。有了这个数字,我们终于有了最后一块拼图,我们可以来解决这个挑战。

回到GuessTheRandomNumberSolver合约,让我们创建一个solve函数,我们将调用它来联系我们的挑战合约。

为了提高可读性,我还创建了两个新变量:

bytes32 public previousBlockHashuint public previousTimestamp.

创建它们,但要赋予它们挑战的价值。

然后,在我们的solve函数中,我们将创建uint8 answer变量,并将其赋值:

uint8(uint256(keccak256(abi.encodePacked(previousBlockHash, previousTimestamp))))

语法和格式的变化是因为我们使用的是^0.8.0版本的编译器,而挑战是使用^0.4.21版本。

现在我们已经将答案赋给了变量,我们只需要通过接口调用挑战。这就是下一行要做的:

_interface.guess{value: 1 ether}(answer)

我假设你正在使用remix,所以继续,通过Injected Web3环境连接到metamask钱包,并部署合约,指定自己的挑战地址来分配给自己的界面。

现在,在将值输入为1的情况下,继续调用guess函数。

我已经添加了更多的函数:

getBalance()withdraw()receive()

这是因为挑战是msg.sender将是我们的GuessTheRandomNumberSolver合约,而不是我们的EOA -所以我们需要接收2个以太,并能够将它们发送到我们的EOA。

Source:https://betterprogramming.pub/capture-the-ether-guess-the-random-number-2ebb8c9c0347