为什么要将彩票去中心化?

开发区块链彩票(或去中心化彩票)只需创建几个合约即可,因此开发难度相对较低。使用Chainlink可验证随机函数(VRF)Chainlink Alarm Clock(闹钟)可以轻松运营彩票,保障安全性、永续性以及可验证的随机性。不过在我们开始谈如何开发去中心化彩票这个话题之前,先简要说明一下在区块链上开发彩票的意义。

无法被篡改的可验证随机数

在传统的彩票机制中,你必须相信运行这个机制的人是诚实守信的。然而,现实往往事与愿违。最近,某个彩票平台被人操纵,用户被骗取了1400万美元。这种事情本不应该发生,买彩票的人不应该担心运营商是否有作弊的可能。区块链可以为彩票提供一个无法篡改或攻击的平台,从而解决上述问题,彩票号码是随机生成的,而且所有人都可以证明这些号码的随机性。

这也是区块链彩票最大的优势,因为对于彩票来说最重要的是所有参与者都能相信它的公平性。

降低运营开支

中心化应用运营成本高,这点非常令人头疼。目前美国州立彩票的运营开支包含以下项目:

  • 维护彩票服务器的员工成本
  • 维护票面和包装的员工成本
  • 电视抽奖,广播和网络广告
  • 开发新彩票游戏的员工成本

虽然区块链彩票无法解决上述所有问题,但是至少可以解决第一和第四条。一旦开发出智能合约并证明其安全性,那么就不用再重复劳动了。你可以在同样的开源彩票智能合约上统一添加前端模块。另外,彩票所在的底层区块链目前已完全取代了服务器的功能。

如何在区块链上打造去中心化彩票?

为了化繁为简,本文将基于Chainlink彩票github代码库中的代码进行阐述。目前市场上已经出现了许多应用案例,比如CandyShop(ETHGlobal HackMoney优胜项目)、lotto-buffalo(科罗拉多州的GameJam优胜项目)以及Buffi’s WoF(科罗拉多州的GameJam优胜项目)。

在接下来的分步演示中,我们将接入一个预言机为彩票输入数据。也就是说彩票会围绕着这些单一的节点形成中心化架构。理论上来说应该建立节点网络,连接至几个alarm clock和Chainlink VRF,以保障彩票的去中心化水平。然而在最初的模块建立起来以后,这些问题都无足轻重了。

本教学示例中使用的代码没有经过代码审计或评估,因此仅针对本教学场景使用。彩票是受到严格监管的行业,因此在发行去中心化或中心化彩票之前请务必先查阅当地的法律法规。

初始设置

为了简化叙述,在演示中假设开发者使用的是Remix。你可以点击链接,部署演示中使用的所有代码。然而,如果要获得完整的开发套件或应用,我们还是建议大家学习Truffle/Buidler和React。用Truffle/BuildlerReact可以快速进行测试和部署迭代,并为应用添加前端模块。若想深入了解如何用Truffle开发,请查看这篇教学博客文章。你会注意到Nodejs应用中数据输入的语法与Remix稍微有些差异,但除此之外,下方所有代码都是一样的。你可以clone和fork上述链接中的所有代码库,并灵活进行定制化。

着手开发

理论上来说,做一个彩票只需要完成几个步骤:

  • 输入彩票并对输入的数据进行追踪
  • 在某个时间随机选取一个中奖者
  • 管理中奖彩票付款
  • 返回第一步

首先,我们要创建一个“Lottery.sol”合约。我们需要生成一些实例变量,追踪参与者、彩票状态以及目前为止的彩票数量(“lotterId”)。当然,智能合约要继承ChainlinkClient,这样就可以使用Chainlink Alarm。

你可以发现我们目前还没有关注到接口和治理合约这两个模块。之后我们再深入探讨。

pragma solidity ^0.6.6;
import "github.com/smartcontractkit/chainlink/evm-contracts/src/v0.6/ChainlinkClient.sol";
contract Lottery is ChainlinkClient {    
  enum LOTTERY_STATE { OPEN, CLOSED, CALCULATING_WINNER }    
  LOTTERY_STATE public lottery_state;    
  address payable[] public players;    
  uint256 public lotteryId;
}

