资讯专栏INFORMATION COLUMN

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

sushi / 1385人阅读

摘要:接上篇文章,这里继续学习高级理论。实战演练我们来写一个返回某玩家的整个僵尸军团的函数。但这样每做一笔交易,都会改变僵尸军团的秩序。在这里开始五可支付截至目前,我们只接触到很少的函数修饰符。

接上篇文章,这里继续学习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 的新函数。它有一个名为 _owneraddress 类型的参数。

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、声明一个名为resultuint [] 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

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,例如在第三课学习的: 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将存储在你拥有的合约中 —— 一个简单明了的例子,向你展示你可以通过自己的游戏赚钱。

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 合约中的 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)

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

实战演练

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、importzombiehelper.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、给我们合约一个名为 randNonceuint,将其值设置为 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 {
    
  }
}

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/24132.html

相关文章

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

    摘要:第一个例子,在你把智能协议传上以太坊之后,它就变得不可更改这种永固性意味着你的代码永远不能被调整或更新。允许将合约所有权转让给他人。为何要来驱动以太坊就像一个巨大缓慢但非常安全的电脑。 通过前边的 Solidity 基础语法学习,我们已经有了Solidity编程经验,在这节就要学学 Ethereum 开发的技术细节,编写真正的 DApp 时必知的:智能协议的所有权,Gas的花费,代码优...

    feng409 评论0 收藏0
  • 以太开发实战学习-高级Solidity理论 (六)

    摘要:接上篇文章,这里继续学习高级理论。将这个函数的定义修改为其使用修饰符。我们将用一个到的随机数来确定我们的战斗结果。在这个教程中,简单起见我们将这个状态保存在结构体中,将其命名为和。在第六章我们计算出来一个到的随机数。 接上篇文章,这里继续学习Solidity高级理论。 一、重构通用逻辑 不管谁调用我们的 attack 函数 —— 我们想确保用户的确拥有他们用来攻击的僵尸。如果你能用其他...

    qc1iu 评论0 收藏0
  • 区块链技术学习指引

    摘要:引言给迷失在如何学习区块链技术的同学一个指引,区块链技术是随比特币诞生,因此要搞明白区块链技术,应该先了解下比特币。但区块链技术不单应用于比特币,还有非常多的现实应用场景,想做区块链应用开发,可进一步阅读以太坊系列。 本文始发于深入浅出区块链社区, 原文:区块链技术学习指引 原文已更新,请读者前往原文阅读 本章的文章越来越多,本文是一个索引帖,方便找到自己感兴趣的文章,你也可以使用左侧...

    Cristic 评论0 收藏0
  • SegmentFault 技术周刊 Vol.41 - 深入学习区块链

    摘要:和比特币协议有所不同的是,以太坊的设计十分灵活,极具适应性。超级账本区块链的商业应用超级账本超级账本是基金会下的众多项目中的一个。证书颁发机构负责签发撤 showImg(https://segmentfault.com/img/bV2ge9?w=900&h=385); 从比特币开始 一个故事告诉你比特币的原理及运作机制 这篇文章的定位会比较科普,尽量用类比的方法将比特币的基本原理讲出来...

    qianfeng 评论0 收藏0
  • 以太开发实战学习-solidity语法(二)

    摘要:以太坊开发高级语言学习。地址以太坊区块链由账户组成,你可以把它想象成银行账户。使用很安全,因为它具有以太坊区块链的安全保障除非窃取与以太坊地址相关联的私钥,否则是没有办法修改其他人的数据的。 以太坊开发高级语言学习。 一、映射(Mapping)和地址(Address) 我们通过给数据库中的僵尸指定主人, 来支持多玩家模式。 如此一来,我们需要引入2个新的数据类型:mapping(映射)...

    wemall 评论0 收藏0

发表评论

0条评论

sushi

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<