我已經智能合約領域工作了4年,主要在比特幣區塊鏈上。我參與的一些項目包括存在證明,bitcore(比特核心)以及Streamium. 過去這個月,我探索了在以太坊平臺上進行開發。
我決定制作一個簡短的指南服務未來想要學習以太坊開發的程序員。手冊分為兩個部分:如何開始以太坊智能合約開發,智能合約安全簡述.
如何開始學習以太坊智能合約
0.基礎概念
這個指南假設你已經有了一些密碼學貨幣和區塊鏈的基礎技術背景。 如果你沒有,我建議快速過一遍Andreas Antonopoulos的《完全掌握比特幣》(Mastering Bitcoin),Consensys的《用剛剛夠的比特幣來搞懂以太坊》(Just Enough Bitcoin for Ethereum),或者至少看看Scott Driscoll的短片。 為了繼續讀下去你得了解公鑰和私鑰,為什么區塊鏈需要礦工,如何達成去中心化的共識,以及交易腳本和智能合約的概念。
另外兩個在你開始進行以太坊開發之前需要了解的重要的,相關的概念是以太坊虛擬機和汽油(gas)。以太坊的目的在于成為一個智能合約平臺。它的起源可以被追溯到Vitalik Buterin對比特幣做為智能合約平臺具有的局限性的評論。以太坊虛擬機(EVM)是以太坊智能合約執行之處。與比特幣相比,它為撰寫合約提供了更具表現力和完整性的語言。事實上,它是一個圖靈完備的編程語言。一個比較好的比喻是,EVM是一個執行智能合約的分布式的世界電腦。由于智能合約由EVM執行, 必須存在一種限制每個合約占用資源的機制。EVM內運行的每一步操作實際上同時在被所有節點所執行。這是為什么需要有汽油(gas)存在。一個以太坊合約代碼交易可以引發數據讀寫,密碼學原語,調動(發送信息給)其他合約等等昂貴的運算。每個此類運算都有用汽油計量的價格,每筆交易所耗費的汽油單元需要用以太幣來支付,根據隨時變化的汽油和以太幣的匯率計算。相應的價格會從提交交易請求的以太坊賬戶中扣除。同時每筆交易對可使用的汽油會設置上限參數,用以防止編程錯誤導致耗干賬戶中資金。點擊這里閱讀更多關于汽油。
1.設置你的環境
好了,你已經知道了那些基礎的,讓我們趕緊把環境搞起來寫代碼吧。為了開始開發以太坊app(或者dapp,去中心化應用的簡稱,許多人喜歡這樣叫),你需要安裝一個客戶端來接入主網。它會成為你進入這個分布式網絡的窗口,提供一個觀察區塊鏈的方法,那里所有EVM(以太坊虛擬機)狀態被顯示出來。有很多與條款兼容的客戶端,最受歡迎的是geth,用Go語言實現。但它并不是最開發者友好的客戶端。我目前找到最好的選擇是testrpc節點(是的,名字起得很糟糕)。相信我,它會節省你很多時間。安裝它,運行它:
$ sudo npm install -g ethereumjs-testrpc$ testrpc
你應該在一個新的終端中運行‘testrpc’,并且在你開發的過程中一直讓它運行。每次你運行testrpc,它會生成10個包涵模擬測試資金的新地址供你使用。這個不是真錢,你可以安全得用這些進行任何實驗,不會有損失資金的風險。在以太坊中撰寫智能合約最受歡迎的語言是Solidity,因此我們會使用這個語言。我們也會用Truffle開發框架,它會幫助創造智能合約,編譯,部署以及測試。讓我們開始吧
# First, let's install truffle首先,讓我們安裝truffle$ sudo npm install -g truffle# let's setup our project$ mkdir solidity-experiments$ cd solidity-experiments/$ truffle init
Truffle 會生成一個示范項目所需要的文件,包括MetaCoin,一個token合約的例子。你應該能夠通過運行truffle compile指令來編譯示范合約。然后,你需要通過我們在運行的testrpc節點用‘truffle migrate’指令來在模擬網絡部署合約。
Compiling ConvertLib.sol...Compiling MetaCoin.sol...Compiling Migrations.sol...Writing artifacts to ./build/contracts$ truffle migrateRunning migration: 1_initial_migration.js Deploying Migrations... Migrations: 0x78102b69114dbb846200a6a55c2fce8b16f61a5dSaving successful migration to network...Saving artifacts...Running migration: 2_deploy_contracts.js Deploying ConvertLib... ConvertLib: 0xaa708272521f972b9ceced7e4b0dae92c77a49ad Linking ConvertLib to MetaCoin Deploying MetaCoin... MetaCoin: 0xdd14d0691ca607d9a38f303501c5b0cf6c843fa1Saving successful migration to network...Saving artifacts...Note to Mac OS X users: Truffle is sometimes confused by .DS_Store files. If you get an error mentioning one of those files, just delete it.
我們剛剛往測試節點上部署了我們的示范合約。哇!很簡單,對吧?是時候寫我們自己的合約了!
2.撰寫你的第一個以太坊只能合約
在這個指南里面,我們會寫一個存在證明只能合約。就是創造一個存有用于證明存在的文件哈希的電子公正機關。用‘truffle create:contract’來開始:
$ truffle create:contract ProofOfExistence1
從你的編譯器里面打開合約/ProofOfExistnece1.sol(我用的是帶Soilidity語法高亮顯示的vim)
// Proof of Existence contract, version 1contract ProofOfExistence1 { // state bytes32 public proof; // calculate and store the proof for a document // *transactional function* function notarize(string document) { proof = calculateProof(document); }// helper function to get a document's sha256 // *read-only function* function calculateProof(string document) constant returns (bytes32) { return sha256(document); }}
我們將從一段簡單但是有錯誤的代碼開始向一個更好的解決方案靠近。這是一份Solidity合約定義,有點像其他語言中的類別(class)。合約中有狀態(state)和函數(functions)。區分合約中可能出現的兩種函數非常重要。
只讀(常數)函數:這些函數不對任何狀態(state)進行改變。他們只讀取狀態,進行計算,并且返回數值。因為這些函數可以在每一個節點內本地解決,他們不回花費任何的汽油(gas)。他們被用‘contant’關鍵詞標出。
交易函數:這些函數對狀態進行改變,轉移資金。因為這些變化需要在區塊鏈中被反應出來,執行交易函數需要向網絡提交交易,這會消耗汽油(gas)。
我們的合約中兩種函數各有一個,已在注釋中標注。下一段我們將會看到我們使用函數的類型會如何改變我們與智能合約交互。這個簡單的版本每次只儲存一個證明,用數據類型bytes32或者32bytes,跟sha256哈希的大小一樣。交易函數‘notarize’允許我們在合約的狀態變量‘proof’里存儲一個文件的哈希。這個變量是個公開變量,是我們合約的用戶認證一個文件是否被公正的唯一途徑。我們一會就會自己做一下,但是首先。。。
讓我們把ProofOfExistence1部署到網絡上!這次,你需要通過編輯移動文檔(migration file)(migrations/2_deploy_contracts.js)讓Truffle部署我們的新合約。用以下的來代替內容:
/* * migrations/2_deploy_contracts.js: */module.exports = function(deployer) { deployer.deploy(ConvertLib); deployer.autolink(); deployer.deploy(MetaCoin); // add this line deployer.deploy(ProofOfExistence1);};
你也可以選擇性的刪除有關ConvertLib和MetaCoin的語句,這些我們不會再用了。為了再次運行這個移動,你需要使用重啟標簽確保它再次運行。
truffle migrate --reset
更多的關于Truffle移動如何工作的內容可以看這里。
3. 與你的智能合約互動
現在我們已經將智能合約部署好了,讓我們擺弄擺弄它!我們可以通過函數調用來給它發信息或者讀取它的公開狀態。我們通過Truffle操縱臺來完成:
$ truffle console// get the deployed version of our contracttruffle(default)> var poe = ProofOfExistence1.deployed()// and print its address truffle(default)> console.log(poe.address)0x3d3bce79cccc331e9e095e8985def13651a86004// let's register our first "document"truffle(default)> poe.notarize('An amazing idea')Promise { <pending> }// let's now get the proof for that documenttruffle(default)> poe.calculateProof('An amazing idea').then(console.log)Promise { <pending> }0xa3287ff8d1abde95498962c4e1dd2f50a9f75bd8810bd591a64a387b93580ee7// To check if the contract's state was correctly changed:truffle(default)> poe.proof().then(console.log)0xa3287ff8d1abde95498962c4e1dd2f50a9f75bd8810bd591a64a387b93580ee7// The hash matches the one we previously calculated
注意所有函數調用都會返回一個Promise,當Promise被解決如果我們想要檢驗它我們可以通過‘.then(console.log)’來輸出。
我們要做的第一件事是獲得一個我們部署合約的表達,并把它存儲在一個叫做‘poe’的變量之中。
然后我們調用交易方程‘notarize’,這會涉及一個狀態改變。當我們調用一個交易方程,我們得到的是一個被轉化為交易id的Promise,而不是函數返回的值。記住為了改變EVM狀態我們需要消耗汽油(gas)并且向網絡提交一個交易。這是為什么我們會得到交易id做為Promise的結果,從改變狀態的那項交易那里得到。在這里,我們對交易id不感興趣,所以我們可以把Promise丟掉。不過當我們真正寫app時,我們會想要把它存起來用以檢查相應的交易,捕捉錯誤。
接下來,我們調用只讀(常數)函數‘calculateProof‘. 記得用’constant‘關鍵詞來標記你的只讀函數,否則Truffle會試著創造一個交易來執行這個函數。這個是我們告訴Truffle,我們并沒有跟區塊鏈交互而只是在讀取。通過這個只讀函數,我們會得到’An amazing idea‘文件的sha256。
我們現在需要把這個和我們智能合約的狀態進行對比。為了檢查狀態的改變是否正確,我們需要讀取‘Proof’這個公開狀態變量。要獲得一個公開狀態變量的值,我們得調用具有同樣名字的一個函數,它會返回一個Promise。我們這次,輸出的哈希值是一致的,所以一切都如我們所料得進行了 :)
像你從上面的片段看到的,我們第一版存在證明智能合約似乎可以工作!干得好!但是它每次只可以注冊一個文件。讓我們做一版更好的。
4. 合約代碼迭代
讓我們修改合約來支持多個文件驗證。把原文件復制到名為contracts/ProofOfExistence2.sol的新文件中,并且采取以下改變。主要的變化包括:我們把‘proof’變量變成了bytes32的數組,并且命名為‘proofs’,我們把它變成私有,然后加入一個通過循環訪問數組來檢查一個文件是否被公正的函數。
// Proof of Existence contract, version 2contract ProofOfExistence2 { // state bytes32[] private proofs; // store a proof of existence in the contract state // *transactional function* function storeProof(bytes32 proof) { proofs.push(proof); } // calculate and store the proof for a document // *transactional function* function notarize(string document) { var proof = calculateProof(document); storeProof(proof); } // helper function to get a document's sha256 // *read-only function* function calculateProof(string document) constant returns (bytes32) { return sha256(document); } // check if a document has been notarized // *read-only function* function checkDocument(string document) constant returns (bool) { var proof = calculateProof(document); return hasProof(proof); } // returns true if proof is stored // *read-only function* function hasProof(bytes32 proof) constant returns (bool) { for (var i = 0; i < proofs.length; i++) { if (proofs[i] == proof) { return true; } } return false; }}
讓我們與新的函數互動一下:(不要忘了更新migrations/2_deploy_contracts.js來加入新的合約并且運行‘truffle mirgrate--reset’)
// deploy contractstruffle(default)> migrate --reset// Get the new version of the contracttruffle(default)> var poe = ProofOfExistence2.deployed()// let's check for some new document, and it shouldn't be there.truffle(default)> poe.checkDocument('hello').then(console.log)Promise { <pending> }false// let's now add that document to the proof storetruffle(default)> poe.notarize('hello')Promise { <pending> }// let's now check again if the document has been notarized!truffle(default)> poe.checkDocument('hello').then(console.log)Promise { <pending> }true// success!// we can also store other documents and they are recorded tootruffle(default)> poe.notarize('some other document');truffle(default)> poe.checkDocument('some other document').then(console.log)Promise { <pending> }true
這一版比第一版強,但是仍然有些問題。注意每一次我們想要檢查一個文件是否有被公正過時都需要循環訪問所有存在的‘proofs’。儲存proofs更好的結構會是用映射(map)。走運的是,Solidity支持映射結構,在這個語言里稱此結構為mappings。另外一個我們會在這一版代碼做出的改進是我們會去掉那些多余的標識只讀(read-only)或交易(transactional)函數的那些注釋。我想現在你已經都知道這些了:)下面是最終版本,我想應該不難理解,因為是從之前的版本一點點變過來的:
// Proof of Existence contract, version 3contract ProofOfExistence3 { mapping (bytes32 => bool) private proofs; // store a proof of existence in the contract state function storeProof(bytes32 proof) { proofs[proof] = true; } // calculate and store the proof for a document function notarize(string document) { var proof = calculateProof(document); storeProof(proof); } // helper function to get a document's sha256 function calculateProof(string document) constant returns (bytes32) { return sha256(document); } // check if a document has been notarized function checkDocument(string document) constant returns (bool) { var proof = calculateProof(document); return hasProof(proof); } // returns true if proof is stored function hasProof(bytes32 proof) constant returns(bool) { return proofs[proof]; }}
這下看起來已經足夠好了。它跟第二版運行起來沒有差別。記得更新移動文檔(migration file)同時再次運行‘truffle migrate -- reset’來測試一下它。這個教程中的所有代碼都可以在這里找到。
5.在真正的測試網絡上部署
在你用testrpc在模擬網絡上大量測試你的合約之后,你就可以在真正的網絡上測試你的合約啦!這就需要你有一個真正的testnet/livenet以太坊客戶端。點擊這里看如何安裝geth的說明。
開發的過程中,你應該在testnet模式中運行你的節點,這樣你就可以在沒有損失真金白銀的風險下進行所有的測試。Testnet模式(在以太坊也叫Morden)基本上與真正的以太坊一模一樣,但是這里的以太幣token沒有任何金錢價值。不要發懶,記得永遠要在testnet模式下開發,如果你因為編程錯誤而損失以太幣,你會非常后悔的。
在testnet模式下運行geth, 打開RPC服務器:
geth --testnet --rpc console 2>> geth.log
這會打開一個你可以輸入基本口令來控制你的節點/客戶端的控制器。你的節點會開始下載testnet區塊鏈,你可以在eth.blockNumber上查看下載進度。區塊鏈下載的同時,你仍然可以運行口令。比如,讓我們設置一個賬戶:(千萬要記住密碼!)
> personal.newAccount()Passphrase:Repeat passphrase:"0xa88614166227d83c93f4c50be37150b9500d51fc"
讓我們發送一些以太幣過去并且查詢余額。你可以從這里獲得免費testnet以太幣:https://zerogox.com/ethereum/wei_faucet. 只需復制粘帖你剛剛生成的那個地址,這個水龍頭就是給你發送一個以太幣。想要查詢余額,運行以下代碼:
> eth.getBalance(eth.accounts[0])0
它會告訴你沒有余額因為你還沒有與全網絡同步。在你等待的同時,去testnet block explorer去查詢一下余額。那里,你也可以看到testnet目前最高的塊數(寫這個的時候是#1355293),你可以將這個信息與eth.blockNumber的信息結合去判斷你的節點是否已經完成同步。
一旦你的節點同步好,你就可以開始通過Truffle在testnet上部署你的合約了。首先,解鎖你的主geth賬戶,這樣Truffle就可以使用它。確認里面有一些余額,否則你將不能夠把新的合約推向網絡。
> personal.unlockAccount(eth.accounts[0], "mypassword", 24*3600)true> eth.getBalance(eth.accounts[0])1000000000000000000
準備好了吧!如果這兩個的某一個無法運行,檢查之前的步驟以確保你正確的完成了它們。現在,運行:
$ truffle migrate --reset
注意這次會需要更長的時間來完成,因為我們是在連接到真正的網絡而不是一個用testrpc模擬出來的網絡。一旦完成,你就可以用之前同樣的方法跟智能合約互動。
在testnet上部署的版本ProofOfExistence3可以在這個地址找到:0xcaf216d1975f75ab3fed520e1e3325dac3e79e05.
我想把如何在以太坊現場網絡部署合約的細節留給讀者。你只應該在模擬網絡和testnet大量測試你的合約之后再做這個。千萬記得,任何編程錯誤都可能導致在livenet上的金錢損失!
以太坊中智能合約的安全性問題很具有挑戰性。參見 Emin Gun Sirer的 “智能合約挺難弄對的”。
考慮到智能合約是定義金錢如何移動的電腦代碼的性質,我不得不在安全問題上稍做提示。我會在以后的文章里深度的討論合約安全性問題(像這里),但是這里我會先簡單的提幾點。
一些你應該知道(并且避免)的問題:
重入攻擊(reentrancy):不要在合約里使用外部調用。如果迫不得已,確保它是你做得最后一件事。
發送失敗(send can fail):發送資金時,你的代碼應該為發送失敗的情況做好準備。
循環可能引發汽油限制(Loops can trigger gas limit):當你在狀態變量上做循環的時候千萬當心,變量的大小會增長這可能導致汽油消耗到達極限。
調用棧深度限制(Call stack depth limit):不要使用遞歸,記住任何調用都可能因為調用棧到達極限而失敗。
時間戳依賴性(Timestamp dependency):不用在代碼的關鍵部分使用時間戳,因為礦工可以操縱它們。
這些是智能合約中可能導致資金盜竊以及毀壞的一些意外行為的例子。中心思想是:如果你在撰寫智能合約,你就在寫真正處理金錢的代碼。你應該加一萬個當心!寫測試,反復檢查代碼,并且做代碼審核。
避免明顯安全問題的最好方法就是對語言有扎扎實實的理解。我建議熟讀Solidity文檔,如果你有時間。我們將會需要更多更好的工具來完善智能合約安全。
新聞熱點
疑難解答