關鍵字:智能合約,remix,Solidity,truffle,geth,leveldb,datadir,ganache,web3jjavascript
合約也稱合同、協議,是甲乙雙方參與的,制定一系列條目規範雙方權利與義務的文件。智能合約是電子化的,自動執行的,去中心化的,具備不可抵賴性,本質上它是一段代碼,依託於區塊鏈技術,它能夠作不少事情,基於以太坊的智能合約可讓你的區塊鏈擴展出任何你想要的功能。html
我相信,智能合約是區塊鏈的將來,由於基於它能作的商業模型太多樣了,遠遠不只是數字貨幣一種。java
智能合約的編程語言是Solidity,擴展名爲.sol,它是基於C++、JavaScript、Python創造而來的,這裏是官方文檔。node
Solidity是靜態類型的,支持繼承,有本身的函數庫,它一樣支持面嚮對象語言的自定義類型等其餘功能。react
Solidity編寫的智能合約代碼運行在EVM,即以太坊虛擬機,正如java編寫的代碼運行在JVM同樣,在同一個區塊鏈中每個結點的EVM都是相同的運行環境。經過智能合約,能夠開發匿名投票、匿名拍賣、衆籌以及多重簽名的錢包等,以太坊每個結點能夠有多個帳戶,因此每一個結點均可以稱做錢包,能夠管理名下的帳戶,以及轉帳、挖礦等操做。linux
官方推薦IDE:Remixwebpack
其實Solidity智能合約開發的IDE有不少,官方推薦的Remix是基於瀏覽器的,運行環境能夠切換:git
我使用之後,以爲瀏覽器的方式仍是不習慣,尤爲保存的文件無端消失,讓我始終心有餘悸,通過調研,下面咱們將採用goLand,安裝Intellij-Solidity-2.0.4插件的方式開發智能合約,而後使用Remix環境進行智能合約的部署。固然咱們也可使用Remix進行運行、測試以及調試工做,下面酌情展現。github
區塊鏈中比較有意思的命名,至關於手續費但又有些不一樣。gas爲自然氣,用來表明咱們程序運行全部的能耗,當發生交易等操做時會消耗相應的gas,gas的計算方式是web
gas 單價 × gas 數量
其中gas單價是由用戶,像咱們這樣的發起者願意爲這次操做付出多少以太幣而定的(至關於你開車上路前願意給你的油箱加多少油,假設你的油箱是無限大的)。gas數量是程序根據你操做的複雜度自動定義的。
智能合約也是同樣的,當一個發起者部署運行一段智能合約時,以太坊會收取gas費用,就像汽車行駛須要燒油同樣,直到你的智能合約運行完畢,「油箱」中剩餘的gas會退還給你,若是你的代碼死循環了,耗盡了你「油箱」中的gas,那麼以太坊會自動報出異常中止你的智能合約。咱們在學習智能合約階段,可使用testnet環境來避免真的花費以太幣。
Dapp爲Solidity提供了源碼構建工具,包管理工具,單元測試以及智能合約部署,一下子咱們看看是否必需要用它。有時它也被稱做去中心化的應用程序(Decentralized App)。這種應用程序除了有一段代碼的智能合約之外,還須要UI,UE設計等,正如apple的app開發,咱們將來的目標之一能夠是開發本身的Dapp。
首先要開啓一個本地的EVM,前面的文章對Geth作了詳細的介紹,這裏直接啓動一個本地開發模式的結點。
geth --datadir testNet --dev console 2>>Documents/someLogs/testGeth.log
簡介一下geth的參數選項:
Ephemeral proof-of-authority network with a pre-funded developer account, mining enabled
短暫的認證證實網絡,同時建立一個預存款不少錢的一個開發者帳戶,並自動開始挖礦。
datadir,指定結點文件目錄,若是沒有會自動建立一個,該目錄包含:
以上目錄中元素精解:
結點之間相互尋找是經過一個發現協議:一個基於S/Kademlia的網絡協議。這個協議會把包含IP地址的公鑰聯繫起來。實際上在結點之間的peer鏈接使用的是一個徹底不一樣的,加密的協議(RLPX)。RLPX加密的工做方式須要遠程終端鏈接發起者的公鑰做爲身份識別。本質上來講,這個key連接了發現協議和RLPX。
你能夠隨時刪除這個nodekey,重啓的時候會自動生成一個新的。
這是存儲結點私鑰的位置,文件名爲時間戳加上本地帳戶拼成的字符串。打開文件,內容爲一個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 }
這部分範疇屬於密碼學方面了,能夠參考《應用密碼學初探》
RLP(Recursive Length Prefix),遞歸長度前綴。是以太坊中用於序列號對象的主要編碼方法。根據文件名能夠猜出,這是全部交易的序列化對象文件。
數據庫採用leveldb,存儲了區塊數據以及狀態數據。該目錄下打包存儲以.ldb爲擴展名的每一個區塊的數據文件。每一個塊文件有容量的最大值,目前我本機默認的是2.1M,咱們設想一下目前以太坊的區塊高度爲5039768,若是一個塊是2.1M的話,那麼整個區塊鏈的數據大小爲10TB。
Google出品的另外一利器,使用C++編寫,基於LSM(Log-Structured-Merge Tree)日誌結構化合並樹,是一個高效的鍵值對存儲系統,是沒有Sql語句的非關係型數據庫。鍵值對均採用字符串類型,按照key排序。
特色包括:
侷限性包括:
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
咱們逐行分析,
下面在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這個數字可讀性還行,因此拿這個測試也能夠。
上面說道了咱們採用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; } }
代碼編寫很簡單,咱們逐行解讀:
上面咱們使用了goLand的Solidity插件進行了合約代碼的開發,然而該插件的功能僅包括:
能夠說都是針對編碼輔助的操做,然而若咱們要部署智能合約,還得回到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
每當咱們提交了一個合約,
上面使用Solidity編寫了一個helloworld智能合約,稍顯力不從心,下面咱們專門來學習一下Solidity語法,爲將來咱們編寫複雜的智能合約工程打下基礎。
學習一門新的編程語言,首先要看它的類型,Solidity是靜態類型語言,跟java同樣,也就是說在編譯以前都要指定好每一個變量的具體類型。類型能夠分爲值類型和引用類型,與java相似。
值類型做爲參數時永遠傳的是值,每一次入參出參都是內存中值的副本。包括:
下面是針對以上類型的字面量類型:
字面量是一種針對某種值的表示法,簡單來講,就是變量賦值時必須是等號右邊的部分。
mapping類型就是鍵值對,如今最新語言都會給自身增長鍵值對數據結構的封裝支持。mapping的聲明方式爲:
mapping(_KeyType => _ValueType)
鍵值對中間經過一個「=>」鏈接。元素內容,Solidity類型都可,與其餘鍵值對使用差很少,遇到問題再深刻研究。
關於Solidity其餘語法這裏暫不過多介紹,掌握以上Solidity的類型知識,我想其餘語法能夠在實戰中解決掉。下面會以「Solidit語法補充說明」的形式對新遇到的語法問題進行補充研究。
上面咱們開發部署運行智能合約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
此時應該能夠直接使用命令truffle了,下面咱們創建一個工做間truffle-workspace,而後在工做間執行:
mkdir MetaCoin cd MetaCoin truffle unbox metacoin
原來使用truffle init,但如今它存在於unbox。
Truffle 的盒子Boxs裝有不少很是實用的項目樣板,可讓你忽略一些環境配置問題,從而能夠集中與開發你本身的DApp的業務惟一性。除此以外,Truffle Boxes可以容納其餘有用的組件、Solidity合約或者庫,先後端視圖等等。全部這些都是一個完整的實例Dapp程序。均可如下載下來逐一研究,尋找適合本身公司目前業務模型的組件。
能夠看到,如今官方盒子還很少,總共7個,有三個是關於react的,兩個是truffle本身的項目,能夠下載體驗,剩下兩個是咱們比較關心的,一個是metacoin,很是好的入門示例,另外一個是webpack,顧名思義,它是一套比起metacoin更加完整的模板的存在。既然咱們是初學,下面咱們就從metacoin入手學習。
進入metacoin目錄,當前目錄已經被初始化成一個新的空的以太坊工程,目錄結構以下:
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具體的類型語法,咱們來分析一下這個文件:
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 onlyOwner是一個特別好的可讀性極高的限制訪問的手段。
那麼restricted關鍵字如何使用呢?
好吧,我剛剛帶着modifier的知識從新看了上面的Migrations合約的內容發現,restricted並非關鍵字,而是modifier的方法名,在其下的想增長該modifier功能的函數中,都使用了public restricted的方式來聲明。
說到這裏,我又明白了爲何要使用public onlyOwner這種寫法,由於public是函數可見性修飾符,onlyOwner是自定義的限制訪問的modifier方法,他們都是關於函數使用限制方面的,因此會寫在一塊兒,能夠假想一個括號將它倆括起來,他們佔一個位置,就是原來屬於public|private|internal|external的那個位置。
這一點很重要了,咱們研究一下Solidity自身攜帶的特殊變量以及函數:
msg有兩個屬性,一個是msg.sender,另外一個是msg.value,這兩個值能夠被任何external函數調用,包含庫裏面的函數。
注意謹慎使用block.timestamp, now and block.blockhash,由於他們都是有可能被篡改的。
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]; } }
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方法,如何操做呢,有兩種辦法:
第一種辦法在方法自己比較耗時的狀況下會阻塞,或者不會獲取到準確的返回值。因此採用第二種辦法:就是經過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消耗。
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源碼也算功不可沒。
var Migrations = artifacts.require("./Migrations.sol"); module.exports = function(deployer) { deployer.deploy(Migrations); };
這個js文件是nodejs的寫法,看上去它的做用就是部署了上面的Migrations智能合約文件。
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三個方法的詳細用法,不難這裏再也不贅述。
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 } } };
這個例子展現了該配置文件能夠配置網絡環境,暫先到這,之後趕上了針對該配置文件進行研究。
This is a placeholder file to ensure the parent directory in the git repository. Feel free to remove.
翻譯過來就是:placeholder文件是用來保證在git庫中父級目錄的,能夠刪除。
和下面的文件同樣,他們的功能都是用來作單元測試的,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版本的單元測試:
這是官方文檔,詳細說明如何使用JS來編寫智能合約的單元測試。
好下面來看看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"); } }
繼續分析:
這是官方文檔,詳細說明如何使用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
這裏遇到的問題較多,我來一一解決:
以太坊客戶端有不少,truffle本身就有一個ganache,但我沒安裝成功,下面列舉一下:
固然了,咱們仍是繼續使用geth,仍舊使用上面介紹過的啓動命令啓動
geth --datadir testNet --dev console 2>>Document/someLogs/testGeth.log
上文說到了,truffle.js是truffle的配置文件,啓動好以太坊本地結點之後,咱們須要讓truffle去識別它並使用它,這就須要在truffle.js中配置相關屬性:
module.exports = { networks: { development: { host: "127.0.0.1", port: 8545, network_id: "*" // Match any network id } } };
以上兩個問題解決之後,咱們使用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 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內部打開一個terminal,啓動EVM
liuwenbin@liuwenbin-H81M-DS2:~$ geth --datadir testNet --dev --rpc console
再開啓一個terminal,執行truffle compile, truffle migrate。
WARN: 這一步遇到問題,上面所謂監聽狀態其實是卡住了,咱們的智能合約並未部署成功,雖然在EVM中已經寫入了塊,可是沒法識別該合約對象。理想狀態下咱們能夠調用合約對象了,這個流程就全通了,可是沒事,我去繼續查一下解決方案。
上文說明了這些緣由,我也在官網下載了ganache,這是一個AppImage文件,這個文件在linux系統能夠直接啓動,首先咱們須要將它的執行權限修改一下,而後啓動便可。
chmod a+x exampleName.AppImage
啓動之後,能夠看到這個界面。
很豐滿。
我想到一個事情,這裏重申一下:我目前的測試開發環境,若是沒有交易產生,挖礦不會自動進行。對於比特幣和以太坊的正式環境來講,他們會限制出塊時間,由於如今他們的交易量都很大,交易就會被拖慢,而不會產生沒有交易,到了固定時間就要出個空塊的狀況。不過也有特例,由於共識算法加上對出塊時間的限制,是有可能出現空塊的。這很浪費,不過就我目前來看,算是留個思考題吧。
咱們應該均可以直觀的看懂,而後咱們將它的網絡配置到工程的truffle.js中去。
咱們仍舊可使用命令「geth attach http://localhost:7545」 ,從geth命令行attach到這個ganache EVM網絡中去。
配置完成後,繼續執行以上命令,能夠看到再也不發生以上被卡住的狀況了,可是不識別個人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 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框架沒有直接使用abi,而是爲咱們封裝提供了更加方便的調用方式。
我雖然但願可以獲得大一統的簡單編寫的開發測試環境,可是我並不肯意使用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等網站,沒有實體書,這種新知識實體書永遠是滯後太多的。