【CITA 智能合约开发系列】智能合约开发

智能合约开发

现在,我们开始智能合约的开发部分,Solidity 与 Javascript 很接近,但它们并不相同。而且不能在一段代码上强加 JQuery,智能合约是无法调用区块链体系之外的代码的。同时还有一个特点是,你在开发的时候需要特别注意安全性,因为在区块链上的交易是不可逆的。

基本语法

通过一个例子说明基本语法,这里参考了ethfans上的一个例子,如果难以理解的话可以换一个,使用当时 PeckShield 讲的一个分饼干的例子。

现在,关于我们的第一个例子,我正在考虑一个由电影《时间规划局》启发的脚本。电影中,人们生活在一个反乌托邦式的未来,改用时间作为货币流通。他们可以通过掰手腕的方式赢取对手的时间(他们的“手臂”上存储着时间,输方的时间将会传送给赢家),我们也可以这么做!用智能合约以角力( Wrestling )的方式赚钱。

首先,solidity 脚本的基础是下面这段代码,pragma 指明正在使用的 Solidity 版本。Wrestling 是合约的名称,是一种与 Javascrip 上的类(class)相似的结构。

pragma solidity ^0.4.18;
contract Wrestling {    
        // our code will go here
}

我们需要两个参与者,所以我们要添加两个保存他们账户地址的变量(他们的公钥),分别是 wrestler1wrestler2 ,变量声明方式如下。

address public wrestler1;
address public wrestler2;

在我们的小游戏中,每一轮的比赛,参与者都可以投入一笔钱,如果一个人投入的钱是另一个人的两倍(总计),那他就赢了。定义两个玩家是否已经投入的flag wrestler1Playedwrestler2Played 以及两位玩家投入的金额wrestler1Depositwrestler1Deposit

bool public wrestler1Played;
bool public wrestler2Played;
uint private wrestler1Deposit;
uint private wrestler2Deposit;

还有判断游戏结束与否,赢家和收益的变量。

bool public gameFinished; 
address public theWinner;
uint gains;

下面介绍一些关于公钥/私钥的规则,在区块链上每一个账户都是一对公私钥,私钥可以对一个信息进行签名,从而使这条信息可以被他人验证,被验证的时候它的公钥需要被使用到。在整个签名和验证的过程中,没有信息是加密的,实际上任何信息都是公开课查验的。

对于合约里面的变量,本质上来讲,也是可以被公开访问的。在这里要注意是的,即使一个变量是私有的,并不是说其他人不能读取它的内容,而是意味着它只能在合约中被访问。但实际上,由于整个区块链存储在许多计算机上,所以存储在变量中的信息总是可以被其他人看到,这是在区块链中一个很重要额原则。

另一方面,和很多编程语言很像,编译器会自动为公共变量创建 getter 函数。为了使其他的合约和用户能够更改公共变量的值,通知也需要针对不同的变量创建一个 setter 函数。

现在我们将为游戏的每一步添加三个事件。

  1. 开始,参与者注册;

  2. 游戏期间,登记每一轮赛果;

  3. 最后,其中一位参与者获胜。

事件是简单的日志,可以在分布式应用程序(也称为 dapps)的用户界面中调用 JavaScript 回调函数。在开发过程中,事件甚至可以用于调试的目的,因为不同于 JavaScript 有console.log() 函数,solidity 中是没有办法在 console 中打印出信息的。代码如下:

event WrestlingStartsEvent(address wrestler1, address wrestler2);
event EndOfRoundEvent(uint wrestler1Deposit, uint wrestler2Deposit);
event EndOfWrestlingEvent(address winner, uint gains);

现在我们将添加构造函数,在 Solidity 中,它与我们的合约具有相同的名称,并且在创建合约时只调用一次。在这里,第一位参与者将是创造合约的人。msg.sender 是调用该函数的人的地址。

function Wrestling() public {  wrestler1 = msg.sender;}

接下来,我们让另一个参与者使用以下函数进行注册:

function registerAsAnOpponent() public {    
    require(wrestler2 == address(0));
    wrestler2 = msg.sender;
    WrestlingStartsEvent(wrestler1, wrestler2);
}

Require 函数是 Solidity 中一个特殊的错误处理函数,如果条件不满足,它会回滚更改。在我们的示例中,如果变量参与者2等于0x0地址(地址等于0),我们可以继续;如果参与者2的地址与0x0地址不同,这就意味着某个玩家已经注册为对手,所以我们会拒绝新的注册。可以把它认为是 solidity 中的 if() {} else{} 条件判断。

再次强调, msg.sender 是调用该函数的帐户地址,并且当我们触发一个事件,就标志着角力的开始。