接下来,需要创建一个构造函数,并在函数中设置一些初始变量。我们将彩票的状态设置成关闭状态(因为现在还没有启动),然后将“lotterId”设置成“1”。另外还需要使用“setPublicChainlinkToken()”,与Chainlink预言机交互。

constructor() public    
{        
 setPublicChainlinkToken();        
 lotteryId = 1;        
 lottery_state = LOTTERY_STATE.CLOSED;    
}

为彩票设置计时器

完成了以上任务后,我们现在怎么启动彩票呢?怎么进行设置,让彩票按照我们规定的时间启动和停止呢?这里就需要用到Chainlink Alarm了。我们可以创建一个函数,连接至Chainlink Alarm,一旦到了设定的时间彩票就会启动或停止。

我们需要向接入Chainlink Alarm的Chainlink节点定“CHAINLINK_ALARM_JOB_ID”和“CHAINLINK_ALARM_ORACLE”,可以在文档中找到这些信息。这个函数可以向alarm发送指令,在“now+duration”时间段后向“fulfill_alarm”函数返回数据。duration(时间段)的单位是秒,指alarm应该等待的时间段。一旦到了设定的时间,alarm就会调用“fulfill_alarm”函数。

接下来,“fulfill_alarm”函数就会选出中奖者。这可以通过添加“pickWinner”函数实现,这个之后会详细阐述。

function start_new_lottery(uint256 duration) public {
    require(lottery_state == LOTTERY_STATE.CLOSED, "can't start a new lottery yet");
    lottery_state = LOTTERY_STATE.OPEN;
    Chainlink.Request memory req = buildChainlinkRequest(CHAINLINK_ALARM_JOB_ID, address(this), this.fulfill_alarm.selector);
    req.addUint("until", now + duration);
    sendChainlinkRequestTo(CHAINLINK_ALARM_ORACLE, req, ORACLE_PAYMENT);
  }

  function fulfill_alarm(bytes32 _requestId)
    public
    recordChainlinkFulfillment(_requestId)
      {
        require(lottery_state == LOTTERY_STATE.OPEN, "The lottery hasn't even started!");
        lottery_state = LOTTERY_STATE.CALCULATING_WINNER;
        lotteryId = lotteryId + 1;
        pickWinner();
    }

输入彩票

现在我们已经学会了如何启动彩票,接下来参与者需要用以太币购买彩票。可以设置一个“MINIMUM”(最低)彩票价格,或设置成固定的以太币价格。

下面的代码可以检查彩票状态是否是开放的,以及用户输入是否达到了最低彩票价格。然后会将用户压入彩票用户的动态数组中。

function enter() public payable {
	assert(msg.value == MINIMUM);
	assert(lottery_state == LOTTERY_STATE.OPEN);
	players.push(msg.sender);
} 

这就是用户管理所涉及的全部工作。接下来就是重头戏了,我们要选出一名中奖者。

选出中奖者

在这个演示中,我们将使用Chainlink VRF来生成随机数,为智能合约提供数据。我们要为所有智能合约开发接口,以在合约间轻松实现交互,然后再创建一个治理合约,将智能合约相互连接在一起。(你还可以让合约相互治理,但如果你想要丰富彩票的功能并创建许多智能合约,最好还是统一创建一个治理合约比较方便。)

以下是“pickWinner”函数:

function pickWinner() private {
    require(lottery_state == LOTTERY_STATE.CALCULATING_WINNER, "You aren't at that stage yet!");
    RandomnessInterface(governance.randomness()).getRandom(lotteryId, lotteryId);
    //this kicks off the request and returns through fulfill_random
}

这个函数的意思是:

“给我“governance.randomness()”函数中定义的随机数智能合约地址。随机数智能合约将包含一个“getRandom”函数,将两个uint256类型的数据作为参数。”

将彩票连接至随机数

要定义一个治理智能合约,将生成随机数的智能合约连接至彩票智能合约。我们的治理合约如下:

pragma solidity ^0.6.6;

