以太坊開發實戰學習-高級Solidity理論 (五)

接上篇 文章,這裏繼續學習Solidity高級理論。

1、深刻函數修飾符

接下來,咱們將添加一些輔助方法。咱們爲您建立了一個名爲 zombiehelper.sol 的新文件,而且將 zombiefeeding.sol 導入其中,這讓咱們的代碼更整潔。前端

咱們打算讓殭屍在達到必定水平後,得到特殊能力。可是達到這個小目標,咱們還須要學一學什麼是「函數修飾符」。web

帶參的函數修飾符

以前咱們已經讀過一個簡單的函數修飾符了: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 把參數傳遞給它的修飾符的。segmentfault

來,咱們本身生產一個修飾符,經過傳入的level參數來限制殭屍使用某些特殊功能。數組

實戰演練

  • 一、在ZombieHelper 中,建立一個名爲 aboveLevel 的modifier,它接收2個參數, _level (uint類型) 以及 _zombieId (uint類型)。
  • 二、運用函數邏輯確保殭屍 zombies[_zombieId].level 大於或等於 _level。
  • 三、記住,修飾符的最後一行爲 _;表示修飾符調用結束後返回,並執行調用函數餘下的部分
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) {
  // 其他的程序邏輯
}

實戰演練

  • 一、建立一個名爲 changeName 的函數。它接收2個參數:_zombieId(uint類型)以及 _newName(string類型),可見性爲 external。它帶有一個 aboveLevel 修飾符,調用的時候經過 _level 參數傳入2, 固然,別忘了同時傳 _zombieId 參數。
  • 二、在這個函數中,首先咱們用 require 語句,驗證 msg.sender 是否就是 zombieToOwner [_zombieId]
  • 三、而後函數將 zombies[_zombieId] .name 設置爲 _newName
  • 四、在 changeName 下建立另外一個名爲 changeDna 的函數。它的定義和內容幾乎和 changeName 相同,不過它第二個參數是 _newDna(uint類型),在修飾符 aboveLevel 的 _level 參數中傳遞 20 。如今,他能夠把殭屍的 dna 設置爲 _newDna 了。

zombiehelper.soloracle

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;

  }

}

2、利用view節省Gas

如今須要添加的一個功能是:咱們的 DApp 須要一個方法來查看某玩家的整個殭屍軍團 - 咱們稱之爲 getZombiesByOwnerapp

實現這個功能只需從區塊鏈中讀取數據,因此它能夠是一個 view 函數。這讓咱們不得不回顧一下「gas優化」這個重要話題。

「view」 函數不花 「gas」

當玩家從外部調用一個view函數,是不須要支付一分 gas 的。

這是由於 view 函數不會真正改變區塊鏈上的任何數據 - 它們只是讀取。所以用 view 標記一個函數,意味着告訴 web3.js運行這個函數只須要查詢你的本地以太坊節點,而不須要在區塊鏈上建立一個事務(事務須要運行在每一個節點上,所以花費 gas)

稍後咱們將介紹如何在本身的節點上設置 web3.js。但如今,你關鍵是要記住,在所能只讀的函數上標記上表示「只讀」的external view 聲明,就能爲你的玩家減小在 DApp 中 gas 用量。

注意:若是一個 view 函數在另外一個函數的內部被調用,而調用函數與 view 函數的不屬於同一個合約,也會產生調用成本。這是由於若是主調函數在以太坊建立了一個事務,它仍然須要逐個節點去驗證。因此標記爲 view 的函數只有在外部調用時纔是免費的。

實戰演練

咱們來寫一個」返回某玩家的整個殭屍軍團「的函數。當咱們從 web3.js 中調用它,便可顯示某一玩家的我的資料頁。

這個函數的邏輯有點複雜,咱們須要好幾個章節來描述它的實現。

  • 一、建立一個名爲 getZombiesByOwner 的新函數。它有一個名爲 _owneraddress 類型的參數。
  • 二、將其申明爲 external view 函數,這樣當玩家從 web3.js 中調用它時,不須要花費任何 gas。
  • 三、函數須要返回一個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 []) {

  }

}

3、存儲很是昂貴

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 []數組的形式返回某一用戶所擁有的全部殭屍。

  • 一、聲明一個名爲resultuint [] memory (內存變量數組)
  • 二、將其設置爲一個新的 uint 類型數組。數組的長度爲該 _owner 所擁有的殭屍數量,這可經過調用 ownerZombieCount [_ owner] 來獲取。
  • 三、函數結束,返回 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;
  }

}

4、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;
  }

}

5、可支付