现在,每一个参与者都会调用一个函数, wrestle() ,并投入资金。如果双方已经玩过这场游戏,我们就能知道其中一方是否获胜(我们的规则是其中一方投入的资金必须是另一方的双倍)。关键字 payable 意味着函数可以接收资金,如果它不是集合,函数则不会接受币。 msg.value 是发送到合约中的币的数量。

    function wrestle() public payable {        
        require(!gameFinished && (msg.sender == wrestler1 || msg.sender == wrestler2));
        if(msg.sender == wrestler1) {            
                require(wrestler1Played == false);            
                wrestler1Played = true;            
                wrestler1Deposit = wrestler1Deposit + msg.value;        
        } else {            
               require(wrestler2Played == false);            
               wrestler2Played = true;            
               wrestler2Deposit = wrestler2Deposit + msg.value;        
        }        
        if(wrestler1Played && wrestler2Played) {            
               if(wrestler1Deposit >= wrestler2Deposit * 2) {                
                         endOfGame(wrestler1);            
                } else if (wrestler2Deposit >= wrestler1Deposit * 2) { 
                        endOfGame(wrestler2);            
               } else {                
                     endOfRound();            
               }       
        }    
}

然后,我们添加 endOfGame()endOfRound() 函数。关键字 internalprivate 是一样的,唯一的区别是 internal 函数可以由其他合约继承(因为Solidity 与其他面向对象语言相似,而 private 函数不能继承。

    function endOfRound() internal {        
        wrestler1Played = false;        
        wrestler2Played = false;
        EndOfRoundEvent(wrestler1Deposit, wrestler2Deposit);    
}
    function endOfGame(address winner) internal {        
        gameFinished = true;       
        theWinner = winner;  
        gains = wrestler1Deposit + wrestler2Deposit;        
        EndOfWrestlingEvent(winner, gains);   
 }

请注意,我们不是直接把钱交给赢家,在此情况下这并不重要,因为赢家会把该合约所有的钱提取出来;而在其他情况下,当多个用户要把合约中的以太币提取出来,使用 withdraw 模式会更安全,可以避免重入,在合约安全部分我们会详细讨论这些情况。

简单地说,如果多个用户都可以从合约中提取资金,那么任谁都能一次性多次调用 withdraw 函数并多次得到报酬。所以我们需要以这样一种方式来编写我们的取款功能:在他继续得到报酬之前,他应得的数额会作废。

它看起来像这样:

    function withdraw() public {        
        require(gameFinished && theWinner == msg.sender);
        uint amount = gains;
        gains = 0;       
        msg.sender.transfer(amount);   
 }

https://github.com/devzl/ethereum-walkthrough-1/blob/master/Wrestling.sol 有代码段。

智能合约的 IDE

在区块链技术中,不仅转账是一笔交易,对合约中函数的调用和合约的部署都是以发送交易的方式完成。整个过程比较繁琐,正如同其他的编程语言一样,针对于 solidity 智能合约,我们也提供了 IDE (CITA IDE) 来编译和部署合约。

CITA IDE

CITA IDE 是基于 Ethereum 的 Solidity 编辑器进行修改并适配了 CITA ,是面向 CITA 的智能合约编辑器,能够编写、编译、debug、部署智能合约。可直接运行官方 CITA IDE 进行体验。

使用说明

  • browser 内置常用的模板合约,首先从内置合约模板中选择合适的模板开始开发
  • Compile 本地编译,选择当前 solidity 版本,与合约 pragma 一致
  • 进入右侧的 Run 标签, 在 Deploy to CITA 中填入相关信息
    • 勾选 Auto ValidUntilBlock 则发送交易前会自动更新 validUntilBlock 字段
    • 勾选 store ABI on chain 则会在合约部署成功后将合约 ABI 存储到 CITA 上
    • 此处特别注意 Quota 的设置, 一般合约需要较多 Quota, 若 quota 不足, 在交易信息打印的时候可以查看 Error Message 获知
  • 点击 Load Contracts 加载当前编译完成的合约, 并选择要部署的合约
  • 点击 Deploy to CITA 发起部署合约的交易
  • 观察控制台的输出, 交易详细信息会显示在控制台上, 当流程结束时, 会输出交易 hash 和合约地址, 并且以链接形式支持到 Microscope 查看

DApp及智能合约开发实例

First Forever 是一个DApp demo,展示了在 CITA 上开发一个最小可用的 DApp 的完整流程。

以下是区块链DApp的开发步骤示意图:

在该项目中使用了一个简单的可以存储用户提交内容的智能合约,源码:SimpleStore

更详细的介绍看:如何动手做一个DApp

系列文章:

  1. 智能合约定义
    智能合约的历史和定义, 介绍在cita上使用智能合约
  2. 智能合约开发(本文)
    智能合约的基础语法介绍,cita ide编辑器介绍,如何cita开发Dapp
  3. 智能合约安全性
    智能合约一些高级技巧
  4. 智能合约场景
    两种智能合约代币ERC20,ERC721的标准接口讲解
4赞