contract Governance {
    uint256 public one_time;
    address public lottery;
    address public randomness;
    constructor() public {
    }
function init(address _lottery, address _randomness) public {
        require(_randomness != address(0), "governance/no-randomnesss-address");
        require(_lottery != address(0), "no-lottery-address-given");
        randomness = _randomness;
        lottery = _lottery;
    }
}

每次部署彩票合约和随机数合约时都需要调用init函数,让他们互相知道对方的合约地址是什么。

接口的代码非常简洁:

pragma solidity 0.6.6;

interface RandomnessInterface {
    function randomNumber(uint) external view returns (uint);
    function getRandom(uint, uint) external;
}

接口就是需要与另一个智能合约分享的函数。若感兴趣深入了解接口的作用和价值,请查这篇看关于接口与抽象合约对比分析的文章,了解更多详情。你可以在Remix上查看已经完成的接口设置示例。

一旦连接至治理合约和数据接口,就可以开始获得随机数了。

获得随机数

随机数生成代码几乎与Chainlink VRF文档中的演示代码一样。我们正在Ropsten上进行测试,所以几乎与文档中的代码一模一样。

/**
 * Constructor inherits VRFConsumerBase
 * 
 * Network: Ropsten
 * Chainlink VRF Coordinator address: 0xf720CF1B963e0e7bE9F58fd471EFa67e7bF00cfb
 * LINK token address:                0x20fE562d797A42Dcb3399062AE9546cd06f63280
 * Key Hash: 0xced103054e349b8dfb51352f0f8fa9b5d20dde3d06f9f43cb2b85bc64b238205
 */
constructor(address _governance) 
    VRFConsumerBase(
        0xf720CF1B963e0e7bE9F58fd471EFa67e7bF00cfb, // VRF Coordinator
        0x20fE562d797A42Dcb3399062AE9546cd06f63280  // LINK Token
    ) public
{
    keyHash = 0xced103054e349b8dfb51352f0f8fa9b5d20dde3d06f9f43cb2b85bc64b238205;
    fee = 0.1 * 10 ** 18; // 0.1 LINK
    governance = GovernanceInterface(_governance);
}

在构造函数中添加一段连接至治理合约的代码。唯一的区别是“fulfillRandomness”函数,其中我们将生成的随机数重新输入到彩票合约。

/**
 * Callback function used by VRF Coordinator
 */
function fulfillRandomness(bytes32 requestId, uint256 randomness) external override {
    require(msg.sender == vrfCoordinator, "Fulfillment only permitted by Coordinator");
    most_recent_random = randomness;
    uint lotteryId = requestIds[requestId];
    randomNumber[lotteryId] = randomness;
    LotteryInterface(governance.lottery()).fulfill_random(randomness);
}

选出中奖者

调用彩票合约中的“fulfill_random”函数,基于随机数选出中奖者。采用简单的取模运算获得随机数。彩票合约如下:

 function fulfill_random(uint256 randomness) external {
        require(lottery_state == LOTTERY_STATE.CALCULATING_WINNER, "You aren't at that stage yet!");
        require(randomness > 0, "random-not-found");
        uint256 index = randomness % players.length;
        players[index].transfer(address(this).balance);
        players = new address payable[](0);
        lottery_state = LOTTERY_STATE.CLOSED;
    }

最后彩票状态关闭,然后彩票合约调用“start_new_lottery”函数,创建合约循环。只要彩票合约中一直有足够的LINK支付预言机gas费,彩票就可以永远运行下去。

立刻开始使用Chainlink VRF进行开发

如果本文对你有任何新的启发,并想展示你的开发成果;或者你为演示的代码库开发了前端模块;或者你使用pull/request对代码库进行了完善,请务必在Twitter、Discord或Reddit上分享你的成果,并在帖子中加上 #chainlink和 #ChainlinkVRF话题。

如果Chainlink预言机可以为你目前开发的产品提供任何附加价值;抑或你希望参与Chainlink网络的开源开发工作,请查看开发者文档或加入我们在Discord上的技术讨论群。

搜索微信号ChainlinkOfficial加入Chainlink中文微信社区,搜索微信号neils_加入Chainlink中文开发者社区。

Website | Twitter I Reddit | YouTube | Telegram | Events | GitHub | Price Feeds | DeFi