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