智能合約編寫之Solidity的基礎(chǔ)特性
如前篇介紹,目前大部分的聯(lián)盟鏈平臺(tái),包括 FISCO BCOS,都采用 Solidity 作為智能合約開發(fā)語言,因此熟悉并上手 Solidity 十分必要。
作為一門面向區(qū)塊鏈平臺(tái)設(shè)計(jì)的圖靈完備的編程語言,Solidity 支持函數(shù)調(diào)用、修飾符、重載、事件、繼承等多種特性,在區(qū)塊鏈社區(qū)中,擁有廣泛的影響力和踴躍的社區(qū)支持。但對(duì)于剛接觸區(qū)塊鏈的人而言,Solidity 是一門陌生的語言。
智能合約編寫階段將從 Solidity 基礎(chǔ)特性、高級(jí)特性、設(shè)計(jì)模式以及編程攻略分別展開,帶讀者認(rèn)識(shí) Solidity 并掌握其運(yùn)用,更好地進(jìn)行智能合約開發(fā)。
本篇將圍繞 Solidity 的基礎(chǔ)特性,帶大家上手開發(fā)一個(gè)最基本的智能合約。
智能合約代碼結(jié)構(gòu)
任何編程語言都有其規(guī)范的代碼結(jié)構(gòu),用于表達(dá)在一個(gè)代碼文件中如何組織和編寫代碼,Solidity 也一樣。
本節(jié),我們將通過一個(gè)簡單的合約示例,來了解智能合約的代碼結(jié)構(gòu)。
pragma solidity ^0.4.25;
contract Sample{
//State variables
address private _admin;
uint private _state;
//Modifier
modifier onlyAdmin(){
require(msg.sender == _admin, "You are not admin");
_;
}
//Events
event SetState(uint value);
//Constructor
constructor() public{
_admin = msg.sender;
}
//Functions
function setState(uint value) public onlyAdmin{
_state = value;
emit SetState(value);
}
function getValue() public view returns (uint){
return _state;
}
}
上面這段程序包括了以下功能:
-
通過構(gòu)造函數(shù)來部署合約
-
通過
setValue
函數(shù)設(shè)置合約狀態(tài) -
通過
getValue
函數(shù)查詢合約狀態(tài)
整個(gè)合約主要分為以下幾個(gè)構(gòu)成部分:
-
狀態(tài)變量 -
_admin
、_state
,這些變量會(huì)被永久保存,也可以被函數(shù)修改 -
構(gòu)造函數(shù) - 用于部署并初始化合約
-
事件 -
SetState
, 功能類似日志,記錄了一個(gè)事件的發(fā)生 -
修飾符 -
onlyAdmin
, 用于給函數(shù)加一層“外衣” -
函數(shù) -
setState
、getState
,用于讀寫狀態(tài)變量
下面將逐一介紹上述構(gòu)成部分。
狀態(tài)變量
狀態(tài)變量是合約的骨髓,它記錄了合約的業(yè)務(wù)信息。用戶可以通過函數(shù)來修改這些狀態(tài)變量,這些修改也會(huì)被包含到交易中;交易經(jīng)過區(qū)塊鏈網(wǎng)絡(luò)確認(rèn)后,修改即為生效。
uint private _state;
狀態(tài)變量的聲明方式為:[類型] [訪問修飾符-可選] [字段名]
。
構(gòu)造函數(shù)
構(gòu)造函數(shù)用于初始化合約,它允許用戶傳入一些基本的數(shù)據(jù),寫入到狀態(tài)變量中。
在上述例子中,設(shè)置了 _admin
字段,作為后面演示其他功能的前提。
constructor() public{
_admin = msg.sender;
}
和 Java 不同的是,構(gòu)造函數(shù)不支持重載,只能指定一個(gè)構(gòu)造函數(shù)。
函數(shù)
函數(shù)被用來讀寫狀態(tài)變量。對(duì)變量的修改將會(huì)被包含在交易中,經(jīng)區(qū)塊鏈網(wǎng)絡(luò)確認(rèn)后才生效。生效后,修改會(huì)被永久的保存在區(qū)塊鏈賬本中。
函數(shù)簽名定義了函數(shù)名、輸入輸出參數(shù)、訪問修飾符、自定義修飾符。
function setState(uint value) public onlyAdmin;
函數(shù)還可以返回多個(gè)返回值:
function functionSample() public view returns(uint, uint){
return (1,2);
}
在本合約中,還有一個(gè)配備了 view
修飾符的函數(shù)。這個(gè) view
表示了該函數(shù)不會(huì)修改任何狀態(tài)變量。
與 view
類似的還有修飾符 pure
,其表明該函數(shù)是純函數(shù),連狀態(tài)變量都不用讀,函數(shù)的運(yùn)行僅僅依賴于參數(shù)。
function add(uint a, uint b) public pure returns(uint){
return a+b;
}
如果在 view
函數(shù)中嘗試修改狀態(tài)變量,或者在 pure
函數(shù)中訪問狀態(tài)變量,編譯器均會(huì)報(bào)錯(cuò)。
事件
事件類似于日志,會(huì)被記錄到區(qū)塊鏈中,客戶端可以通過 web3 訂閱這些事件。
定義事件:
event SetState(uint value);
構(gòu)造事件:
emit SetState(value);
這里有幾點(diǎn)需要注意:
-
事件的名稱可以任意指定,不一定要和函數(shù)名掛鉤,但推薦兩者掛鉤,以便清晰地表達(dá)發(fā)生的事情。
-
構(gòu)造事件時(shí),也可不寫
emit
,但因?yàn)槭录秃瘮?shù)無論是名稱還是參數(shù)都高度相關(guān),這樣操作很容易筆誤將事件寫成函數(shù)調(diào)用,因此不推薦不寫。function setState(uint value) public onlyAdmin{
_state = value;
emit SetState(value);
// 下面這樣寫也可以,但不推薦,因?yàn)楹苋菀坠P誤寫成 setState
// SetState(value);
}
-
Solidity 編程風(fēng)格應(yīng)采用一定的規(guī)范。關(guān)于編程風(fēng)格,建議參考:https://learnblockchain.cn/docs/solidity/style-guide.html#id16
修飾符
修飾符是合約中非常重要的一環(huán)。它掛在函數(shù)聲明上,為函數(shù)提供一些額外的功能,例如檢查、清理等工作。
在本例中,修飾符 onlyAdmin
要求函數(shù)調(diào)用前,需要先檢測函數(shù)的調(diào)用者是否為函數(shù)部署時(shí)設(shè)定的那個(gè)管理員(即合約的部署人)。
//Modifer
modifier onlyAdmin(){
require(msg.sender == _admin, "You are not admin");
_;
}
...
//Functions
function setState(uint value) public onlyAdmin{
...
}
值得注意的是,定義在修飾符中的下劃線 “_
”,表示函數(shù)的調(diào)用,指代的是開發(fā)者用修飾符修飾的函數(shù)。在本例中,表達(dá)的是 setState
函數(shù)調(diào)用的意思。
智能合約的運(yùn)行
了解了上述的智能合約示例的結(jié)構(gòu),就可以直接上手運(yùn)行,運(yùn)行合約的方式有多種,大家可以任意采取其中一種:
-
方法一:可以使用 FISCO BCOS 控制臺(tái)的方式來部署合約,具體請(qǐng)參考:https://fisco-bcos-documentation.readthedocs.io/zh_CN/latest/docs/installation.html#id7
-
方法二:使用 FISCO BCOS 開源項(xiàng)目 WeBASE 提供的在線 ide WEBASE-front 運(yùn)行
-
方法三:通過在線 ide remix 來進(jìn)行合約的部署與運(yùn)行,remix 的地址為:http://remix.ethereum.org/
本例中使用 remix 作為運(yùn)行示例。
編譯
首先,在 remix 的在線 ide 中鍵入代碼后,通過編譯按鈕來編譯。成功后會(huì)在按鈕上出現(xiàn)一個(gè)綠色對(duì)勾:
部署
編譯成功后就可進(jìn)行部署環(huán)節(jié),部署成功后會(huì)出現(xiàn)合約實(shí)例。
setState
合約部署后,我們來調(diào)用 setState(4)
。在執(zhí)行成功后,會(huì)產(chǎn)生一條交易收據(jù),里面包含了交易的執(zhí)行信息。
在這里,用戶可以看到交易執(zhí)行狀態(tài)(status)、交易執(zhí)行人(from)、交易輸入輸出(decoded input、decoded output)、交易開銷(execution cost)以及交易日志(logs)。
在交易日志中,我們看到 SetState
事件被拋出,里面的參數(shù)也記錄了事件傳入的值 4
。
如果我們換一個(gè)賬戶來執(zhí)行,那么調(diào)用會(huì)失敗,因?yàn)?onlyAdmin
修飾符會(huì)阻止用戶調(diào)用。
getState
調(diào)用 getState
后,可以直接看到所得到的值為 4
,正好是我們先前 setState
所傳入的值:
Solidity 數(shù)據(jù)類型
在前文的示例中,我們用到了 uint
等數(shù)據(jù)類型。由于 Solidity 類型設(shè)計(jì)比較特殊,這里也會(huì)簡單介紹一下 Solidity 的數(shù)據(jù)類型。
整型系列
Solidity 提供了一組數(shù)據(jù)類型來表示整數(shù), 包含無符號(hào)整數(shù)與有符號(hào)整數(shù)。每類整數(shù)還可根據(jù)長度細(xì)分,具體細(xì)分類型如下。
類型 |
長度(位) |
有符號(hào) |
uint |
256 |
否 |
uint8 |
8 |
否 |
uint16 |
16 |
否 |
... |
... |
否 |
uint256 |
256 |
否 |
int |
256 |
是 |
int8 |
8 |
是 |
int16 |
16 |
是 |
... |
... |
是 |
int256 |
256 |
是 |
定長字節(jié)系列
Solidity 提供了 bytes1
到 bytes32
的類型,它們是固定長度的字節(jié)數(shù)組。
用戶可以讀取定長字節(jié)的內(nèi)容。
function bytesSample() public{
bytes32 barray;
//Initialize baarray
//read brray[0]
byte b = barray[0];
}
并且,可以將整數(shù)類型轉(zhuǎn)換為字節(jié)。
uint256 s = 1;
bytes32 b = bytes32(s);
這里有一個(gè)關(guān)鍵細(xì)節(jié),Solidity 采取大端序編碼,高地址存的是整數(shù)的小端。例如,b[0]
是低地址端,它存整數(shù)的高端,所以值為 0
;取 b[31]
才是 1
。
function bytesSample() public pure returns(byte, byte){
uint256 value = 1;
bytes32 b = bytes32(value);
//Should be (0, 1)
return (b[0], b[31]);
}
變長字節(jié)
從上文中,讀者可了解定長字節(jié)數(shù)組。此外,Solidity 還提供了一個(gè)變長字節(jié)數(shù)組:bytes
。使用方式類似數(shù)組,后文會(huì)有介紹。
字符串
Solidity 提供的字符串,本質(zhì)是一串經(jīng) UTF-8 編碼的字節(jié)數(shù)組,它兼容于變長字節(jié)類型。
目前 Solidity 對(duì)字符串的支持不佳,也沒有字符的概念。用戶可以將字符串轉(zhuǎn)成字節(jié)。
function stringSample() public view returns(bytes){
string memory str = "abc";
bytes memory b = bytes(str);
//0x616263
return b;
}
要注意的是,當(dāng)將 string
轉(zhuǎn)換成 bytes
時(shí),數(shù)據(jù)內(nèi)容本身不會(huì)被拷貝,如上文中,str
和 b
變量指向的都是同一個(gè)字符串 "abc"
。
地址類型
address
表示賬戶地址,它由私鑰間接生成,是一個(gè) 20 字節(jié)的數(shù)據(jù)。同樣,它也可以被轉(zhuǎn)換為 bytes20
。
function addressSample() public view returns(bytes20){
address me = msg.sender;
bytes20 b = bytes20(me);
return b;
}
映射
mapping
表示映射,是極其重要的數(shù)據(jù)結(jié)構(gòu)。它與 Java 中的映射存在如下幾點(diǎn)差別:
-
它無法迭代鍵名,因?yàn)樗槐4骀I的哈希,而不保存鍵值,如果想迭代,可以用開源的可迭代哈希類庫
-
如果一個(gè)鍵名未被保存在映射中,一樣可以正常讀取到對(duì)應(yīng)的鍵值,只是值是空值(字節(jié)全為
0
)。所以它也不需要put
、get
等操作,用戶直接去操作它即可。
contract Sample{
mapping(uint=>string) private values;
function mappingSample() public view returns(bytes20){
//put a key value pair
values[10] = "hello";
//read value
string value = values[10];
}
}
數(shù)組
如果數(shù)組是狀態(tài)變量,那么支持 push
等操作:
contract Sample{
string[] private arr;
function arraySample() public view {
arr.push("Hello");
uint len = arr.length;//should be 1
string value = arr[0];//should be Hello
}
}
數(shù)組也可以以局部變量的方式使用,但稍有不同:
function arraySample() public view returns(uint){
//create an empty array of length 2
uint[] memory p = new uint[](2);
p[3] = 1;//THIS WILL THROW EXCEPTION
return p.length;
}
結(jié)構(gòu)
Solidity 允許開發(fā)者自定義結(jié)構(gòu)對(duì)象。結(jié)構(gòu)體既可以作為狀態(tài)變量存儲(chǔ),也可以在函數(shù)中作為局部變量存在。
struct Person{
uint age;
string name;
}
Person private _person;
function structExample() {
Person memory p = Person(1, "alice");
_person = p;
}
本節(jié)中只介紹了比較常見的數(shù)據(jù)類型,更完整的列表可參考 Solidity 官方網(wǎng)站:https://solidity.readthedocs.io/en/v0.6.3/types.html
全局變量
示例合約代碼的構(gòu)造函數(shù)中,包含 msg.sender
。它屬于全局變量。在智能合約中,全局變量或全局方法可用于獲取和當(dāng)前區(qū)塊、交易相關(guān)的一些基本信息,如塊高、塊時(shí)間、合約調(diào)用者等。
比較常用的全局變量是 msg
變量,表示調(diào)用上下文,常見的全局變量有以下幾種:
-
msg.sender
:合約的直接調(diào)用者。由于是直接調(diào)用者,所以當(dāng)處于“用戶 A->合約 1->合約 2”調(diào)用鏈下,若在合約 2內(nèi)使用msg.sender
,得到的會(huì)是合約 1 的地址。如果想獲取用戶 A,可以用tx.origin
。 -
tx.origin
:交易的"始作俑者",整個(gè)調(diào)用鏈的起點(diǎn)。 -
msg.calldata
:包含完整的調(diào)用信息,包括函數(shù)標(biāo)識(shí)、參數(shù)等。calldata
的前 4 字節(jié)就是函數(shù)標(biāo)識(shí),與msg.sig
相同。 -
msg.sig
:msg.calldata
的前 4 字節(jié),用于標(biāo)識(shí)函數(shù)。 -
block.number
:表示當(dāng)前所在的區(qū)塊高度。 -
now
:表示當(dāng)前的時(shí)間戳。也可以用block.timestamp
表示。
這里只列出了部分常見全局變量,完整版本請(qǐng)參考:https://solidity.readthedocs.io/en/v0.4.24/units-and-global-variables.html 。
結(jié)語
本文以一個(gè)簡單的示例合約作為引入,介紹了運(yùn)用 Solidity 開發(fā)智能合約的基本知識(shí)。讀者可以嘗試運(yùn)行該合約,感受智能合約的開發(fā)。