一.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),值为 true 或 false。这个功能的目的是判断下次允许猎食的时间是否已经到了。
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和智能合约有更加深刻的认识!!!