目录
一、前言
看了一些区块链的教程,论文,在网上刚刚找到了一个项目实战,CryptoZombies。
本次要为大家讲解有关于Gas的相关知识。
如果你想了解更多有关于机器学习、深度学习、区块链、计算机视觉等相关技术的内容,想与更多大佬一起沟通,那就扫描下方二维码加入我们吧!
二、Gas - 驱动以太坊DApps的能源
1、讲解
Gas是solidity编程语言中非常与众不同的特征,我猜想,Gas是受到了比特币的启发。用户每次执行你的DAPP都需要支付一定的Gas,Gas可以使用以太币购买。
1.以太币
在讲Gas之前,我们先来简单了解一下什么是以太币。
以太币(ETH)是以太坊(Ethereum)的一种数字代币,被视为“比特币2.0版”,以太币涉及到的技术是与比特币不同的区块链技术“以太坊”(Ethereum),以太坊是一个开源的有智能合约成果的民众区块链平台。
在此,膜拜V神!
在此,膜拜V神!
在此,膜拜V神!
但是我们只是在此做个简单了解,因为我们这个博客是为了做CryptoZombies的学习记录,以后,我会逐步完善有关区块链和以太坊的内容。
2.Gas是啥
Gas是测量以太坊网络中运行交易或智能合约的计算工作的单位。该系统类似于使用千瓦(kW)来测量房屋内的电力;您使用的电力不是以美元和美分计量,而是以每小时千瓦时或千瓦计。
Gas用来衡量执行某些动作需要多少“工作量”,这些“工作量”就是为了执行该动作支付给网络的费用额。通俗理解,Gas是给矿工的佣金,以ETH 支付,无论是交易、执行智能合约并启动 DApps,还是支付数据存储费用,都需要用到 Gas。
3.Gas能干嘛?
Gas常用来作为激励矿工,那为什么要用Gas来作为激励呢?
区块链中有很多矿工,他们耗费大量的算力进行挖矿,无非就是为了抢夺nonce值,谁最先计算出来得到全网认可的nonce值,谁就获得了记账权,并能够获得对应的比特币。
以太坊就像一个巨大、缓慢、但非常安全的电脑。当你运行一个程序的时候,网络上的每一个节点都在进行相同的运算,以验证它的输出 —— 这就是所谓的“去中心化” 由于数以千计的节点同时在验证着每个功能的运行,这可以确保它的数据不会被被监控,或者被刻意修改。
可能会有用户用无限循环堵塞网络,抑或用密集运算来占用大量的网络资源,为了防止这种事情的发生,以太坊的创建者为以太坊上的资源制定了价格,想要在以太坊上运算或者存储,你需要先付费。
注:如果你使用侧链,倒是不一定需要付费,比如咱们在 Loom Network 上构建的 CryptoZombies 就免费。你不会想要在以太坊主网上玩儿“魔兽世界”吧? - 所需要的 gas 可能会买到你破产。但是你可以找个算法理念不同的侧链来玩它。我们将在以后的课程中咱们会讨论到,什么样的 DApp 应该部署在太坊主链上,什么又最好放在侧链。
更多详细的有关Gas的内容,我们在介绍以太坊相关概念的时候,会着重介绍,在这里我们主要还是来聊一下技术吧!
4.如何节省Gas
我们了解到用户执行DAPP就会消耗Gas,那我们肯定是希望Gas消耗的越少越好,那我们如何节省Gas呢?
在第1课中,我们提到除了基本版的 uint
外,还有其他变种 uint
:uint8
,uint16
,uint32
等。
通常情况下我们不会考虑使用 uint
变种,因为无论如何定义 uint
的大小,Solidity 为它保留256位的存储空间。例如,使用 uint8
而不是uint
(uint256
)不会为你节省任何 gas。
如果一个 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);
所以,当 uint
定义在一个 struct
中的时候,尽量使用最小的整数子类型以节约空间。 并且把同样类型的变量放一起(即在 struct 中将把变量按照类型依次放置),这样 Solidity 可以将存储空间最小化。例如,有两个 struct
:
uint c; uint32 a; uint32 b;
和
uint32 a; uint c; uint32 b;
前者比后者需要的gas更少,因为前者把uint32
放一起了。
2、实战
1.要求
为 Zombie
结构体添加两个属性:level
(uint32
)和readyTime
(uint32
)。因为希望同类型数据打成一个包,所以把它们放在结构体的末尾。32位足以保存僵尸的级别和时间戳了,这样比起使用普通的uint
(256位),可以更紧密地封装数据,从而为我们省点 gas。
2.代码
pragma solidity >=0.5.0 <0.6.0;
import "./ownable.sol";
contract ZombieFactory is Ownable {
event NewZombie(uint zombieId, string name, uint dna);
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;
struct Zombie {
string name;
uint dna;
// Add new data here
uint32 level;
uint32 readyTime;
}
Zombie[] public zombies;
mapping (uint => address) public zombieToOwner;
mapping (address => uint) ownerZombieCount;
function _createZombie(string memory _name, uint _dna) internal {
uint id = zombies.push(Zombie(_name, _dna)) - 1;
zombieToOwner[id] = msg.sender;
ownerZombieCount[msg.sender]++;
emit NewZombie(id, _name, _dna);
}
function _generateRandomDna(string memory _str) private view returns (uint) {
uint rand = uint(keccak256(abi.encodePacked(_str)));
return rand % dnaModulus;
}
function createRandomZombie(string memory _name) public {
require(ownerZombieCount[msg.sender] == 0);
uint randDna = _generateRandomDna(_name);
randDna = randDna - randDna % 100;
_createZombie(_name, randDna);
}
}
三、Time Units
1、讲解
在上面的例题中,我们添加了两个uint32类型的数据level和readyTime。
level属性表示僵尸的级别。以后,在我们创建的战斗系统中,打胜仗的僵尸会逐渐升级并获得更多的能力。
重点是readyTime。
我们希望增加一个“冷却周期”,表示僵尸在两次猎食或攻击之之间必须等待的时间。如果没有它,僵尸每天可能会攻击和繁殖1,000次,这样游戏就太简单了。
为了记录僵尸在下一次进击前需要等待的时间,我们使用了 Solidity 的时间单位。
Solidity 使用自己的本地时间单位。
变量
now
将返回当前的unix时间戳(自1970年1月1日以来经过的秒数)。注:Unix时间传统用一个32位的整数进行存储。这会导致“2038年”问题,当这个32位的unix时间戳不够用,产生溢出,使用这个时间的遗留系统就麻烦了。所以,如果我们想让我们的 DApp 跑够20年,我们可以使用64位整数表示时间,但为此我们的用户又得支付更多的 gas。真是个两难的设计啊!
Solidity 还包含
秒(seconds)
,分钟(minutes)
,小时(hours)
,天(days)
,周(weeks)
和年(years)
等时间单位。它们都会转换成对应的秒数放入uint
中。所以1分钟
就是60
,1小时
是3600
(60秒×60分钟),1天
是86400
(24小时×60分钟×60秒),以此类推。
下面是一些使用时间单位的实用案例:
uint lastUpdated;
// 将‘上次更新时间’ 设置为 ‘现在’
function updateTimestamp() public {
lastUpdated = now;
}
// 如果到上次`updateTimestamp` 超过5分钟,返回 'true'
// 不到5分钟返回 'false'
function fiveMinutesHavePassed() public view returns (bool) {
return (now >= (lastUpdated + 5 minutes));
}
2、实战1
1.要求
给DApp添加一个“冷却周期”的设定,让僵尸两次攻击或捕猎之间必须等待 1天。
1.声明一个名为
cooldownTime
的uint
,并将其设置为1 days
。2.因为在上一章中我们给
Zombie
结构体中添加level
和readyTime
两个参数,所以现在创建一个新的Zombie
结构体时,需要修改_createZombie()
,在其中把新旧参数都初始化一下。修改
zombies.push
那一行, 添加加2个参数:1
(表示当前的level
)和uint32(now + cooldownTime)
(现在+冷却时间,表示下次允许攻击的时间readyTime
)。注:必须使用
uint32(...)
进行强制类型转换,因为now
返回类型uint256
。所以我们需要明确将它转换成一个uint32
类型的变量。
2.代码
pragma solidity >=0.5.0 <0.6.0;
import "./ownable.sol";
contract ZombieFactory is Ownable {
event NewZombie(uint zombieId, string name, uint dna);
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;
// 1. Define `cooldownTime` here
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 memory _name, uint _dna) internal {
// 2. Update the following line:
uint id = zombies.push(Zombie(_name, _dna, 1, uint32(now + cooldownTime))) - 1;
zombieToOwner[id] = msg.sender;
ownerZombieCount[msg.sender]++;
emit NewZombie(id, _name, _dna);
}
function _generateRandomDna(string memory _str) private view returns (uint) {
uint rand = uint(keccak256(abi.encodePacked(_str)));
return rand % dnaModulus;
}
function createRandomZombie(string memory _name) public {
require(ownerZombieCount[msg.sender] == 0);
uint randDna = _generateRandomDna(_name);
randDna = randDna - randDna % 100;
_createZombie(_name, randDna);
}
}
3、实战2——僵尸冷却
冷却用于防止僵尸无限制地繁殖和捕猎,也会在以后用于限制僵尸之间的打频率。
1.要求
给DApp添加一个“冷却周期”的设定,让僵尸两次攻击或捕猎之间必须等待 1天。
1.先定义一个
_triggerCooldown
函数。它要求一个参数,_zombie
,表示一某个Zombie的storage指针。这个函数可见性设置为internal
。2.在函数中,把
_zombie.readyTime
设置为uint32(now + cooldownTime)
。3.创建一个名为
_isReady
的函数。这个函数的参数也是名为_zombie
的Zombie storage
。这是一个 internal view 函数,并返回一个bool
值。。4.函数计算返回
(_zombie.readyTime <= now)
,值为true
或false
。这个功能的目的是判断下次允许猎食的时间是否已经到了。
2.代码
pragma solidity >=0.5.0 <0.6.0;
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);
}
// 1. Define `_triggerCooldown` function here
function _triggerCooldown(Zombie storage _zombie) internal {
_zombie.readyTime = uint32(now + cooldownTime);
}
// 2. Define `_isReady` function here
function _isReady(Zombie storage _zombie) internal view returns(bool) {
return (_zombie.readyTime <= now);
}
function feedAndMultiply(uint _zombieId, uint _targetDna, string memory _species) public {
require(msg.sender == zombieToOwner[_zombieId]);
Zombie storage myZombie = zombies[_zombieId];
_targetDna = _targetDna % dnaModulus;
uint newDna = (myZombie.dna + _targetDna) / 2;
if (keccak256(abi.encodePacked(_species)) == keccak256(abi.encodePacked("kitty"))) {
newDna = newDna - newDna % 100 + 99;
}
_createZombie("NoName", newDna);
}
function feedOnKitty(uint _zombieId, uint _kittyId) public {
uint kittyDna;
(,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId);
feedAndMultiply(_zombieId, kittyDna, "kitty");
}
}