【劉文彬】【精解】EOS智慧合約演練
原文連結:醒者呆的部落格園,https://www.cnblogs.com/Evsward/p/eos-contract.html
EOS,智慧合約,abi,wasm,cleos,eosiocpp,開發除錯,錢包,賬戶,簽名許可權
熱身
本文旨在針對EOS智慧合約進行一個完整的實操演練,過程中深入熟悉掌握整個EOS智慧合約的流程,過程中出現的問題也會及時研究併入我們自己的知識體系。本文會主要跟隨EOS官方Wiki的智慧合約部分進行研究學習,主要分為
- 開啟一個私有鏈
- 建立錢包
- 載入基礎IO的智慧合約支援
- 建立賬戶
- 智慧合約學習:
- token
- 交易所
- 智慧合約實戰:
- Helloworld
準備
EOS的智慧合約採用C++ 編寫,因為C++ 的高效性,沒有C++ 程式設計基礎的同學可以先學習《Efficient&Elegant:Java程式設計師入門Cpp》。EOS中使用者開發的應用程式或程式碼都是通過WebAssembly(WASM)來與主鏈進行互動的,它的編譯工具是clang.llvm。關於EOS相關的基礎準備請先過目《區塊鏈3.0:擁抱EOS》,這裡面介紹了包括EOS概念,安裝部署以及工具等基礎內容,其中包括了上面提到的《開啟一個私有鏈》。這裡還有一些準備知識需要過目:
- 智慧合約之間的互動通過action和共享資料檔案
- 這個共享資料檔案在我本機的位置是.local/share/eosio/nodeos/data/shared_mem,隨著節點挖礦執行的時間越來越久,這個目錄下的資料檔案也越來越大。
- 一個合約可以非同步只讀訪問另一個合約的共享資料檔案。
- 針對其他讀取許可權,通過資源限制演算法可以有效避免非同步通訊結果失真的問題。
- 合約之間的兩種互動模式:
- 內聯,意思就是直接採用內部函式體發起,呼叫其他函式的方式。這可以保證交易無阻礙執行,不必通知外部失敗或者成功結果,同時內聯也可保證交易始終處於同一作用域以及許可權。
- 延遲,通過生產者的判定來決定延後按時執行,可能會發生timeout的問題,但是這種方式可以跨多個作用域工作,並且可以攜帶著傳送給它的合約許可權。
- action和transaction:
- action是一個動作,賬戶和合約互動是通過action,可以單獨傳送一個action。
- Transaction是一組動作。所有action都必須成功,該Transaction才會成功。
- 接收到交易雜湊表示節點成功接受了這個交易,也意味著其他生產者也有很大可能接收它。
- 交易驗證需要通過檢視已打包區塊中含有的交易歷史來確定。
這些都瞭解了以後,我們繼續智慧合約的準備。
建立錢包
首先,先確定區塊鏈中錢包的概念:
錢包是一個私鑰庫,用來授權發生在區塊鏈上的動作(action,記住這個概念)。這些私鑰使用密碼生成,被加密儲存在磁碟上。這個密碼應該被儲存在一個安全的密碼管理器中。
提取一下重點:
- 錢包是一個私鑰庫
- 私鑰是通過密碼生成
操作流程:
- 先啟動私鏈,通過命令nodeos即可。
- 建立錢包,使用命令cleos wallet create,這是通過外掛eosio::wallet_api_plugin完成的操作。
[email protected]:~$ cleos wallet create
Creating wallet: default
Save password to use in the future to unlock this wallet.
Without password imported keys will not be retrievable.
"PW5KeMG82A6zEmmHK4sBj3HE8pxBYBFw4CXVoQGt24Zy7AoRgMWxn"
default改為自定義錢包名字wbs:
[email protected]:~$ cleos wallet create -n wbs
Creating wallet: wbs
Save password to use in the future to unlock this wallet.
Without password imported keys will not be retrievable.
"PW5KPAwucXR66NqqzW5R5wdkHCMNGyHLrCWVPaE1nhj7hfacP7ZaL"
wbs錢包解密:
[email protected]:~$ cleos wallet unlock -n wbs
password: Unlocked: wbs
加密即改為:
[email protected]:~$ cleos wallet lock -n wbs
Locked: wbs
如果不加-n引數,即操作預設錢包。
匯入初始賬戶eosio的主祕鑰到錢包
所有新的blockchains,都是通過主祕鑰啟動,唯一初始賬戶:eosio。要與區塊鏈互動,需要將這個初始賬戶的私鑰匯入到你的錢包。
這個主祕鑰我們在上一篇文章也分析到了,是在~/.local/share/eosio/nodeos/config資料夾下的config.ini檔案中自動配置的(可修改,預設安裝EOS會生成一個)。
# 值為[公鑰,私鑰WIF編碼的]
private-key = ["EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV","5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3"]
Private key should be encoded in base58 WIF。所以上篇文章中經常出現的WIF的意思是一種base58編碼方式。
$ cleos wallet import 5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3
imported private key for: EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV
載入基礎IO智慧合約
現在我們擁有了一個錢包default,該錢包內部包含一個預設主金鑰的賬戶eosio,預設的智慧合約eosio.bios已經可以使用,這個合約是EOS很多基本action的基礎系統,所以要保證這個合約的有效執行。這個合約可以讓你能夠直接控制資源分配,並且有許可權訪問API。在公鏈上,這個合約將管理已募集和待募集token,以儲備頻寬給CPU、記憶體以及網路活動使用。我們提取一下重點:
- 建立錢包
- 匯入賬戶
- 預設合約eosio.bios,它的功能是控制資源分配。
這個預設合約eosio.bios可以在EOS原始碼位置contracts/eosio.bios找到。可以通過cleos來指定這個合約執行:
[email protected]:~/work/CLionProjects/github.com/eos$ cleos set contract eosio build/contracts/eosio.bios -p eosio
Reading WAST/WASM from build/contracts/eosio.bios/eosio.bios.wast...
Assembling WASM...
Publishing contract...
executed transaction: 36736dabac246732ef389fb5dd47099887854e25178a320b0e288324b5c87a9c 3288 bytes 2200576 cycles
# eosio <= eosio::setcode {"account":"eosio","vmtype":0,"vmversion":0,"code":"0061736d0100000001581060037f7e7f0060057f7e7e7e7e...
# eosio <= eosio::setabi {"account":"eosio","abi":{"types":[],"structs":[{"name":"set_account_limits","base":"","fields":[{"n...
-
命令列工具仍舊是使用cleos,通過set contract來執行指定合約,後面跟著賬戶名稱(這裡是預設的eosio,我們剛剛匯入的),然後是指定合約的路徑。
-
命令最後的“-p eosio”的含義是:使用賬戶eosio(使用的是賬戶的私鑰)來為這個action簽名。
-
讀取 WAST/WASM檔案(這個檔案是被新部署到build目錄下的)
-
裝配 WASM
-
釋出合約
-
執行交易(合約也是一個交易),這裡通過兩個動作來生成一個交易,
- setcode,code描述了合約是如何執行的。
- setabi,abi描述瞭如何在二進位制檔案和json檔案(代表了各種引數)中轉換。
從技術角度來將,abi是可選的,所有的EOSIO工具取決於它的易用性。
eosio <= eosio::setcode {...}
這一行的閱讀方式為:action setcode是eosio名稱空間下的,同時它是通過eosio賬戶授權來執行的,帶的引數有…
注意,action是可以被多個合約執行的。
此時私鏈日誌顯示:
eosio generated block 11f6a66b... #6149 @ 2018-04-23T10:20:21.500 with 0 trxs, lib: 6148
1221781ms thread-0 abi_serializer.hpp:349 extract ] vo: {...}
私鏈日誌打出來合約部署的相關資訊。其中vo的值很多,它的結構是:
{
"signatures": [
"EOSJzRXuKWJT77BGmBv26SoHGcKkR1XyaCBzkd8Yck5wE9fFptcgLReQZ8wZsxjizAbMxELmVnFPYkv5rT5VDxYk3UoiRPDTC"(這一看就是公鑰格式)
],
"compression": "zlib",
"packed_context_free_data": "",
"packed_trx": "很多內容"
}
這裡面的結構包含一個簽名串,一個加密資訊,打包交易資訊是核心資料,它包含了很長的交易相關的打包後的格式的內容。
建立賬戶
上面提到了如何在錢包中匯入預設賬戶eosio,下面來看一下如何建立賬戶。
注意:key和賬戶是分開的,我們接下來建立兩個賬戶,他們可以使用同一套祕鑰。
建立祕鑰
首先,建立祕鑰對:
[email protected]:~/work/CLionProjects/github.com/eos$ cleos create key
Private key: 5KFGqe3Bv2maZ5jGMrufdkLhGV91ZYSysBWfxUUaR9VzbDk2UdA
Public key: EOS6nbWS7ZReiPMdMABoEmVBYanyTMb3GYRQGsTRMCYx9vijWoaS3
已生成一對公鑰和私鑰。下面將私鑰匯入我們自己的錢包wbs
[email protected]:~/work/CLionProjects/github.com/eos$ cleos wallet import -n wbs 5KFGqe3Bv2maZ5jGMrufdkLhGV91ZYSysBWfxUUaR9VzbDk2UdA
imported private key for: EOS6nbWS7ZReiPMdMABoEmVBYanyTMb3GYRQGsTRMCYx9vijWoaS3
注意格式:
Usage: cleos wallet import [OPTIONS] key
建立user和tester兩個賬戶
[email protected]:~/work/CLionProjects/github.com/eos$ cleos create account eosio user EOS6nbWS7ZReiPMdMABoEmVBYanyTMb3GYRQGsTRMCYx9vijWoaS3 EOS6nbWS7ZReiPMdMABoEmVBYanyTMb3GYRQGsTRMCYx9vijWoaS3
executed transaction: 927762a883e1c92a695a51c312eb5339bf911c1dbd56bab53e74d5fe20365106 352 bytes 102400 cycles
# eosio <= eosio::newaccount {"creator":"eosio","name":"user","owner":{"threshold":1,"keys":[{"key":"EOS6nbWS7ZReiPMdMABoEmVBYany...
注意,建立賬戶命令的格式是:cleos create account [OPTIONS] creator name OwnerKey ActiveKey
- 使用create account命令來建立賬戶
- 是由賬戶eosio建立的
- 待建立的賬戶是user
- OwnerKey,如果在生產環境中,這個值應該保持高度安全
- ActiveKey,這是上面剛生成的金鑰對中的公鑰
- 這裡只是學習使用,這兩個值可以設定成一個
觀察執行結果,這時的action是newaccount,後面是action的引數,以json格式存在。下面同理生成tester賬戶(只需更改上面的user為tester即可,祕鑰採用同一個)。
查詢賬戶
我們可以通過一個公鑰來查詢有它“管轄”的賬戶列表,這是通過eosio::account_history_api_plugin外掛完成的操作:
[email protected]:~/work/CLionProjects/github.com/eos$ cleos get accounts EOS6nbWS7ZReiPMdMABoEmVBYanyTMb3GYRQGsTRMCYx9vijWoaS3
{
"account_names": [
"tester",
"user"
]
}
命令列是通過get accounts子命令來執行,後面要加入查詢條件 -> 公鑰。
準備完畢
通過以上的操作,我們熟悉了cleos命令的一些常用操作,包括建立錢包、加解鎖,執行基礎系統級合約,以及非常常用的建立祕鑰、通過祕鑰建立賬戶,通過祕鑰反查賬戶。過程中,我們涉及到很多action的學習研究。
學習
我們已經準備好了錢包、賬戶、基本合約系統(用於支援基本action)。下面我們可以正式展開對智慧合約的學習,本章主要通過原始碼中已經存在的較簡單的token和exchange兩個合約來學習。
token
為了避免混淆,我們根據上面學習過的內容,重新建立一個賬戶eosio.token專門用來執行token智慧合約。
[email protected]:~/work/CLionProjects/github.com/eos$ cleos create account eosio eosio.token EOS6nbWS7ZReiPMdMABoEmVBYanyTMb3GYRQGsTRMCYx9vijWoaS3 EOS6nbWS7ZReiPMdMABoEmVBYanyTMb3GYRQGsTRMCYx9vijWoaS3
executed transaction: 745de4ad0d7e2f415a1fb962e7f072d4c036e831bdf01f06931d91bc2fcc3a91 352 bytes 102400 cycles
# eosio <= eosio::newaccount {"creator":"eosio","name":"eosio.token","owner":{"threshold":1,"keys":[{"key":"EOS6nbWS7ZReiPMdMABoE...
接下來,我們使用這個賬戶部署eosio.token智慧合約,同樣通過上面學習到的方式:指定路徑,指定加密賬戶{-p eosio.token}:
[email protected]:~/work/CLionProjects/github.com/eos$ cleos set contract eosio.token build/contracts/eosio.token -p eosio.token
Reading WAST/WASM from build/contracts/eosio.token/eosio.token.wast...
Assembling WASM...
Publishing contract...
executed transaction: a95dc5be83d202730f798d2ce75df42eff518a3ed8f82e4c5b0f3c8881d75d70 8024 bytes 2200576 cycles
# eosio <= eosio::setcode {"account":"eosio.token","vmtype":0,"vmversion":0,"code":"0061736d01000000018a011660067f7e7f7f7f7f00...
# eosio <= eosio::setabi {"account":"eosio.token","abi":{"types":[],"structs":[{"name":"transfer","base":"","fields":[{"name"...
建立EOS的代幣
就像以太坊token那樣,我們在EOS上可以更加方便的建立一個基於EOS的代幣。首先,去token合約中的標頭檔案eosio.token.hpp,檢視一下token相關的介面都有哪些,其中有一個create函式,我們正是將要使用這個函式來建立token,所以我們可以留意一下它的引數都包括哪些。
void create(account_name issuer, // 發行人,有許可權呼叫下面的freeze、recall以及whitelist函式。
asset maximum_supply, // 最大發行量,注意單位,這個單位就是該token的名字,symbol。
uint8_t issuer_can_freeze,
uint8_t issuer_can_recall,
uint8_t issuer_can_whitelist );
我們可以通過命令列來呼叫該create函式:
[email protected]:~/work/CLionProjects/github.com/eos$ cleos push action eosio.token create '[ "eosio", "1000000000.0000 EOS", 0, 0, 0]' -p eosio.token
executed transaction: 5b3f974030f7311a0c65cc6d4123be18f7435d0b06b615d939ff81255059aaf8 248 bytes 104448 cycles
# eosio.token <= eosio.token::create {"issuer":"eosio","maximum_supply":"1000000000.0000 EOS","can_freeze":0,"can_recall":0,"can_whitelis...
- 命令列使用push action來執行這個動作
- 動作是eosio.token合約的create動作
- 動作的引數包括:我們的發行人是eosio,發行量是10億EOS,三個呼叫函式都是0
- 動作的執行人(簽名者)是eosio.token,用來授權這個動作的執行
直接按引數順序傳入值比較方便,如果你需要更加準確的傳值,可以將以上動作的引數內容改寫為“Key,Value”的形式改造一下,會比較冗餘。
思考,這個token沒有名字麼?就像EOS之於ethereum那樣。
代幣發放
我們已經發行了一種代幣EOS,下面我們可以將這個代幣EOS發放給賬戶user(我們上面建立的)。繼續檢視那個eosio.token.hpp標頭檔案中關於issue(發放)操作的引數。
void issue( account_name to, asset quantity, string memo );// memo:備註,一般可以不填寫。
然後,我們繼續使用命令列工具cleos來push action到智慧合約eosio.token中這個issue函式中:
[email protected]:~/work/CLionProjects/github.com/eos$ cleos push action eosio.token issue '["user","100.0000 EOS", "memo"]' -p eosio
executed transaction: b93b9e709ce4720dd5e89789c14a785790605ec093194710736cf30bb195fae9 256 bytes 124928 cycles
# eosio.token <= eosio.token::issue {"to":"user","quantity":"100.0000 EOS","memo":"memo"}
>> issue
# eosio.token <= eosio.token::transfer {"from":"eosio","to":"user","quantity":"100.0000 EOS","memo":"memo"}
>> transfer
# eosio <= eosio.token::transfer {"from":"eosio","to":"user","quantity":"100.0000 EOS","memo":"memo"}
# user <= eosio.token::transfer {"from":"eosio","to":"user","quantity":"100.0000 EOS","memo":"memo"}
注意,在命令列結尾處,我們仍要使用賬戶來簽名授權,這裡是使用者eosio(因為上面eosio是發行者,全都發行到它的兜裡啦,所以要從它兜裡取錢)。
我們先來看issue函式的實現原始碼:
void token::issue( account_name to, asset quantity, string memo )
{
print( "issue" );// >> issue
auto sym = quantity.symbol.name();// EOS
stats statstable( _self, sym );
const auto& st = statstable.get( sym );// 通過代幣id獲取代幣物件,包含代幣相關資訊
require_auth( st.issuer );// 檢查發行人許可權,是否有足夠
eosio_assert( quantity.is_valid(), "invalid quantity" );// 轉移金額是否有效
eosio_assert( quantity.amount > 0, "must issue positive quantity" );// 轉移金額必須是正數
statstable.modify( st, 0, [&]( auto& s ) {
s.supply.amount += quantity.amount;
});
add_balance( st.issuer, quantity, st, st.issuer );// TODO: 轉賬金額居然要加到發行者的餘額中?等於發行者轉賬完畢仍舊是原發行量?
if( to != st.issuer )// 別發給自己
{
//這是action的內部函式,執行轉賬的意思
dispatch_inline( permission_level{st.issuer,N(active)}, _self, N(transfer), &token::transfer, { st.issuer, to, quantity, memo } );
}
}
TODO: add_balance函式原始碼要好好研究
執行發放動作,通過日誌可以看到包括了幾個步驟:
- eosio.token名稱空間(就是程式碼中的eosio.token包內)下的發放函式issue,該操作由使用者eosio.token授權(因為正是eosio.token授權的代幣發行!)
- 這三行transfer都是eosio.token名稱空間下的transfer函式,他們都是內聯交易:是由上面的發放函式issue自動觸發的
- 第一行由賬戶eosio.token授權,執行issue函式。">> issue"就是該函式的輸出。
- 第二行由賬戶eosio.token授權,執行transfer函式。">> transfer"就是該函式的輸出。
- 第三行和第四行應該是transfer內部呼叫(notified)sub_balance和add_balance
實際上,eosio.token可以直接修改賬戶餘額而不使用“內聯呼叫transfer”。但是這種情況下,eosio.token智慧合約會要求我們的token必須有所有的賬戶餘額,通過計算引用過他們的所有交易動作的總和。它還需要傳送者和接收者的存款情況,以支援他們可以自動處理充值和提現。
如果你想看到廣播出去的真實交易的情況,可以使用-d -j選項來表達“不要廣播”以及“以json格式返回交易”:
“不要廣播”的意思是這條動作無效,只是用來做測試的。(這與上面的廣播出去的“真實交易”不同)
[email protected]:~/work/CLionProjects/github.com/eos$ cleos wallet unlock
password: Unlocked: default
[email protected]:~/work/CLionProjects/github.com/eos$ cleos push action eosio.token issue '["user","100.0000 EOS", "memo"]' -p eosio -d -j
{
"expiration": "2018-04-24T02:13:26",
"region": 0,
"ref_block_num": 26069,
"ref_block_prefix": 2157111066,
"max_net_usage_words": 0,
"max_kcpu_usage": 0,
"delay_sec": 0,
"context_free_actions": [],
"actions": [{
"account": "eosio.token",
"name": "issue",
"authorization": [{
"actor": "eosio",
"permission": "active"
}
],
"data": "00000000007015d640420f000000000004454f5300000000046d656d6f"
}
],
"signatures": [
"EOSKjnxnDwjjyZaZPSeQnmMFYjpgBWRzby5YFqVqZEn6uA3TUhUo4yWmfQhXdxNykgsSVvAwGnkGUyUK7jcJt5qNg8xhstRqy"
],
"context_free_data": []
由於第二天來上班,我們的錢包已經被鎖定,所以要先解鎖,然後再呼叫上面的命令。可以看到輸出的內容很多,是按照我們的要求“以json格式返回交易”,“不要廣播”。我們可以看到交易的具體資訊,包含了很多屬性:有效期、地區、引用區塊號、引用區塊字首、最大網路使用詞數、最大cpu使用,延遲秒數、上下文的自由動作(為空說明上下文中沒有動作),動作內容(包括動作執行人,動作名稱,授權資訊包括行動者以及許可權狀態、資料串、簽名、上下文自由資料(為空,上下文無內容)。
代幣交易
現在user賬戶已經存在100個EOS代幣了,我們使用上面建立的另一個賬戶tester,用來測試代幣交易:從user賬戶中轉出一部分EOS到tester賬戶。
同樣的,我們還是先來看一下原始碼中設計的transfer函式的引數列表:
void transfer( account_name from,
account_name to,
asset quantity,
string memo );
很簡單,下面使用cleos來呼叫:
這裡我們可以嘗試使用user賬戶本身來簽名動作。
[email protected]:~/work/CLionProjects/github.com/eos$ cleos push action eosio.token transfer '["user","tester","25.0000 EOS", "m"]' -p user
executed transaction: a31e56b49837e53fb1e7d55aa7aba934dc751938241488183e54ad52dc7804fe 256 bytes 110592 cycles
# eosio.token <= eosio.token::transfer {"from":"user","to":"tester","quantity":"25.0000 EOS","memo":"m"}
>> transfer
# user <= eosio.token::transfer {"from":"user","to":"tester","quantity":"25.0000 EOS","memo":"m"}
# tester <= eosio.token::transfer {"from":"user","to":"tester","quantity":"25.0000 EOS","memo":"m"}
成功!仍舊是eosio.token合約發起一個transfer交易動作,輸出“>> transfer”,然後內聯呼叫賬戶餘額的操作分別操作傳送者和接收者。
注意:我們也可以不使用push action的方式來交易,而是直接使用cleos的自命令transfer即可,後面的命令引數與上面的差不多。但是發行和發放token仍舊需要使用合約的push action來操作。我理解的是由於交易比較常用且可不依賴某一個合約,所以被封裝在了根命令中,而其他與合約相關的仍舊需要使用push action的方式。
檢視餘額
我們需要整體研究一下cleos的所有子命令,列舉的方式比較枯燥,這裡不展開,只是使用到哪裡就展示哪裡。我們上面進行了代幣發放和代幣交易,此時兩個測試賬戶user和tester的EOS餘額都發生了變化。下面我們要利用cleos查詢一下這兩個賬戶的代幣EOS的餘額狀況:
[email protected]:~/work/CLionProjects/github.com/eos$ cleos get currency balance eosio.token user EOS
75.0000 EOS
[email protected]:~/work/CLionProjects/github.com/eos$ cleos get currency balance eosio.token tester EOS
25.0000 EOS
可以看到user賬戶的代幣餘額為75,而tester賬戶的代幣餘額是25。這與我們上面的交易動作是可匹配的。
重要發現
上面我們留的TODO,因為我搞不懂為什麼要先給代幣發行人餘額增加轉賬額,再轉賬,這個操作是怎麼來的。我在上面查詢餘額的命令發現:
[email protected]:~/work/CLionProjects/github.com/eos$ cleos get currency balance eosio.token eosio EOS
0.0000 EOS
發行人的餘額是0!
所以這樣我們就搞明白了為什麼每次發放代幣的時候要先add_balance然後再sub_balance了。
exchange
我們建立一個賬戶user1,然後用該賬戶部署合約exchange:
[email protected]:~/work/CLionProjects/github.com/eos$ cleos create account eosio user1 EOS6nbWS7ZReiPMdMABoEmVBYanyTMb3GYRQGsTRMCYx9vijWoaS3 EOS6nbWS7ZReiPMdMABoEmVBYanyTMb3GYRQGsTRMCYx9vijWoaS3
executed transaction: b2c4a1acb831a4bd12d1686c271ca49b0ed85efe5a31f5c24abe212bc44d4009 352 bytes 102400 cycles
# eosio <= eosio::newaccount {"creator":"eosio","name":"user1","owner":{"threshold":1,"keys":[{"key":"EOS6nbWS7ZReiPMdMABoEmVBYan...
[email protected]:~/work/CLionProjects/github.com/eos$ cleos set contract user1 build/contracts/exchange -p user1
Reading WAST/WASM from build/contracts/exchange/exchange.wast...
Assembling WASM...
Publishing contract...
executed transaction: 4e2bc4496ef25f187bb90da0f0b3398d5c1970ba62b062ab34160d09975c0591 34056 bytes 2200576 cycles
# eosio <= eosio::setcode {"account":"user1","vmtype":0,"vmversion":0,"code":"0061736d0100000001cd023160067f7e7f7f7f7f0060037f...
# eosio <= eosio::setabi {"account":"user1","abi":{"types":[{"new_type_name":"account_name","type":"name"}],"structs":[{"name...
exchange合約能做的事情有很多,包括建立和交易currency(電子貨幣)。它能做的事可以參考原始碼位置contract/exchange/*。
Eosio.msig
msig的意思是multi-signature,多重簽名的意思。這個合約是可以支援多方對同一筆交易進行非同步簽名,它是一個對使用者友好的支援多方同意的非同步進行提案、批覆以及最終釋出交易的合約。
[email protected]:~/work/CLionProjects/github.com/eos$ cleos create account eosio user2 EOS6nbWS7ZReiPMdMABoEmVBYanyTMb3GYRQGsTRMCYx9vijWoaS3 EOS6nbWS7ZReiPMdMABoEmVBYanyTMb3GYRQGsTRMCYx9vijWoaS3
executed transaction: b156f5aa2fec6ca8ed6e55ba2be2e403cef0dd26f3c022a51e74f9ccf348fef2 352 bytes 102400 cycles
# eosio <= eosio::newaccount {"creator":"eosio","name":"user2","owner":{"threshold":1,"keys":[{"key":"EOS6nbWS7ZReiPMdMABoEmVBYan...
[email protected]:~/work/CLionProjects/github.com/eos$ cleos set contract user2 build/contracts/eosio.msig -p user2
Reading WAST/WASM from build/contracts/eosio.msig/eosio.msig.wast...
Assembling WASM...
Publishing contract...
executed transaction: 66c64a3f55f011d40483c627ea43fd62f303fd96f9851141df67087867d4e02f 7320 bytes 2200576 cycles
# eosio <= eosio::setcode {"account":"user2","vmtype":0,"vmversion":0,"code":"0061736d01000000016b1260017f0060047f7e7e7f006004...
# eosio <= eosio::setabi {"account":"user2","abi":{"types":[{"new_type_name":"account_name","type":"name"},{"new_type_name":"...
部署方式與前面沒有大區別,這裡使用的是賬戶user2,它能做的事可以參考原始碼位置contract/eosio.msig/*。
我們儘量使用與合約名字相同的賬戶名字來發布合約,這樣可以有效記錄該賬戶的功能,可快速與其他普通使用者做出區分。
實戰
以上我們提取了eos.io合約中的三個,進行了部署、學習與操作演練,下面我們將嘗試開發自己的基於eos的智慧合約。
eosiocpp構建合約檔案系統
eosiocpp:智慧合約的載入程式工具。
[email protected]:~/work/CLionProjects/github.com/eos/contracts$ eosiocpp -n testcontract
created testcontract from skeleton
[email protected]:~/work/CLionProjects/github.com/eos/contracts$ ls
asserter CMakeLists.txt dice eosiolib eosio.system exchange identity libc++ musl proxy skeleton stltest test_api_db test_api_multi_index test.inline
bancor currency eosio.bios eosio.msig eosio.token hello infinite multi_index_test noop simple.token social test_api test_api_mem testcontract tic_tac_toe
[email protected]:~/work/CLionProjects/github.com/eos/contracts$ cd testcontract/
[email protected]:~/work/CLionProjects/github.com/eos/contracts/testcontract$ tree
.
├── testcontract.abi
├── testcontract.cpp
└── testcontract.hpp
0 directories, 3 files
我們在原始碼contracts目錄下執行了以上命令以後,就會得到一個空的智慧合約的開發框架,其中包含了abi,cpp以及hpp三個檔案。
- hpp檔案,是標頭檔案,包括了cpp檔案要使用到的變數、常量以及方法引用。
testcontract.cpp
/**
* @file
* @copyright defined in eos/LICENSE.txt
*/
#include <testcontract.hpp>
/**
* The init() and apply() methods must have C calling convention so that the blockchain can lookup and
* call these methods.
*/
extern "C" {
/// The apply method implements the dispatch of events to this contract
void apply( uint64_t receiver, uint64_t code, uint64_t action ) {
eosio::print( "Hello World: ", eosio::name(code), "->", eosio::name(action), "\n" );
}
} // extern "C"
- cpp檔案(以上),是原始檔,編寫了具體的合約執行方法。
- init方法和apply方法必須有C呼叫協定,區塊鏈才可以查詢以及呼叫這些方法。
- extern “C”{},這是在C++ 程式碼中引入C語言的語法。
- init方法,是初始化僅執行一次的方法內容,例如eosio.token第一次的發行一種代幣的發行量指定。
- apply方法的實現,引數包含接收者,程式碼,action。方法體是呼叫eosio庫的列印方法print,拼串打印出相應資訊。未來應該包含更多內容,它是一個action處理器,會監聽所有進來的action,根據特定方法處理以後返回結果。其中該方法體中還會涉及針對傳入引數的各種校驗,例如code_filter,action_filter。
- 目前cpp檔案都是日誌列印方面的內容,具體實現還是空的,當前這個合約不會有任何檢查,包括許可權,簽名等。
- wast檔案,WASM適用的由cpp檔案編譯後的檔案格式,這是區塊連結收的唯一格式。
wast檔案生成方式:
eosiocpp -o ${contract}.wast ${contract}.cpp
-
abi檔案,Application Binary Interface,應用程式的二進位制介面,這在以太坊是相同的概念,請參照《【精解】開發一個智慧合約》。
abi是一個json格式的,用來描述智慧合約如何在action和二進位制程式中進行轉變的方法,也用來描述資料庫狀態。有了abi來描述你的智慧合約,開發者和使用者都可以通過JSON無縫地與合約進行互動。
abi檔案生成時原始檔語法包括:
- typedef,可以自定義型別,供合約使用。在原始檔中可通過typedef關鍵字匯出型別。
- ///@abi table,註解,標在一個類或者結構上,會通過eosiocpp工具生成abi檔案中包含資料庫表table的名字。
- ///@abi action,註解,標在一個函式或方法上,這是合約暴露給外部的功能介面,外部可以呼叫這些動作。(可以給同一個功能函式定義多個action名字,例如//@abi action action1 action2大家都呼叫一個。)
abi檔案生成方式:
eosiocpp -g ${contract}.abi ${contract}.hpp
abi檔案生成以後,我們可以找一個開啟看一下,裡面包含的內容很多,有各種屬性,資料,方法功能的描述。
helloworld
部署學習和操作我們都已經學會,那麼現在要開發一個helloworld智慧合約,首先在eos原始碼中找到一個位置(因為要include相關庫),建立一個目錄hello,在裡面建立一個
1,hello.cpp,
//
// Created by liuwenbin on 18-4-24.
//
#include <eosiolib/eosio.hpp>
#include <eosiolib/print.hpp>
using namespace eosio;
class hello : public eosio::contract {
public:
using contract::contract;
/// @abi action
void hi(account_name user) {
print("Hello, ", name{user});
}
};
EOSIO_ABI(hello, (hi)) // CLion程式碼檢查,這裡會報錯,先不理會
2,編譯wast
在hello.cpp路徑下執行:
eosiocpp -o hello.wast hello.cpp
會有很多警告出來,不要理會,檢視一下,當前目錄應該已經有了hello.wast。
3,編譯abi
然後繼續在hello.cpp路徑下執行:
eosiocpp -g hello.abi hello.cpp
Generated hello.abi ...
檢視當前目錄,又生成了一個hello.abi檔案。
我們的合約開發就完成了。下面的操作與上一章節的操作是類似的。
我們先建立一個賬戶hello.a,然後用這個賬戶部署合約hello。部署完成以後,我們可以進行合約呼叫:
[email protected]:~/work/CLionProjects/github.com/eos$ cleos push action hello.a hi '["Edward"]' -p hello.a
executed transaction: 8f4b9a4fe271a7981ee40348179dcdede025ebece10c38d0a4d5a0aa5d41ffac 232 bytes 102400 cycles
# hello.a <= hello.a::hi {"user":"Edward"}
>> Hello, Edward
通過賬戶hello.a 呼叫hi函式,傳入引數’[使用者名稱]’,使用hello.a簽名該action。執行以後,會在日誌中打印出“>> …”。
查詢當前賬戶
以上操作都是測試用賬戶,他們都是基於相同的公鑰建立的,我們現在來檢視下目前該公鑰有多少個賬戶:
[email protected]:~/work/CLionProjects/github.com/eos$ cleos get accounts EOS6nbWS7ZReiPMdMABoEmVBYanyTMb3GYRQGsTRMCYx9vijWoaS3
{
"account_names": [
"eosio.token",
"hello.a",
"tester",
"tokener",
"user",
"user1",
"user2"
]
}
加入許可權
目前我們的hello合約是不限制hi引數的,也就是說其實我們是沒有“Edward”這個簽名人的,也就是說這個引數中無論是否傳入賬戶名,都可以輸出。
[email protected]:~/work/CLionProjects/github.com/eos$ cleos push action hello.a hi '["Edward"]' -p user
executed transaction: fc38858f89e7dfdd9dcff8db8626545f387960cee63f80e8352b7f7596a986a7 232 bytes 102400 cycles
# hello.a <= hello.a::hi {"user":"Edward"}
>> Hello, liuwenbin
[email protected]:~/work/CLionProjects/github.com/eos$ cleos push action hello.a hi '["Edward"]' -p user1
executed transaction: f81acea4f1ef9207afc5827608fd255bda063b0a0aeaa214f6bc2742ce480a34 232 bytes 102400 cycles
# hello.a <= hello.a::hi {"user":"Edward"}
>> Hello, liuwenbin
另外,我們可以使用user,也可以使用user1來簽名hello.a部署的我們的hello智慧合約,這顯然是不合理的。
我們期望智慧合約hi函式的引數必須是有效賬戶名,同時只有該賬戶擁有當前action的簽名權。所以,我們要修改hello.cpp檔案。
/// @abi action
void hi(account_name user) {
require_auth(user);// 只有該user賬戶有權簽名當前action
print("Hello, ", name{user});
}
然後重複以上編譯和部署的操作。再傳入非有效賬戶名時,或者用其他賬戶簽名的時候就會報錯:
[email protected]:~/work/CLionProjects/github.com/eos$ cleos push action hello.a hi '["Edward"]' -p hello.a
Error 3120001: Invalid name
Name should be less than 13 characters and only contains the following symbol .12345abcdefghijklmnopqrstuvwxyz
Error Details:
Name not properly normalized (name: Edward, normalized: .dward)
'["Edward"]' is invalid args for action 'hi' code 'hello.a'
報錯Edward不是有效引數。
[email protected]:~/work/CLionProjects/github.com/eos$ cleos push action hello.a hi '["user"]' -p hello.a
Error 3030001: missing required authority
Ensure that you have the related authority inside your transaction!;
If you are currently using 'cleos push action' command, try to add the relevant authority using -p option.
Error Details:
missing authority of user
hello.a無法給賬戶user簽名。
[email protected]:~/work/CLionProjects/github.com/eos$ cleos push action hello.a hi '["tester"]' -p tester
executed transaction: be235f5fbd6173a4acac23b8faf4cd3de9d73721b839d3092b2db23eaa3ef51d 232 bytes 102400 cycles
# hello.a <= hello.a::hi {"user":"tester"}
>> Hello, tester
成功!我們傳入有效使用者名稱tester,並且用tester賬戶本身去簽名當前action,最終成功輸出了結果。
思考,我們可發現其他eos的智慧合約都是符合以上期望的,這是什麼規則?
這是Ricardian Contract,意思是該合約符合李嘉圖等價原則。
李嘉圖等價的合約,會指定其合法繫結者來關聯該合約的每一個action。
關於賬戶和合約關係的一些心得:
- 可以使用某個賬戶作為合約釋出者,那麼該賬戶就擁有了此合約的操作權,後續對該合約的操作不必再寫合約名字,直接使用該賬戶加上合約內部函式即可。
- 一個賬戶可以釋出多次不同的合約,但是以最後一次為有效,因為作為合約code的hash是隻有一個,每次部署新的合約會覆蓋原有的。
[email protected]:~/work/CLionProjects/github.com/eos$ cleos get code hello.a
code hash: 7c6a300874835ad928de4f30712023758157bd50cb423ab039443f56a84167ff
- 多個賬戶可以釋出同一個合約,並沒有限制,只是要使用哪個賬戶的,就得指定哪個賬戶名,這個特點我想可以與合約版本除錯結合使用。
- 合約使用的時候通過action,而每次action涉及到交易的都要簽名,一般是from使用者簽名(扣錢了你得拿到使用者密碼,一樣的道理),其他合約的action可能不必簽名,例如查詢action。
除錯
我們編寫一個智慧合約,需要在本地私有鏈上進行除錯,通過以後再上公有鏈。
官方聲稱這叫做Caveman debugging(瞬間不想再愛了,照以太坊差遠了),什麼意思呢?就是eosio::print可以輸出log來除錯,EOS目前沒辦法進行程式碼斷點除錯。
總結
本文介紹了EOS智慧合約的內容,這部分內容的確比以太坊的要少很多,因為以太坊上面成熟的開發框架更多一些,功能也更強大(例如可以斷點除錯,本地虛機等),而EOS比較新,在這方面沒有那麼多工具可選。但是EOS的智慧合約比起使用Solidity的以太坊合約來講,還是非常方便的,很多想法也比較新穎。本文主要從準備、學習、實戰和除錯這幾個步驟進行循序漸進地瞭解與學習。這期間,我們學習了bios、token、exchange、msig以及自己實現了helloworld合約,掌握了錢包、賬戶、簽名許可權等很多基本功能,熟悉了cleos和eosiocpp命令的使用,掌握了智慧合約的編寫、編譯、部署以及除錯的知識。
參考資料
EOS官方文件
相關文章和視訊推薦
圓方圓學院彙集大批區塊鏈名師,打造精品的區塊鏈技術課程。 在各大平臺都長期有優質免費公開課,歡迎報名收看。