您好,登錄后才能下訂單哦!
上一篇:智能合約編程語言-solidity快速入門(上)
在介紹區塊及交易屬性之前,我們需要先知道solidity中自帶了一些全局變量和函數,這些變量和函數可以認為是solidity提供的API,這些 API 主要表現為Solidity 內置的特殊的變量及函數,它們存在于全局命名空間里,主要分為以下幾類:
我們在編寫智能合約的時候就可以通過這些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全稱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最開始的錯誤處理方式是使用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類型異常的場景:
自動產生require類型異常的場景:
除了可以兩個函數assert和require來進行條件檢查,另外還有兩種方式來觸發異常:
當子調用中發生異常時,異常會自動向上“冒泡”。 不過也有一些例外: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 函數可以提供參數作為輸入并且函數類型本身也可以作為參數,與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 的流程控制語句與其他大多數語言一致,擁有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 中有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 中的權限修飾符,其中涉及到了內部函數調用和外部函數調用的概念,所以這一節我們進一步介紹這兩個概念。
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 有以下四種函數:
1.構造函數:
構造函數在合約創建的時候運行,我們通常會在構造函數做一些初始化的操作,構造函數也是可以有參數的
如下示例:
2.視圖函數(constant / view):
使用 constant 或者 view 關鍵字修飾的函數就是視圖函數,視圖函數不會修改合約的狀態變量。constant 與 view 是等價的,constant 是view 的別名,constant在計劃Solidity 0.5.0版本之后會棄用(constant這個詞有歧義,view 也更能表達返回值可視),所以在新版的solidity中推薦優先使用view
視圖函數有個特點就是在remix執行后可以直接看到返回值:
一個函數如果它不修改狀態變量,應該聲明為視圖函數,以下幾種情況被認為修改了狀態變量:
3.純函數(pure):
純函數是使用 pure 關鍵字修飾的函數,純函數不會讀取狀態變量,也不會修改狀態變量
如下示例:
以下幾種情況被認為是讀取了狀態:
4.回退函數:
回退函數實際上是一個匿名函數,并且是一個只能被動調用的函數,一個合約中只能有一個回退函數。通常當我們的一個智能合約需要接收以太幣的時,就需要實現回退函數,而且回退函數的實現應該盡量的簡單
如下示例:
如果沒有實現回退函數,其他合約是無法往該合約發送以太幣的:
回退函數會在以下情況被調用:
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。