接上篇文章,這裡繼續學習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
的新函式。它有一個名為_owner
的address
型別的引數。 - 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、宣告一個名為
result
的uint [] 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 <zombies.length。它將遍歷陣列中的每一頭殭屍。
- 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
,例如在第三課學習的:onlyOwner
和aboveLevel
。 對於這些修飾符我們可以自定義其對函式的約束邏輯。
這些修飾符可以同時作用於一個函式定義上:
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
合約中的 owner
和 onlyOwner
,假定它已經被引入了。
你可以通過 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、
import
自zombiehelper.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、給我們合約一個名為
randNonce
的uint
,將其值設定為 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 {
}
}