以太坊开发实战学习-高级Solidity理论 (五)

接上篇 文章,这里继续学习Solidity高级理论。

1、深刻函数修饰符

接下来,咱们将添加一些辅助方法。咱们为您建立了一个名为 zombiehelper.sol 的新文件,而且将 zombiefeeding.sol 导入其中,这让咱们的代码更整洁。前端

咱们打算让僵尸在达到必定水平后,得到特殊能力。可是达到这个小目标,咱们还须要学一学什么是“函数修饰符”。web

带参的函数修饰符

以前咱们已经读过一个简单的函数修饰符了: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 把参数传递给它的修饰符的。segmentfault

来,咱们本身生产一个修饰符,经过传入的level参数来限制僵尸使用某些特殊功能。数组

实战演练

  • 一、在ZombieHelper 中,建立一个名为 aboveLevel 的modifier,它接收2个参数, _level (uint类型) 以及 _zombieId (uint类型)。
  • 二、运用函数逻辑确保僵尸 zombies[_zombieId].level 大于或等于 _level。
  • 三、记住,修饰符的最后一行为 _;表示修饰符调用结束后返回,并执行调用函数余下的部分
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) {
  // 其他的程序逻辑
}

实战演练

  • 一、建立一个名为 changeName 的函数。它接收2个参数:_zombieId(uint类型)以及 _newName(string类型),可见性为 external。它带有一个 aboveLevel 修饰符,调用的时候经过 _level 参数传入2, 固然,别忘了同时传 _zombieId 参数。
  • 二、在这个函数中,首先咱们用 require 语句,验证 msg.sender 是否就是 zombieToOwner [_zombieId]
  • 三、而后函数将 zombies[_zombieId] .name 设置为 _newName
  • 四、在 changeName 下建立另外一个名为 changeDna 的函数。它的定义和内容几乎和 changeName 相同,不过它第二个参数是 _newDna(uint类型),在修饰符 aboveLevel 的 _level 参数中传递 20 。如今,他能够把僵尸的 dna 设置为 _newDna 了。

zombiehelper.soloracle

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;

  }

}

2、利用view节省Gas

如今须要添加的一个功能是:咱们的 DApp 须要一个方法来查看某玩家的整个僵尸军团 - 咱们称之为 getZombiesByOwnerapp

实现这个功能只需从区块链中读取数据,因此它能够是一个 view 函数。这让咱们不得不回顾一下“gas优化”这个重要话题。

“view” 函数不花 “gas”

当玩家从外部调用一个view函数,是不须要支付一分 gas 的。

这是由于 view 函数不会真正改变区块链上的任何数据 - 它们只是读取。所以用 view 标记一个函数,意味着告诉 web3.js运行这个函数只须要查询你的本地以太坊节点,而不须要在区块链上建立一个事务(事务须要运行在每一个节点上,所以花费 gas)

稍后咱们将介绍如何在本身的节点上设置 web3.js。但如今,你关键是要记住,在所能只读的函数上标记上表示“只读”的external view 声明,就能为你的玩家减小在 DApp 中 gas 用量。

注意:若是一个 view 函数在另外一个函数的内部被调用,而调用函数与 view 函数的不属于同一个合约,也会产生调用成本。这是由于若是主调函数在以太坊建立了一个事务,它仍然须要逐个节点去验证。因此标记为 view 的函数只有在外部调用时才是免费的。

实战演练

咱们来写一个”返回某玩家的整个僵尸军团“的函数。当咱们从 web3.js 中调用它,便可显示某一玩家的我的资料页。

这个函数的逻辑有点复杂,咱们须要好几个章节来描述它的实现。

  • 一、建立一个名为 getZombiesByOwner 的新函数。它有一个名为 _owneraddress 类型的参数。
  • 二、将其申明为 external view 函数,这样当玩家从 web3.js 中调用它时,不须要花费任何 gas。
  • 三、函数须要返回一个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 []) {

  }

}

3、存储很是昂贵

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 []数组的形式返回某一用户所拥有的全部僵尸。

  • 一、声明一个名为resultuint [] memory (内存变量数组)
  • 二、将其设置为一个新的 uint 类型数组。数组的长度为该 _owner 所拥有的僵尸数量,这可经过调用 ownerZombieCount [_ owner] 来获取。
  • 三、函数结束,返回 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;
  }

}

4、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;
  }

}

5、可支付

