如何实现Solidity智能合约的安全随机数?
- 原文:https://soliditydeveloper.com/2019-06-23-randomness-blockchain 作者:Markus Waas
- 译文出自:登链翻译计划
- 译者:翻译小组
- 校对:Tiny 熊
- 本文永久链接:learnblockchain.cn/article…
当我们谈论随机数和区块链时,实际上是两个问题:
当然这两个问题也有一些重叠的地方,一些用于第一个问题的方法也可能用于第二个问题,反之亦然。 但我可以告诉你,这两个问题的最佳解决方案很可能还没有找到。 事实上,这些问题真的很重要,用著名的唐纳德的话说就是:”随机数不应该用随机选择的方法生成“。
为什么这么难? 嗯,这是由于随机数的性质。 人们可以很容易地创造出一个看似随机的数字流,但这个数字流遵循某种已知的逻辑,从使攻击者能够预测到数字。
我们可能会天真地提出,每个节点在本地计算出一个随机数。 然后在广播出这个随机数。 由于每个节点都会做同样的事情,所以可以用一个函数来计算最后的随机数,这个函数将之前本地产生的所有数字作为输入,并产生一个单一的输出结果随机数,例如: v1 ⊕ v2 - - - ⊕ vn
。然而,最后一个广播本地随机数的节点可以等待,直到他从其他节点收到随机数。 然后,他可以通过选取一个本地随机数R 以满足vx = R ⊕ v1 ⊕ v2 - - ⊕ vn
来产生分布式系统的任何最终随机数。 显然,这样一个产生随机数的系统是有缺陷的。
我们需要更好的方式。如何解决随机数的这些问题,值得继续关注详细介绍。你也可以看看预测以太坊智能合约中的随机数。 本文是讨论第一个问题(智能合约中生成随机数)的良好开端。 而对于第二个问题,现在有一些有趣的想法,也有一些看似疯狂的想法,比如以太坊基金会的新想法,建立成千上万的ASIC来验证VDF。
现在,大多数人都知道,当人们试图在智能合约中生成随机数时,会面临一个问题。 遗憾的是没有一个万能的解决方案,让我来介绍一下现有的解决方案。
我们简单的看一下常见的方案,以及它们为什么不好。 在这里我就不详细描述,因为已经有其他的文章描述的很好了。
block.number
: 区块号。block.timestamp
: 区块时间戳。block.difficulty
: 区块难度,即尾部多少个零才够成新哈希。block.gaslimit
: 区块的gas限制,即每笔交易允许的最大gas。block.coinbase
: 出块矿工的地址。这些都是明显的错误选择,因为它们可以被任何人或者至少是矿工可以预测值。 有些(block.number
)比其他的(block.difficulty
)更容易预测。
如果我们在合约中增加一个私有的种子呢? 可以用一个传递的变量和私有存储的种子作为输入进行计算由此产生随机数。然而,这种方法并没有考虑到不可能在公共网络内存储私有数据。 尽管以太坊在智能合约中有私有存储的概念,但任何运行以太坊节点的人仍然可以读取这个存储。 读取私有状态或内部状态可以通过web3.eth.getStorageAt
来实现。 因此,这个方式只是增加了试图预测随机数的人的努力。
从技术上讲,它也是一个区块变量,但它有自己的部分。 以太坊中的区块哈希计算为Keccak256,这是SHA-3的早期实现。 Keccak256是一个单向函数,通过要求一定数量的尾部零以及矿工地址作为盐,所产生的哈希值无法被任何人预测。 好吧,这至少是个方案。
首先,你要正确使用它。 也就是使用未来的区块哈希! 如果你用的是当前已有 hash,显然,大家都可以看到。 如果你使用当前区块的哈希值,它将是空的,因为当前还没有被挖出来。
如何使用未来区块哈希?
mapping (address => uint256) gameWeiValues;
mapping (address => uint256) blockHashesToBeUsed;
function playGame() public {
if (!blockHashesToBeUsed[msg.sender]) {
// first run, determine block hash to be used
blockHashesToBeUsed[msg.sender] = block.number + 2; // use 2 or more
gameWeiValues[msg.sender] = msg.value;
return;
}
uint256 randomNumber = uint256(blockhash(blockHashesToBeUsed[msg.sender]));
blockHashesToBeUsed[msg.sender] = 0;
gameWeiValues[msg.sender] = 0;
if (randomNumber != 0 || randomNumber % 2 == 0) {
uint256 winningAmount = gameWeiValues[msg.sender] * 2;
msg.sender.transfer(winningAmount);
}
}
randomNumber != 0
的检查是必不可少的,因为Solidity只能回溯256个块。 因此,如果玩家等待的时间超过256个区块,会强制为0。例如,这已经被用于黑客SmartBillions。
所以,使用未来的区块哈希,就很好吗?
这要看情况! 你是否允许中奖金额高于区块奖励的赌注? 那么就要注意矿工的操作。 如果我们假设区块奖励为3个ETH,任何超过6个ETH的赌注实际上都会给矿工提供作弊的动机。 虽然矿工不能自由选择区块的哈希值,但他可以选择不发布新发现的区块哈希值来影响随机数。
自1981年以来,承诺模式的第一个版本已经存在。 看一下Michael Blum的电话里翻硬币。 这是一个有趣的阅读。 我们可以简单地在Solidity中使用哈希来实现,这是怎样的呢?
我们用开头所说的天真想法:
每个节点在本地计算一个随机数。 它进一步广播这个随机数。 由于每个节点都会做同样的事情,所以可以使用一个函数计算最终的随机数,该函数将之前本地产生的数字作为输入,并产生一个单一的输出,例如,v₁⊕v₂ - - - ⊕vₙ。
现在,在承诺模式下,一个节点将不广播随机数,而是先计算该数的哈希值。 这个哈希将是随机数值的承诺。 然后它就会广播承诺哈希。 这有什么用?
承诺,顾名思义,一个节点之后再提交原始随机数值(称为揭示),因为不可能找到碰撞(另一个产生相同哈希的数字)。 因此,在揭示阶段,一个节点不能再改变其秘密原始随机数值。 当然,每个节点只有在收到所有其他节点的承诺后,才开始揭示阶段。 程序是这样的:
P1
...Pn
,每人产生一个秘密随机值Vi
。Pi
计算其秘密随机值的承诺哈希值:Ci = H(Vi)
。Pi
先发送 Ci
(而不是 Vi
)。Ci
后,每个 Pi
发送 Vi
。 所有参与者可以通过检查Ci == H(Vi)
来验证接收的秘密随机值。Vi
都被揭示和验证后,随机数生成的结果将是R = V1 ⊕ V2 ⊕ ... ⊕ Vn。 (XOR)
Vi
,他就自动输了。听起来好得不像真的? 你是对的。这只适用于两个节点,例如,在一个有银行和单个玩家的赌场中。 我已经在Solidity和AWS Lambda中实现了一个概念验证的原型:https://github.com/gorgos/Highstakes-SmartRoulette。
让我们看看为什么这只对两个节点有效。
我们面临最后一个节点Pi
揭示随机数值的问题,因为它可以比其他人更早地用它秘密值计算最后的R
,这就是最后揭示者问题。 它揭示的Vi
可能无法再影响R
, 然而,它可能选择不揭示该值,使所有其他各方除了中止随机数生成外没有其他选择。 如在两个用户的情况下,不揭示的节点可能会输掉这场赌局。不过,在东方参与情况下是不够的。 由于多个用户参与,只有一个不暴露方会有损失,所以攻击者可能会做以下事情:
多方环境的修改相当简单,但也有一些重大的缺点。
修改:除承诺外,每个参与者还附上抵押品。 揭晓阶段结束后,将向每一个揭晓实体退还抵押款。 如果参与者不披露自己的秘密值,他们不仅输掉了赌局,还输掉了自己的抵押品。 在这种情况下,所有不披露实体的质押物被所有披露的实体瓜分,或者选择销毁。
影响:不幸的是,所需的抵押规模可能高得离谱。 给定1万名参与者的抽奖,每人票费4美元,参与者在退还参与者的抵押金总额近4亿美元(大家自由计算)。
另外,也可以将质押物烧掉(将质押物退给银行、随机数服务机构、慈善机构或其他第三方,会有接收方作弊的风险)。 对于我们的彩票例子来说,燃烧抵押品将必要的抵押规模降低到39992美元,这对于大多数实际使用案例来说还是太高了。
有一个类似的实现,但迄今尚未在实践中使用。 在ETH2.0中,Randao 也将作为基础随机信标,上面有VDF(可验证的延迟函数)。 我们可以在后面的文章中详细讨论ETH2.0中的用法。
我们已经研究了Solidity中多方随机数的两种方法。 虽然blockhash如果使用得当,在很多场景下都能很好地发挥作用,但当涉及到很大利益的时候,它的表现就不尽如人意了,这会让矿工作弊。 其次,承诺模式对于双人方案非常有用。 不幸的是,对于大多数现实世界使用案例的多人情况来说,承诺模式是不够的。 那我们能做什么? 一种选择可能是使用预言机,我们可以在后面相关博文中讨论。
本翻译由 Cell Network 赞助支持。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!