以太坊开发实战学习-高级Solidity理论 (五)
接上篇文章,这里继续学习Solidity高级理论。
一、深入函数修饰符
接下来,我们将添加一些辅助方法。我们为您创建了一个名为 zombiehelper.sol
的新文件,并且将 zombiefeeding.sol
导入其中,这让我们的代码更整洁。
我们打算让僵尸在达到一定水平后,获得特殊能力。但是达到这个小目标,我们还需要学一学什么是“函数修饰符”。
带参的函数修饰符
之前我们已经读过一个简单的函数修饰符了:onlyOwner
。函数修饰符也可以带参数。例如:
// 存储用户年龄的映射
mapping (uint => uint) public age;
// 限定用户年龄的修饰符
modifier olderThan(uint _age, uint _userId) {
require(age[_userId] >= _age);
_;
}
// 必须年满16周岁才允许开车 (至少在美国是这样的).
// 我们可以用如下参数调用`olderThan` 修饰符:
function driveCar(uint _userId) public olderThan(16, _userId) {
// 其余的程序逻辑
}
看到了吧, olderThan
修饰符可以像函数一样接收参数,是“宿主”函数 driveCar
把参数传递给它的修饰符的。
来,我们自己生产一个修饰符,通过传入的level参数来限制僵尸使用某些特殊功能。
实战演练
- 1、在ZombieHelper 中,创建一个名为 aboveLevel 的modifier,它接收2个参数, _level (uint类型) 以及 _zombieId (uint类型)。
- 2、运用函数逻辑确保僵尸 zombies[_zombieId].level 大于或等于 _level。
- 3、记住,修饰符的最后一行为
_;
,表示修饰符调用结束后返回,并执行调用函数余下的部分。
pragma solidity ^0.4.19;
import "./zombiefeeding.sol";
contract ZombieHelper is ZombieFeeding {
// 在这里开始
modifier aboveLevel(uint _level, uint _zombieId) {
require(zombies[_zombieId].level >= _level);
_;
}
}
函数修饰符应用
现在让我们设计一些使用 aboveLevel
修饰符的函数。
作为游戏,您得有一些措施激励玩家们去升级他们的僵尸:
- 2级以上的僵尸,玩家可给他们改名。
- 20级以上的僵尸,玩家能给他们定制的 DNA。
是实现这些功能的时候了。以下是上一课的示例代码,供参考:
// 存储用户年龄的映射
mapping (uint => uint) public age;
// 限定用户年龄的修饰符
modifier olderThan(uint _age, uint _userId) {
require (age[_userId] >= _age);
_;
}
// 必须年满16周岁才允许开车 (至少在美国是这样的).
// 我们可以用如下参数调用`olderThan` 修饰符:
function driveCar(uint _userId) public olderThan(16, _userId) {
// 其余的程序逻辑
}
实战演练
- 1、创建一个名为
changeName
的函数。它接收2个参数:_zombieId
(uint类型)以及_newName
(string类型),可见性为external
。它带有一个aboveLevel
修饰符,调用的时候通过 _level 参数传入2, 当然,别忘了同时传_zombieId
参数。 - 2、在这个函数中,首先我们用 require 语句,验证 msg.sender 是否就是
zombieToOwner [_zombieId]
。 - 3、然后函数将
zombies[_zombieId] .name
设置为_newName
。 - 4、在 changeName 下创建另一个名为
changeDna
的函数。它的定义和内容几乎和 changeName 相同,不过它第二个参数是 _newDna(uint类型),在修饰符 aboveLevel 的 _level 参数中传递 20 。现在,他可以把僵尸的 dna 设置为 _newDna 了。
zombiehelper.sol
pragma solidity ^0.4.19;
import "./zombiefeeding.sol";
contract ZombieHelper is ZombieFeeding {
modifier aboveLevel(uint _level, uint _zombieId) {
require(zombies[_zombieId].level >= _level);
_;
}
// 在这里开始
function changeName(uint _zombieId, string _newName) external aboveLevel(2, _zombieId) {
require(msg.sender == zombieToOwner[_zombieId]);
zombies[_zombieId].name = _newName;
}
function changeDna(uint _zombieId, uint _newDna) external aboveLevel(20, _zombieId) {
require(msg.sender == zombieToOwner[_zombieId]);
zombies[_zombieId].dna = _newDna;
}
}
二、利用view节省Gas
现在需要添加的一个功能是:我们的 DApp 需要一个方法来查看某玩家的整个僵尸军团 - 我们称之为 getZombiesByOwner
。
实现这个功能只需从区块链中读取数据,所以它可以是一个 view
函数。这让我们不得不回顾一下“gas优化”这个重要话题。
“view” 函数不花 “gas”
当玩家从外部调用一个view
函数,是不需要支付一分 gas
的。
这是因为 view
函数不会真正改变区块链上的任何数据 - 它们只是读取。因此用 view
标记一个函数,意味着告诉 web3.js
,运行这个函数只需要查询你的本地以太坊节点,而不需要在区块链上创建一个事务(事务需要运行在每个节点上,因此花费 gas)。
稍后我们将介绍如何在自己的节点上设置 web3.js。但现在,你关键是要记住,在所能只读的函数上标记上表示“只读”的external view
声明,就能为你的玩家减少在 DApp 中 gas 用量。
注意:如果一个
view
函数在另一个函数的内部被调用,而调用函数与 view 函数的不属于同一个合约,也会产生调用成本。这是因为如果主调函数在以太坊创建了一个事务,它仍然需要逐个节点去验证。所以标记为 view 的函数只有在外部调用时才是免费的。
实战演练
我们来写一个”返回某玩家的整个僵尸军团“的函数。当我们从 web3.js
中调用它,即可显示某一玩家的个人资料页。
这个函数的逻辑有点复杂,我们需要好几个章节来描述它的实现。
- 1、创建一个名为
getZombiesByOwner
的新函数。它有一个名为_owner
的address
类型的参数。 - 2、将其申明为
external view
函数,这样当玩家从 web3.js 中调用它时,不需要花费任何 gas。 - 3、函数需要返回一个
uint []
(uint数组
)。
先这么声明着,我们将在下一章中填充函数体。
zombiehelper.sol
pragma solidity ^0.4.19;
import "./zombiefeeding.sol";
contract ZombieHelper is ZombieFeeding {
modifier aboveLevel(uint _level, uint _zombieId) {
require(zombies[_zombieId].level >= _level);
_;
}
function changeName(uint _zombieId, string _newName) external aboveLevel(2, _zombieId) {
require(msg.sender == zombieToOwner[_zombieId]);
zombies[_zombieId].name = _newName;
}
function changeDna(uint _zombieId, uint _newDna) external aboveLevel(20, _zombieId) {
require(msg.sender == zombieToOwner[_zombieId]);
zombies[_zombieId].dna = _newDna;
}
// 在这里创建你的函数
function getZombiesByOwner (address _owner) external view returns (uint []) {
}
}
三、存储非常昂贵
Solidity 使用 storage
(存储)是相当昂贵的,”写入“操作尤其贵。
这是因为,无论是写入还是更改一段数据, 这都将永久性地写入区块链。”永久性“啊!需要在全球数千个节点的硬盘上存入这些数据,随着区块链的增长,拷贝份数更多,存储量也就越大。这是需要成本的!
为了降低成本,不到万不得已,避免将数据写入存储。这也会导致效率低下的编程逻辑 - 比如每次调用一个函数,都需要在 memory
(内存) 中重建一个数组,而不是简单地将上次计算的数组给存储下来以便快速查找。
在大多数编程语言中,遍历大数据集合都是昂贵的。但是在 Solidity 中,使用一个标记了external view
的函数,遍历比 storage
要便宜太多,因为 view
函数不会产生任何花销。 (gas可是真金白银啊!)。
我们将在下一章讨论 for
循环,现在我们来看一下看如何如何在内存中声明数组。
在内存中声明数组
在数组后面加上 memory
关键字, 表明这个数组是仅仅在内存中创建,不需要写入外部存储,并且在函数调用结束时它就解散了。与在程序结束时把数据保存进 storage
的做法相比,内存运算可以大大节省gas开销 -- 把这数组放在view
里用,完全不用花钱。
以下是申明一个内存数组的例子:
function getArray() external pure returns(uint[]) {
// 初始化一个长度为3的内存数组
uint[] memory values = new uint[](3);
// 赋值
values.push(1);
values.push(2);
values.push(3);
// 返回数组
return values;
}
这个小例子展示了一些语法规则,下一章中,我们将通过一个实际用例,展示它和 for
循环结合的做法。
注意:内存数组 必须 用长度参数(在本例中为3)创建。目前不支持
array.push()
之类的方法调整数组大小,在未来的版本可能会支持长度修改。
实战演练
我们要要创建一个名为 getZombiesByOwner
的函数,它以uint []
数组的形式返回某一用户所拥有的所有僵尸。
- 1、声明一个名为
result
的uint [] memory
(内存变量数组) - 2、将其设置为一个新的
uint
类型数组。数组的长度为该 _owner 所拥有的僵尸数量,这可通过调用ownerZombieCount [_ owner]
来获取。 - 3、函数结束,返回
result
。目前它只是个空数列,我们到下一章去实现它。
zombiehelper.sol
pragma solidity ^0.4.19;
import "./zombiefeeding.sol";
contract ZombieHelper is ZombieFeeding {
modifier aboveLevel(uint _level, uint _zombieId) {
require(zombies[_zombieId].level >= _level);
_;
}
function changeName(uint _zombieId, string _newName) external aboveLevel(2, _zombieId) {
require(msg.sender == zombieToOwner[_zombieId]);
zombies[_zombieId].name = _newName;
}
function changeDna(uint _zombieId, uint _newDna) external aboveLevel(20, _zombieId) {
require(msg.sender == zombieToOwner[_zombieId]);
zombies[_zombieId].dna = _newDna;
}
function getZombiesByOwner(address _owner) external view returns(uint[]) {
// 在这里开始
uint[] memory result = new uint[](ownerZombieCount[_ owner]);
return result;
}
}
四、For循环
在之前的博文中,我们提到过,函数中使用的数组是运行时在内存中通过 for
循环实时构建,而不是预先建立在存储中的。
为什么要这样做呢?
为了实现 getZombiesByOwner
函数,一种“无脑式”的解决方案是在 ZombieFactory
中存入”主人“和”僵尸军团“的映射。
mapping (address => uint[]) public ownerToZombies
然后我们每次创建新僵尸时,执行 ownerToZombies[owner].push(zombieId)
将其添加到主人的僵尸数组中。而 getZombiesByOwner
函数也非常简单:
function getZombiesByOwner(address _owner) external view returns (uint[]) {
return ownerToZombies[_owner];
}
这个做法有问题
做法倒是简单。可是如果我们需要一个函数来把一头僵尸转移到另一个主人名下(我们一定会在后面的课程中实现的),又会发生什么?
这个“换主”函数要做到:
- 1.将僵尸push到新主人的 ownerToZombies 数组中,
- 2.从旧主的 ownerToZombies 数组中移除僵尸,
- 3.将旧主僵尸数组中“换主僵尸”之后的的每头僵尸都往前挪一位,把挪走“换主僵尸”后留下的“空槽”填上,
- 4.将数组长度减1。
但是第三步实在是太贵了!因为每挪动一头僵尸,我们都要执行一次写操作。如果一个主人有20头僵尸,而第一头被挪走了,那为了保持数组的顺序,我们得做19个写操作。
由于写入存储是 Solidity 中最费 gas 的操作之一,使得换主函数的每次调用都非常昂贵。更糟糕的是,每次调用的时候花费的 gas 都不同!具体还取决于用户在原主军团中的僵尸头数,以及移走的僵尸所在的位置。以至于用户都不知道应该支付多少 gas。
注意:当然,我们也可以把数组中最后一个僵尸往前挪来填补空槽,并将数组长度减少一。但这样每做一笔交易,都会改变僵尸军团的秩序。
由于从外部调用一个 view 函数是免费的,我们也可以在 getZombiesByOwner 函数中用一个for循环遍历整个僵尸数组,把属于某个主人的僵尸挑出来构建出僵尸数组。那么我们的 transfer 函数将会便宜得多,因为我们不需要挪动存储里的僵尸数组重新排序,总体上这个方法会更便宜,虽然有点反直觉。
使用for循环
for循环的语法在 Solidity 和 JavaScript 中类似。
来看一个创建偶数数组的例子:
function getEvens() pure external returns(uint[]) {
uint[] memory evens = new uint[](5);
// 在新数组中记录序列号
uint counter = 0;
// 在循环从1迭代到10:
for (uint i = 1; i <= 10; i++) {
// 如果 `i` 是偶数...
if (i % 2 == 0) {
// 把它加入偶数数组
evens[counter] = i;
//索引加一, 指向下一个空的‘even’
counter++;
}
}
return evens;
}
这个函数将返回一个形为 [2,4,6,8,10]
的数组。
实战演练
我们回到 getZombiesByOwner 函数, 通过一条 for 循环来遍历 DApp 中所有的僵尸, 将给定的‘用户id'与每头僵尸的‘主人’进行比较,并在函数返回之前将它们推送到我们的result 数组中。
- 1.声明一个变量 counter,属性为 uint,设其值为 0 。我们用这个变量作为 result 数组的索引。
- 2.声明一个 for 循环, 从 uint i = 0 到 i <zombies.length。它将遍历数组中的每一头僵尸。
- 3.在每一轮 for 循环中,用一个 if 语句来检查 zombieToOwner [i] 是否等于 _owner。这会比较两个地址是否匹配。
- 4.在 if 语句中:
- 通过将 result [counter] 设置为 i,将僵尸ID添加到 result 数组中。
- 将counter加1(参见上面的for循环示例)。
就是这样 - 这个函数能返回 _owner
所拥有的僵尸数组,不花一分钱 gas。
pragma solidity ^0.4.19;
import "./zombiefeeding.sol";
contract ZombieHelper is ZombieFeeding {
modifier aboveLevel(uint _level, uint _zombieId) {
require(zombies[_zombieId].level >= _level);
_;
}
function changeName(uint _zombieId, string _newName) external aboveLevel(2, _zombieId) {
require(msg.sender == zombieToOwner[_zombieId]);
zombies[_zombieId].name = _newName;
}
function changeDna(uint _zombieId, uint _newDna) external aboveLevel(20, _zombieId) {
require(msg.sender == zombieToOwner[_zombieId]);
zombies[_zombieId].dna = _newDna;
}
function getZombiesByOwner(address _owner) external view returns(uint[]) {
uint[] memory result = new uint[](ownerZombieCount[_owner]);
// 在这里开始
uint counter = 0;
for(uint i = 0; i < zombies.length; i++) {
if(zombieToOwner[i] == _owner)
{
result[counter] = i;
counter ++;
}
}
return result;
}
}
五、可支付
截至目前,我们只接触到很少的 函数修饰符
。 要记住所有的东西很难,所以我们来个概览:
- 1、我们有决定函数何时和被谁调用的可见性修饰符:
private
意味着它只能被合约内部调用;internal
就像private
但是也能被继承的合约调用;external
只能从合约外部调用;最后public
可以在任何地方调用,不管是内部还是外部。 - 2、我们也有状态修饰符, 告诉我们函数如何和区块链交互:
view
告诉我们运行这个函数不会更改和保存任何数据;pure
告诉我们这个函数不但不会往区块链写数据,它甚至不从区块链读取数据。这两种在被从合约外部调用的时候都不花费任何gas(但是它们在被内部其他函数调用的时候将会耗费gas)。 - 3、然后我们有了自定义的
modifiers
,例如在第三课学习的:onlyOwner
和aboveLevel
。 对于这些修饰符我们可以自定义其对函数的约束逻辑。
这些修饰符可以同时作用于一个函数定义上:
function test() external view onlyOwner anotherModifier { /* ... */ }
在这一章,我们来学习一个新的修饰符 payable
.
payable修饰符
payable
方法是让 Solidity 和以太坊变得如此酷的一部分 —— 它们是一种可以接收以太的特殊函数。
先放一下。当你在调用一个普通网站服务器上的API函数的时候,你无法用你的函数传送美元——你也不能传送比特币。
但是在以太坊中, 因为钱 (以太), 数据 (事务负载), 以及合约代码本身都存在于以太坊。你可以在同时调用函数 并付钱给另外一个合约。
这就允许出现很多有趣的逻辑, 比如向一个合约要求支付一定的钱来运行一个函数。
示例
contract OnlineStore {
function buySomething() external payable {
// 检查以确定0.001以太发送出去来运行函数:
require(msg.value == 0.001 ether);
// 如果为真,一些用来向函数调用者发送数字内容的逻辑
transferThing(msg.sender);
}
}
在这里,msg.value
是一种可以查看向合约发送了多少以太的方法,另外 ether
是一个內建单元。
这里发生的事是,一些人会从 web3.js
调用这个函数 (从DApp的前端), 像这样 :
// 假设 `OnlineStore` 在以太坊上指向你的合约:
OnlineStore.buySomething().send(from: web3.eth.defaultAccount, value: web3.utils.toWei(0.001))
注意这个 value
字段, JavaScript 调用来指定发送多少(0.001)以太。如果把事务想象成一个信封,你发送到函数的参数就是信的内容。 添加一个 value 很像在信封里面放钱 —— 信件内容和钱同时发送给了接收者。
注意: 如果一个函数没标记为
payable
, 而你尝试利用上面的方法发送以太,函数将拒绝你的事务。
实战演练
我们来在僵尸游戏里面创建一个payable
函数。
假定在我们的游戏中,玩家可以通过支付ETH来升级他们的僵尸。ETH将存储在你拥有的合约中 —— 一个简单明了的例子,向你展示你可以通过自己的游戏赚钱。
- 1、定义一个
uint
,命名为levelUpFee
, 将值设定为0.001 ether
。 - 2、定义一个名为
levelUp
的函数。 它将接收一个uint
参数_zombieId
。 函数应该修饰为external
以及payable
。 - 3、这个函数首先应该
require
确保msg.value
等于levelUpFee
。
然后它应该增加僵尸的 level
: zombies[_zombieId].level++
。
zombiehelper.sol
pragma solidity ^0.4.19;
import "./zombiefeeding.sol";
contract ZombieHelper is ZombieFeeding {
// 1\. 在这里定义 levelUpFee
uint levelUpFee = 0.001 ether;
modifier aboveLevel(uint _level, uint _zombieId) {
require(zombies[_zombieId].level >= _level);
_;
}
// 2\. 在这里插入 levelUp 函数
function levelUp(uint _zombieId) external payable {
// 检查以确定0.001以太发送出去来运行函数:
require(msg.value == levelUpFee);
zombies[_zombieId].level++;
}
function changeName(uint _zombieId, string _newName) external aboveLevel(2, _zombieId) {
require(msg.sender == zombieToOwner[_zombieId]);
zombies[_zombieId].name = _newName;
}
function changeDna(uint _zombieId, uint _newDna) external aboveLevel(20, _zombieId) {
require(msg.sender == zombieToOwner[_zombieId]);
zombies[_zombieId].dna = _newDna;
}
function getZombiesByOwner(address _owner) external view returns(uint[]) {
uint[] memory result = new uint[](ownerZombieCount[_owner]);
uint counter = 0;
for (uint i = 0; i < zombies.length; i++) {
if (zombieToOwner[i] == _owner) {
result[counter] = i;
counter++;
}
}
return result;
}
}
六、提现
在上一节,我们学习了如何向合约发送以太,那么在发送之后会发生什么呢?
在你发送以太之后,它将被存储进以合约的以太坊账户中, 并冻结在哪里 —— 除非你添加一个函数来从合约中把以太提现。
你可以写一个函数来从合约中提现以太,类似这样:
contract GetPaid is Ownable {
function withdraw() external onlyOwner {
owner.transfer(this.balance);
}
}
注意我们使用 Ownable
合约中的 owner
和 onlyOwner
,假定它已经被引入了。
你可以通过 transfer
函数向一个地址发送以太, 然后 this.balance
将返回当前合约存储了多少以太。 所以如果100个用户每人向我们支付1以太, this.balance
将是100以太。
你可以通过 transfer
向任何以太坊地址付钱。 比如,你可以有一个函数在 msg.sender
超额付款的时候给他们退钱:
uint itemFee = 0.001 ether;
msg.sender.transfer(msg.value - itemFee);
或者在一个有卖家和卖家的合约中, 你可以把卖家的地址存储起来, 当有人买了它的东西的时候,把买家支付的钱发送给它 seller.transfer(msg.value)
。
有很多例子来展示什么让以太坊编程如此之酷 —— 你可以拥有一个不被任何人控制的去中心化市场。
实战演练
- 1、在我们的合约里创建一个
withdraw
函数,它应该几乎和上面的GetPaid
一样。 - 2、以太的价格在过去几年内翻了十几倍,在我们写这个教程的时候 0.01 以太相当于1美元,如果它再翻十倍 0.001 以太将是10美元,那我们的游戏就太贵了。
- 所以我们应该再创建一个函数,允许我们以合约拥有者的身份来设置 levelUpFee。
a. 创建一个函数,名为 setLevelUpFee
, 其接收一个参数 uint _fee
,是 external
并使用修饰符 onlyOwner
。
b. 这个函数应该设置 levelUpFee
等于 _fee
。
zombiehelper.sol
pragma solidity ^0.4.19;
import "./zombiefeeding.sol";
contract ZombieHelper is ZombieFeeding {
uint levelUpFee = 0.001 ether;
modifier aboveLevel(uint _level, uint _zombieId) {
require(zombies[_zombieId].level >= _level);
_;
}
// 1\. 在这里创建 withdraw 函数
function withdraw() external onlyOwner {
owner.transfer(this.balance);
}
// 2\. 在这里创建 setLevelUpFee 函数
function setLevelUpFee(uint _fee) external onlyOwner {
levelUpFee = _fee;
}
function levelUp(uint _zombieId) external payable {
require(msg.value == levelUpFee);
zombies[_zombieId].level++;
}
function changeName(uint _zombieId, string _newName) external aboveLevel(2, _zombieId) {
require(msg.sender == zombieToOwner[_zombieId]);
zombies[_zombieId].name = _newName;
}
function changeDna(uint _zombieId, uint _newDna) external aboveLevel(20, _zombieId) {
require(msg.sender == zombieToOwner[_zombieId]);
zombies[_zombieId].dna = _newDna;
}
function getZombiesByOwner(address _owner) external view returns(uint[]) {
uint[] memory result = new uint[](ownerZombieCount[_owner]);
uint counter = 0;
for (uint i = 0; i < zombies.length; i++) {
if (zombieToOwner[i] == _owner) {
result[counter] = i;
counter++;
}
}
return result;
}
}
七、综合应用
我们新建一个攻击功能合约,并将代码放进新的文件中,引入上一个合约。
再来新建一个合约吧。熟能生巧。
如果你不记得怎么做了, 查看一下 zombiehelper.sol
— 不过最好先试着做一下,检查一下你掌握的情况。
- 1、在文件开头定义 Solidity 的版本
^0.4.19
. - 2、
import
自zombiehelper.sol
. - 3、声明一个新的
contract
,命名为ZombieBattle
, 继承自ZombieHelper
。函数体就先空着吧。
zombiebattle.sol
pragma solidity ^0.4.19;
import "./zombiehelper.sol";
contract ZombieBattle is ZombieHelper {
}
八、随机数
优秀的游戏都需要一些随机元素,那么我们在 Solidity 里如何生成随机数呢?
真正的答案是你不能,或者最起码,你无法安全地做到这一点。
我们来看看为什么
用 keccak256
来制造随机数
Solidity 中最好的随机数生成器是 keccak256
哈希函数.
我们可以这样来生成一些随机数
// 生成一个0到100的随机数:
uint randNonce = 0;
uint random = uint(keccak256(now, msg.sender, randNonce)) % 100;
randNonce++;
uint random2 = uint(keccak256(now, msg.sender, randNonce)) % 100;
这个方法首先拿到 now
的时间戳、 msg.sender
、 以及一个自增数 nonce
(一个仅会被使用一次的数,这样我们就不会对相同的输入值调用一次以上哈希函数了)。
然后利用 keccak
把输入的值转变为一个哈希值, 再将哈希值转换为 uint
, 然后利用 % 100
来取最后两位, 就生成了一个0到100之间随机数了。
这个方法很容易被不诚实的节点攻击
在以太坊上, 当你在和一个合约上调用函数的时候, 你会把它广播给一个节点或者在网络上的 transaction
节点们。 网络上的节点将收集很多事务, 试着成为第一个解决计算密集型数学问题的人,作为“工作证明”,然后将“工作证明”(Proof of Work, PoW)和事务一起作为一个 block
发布在网络上。
一旦一个节点解决了一个PoW, 其他节点就会停止尝试解决这个 PoW, 并验证其他节点的事务列表是有效的,然后接受这个节点转而尝试解决下一个节点。
这就让我们的随机数函数变得可利用了
我们假设我们有一个硬币翻转合约——正面你赢双倍钱,反面你输掉所有的钱。假如它使用上面的方法来决定是正面还是反面 (random >= 50
算正面, random < 50
算反面)。
如果我正运行一个节点,我可以 只对我自己的节点 发布一个事务,且不分享它。 我可以运行硬币翻转方法来偷窥我的输赢 — 如果我输了,我就不把这个事务包含进我要解决的下一个区块中去。我可以一直运行这个方法,直到我赢得了硬币翻转并解决了下一个区块,然后获利。
所以我们该如何在以太坊上安全地生成随机数呢 ?
因为区块链的全部内容对所有参与者来说是透明的, 这就让这个问题变得很难,它的解决方法不在本课程讨论范围,你可以阅读 这个 StackOverflow 上的讨论 来获得一些主意。 一个方法是利用 oracle 来访问以太坊区块链之外的随机数函数。
当然, 因为网络上成千上万的以太坊节点都在竞争解决下一个区块,我能成功解决下一个区块的几率非常之低。 这将花费我们巨大的计算资源来开发这个获利方法 — 但是如果奖励异常地高(比如我可以在硬币翻转函数中赢得 1个亿), 那就很值得去攻击了。
所以尽管这个方法在以太坊上不安全,在实际中,除非我们的随机函数有一大笔钱在上面,你游戏的用户一般是没有足够的资源去攻击的。
因为在这个教程中,我们只是在编写一个简单的游戏来做演示,也没有真正的钱在里面,所以我们决定接受这个不足之处,使用这个简单的随机数生成函数。但是要谨记它是不安全的。
实战演练
我们来实现一个随机数生成函数,好来计算战斗的结果。虽然这个函数一点儿也不安全。
- 1、给我们合约一个名为
randNonce
的uint
,将其值设置为 0。 - 2、建立一个函数,命名为
randMod
(random-modulus)。它将作为internal
函数,传入一个名为_modulus
的 uint,并returns
一个uint
。 - 3、这个函数首先将为
randNonce
加一, (使用 randNonce++ 语句)。 - 4、最后,它应该 (在一行代码中) 计算 now, msg.sender, 以及 randNonce 的 keccak256 哈希值并转换为 uint—— 最后 return % _modulus 的值。 (天! 听起来太拗口了。如果你有点理解不过来,看一下我们上面计算随机数的例子,它们的逻辑非常相似)
zombiehelper.sol
pragma solidity ^0.4.19;
import "./zombiehelper.sol";
contract ZombieBattle is ZombieHelper {
// 在这里开始
uint randNonce = 0;
function randMod(uint _modulus) internal returns (uint) {
randNonce ++;
return uint(keccak256(now, msg.sender, randNonce)) % _modulus;
}
}
九、游戏对战
我们的合约已经有了一些随机性的来源,可以用进我们的僵尸战斗中去计算结果。
我们的僵尸战斗看起来将是这个流程:
- 你选择一个自己的僵尸,然后选择一个对手的僵尸去攻击。
- 如果你是攻击方,你将有70%的几率获胜,防守方将有30%的几率获胜。
- 所有的僵尸(攻守双方)都将有一个 winCount 和一个 lossCount,这两个值都将根据战斗结果增长。
- 若攻击方获胜,这个僵尸将升级并产生一个新僵尸。
- 如果攻击方失败,除了失败次数将加一外,什么都不会发生。
- 无论输赢,当前僵尸的冷却时间都将被激活。
这有一大堆的逻辑需要处理,我们将把这些步骤分解到接下来的课程中去。
实战演练
- 1、给我们合约一个
uint
类型的变量,命名为attackVictoryProbability
, 将其值设定为 70。 - 2、创建一个名为
attack
的函数。它将传入两个参数:_zombieId
(uint 类型) 以及_targetId
(也是 uint)。它将是一个external
函数。
zombiehelper.sol
pragma solidity ^0.4.19;
import "./zombiehelper.sol";
contract ZombieBattle is ZombieHelper {
uint randNonce = 0;
// 在这里创建 attackVictoryProbability
uint attackVictoryProbability = 70;
function randMod(uint _modulus) internal returns(uint) {
randNonce++;
return uint(keccak256(now, msg.sender, randNonce)) % _modulus;
}
// 在这里创建新函数
function attack(uint _zombieId, uint _targetId) external {
}
}