截至目前,咱们只接触到不多的 函数修饰符。 要记住全部的东西很难,因此咱们来个概览:

  • 一、咱们有决定函数什么时候和被谁调用的可见性修饰符: private 意味着它只能被合约内部调用internal 就像 private 可是也能被继承的合约调用external 只能从合约外部调用;最后 public 能够在任何地方调用,无论是内部仍是外部
  • 二、咱们也有状态修饰符, 告诉咱们函数如何和区块链交互: view 告诉咱们运行这个函数不会更改和保存任何数据; pure 告诉咱们这个函数不但不会往区块链写数据,它甚至不从区块链读取数据。这两种在被从合约外部调用的时候都不花费任何gas(可是它们在被内部其余函数调用的时候将会耗费gas)。
  • 三、而后咱们有了自定义的 modifiers,例如在第三课学习的: onlyOwneraboveLevel。 对于这些修饰符咱们能够自定义其对函数的约束逻辑。

这些修饰符能够同时做用于一个函数定义上:

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将存储在你拥有的合约中 —— 一个简单明了的例子,向你展现你能够经过本身的游戏赚钱。

  • 一、定义一个 uint ,命名为 levelUpFee, 将值设定为 0.001 ether
  • 二、定义一个名为 levelUp 的函数。 它将接收一个 uint 参数 _zombieId。 函数应该修饰为 external 以及 payable
  • 三、这个函数首先应该 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;
  }

}

6、提现

在上一节,咱们学习了如何向合约发送以太,那么在发送以后会发生什么呢?

在你发送以太以后,它将被存储进以合约的以太坊帐户中, 并冻结在哪里 —— 除非你添加一个函数来从合约中把以太提现。

你能够写一个函数来从合约中提现以太,相似这样:

contract GetPaid is Ownable {
  function withdraw() external onlyOwner {
    owner.transfer(this.balance);
  }
}

注意咱们使用 Ownable 合约中的 owneronlyOwner,假定它已经被引入了。

你能够经过 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)

有不少例子来展现什么让以太坊编程如此之酷 —— 你能够拥有一个不被任何人控制的去中心化市场。

实战演练

  • 一、在咱们的合约里建立一个 withdraw 函数,它应该几乎和上面的GetPaid同样。
  • 二、以太的价格在过去几年内翻了十几倍,在咱们写这个教程的时候 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;
  }

}

7、综合应用

咱们新建一个攻击功能合约,并将代码放进新的文件中,引入上一个合约。

再来新建一个合约吧。熟能生巧。

若是你不记得怎么作了, 查看一下 zombiehelper.sol — 不过最好先试着作一下,检查一下你掌握的状况。

  • 一、在文件开头定义 Solidity 的版本 ^0.4.19.
  • 二、importzombiehelper.sol .
  • 三、声明一个新的 contract,命名为 ZombieBattle, 继承自ZombieHelper。函数体就先空着吧。

zombiebattle.sol

pragma solidity ^0.4.19;
import "./zombiehelper.sol";

contract ZombieBattle is ZombieHelper {
  
}

8、随机数

优秀的游戏都须要一些随机元素,那么咱们在 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个亿), 那就很值得去攻击了。

因此尽管这个方法在以太坊上不安全,在实际中,除非咱们的随机函数有一大笔钱在上面,你游戏的用户通常是没有足够的资源去攻击的。

由于在这个教程中,咱们只是在编写一个简单的游戏来作演示,也没有真正的钱在里面,因此咱们决定接受这个不足之处,使用这个简单的随机数生成函数。可是要谨记它是不安全的。

实战演练

咱们来实现一个随机数生成函数,好来计算战斗的结果。虽然这个函数一点儿也不安全。

  • 一、给咱们合约一个名为 randNonceuint,将其值设置为 0。
  • 二、创建一个函数,命名为 randMod (random-modulus)。它将做为internal 函数,传入一个名为 _modulus的 uint,并 returns 一个 uint
  • 三、这个函数首先将为 randNonce加一, (使用 randNonce++ 语句)。
  • 四、最后,它应该 (在一行代码中) 计算 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;

  }
}

9、游戏对战

咱们的合约已经有了一些随机性的来源,能够用进咱们的僵尸战斗中去计算结果。

咱们的僵尸战斗看起来将是这个流程:

  • 你选择一个本身的僵尸,而后选择一个对手的僵尸去攻击。
  • 若是你是攻击方,你将有70%的概率获胜,防守方将有30%的概率获胜。
  • 全部的僵尸(攻守双方)都将有一个 winCount 和一个 lossCount,这两个值都将根据战斗结果增加。
  • 若攻击方获胜,这个僵尸将升级并产生一个新僵尸。
  • 若是攻击方失败,除了失败次数将加一外,什么都不会发生。
  • 不管输赢,当前僵尸的冷却时间都将被激活。

这有一大堆的逻辑须要处理,咱们将把这些步骤分解到接下来的课程中去。

实战演练

  • 一、给咱们合约一个 uint 类型的变量,命名为 attackVictoryProbability, 将其值设定为 70。
  • 二、建立一个名为 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 {
    
  }
}
相关文章
相关标签/搜索