学习区块链(九)--创建僵尸军团进阶Ⅳ

一.gas
我们知道,运行在以太坊上的智能合约每一步操作都需要消耗Gas,这些gas是真金白银的,一个 DApp 收取多少 gas 取决于功能逻辑的复杂程度。每个操作背后,都在计算完成这个操作所需要的计算资源,(比如,存储数据就比做个加法运算贵得多), 一次操作所需要花费的gas等于这个操作背后的所有运算花销的总和。所以我觉得学习编写智能合约是真的很有必要的,他强迫每一个程序员去绞尽脑汁的调优代码,如果同样的功能,使用笨拙的代码开发的程序,比起经过精巧优化的代码来,运行花费更高,这显然会给成千上万的用户带来大量不必要的开销,我想白花花的银子因为我们代码结构而损失掉,是大家都不愿意看到的吧!
那么问题来了,以太坊为什么需要gas来运行这些智能合约呢?
以太坊就像一个巨大、缓慢、但非常安全的电脑。当你运行一个程序的时候,网络上的每一个节点都在进行相同的运算,以验证它的输出 —— 这就是所谓的”去中心化“ 由于数以千计的节点同时在验证着每个功能的运行,这可以确保它的数据不会被被监控,或者被刻意修改。

如果我们知道ddos攻击的话就很好理解了,可能会有恶意用户用无限循环堵塞或者攻击网络,抑或用密集运算来占用大量的网络资源,为了防止这种事情的发生,以太坊的创建者为以太坊上的资源制定了价格,想要在以太坊上运算或者存储,你需要先付费,那么当然没人会用真金白银来做这种无意义的事了!

之前我们提到除了基本版的 uint 外,还有其他变种 uint:uint8,uint16,uint32等。

通常情况下我们不会考虑使用 unit 变种,因为无论如何定义 uint的大小,Solidity 为它保留256位的存储空间。例如,使用 uint8 而不是uint(uint256)不会为你节省任何gas。

除非,把 unit 绑定到 struct 里面。

如果一个 struct 中有多个 uint,则尽可能使用较小的 uint, Solidity 会将这些 uint 打包在一起,从而占用较少的存储空间。例如:

struct NormalStruct {
  uint a;
  uint b;
  uint c;
}

struct MiniMe {
  uint32 a;
  uint32 b;
  uint c;
}

// 因为使用了结构打包,`mini` 比 `normal` 占用的空间更少
NormalStruct normal = NormalStruct(10, 20, 30);
MiniMe mini = MiniMe(10, 20, 30);

我们之前在ZombieFactory有个存储僵尸属性的struct,里面有时间和dna,现在我们加入uint32的僵尸等级level和冷却时间readyTime,这样我们构造新的僵尸就能更节省gas了。

struct Zombie{
  string name;
  uint dna;
  uint32 level;
  uint32 readyTime;
}

二.时间单位

上面struct定义的level 属性表示僵尸的级别。以后,在我们创建的战斗系统中,打胜仗的僵尸会逐渐升级并获得更多的能力。

readyTime 稍微复杂点。我们希望增加一个“冷却周期”,表示僵尸在两次猎食或攻击之之间必须等待的时间。如果没有它,僵尸每天可能会攻击和繁殖1,000次,这样游戏就太简单了。

为了记录僵尸在下一次进击前需要等待的时间,我们使用了 Solidity 的时间单位。

Solidity 还包含秒(seconds),分钟(minutes),小时(hours),天(days),周(weeks) 和 年(years) 等时间单位。它们都会转换成对应的秒数放入 uint 中。所以 1分钟 就是 60,1小时是 3600(60秒×60分钟),1天是86400(24小时×60分钟×60秒),以此类推。
现在我们给我们dapp设定一个冷却时间,僵尸攻击有1天的冷却时间:

声明一个名为 cooldownTime 的uint,并将其设置为 1 days。(没错,”1 days“使用了复数, 否则通不过编译器)

因为在上一章中我们给 Zombie 结构体中添加 level 和 readyTime 两个参数,所以现在创建一个新的 Zombie 结构体时,需要修改 _createZombie(),在其中把新旧参数都初始化一下。

