通過前邊的 Solidity 基礎(chǔ)語法學(xué)習(xí)瞳遍,我們已經(jīng)有了Solidity編程經(jīng)驗彻磁,在這節(jié)就要學(xué)學(xué)
Ethereum
開發(fā)的技術(shù)細節(jié),編寫真正的 DApp 時必知的:智能協(xié)議的所有權(quán)
佃扼,Gas的花費
,代碼優(yōu)化
蔼夜,和代碼安全
兼耀。
一、智能協(xié)議的永固性
到現(xiàn)在為止求冷,我們講的 Solidity 和其他語言沒有質(zhì)的區(qū)別翠订,它長得也很像 JavaScript.
但是,在有幾點以太坊上的 DApp 跟普通的應(yīng)用程序有著天壤之別遵倦。
第一個例子尽超,在你把智能協(xié)議傳上以太坊之后,它就變得不可更改
, 這種永固性意味著你的代碼永遠不能被調(diào)整或更新梧躺。
你編譯的程序會一直似谁,永久的,不可更改的掠哥,存在以太網(wǎng)上巩踏。這就是Solidity代碼的安全性如此重要的一個原因。如果你的智能協(xié)議有任何漏洞续搀,即使你發(fā)現(xiàn)了也無法補救塞琼。你只能讓你的用戶們放棄這個智能協(xié)議,然后轉(zhuǎn)移到一個新的修復(fù)后的合約上禁舷。
但這恰好也是智能合約的一大優(yōu)勢彪杉。 代碼說明一切毅往。 如果你去讀智能合約的代碼,并驗證它派近,你會發(fā)現(xiàn)攀唯, 一旦函數(shù)被定義下來,每一次的運行渴丸,程序都會嚴格遵照函數(shù)中原有的代碼邏輯一絲不茍地執(zhí)行侯嘀,完全不用擔(dān)心函數(shù)被人篡改而得到意外的結(jié)果。
外部依賴關(guān)系
在上邊的文章中谱轨,我們將加密小貓(CryptoKitties)合約的地址硬編碼
到DApp中去了戒幔。有沒有想過,如果加密小貓出了點問題土童,比方說溪食,集體消失了會怎么樣? 雖然這種事情幾乎不可能發(fā)生娜扇,但是错沃,如果小貓沒了,我們的 DApp 也會隨之失效 -- 因為我們在 DApp 的代碼中用“硬編碼”的方式指定了加密小貓的地址雀瓢,如果這個根據(jù)地址找不到小貓枢析,我們的僵尸也就吃不到小貓了,而按照前面的描述刃麸,我們卻沒法修改合約去應(yīng)付這個變化醒叁!
因此,我們不能硬編碼泊业,而要采用“函數(shù)”把沼,以便于 DApp 的關(guān)鍵部分可以以參數(shù)形式修改。
比方說吁伺,我們不再一開始就把獵物地址給寫入代碼饮睬,而是寫個函數(shù) setKittyContractAddress
, 運行時再設(shè)定獵物的地址,這樣我們就可以隨時去鎖定新的獵物篮奄,也不用擔(dān)心加密小貓集體消失了捆愁。
實戰(zhàn)演練
請修改前邊的代碼,使得可以通過程序更改CryptoKitties合約地址窟却。
- 1昼丑、刪除采用硬編碼 方式的
ckAddress
代碼行。 - 2夸赫、之前創(chuàng)建
kittyContract
變量的那行代碼菩帝,修改為對kittyContract
變量的聲明 -- 暫時不給它指定具體的實例。 - 3、創(chuàng)建名為 setKittyContractAddress 的函數(shù)呼奢, 它帶一個參數(shù) _address(address類型)宜雀, 可見性設(shè)為external。
- 4控妻、在函數(shù)內(nèi)部州袒,添加一行代碼揭绑,將 kittyContract 變量設(shè)置為返回值:KittyInterface(_address)弓候。
注意:你可能會注意到這個功能有個安全漏洞,別擔(dān)心 - 咱們到下一章里解決它;)
zombiefeeding.sol
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 {
// 1\. 移除這一行:
// address ckAddress = 0x06012c8cf97BEaD5deAe237070F9587f8E7A266d;
// 2\. 只聲明變量:
// KittyInterface kittyContract = KittyInterface(ckAddress);
KittyInterface kittyContract;
// 3\. 增加 setKittyContractAddress 方法
function setKittyContractAddress(address _address) external {
kittyContract = KittyInterface(_address);
}
function feedAndMultiply(uint _zombieId, uint _targetDna, string species) public {
require(msg.sender == zombieToOwner[_zombieId]);
Zombie storage myZombie = zombies[_zombieId];
_targetDna = _targetDna % dnaModulus;
uint newDna = (myZombie.dna + _targetDna) / 2;
if (keccak256(species) == keccak256("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");
}
}
二他匪、Ownable Contracts
上面代碼中菇存,您有沒有發(fā)現(xiàn)任何安全漏洞呢?
呀邦蜜!setKittyContractAddress
可見性居然申明為“外部的”(external
)依鸥,豈不是任何人都可以調(diào)用它! 也就是說悼沈,任何調(diào)用該函數(shù)的人都可以更改 CryptoKitties 合約的地址贱迟,使得其他人都沒法再運行我們的程序了。
我們確實是希望這個地址能夠在合約中修改絮供,但我可沒說讓每個人去改它呀衣吠。
要對付這樣的情況,通常的做法是指定合約的“
所有權(quán)
” - 就是說壤靶,給它指定一個主人(沒錯缚俏,就是您),只有主人對它享有特權(quán)贮乳。
Ownable
下面是一個 Ownable
合約的例子: 來自 OpenZeppelin
Solidity 庫的 Ownable
合約忧换。 OpenZeppelin
是主打安保和社區(qū)審查的智能合約庫,您可以在自己的 DApps中引用向拆。等把這一課學(xué)完亚茬,您不要催我們發(fā)布下一課,最好利用這個時間把 OpenZeppelin 的網(wǎng)站看看浓恳,保管您會學(xué)到很多東西才写!
把樓下這個合約讀讀通,是不是還有些沒見過代碼奖蔓?別擔(dān)心赞草,我們隨后會解釋。
/**
* @title Ownable
* @dev The Ownable contract has an owner address, and provides basic authorization control
* functions, this simplifies the implementation of "user permissions".
*/
contract Ownable {
address public owner;
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
/**
* @dev The Ownable constructor sets the original `owner` of the contract to the sender
* account.
*/
function Ownable() public {
owner = msg.sender;
}
/**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
/**
* @dev Allows the current owner to transfer control of the contract to a newOwner.
* @param newOwner The address to transfer ownership to.
*/
function transferOwnership(address newOwner) public onlyOwner {
require(newOwner != address(0));
OwnershipTransferred(owner, newOwner);
owner = newOwner;
}
}
下面有沒有您沒學(xué)過的東東吆鹤?
- 構(gòu)造函數(shù):
function Ownable()
是一個constructor
(構(gòu)造函數(shù))厨疙,構(gòu)造函數(shù)不是必須的,它與合約同名疑务,構(gòu)造函數(shù)一生中唯一的一次執(zhí)行沾凄,就是在合約最初被創(chuàng)建的時候梗醇。 - 函數(shù)修飾符:
modifier onlyOwner()
。 修飾符跟函數(shù)很類似撒蟀,不過是用來修飾其他已有函數(shù)用的叙谨, 在其他語句執(zhí)行前,為它檢查下先驗條件保屯。 在這個例子中手负,我們就可以寫個修飾符onlyOwner
檢查下調(diào)用者,確保只有合約的主人才能運行本函數(shù)姑尺。我們下一章中會詳細講述修飾符竟终,以及那個奇怪的_;。 -
indexed
關(guān)鍵字:別擔(dān)心切蟋,我們還用不到它强挫。
所以 Ownable
合約基本都會這么干:
- 1蒿讥、合約創(chuàng)建捕仔,構(gòu)造函數(shù)先行键畴,將其
owner
設(shè)置為msg.sender
(其部署者) - 2、為它加上一個修飾符
onlyOwner
驻右,它會限制陌生人的訪問什黑,將訪問某些函數(shù)的權(quán)限鎖定在owner
上。 - 3旺入、允許將合約所有權(quán)轉(zhuǎn)讓給他人兑凿。
onlyOwner
簡直人見人愛,大多數(shù)人開發(fā)自己的 Solidity DApps茵瘾,都是從復(fù)制/粘貼 Ownable
開始的礼华,從它再繼承出的子類,并在之上進行功能開發(fā)拗秘。
既然我們想把 setKittyContractAddress
限制為 onlyOwner
圣絮,我們也要做同樣的事情。
實戰(zhàn)演練
首先雕旨,將 Ownable
合約的代碼復(fù)制一份到新文件 ownable.so
l 中扮匠。 接下來,創(chuàng)建一個 ZombieFactory
凡涩,繼承 Ownable
棒搜。
- 1.在程序中導(dǎo)入
ownable.sol
的內(nèi)容。 如果您不記得怎么做了活箕,參考下zombiefeeding.sol
力麸。 - 2.修改
ZombieFactory
合約, 讓它繼承自Ownable
。 如果您不記得怎么做了克蚂,看看zombiefeeding.sol
闺鲸。
ownable.sol
文件:
/**
* @title Ownable
* @dev The Ownable contract has an owner address, and provides basic authorization control
* functions, this simplifies the implementation of "user permissions".
*/
contract Ownable {
address public owner;
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
/**
* @dev The Ownable constructor sets the original `owner` of the contract to the sender
* account.
*/
function Ownable() public {
owner = msg.sender;
}
/**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
/**
* @dev Allows the current owner to transfer control of the contract to a newOwner.
* @param newOwner The address to transfer ownership to.
*/
function transferOwnership(address newOwner) public onlyOwner {
require(newOwner != address(0));
OwnershipTransferred(owner, newOwner);
owner = newOwner;
}
}
zombiefactory.sol
pragma solidity ^0.4.19;
// 1\. 在這里導(dǎo)入
import "./ownable.sol";
// 2\. 在這里繼承:
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;
}
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;
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);
}
}
三、onlyOwner函數(shù)修飾符
現(xiàn)在我們有了個基本版的合約 ZombieFactory
了埃叭,它繼承自 Ownable
接口摸恍,我們也可以給 ZombieFeeding
加上 onlyOwner
函數(shù)修飾符。
這就是合約繼承的工作原理赤屋。記得:
ZombieFeeding 是個 ZombieFactory
ZombieFactory 是個 Ownable
函數(shù)修飾符modifier
函數(shù)修飾符看起來跟函數(shù)沒什么不同立镶,不過關(guān)鍵字modifier
告訴編譯器,這是個modifier(修飾符)
益缎,而不是個function(函數(shù))
谜慌。它不能像函數(shù)那樣被直接調(diào)用然想,只能被添加到函數(shù)定義的末尾莺奔,用以改變函數(shù)的行為。
再仔細讀讀 onlyOwner
:
/**
* @dev 調(diào)用者不是‘主人’变泄,就會拋出異常
*/
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
onlyOwner
函數(shù)修飾符是這么用的:
contract MyContract is Ownable {
event LaughManiacally(string laughter);
//注意令哟! `onlyOwner`上場 :
function likeABoss() external onlyOwner {
LaughManiacally("Muahahahaha");
}
}
注意 likeABoss
函數(shù)上的 onlyOwner
修飾符。 當(dāng)你調(diào)用 likeABoss
時妨蛹,首先執(zhí)行 onlyOwner
中的代碼屏富, 執(zhí)行到 onlyOwner
中的_;
語句時,程序再返回并執(zhí)行 likeABoss
中的代碼蛙卤。
可見狠半,盡管函數(shù)修飾符也可以應(yīng)用到各種場合,但最常見的還是放在函數(shù)執(zhí)行之前添加快速的 require
檢查颤难。
因為給函數(shù)添加了修飾符 onlyOwner
神年,使得唯有合約的主人(也就是部署者)才能調(diào)用它。
注意:主人對合約享有的特權(quán)當(dāng)然是正當(dāng)?shù)男朽停贿^也可能被惡意使用已日。比如,萬一栅屏,主人添加了個后門飘千,允許他偷走別人的僵尸呢?
所以非常重要的是栈雳,部署在以太坊上的 DApp护奈,并不能保證它真正做到去中心,你需要閱讀并理解它的源代碼哥纫,才能防止其中沒有被部署者惡意植入后門霉旗;作為開發(fā)人員,如何做到既要給自己留下修復(fù) bug 的余地,又要盡量地放權(quán)給使用者奖慌,以便讓他們放心你抛虫,從而愿意把數(shù)據(jù)放在你的 DApp 中,這確實需要個微妙的平衡简僧。
實戰(zhàn)演練
現(xiàn)在我們可以限制第三方對 setKittyContractAddress
的訪問建椰,除了我們自己,誰都無法去修改它岛马。
- 1棉姐、將
onlyOwner
函數(shù)修飾符添加到setKittyContractAddress
中。
zombiefeeding.sol
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;
// 修改這個函數(shù),添加權(quán)限onlyOwner
function setKittyContractAddress(address _address) external onlyOwner {
kittyContract = KittyInterface(_address);
}
function feedAndMultiply(uint _zombieId, uint _targetDna, string species) public {
require(msg.sender == zombieToOwner[_zombieId]);
Zombie storage myZombie = zombies[_zombieId];
_targetDna = _targetDna % dnaModulus;
uint newDna = (myZombie.dna + _targetDna) / 2;
if (keccak256(species) == keccak256("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");
}
}
四啦逆、Gas
現(xiàn)在我們懂了如何在禁止第三方修改我們的合約的同時伞矩,留個后門給咱們自己去修改。
讓我們來看另一種使得 Solidity 編程語言與眾不同的特征:
Gas-驅(qū)動以太坊DApps的能源
在 Solidity 中夏志,你的用戶想要每次執(zhí)行你的 DApp 都需要支付一定的 gas乃坤,gas 可以用以太幣購買,因此沟蔑,用戶每次跑 DApp 都得花費以太幣湿诊。
一個 DApp 收取多少 gas 取決于功能邏輯的復(fù)雜程度。每個操作背后瘦材,都在計算完成這個操作所需要的計算資源厅须,(比如,存儲數(shù)據(jù)就比做個加法運算貴得多)食棕, 一次操作所需要花費的 gas
等于這個操作背后的所有運算花銷的總和朗和。
由于運行你的程序需要花費用戶的真金白銀,在以太坊中代碼的編程語言簿晓,比其他任何編程語言都更強調(diào)優(yōu)化眶拉。同樣的功能,使用笨拙的代碼開發(fā)的程序抢蚀,比起經(jīng)過精巧優(yōu)化的代碼來镀层,運行花費更高,這顯然會給成千上萬的用戶帶來大量不必要的開銷皿曲。
為何要gas來驅(qū)動唱逢?
以太坊就像一個巨大、緩慢屋休、但非常安全的電腦坞古。當(dāng)你運行一個程序的時候,網(wǎng)絡(luò)上的每一個節(jié)點都在進行相同的運算劫樟,以驗證它的輸出 —— 這就是所謂的”去中心化“ 由于數(shù)以千計的節(jié)點同時在驗證著每個功能的運行痪枫,這可以確保它的數(shù)據(jù)不會被被監(jiān)控织堂,或者被刻意修改。
可能會有用戶用無限循環(huán)堵塞網(wǎng)絡(luò)奶陈,抑或用密集運算來占用大量的網(wǎng)絡(luò)資源易阳,為了防止這種事情的發(fā)生,以太坊的創(chuàng)建者為以太坊上的資源制定了價格吃粒,想要在以太坊上運算或者存儲潦俺,你需要先付費
。
注意:如果你使用
側(cè)鏈
徐勃,倒是不一定需要付費事示,比如咱們在 Loom Network 上構(gòu)建的 CryptoZombies 就免費。你不會想要在以太坊主網(wǎng)上玩兒“魔獸世界”吧僻肖? - 所需要的 gas 可能會買到你破產(chǎn)肖爵。但是你可以找個算法理念不同的側(cè)鏈來玩它。我們將在以后的課程中咱們會討論到臀脏,什么樣的 DApp 應(yīng)該部署在太坊主鏈上劝堪,什么又最好放在側(cè)鏈。
省gas的招數(shù)
省 gas 的招數(shù):結(jié)構(gòu)封裝(Struct packing)
在第1課中谁榜,我們提到除了基本版的 uint
外幅聘,還有其他變種 uint
:uint8
凡纳,uint16
窃植,uint32
等。
通常情況下我們不會考慮使用 uint
變種荐糜,因為無論如何定義 uint
的大小巷怜,Solidity 為它保留256位的存儲空間。例如暴氏,使用 uint8
而不是uint(uint256)不會為你節(jié)省任何 gas延塑。
除非,把 uint
綁定到 struct
里面答渔。
如果一個 struct
中有多個 uint
关带,則盡可能使用較小的 uint
, Solidity 會將這些 uint 打包在一起,從而占用較少的存儲空間沼撕。例如:
struct NormalStruct {
uint a;
uint b;
uint c;
}
struct MiniMe {
uint32 a;
uint32 b;
uint c;
}
// 因為使用了結(jié)構(gòu)打包宋雏,`mini` 比 `normal` 占用的空間更少
NormalStruct normal = NormalStruct(10, 20, 30);
MiniMe mini = MiniMe(10, 20, 30);
所以,當(dāng) uint
定義在一個 struct
中的時候务豺,盡量使用最小的整數(shù)子類型以節(jié)約空間磨总。 并且把同樣類型的變量放一起(即在 struct 中將把變量按照類型依次放置),這樣 Solidity 可以將存儲空間最小化笼沥。例如蚪燕,有兩個 struct
:
uint c; uint32 a; uint32 b;
和 uint32 a; uint c; uint32 b;
前者比后者需要的gas更少娶牌,因為前者把uint32
放一起了。
實戰(zhàn)演練
咱們給僵尸添2個新功能:le??vel
和 readyTime
- 后者是用來實現(xiàn)一個“冷卻定時器”馆纳,以限制僵尸獵食的頻率诗良。
讓我們回到 zombiefactory.sol
。
- 1鲁驶、為
Zombie
結(jié)構(gòu)體 添加兩個屬性:level
(uint32
)和readyTime
(uint32
)累榜。因為希望同類型數(shù)據(jù)打成一個包,所以把它們放在結(jié)構(gòu)體的末尾灵嫌。
32位足以保存僵尸的級別和時間戳了壹罚,這樣比起使用普通的uint(256位),可以更緊密地封裝數(shù)據(jù)寿羞,從而為我們省點 gas猖凛。
zombiefactory.sol
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;
struct Zombie {
string name;
uint dna;
// 在這里添加數(shù)據(jù)
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;
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);
}
}
五、時間單位
level
屬性表示僵尸的級別绪穆。以后辨泳,在我們創(chuàng)建的戰(zhàn)斗系統(tǒng)中,打勝仗的僵尸會逐漸升級并獲得更多的能力玖院。
readyTime
稍微復(fù)雜點菠红。我們希望增加一個“冷卻周期”,表示僵尸在兩次獵食或攻擊之之間必須等待的時間难菌。如果沒有它试溯,僵尸每天可能會攻擊和繁殖1,000次,這樣游戲就太簡單了郊酒。
為了記錄僵尸在下一次進擊前需要等待的時間遇绞,我們使用了 Solidity 的時間單位。
時間單位
Solidity 使用自己的本地時間單位燎窘。
變量 now
將返回當(dāng)前的unix時間戳(自1970年1月1日以來經(jīng)過的秒數(shù))摹闽。我寫這句話時 unix 時間是 1515527488。
注意:Unix時間傳統(tǒng)用一個32位的整數(shù)進行存儲褐健。這會導(dǎo)致“2038年”問題付鹿,當(dāng)這個32位的unix時間戳不夠用,產(chǎn)生溢出蚜迅,使用這個時間的遺留系統(tǒng)就麻煩了舵匾。所以,如果我們想讓我們的 DApp 跑夠20年慢叨,我們可以使用64位整數(shù)表示時間纽匙,但為此我們的用戶又得支付更多的 gas。真是個兩難的設(shè)計芭男场烛缔!
Solidity 還包含秒(seconds)
馏段,分鐘(minutes)
,小時(hours)
践瓷,天(days)
院喜,周(weeks)
和 年(years)
等時間單位。它們都會轉(zhuǎn)換成對應(yīng)的秒數(shù)放入 uint
中晕翠。所以 1分鐘 就是 60喷舀,1小時是 3600(60秒×60分鐘),1天是86400
(24小時×60分鐘×60秒)淋肾,以此類推硫麻。
下面是一些使用時間單位的實用案例:
uint lastUpdated;
// 將‘上次更新時間’ 設(shè)置為 ‘現(xiàn)在’
function updateTimestamp() public {
lastUpdated = now;
}
// 如果到上次`updateTimestamp` 超過5分鐘,返回 'true'
// 不到5分鐘返回 'false'
function fiveMinutesHavePassed() public view returns (bool) {
return (now >= (lastUpdated + 5 minutes));
}
有了這些工具樊卓,我們可以為僵尸設(shè)定”冷靜時間“功能
實戰(zhàn)演練
現(xiàn)在咱們給DApp添加一個“冷卻周期”的設(shè)定拿愧,讓僵尸兩次攻擊或捕獵之間必須等待 1天。
- 1碌尔、聲明一個名為
cooldownTime
的uint
浇辜,并將其設(shè)置為 1 days。(沒錯唾戚,”1 days“使用了復(fù)數(shù)柳洋, 否則通不過編譯器) - 2、因為在上一章中我們給
Zombie
結(jié)構(gòu)體中添加level
和readyTime
兩個參數(shù)叹坦,所以現(xiàn)在創(chuàng)建一個新的 Zombie 結(jié)構(gòu)體時熊镣,需要修改_createZombie()
,在其中把新舊參數(shù)都初始化一下立由。 - 3轧钓、修改
zombies.push
那一行, 添加加2個參數(shù):1(表示當(dāng)前的 level )和uint32(now + cooldownTime 現(xiàn)在+冷靜時間)(表示下次允許攻擊的時間 readyTime)锐膜。
注意:必須使用
uint32
(...) 進行強制類型轉(zhuǎn)換,因為 now 返回類型uint256
弛房。所以我們需要明確將它轉(zhuǎn)換成一個 uint32 類型的變量道盏。
now + cooldownTime
將等于當(dāng)前的unix時間戳(以秒為單位)加上”1天“里的秒數(shù) - 這將等于從現(xiàn)在起1天后的unix時間戳。然后我們就比較文捶,看看這個僵尸的 readyTime是否大于 now荷逞,以決定再次啟用僵尸的時機有沒有到來。
下一節(jié)中粹排,我們將討論如何通過 readyTime
來規(guī)范僵尸的行為种远。
zombiefactory.sol
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;
// 1\. 在這里定義 `cooldownTime`
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 {
// 2\. 修改下面這行:
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);
}
}
六、時間周期定時器
現(xiàn)在顽耳,Zombie 結(jié)構(gòu)體中定義好了一個 readyTime
屬性坠敷,讓我們跳到 zombiefeeding.sol
妙同, 去實現(xiàn)一個”冷卻周期定時器
“。
按照以下步驟修改 feedAndMultiply
:
- 1膝迎、”捕獵“行為會觸發(fā)僵尸的”冷卻周期“
- 2粥帚、僵尸在這段”冷卻周期“結(jié)束前不可再捕獵小貓
這將限制僵尸限次,防止其無限制地捕獵小貓或者整天不停地繁殖羊始。將來,當(dāng)我們增加戰(zhàn)斗功能時,我們同樣用”冷卻周期“限制僵尸之間打斗的頻率。
首先,我們要定義一些輔助函數(shù)忽舟,設(shè)置并檢查僵尸的 readyTime。
將結(jié)構(gòu)體作為參數(shù)傳入
由于結(jié)構(gòu)體的存儲指針可以以參數(shù)的方式傳遞給一個 private 或 internal 的函數(shù),因此結(jié)構(gòu)體可以在多個函數(shù)之間相互傳遞。
遵循這樣的語法:
function _doStuff(Zombie storage _zombie) internal {
// do stuff with _zombie
}
這樣我們可以將某僵尸的引用直接傳遞給一個函數(shù)担猛,而不用是通過參數(shù)傳入僵尸ID后,函數(shù)再依據(jù)ID去查找比驻。
實戰(zhàn)演練
- 1掸掸、先定義一個
_triggerCooldown
函數(shù)羽莺。它要求一個參數(shù),_zombie
桐绒,表示一某個僵尸的存儲指針之拨。這個函數(shù)可見性設(shè)置為internal
。 - 2咧叭、在函數(shù)中蚀乔,把 _zombie.readyTime 設(shè)置為
uint32(now + cooldownTime)
。 - 3菲茬、接下來吉挣,創(chuàng)建一個名為
_isReady
的函數(shù)。這個函數(shù)的參數(shù)也是名為 _zombie 的Zombie storage
婉弹。這個功能只具有 internal 可見性睬魂,并返回一個 bool 值。 - 4镀赌、函數(shù)計算返回
(_zombie.readyTime <= now)
氯哮,值為true
或false
。這個功能的目的是判斷下次允許獵食的時間是否已經(jīng)到了商佛。
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);
}
// 1\. 在這里定義 `_triggerCooldown` 函數(shù)
function _triggerCooldown(Zombie storage _zombie) internal {
_zombie.readyTime = uint32(now + cooldownTime);
}
// 2\. 在這里定義 `_isReady` 函數(shù)
function _isReady(Zombie storage _zombie) internal view returns (bool) {
return (_zombie.readyTime <= now);
}
function feedAndMultiply(uint _zombieId, uint _targetDna, string species) public {
require(msg.sender == zombieToOwner[_zombieId]);
Zombie storage myZombie = zombies[_zombieId];
_targetDna = _targetDna % dnaModulus;
uint newDna = (myZombie.dna + _targetDna) / 2;
if (keccak256(species) == keccak256("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");
}
}
七喉钢、公有函數(shù)和安全性
現(xiàn)在來修改 feedAndMultiply
,實現(xiàn)冷卻周期良姆。
回顧一下這個函數(shù)肠虽,前一課上我們將其可見性設(shè)置為public
。你必須仔細地檢查所有聲明為 public
和 external
的函數(shù)歇盼,一個個排除用戶濫用它們的可能舔痕,謹防安全漏洞。請記住豹缀,如果這些函數(shù)沒有類似 onlyOwner
這樣的函數(shù)修飾符伯复,用戶能利用各種可能的參數(shù)去調(diào)用它們。
檢查完這個函數(shù)邢笙,用戶就可以直接調(diào)用這個它啸如,并傳入他們所希望的 _targetDna
或 species
。打個游戲還得遵循這么多的規(guī)則氮惯,還能不能愉快地玩耍岸vā!
仔細觀察妇汗,這個函數(shù)只需被 feedOnKitty()
調(diào)用帘不,因此,想要防止漏洞杨箭,最簡單的方法就是設(shè)其可見性為 internal
寞焙。
實戰(zhàn)演練
- 1、目前函數(shù)
feedAndMultiply
可見性為public
。我們將其改為internal
以保障合約安全捣郊。因為我們不希望用戶調(diào)用它的時候塞進一堆亂七八糟的 DNA辽狈。 - 2、
feedAndMultiply
過程需要參考cooldownTime
呛牲。首先刮萌,在找到 myZombie 之后,添加一個require
語句來檢查_isReady()
并將 myZombie 傳遞給它娘扩。這樣用戶必須等到僵尸的 冷卻周期 結(jié)束后才能執(zhí)行feedAndMultiply
功能着茸。 - 3、在函數(shù)結(jié)束時畜侦,調(diào)用
_triggerCooldown(myZombie)
元扔,標明捕獵行為觸發(fā)了僵尸新的冷卻周期。
zombiefeeding.sol
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);
}
// 1\. 使這個函數(shù)的可見性為 internal
function feedAndMultiply(uint _zombieId, uint _targetDna, string species) internal {
require(msg.sender == zombieToOwner[_zombieId]);
Zombie storage myZombie = zombies[_zombieId];
// 2\. 在這里為 `_isReady` 增加一個檢查
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);
// 3\. 調(diào)用 `triggerCooldown`
_triggerCooldown(myZombie);
}
function feedOnKitty(uint _zombieId, uint _kittyId) public {
uint kittyDna;
(,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId);
feedAndMultiply(_zombieId, kittyDna, "kitty");
}
}