截至目前,咱們只接觸到不多的 函數修飾符。 要記住全部的東西很難,因此咱們來個概覽:

  • 一、咱們有決定函數什麼時候和被誰調用的可見性修飾符: private 意味着它只能被合約內部調用internal 就像 private 可是也能被繼承的合約調用external 只能從合約外部調用;最後 public 能夠在任何地方調用,無論是內部仍是外部
  • 二、咱們也有狀態修飾符, 告訴咱們函數如何和區塊鏈交互: view 告訴咱們運行這個函數不會更改和保存任何數據; pure 告訴咱們這個函數不但不會往區塊鏈寫數據,它甚至不從區塊鏈讀取數據。這兩種在被從合約外部調用的時候都不花費任何gas(可是它們在被內部其餘函數調用的時候將會耗費gas)。
  • 三、而後咱們有了自定義的 modifiers,例如在第三課學習的: onlyOwneraboveLevel。 對於這些修飾符咱們能夠自定義其對函數的約束邏輯。

這些修飾符能夠同時做用於一個函數定義上:

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將存儲在你擁有的合約中 —— 一個簡單明瞭的例子,向你展現你能夠經過本身的遊戲賺錢。

  • 一、定義一個 uint ,命名爲 levelUpFee, 將值設定爲 0.001 ether
  • 二、定義一個名爲 levelUp 的函數。 它將接收一個 uint 參數 _zombieId。 函數應該修飾爲 external 以及 payable
  • 三、這個函數首先應該 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;
  }

}

6、提現

在上一節,咱們學習瞭如何向合約發送以太,那麼在發送以後會發生什麼呢?

在你發送以太以後,它將被存儲進以合約的以太坊帳戶中, 並凍結在哪裏 —— 除非你添加一個函數來從合約中把以太提現。

你能夠寫一個函數來從合約中提現以太,相似這樣:

contract GetPaid is Ownable {
  function withdraw() external onlyOwner {
    owner.transfer(this.balance);
  }
}

注意咱們使用 Ownable 合約中的 owneronlyOwner,假定它已經被引入了。

你能夠經過 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)

有不少例子來展現什麼讓以太坊編程如此之酷 —— 你能夠擁有一個不被任何人控制的去中心化市場。

實戰演練

  • 一、在咱們的合約裏建立一個 withdraw 函數,它應該幾乎和上面的GetPaid同樣。
  • 二、以太的價格在過去幾年內翻了十幾倍,在咱們寫這個教程的時候 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;
  }

}

7、綜合應用

咱們新建一個攻擊功能合約,並將代碼放進新的文件中,引入上一個合約。

再來新建一個合約吧。熟能生巧。

若是你不記得怎麼作了, 查看一下 zombiehelper.sol — 不過最好先試着作一下,檢查一下你掌握的狀況。

  • 一、在文件開頭定義 Solidity 的版本 ^0.4.19.
  • 二、importzombiehelper.sol .
  • 三、聲明一個新的 contract,命名爲 ZombieBattle, 繼承自ZombieHelper。函數體就先空着吧。

zombiebattle.sol

pragma solidity ^0.4.19;
import "./zombiehelper.sol";

contract ZombieBattle is ZombieHelper {
  
}

8、隨機數

優秀的遊戲都須要一些隨機元素,那麼咱們在 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個億), 那就很值得去攻擊了。

因此儘管這個方法在以太坊上不安全,在實際中,除非咱們的隨機函數有一大筆錢在上面,你遊戲的用戶通常是沒有足夠的資源去攻擊的。

由於在這個教程中,咱們只是在編寫一個簡單的遊戲來作演示,也沒有真正的錢在裏面,因此咱們決定接受這個不足之處,使用這個簡單的隨機數生成函數。可是要謹記它是不安全的。

實戰演練

咱們來實現一個隨機數生成函數,好來計算戰鬥的結果。雖然這個函數一點兒也不安全。

  • 一、給咱們合約一個名爲 randNonceuint,將其值設置爲 0。
  • 二、創建一個函數,命名爲 randMod (random-modulus)。它將做爲internal 函數,傳入一個名爲 _modulus的 uint,並 returns 一個 uint
  • 三、這個函數首先將爲 randNonce加一, (使用 randNonce++ 語句)。
  • 四、最後,它應該 (在一行代碼中) 計算 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;

  }
}

9、遊戲對戰

咱們的合約已經有了一些隨機性的來源,能夠用進咱們的殭屍戰鬥中去計算結果。

咱們的殭屍戰鬥看起來將是這個流程:

  • 你選擇一個本身的殭屍,而後選擇一個對手的殭屍去攻擊。
  • 若是你是攻擊方,你將有70%的概率獲勝,防守方將有30%的概率獲勝。
  • 全部的殭屍(攻守雙方)都將有一個 winCount 和一個 lossCount,這兩個值都將根據戰鬥結果增加。
  • 若攻擊方獲勝,這個殭屍將升級併產生一個新殭屍。
  • 若是攻擊方失敗,除了失敗次數將加一外,什麼都不會發生。
  • 不管輸贏,當前殭屍的冷卻時間都將被激活。

這有一大堆的邏輯須要處理,咱們將把這些步驟分解到接下來的課程中去。

實戰演練

  • 一、給咱們合約一個 uint 類型的變量,命名爲 attackVictoryProbability, 將其值設定爲 70。
  • 二、建立一個名爲 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 {
    
  }
}
相關文章
相關標籤/搜索