我已經(jīng)智能合約領(lǐng)域工作了4年,主要在比特幣區(qū)塊鏈上。我參與的一些項目包括存在證明,bitcore(比特核心)以及Streamium. 過去這個月,我探索了在以太坊平臺上進行開發(fā)。
我決定制作一個簡短的指南服務(wù)未來想要學(xué)習(xí)以太坊開發(fā)的程序員。手冊分為兩個部分:如何開始以太坊智能合約開發(fā),智能合約安全簡述.
如何開始學(xué)習(xí)以太坊智能合約
0.基礎(chǔ)概念
這個指南假設(shè)你已經(jīng)有了一些密碼學(xué)貨幣和區(qū)塊鏈的基礎(chǔ)技術(shù)背景。 如果你沒有,我建議快速過一遍Andreas Antonopoulos的《完全掌握比特幣》(Mastering Bitcoin),Consensys的《用剛剛夠的比特幣來搞懂以太坊》(Just Enough Bitcoin for Ethereum),或者至少看看Scott Driscoll的短片。 為了繼續(xù)讀下去你得了解公鑰和私鑰,為什么區(qū)塊鏈需要礦工,如何達成去中心化的共識,以及交易腳本和智能合約的概念。
另外兩個在你開始進行以太坊開發(fā)之前需要了解的重要的,相關(guān)的概念是以太坊虛擬機和汽油(gas)。以太坊的目的在于成為一個智能合約平臺。它的起源可以被追溯到Vitalik Buterin對比特幣做為智能合約平臺具有的局限性的評論。以太坊虛擬機(EVM)是以太坊智能合約執(zhí)行之處。與比特幣相比,它為撰寫合約提供了更具表現(xiàn)力和完整性的語言。事實上,它是一個圖靈完備的編程語言。一個比較好的比喻是,EVM是一個執(zhí)行智能合約的分布式的世界電腦。由于智能合約由EVM執(zhí)行, 必須存在一種限制每個合約占用資源的機制。EVM內(nèi)運行的每一步操作實際上同時在被所有節(jié)點所執(zhí)行。這是為什么需要有汽油(gas)存在。一個以太坊合約代碼交易可以引發(fā)數(shù)據(jù)讀寫,密碼學(xué)原語,調(diào)動(發(fā)送信息給)其他合約等等昂貴的運算。每個此類運算都有用汽油計量的價格,每筆交易所耗費的汽油單元需要用以太幣來支付,根據(jù)隨時變化的汽油和以太幣的匯率計算。相應(yīng)的價格會從提交交易請求的以太坊賬戶中扣除。同時每筆交易對可使用的汽油會設(shè)置上限參數(shù),用以防止編程錯誤導(dǎo)致耗干賬戶中資金。點擊這里閱讀更多關(guān)于汽油。
1.設(shè)置你的環(huán)境
好了,你已經(jīng)知道了那些基礎(chǔ)的,讓我們趕緊把環(huán)境搞起來寫代碼吧。為了開始開發(fā)以太坊app(或者dapp,去中心化應(yīng)用的簡稱,許多人喜歡這樣叫),你需要安裝一個客戶端來接入主網(wǎng)。它會成為你進入這個分布式網(wǎng)絡(luò)的窗口,提供一個觀察區(qū)塊鏈的方法,那里所有EVM(以太坊虛擬機)狀態(tài)被顯示出來。有很多與條款兼容的客戶端,最受歡迎的是geth,用Go語言實現(xiàn)。但它并不是最開發(fā)者友好的客戶端。我目前找到最好的選擇是testrpc節(jié)點(是的,名字起得很糟糕)。相信我,它會節(jié)省你很多時間。安裝它,運行它:
$ sudo npm install -g ethereumjs-testrpc$ testrpc
你應(yīng)該在一個新的終端中運行‘testrpc’,并且在你開發(fā)的過程中一直讓它運行。每次你運行testrpc,它會生成10個包涵模擬測試資金的新地址供你使用。這個不是真錢,你可以安全得用這些進行任何實驗,不會有損失資金的風(fēng)險。在以太坊中撰寫智能合約最受歡迎的語言是Solidity,因此我們會使用這個語言。我們也會用Truffle開發(fā)框架,它會幫助創(chuàng)造智能合約,編譯,部署以及測試。讓我們開始吧
# 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合約的例子。你應(yīng)該能夠通過運行truffle compile指令來編譯示范合約。然后,你需要通過我們在運行的testrpc節(jié)點用‘truffle migrate’指令來在模擬網(wǎng)絡(luò)部署合約。
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.
我們剛剛往測試節(jié)點上部署了我們的示范合約。哇!很簡單,對吧?是時候?qū)懳覀冏约旱暮霞s了!
2.撰寫你的第一個以太坊只能合約
在這個指南里面,我們會寫一個存在證明只能合約。就是創(chuàng)造一個存有用于證明存在的文件哈希的電子公正機關(guān)。用‘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)。合約中有狀態(tài)(state)和函數(shù)(functions)。區(qū)分合約中可能出現(xiàn)的兩種函數(shù)非常重要。
只讀(常數(shù))函數(shù):這些函數(shù)不對任何狀態(tài)(state)進行改變。他們只讀取狀態(tài),進行計算,并且返回數(shù)值。因為這些函數(shù)可以在每一個節(jié)點內(nèi)本地解決,他們不回花費任何的汽油(gas)。他們被用‘contant’關(guān)鍵詞標(biāo)出。
交易函數(shù):這些函數(shù)對狀態(tài)進行改變,轉(zhuǎn)移資金。因為這些變化需要在區(qū)塊鏈中被反應(yīng)出來,執(zhí)行交易函數(shù)需要向網(wǎng)絡(luò)提交交易,這會消耗汽油(gas)。
我們的合約中兩種函數(shù)各有一個,已在注釋中標(biāo)注。下一段我們將會看到我們使用函數(shù)的類型會如何改變我們與智能合約交互。這個簡單的版本每次只儲存一個證明,用數(shù)據(jù)類型bytes32或者32bytes,跟sha256哈希的大小一樣。交易函數(shù)‘notarize’允許我們在合約的狀態(tài)變量‘proof’里存儲一個文件的哈希。這個變量是個公開變量,是我們合約的用戶認(rèn)證一個文件是否被公正的唯一途徑。我們一會就會自己做一下,但是首先。。。
讓我們把ProofOfExistence1部署到網(wǎng)絡(luò)上!這次,你需要通過編輯移動文檔(migration file)(migrations/2_deploy_contracts.js)讓Truffle部署我們的新合約。用以下的來代替內(nèi)容:
/* * migrations/2_deploy_contracts.js: */module.exports = function(deployer) { deployer.deploy(ConvertLib); deployer.autolink(); deployer.deploy(MetaCoin); // add this line deployer.deploy(ProofOfExistence1);};
你也可以選擇性的刪除有關(guān)ConvertLib和MetaCoin的語句,這些我們不會再用了。為了再次運行這個移動,你需要使用重啟標(biāo)簽確保它再次運行。
truffle migrate --reset
更多的關(guān)于Truffle移動如何工作的內(nèi)容可以看這里。
3. 與你的智能合約互動
現(xiàn)在我們已經(jīng)將智能合約部署好了,讓我們擺弄擺弄它!我們可以通過函數(shù)調(diào)用來給它發(fā)信息或者讀取它的公開狀態(tài)。我們通過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
注意所有函數(shù)調(diào)用都會返回一個Promise,當(dāng)Promise被解決如果我們想要檢驗它我們可以通過‘.then(console.log)’來輸出。
我們要做的第一件事是獲得一個我們部署合約的表達,并把它存儲在一個叫做‘poe’的變量之中。
然后我們調(diào)用交易方程‘notarize’,這會涉及一個狀態(tài)改變。當(dāng)我們調(diào)用一個交易方程,我們得到的是一個被轉(zhuǎn)化為交易id的Promise,而不是函數(shù)返回的值。記住為了改變EVM狀態(tài)我們需要消耗汽油(gas)并且向網(wǎng)絡(luò)提交一個交易。這是為什么我們會得到交易id做為Promise的結(jié)果,從改變狀態(tài)的那項交易那里得到。在這里,我們對交易id不感興趣,所以我們可以把Promise丟掉。不過當(dāng)我們真正寫app時,我們會想要把它存起來用以檢查相應(yīng)的交易,捕捉錯誤。
接下來,我們調(diào)用只讀(常數(shù))函數(shù)‘calculateProof‘. 記得用’constant‘關(guān)鍵詞來標(biāo)記你的只讀函數(shù),否則Truffle會試著創(chuàng)造一個交易來執(zhí)行這個函數(shù)。這個是我們告訴Truffle,我們并沒有跟區(qū)塊鏈交互而只是在讀取。通過這個只讀函數(shù),我們會得到’An amazing idea‘文件的sha256。
我們現(xiàn)在需要把這個和我們智能合約的狀態(tài)進行對比。為了檢查狀態(tài)的改變是否正確,我們需要讀取‘Proof’這個公開狀態(tài)變量。要獲得一個公開狀態(tài)變量的值,我們得調(diào)用具有同樣名字的一個函數(shù),它會返回一個Promise。我們這次,輸出的哈希值是一致的,所以一切都如我們所料得進行了 :)
像你從上面的片段看到的,我們第一版存在證明智能合約似乎可以工作!干得好!但是它每次只可以注冊一個文件。讓我們做一版更好的。
4. 合約代碼迭代
讓我們修改合約來支持多個文件驗證。把原文件復(fù)制到名為contracts/ProofOfExistence2.sol的新文件中,并且采取以下改變。主要的變化包括:我們把‘proof’變量變成了bytes32的數(shù)組,并且命名為‘proofs’,我們把它變成私有,然后加入一個通過循環(huán)訪問數(shù)組來檢查一個文件是否被公正的函數(shù)。
// 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; }}
讓我們與新的函數(shù)互動一下:(不要忘了更新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
這一版比第一版強,但是仍然有些問題。注意每一次我們想要檢查一個文件是否有被公正過時都需要循環(huán)訪問所有存在的‘proofs’。儲存proofs更好的結(jié)構(gòu)會是用映射(map)。走運的是,Solidity支持映射結(jié)構(gòu),在這個語言里稱此結(jié)構(gòu)為mappings。另外一個我們會在這一版代碼做出的改進是我們會去掉那些多余的標(biāo)識只讀(read-only)或交易(transactional)函數(shù)的那些注釋。我想現(xiàn)在你已經(jīng)都知道這些了:)下面是最終版本,我想應(yīng)該不難理解,因為是從之前的版本一點點變過來的:
// 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]; }}
這下看起來已經(jīng)足夠好了。它跟第二版運行起來沒有差別。記得更新移動文檔(migration file)同時再次運行‘truffle migrate -- reset’來測試一下它。這個教程中的所有代碼都可以在這里找到。
5.在真正的測試網(wǎng)絡(luò)上部署
在你用testrpc在模擬網(wǎng)絡(luò)上大量測試你的合約之后,你就可以在真正的網(wǎng)絡(luò)上測試你的合約啦!這就需要你有一個真正的testnet/livenet以太坊客戶端。點擊這里看如何安裝geth的說明。
開發(fā)的過程中,你應(yīng)該在testnet模式中運行你的節(jié)點,這樣你就可以在沒有損失真金白銀的風(fēng)險下進行所有的測試。Testnet模式(在以太坊也叫Morden)基本上與真正的以太坊一模一樣,但是這里的以太幣token沒有任何金錢價值。不要發(fā)懶,記得永遠(yuǎn)要在testnet模式下開發(fā),如果你因為編程錯誤而損失以太幣,你會非常后悔的。
在testnet模式下運行g(shù)eth, 打開RPC服務(wù)器:
geth --testnet --rpc console 2>> geth.log
這會打開一個你可以輸入基本口令來控制你的節(jié)點/客戶端的控制器。你的節(jié)點會開始下載testnet區(qū)塊鏈,你可以在eth.blockNumber上查看下載進度。區(qū)塊鏈下載的同時,你仍然可以運行口令。比如,讓我們設(shè)置一個賬戶:(千萬要記住密碼?。?/p>
> personal.newAccount()Passphrase:Repeat passphrase:"0xa88614166227d83c93f4c50be37150b9500d51fc"
讓我們發(fā)送一些以太幣過去并且查詢余額。你可以從這里獲得免費testnet以太幣:https://zerogox.com/ethereum/wei_faucet. 只需復(fù)制粘帖你剛剛生成的那個地址,這個水龍頭就是給你發(fā)送一個以太幣。想要查詢余額,運行以下代碼:
> eth.getBalance(eth.accounts[0])0
它會告訴你沒有余額因為你還沒有與全網(wǎng)絡(luò)同步。在你等待的同時,去testnet block explorer去查詢一下余額。那里,你也可以看到testnet目前最高的塊數(shù)(寫這個的時候是#1355293),你可以將這個信息與eth.blockNumber的信息結(jié)合去判斷你的節(jié)點是否已經(jīng)完成同步。
一旦你的節(jié)點同步好,你就可以開始通過Truffle在testnet上部署你的合約了。首先,解鎖你的主geth賬戶,這樣Truffle就可以使用它。確認(rèn)里面有一些余額,否則你將不能夠把新的合約推向網(wǎng)絡(luò)。
> personal.unlockAccount(eth.accounts[0], "mypassword", 24*3600)true> eth.getBalance(eth.accounts[0])1000000000000000000
準(zhǔn)備好了吧!如果這兩個的某一個無法運行,檢查之前的步驟以確保你正確的完成了它們。現(xiàn)在,運行:
$ truffle migrate --reset
注意這次會需要更長的時間來完成,因為我們是在連接到真正的網(wǎng)絡(luò)而不是一個用testrpc模擬出來的網(wǎng)絡(luò)。一旦完成,你就可以用之前同樣的方法跟智能合約互動。
在testnet上部署的版本ProofOfExistence3可以在這個地址找到:0xcaf216d1975f75ab3fed520e1e3325dac3e79e05.
我想把如何在以太坊現(xiàn)場網(wǎng)絡(luò)部署合約的細(xì)節(jié)留給讀者。你只應(yīng)該在模擬網(wǎng)絡(luò)和testnet大量測試你的合約之后再做這個。千萬記得,任何編程錯誤都可能導(dǎo)致在livenet上的金錢損失!
以太坊中智能合約的安全性問題很具有挑戰(zhàn)性。參見 Emin Gun Sirer的 “智能合約挺難弄對的”。
考慮到智能合約是定義金錢如何移動的電腦代碼的性質(zhì),我不得不在安全問題上稍做提示。我會在以后的文章里深度的討論合約安全性問題(像這里),但是這里我會先簡單的提幾點。
一些你應(yīng)該知道(并且避免)的問題:
重入攻擊(reentrancy):不要在合約里使用外部調(diào)用。如果迫不得已,確保它是你做得最后一件事。
發(fā)送失敗(send can fail):發(fā)送資金時,你的代碼應(yīng)該為發(fā)送失敗的情況做好準(zhǔn)備。
循環(huán)可能引發(fā)汽油限制(Loops can trigger gas limit):當(dāng)你在狀態(tài)變量上做循環(huán)的時候千萬當(dāng)心,變量的大小會增長這可能導(dǎo)致汽油消耗到達極限。
調(diào)用棧深度限制(Call stack depth limit):不要使用遞歸,記住任何調(diào)用都可能因為調(diào)用棧到達極限而失敗。
時間戳依賴性(Timestamp dependency):不用在代碼的關(guān)鍵部分使用時間戳,因為礦工可以操縱它們。
這些是智能合約中可能導(dǎo)致資金盜竊以及毀壞的一些意外行為的例子。中心思想是:如果你在撰寫智能合約,你就在寫真正處理金錢的代碼。你應(yīng)該加一萬個當(dāng)心!寫測試,反復(fù)檢查代碼,并且做代碼審核。
避免明顯安全問題的最好方法就是對語言有扎扎實實的理解。我建議熟讀Solidity文檔,如果你有時間。我們將會需要更多更好的工具來完善智能合約安全。
新聞熱點
疑難解答
圖片精選