修改 zombies.push 那一行, 添加加2个参数:1(表示当前的 level )和uint32(now + cooldownTime 现在+冷静时间)(表示下次允许攻击的时间 readyTime)。
pragma solidity ^0.4.19;

import "./ownable.sol";

contract ZombieFactory is Ownable {

    event NewZombie(uint zombieId, string name, uint dna);

    uint dnaDigits = 16;
    uint dnaModulus = 10 ** dnaDigits;
    uint cooldownTime = 1 days;

    struct Zombie {
      string name;
      uint dna;
      uint32 level;
      uint32 readyTime;
    }

    Zombie[] public zombies;

    mapping (uint => address) public zombieToOwner;
    mapping (address => uint) ownerZombieCount;

    function _createZombie(string _name, uint _dna) internal {
        uint id = zombies.push(Zombie(_name, _dna, 1, uint32(now + cooldownTime))) - 1;
        zombieToOwner[id] = msg.sender;
        ownerZombieCount[msg.sender]++;
        NewZombie(id, _name, _dna);
    }

    function _generateRandomDna(string _str) private view returns (uint) {
        uint rand = uint(keccak256(_str));
        return rand % dnaModulus;
    }

    function createRandomZombie(string _name) public {
        require(ownerZombieCount[msg.sender] == 0);
        uint randDna = _generateRandomDna(_name);
        randDna = randDna - randDna % 100;
        _createZombie(_name, randDna);
    }

}

然后我们需要来规范僵尸的冷却时间,按照以下步骤修改 ZombieFeeding合约的feedAndMultiply:

”捕猎“行为会触发僵尸的”冷却周期“

僵尸在这段”冷却周期“结束前不可再捕猎小猫

这将限制僵尸,防止其无限制地捕猎小猫或者整天不停地繁殖。将来,当我们增加战斗功能时,我们同样用”冷却周期“限制僵尸之间打斗的频率。

首先,我们要定义一些辅助函数,设置并检查僵尸的 readyTime。

1 先定义一个 _triggerCooldown 函数。它要求一个参数,_zombie,表示一某个僵尸的存储指针。这个函数可见性设置为 internal。

2 在函数中,把 _zombie.readyTime 设置为 uint32(now + cooldownTime)。

3 接下来,创建一个名为 _isReady 的函数。这个函数的参数也是名为 _zombie 的 Zombie storage。这个功能只具有 internal 可见性,并返回一个 bool 值。

函数计算返回(_zombie.readyTime <= now),值为 truefalse。这个功能的目的是判断下次允许猎食的时间是否已经到了。

4 feedAndMultiply 过程需要参考 cooldownTime。首先,在找到 myZombie 之后,添加一个 require 语句来检查 _isReady() 并将 myZombie 传递给它。这样用户必须等到僵尸的 冷却周期 结束后才能执行 feedAndMultiply 功能。

5 在函数结束时,调用 _triggerCooldown(myZombie),标明捕猎行为触发了僵尸新的冷却周期。

完整ZombieFeeding合约代码:

pragma solidity ^0.4.19;

import "./zombiefactory.sol";

contract KittyInterface {
  function getKitty(uint256 _id) external view returns ( bool isGestating, bool isReady, uint256 cooldownIndex, uint256 nextActionAt, uint256 siringWithId, uint256 birthTime, uint256 matronId, uint256 sireId, uint256 generation, uint256 genes ); } contract ZombieFeeding is ZombieFactory {

  KittyInterface kittyContract;

  function setKittyContractAddress(address _address) external onlyOwner {
    kittyContract = KittyInterface(_address);
  }
   //攻击后触发增加冷却时间
  function _triggerCooldown(Zombie storage _zombie) internal {
    _zombie.readyTime = uint32(now + cooldownTime);
  }
   //判断当前僵尸攻击是否冷却
  function _isReady(Zombie storage _zombie) internal view returns (bool) {
      return (_zombie.readyTime <= now);
  }

  function feedAndMultiply(uint _zombieId, uint _targetDna, string species) internal {
    require(msg.sender == zombieToOwner[_zombieId]);
    Zombie storage myZombie = zombies[_zombieId];
    require(_isReady(myZombie));
    _targetDna = _targetDna % dnaModulus;
    uint newDna = (myZombie.dna + _targetDna) / 2;
    if (keccak256(species) == keccak256("kitty")) {
      newDna = newDna - newDna % 100 + 99;
    }
    _createZombie("NoName", newDna);
    _triggerCooldown(myZombie);
  }

  function feedOnKitty(uint _zombieId, uint _kittyId) public {
    uint kittyDna;
    (,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId);
    feedAndMultiply(_zombieId, kittyDna, "kitty");
  }

}

