智能合约开发
现在,我们开始智能合约的开发部分,Solidity 与 Javascript 很接近,但它们并不相同。而且不能在一段代码上强加 JQuery,智能合约是无法调用区块链体系之外的代码的。同时还有一个特点是,你在开发的时候需要特别注意安全性,因为在区块链上的交易是不可逆的。
基本语法
通过一个例子说明基本语法,这里参考了ethfans上的一个例子,如果难以理解的话可以换一个,使用当时 PeckShield 讲的一个分饼干的例子。
现在,关于我们的第一个例子,我正在考虑一个由电影《时间规划局》启发的脚本。电影中,人们生活在一个反乌托邦式的未来,改用时间作为货币流通。他们可以通过掰手腕的方式赢取对手的时间(他们的“手臂”上存储着时间,输方的时间将会传送给赢家),我们也可以这么做!用智能合约以角力( Wrestling )的方式赚钱。
首先,solidity 脚本的基础是下面这段代码,pragma 指明正在使用的 Solidity 版本。Wrestling 是合约的名称,是一种与 Javascrip 上的类(class)相似的结构。
pragma solidity ^0.4.18;
contract Wrestling {
// our code will go here
}
我们需要两个参与者,所以我们要添加两个保存他们账户地址的变量(他们的公钥),分别是 wrestler1
和 wrestler2
,变量声明方式如下。
address public wrestler1;
address public wrestler2;
在我们的小游戏中,每一轮的比赛,参与者都可以投入一笔钱,如果一个人投入的钱是另一个人的两倍(总计),那他就赢了。定义两个玩家是否已经投入的flag wrestler1Played
和 wrestler2Played
以及两位玩家投入的金额wrestler1Deposit
和 wrestler1Deposit
。
bool public wrestler1Played;
bool public wrestler2Played;
uint private wrestler1Deposit;
uint private wrestler2Deposit;
还有判断游戏结束与否,赢家和收益的变量。
bool public gameFinished;
address public theWinner;
uint gains;
下面介绍一些关于公钥/私钥的规则,在区块链上每一个账户都是一对公私钥,私钥可以对一个信息进行签名,从而使这条信息可以被他人验证,被验证的时候它的公钥需要被使用到。在整个签名和验证的过程中,没有信息是加密的,实际上任何信息都是公开课查验的。
对于合约里面的变量,本质上来讲,也是可以被公开访问的。在这里要注意是的,即使一个变量是私有的,并不是说其他人不能读取它的内容,而是意味着它只能在合约中被访问。但实际上,由于整个区块链存储在许多计算机上,所以存储在变量中的信息总是可以被其他人看到,这是在区块链中一个很重要额原则。
另一方面,和很多编程语言很像,编译器会自动为公共变量创建 getter 函数。为了使其他的合约和用户能够更改公共变量的值,通知也需要针对不同的变量创建一个 setter 函数。
现在我们将为游戏的每一步添加三个事件。
-
开始,参与者注册;
-
游戏期间,登记每一轮赛果;
-
最后,其中一位参与者获胜。
事件是简单的日志,可以在分布式应用程序(也称为 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()
函数。关键字 internal
与 private
是一样的,唯一的区别是 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