你好,我是不羁,一名程序员,带你玩转EOS智能合约开发。如果你对EOS智能合约感兴趣,欢迎关注我的专栏。
简介:昨天我在为什么EOSBet不敢开源?一文中,和大家探讨了EOS上随机数的生成问题。随机数的生成问题的确是很多区块链项目的硬伤,也是不少项目不愿意开源的原因之一。今天我们继续探讨怎样生成随机数,既能保证公平,又能让黑客无法根据随机过程提前预测随机结果,从而避免项目方利益受损;同时本文会引出一个目前业界用的比较成熟的方案(其实仍稍欠完美,本文也会解析)。
我们昨天留了这么个问题:
学过C++的都知道,向系统请求开辟一块内存时,该内存的地址是不确定的,那你觉得可以利用这个特点,在智能合约中生成随机数吗?
不知道你有没有写个试验代码试试呢?不羁做过实验,实验过程就不在这里演示了。
这个问题的答案是,虽然内存的地址生成我们是不确定的,但对于虚拟机来说是确定的,在它每次实行时,相应执行路径上的变量的地址都是一样的。这是可以想见的,因为每次执行action,虚拟机都是初始化一个新的运行沙盒环境,相同的action,执行过程也必然相同,中间没有任何不确定的因素扰动执行过程,从而执行过程也都是可以确定的。这样黑客把你的代码完全拿过来,然后打印出相关变量的地址,就可以确定了。而因为每次执行都一样,所以黑客在本地打印的结果也就是你在主网上运行时相关变量的地址。
可见,利用随机开辟的内存地址作为随机种子
也是不可取的。
随机种子
我刚才用到了随机种子
这个词,什么是随机种子
呢?因为随机过程是计算机程序,程序分两种,一种是无状态的,一种是有状态的。
无状态的程序,每次对于相同的输入总是给出相同的输出,也就是说,只要你给我一个输入,我都可以私下把程序运行一下,然后给你一个结果,你自己再执行一次,也必要是这个结果。
有状态的程序,虽然每次的执行结果可能不同,但只要你告诉我初始状态,我也可以根据你每次给的输入,预测出它的输出。
严格来数,所有的随机过程都是有状态的计算机程序。而随机种子呢,就相当于它的初始状态,或者可对它的当前状态产生一个不可预测的扰动,变成另外一个状态。
所以假如你想让你的随机数是不可预测的,你必须保证随机种子是不可预测的。
这样我们就可以把产生随机数的机理分成两部分了,一部分是随机种子,一部分是随机过程。只要随机种子不公开,把随机过程公开,那么黑客也就无法对产生的随机数进行预测了。随机过程可以写在智能合约里,随机种子就需要外界的输入了,并且是不可预测的外界输入。只要随机种子是不可预测的,那么随机结果也就是不可预测的了。
我前面讨论过,用时间和wasm虚拟机分配的内存地址都是可以预测的,尽管预测的难度有点技术大,但还是能预测的,所以都不是良好的随机种子。
什么是良好的随机种子呢?因为虚拟机执行过程的确定性,所以这个随机种子我们不能从虚拟机中来找了,只能从外界来找了。
利用区块中的数据作为随机种子
昨天有一位同学在留言中提到了这个想法,他是这么说的:
Screen Shot 2018-09-22 at 09.21.43.png
他提到使用当前区块的hash值
作为随机种子,这是很好的想法。
目前EOS的实际使用的tps平均在16左右,一般情况下,每个区块都不止一个交易,也就是说,黑客发起的攻击性交易总会伴随着其他的交易,而其他的交易数据是黑客无法预测的,所以用当前区块的hash值
作为随机种子是比较优良的随机种子
。
然而,据我目前所知,当前EOS智能合约中还没有获得当前区块的hash值的接口,如果你发现了,欢迎告诉我。倒是有获取当前action所在的transaction的数据,对于dice
合约而言,这个东西黑客就是可以预测的了,因为交易是由黑客主动发起的,它完全知道自己的数据。
EOS智能合约中,还有获得当前区块的blocknumber
以及当前交易的ref block
的接口,不过这些也不能作为随机种子,前者是可以预测的,后者是黑客可以自己指定的。
一个可能的方案
经过我们的分析发现,目前在EOS上,好的随机种子无法在智能合约内部获得,那么只能求助外界了。我们看看外界目前有什么公共的或许可用的不可预测的噪音数据,我列出了如下几条:
- RAM的交易数据
- 某些活跃度比较高的dapps公开的数据
这些数据的确是不可预测的,任何一秒钟都可能发生变化,并且变化的方式是极难预测的,他们可以作为随机种子的一部分,但如果单纯依靠它们中的任何一个,都是不够可靠的。以RAM的交易数据为例,交易往往几秒钟才有一次,变化频次不够,黑客完全可以在这个变化的空档期下手。最活跃的账号,和活跃的dapps公开的数据也是如此,不过如果把它们结合起来一起用作随机种子,可靠性会高很多。
不过这种方式也有缺点:
- 太依赖于别人的公开数据了,如果别人的数据格式变了,你的智能合约可能也要跟着更新
- 如果这些公开的数据变化的频率,有时高,有时低,你必须收集足够多的频繁变化公开数据才能确保每时每刻的不可预测性
- 目前EOS上满足条件的可作为随机种子使用的公开数据仍然比较少
成熟可靠的方案
我们仍然需要沿着前面的思路,好的随机种子只能从我们自己的智能合约以外去寻找。我们发现其他的dapps的智能合约的公开的、并且变化的数据可以作为随机种子,但因为现阶段EOS上的生态还在起步阶段,可以作为随机种子的数据还是太少,可靠性不足。
那怎么办呢?让游戏的参与方提供随机种子。
以dice
游戏为例,双方同时提供随机种子,然后智能合约把这两个种子一起作运算,这个运算算法是公开的。因为双方是同时提供的,所以任何一方都无法提前预知对方的种子是什么,自然无法对结果做出预测。
“可是不行啊,同时提供种子,这在实操中很难办到啊!”
是的,的确如此。不过可以用另外一个方法来解决这个问题。
EOSIO提供了一个dice
的示例合约,它是两个玩家一起玩,不像EOSbet那种是玩家和项目方玩的。
我们先看看EOSIO提供的dice
示例的玩法:
- 用户A和B各有一个自己的种子,分别是
seedA
和seedB
,但一开始互相保密的。 - A对
seedA
进行hash运算
,生成seedA_hash
,然后把seedA_hash
发给智能合约。 - B对
seedB
进行hash运算
,生成seedB_hash
,然后把seedB_hash
发给智能合约。 - 在上面的步骤完成之后,A把
seedA
发送给智能合约 - B把
seedB
发送给智能合约 - 智能合约先验证A和B的seed,验证通过后,再对两个seed进行运算,根据运算结果判定输赢。
在1,2之后,B虽然先看到了A的seedA_hash
,但因为hash运算的性质,B无法通过seedA_hash
计算出seedA
,而智能合约的运算过程是对双方的seed
进行的,所以B此时无法通过调整自己的seed
来影响运算结果的倾向性。于是B也只能老老实实的生成一个随机的seedB
,并进行hash运算。
在5步的时候,B看到了seedA
,自己还没有发送seedB
,那这个时候他可以调整seedB
来影响结果了吗?不行,因为第6步,智能合约会对seed
进行有效性验证,具体是判断这个等式是否成立:
hash(seed) == seed_hash
如果不成立,就代表这个验证失败了。seed_hash是玩家在第2和3步已经发送给智能合约了的。同样因为hash的性质,你想生成另外一个seed同时还能满足上面的等式,是相当困难对策。
通过这种方式,dice的玩家双方就可以不用顾虑谁先出示seed_hash
了,也不用担心谁先出示seed
了,这个过程巧妙的利用了hash运算的性质:
- 很难根据hash(seed)的结果倒推出seed
- 不同的seed进行hash之后,生成的结果极大概率是不同。这个极大概率无限接近于100%
hash运算在加密领域应用非常广泛,并且有多种不同的hash算法,这里就不展开了。
改进的版本
上面的方案中,玩家双方都各生成了一个随机种子参与运算,所以任何一方都无法提前预测随机结果,对于双方都是公平的。
我们假设A、B双方中,A是用户,B是项目方。项目方提供智能合约,并开源,然后采用上面的玩法,可以吗?
完全可行,B的相关操作可以根据A的请求自动执行,但A的操作就变复杂了。
以dice
合约为例,对于A而言,最好的体验是像EOSBet那样,用户只需要进行一次操作,即可得到输赢的结果。然而在上面的方案中,A却需要两步:第一步是向合约发送seed_hash
,第二步是向合约发送seed
。
如何做到用户A只需要一步合约操作就可以得到结果呢?
方案是有的,我们再回头看看上面的过程,第2步和第3步是可以替换的,像下面这样:
- 用户A和B各有一个自己的种子,分别是
seedA
和seedB
,但一开始互相保密的。 - B对
seedB
进行hash运算
,生成seedB_hash
,然后把seedB_hash
发给智能合约。 - A对
seedA
进行hash运算
,生成seedA_hash
,然后把seedA_hash
发给智能合约。 - 在上面的步骤完成之后,A把
seedA
发送给智能合约 - B把
seedB
发送给智能合约 - 智能合约先验证A和B的seed,验证通过后,再对两个seed进行运算,根据运算结果判定输赢。
我们可以看到第3步和第4步是紧挨着的,于是我们就可以把它合成一个操作了。变成一个步骤之后,seedA_hash
就不用提供了,因为seedA_hash
本身是为了校验seedA
的,现在在这个一次操作已经给了seedA
,那么seedA_hash
就没有存在的必要的。
如此,改进后的版本就变成下面这样:
- 用户A和B各有一个自己的种子,分别是
seedA
和seedB
,但一开始互相保密的。 - B对
seedB
进行hash运算
,生成seedB_hash
,然后把seedB_hash
发给智能合约。 - A把
seedA
发送给智能合约 - B把
seedB
发送给智能合约 - 智能合约先验证B的
seedB
是否合法,验证通过后,再对两个seed进行运算,根据运算结果判定输赢。
如此就实现了用户只需要一次智能合约交互即可,但项目方B还是需要做两次智能合约交互,能不能再改进呢?
能的。
项目方B可以把seedB_hash
交给A,并由A在与智能合约的交互中带上。于是,整个过程就变成这样了:
- 用户A和B各有一个自己的种子,分别是
seedA
和seedB
,但一开始互相保密的。 - B对
seedB
进行hash运算
,生成seedB_hash
,然后把seedB_hash
交给A。 - A把
seedA
以及seedB_hash
发送给智能合约 - B把
seedB
发送给智能合约 - 智能合约先验证B的
seedB
是否合法,验证通过后,再对两个seed进行运算,根据运算结果判定输赢。
这样A和B与智能合约就分别只有一次交互了(省了点链上的资源消耗),而是在A和B之间增加了一次交互(链下的),很容易看出这次交互是安全的。
对于dice
合约来说,每次的游戏都是玩家发起的,也就是A发起的,而项目方B则是被动的,所以当A在把seedA
发送给智能合约之前,本身就需要通知B去生成seedB
和seedB_hash
,于是B就可以在收到A的通知的时候,顺便把seedB_hash
返回给A。
EOSBet或许也是这么做的,但因为它没开源,我们不敢定论。不过倒是有一个开源的仿EOSBet的dapps: fairdicegame,它的合约源码在这里。这个项目使用的就是改进后的方案,不过因为它涉及到需要让玩家A知道B是预先生成的seedB
,所以它会把seedB_hash
预先发送给A,由A在与智能合约的交互中,把seedB_hash
也带上。
首先声明一下,我和该项目没有任何关系,却无意间替他们做了宣传,我把他们的合约代码地址放在这里,主要是因为比较欣赏他们这种把合约开源的态度,同时也是为了方便智能合约的开发者们学习。
还能再改进吗?
上面方案从用户使用的角度已经能够满足需要了,同时也保证了公平公正,但在自证清白方面不够彻底。因为在玩家操作期间,客户端除了和智能合约有交互外,客户端还需要服务端进行一次交互,主要是为了在为玩家生成seed
以前,获取服务端的seed_hash
,然后把服务
智能合约代码已经公开了,可以自证清白,但服务端就很难了。
以fairdicegame
为例,首先它的服务端没有开源,其次,即便它开源了也无法自证清白,因为没办法验证它开源的版本与实际使用的版本是同一个版本。智能合约因为可以进行源码验证,所以没有这个问题。
我看了一下fairdicegame
与服务端交互的数据,从几次的试验来看,客户端是在收到服务端的seedB_hash
之后,再把自己的seedA
和seedB_hash
发送给服务端的,所以服务端在生成seedB
的时候,是不知道seedA
的,从而可以认为fairdicegame
对于玩家来说还是比较公平的。不过因为客户端的代码和服务端代码都完全是由项目方控制的,假如项目方不定时的给了用户另一个不公正的版本,用户也很难感知到。
你可能会说:“你是不是太严格了点儿?”
是的,的确是严格了点,我只有不停的严格要求自己,才能做出让用户更加可信的dapps。
另一方面,这种方式产生随机数的方式,严格依赖于双方共同参与随机数的生成过程,尽管某些程序可以对交互逻辑进行优化,但还是比较复杂,这种复杂性也增加了随机数生成场景的通用性,比如区块链游戏中生成宝物的随机算法,完全是官方随机生成的,整个过程无需用户参与,上面适用于dice
的随机数生成机制在这里就不合适了。
那如何改进呢?
我们节后再探讨。
国庆节快乐!
网友评论