三. 自定义modifier
假如我们需要一些激励措施鼓励玩家去升级他们的僵尸,比如2级可以修改名字,5级可以修改dna,所以我们我们创建changeName和changeDna函数,但是这两个函数都必须有等级限制才能调用,我们前面使用过onlyOwner来限制某些函数只能让合约主人调用,那么我们同样通过自定义modifier来限制函数。

新建ZombieHelper合约继承ZombieFeeding,

1 在ZombieHelper 中,创建一个名为 aboveLevel 的modifier,它接收2个参数, _level (uint类型) 以及 _zombieId (uint类型)。
运用函数逻辑确保僵尸 zombies[_zombieId].level 大于或等于 _level。
2 创建一个名为 changeName 的函数。它接收2个参数:_zombieId(uint类型)以及 _newName(string类型),可见性为 external。它带有一个 aboveLevel 修饰符,调用的时候通过 _level 参数传入2, 当然,别忘了同时传 _zombieId 参数。

3 在这个函数中,首先我们用 require 语句,验证 msg.sender 是否就是 zombieToOwner [_zombieId]。

然后函数将 zombies[_zombieId] .name 设置为 _newName。

4 在 changeName 下创建另一个名为 changeDna 的函数。它的定义和内容几乎和 changeName 相同,不过它第二个参数是 _newDna(uint类型),在修饰符 aboveLevel 的 _level 参数中传递 20 。现在,他可以把僵尸的 dna 设置为 _newDna 了。
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

当我们把一个函数标记为view的时候,意味着告诉 web3.js,运行这个函数只需要查询你的本地以太坊节点,而不需要在区块链上创建一个事务(事务需要运行在每个节点上,因此花费 gas),因为本地以太坊存有所有的数据,这些数据是已经经过证实可信的,你去查询就不需要再去其它节点验证,自然不会产生事务,当然你去调用了另一个合约的函数就是在以太坊创建了一个事务,是需要花费gas的

五.存储非常昂贵
我们之前就说过只能合约的存储是非常昂贵的,因为无论是写入还是更新数据都需要在全球成千上万个节点的硬盘 永久性的写入数据。光想想是不是就很刺激,哈哈!

在大多数编程语言中,遍历大数据集合都是很昂贵的,但是solidity中遍历比存储便宜的多,因为view函数不会产生任何花销!

假如我们想返回某个地址拥有的所有僵尸,我们可以通过遍历zombies数组里的所有僵尸,把该拥有者的僵尸挑出来放到一个memory的临时数组里返回给调用者,这样就不会产生任何gas。

  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++) {
       //循环中如果发现有的僵尸是该拥有者的把僵尸id放进临时数组
      if (zombieToOwner[i] == _owner) {
        result[counter] = i;
        counter++;
      }
    }
    return result;
  }

六 总结
到此为此,我们在创建僵尸军团的进阶的过程中学习了很多solidity的高级理论,包括:

1 添加了一种新方法来修改CryptoKitties合约
2 学会使用 onlyOwner 进行调用权限限制
3 了解了 gas 和 gas 的优化
4 为僵尸添加了 “级别” 和 “冷却周期”属性
5 当僵尸达到一定级别时,允许修改僵尸的名字和 DNA
6 最后,定义了一个函数,用以返回某个玩家的僵尸军团

到此关于solidity相关的语法知识及和以太坊结合的特性已经学习完了,实际上这肯定是一个基础的学习,希望在后面能对solitidy和智能合约有更加深刻的认识!!!

阅读更多

更多精彩内容