不可篡改性javascript
區塊鏈的一個顯著特色是,數據一旦寫入鏈中,就不可篡改重寫。php
在傳統的關係型數據庫中,你能夠很容易地更新一條數據記錄。可是,在區塊鏈中,一旦數據寫入就沒法 再更新了 —— 所以,區塊鏈是一直增加的。css
那麼,區塊鏈是如何實現數據的不可篡改特性?html
這首先得益於哈希(Hash
)函數 —— 若是你還沒接觸過哈希函數,不妨將它視爲一個數字指紋的計算函數: 輸入任意長度的內容,輸出定長的碼流(指紋)。哈希函數的一個重要特性就是,輸入的任何一點微小變化,都會 致使輸出的改變。所以能夠將哈希值做爲內容的指紋來使用。 你能夠點擊這裏 進一步瞭解哈希函數。前端
因爲區塊鏈裏的每一個塊都存儲有前一個塊內容的哈希值,所以若是有任何塊的內容被篡改,被篡改的塊以後 全部塊的哈希值也會隨之改變,這樣咱們就很容易檢測出區塊鏈的各塊是否被篡改了。java
去中心化的挑戰node
所謂去中心化應用(DApp
:Dcentralized Application),就是一個不存在中心服務器 的應用。在網絡中成百上千的電腦上,均可以運行該應用的副本,這使得它幾乎不可能 出現宕機的狀況。python
基於區塊鏈的投票是徹底去中心化的,所以無須任何中心化機構的存在。react
一旦徹底去中心化,在網絡上就會存在大量的區塊鏈副本(即:全節點),不少事情都會變得比以前中心化 應用環境複雜的多,例如:git
你應該已經注意到,每一個客戶端(瀏覽器)都是與各自的節點應用實例進行交互,而不是向 一箇中心化的服務器請求服務。
在一個理想的去中心化環境中,每一個想要跟DApp交互的人,都須要在他們的計算機或手機上面運行 一個的完整區塊鏈節點 —— 簡言之,每一個人都運行一個全節點。這意味着,在可以真正使用一個 去中心化應用以前,用戶不得不下載整個區塊鏈。
不過咱們並不是生活在一個烏托邦裏,期待每一個用戶都先運行一個全節點,而後再使用你的應用是不現實的。 可是去中心化背後的核心思想,就是不依賴於中心化的服務器。因此,區塊鏈社區已經出現了 一些解決方案,例如提供公共區塊鏈節點的Infura
, 以及瀏覽器插件Metamask
等。經過這些方案, 你就不須要花費大量的硬盤、內存和時間去下載並運行完整的區塊鏈節點,同時也能夠利用去中心化 的優勢。咱們將會之後的課程中對這些解決方案分別進行評測。
以太坊是一種區塊鏈的實現。在以太坊網絡中,衆多的節點彼此鏈接,構成了以太坊網絡:
以太坊節點軟件提供兩個核心功能:數據存儲、合約代碼執行。
在每一個以太坊全節點中,都保存有完整的區塊鏈數據。以太坊不只將交易數據保存在鏈上,編譯後 的合約代碼一樣也保存在鏈上。
以太坊全節點中,同時還提供了一個虛擬機來執行合約代碼。
交易數據
以太坊中每筆交易都存儲在區塊鏈上。當你部署合約時,一次部署就是一筆交易。當你爲候選者投票時,一次投票 又是另外一筆交易。全部的這些交易都是公開的,每一個人均可以看到並進行驗證。這個數據永遠也沒法篡改。
爲了確保網絡中的全部節點都有着同一份數據拷貝,而且沒有向數據庫中寫入任何無效數據,以太坊 目前使用工做量證實(POW:Proof Of Work
)算法來保證網絡安全,即經過礦工挖礦(Mining
)來達成共識(Consensus
)—— 將數據同步到全部節點。
工做量證實不是達成共識的惟一算法,挖礦也不是區塊鏈的惟一選擇。如今,咱們只須要了解,共識是指各節點 的數據實現了一致,POW
只是衆多用於創建共識的算法中的一種,這種算法須要經過礦工的挖礦來實現非可信環境下的 可信交易。共識是目的,POW是手段。
合約代碼
以太坊不只僅在鏈上存儲交易數據,它還能夠在鏈上存儲合約代碼。
在數據庫層面,區塊鏈的做用就是存儲交易數據。那麼給候選者投票、或者檢索投票結果的邏輯放在哪兒呢? 在以太坊的世界裏,你可使用Solidity
語言來編寫業務邏輯/應用代碼(也就是合約:Contract
), 而後將合約代碼編譯爲以太坊字節碼,並將字節碼部署到區塊鏈上:
編寫合約代碼也可使用其餘的語言,不過 Solidity
是到目前爲止最流行的選擇。
以太坊虛擬機
以太坊區塊鏈不只存儲數據和代碼,每一個節點中還包含一個虛擬機(EVM:Ethereum Virtual Machine)來執行 合約代碼 —— 聽起來就像計算機操做系統。
事實上,這一點是以太坊區別於比特幣(Bitcoin
)的最核心的一點:虛擬機的存在使區塊鏈邁入了2.0 時代,也讓區塊鏈第一次成爲應用開發者友好的平臺。
JS開發庫
爲了便於構建基於web的DApp,以太坊還提供了一個很是方便的JavaScript庫web3.js
,它封裝了以太坊節點的API 協議,從而讓開發者能夠輕鬆地鏈接到區塊鏈節點而沒必要編寫繁瑣的RPC
協議包。因此,咱們能夠在經常使用的JS框架 (好比 reactjs、angularjs 等)中直接引入該庫來構建去中心化應用:
總體構架
從圖中能夠看到,網頁經過(HTTP上的)遠程過程調用(RPC:Remote Procedure Call
)與區塊鏈節點進行通訊。web3.js
已經封裝了以太坊規定的所有 RPC 調用,所以利用它就能夠與區塊鏈進行交互,而沒必要手寫那些RPC請求包。 使用web3.js
的另外一個好處是,你可使用本身喜歡的前端框架來構建出色的web 應用。
因爲得到一個同步的全節點至關耗時,並佔用大量磁盤空間。爲了在咱們對區塊鏈的興趣消失以前掌握 如何開發一個去中心化應用,本課程將使用ganache
軟件來模擬區塊鏈節點,以便快速開發並測試應用, 從而能夠將注意力集中在去中心化的思想理解與DApp應用邏輯開發方面。
接下來,咱們將編寫一個投票合約,而後編譯合約並將其部署到區塊鏈節點 —— ganache
上。
最後,咱們將分別經過命令行和網頁這兩種方式,與區塊鏈進行交互。
咱們使用Solidity
語言來編寫合約。若是你熟悉面向對象的開發和JavaScript
,那麼學習Solidity
應該很是簡單。能夠將合約類比於OOP
的類:合約中的屬性用來聲明合約的狀態,而合約中的方法則提 供修改狀態的訪問接口。下圖給出了投票合約的主要接口:
基本上,投票合約Voting
包含如下內容:
Vote()
,每次執行就將指定的候選人得票數加 1totalVotesFor()
,執行後將返回指定候選人的得票數有兩點須要特別指出:
pragma solidity ^0.4.0; contract Voting { mapping (bytes32 => uint8) public votesReceived; bytes32[] public candidateList; function Voting(bytes32[] candidateNames) public { candidateList = candidateNames; } function totalVotesFor(bytes32 candidate) view public returns (uint8) { require(validCandidate(candidate)); return votesReceived[candidate]; } function voteForCandidate(bytes32 candidate) public { require(validCandidate(candidate)); votesReceived[candidate] += 1; } function validCandidate(bytes32 candidate) view public returns (bool) { for(uint i = 0; i < candidateList.length; i++) { if (candidateList[i] == candidate) { return true; } } return false; } }
編譯器要求
pragma solidity ^0.4.18;
聲明合約代碼的編譯器版本要求。^0.4.18
表示要求合約編譯器版本不低於0.4.18
。
合約聲明
contract Voting{}
contract
關鍵字用來聲明一個合約。
字典類型:mapping
mapping (bytes32 => uint8) public votesReceived;
mapping
能夠類比於一個關聯數組或者是字典,是一個鍵值對。例如,votesReceived
狀態的 鍵是候選者的名字,類型爲bytes32
—— 32個字節定長字符串。votesReceived
狀態中每一個鍵對應的值 是一個單字節無符號整數(uint8
),用來存儲該候選人的得票數:
bytes32[] public candidateList;
在JS中,使用votesReceived.keys
就能夠獲取全部的候選人姓名。可是在Solidity
中 沒有這樣的方法,因此咱們須要單獨管理所有候選人的名稱 —— candidateList
數組。
function voteForCandidate(bytes32 candidate) public { require(validCandidate(candidate)); // 相似於if(!false) {return false} 的意思,只有爲真,合約才繼續執行。 votesReceived[candidate] += 1; }
在voteForCandidate()
方法中,請注意 votesReceived[key]
有默認值 0,因此咱們沒有進行初始化, 而是直接加1。
在合約方法體內的require()
語句相似於斷言,只有條件爲真時,合約才繼續執行。validateCandidate()
方法只有在給定的候選人名稱在部署合約時傳入的候選人名單中時才返回真值,從而避免亂投票的行爲:
function validCandidate(bytes32 candidate) view public returns (bool) { for(uint i = 0; i < candidateList.length; i++) { if (candidateList[i] == candidate) { return true; } } return false; }
方法聲明符與修飾符
在Solidity
中,能夠爲函數應用可視性聲明符(visibility specifier
),例如 public
、private
。 public
意味着能夠從合約外調用函數。若是一個方法僅限合約內部調用,能夠把它聲明爲私有(private
)。 點擊這裏 能夠查看全部的可視性說明符。
在Solidity
中,還能夠爲函數聲明修飾符(modifier
),例如view
用來告訴編譯器,這個函數是隻讀的,也就是說, 該函數的執行不會改變區塊鏈的狀態)。全部的修飾符均可以在這裏 看到。
合約代碼編譯
首先,請確保ganache
已經在第一個終端窗口中運行:~$ ganache-cli
。(對於ganache可運行 npm install -g ganache-cli 來全局安裝 ganache )
而後,在另外一個終端中進入repo/chapter1
目錄,啓動node 控制檯,而後初始化 web3 對象,並向本地區塊 鏈節點(http://localhost:8545
)查詢獲取全部的帳戶:
~$ cd ~/repo/chapter1 ~/repo/chapter1$ node > Web3 = require('web3') > web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545")); > web3.eth.accounts ['0x5c252a0c0475f9711b56ab160a1999729eccce97' '0x353d310bed379b2d1df3b727645e200997016ba3' '0xa3ddc09b5e49d654a43e161cae3f865261cabd23' '0xa8a188c6d97ec8cf905cc1dd1cd318e887249ec5' '0xc0aa5f8b79db71335dacc7cd116f357d7ecd2798' '0xda695959ff85f0581ca924e549567390a0034058' '0xd4ee63452555a87048dcfe2a039208d113323790' '0xc60c8a7b752d38e35e0359e25a2e0f6692b10d14' '0xba7ec95286334e8634e89760fab8d2ec1226bf42' '0x208e02303fe29be3698732e92ca32b88d80a2d36']
要編譯合約,首先須要載入 Voting.sol
文件的內容,而後使用編譯器(solc
)的compile()
方法 對合約代碼進行編譯:
> code = fs.readFileSync('Voting.sol').toString() > solc = require('solc') > compiledCode = solc.compile(code)
成功編譯合約後能夠查看一下編譯結果。直接在控制檯輸入:
> compiledCode
至關長一大段輸出霸屏...
便以結果是一個JSON對象,其中包含兩個重要的字段:
ABI:Application Binary Interface
), 它聲明瞭合約中包含的接口方法。不管什麼時候須要跟一個合約進行交互,都須要該合約的abi
定義。你能夠在 這裏查看ABI的詳細信息。關於代碼:https://github.com/sunshineofparadise/solidity/tree/master/chapter1
原始代碼存在衝突問題,因此能夠直接拉取遠程github代碼
cd chapter1
npm安裝
npm開始
/ *打開另外一個終端窗口,確保二者都在運行。* /
節點
> Web3 = require(' web3 ')
> web3 = new Web3(new Web3.providers.HttpProvider(「 http:// localhost:8545 」));
> web3.eth.accounts
> code = fs.readFileSync('Voting.sol',‘utf8’)。toString()//這兩行的改動是爲了防止報錯,可不應,報錯了再運行這個
> solc = require(' solc ',1)
> compiledCode = solc.compile(code)
將投票合約部署到區塊鏈上
須要先傳入合約的abi
定義來建立合約對象VotingContract
,而後利用該對象完成合約在鏈上的部署和初始化。
在node控制檯執行如下命令:
> abiDefinition = JSON.parse(compiledCode.contracts[':Voting'].interface) > VotingContract = web3.eth.contract(abiDefinition) > byteCode = compiledCode.contracts[':Voting'].bytecode > deployedContract = VotingContract.new(['Rama','Nick','Jose'],{data: byteCode, from: web3.eth.accounts[0], gas: 4700000}) > deployedContract.address '0x0396d2b97871144f75ba9a9c8ae12bf6c019f610' <- 你的部署地址可能和這個不同 > contractInstance = VotingContract.at(deployedContract.address)
調用VotingContract
對象的new()
方法來將投票合約部署到區塊鏈。new()
方法參數列表應當與合約的 構造函數要求相一致。對於投票合約而言,new()
方法的第一個參數是候選人名單。
new()
方法的最後一個參數用來聲明部署選項。如今讓咱們來看一下這個參數的內容:
{ data: byteCode, //合約字節碼 from: web3.eth.accounts[0], //部署者帳戶,將從這個帳戶扣除執行部署交易的開銷 gas: 4700000 //願意爲本次部署最多支付多少油費,單位:Wei }
web3.eth.accounts
返回的 第一個帳戶,做爲部署這個合約的帳戶。在提交交易以前,你必須擁有並解鎖這個帳戶。不過爲了方便 起見,ganache
默認會自動解鎖這10個帳戶。咱們已經成功部署了投票合約,而且得到了一個合約實例(變量contractInstance
),如今能夠用這個實例 與合約進行交互了。
在區塊鏈上有上千個合約。那麼,如何識別你的合約已經上鍊了呢?
答案是:使用deployedContract.address
。 當你須要跟合約進行交互時,就須要這個部署地址和咱們以前 談到的abi
定義。 所以,請記住這個地址。
調用合約的totalVotesFor()
方法來查看某個候選人的得票數。例如,下面的代碼 查看候選人Rama
的得票數:
> contractInstance.totalVotesFor.call('Rama') { [String: '0'] s: 1, e: 0, c: [ 0 ] }
{ [String: '0'] s: 1, e: 0, c: [ 0 ] }
是數字 0 的科學計數法表示. 你能夠在 這裏 瞭解科學計數法的詳細信息。
調用合約的voteForCandidate()
方法投票給某個候選人。下面的代碼給Rama
投了三次票:
> contractInstance.voteForCandidate('Rama', {from: web3.eth.accounts[0]}) '0xdedc7ae544c3dde74ab5a0b07422c5a51b5240603d31074f5b75c0ebc786bf53' > contractInstance.voteForCandidate('Rama', {from: web3.eth.accounts[0]}) '0x02c054d238038d68b65d55770fabfca592a5cf6590229ab91bbe7cd72da46de9' > contractInstance.voteForCandidate('Rama', {from: web3.eth.accounts[0]}) '0x3da069a09577514f2baaa11bc3015a16edf26aad28dffbcd126bde2e71f2b76f'
如今咱們再次查看Rama
的得票數:
> contractInstance.totalVotesFor.call('Rama').toLocaleString() '3'
投票 = 交易
每執行一次投票,就會產生一次交易,所以voteForCandidate()
方法將返回一個交易id,做爲 交易的憑據。好比:0xdedc7ae544c3dde74ab5a0b07422c5a51b5240603d31074f5b75c0ebc786bf53
。 交易id是交易發生的憑據,交易是不可篡改的,所以任什麼時候候可使用交易id引用或查看交易內容 都會獲得一樣的結果。
對於區塊鏈而言,交易不可篡改是其核心特性。接下來,咱們將會利用這一特性來構建應用。
接下來讓咱們建立一個簡單的html
頁面,以便用戶可使用瀏覽器 而不是複雜的命令行來與投票合約交互:
頁面的主要功能以下:
voteForCandidate()
方法 —— 和咱們nodejs控制檯裏的流程同樣。你能夠在實驗環境編輯器中打開~/repo/chapter1/index.html
來查看頁面源代碼。 爲了聚焦核心業務邏輯,咱們在網頁中硬編碼了候選人姓名。若是你喜歡的話,能夠調整代碼來動態生成候選人。
頁面文件中的JS代碼都封裝在了一個單獨的JS文件中,能夠在試驗環境編輯器中打開 ~/repo/chapter1/index.js
來查看其內容。
爲了將頁面運行起來,須要根據你的私有試驗環境對JS代碼進行一下調整:
節點的RPC API地址:
web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));
HttpProvier()
對象的構造函數參數是web3js庫須要連接的以太坊節點RPC API的URL,要調整爲 你的私有試驗環境中ganache
的訪問端結點,格式爲:
http://8545.<你的私有實驗環境URL>/
查看試驗環境中的嵌入瀏覽器地址欄來獲取你的私有實驗環境URL:
投票合約地址
當一個合約部署到區塊鏈上時,將得到一個地址,例如0x329f5c190380ebcf640a90d06eb1db2d68503a53
。 因爲每次部署都會得到一個不一樣的地址,所以你須要指定它:
contractInstance = VotingContract.at('0x329f5c190380ebcf640a90d06eb1db2d68503a53')
若是你在部署合約的時候沒有記錄這個地址,就從新部署吧。
運行web服務
在第二個終端中輸入如下命令來啓動一個簡單的Web服務器,以便咱們能夠在試驗環境中的嵌入瀏覽器中訪問頁面:
~$ cd ~/repo/chapter1 ~/repo/chapter1$ python -m SimpleHTTPServer
Python的SimpleHTTPServer
模塊將啓動在8000端口的監聽。
如今,在試驗環境的嵌入瀏覽器中點擊刷新按鈕。若是一切順利的話,你應該能夠看到投票應用的頁面了。 當你在文本框中輸入候選人姓名,例如Rama
,而後點擊按鈕後,應該會看到候選人Rama
的得票數加 1 。
注:其中純nodejs開發項目可參考項目
Truffle
是一個DApp開發框架,它簡化了去中心化應用的構建和管理。你能夠在 這裏瞭解框架的 更多內容和完整特性。
若是你須要在本身的機器上安裝,可使用 npm 全局安裝:npm install -g truffle
end