1. 程式人生 > >智能合約編程語言-solidity快速入門(下)

智能合約編程語言-solidity快速入門(下)

接口 tex 實現接口 四種 上一個 控制語句 交互 哈希 回退

上一篇:智能合約編程語言-solidity快速入門(上)


solidity區塊及交易屬性

在介紹區塊及交易屬性之前,我們需要先知道solidity中自帶了一些全局變量和函數,這些變量和函數可以認為是solidity提供的API,這些 API 主要表現為Solidity 內置的特殊的變量及函數,它們存在於全局命名空間裏,主要分為以下幾類:

  1. 有關區塊和交易的屬性
  2. ABI編碼函數
  3. 有關錯誤處理
  4. 有關數學及加密功能
  5. 有關地址和合約

我們在編寫智能合約的時候就可以通過這些API來獲取區塊和交易的屬性(Block And Transaction Properties),簡單來說這些API主要用來提供一些區塊鏈當前的信息,下表列出常用的一些API:

API 描述
blockhash(uint blockNumber) returns (bytes32) 返回給定區塊號的哈希值,只支持最近256個區塊,且不包含當前區塊
block.coinbase (address) 獲取當前塊礦工的地址
block.difficulty (uint) 獲取當前塊的難度
block.gaslimit (uint) 獲取當前塊的gaslimit
block.number (uint) 獲取當前區塊的塊號
block.timestamp (uint) 獲取當前塊的Unix時間戳(從1970/1/1 00:00:00 UTC開始所經過的秒數)
gasleft() (uint256) 獲取剩余gas
msg.data (bytes) 獲取完整的調用數據(calldata)
msg.gas (uint) 獲取當前還剩的gas(已棄用)
msg.sender (address) 獲取當前調用發起人的地址
msg.sig (bytes4) 獲取調用數據(calldata)的前四個字節(例如為:函數標識符)
msg.value (uint) 獲取這個消息所附帶的以太幣,單位為wei
now (uint) 獲取當前塊的時間戳(實際上是block.timestamp的別名)
tx.gasprice (uint) 獲取交易的gas價格
tx.origin (address) 獲取交易的發送者(全調用鏈)

註意:

msg的所有成員值,如msg.sender,msg.value的值可以因為每一次外部函數調用,或庫函數調用發生變化(因為msg就是和調用相關的全局變量)。

不應該依據 block.timestamp, now 和 block.blockhash來產生一個隨機數(除非你確實需要這樣做),這幾個值在一定程度上被礦工影響(比如在×××合約裏,不誠實的礦工可能會重試去選擇一個對自己有利的hash)。

對於同一個鏈上連續的區塊來說,當前區塊的時間戳(timestamp)總是會大於上一個區塊的時間戳。為了可擴展性的原因,你只能查最近256個塊,所有其它的將返回0.

接下來使用代碼演示一下常用的全局變量:

pragma solidity ^0.4.17;

contract SolidityAPI {

    function getSender() public constant returns(address) {
        // 獲取當前調用發起人的地址
        return msg.sender;
    }

    function getValue() public constant returns(uint) {
        // 獲取這個消息所附帶的以太幣,單位為wei
        return msg.value;
    }

    function getBlockCoinbase() public constant returns(address) {
        // 獲取當前塊礦工的地址
        return block.coinbase;
    }

    function getBlockDifficulty() public constant returns(uint) {
        // 獲取當前塊的難度
        return block.difficulty;
    }

    function getBlockNumber() public constant returns(uint) {
        // 獲取當前區塊的塊號
        return block.number;
    }

    function getBlockTimestamp() public constant returns(uint) {
        // 獲取當前塊的Unix時間戳
        return block.timestamp;
    }

    function getNow() public constant returns(uint) {
        // 獲取當前塊的時間戳
        return now;
    }

    function getGasprice() public constant returns(uint) {
        // 獲取交易的gas價格
        return tx.gasprice;
    }
}

ABI編碼函數

ABI全稱Application Binary Interface,翻譯過來就是:應用程序二進制接口,是調用智能合約函數以及合約之間函數調用的消息編碼格式定義,也可以理解為智能合約函數調用的接口說明。類似Webservice裏的SOAP協議一樣;也就是定義操作函數簽名,參數編碼,返回結果編碼等。

簡單來說從外部施加給以太坊的行為都稱之為向以太坊網絡提交了一個交易, 調用合約函數其實是向合約地址(賬戶)提交了一個交易,這個交易有一個附加數據,這個附加的數據就是ABI的編碼數據。因此要想和合約交互,就離不開ABI數據。

