基于以太坊却不上链的抽奖服务,是不是有一点可疑。

前言

本文是随笔,记录了一次基于以太坊的抽奖服务的方案讨论。从常规的智能合约的实现方案,到最终改用不上链的实现,主要是分享一下思路,包含了一点不看也没事的技术细节,以及一点区块链相关的内容。

因为抽奖主题的区块链开发入门文章已经泛滥了,所以文章尽可能避免变成教程。

背景

之前一位做论坛的同学说他们那个论坛经常会做一些活动,抽奖什么的。如何让用户能感受到公平公正,让大家信任。之前一个的办法是在指定时间录视频,并且边上放个北京时间,表示这个是准时开奖没有作弊。这个和澳门最大线上赌场异曲同工,边上搞台电视放新闻的直播,令人信服。

但是总一直这样也不行,我们可是搞技术的,而且这种方式扩展性太弱。于是他们想做一个公正公开可信任,简单易用易理解的抽奖系统,选定的方案是区块链。我听了觉得很有意思,而且公正公开不可变,区块链太合适不过了,说就研究研究吧。不过为啥不找区块链团队帮忙,可以部门间合作合作。他说这个需求太简单了,就不麻烦其他部门的人了。

方案

虽然现在公链有很多了,而且性能一个比一个强,但是实际上有大量应用的目前也就两三个,我个人比较喜欢古典一些的ETH。

其实需求很简单,方案也预定了一下。基本的逻辑是,用指定eth地址将帖子id作为参数来执行合约函数,得到随机数结果。指定eth地址是用来做权限控制,私钥不公开,可以防止被无关人员开奖。因为合约是公开的,执行函数的时间也是公开的,执行结果也是公开的,所以开完奖之后,大家可以直接查询验证结果。

只要在活动开始时提前公布用来开奖的地址与开奖时间与帖子id,预定时间开奖之后,大家一查就有了,简直完美。 其实当时想了有一个缺点,就是得花钱,执行合约是要付费的,大概算了下按当前eth的价格开一次奖得花一块人民币,会不会太浪费,他说完全ojbk。

合约版原型

然后顺手写了个合约原型:

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
pragma solidity >= 0.5;

contract Lottery {
struct Record {
bool success;
uint value;
}
mapping (uint => Record) records; // 记录每次的结果

function _random(uint recordId, uint max) private pure returns (uint) {
// 生成一个随机数
// TODO
return 0;
}

function generate(uint recordId, uint max) public returns (bool) {
// 生成一个新结果
if(records[recordId].success){
revert("record already exists");
return false;
}
uint ranNum = _random(recordId, max);
records[recordId] = Record(true, ranNum);
return true;
}

function query(uint recordId) public view returns (uint) {
// 查询结果
if(!records[recordId].success){
revert("record not exists");
return uint(-1);
}
return records[recordId].value;
}
}

真的是原型,就这么点代码,只有最基本的实现。我也是现学现卖的,具体的语法可以参考solidity这个语言,0.5.x之后这个语言和之前有比较大的不兼容的语法,很坑,所以市面上的很多资料都已经不行了,还是得看原文档。搞开发的就是编程语言多。

因为eth私链的部署还有合约的发布相关的知识点比较多,单独介绍的文章也比较多,我这就不详细介绍讲了。大家感兴趣的可以搜下有很多相关介绍。

可能有人不清楚eth合约的执行,我略微解释一下。上面这个合约有两个公开方法,generatequery。其中generate是开始抽奖,抽完后要把结果写到链上,需要矿工费。这个函数的执行,操作的过程就是向这个合约转账,向合约地址发送一小笔eth,并带上数据,数据里指定了要执行的函数与参数。当然如果是一般的转账也是可以带上数据的,可以写任何东西,一段话、一篇文章都可以。很久之前的不是有流行过,把XXX内容写到区块链上,其实就是这样。所以每次执行函数实际上就是一笔转账交易。而query函数是查询链上数据,没有写操作,没有矿工费,不需要转账,因此eth钱包里没钱也可以直接执行。

显然,最最关键的函数就是那个随机数生成函数。在各种高级语言里,一般都会提供random相关的函数,虽然基本是伪随机数,但是大家还是用的很欢乐。可惜在这里,并不存在这样的函数。我这个同学他问我为什么没随机数函数,java啊其他的不都有的吗,那我这就没法解释了。

关于随机数生成

在区块链里,生成随机数是一个老大难的问题。如果大家了解接触过一些区块链菠菜游戏,肯定知道去年十一月左右多个eos上的菠菜游戏因为随机数被破解,导致损失惨重。虽然基于eth合约的菠菜游戏并不多,但是随机数生成一样是个问题。一篇文章区块链随机数的原罪与救赎大概讲了这个主题,从计算机的随机数到区块链的随机数,再到线上菠菜游戏。随机数生成向来不是简单的事,我以为这是常识。每年都会有关于生成真正的随机数的文章出来,方法涉及物理天文量子力学方方面面都有。

