Random Number
在区块链上处理随机性非常棘手,因为区块链是确定性的,而随机性需要非确定性(否则它就会变得可预测)。本文假设用户已经对 solidity 有了一定的了解,特别是 block.number()、block.hash() 操作以及数字签名。
如果你使用 block.timestamp 或前一个 blockhash 等数据,那么有人就可以使用智能合约来预测交易是否会产生预期的结果。Capture the ether 有一系列的黑客挑战来测试你在这方面的知识。
如果你需要随机数,可以考虑以下几种设计模式。
Commit Reveal
尽管区块链交易在执行时是完全确定性的,但它们无法预测未来。具体来说,未来的 blockhash 是无法预测的(但有下文描述的一个注意事项)。
它的工作原理如下:一笔交易提交承诺,从当前区块 n 开始向后数第 20 个区块,无论 20 + n 处的 blockhash 是多少,该 blockhash 都将作为随机数。由于你无法预测区块 n + 2(其中 n 是当前区块)的 blockhash,因此区块 n + 20 被认为是随机的。
系统允许最多回溯 256 个区块来获取哈希函数,因此用户必须在区块 n + 20 到 n + 276 之间的某个时间发起第二笔交易。第二笔交易即为揭示(reveal)。当然,用户可以看到这个随机数是否对自己有利,因此应用程序的配置必须确保:如果结果对用户有利,他们会有动机去发送这第二笔交易。
例如,在一个公平的抛硬币游戏中,假设当 blockhash 为偶数时,系统会向用户支付奖励。如果用户看到 blockhash 是奇数,他们就不会进行第二笔交易,反正他们也不会赢得任何东西。为了获胜,blockhash 必须是偶数,只有在这个时候他们才会愿意去发送第二笔交易。
这种方案可能会被区块生产者篡改。虽然区块生产者不能强制生成一个精确的哈希值,但他们可以重新排序交易,直到 block hash 产生一个偶数(或对他们有利的其他结果)。
你可以通过让用户在区块 n 提交一个秘密数字的哈希值来防御这种行为。在区块 n + 20 时,用户揭示该哈希的 preimage,并将 preimage 与 blockhash 拼接在一起。然后将这两个值的拼接结果进行哈希运算,该哈希值即被用作随机数。
区块生产者无法知道哈希的 preimage,所以他们无法篡改它。但这仍然无法完全防范区块生产者。
如果区块生产者本身也参与抽奖,他们可以提交一个已知的秘密数字,然后篡改 blockhash,使得最终结果对他们有利。
既然 Ethereum 已经转向了权益证明(proof of stake),这种攻击就更难实施了,因为恶意的生产者必须恰好是揭示阶段(reveal)对应区块的区块生产者。
但如果你想安全地防范恶意区块生产者,你应该使用 Chainlink VRF(接下来会介绍)。
如果动机足够强烈,资金充裕的攻击者也可以利用这个方案。假设只要 blockhash 是偶数我们就向玩家支付奖励。攻击者可以用高 gas 交易对网络发起洪泛攻击,以阻止在区块 20 到 276 之间发生揭示交易。请记住,如果回溯超过 256 个区块,blockhash 将产生零。这对攻击者来说成本极高,但它仍然是一个潜在的攻击媒介。
Chainlink VRF
关于如何使用 Chainlink VRF(Verifiable Random Function)生成随机数,网上已经有很多文章了。他们的文档非常详细且易于理解。简而言之,它的工作原理如下。
需要随机数的智能合约会调用 chainlink 智能合约来请求随机数(并支付一些 LINK 以涵盖成本)。
Chainlink 将接受该请求,等待指定数量的区块,然后回调请求随机数的合约。Chainlink 生成随机数的算法是透明的,因此任何人都可以验证它是以公平的方式创建的。
Chainlink 不能在一笔交易中完成这个过程,否则恶意玩家如果在得到他们不想要的结果时,就可以 revert 交易。
由于可能发生链重组(chain reorganization),对于高价值的应用场景,应用程序应指定回调在更远的未来发生。
Chainlink 会发起第二笔交易,这为用户省去了授权第二笔交易的麻烦。然而,这种便利是有真实代价的,因为不仅用户必须支付 gas 成本,他们(或应用程序)还必须支付 LINK 代币才能使用该服务。
Offchain signature
我必须要归功于我与 gaspack.xyz 的一次对话,是他们提出了这个想法的核心。
上述解决方案在 UX(用户体验)上的一个明显问题是,它们都需要某种程度的延迟,并且可能需要两笔交易。在区块链游戏中,玩家可能不会喜欢这种延迟。
如何在不创建存在漏洞的智能合约的情况下做到这一点呢?
一种获取随机数的半去中心化方法是,让一个链下随机数生成器创建一个随机数,并对该随机数、发送者和未来的区块号进行密码学签名。
那个随机数将与未来的 blockhash 拼接在一起,然后对生成的字符串进行哈希运算。这就是产生的随机数。负责分发奖励的智能合约会根据发送者和区块号来验证该签名。
即使链下随机数生成器不是完美的随机,或者甚至带有一点恶意,它也无法预测未来的 blockhash,并且不知道它的随机数将与什么拼接在一起。由于签名仅在特定的区块有效,玩家无法等到对自己有利的区块出现才去掷骰子。
这种方案在三个方面可能存在弱点:
- 矿工同时控制着随机数生成器,并且在指定时间也是区块生产者。只要区块生产者和随机数生成器不串谋,这个方案就是安全的。
- 随机数生成器一遍又一遍地产生相同的数字。在这种情况下,矿工可以很容易地预测出他们应该如何篡改哈希。
- 必须有一种方法来防止玩家获取多个随机数并不断尝试直到结果对自己有利。这必然需要某种 off-chain gating(链下门控),而这很可能会降低透明度和安全性。
通过进一步去中心化随机数生成器,可以在一定程度上缓解这些缺点。例如,你可以使用 bitcoin 网络上的 block hashes 作为随机数来源。如果存在任何暗箱操作,这会让一切变得公开透明。
Originally Published Dec 2, 2022