solidity 提供了以下函數,用來直接得到ABI編碼信息,如下表:

函數 描述
abi.encode(...) returns (bytes) 計算參數的ABI編碼
abi.encodePacked(...) returns (bytes) 計算參數的緊密打包編碼
abi. encodeWithSelector(bytes4 selector, ...) returns (bytes) 計算函數選擇器和參數的ABI編碼
abi.encodeWithSignature(string signature, ...) returns (bytes) 等價於 abi.encodeWithSelector(bytes4(keccak256(signature), ...)

通過ABI編碼函數可以在不用調用函數的情況下,獲得ABI編碼值,下面通過一段代碼來看看這些方式的使用:

pragma solidity ^0.4.24;

contract testABI {
    uint storedData;

    function set(uint x) public {
        storedData = x;
    }

    function abiEncode() public constant returns (bytes) {
            // 計算 1 的ABI編碼
        abi.encode(1);  

                //計算函數set(uint256) 及參數1 的ABI 編碼
        return abi.encodeWithSignature("set(uint256)", 1); 
    }
}

solidity錯誤處理

在很多編程語言中都具有錯誤處理機制,在solidity中自然也不例外,solidity最開始的錯誤處理方式是使用throw以及if … throw,後來因為這種方式會消耗掉所有剩余的gas,所以目前throw的方式已經被棄用,改為使用以下函數進行錯誤處理:
函數 描述
assert(bool condition) 用於判斷內部錯誤,條件不滿足時拋出異常
require(bool condition) 用於判斷輸入或外部組件錯誤,條件不滿足時拋出異常
require(bool condition, string message) 同上,多了一個錯誤信息
revert() 終止執行並還原改變的狀態
revert(string reason) 同上,提供一個錯誤信息

solidity中的錯誤處理機制和其他大多數編程語言不一樣,solidity是通過回退狀態來進行錯誤處理的,就像數據庫事務一樣,也就是說solidity沒有try-catch這種捕獲異常的方式。在發生異常時solidity會撤銷當前調用(及其所有子調用)所改變的狀態,同時給調用者返回一個錯誤標識。但是消耗的gas不會回退,會正常消耗掉。

solidity之所以使用這種方式處理錯誤,是因為區塊鏈就類似於全球共享的分布式事務性數據庫(公鏈)。全球共享意味著參與這個網絡的每一個人都可以讀寫其中的數據,如果沒有這種事務一般的錯誤處理機制就會導致一些操作成功一些操作失敗,所帶來的結果就是數據的混亂、不一致。所以使用這種事務一般的錯誤處理機制可以保證一組調用及其子調用要麽成功要麽失敗回滾,就像啥事都沒有發生一樣,solidity錯誤處理就是要保證每次調用都是具有事務性的。


大概了解了solidity的錯誤處理機制後,我們來看看如何在solidity中進行錯誤處理。從上表中可以看到solidity提供了兩個函數assert和require來進行條件檢查,如果條件不滿足則拋出異常。assert函數通常用來檢查(測試)內部錯誤,而require函數來檢查輸入變量或合同狀態變量是否滿足條件以及驗證調用外部合約返回值。

另外,如果我們正確使用assert,使用一些solidity分析工具就可以幫我們分析出智能合約中的錯誤,幫助我們發現合約中有邏輯錯誤的bug。

assert和require兩個函數實際上也就對應著兩種類型的異常 ,即assert類型異常及require類型異常。當發生assert類型異常時,會消耗掉所有提供的gas,而require類型異常則不會消耗。當發生require類型的異常時,Solidity會執行一個回退操作(指令0xfd)。當發生assert類型的異常時,Solidity會執行一個無效操作(指令0xfe)。

在上述的兩種情況下,EVM都會撤回所有的狀態改變。是因為期望的結果沒有發生,就沒法繼續安全執行。必須保證交易的原子性(一致性,要麽全部執行,要麽一點改變都沒有,不能只改變一部分),所以需要撤銷所有操作,讓整個交易沒有任何影響。

自動產生assert類型異常的場景:

  1. 如果越界,或負的序號值訪問數組,如i >= x.length 或 i < 0時訪問x[i]
  2. 如果序號越界,或負的序號值時訪問一個定長的bytesN。
  3. 被除數為0, 如5/0 或 23 % 0。
  4. 對一個二進制移動一個負的值。如:5<<i; i為-1時。
  5. 整數進行可以顯式轉換為枚舉時,如果將過大值,負值轉為枚舉類型則拋出異常
  6. 如果調用未初始化內部函數類型的變量。
  7. 如果調用assert的參數為false

自動產生require類型異常的場景:

  1. 調用throw
  2. 如果調用require的參數為false
  3. 如果你通過消息調用一個函數,但在調用的過程中,並沒有正確結束(gas不足,沒有匹配到對應的函數,或被調用的函數出現異常)。底層操作如call,send,delegatecall或callcode除外,它們不會拋出異常,但它們會通過返回false來表示失敗。
  4. 如果在使用new創建一個新合約時出現第3條的原因沒有正常完成。
  5. 如果調用外部函數調用時,被調用的對象不包含代碼。
  6. 如果合約沒有payable修飾符的public的函數在接收以太幣時(包括構造函數,和回退函數)。
  7. 如果合約通過一個public的getter函數(public getter funciton)接收以太幣。
  8. 如果.transfer()執行失敗

除了可以兩個函數assert和require來進行條件檢查,另外還有兩種方式來觸發異常:

  • revert函數可以用來標記錯誤並回退當前調用
  • 使用throw關鍵字拋出異常(從0.4.13版本,throw關鍵字已被棄用,將來會被淘汰。)

當子調用中發生異常時,異常會自動向上“冒泡”。 不過也有一些例外:send,和底層的函數調用call, delegatecall,callcode,當發生異常時,這些函數返回false。

註意:在一個不存在的地址上調用底層的函數call,delegatecall,callcode 也會返回成功,所以我們在進行調用時,應該總是優先進行函數存在性檢查。

在下面通過一個示例來說明如何使用require來檢查輸入條件,代碼中使用了require函數檢查msg.value的值是否為偶數,此時我們設置value值為2,可以正常的運行sendHalf函數:
技術分享圖片

詳細的日誌如下:
技術分享圖片

接著我們測試異常的情況,將value改成1,即不能被2整除的數,執行sendHalf函數後,控制臺輸出的錯誤日誌如下,從錯誤日誌中我們可以看到此次交易被reverted到一個初始的狀態:
技術分享圖片

然後我們再來看一個示例,使用assert函數檢查內部錯誤:

pragma solidity ^0.4.20;

contract Sharer {
    function sendHalf(address addr) public payable returns(uint balance){
        // 僅允許偶數
        require(msg.value % 2 == 0); 
        uint balanceBeforeTransfer = this.balance;

        addr.transfer(msg.value / 2);
        // 檢查當前的balance是否為轉移之前的一半,不符合條件則會拋出異常
        assert(this.balance == balanceBeforeTransfer - msg.value / 2);
        return this.balance;
    }
}

solidity 函數參數

本小節我們來介紹一下solidity中的函數參數,與其他編程語言一樣,solidity 函數可以提供參數作為輸入並且函數類型本身也可以作為參數,與JavaScript和C不同的是,solidity還可以返回任意數量的返回值作為輸出。

1.輸入參數,輸入參數的聲明方式與變量相同, 未使用的參數可以省略變量名稱。假設我們希望合約中的某個函數被外部調用時,傳入兩個整型參數,那麽就可以這樣寫:

pragma solidity ^0.4.16;

contract Test {
    function inputParam(uint a, uint b) public {
        // ...
    }
}

2.輸出參數,輸出參數的聲明和輸入參數一樣,只不過它接在returns之後,也就是函數的返回值,只不過在solidity中函數的返回值可以像輸入參數一樣被處理。假設我們希望返回兩個結果,兩個給定整數的和以及積,可以這樣寫:

pragma solidity ^0.4.16;

contract Test {
    function testOutput(uint a, uint b) public returns (uint sum, uint mul) {
        sum = a + b;
        mul = a * b;
    }
}

可以省略輸出參數的名稱,也可以使用return語句指定輸出值,return可以返回多個值。(當返回一個沒有賦值的參數時,默認為0)

輸入參數和輸出參數可以在函數內表達式中使用,也可以作為被賦值的對象, 如下示例:

contract Test {
    function testOutput(uint a, uint b) public returns (uint c) {
        a = 1;
        b = 2;
        c = 3;
    }
}

3.命名參數,調用某個函數時傳遞的參數,可以通過指定名稱的方式傳遞,使用花括號{}包起來,參數順序任意,但參數的類型和數量要與定義一致,這與Python中的關鍵字參數一樣的。如:

pragma solidity ^0.4.0;

contract Test {
    function a(uint key, uint value) public {
        // ...
    }

    function b() public {
        // 命名參數
        a({value: 2, key: 3});
    }
}

4.參數解構,當一個函數有多個輸出參數時,可以使用元組(tuple)來返回多個值。元組(tuple)是一個數量固定,類型可以不同的元素組成的一個列表(用小括號表示),使用return (v0, v1, …, vn) 語句,就可以返回多個值,返回值的數量需要和輸出參數聲明的數量一致。當函數返回多個值時,可以使用多個變量去接收,此時元組內的元素就會同時賦值給多個變量,這個過程就稱之為參數解構。如下示例:

function a() public pure returns (uint, bool, uint) {
    // 使用元組返回多個值
    return (7, true, 2);
}

function b() public {
    uint x;
    bool y;
    uint z;

    // 使用元組給多個變量賦值
    (x, y , z)  = a();
}

solidity 流程控制語句

solidity 的流程控制語句與其他大多數語言一致,擁有if、else、while、do、for、break、continue、return以及三元表達式 ? :等流程控制語句,這些語句在solidity中的含義與其他語言是一致的這裏就不再詳細贅述了,不過要註意的是solidity中沒有switch和goto語句。

以下使用一個簡單的例子演示一下這些流程控制語句的使用方式,代碼如下:

pragma solidity ^0.4.20;

contract Test {
    function testWhile() public constant returns(uint){
        uint i = 0;
        uint sumOfAdd = 0;

        while(true) {
            i++;

            if (i > 10){
                break;
            }

            if (i % 2 == 0) {
                continue;
            } else {
                sumOfAdd += i;
            }
        }

        sumOfAdd = sumOfAdd > 20 ? sumOfAdd + 10 : sumOfAdd;

        return sumOfAdd;
    }

    function testForLoop() public constant returns(uint) {
        uint sum = 0;
        for (uint i = 0; i < 10; i++) {
            sum +=i;
        }

        return sum;
    }
}

solidity 權限修飾符

大多數的語言都會有權限修飾符,盡管它們都不盡相同,在 solidity 中有public、private、external以及internal四種權限修飾符,接下來我們看看四種權限修飾符的作用。

1.public

public所修飾的函數稱為公開函數,是合約接口的一部分,可以通過內部,或者消息來進行調用。對於public類型的狀態變量,會自動創建一個訪問器,這個訪問器其實是一個函數。solidity 中的函數默認是public的

我們來看一個公開函數的例子,在remix上我們可以看到並執行公開的函數:
技術分享圖片


2.private

表示私有的函數和狀態變量,僅在當前合約中可以訪問,在繼承的合約內不可以訪問,也不可以被外部訪問

例如我們來寫一個私有函數,並且進行部署,此時會發現在外部是看不到這個函數的:
技術分享圖片


3.external

表示外部函數,與public修飾的函數有些類似,也是合約接口的一部分,但只能使用消息調用,不可以直接通過內部調用,值得註意的是external函數消耗的gas比public函數要少,所以當我們一個函數只能被外部調用時盡量使用external修飾

同樣的,我們來看一個簡單的例子,代碼如下:
技術分享圖片


4.internal

使用此修飾符修飾的函數和狀態變量只能通過內部訪問,例如在當前合約中調用,或繼承的合約中調用。solidity 中的狀態變量默認是internal的

如下示例:
技術分享圖片


solidity 函數調用

在上一小節中,我們介紹了 solidity 中的權限修飾符,其中涉及到了內部函數調用和外部函數調用的概念,所以這一節我們進一步介紹這兩個概念。

1.內部函數調用(Internal Function Calls)

內部調用,不會創建一個EVM消息調用。而是直接調用當前合約的函數,也可以遞歸調用。

如下面這個的例子:

pragma solidity ^0.4.20;

contract Test {
    function a(uint a) public pure returns (uint ret) {
       // 直接調用
       return b();
    }

    function b() internal pure returns (uint ret) {
       // 直接調用及遞歸調用
       return a(7) + b();    
    }
}

這些函數調用被轉換為EVM內部的簡單指令跳轉(jumps)。 這樣帶來的一個好處是,當前的內存不會被回收。在一個內部調用時傳遞一個內存型引用效率將非常高的。當然,僅僅是同一個合約的函數之間才可通過內部的方式進行調用。


2.外部函數調用(External Function Calls)

外部調用,會創建EVM消息調用。表達式this.sum(8);number.add(2);(這裏的number是一個合約實例)是外部調用函數的方式,它會發起一個消息調用,而不是EVM的指令跳轉。需要註意的是,在合約的構造器中,不能使用this調用函數,因為當前合約還沒有創建完成

其它合約的函數必須通過外部的方式調用。對於一個外部調用,所有函數的參數必須要拷貝到內存中。當調用其它合約的函數時,可以通過選項.value(),和.gas()來分別指定要發送的以太幣(以wei為單位)和gas值。如下示例:

pragma solidity ^0.4.20;

contract InfoFeed {
    // 必須使用`payable`關鍵字修飾,否則不能通過`value()`函數來接收以太幣
    function info() public payable returns (uint ret) { 
        return 42; 
    }
}

contract Consumer {
    InfoFeed feed;

    function setFeed(address addr) public {
      // 這句代碼進行了一個顯示的類型轉換,表示給定的地址是合約`InfoFeed`類型,這裏並不會執行構造器的初始化。
      // 在進行顯式的類型強制轉換時需要非常小心,不要調用一個未知類型的合約函數
      feed = InfoFeed(addr);
    }

    function callFeed() public {
      // 附加以太幣及gas來調用info,註意這裏僅僅是對發送的以太幣和gas值進行了設置,真正的調用是後面的括號()
      feed.info.value(10).gas(800)();
    }
}

註:調用callFeed時,需要預先存入一定量的以太幣,不然可能會因余額不足報錯。

在與外部合約交互時需要註意的事項:

如果我們不知道被調用的合約源代碼,那麽和這些合約的交互就會有潛在的風險,即便被調用的合約繼承自一個已知的父合約(因為繼承僅僅要求正確實現接口,而不關註實現的內容)。因為和這些合約交互時,就相當於把自己控制權交給被調用的合約,對方幾乎可以利用它做任何事。此外, 被調用的合約可以改變調用合約的狀態變量,所以在編寫函數時需要註意可重入性漏洞問題


solidity 函數

solidity 有以下四種函數:

  • 構造函數
  • 視圖函數(constant / view)
  • 純函數(pure)
  • 回退函數

1.構造函數:

構造函數在合約創建的時候運行,我們通常會在構造函數做一些初始化的操作,構造函數也是可以有參數的

如下示例:
技術分享圖片


2.視圖函數(constant / view):

使用 constant 或者 view 關鍵字修飾的函數就是視圖函數,視圖函數不會修改合約的狀態變量。constant 與 view 是等價的,constant 是view 的別名,constant在計劃Solidity 0.5.0版本之後會棄用(constant這個詞有歧義,view 也更能表達返回值可視),所以在新版的solidity中推薦優先使用view

視圖函數有個特點就是在remix執行後可以直接看到返回值:
技術分享圖片

一個函數如果它不修改狀態變量,應該聲明為視圖函數,以下幾種情況被認為修改了狀態變量:

  • 寫狀態變量
  • 觸發事件(events)
  • 創建其他的合約
  • call調用附加了以太幣
  • 調用了任何沒有view或pure修飾的函數
  • 使用了低級別的調用(low-level calls)
  • 使用了包含特定操作符的內聯匯編

3.純函數(pure):

純函數是使用 pure 關鍵字修飾的函數,純函數不會讀取狀態變量,也不會修改狀態變量

如下示例:
技術分享圖片

以下幾種情況被認為是讀取了狀態:

  • 讀狀態變量
  • 訪問了 this.balance
  • 訪問了block、tx、msg 的成員 (msg.sig 和 msg.data除外)
  • 調用了任何沒有pure修飾的函數
  • 使用了包含特定操作符的內聯匯編

4.回退函數:

回退函數實際上是一個匿名函數,並且是一個只能被動調用的函數,一個合約中只能有一個回退函數。通常當我們的一個智能合約需要接收以太幣的時,就需要實現回退函數,而且回退函數的實現應該盡量的簡單

如下示例:
技術分享圖片

如果沒有實現回退函數,其他合約是無法往該合約發送以太幣的:
技術分享圖片

回退函數會在以下情況被調用:

  • 發送以太幣
  • 被外部調用了一個不存在的函數

智能合約編程語言-solidity快速入門(下)