因为eth内部没有提供随机数生成器,所以目前的随机数生成方式都需要依赖外部。

和文章中说的一样,一个目前比较流行的办法是使用oraclize。这个库就很有意思了,可以在从eth合约内向外部发送http请求,并且请求结果是可信的。那这样问题就很简单了,可以直接找一个公共的大家都认可的随机数生成服务就可以了。但是我觉得这样不优雅。

第二种是使用randao,这是一个随机数生成组织。每一个生成周期都会由组织成员上传一个随机值,经过计算得到最终结果。如果上传的成员数量不足则当前周期生成失败,下一轮重新开始。其实这样也比较靠谱。这个组织只要交了会费任何人都可以加入。如果感兴趣大家可以读下他们的白皮书,这个也是最符合区块链精神的一个解决方案。方案的具体实现我也就没有继续详细研究。

第三种大家回答的比较多的办法是使用当前最近一个区块的信息作为随机数种子,进行计算得到随机数。这个方案对于开发成本会比较低,于是我直接采用了区块的hash作为种子。上面的随机生成函数就是下面这样:

1
2
3
4
5
function _random(bytes32 bhash, uint max) private pure returns (uint) {
require(max > 0, "max should greater then 0");
uint bhash = blockhash(block.number - 1);
return uint(sha256(abi.encodePacked(bhash))) % max + 1;
}

看上去还行。为了防止同一个时间不同帖子的抽奖结果一样,可以用帖子id给随机数函数里面的hash加盐。

卧槽简直完美解决问题。有人可能会问了,凭什么区块的hash可以用来做随机数种子,这个是真的随机的吗。我再略微解释一下,区块的hash是由上一个区块的hash然后与当前区块的内容进行进一步计算得到的,这个是区块链的基本形态,之所以称为链就是因为这个。上一个区块是公开的,那么当前区块里面有什么内容呢?bitcoin的话里面比较单纯,只有区块信息和交易记录。eth的话,包含的东西就多一些了。里面的任意一项,几乎都是无法预测的,比如矿工信息,交易信息,合约信息,等等等。没法预测区块里会有包含哪些交易,包含哪些合约,也不知道是哪个矿工挖出的,eth这类的挖矿就是不停的产生随机数去匹配目标结果,所以这个方案我觉得简直完美,非常符合抽奖的这个场景。不过要注意的是开奖时间不能离当前时间过近,因为区块链的广播速度有限,会有一些延时。

关于区块hash还有一个小插曲。之前有人造谣eth的创始人v神死掉了。人红是非多。然后v神就在某网站上发了一条写某个区块hash的纸张和自己的合影,Another day, another blockchain use case,区块的高度是3930000,大家可以去参观参观。其实也不是很帅。

Another day, another blockchain use case

这就证明了至少在这个区块产生的时候,这人还活着。

链外实现

我本来以为这样就结束了,其实重点才来了。

然后又后来的某一天,建了个eth私链,把合约部署上去,测试了一番没啥问题了。然后再一天,兴冲冲得准备给他展示。结果,我私链才启动还没弄好给他展示,就被一眼看出问题:既然是用区块信息做随机数种子,那能不能直接从什么开发平台api请求这个信息呢?

卧槽,突然觉得自己之前简直是傻逼。。。。。。 然后就有了最终的实现:使用公开的区块链浏览器的api接口直接获取区块的hash。如此一来,就把区块链上的开发转化成了经典服务端应用的开发,大大降低了开发成本,而且即使完全没有任何区块链开发经验的同学也可以轻松搞定。。。。。。卧槽,这个才是完美实现。

eth的出块时间大约在10秒上下,作为服务端完全可以把所有区块的信息爬取下来。比如预定一小时后开奖,可以等一小时后,将对应的区块hash查出,使用固定并且公开的计算方式得到抽奖结果。开奖后,其他人拿到当时的区块hash,使用相同的方式可以自行复现出结果。很显然,整套流程比之前的清晰多了,然后之后具体的实现就交给他们的产品研发了。

然后我花了几天时间,使用相同的原理,也写了一个抽奖原型出来,live demo。eth区块信息使用了etherscan的接口。这个原型应该很容易理解,流程也很直观,纯前端的实现。我觉得这个前端原型UI设计一下,改改其实也是能用。

结语

我怕有人会问,你这区块链啥都没用到,请求区块的信息用的公开http api接口,数据结果也没上链,还算基于区块链的抽奖服务吗。这个服务核心数据就是从区块链来的,当然是基于区块链的,如果区块的信息是从web3js api来的,是不是就让人觉得像区块链应用一些了。

需求其实是确定的,但是很多区块链应用做着做着就把上链变成目标了,然后做着做着发现,原来的目标都已经忘到哪去都不知道了。我们公司其他部门之前不计成本的想做一个区块链应用,就是这样,做着做着都不知道做什么去了,我都给他们气死了。