智能合約
這兩天被老大搞去搬磚,學習計划有變但無大礙,這篇文章將仔細分析智能合約相關內容。
關鍵字:智能合約,remix,Solidity,truffle,geth,leveldb,datadir,ganache,web3j
合約
合約也稱合同、協議,是甲乙雙方參與的,制定一系列條目規范雙方權利與義務的文件。智能合約是電子化的,自動執行的,去中心化的,具有不可抵賴性,本質上它是一段代碼,依托於區塊鏈技術,它可以做很多事情,基於以太坊的智能合約可以讓你的區塊鏈擴展出任何你想要的功能。
我相信,智能合約是區塊鏈的未來,因為基於它能做的商業模型太多樣了,遠遠不僅是數字貨幣一種。
Solidity
智能合約的編程語言是Solidity,擴展名為.sol,它是基於C++、JavaScript、Python創造而來的,這里是官方文檔。
Solidity是靜態類型的,支持繼承,有自己的函數庫,它同樣支持面向對象語言的自定義類型等其他功能。
Solidity編寫的智能合約代碼運行在EVM,即以太坊虛擬機,正如java編寫的代碼運行在JVM一樣,在同一個區塊鏈中每一個結點的EVM都是相同的運行環境。通過智能合約,可以開發匿名投票、匿名拍賣、眾籌以及多重簽名的錢包等,以太坊每一個結點可以有多個賬戶,所以每個結點都可以稱作錢包,可以管理名下的賬戶,以及轉賬、挖礦等操作。
官方推薦IDE:Remix
其實Solidity智能合約開發的IDE有很多,官方推薦的Remix是基於瀏覽器的,運行環境可以切換:
- 掛在自己的JavaScript EVM
- 也可以使用web3 provider
- 還可以使用注入的web3連接到本機調試環境
我使用以后,覺得瀏覽器的方式還是不習慣,尤其保存的文件無故消失,讓我始終心有余悸,經過調研,下面我們將采用goLand,安裝Intellij-Solidity-2.0.4插件的方式開發智能合約,然后使用Remix環境進行智能合約的部署。當然我們也可以使用Remix進行運行、測試以及調試工作,下面酌情展示。
gas
區塊鏈中比較有意思的命名,相當於手續費但又有些不同。gas為天然氣,用來代表我們程序運行所有的能耗,當發生交易等操作時會消耗相應的gas,gas的計算方式是
gas 單價 × gas 數量
其中gas單價是由用戶,像我們這樣的發起者願意為此次操作付出多少以太幣而定的(相當於你開車上路前願意給你的油箱加多少油,假設你的油箱是無限大的)。gas數量是程序根據你操作的復雜度自動定義的。
智能合約也是一樣的,當一個發起者部署運行一段智能合約時,以太坊會收取gas費用,就像汽車行駛需要燒油一樣,直到你的智能合約運行完畢,“油箱”中剩余的gas會退還給你,如果你的代碼死循環了,耗盡了你“油箱”中的gas,那么以太坊會自動報出異常停止你的智能合約。我們在學習智能合約階段,可以使用testnet環境來避免真的花費以太幣。
Dapp
Dapp為Solidity提供了源碼構建工具,包管理工具,單元測試以及智能合約部署,一會兒我們看看是否必須要用它。有時它也被稱作去中心化的應用程序(Decentralized App)。這種應用程序除了有一段代碼的智能合約以外,還需要UI,UE設計等,正如apple的app開發,我們未來的目標之一可以是開發自己的Dapp。
准備工作
首先要開啟一個本地的EVM,前面的文章對Geth做了詳細的介紹,這里直接啟動一個本地開發模式的結點。
geth --datadir testNet --dev console 2>>Documents/someLogs/testGeth.log
簡介一下geth的參數選項:
dev
Ephemeral proof-of-authority network with a pre-funded developer account, mining enabled
短暫的認證證明網絡,同時創建一個預存款很多錢的一個開發者賬戶,並自動開始挖礦。
datadir
datadir,指定結點文件目錄,如果沒有會自動創建一個,該目錄包含:
- geth
- chaindata 區塊數據、狀態數據的目錄,數據庫是leveldb(一個鍵值對數據庫)
- 000001.log
- CURRENT 指向MANIFEST
- LOCK 區塊數據鎖定標識文件
- LOG 數據庫(區塊和狀態)操作日志
- *.ldb 塊數據文件
- MANIFEST-000000 (TODO,我也不知道是什么,誰能告訴我一下)
- LOCK 結點鎖定標識文件
- nodekey 結點身份公鑰,用於p2p網絡尋找結點使用
- transactions.rlp
- chaindata 區塊數據、狀態數據的目錄,數據庫是leveldb(一個鍵值對數據庫)
- geth.ipc Mist是以太坊錢包,該文件是Mist用來內部過程通信的socket文件。
- keystore 存儲私鑰
- UTC--2018-02-06T03-46-35.626115529Z--740b9c48d67cf333c8b1c0e609b6b90b40d3cdea
以上目錄中元素精解:
① nodekey
結點之間相互尋找是通過一個發現協議:一個基於S/Kademlia的網絡協議。這個協議會把包含IP地址的公鑰聯系起來。實際上在結點之間的peer連接使用的是一個完全不同的,加密的協議(RLPX)。RLPX加密的工作方式需要遠程終端連接發起者的公鑰作為身份識別。本質上來說,這個key鏈接了發現協議和RLPX。
你可以隨時刪除這個nodekey,重啟的時候會自動生成一個新的。
② keystore/UTC--2018-02-06T03-46-35.626115529Z--740b9c48d67cf333c8b1c0e609b6b90b40d3cdea
這是存儲結點私鑰的位置,文件名為時間戳加上本地賬戶拼成的字符串。打開文件,內容為一個json,格式化以后為:
{
"address": "740b9c48d67cf333c8b1c0e609b6b90b40d3cdea", "comment":"本地賬戶地址",
"crypto": {
"cipher": "aes-128-ctr", "comment":"加密協議采用的是AES-128",
"ciphertext": "b331a3dbdde9abd14991116ac0bb1b742f22edda162b567974f8fbf1d694daef", "comment":"密文",
"cipherparams": {
"iv": "06d0df7a5b7160da852fbb01339149ae", "comment":"加密參數"
},
"kdf": "scrypt", "comment":"Key Derivation Function, 將短密碼加鹽hash成長密碼,防彩虹表、防暴力破解",
"kdfparams": {
"dklen": 32, "comment":"KDF加密參數",
"n": 262144,
"p": 1,
"r": 8,
"salt": "6ffbd23fac4ed386aac703bc180f50be02690bef5239057a34dde4dd4de2416b", "comment":"鹽值,加鹽加密"
},
"mac": "06b7d92b98a3b732dc1e63e7e09b8e3d79a9e8e1d43ee7a1b40482db295ea367", "comment":"message authentication code,消息認證碼"
},
"id": "ff7e243a-150e-45f6-ac64-06b0ed2e68ec", "comment":"文件主鍵",
"version": 3
}
這部分范疇屬於密碼學方面了,可以參考《應用密碼學初探》
③ transactions.rlp
RLP(Recursive Length Prefix),遞歸長度前綴。是以太坊中用於序列號對象的主要編碼方法。根據文件名可以猜出,這是所有交易的序列化對象文件。
④ chaindata
數據庫采用leveldb,存儲了區塊數據以及狀態數據。該目錄下打包存儲以.ldb為擴展名的每個區塊的數據文件。每個塊文件有容量的最大值,目前我本機默認的是2.1M,我們設想一下目前以太坊的區塊高度為5039768,如果一個塊是2.1M的話,那么整個區塊鏈的數據大小為10TB。
⑤ leveldb
Google出品的另一利器,使用C++編寫,基於LSM(Log-Structured-Merge Tree)日志結構化合並樹,是一個高效的鍵值對存儲系統,是沒有Sql語句的非關系型數據庫。鍵值對均采用字符串類型,按照key排序。
特點包括:
- 鍵和值都是當作簡單的字節數組,所以內容可以從ASCII字符串到二進制文件。
- 數據按照key排序存儲。
- 調用者可以自定義一個比較方法來復寫排序。
- 基本操作有插入、獲取和刪除:Put(key,value), Get(key), Delete(key).
- 一次原子批量操作可以執行多重變更操作。
- 用戶能夠創建一個瞬時快照來獲取一個統一的數據視圖。
- 數據可以向前亦或是向后迭代。
- 數據采用Snappy(也是Google的一個壓縮庫)自動被壓縮。
- 用戶可以通過一個虛擬接口自定義操作交互系統來實現一些額外的操作。
局限性包括:
- 無SQL,無索引,非關系型數據庫
- 同時只允許一個進程訪問(但支持多線程)
- 無客戶端-服務端內置庫支持,一個應用程序必須要包裝自己的服務器到庫才能獲得這樣的支持。
console
console命令在EVM啟動的同時開啟了一個交互控制台,后面的一串命令是將輸出的log轉存到文件testGeth.log中去,啟動時的日志文件:
WARN [02-06|11:46:35] No etherbase set and no accounts found as default
INFO [02-06|11:46:37] Using developer account address=0x740b9C48D67Cf333C8b1c0E609b6b90b40D3CdeA
INFO [02-06|11:46:37] Starting peer-to-peer node instance=Geth/v1.7.3-stable-4706005b/linux-amd64/go1.9.2
INFO [02-06|11:46:37] Allocated cache and file handles database=/home/liuwenbin/testNet/geth/chaindata cache=128 handles=1024
INFO [02-06|11:46:37] Writing custom genesis block
INFO [02-06|11:46:37] Initialised chain configuration config="{ChainID: 1337 Homestead: 0 DAO: <nil> DAOSupport: false EIP150: 0 EIP155: 0 EIP158: 0 Byzantium: 0 Engine: clique}"
INFO [02-06|11:46:37] Initialising Ethereum protocol versions="[63 62]" network=1
INFO [02-06|11:46:37] Loaded most recent local header number=0 hash=593c0e…256b90 td=1
INFO [02-06|11:46:37] Loaded most recent local full block number=0 hash=593c0e…256b90 td=1
INFO [02-06|11:46:37] Loaded most recent local fast block number=0 hash=593c0e…256b90 td=1
INFO [02-06|11:46:37] Regenerated local transaction journal transactions=0 accounts=0
INFO [02-06|11:46:37] Starting P2P networking
INFO [02-06|11:46:37] started whisper v.5.0
INFO [02-06|11:46:37] RLPx listener up self="enode://ede08b763001ed3642e0b3860d57e694489bcc1f47dde8563f2577bdec48e6949748826d9b88f55f456af2ae1e75ce2ea04a59eb0ef1c2c53330be92e44e6515@[::]:46591?discport=0"
INFO [02-06|11:46:37] Transaction pool price threshold updated price=18000000000
INFO [02-06|11:46:37] IPC endpoint opened: /home/liuwenbin/testNet/geth.ipc
INFO [02-06|11:46:37] Starting mining operation
INFO [02-06|11:46:37] Commit new mining work number=1 txs=0 uncles=0 elapsed=53.048µs
我們逐行分析,
- 啟動時第一行並未找到以太坊base的設置以及默認賬戶。
- 說明使用了開發者賬戶,后面給出了賬戶地址。
- 開始p2p網絡結點,實例采用的是基於go1.9.2版本的geth實例。
- 分配緩存和文件句柄(打開文件的唯一標識,給一個文件、設備、socket或管道一個名字,隱藏關聯細節),數據庫位置在/home/liuwenbin/testNet/geth/chaindata,緩存大小為128M, 文件句柄數為1024。
- 寫入當前創世塊。
- 初始化鏈配置,展示配置信息。
- 初始化以太坊協議。
- 載入大部分最近的本地數據頭
- 載入大部分最近的本地完整塊數據
- 載入大部分最近的本地最高塊數據
- 重新生成本地交易賬本
- 開始p2p網絡
- 開始whisper
- RLPx開始監控,並打印出當前enode信息
- 交易池價格閥值更新,價格為=18000000000
- IPC端點開啟:/home/liuwenbin/testNet/geth.ipc
- 開始挖礦操作
- 提交新的挖礦工作
helloworld
下面在console中查看一下當前賬戶的余額,發現開發環境默認給分配的余額太大,並不好測試,那么我們自己再創建一個用戶,余額為0,然后用第一個“大款”賬戶轉賬給新創建用戶1個以太幣。
> eth.sendTransaction({from: '0x740b9c48d67cf333c8b1c0e609b6b90b40d3cdea',to:'0x1d863371462223910a1f05329b6dea0b0f9c49f8',value:web3.toWei(1,"ether")})
"0xb456244e4fb25b74108f05afe53670b5f1a857f5671e7d3fa2e221419d04382c"
> eth.getBalance(eth.accounts[1])
333333333333333333
我發現一個事,之前乘三那個geth還存在呢(捂臉笑出淚),讓我改一下吧。改后我重新部署了geth命令,然后將新建用戶的3個以太轉回大款賬戶,由於gas的存在(實際上即使轉賬時你自己指定,也是基於一個最小值,往多了給,如果低於這個最小值,就會報錯:“你加的油太少啦,我根本跑不過去”。所以最終費了大力,讓新賬戶保留下了
> eth.getBalance(eth.accounts[1])
79000
這79000wei的以太幣是無法轉出去了,因為我的余額付不起油錢。實際上79000這個數字可讀性還行,所以拿這個測試也可以。
IDE編碼
上面說道了我們采用goLand安裝Solidity插件的方式來開發智能合約。JetBrain系列IDE插件的安裝我就不介紹了,網上隨便查。下面我們開始編碼:
pragma solidity ^0.4.0;
contract helloworld {
string content;
function helloworld(string _str) public {
content = _str;
}
function getContent() constant public returns (string){
return content;
}
}
代碼編寫很簡單,我們逐行解讀:
- 通過關鍵字pragma標識Solidity的版本為0.4.0,我們下面的代碼都會采用該版本來編譯。
- contract關鍵字定義一個合約,它可以有自己的方法,自己的屬性(智能合約里面更願意稱為狀態),將會存儲在區塊鏈中特定的地址。
- 聲明了一個字符串類型(注意首字母小寫的類型關鍵字string)的content狀態(叫做屬性、成員變量都可以)
- 通過關鍵字function定義一個構造方法,需要傳入一個字符串數據,注意該方法的權限public被標識在了參數列表的后面。
- 通過該方法賦值給狀態content(注意不用使用this),方法的參數變量名采用了下划線開頭的方式用來代表該變量的作用域很小,是私有變量,這是編程語言中的一種約定俗成的命名規則。
- 通過關鍵字function定義一個打印方法,返回狀態content的值,注意除了public權限以外,public的前側還有一個constant關鍵字,后側還通過關鍵字returns定義了返回值類型。
部署
上面我們使用了goLand的Solidity插件進行了合約代碼的開發,然而該插件的功能僅包括:
- 語法高亮,代碼提示
- 代碼完整性檢查
- 文件模板
- goto聲明
- Find usages
- 代碼格式化
可以說都是針對編碼輔助的操作,然而若我們要部署智能合約,還得回到Remix,我們新建一個sol文件,粘貼進去上面寫好的helloworld代碼,然后點擊右側Details,彈出的界面包含了名字、字節碼、元數據等內容,我們只要其中的WEB3DEPLOY,復制出其中內容,將第一行傳入參數“hello world”:
var string_str = "hello world" ;
var helloworldContract = web3.eth.contract([{"constant":true,"inputs":[],"name":"getContent","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[{"name":"string_str","type":"string"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"}]);
var helloworld = helloworldContract.new(
string_str,
{
from: web3.eth.accounts[0],
data: '0x6060604052341561000f57600080fd5b6040516102b83803806102b8833981016040528080518201919050508060009080519060200190610041929190610048565b50506100ed565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061008957805160ff19168380011785556100b7565b828001600101855582156100b7579182015b828111156100b657825182559160200191906001019061009b565b5b5090506100c491906100c8565b5090565b6100ea91905b808211156100e65760008160009055506001016100ce565b5090565b90565b6101bc806100fc6000396000f300606060405260043610610041576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806359016c7914610046575b600080fd5b341561005157600080fd5b6100596100d4565b6040518080602001828103825283818151815260200191508051906020019080838360005b8381101561009957808201518184015260208101905061007e565b50505050905090810190601f1680156100c65780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b6100dc61017c565b60008054600181600116156101000203166002900480601f0160208091040260200160405190810160405280929190818152602001828054600181600116156101000203166002900480156101725780601f1061014757610100808354040283529160200191610172565b820191906000526020600020905b81548152906001019060200180831161015557829003601f168201915b5050505050905090565b6020604051908101604052806000815250905600a165627a7a72305820f4bd9a6659a8625f89177c604c901764cf9cca4fa8aa2e792525da3647ca7a510029',
gas: '4700000'
}, function (e, contract){
console.log(e, contract);
if (typeof contract.address !== 'undefined') {
console.log('Contract mined! address: ' + contract.address + ' transactionHash: ' + contract.transactionHash);
}
})
仔細觀察上面的代碼,Remix幫我們將代碼轉成了EVM可識別的樣子,也就是將Solidity代碼編譯成web3的版本,其中也幫我們估算好了gas的金額,當我們執行這段合約時會自動扣掉我們余額中相應的數值作為gas費用。
接着,我們回到console,先解鎖智能合約發布者的賬號,我們選擇剛才新建的
> personal.unlockAccount(eth.accounts[1],"lwb")
true
然后將上面的web3版的代碼復制過來,回車,輸出:
Contract mined! address: 0x71db931bdb2f9516cf892aa0c620bd686d1095e5 transactionHash: 0x6e39a97dd2f260517bedeb9934cf88430526b46a379d5680cc092d8ea3f44602
合約被挖出,打印出來了合約地址,交易hash(這在以太坊中也被認定為是一筆交易,我們付費gas給以太坊)。
然后繼續在console中輸入
> helloworld.getContent()
"hello world"
由於我們余額是79000,上面gas給預估的是4700000,所以預想結果是您的余額不足,合約無法運行,然而合約部署運行成功了。
我們從大款那再轉賬一個以太幣過來。然后關閉重啟geth console,重復上面的操作。
TODO: 余額仍舊未減少。不知道gas扣到哪去了。
同步查看日志輸出:
INFO [02-06|17:36:34] Submitted contract creation fullhash=0x6e39a97dd2f260517bedeb9934cf88430526b46a379d5680cc092d8ea3f44602 contract=0x71DB931bdb2f9516Cf892aA0c620bD686D1095E5
INFO [02-06|17:36:34] Commit new mining work number=18 txs=1 uncles=0 elapsed=313.823µs
INFO [02-06|17:36:34] Successfully sealed new block number=18 hash=37913b…f101af
INFO [02-06|17:36:34] 🔨 mined potential block number=18 hash=37913b…f101af
每當我們提交了一個合約,
- 第一行打印出了上面合約部署成功時的交易hash和合約地址。
- 然后開始挖礦記賬,目前已經記到第18個塊了
- 第三行第四行顯示成功密封了一個新的區塊。
Solidity語法
上面使用Solidity編寫了一個helloworld智能合約,稍顯力不從心,下面我們專門來學習一下Solidity語法,為未來我們編寫復雜的智能合約工程打下基礎。
類型
學習一門新的編程語言,首先要看它的類型,Solidity是靜態類型語言,跟java一樣,也就是說在編譯之前都要指定好每個變量的具體類型。類型可以分為值類型和引用類型,與java類似。
1.值類型
值類型作為參數時永遠傳的是值,每一次入參出參都是內存中值的副本。包括:
- 布爾類型bool,true\false,與非門操作不多說。
- 整型int,與go語言相同,有符無符int/uint,從長度8到256(int8, int16, int32... int256),數學運算包括位運算不多講,有不明的地方請轉到掌握一門語言Go。
- 定長浮點型fixed,有符無符fixed/ufixed,還未完全被Solidity支持,可聲明不可賦值,多說無益。
- 定長字節數組(byte/bytes1, bytes2, bytes3, ..., bytes32),沒什么好說的,有個length屬性可以取出長度。
- 變長字節數組(bytes, string),與以上定長類型不同的是,變長類型不必預先指定長度,但bytes和string都屬於引用類型,下面會具體介紹。
- 地址類型address,根據以太坊結點、賬戶、合約等address的概念設計,長度限制為20字節。神奇的是,address封裝好了一個balance屬性,可以查看賬戶余額,以及transfer方法,可以直接轉賬,非常方便。此外它還有send、call等很多常用方法,這是Solidity封裝好的一個基本類型,適用於智能合約開發,以后用到了再詳細探究細節。
- 枚舉類型enum,例如“enum ColorEnums {Red, White, Black}”,注意返回的都是下標值,Red會返回0,White返回1,Black返回2。
- 函數類function,變量可以作為其他function的參數,也可以作為其他function的返回值。方法在參數后面可以聲明函數可見性,除了public(任意合約)和private(當前合約)以外,還有internal(當前合約以及繼承合約)和external(僅外部訪問)。external是由address和function簽名組成,可作為外部調用函數的參數或者返回值,默認情況無顯式聲明時就是internal。function還需要聲明返回值類型,returns (type),但若方法無返回值時要省略這個部分。另外還有特殊的部分是在internal和returns中間還可以加入一個配置屬性,[pure|constant|view|payable]。constant標識了一個常量不會被更改,只讀不可寫入。view是查看區塊鏈上的數據的意思,比constant更加准確地表達了只是看看,不做修改的意圖 。pure是純函數的意思,就是保證不讀取和寫入到區塊鏈內存的函數。payable是聲明了該函數設計支付操作,需要虛擬機提供事務支持。 protected,
下面是針對以上類型的字面量類型:
字面量是一種針對某種值的表示法,簡單來說,就是變量賦值時必須是等號右邊的部分。
- Address字面量,十六進制字面量的一種特殊情況:長度在為40個十六進制數(一個字節8位可存儲兩個十六進制數,一個4位),且通過了address checksum 校驗。
- 有理數整型字面量,整數,小數,科學計數法2e10,最廣泛的字面量類型。
- 字符串字面量,單引號、雙引號均可的字符串。
- 十六進制字面量,hex開頭,例如hex"001122FF",必須是一個字符串,內容必須是十六進制。
2.引用類型
- 數據位置類型,分為memory(內存-臨時)和storage(區塊鏈-永久),通過在變量名前聲明memory還是storage來定義該變量的數據位置。一般來講,函數參數默認為memory,局部復雜類型(作用域為局部)以及狀態變量(作用域為全局)屬於storage類型。還有一個calldata與memory差不多,專門用於存儲函數參數的,也不是永久存儲。額外提一點,EVM的memory是基於stack的,stack可以臨時存儲一些小的局部變量。這些變量存儲消耗的gas是不同的,storage最大,memory較小,stack幾乎免費,calldata與memory差不多。
- 數組類型Arrays,長度可定可變,可以存儲於storage和memory,元素類型可以是任何類型,但memory時不能是映射類型(就是鍵值對類型)。
- 結構體struct,與Go語言相同的設定,自定義類型,使用方式也與Go極為相似。
mapping類型
mapping類型就是鍵值對,現在最新語言都會給自身增加鍵值對數據結構的封裝支持。mapping的聲明方式為:
mapping(_KeyType => _ValueType)
鍵值對中間通過一個“=>”連接。元素內容,Solidity類型均可,與其他鍵值對使用差不多,遇到問題再深入研究。
其他
關於Solidity其他語法這里暫不過多介紹,掌握以上Solidity的類型知識,我想其他語法可以在實戰中解決掉。下面會以“Solidit語法補充說明”的形式對新遇到的語法問題進行補充研究。
Truffle MetaCoin環境搭建實例
上面我們開發部署運行智能合約helloworld時,編碼是在goLand,編譯是在Remix,部署運行是在geth console,感覺好混亂,也不適合大規模工程開發,是否有一種工具可以集成這一切?
Truffle!
准備工作
由於truffle是依賴於nodejs,可能會有版本不兼容的問題,因此要先完全刪除你機器上的nodejs和npm,然后再安裝純凈版的nodejs,npm,truffle,請按照以下命令進行。
sudo apt-get remove nodejs
sudo apt-get remove npm
sudo apt-get update
which node
wget https://nodejs.org/dist/v8.8.0/node-v8.8.0-linux-x64.tar.gz
sudo tar -xf node-v8.8.0-linux-x64.tar.gz --directory /usr/local --strip-components 1
node --version
npm --version
sudo npm install -g truffle
MetaCoin初始化
此時應該可以直接使用命令truffle了,下面我們建立一個工作間truffle-workspace,然后在工作間執行:
mkdir MetaCoin
cd MetaCoin
truffle unbox metacoin
原來使用truffle init,但現在它存在於unbox。
unbox
Truffle 的盒子Boxs裝有很多非常實用的項目樣板,可以讓你忽略一些環境配置問題,從而可以集中與開發你自己的DApp的業務唯一性。除此之外,Truffle Boxes能夠容納其他有用的組件、Solidity合約或者庫,前后端視圖等等。所有這些都是一個完整的實例Dapp程序。都可以下載下來逐一研究,尋找適合自己公司目前業務模型的組件。
可以看到,現在官方盒子還不多,總共7個,有三個是關於react的,兩個是truffle自己的項目,可以下載體驗,剩下兩個是我們比較關心的,一個是metacoin,非常好的入門示例,另一個是webpack,顧名思義,它是一套比起metacoin更加完整的模板的存在。既然我們是初學,下面我們就從metacoin入手學習。
目錄結構
進入metacoin目錄,當前目錄已經被初始化成一個新的空的以太坊工程,目錄結構如下:
- contracts
- ConvertLib.sol
- MetaCoin.sol
- Migrations.sol
- .placeholder
- migrations
- 1_initial_migration.js
- 2_deploy_contracts.js
- test
- metacoin.js
- TestMetacoin.sol
- .placeholder
- truffle-config.js
- truffle.js
初始化文件解釋1:Migrations.sol
pragma solidity ^0.4.17;
contract Migrations {
address public owner;
uint public last_completed_migration;
modifier restricted() {
if (msg.sender == owner) _;
}
function Migrations() public {
owner = msg.sender;
}
function setCompleted(uint completed) public restricted {
last_completed_migration = completed;
}
function upgrade(address new_address) public restricted {
Migrations upgraded = Migrations(new_address);
upgraded.setCompleted(last_completed_migration);
}
}
上面我們學習了Solidity具體的類型語法,我們來分析一下這個文件:
- 它定義了一個名字為“遷移”的合約
- 有一個任意訪問的全局變量,存儲於storage的地址類型變量owner
- 有一個可任意訪問的全局變量,存儲於storage的無符號整型類型的變量last_completed_migration
- modifier下面細說,此處略過
- msg.sender下面細說,此處略過
- 構造函數,初始化將發送方賦值給owner保存
- 一個setCompleted賦值方法,賦值給last_completed_migration,其中該方法被聲明為restricted,下面細說,此處略過
- upgrade方法,調用當前合約自己的方法,得到合約的實例upgraded,然后通過該是咧調用setCompleted賦值方法。
Solidity語法補充說明1:function modifier
modifier的使用方法,就看上面的Migrations合約的例子即可,它可以自動改變函數的行為,例如你可以給他預設一個條件,他會不斷檢查,一旦符合條件即可走預設分支。它可以影響當前合約以及派生合約。
pragma solidity ^0.4.11;
contract owned {
function owned() public { owner = msg.sender; }
address owner;
// 這里僅定義了一個modifier但是沒有使用,它將被子類使用,方法體在這里“_;”,這意味着如果owner調用了這個函數,函數會被執行,其他人調用會拋出一個異常。
modifier onlyOwner {
require(msg.sender == owner);
_;
}
}
// 通過is關鍵字來繼承一個合約類,mortal是owned的子類,也叫派生類。
contract mortal is owned {
// 當前合約派生了owned,此方法使用了父類的onlyOwner的modifier
// public onlyOwner, 這種寫法挺讓人困惑,下面給出了我的思考,暫理解為派生類要使用基類的modifier。
function close() public onlyOwner {
selfdestruct(owner);
}
}
contract priced {
// Modifiers可以接收參數
modifier costs(uint price) {
// 這里modifier方法體是通過條件判斷,是否滿足,滿足則執行“_;”分支。
if (msg.value >= price) {
_;
}
}
}
contract Register is priced, owned {
mapping (address => bool) registeredAddresses;
uint price;
// 構造函數給全局變量price賦值。
function Register(uint initialPrice) public { price = initialPrice; }
// payable關鍵字重申,如果不聲明的話,函數關於以太幣交易的操作都會被拒回。
function register() public payable costs(price) {
registeredAddresses[msg.sender] = true;
}
// 此派生類也要使用基類的modifier。
function changePrice(uint _price) public onlyOwner {
price = _price;
}
}
contract Mutex {
bool locked;
modifier noReentrancy() {
require(!locked);
locked = true;
_;
locked = false;
}
function f() public noReentrancy returns (uint) {
require(msg.sender.call());
return 7;
}
}
又延伸出來一個盲點:require關鍵字,它是錯誤判斷,提到assert就懂了,官方文檔的解釋為:
require(bool condition):
throws if the condition is not met - to be used for errors in inputs or external components.
總結一下modifier:
- 聲明modifier時,特殊符號“_;”的意思有點像TODO,是一個“占位符”,指出了你要寫的具體方法體內容的位置。
- function close() public onlyOwner,派生類某方法想“如虎添翼”加入基類的某個modifier功能,就可以這樣寫,這行的具體意思就是:close方法也必須是owner本人執行,否則報錯!
Solidity語法補充說明2:Restricting Access
限制訪問一種針對合約的常見模式。但其實你永遠不可能限制得了任何人或電腦讀取你的交易內容或者你的合同狀態。你可以使用加密增大困難,但你的合約就是用來讀取數據的,那么其他人也會看到。所以,其實上面的modifier onlyOwner是一個特別好的可讀性極高的限制訪問的手段。
那么restricted關鍵字如何使用呢?
好吧,我剛剛帶着modifier的知識重新看了上面的Migrations合約的內容發現,restricted並不是關鍵字,而是modifier的方法名,在其下的想增加該modifier功能的函數中,都使用了public restricted的方式來聲明。
說到這里,我又明白了為什么要使用public onlyOwner這種寫法,因為public是函數可見性修飾符,onlyOwner是自定義的限制訪問的modifier方法,他們都是關於函數使用限制方面的,所以會寫在一起,可以假想一個括號將它倆括起來,他們占一個位置,就是原來屬於public|private|internal|external的那個位置。
Solidity語法補充說明3:Special Variables and Functions
這一點很重要了,我們研究一下Solidity自身攜帶的特殊變量以及函數:
- block.blockhash(uint blockNumber) returns (bytes32): 返回參數區塊編號的hash值。(范圍僅限於最近256塊,還不包含當然塊)
- block.coinbase (address): 當前區塊礦工地址
- block.difficulty (uint): 當前區塊難度
- block.gaslimit (uint): 當前區塊的gaslimit
- block.number (uint): 當前區塊編號
- block.timestamp (uint): 當前區塊的timestamp,使用UNIX時間秒
- msg.data (bytes): 完整的calldata
- msg.gas (uint): 剩余的gas
- msg.sender (address): 信息的發送方 (當前調用)
- msg.sig (bytes4): calldata的前四個字節 (i.e. 函數標識符)
- msg.value (uint): 消息發送的wei的數量
- now (uint): 當前區塊的timestamp (block.timestamp別名)
- tx.gasprice (uint): 交易的gas單價
- tx.origin (address): 交易發送方地址(完全的鏈調用)
msg有兩個屬性,一個是msg.sender,另一個是msg.value,這兩個值可以被任何external函數調用,包含庫里面的函數。
注意謹慎使用block.timestamp, now and block.blockhash,因為他們都是有可能被篡改的。
初始化文件解釋2:MetaCoin.sol
pragma solidity ^0.4.18;
import "./ConvertLib.sol";
// 這是一個簡單的仿幣合約的例子。它並不是標准的可兼容其他幣或token的合約,
// 如果你想創建一個標准兼容的token,請轉到 https://github.com/ConsenSys/Tokens(TODO:一會兒我們再過去轉)
contract MetaCoin {
mapping (address => uint) balances;// 定義了一個映射類型變量balances,key為address類型,值為無符整型,應該是用來存儲每個賬戶的余額,可以存多個。
event Transfer(address indexed _from, address indexed _to, uint256 _value);// Solidity語法event,TODO:見下方詳解。
function MetaCoin() public {// 構造函數,tx.origin查查上面,找到它會返回交易發送方的地址,也就是說合約實例創建時會默認為當前交易發送方的余額塞10000,單位應該是你的仿幣。
balances[tx.origin] = 10000;
}
function sendCoin(address receiver, uint amount) public returns(bool sufficient) {// 函數聲明部分沒有盲點,方法名,參數列表,函數可見性,返回值類型定義。
if (balances[msg.sender] < amount) return false;// 如果余額不足,則返回發送幣失敗
balances[msg.sender] -= amount;// 否則從發送方余額中減去發送值,注意Solidity也有 “-=”,“+=” 的運算符哦
balances[receiver] += amount;// 然后在接收方的余額中加入發送值數量。
Transfer(msg.sender, receiver, amount);// 使用以上event關鍵字聲明的方法
return true;
}
function getBalanceInEth(address addr) public view returns(uint){// 獲取以太幣余額
return ConvertLib.convert(getBalance(addr),2);// 調用了其他合約的方法,TODO:稍后介紹ConvertLib合約時說明。
}
function getBalance(address addr) public view returns(uint) {// 獲取當前賬戶的仿幣余額
return balances[addr];
}
}
Solidity語法補充說明4:Events
Events allow the convenient usage of the EVM logging facilities, which in turn can be used to “call” JavaScript callbacks in the user interface of a dapp, which listen for these events.
Events提供了日志支持,進而可用於在用戶界面上“調用”dapp JavaScript回調,監聽了這些事件。簡單來說,我們的DApp是基於web服務器上的web3.js與EVM以太坊結點進行交互的,而智能合約是部署在EVM以太坊結點上的。舉一個例子:
contract ExampleContract {
// some state variables ...
function foo(int256 _value) returns (int256) {
// manipulate state ...
return _value;
}
}
合約ExampleContract有個方法foo被部署在EVM的一個結點上運行了,此時用戶如果想在DApp上調用合約內部的這個foo方法,如何操作呢,有兩種辦法:
- var returnValue = exampleContract.foo.call(2);// 通過web3 的message的call來調用。
- 合約內部再聲明一個event ReturnValue(address indexed _from, int256 _value);並在foo方法內使用該event用來返回方法執行結果。
第一種辦法在方法本身比較耗時的情況下會阻塞,或者不會獲取到准確的返回值。所以采用第二種辦法:就是通過Solidity的關鍵字event。event在這里就是一個回調函數的概念,當函數運行結束以后(交易進塊),會通過event返回給web3,也就是DApp用戶界面相應的結果。這是以太坊一種客戶端異步調用方法。關於這個回調,要在DApp使用web3時顯示編寫:
exampleEvent.watch(function(err, result) {
if (err) {
console.log(err)
return;
}
console.log(result.args._value)
// 檢查合約方法是否反返回結果,若有則將結果顯示在用戶界面並且調用exampleEvent.stopWatching()方法停止異步回調監聽。
})
寫Solidity最大的不同在於,我們要隨時計算好我們的gas消耗,方法的復雜度,變量類型的存儲位置(memory,storage等等)都會決定gas的消耗量。
使用event可以獲得比storage更便宜的gas消耗。
總結一下event,就是如果你的Dapp客戶端web3.js想調用智能合約內部的函數,則使用event作為橋梁,它能方便執行異步調用同時又節約gas消耗。
初始化文件解釋3:ConvertLib.sol
pragma solidity ^0.4.4;
library ConvertLib{
function convert(uint amount,uint conversionRate) public pure returns (uint convertedAmount)
{
return amount * conversionRate;
}
}
與MetaCoin智能合約不同的是,ConvertLib是由library聲明的一個庫,它只有一個方法,就是返回給定的兩個無符整數值相乘的結果。返回到上面的MetaCoin中該庫的使用位置去分析,即可知道,MetaCoin的仿幣的價格是以太幣的一倍,所以MetaCoin是以以太幣為標桿,通過智能合約發布的一個token,仿幣。
這似乎就可以很好地解決我在《以太坊RPC機制與API實例》文章中需要發布三倍以太幣的token的需求了,而我們完全不必更改以太坊源碼,但那篇文章通過這個需求的路線研究了以太坊的Go源碼也算功不可沒。
初始化文件解釋4:1_initial_migration.js
var Migrations = artifacts.require("./Migrations.sol");
module.exports = function(deployer) {
deployer.deploy(Migrations);
};
這個js文件是nodejs的寫法,看上去它的作用就是部署了上面的Migrations智能合約文件。
初始化文件解釋5:2_deploy_contracts.js
var ConvertLib = artifacts.require("./ConvertLib.sol");
var MetaCoin = artifacts.require("./MetaCoin.sol");
module.exports = function(deployer) {
deployer.deploy(ConvertLib);
deployer.link(ConvertLib, MetaCoin);
deployer.deploy(MetaCoin);
};
這個文件是meatcoin智能合約的部署文件,里面約定了部署順序,依賴關系。這里我們看到了MetaCoin智能合約是要依賴於庫ConvertLib的,所以要先部署ConvertLib,然后link他們,再部署MetaCoin,這部分js的寫法可以參照官方文檔DEPLOYER API,主要就是介紹了一下deploy、link以及then三個方法的詳細用法,不難這里不再贅述。
初始化文件解釋6:truffle-config.js, truffle.js
module.exports = {
// See <http://truffleframework.com/docs/advanced/configuration>
// to customize your Truffle configuration!
};
module.exports = {
// See <http://truffleframework.com/docs/advanced/configuration>
// to customize your Truffle configuration!
};
這兩個文件也都是nodejs,他們都是配置文件,可能作用域不同,目前它倆是完全相同的(因為啥也沒有)。我們去它推薦的網站看一看。給出了一個例子:
module.exports = {
networks: {
development: {
host: "127.0.0.1",
port: 8545,
network_id: "*" // Match any network id
}
}
};
這個例子展示了該配置文件可以配置網絡環境,暫先到這,以后遇上了針對該配置文件進行研究。
初始化文件解釋7:.placeholder
This is a placeholder file to ensure the parent directory in the git repository. Feel free to remove.
翻譯過來就是:placeholder文件是用來保證在git庫中父級目錄的,可以刪除。
初始化文件解釋8:metacoin.js
和下面的文件一樣,他們的功能都是用來做單元測試的,truffle在編譯期間會自動執行這些測試腳本。當前文件為js版本,模擬用戶在DApp客戶端用戶界面操作的情形。
var MetaCoin = artifacts.require("./MetaCoin.sol"); // 這與1_initial_migration.js文件的頭是一樣的,引入了一個智能合約文件。
contract('MetaCoin', function(accounts) {
it("should put 10000 MetaCoin in the first account", function() {
return MetaCoin.deployed().then(function(instance) {
return instance.getBalance.call(accounts[0]);
}).then(function(balance) {
assert.equal(balance.valueOf(), 10000, "10000 wasn't in the first account");
});
});
it("should call a function that depends on a linked library", function() {
var meta;
var metaCoinBalance;
var metaCoinEthBalance;
return MetaCoin.deployed().then(function(instance) {
meta = instance;
return meta.getBalance.call(accounts[0]);
}).then(function(outCoinBalance) {
metaCoinBalance = outCoinBalance.toNumber();
return meta.getBalanceInEth.call(accounts[0]);
}).then(function(outCoinBalanceEth) {
metaCoinEthBalance = outCoinBalanceEth.toNumber();
}).then(function() {
assert.equal(metaCoinEthBalance, 2 * metaCoinBalance, "Library function returned unexpected function, linkage may be broken");
});
});
it("should send coin correctly", function() {
var meta;
// Get initial balances of first and second account.
var account_one = accounts[0];
var account_two = accounts[1];
var account_one_starting_balance;
var account_two_starting_balance;
var account_one_ending_balance;
var account_two_ending_balance;
var amount = 10;
return MetaCoin.deployed().then(function(instance) {
meta = instance;
return meta.getBalance.call(account_one);
}).then(function(balance) {
account_one_starting_balance = balance.toNumber();
return meta.getBalance.call(account_two);
}).then(function(balance) {
account_two_starting_balance = balance.toNumber();
return meta.sendCoin(account_two, amount, {from: account_one});
}).then(function() {
return meta.getBalance.call(account_one);
}).then(function(balance) {
account_one_ending_balance = balance.toNumber();
return meta.getBalance.call(account_two);
}).then(function(balance) {
account_two_ending_balance = balance.toNumber();
assert.equal(account_one_ending_balance, account_one_starting_balance - amount, "Amount wasn't correctly taken from the sender");
assert.equal(account_two_ending_balance, account_two_starting_balance + amount, "Amount wasn't correctly sent to the receiver");
});
});
});
我們來分析一波這個truffle metacoin js版本的單元測試:
- 直接函數contract走起,第一個參數為智能合約名字,第二個參數為匿名內部函數
- 匿名函數傳入了當前賬戶地址,函數體是單元測試集
- 每個單元測試是由關鍵字it函數來做,第一個參數傳入單元測試的comments,第二個參數傳入一個無參匿名函數
- 進到無參匿名函數的函數體內,就是正式的單元測試內容,可以定義自己的成員屬性,通過調用truffle內部組件自動部署合約逐一測試,使用成員屬性接收返回值,最后使用關鍵字assert來判斷是否符合預期。具體業務不詳細展開,可根據自己業務內容隨意更改。
這是官方文檔,詳細說明如何使用JS來編寫智能合約的單元測試。
初始化文件解釋9:TestMetacoin.sol
好下面來看看Solidity智能合約版本的單元測試。一般來講,這種文件的命名規則是Test加待測智能合約的名字拼串組成。
pragma solidity ^0.4.2;
import "truffle/Assert.sol";
import "truffle/DeployedAddresses.sol";
import "../contracts/MetaCoin.sol";
contract TestMetacoin {
function testInitialBalanceUsingDeployedContract() public {
MetaCoin meta = MetaCoin(DeployedAddresses.MetaCoin());
uint expected = 10000;
Assert.equal(meta.getBalance(tx.origin), expected, "Owner should have 10000 MetaCoin initially");
}
function testInitialBalanceWithNewMetaCoin() public {
MetaCoin meta = new MetaCoin();
uint expected = 10000;
Assert.equal(meta.getBalance(tx.origin), expected, "Owner should have 10000 MetaCoin initially");
}
}
繼續分析:
- 首先import了truffle的幾個類庫,用來支持我們接下來的測試內容。然后import了待測智能合約。
- 建立單元測試智能合約,根據合約不同方法定義對應的test測試方法。
- 方法體內部去調用待測智能合約的方法,傳參接收返回值,然后使用關鍵字assert判斷是否符合預期。
這是官方文檔,詳細說明如何使用Solidity來編寫智能合約的單元測試。
編譯合約
鍵入
truffle compile
輸出情況:
liuwenbin@liuwenbin-H81M-DS2:~/work/truffle-workspace/MetaCoin$ truffle compile
Compiling ./contracts/ConvertLib.sol...
Compiling ./contracts/MetaCoin.sol...
Compiling ./contracts/Migrations.sol...
Writing artifacts to ./build/contracts
根據編譯輸出的路徑地址./build/contracts,我們去查看一下
liuwenbin@liuwenbin-H81M-DS2:~/work/truffle-workspace/build/contracts$ ls
ConvertLib.json MetaCoin.json Migrations.json
可以看到原來所在在contracts目錄下的智能合約文件(有合約contract,有庫library)均被編譯成了json文件。
這些json文件就是truffle用來部署合約的編譯文件,這與上面通過Remix編譯的WEB3DEPLOY的js代碼段不同。
部署合約
移植,對這里叫移植,但下面我們仍使用“部署”這個詞,truffle中部署的命令為:
truffle migrate
這里遇到的問題較多,我來一一解決:
問題1:啟動本地以太坊客戶端結點
以太坊客戶端有很多,truffle自己就有一個ganache,但我沒安裝成功,下面列舉一下:
- Geth (go-ethereum)也就是我們之前到現在一直在介紹的。
- WebThree (cpp-ethereum): C++版本,我們一直在使用的是上面的Go版本。
- Parity
- More: https://www.ethereum.org/cli
當然了,我們還是繼續使用geth,仍舊使用上面介紹過的啟動命令啟動
geth --datadir testNet --dev console 2>>Document/someLogs/testGeth.log
問題2:配置truffle.js
上文說到了,truffle.js是truffle的配置文件,啟動好以太坊本地結點以后,我們需要讓truffle去識別它並使用它,這就需要在truffle.js中配置相關屬性:
module.exports = {
networks: {
development: {
host: "127.0.0.1",
port: 8545,
network_id: "*" // Match any network id
}
}
};
問題3:修改geth啟動命令與truffle.js配置文件
以上兩個問題解決以后,我們使用truffle migrate來部署,terminal報錯:
liuwenbin@liuwenbin-H81M-DS2:~/work/truffle-workspace/MetaCoin$ truffle migrate
Could not connect to your Ethereum client. Please check that your Ethereum client:
- is running
- is accepting RPC connections (i.e., "--rpc" option is used in geth)
- is accessible over the network
- is properly configured in your Truffle configuration file (truffle.js)
錯誤信息很清楚,直接增加一個參數--rpc,最終修改我們的啟動命令為:
geth --datadir testNet --dev --rpc console 2>>Document/someLogs/testGeth.log
繼續使用truffle migrate來部署,terminal及繼續報錯:
Error: exceeds block gas limit
去truffle github issues中查找,找到一行解決辦法,粘貼如下:
Possibility: you're giving the transaction too high of a gasLimit. If the transaction has a limit of 2,000,000, it'd stop you since it could theoretically go over the block gas limit, even if in practice it won't. If this is the case, see if you can reduce the transaction's gasLimit while remaining above the amount it actually needs--that might do the trick.
好,我們再修改一下truffle.js如下:
module.exports = {
networks: {
development: {
host: "127.0.0.1",
port: 8545,
network_id: "*", // Match any network id
gas:500000
}
}
};
繼續執行truffle migrate,執行成功。
liuwenbin@liuwenbin-H81M-DS2:~/work/truffle-workspace/MetaCoin$ truffle migrate
Using network 'development'.
Running migration: 1_initial_migration.js
Deploying Migrations...
... 0x2adf8c421a2814ea4d5f1a211048ac64c47f6fcf64a1418dd4abc463d604d8fc
此時terminal處於監聽狀態,我們先不管他,下面請轉到“IDE cooking steps”章節會給出解釋。
去看一下Documents/someLogs/testGeth.log文件:
INFO [02-08|14:59:39] Submitted contract creation fullhash=0x2adf8c421a2814ea4d5f1a211048ac64c47f6fcf64a1418dd4abc463d604d8fc contract=0xc8B95403276e5B4482718803C25A449743d59755
INFO [02-08|14:59:39] Commit new mining work number=23 txs=1 uncles=0 elapsed=351.917µs
INFO [02-08|14:59:39] Successfully sealed new block number=23 hash=b97b83…b19548
INFO [02-08|14:59:39] 🔨 mined potential block number=23 hash=b97b83…b19548
我截取到了日志文件中以上的部分,可以看到,我們的智能合約已經被成功部署了,且日志中的hash值與上面監聽狀態的terminal中顯式的是相同的,說明是一致的。下面我們就可以在終端使用該智能合約了。
測試合約
上面我們介紹了智能合約的單元測試的寫法,包括js版本和Solidity版本,我們也知道在執行編譯時會自動執行這些單元測試,如果有一個測試未通過則會中斷編譯過程。而在開發階段,我們也可以自己使用命令來測試。
truffle test
沒有報錯就說明通過了,綠條,有報錯就會打印在下方。
使用truffle開發智能合約
經過上面truffle metacoin環境模板的搭建,我們整個智能合約的開發、編譯、部署以及運行環境就搭建好了。下面我們用這套環境來重現最初的helloworld智能合約。
創建工程
首先創建我們的工程Helloworld:
liuwenbin@liuwenbin-H81M-DS2:~/work/truffle-workspace$ mkdir helloworld && cd helloworld
liuwenbin@liuwenbin-H81M-DS2:~/work/truffle-workspace/helloworld$ truffle init
Downloading...
Unpacking...
Setting up...
Unbox successful. Sweet!
Commands:
Compile: truffle compile
Migrate: truffle migrate
Test contracts: truffle test
liuwenbin@liuwenbin-H81M-DS2:~/work/truffle-workspace/helloworld$ ls
contracts migrations test truffle-config.js truffle.js
IDE cooking steps
- 仍舊使用goLand + Solidity插件
- *導入項目truffle-workspace/helloworld
- 在contracts目錄下新建一個智能合約文件Helloworld.sol(注意要與工程名相同,同時最好都首字母大寫),輸入與上文相同的內容。
- *然后在IDE內部打開一個terminal,啟動EVM
liuwenbin@liuwenbin-H81M-DS2:~$ geth --datadir testNet --dev --rpc console
- *將上面的truffle.js配置內容粘貼到當前工程的truffle.js配置文件中
- 新增一個helloworld的部署文件“2_deploy_helloworld.js”到migrations目錄下,添加對helloworld智能合約的部署配置。
- 再開啟一個terminal,執行truffle compile, truffle migrate。
WARN: 這一步遇到問題,上面所謂監聽狀態實際上是卡住了,我們的智能合約並未部署成功,雖然在EVM中已經寫入了塊,但是無法識別該合約對象。理想狀態下我們可以調用合約對象了,這個流程就全通了,但是沒事,我去繼續查一下解決方案。
采用客戶端ganache代替geth
上文說明了這些原因,我也在官網下載了ganache,這是一個AppImage文件,這個文件在linux系統可以直接啟動,首先我們需要將它的執行權限修改一下,然后啟動即可。
chmod a+x exampleName.AppImage
啟動以后,可以看到這個界面。
很豐滿。
我想到一個事情,這里重申一下:我目前的測試開發環境,如果沒有交易產生,挖礦不會自動進行。對於比特幣和以太坊的正式環境來說,他們會限制出塊時間,因為現在他們的交易量都很大,交易就會被拖慢,而不會產生沒有交易,到了固定時間就要出個空塊的情況。不過也有特例,因為共識算法加上對出塊時間的限制,是有可能出現空塊的。這很浪費,不過就我目前來看,算是留個思考題吧。
我們應該都可以直觀的看懂,然后我們將它的網絡配置到工程的truffle.js中去。
我們仍舊可以使用命令“geth attach http://localhost:7545” ,從geth命令行attach到這個ganache EVM網絡中去。
truffle migrate
配置完成后,繼續執行以上命令,可以看到不再發生以上被卡住的情況了,但是不識別我的Helloworld智能合約:
Error: Could not find artifacts for Helloworld.sol from any sources
繼續探索...
解決方案:居然是我的contract 名字不匹配的原因,因為我當時想統一將工程名、合約文件名都改為首字母大寫,但忘記該合約文件內部的contract后面的名字了,以及構造函數,這就像你改了java的類文件名,但沒有該內部類名一樣,可惜goland的Solidity插件並未報錯啊,害的我找了半天,不過以后還是要靠自己多注意了。
但是,仍然有問題:
Error encountered, bailing. Network state unknown. Review successful transactions manually.
應該是truffle.js中網絡配置的問題。
繼續探索...
解決方案:哥們定睛一看,在上面這個表明看起來的error面前,不要先入為主,下面還有一行報錯信息:
Error: Helloworld contract constructor expected 1 arguments, received 0
原來是我的合約內部有問題,我們通過truffle部署的時候不知道如何去給構造函數賦值,當時我們使用Remix的時候是手動修改的WEB3DEPLOY的js代碼段,這里我就直接在合約代碼中修改吧,最后是這樣:
pragma solidity ^0.4.0;
contract Helloworld {
string content;
function Helloworld() public {
content = "hello, world!";
}
function getContent() constant public returns (string){
return content;
}
}
多謝博友moqiang02的友情提示,這里可以在部署時進行構造函數的賦值,不必修改智能合約內容:在2_deploy_contracts.js中,修改deploy腳本,“deployer.deploy(Helloworld,"hello, world!");”即可。下面所有流程不影響,繼續
truffle migrate!
liuwenbin@liuwenbin-H81M-DS2:~/work/truffle-workspace/Helloworld$ truffle migrate
Using network 'development'.
Running migration: 2_deploy_contracts.js
Deploying Helloworld...
... 0x391f2c060b1f9cbe7b42493fc858ffa455d40f6e9af754a105092a9ac32e53c3
Helloworld: 0x2c2b9c9a4a25e24b174f26114e8926a9f2128fe4
Saving successful migration to network...
... 0x0e8fab8924d93f0b17aa1c9dc58b976089a61e4debcd185dffa2c16e5cc539e9
Saving artifacts...
liuwenbin@liuwenbin-H81M-DS2:~/work/truffle-workspace/Helloworld$
成功!
對比ganache日志來看:
[5:24:49 PM] Transaction: 0x391f2c060b1f9cbe7b42493fc858ffa455d40f6e9af754a105092a9ac32e53c3
[5:24:49 PM] Contract created: 0x2c2b9c9a4a25e24b174f26114e8926a9f2128fe4
[5:24:49 PM] Gas usage: 205611
[5:24:49 PM] Block Number: 7
[5:24:49 PM] Block Time: Thu Feb 08 2018 17:24:49 GMT+0800 (CST)
[5:24:49 PM] Transaction: 0x0e8fab8924d93f0b17aa1c9dc58b976089a61e4debcd185dffa2c16e5cc539e9
[5:24:49 PM] Gas usage: 26981
[5:24:49 PM] Block Number: 8
[5:24:49 PM] Block Time: Thu Feb 08 2018 17:24:49 GMT+0800 (CST)
可以看到我們通過truffle部署一個智能合約,要提交兩個塊,有兩筆交易產生。為什么呢?
因為第一筆交易是來自與Helloworld.sol的創建,第二筆交易是來自於migration,每次部署一個新的合約都要執行這兩步。
部署成功以后,我們可以得到合約的地址:0x2c2b9c9a4a25e24b174f26114e8926a9f2128fe4,后面會使用這個地址來實現與合約的交互。
與合約交互
geth 方式
此時如果我們直接geth attach到ganache本地環境中,無法與合約實現交互。因為目前雖然我們在EVM中創建了一個合約,但未在基於web3js的geth中注冊合約對象,
geth 中是通過abi來注冊合約對象的。
首先我們找到build/contracts/Helloworld.json中的abi的value,通過json壓縮成一行,
abi = [{"inputs":[{"name":"con","type":"string"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"name":"arg","type":"string"}],"name":"GetGreeting","type":"event"},{"constant":true,"inputs":[],"name":"getContent","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"}]
然后注冊合約對象:
hello = eth.contract(abi).at('0x2c2b9c9a4a25e24b174f26114e8926a9f2128fe4')
對象注冊成功以后,就可以像正常合約那樣去調用了。
> hello.getContent()
"hello, world!"
truffle console
truffle框架沒有直接使用abi,而是為我們封裝提供了更加方便的調用方式。
- truffle console,一個基本的交互控制台,可以連接任何EVM客戶端。如果你已經有了自己的ganache或者geth等EVM的本地環境,那么就可以使用truffle console來交互,所以如果你已經有一個現成的小組共享的開發用EVM,那么使用這個沒錯。
- truffle develop,一個交互控制台,啟動時會自動生成一個開發用區塊鏈環境(其實我認為它與ganache就是一個底層實現機制,都是默認生成10個賬戶)。如果你沒有自己的EVM環境的話,直接使用truffle develop非常方便。
我雖然希望能夠得到大一統的簡單編寫的開發測試環境,但是我並不願意使用develop模式,下面我們使用console模式來與剛剛部署的Helloworld智能合約進行交互。
truffle console
執行以后,我們可以敲出Helloworld了,打印出一個json結構,展示了它的各種屬性內容。它是一個TruffleContract,內容非常多。
tip: 上面提到過Solidity的event語法,里面展示了如果針對未使用event的智能合約,要通過var returnValue = exampleContract.foo.call(2);// 通過web3 的message的call來調用。
我們的Helloworld合約並未使用event方法,所以讓我嘗試一下這種方式來調取:
truffle(development)> Helloworld.at("0x2c2b9c9a4a25e24b174f26114e8926a9f2128fe4").getContent.call()
'hello, world!'
此刻的心情真是揚眉吐氣,從來沒有一次這么艱難的“helloworld”歷程!
調試合約
truffle debug我還沒來得及體驗,先使用Remix吧,等我日后體驗完覺得它不錯我再來補充。Remix的debug其實還不錯,不過很多人好像用不明白。我這里簡單介紹一下吧,當你編寫完一個智能合約以后,一般它會自動幫你編譯,並且會在下方展示出你的屬性,方法(如果沒有的話,請嘗試去交易的位置把交易和gas配置一下即可),然后點擊其中你想調試的方法(注意入參),在控制台會打印出它的執行過程,同時右側會有一個“debug”的小按鈕,點擊它(注意要預先在代碼中設置斷點),然后就可以按行調試了,隨着一行行的運行,屬性變量的值也會有所改變。
總結
今天是2017農歷最后一個工作日,此時周圍早已心飛揚的同事們呼呼啦啦地走光了,我剛剛完成了這篇文章,孤零零的我卻滿腹成就感。本篇文章仍舊采取我的以往習慣,采用主線分支的路線,詳細介紹了如何開發一個智能合約,這里面把我這一條路線上遇到的所有的坑都趟過了,重點研究了Solidity的語法(當然並不是全面的,我只研究相關的了),智能合約的開發環境,各種新鮮工具的使用,最后着重介紹了智能合約的大殺器——truffle。希望能夠對您有所幫助,一起努力!
參考資料
基本全部來自於各種官方文檔,stackoverflow,askUbuntu,github issues等網站,沒有實體書,這種新知識實體書永遠是滯后太多的。