1. 程式人生 > >以太坊智慧合約學習筆記:開發流程及工具鏈使用

以太坊智慧合約學習筆記:開發流程及工具鏈使用

本文主要介紹開發流程和工具鏈的使用,安裝過程百度上有好多,這裡就不贅述了
網上隨便找了一個智慧合約的例子,咱們來做一個投票系統,先用傳統的中心化方案去實現,然後在過度到區塊鏈1.0,最後再用區塊鏈2.0,感受一下開發思想的不同。

業務分析

我們做的簡單點,首先要有一些候選人,然後我們要可以給這些候選人進行投票,投票結束後要統計每位候選人的選票結果。

傳統的中心化方案

如果用傳統的中心化方案去實現,簡直不要太簡單,以C艹為例,先來定義一下類

class Voting 
{
public:
    void voteForCandidate(std::string name);
    int totalVotesFor(std::string name);
private:
    std::vector<std::string> m_vecName;
    std::map<std::string, int> m_mapReceived;
};

void Voting::Voting(std::vector
<std::string>
&vecName) { m_vecName = vecName; } void Voting::voteForCandidate(std::string name) { m_mapReceived[name] = m_mapReceived[name]+1; } int Voting::totalVotesFor(std::string name) { return m_mapReceived[name]; }

然後再搞臺伺服器,搞個數據庫,將投票結果儲存到伺服器上,客戶端可以投票,可以檢視結果等等,這個沒啥難度,不多廢話了。

區塊鏈1.0的方案

比特幣出現後,所有的區塊鏈應用都是把比特幣的原始碼拿過來,改一改,這個時期也被稱為是區塊鏈1.0時代。
完整的比特幣程式碼很龐大,我們這裡弄簡單一點,不用比特幣,用之前“C++從零開始區塊鏈”系列的程式碼來做。
在“C++從零開始區塊鏈”系列中,我們仿照比特幣實現了一個簡單的電子貨幣記賬系統,其核心的記賬資料如下

'transactions': [                                     //交易列表,可以有多個交易
{ 
'sender': "8527147fe1f5426f9dd545de4b27ee00",        //付款方
'recipient': "a77f5cdfa2934df3954a5c7c7da5df1f",     //收款方
'amount': 5,                                         //金額
}
]

這是一個json陣列,每一個元素就代表一支交易,而交易的含義就是地址為8527147fe1f5426f9dd545de4b27ee00的使用者要給地址為a77f5cdfa2934df3954a5c7c7da5df1f的使用者5個電子幣。
那麼怎麼用這個結構來做投票系統呢?程式碼和資料結構都是一樣的,關鍵是對資料的解讀。
同樣是上面的那段json,在投票系統中,我們可以將其解讀為地址8527147fe1f5426f9dd545de4b27ee00的使用者,為地址為a77f5cdfa2934df3954a5c7c7da5df1f的候選人投了5票。投票的時候只要發起這支交易即可,統計結果的時候查詢一下候選人的餘額就行了。
業務主體確定了,在初始化系統的時候,我們將候選人列表寫如創世塊中

'transactions': [                                     //交易列表,可以有多個交易
{ 
'sender': "0",                                       //付款方
'recipient': "a77f5cdfa2934df3954a5c7c7da5df1f",     //收款方
'amount': 0,                                         //金額
}
]

然後限制每筆交易能轉賬的電子幣只能是1,收款人地址必須存在於創世塊的交易列表中,每個地址只能發起一次交易。
最後,根據上面的分析,將“C++從零開始區塊鏈”系列中的程式碼簡單改一改就可以了。

區塊鏈2.0的方案

以太坊是一個區塊鏈的應用框架,使用者不需要關心區塊鏈的底層細節,只要在專注於用智慧合約編寫業務邏輯就可以了,這個時期也被稱為是區塊鏈2.0時代。
以太坊的客戶端有很多,我們這裡使用geth。智慧合約的程式語言也用很多種,我們這裡用使用最多的Solidity,使用http://remix.ethereum.org來進行編輯和編譯。

Solidity編寫智慧合約

我們先來寫智慧合約,關於Solidity的語法百度一下也有好多,這裡就不贅述了,直接上程式碼

pragma solidity ^0.4.24;

contract Voting {

  mapping (bytes32 => uint8) public votesReceived;
  bytes32[] public candidateList;

  constructor (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;
   }
}

有些面向物件開發基礎的人,看了上面的智慧合約程式碼都不會感覺陌生,如果對比上面的C艹,你會發現,和定義一個class也差不多。需要說明的是,在新版本中,智慧合約的建構函式為constructor。
然後開啟http://remix.ethereum.org,將上面的程式碼貼進去,在右邊點選settings,在Solidity version中選擇一個編譯器,由於程式碼中我們寫的是0.4.24,所以也選一個0.4.24版本的編譯器。
然後點選compile,點選start to compile進行編譯,如果不報錯,點選details,在彈出的對話方塊中找到WEB3DEPLOY,將其中的內容考到文字編輯器中備用。

geth建立以太坊測試節點

合約寫好了,接下來要部署到乙太網上,要部署在乙太網上,就要先建立一個乙太網節點,在終端中輸入命令

geth --datadir testNet --dev console 2>> test.log

–datadir用來指定資料存放目錄,–dev是測試網路,console 是進入控制檯,具體命令請查閱相關文件。
執行以上命令後終端進入geth的控制檯,在測試網路中,會預設建立一個賬戶,並分配一些以太幣。部署合約需要消耗一定數量的以太幣,通過合約往以太坊的網路上寫資料也需要消耗一定數量的以太幣(嚴格說其實是gas),但從乙太網上查詢資料是免費的。由於測試網路給預設建立的賬戶以太幣太多了,不方便我們觀察,所以我們要新建一個使用者在終端執行命令

personal.newAccount("123")

123是密碼,建立成功會返回賬戶地址
終端執行命令

eth.accounts

可以看到有兩個使用者了
然後用預設賬戶給新使用者轉賬

eth.sendTransaction({from: '0x290a8ad7b378ea72c705cc55f0f3ecc029ab4854', to: '0xe93b37033d3ddfc0421304bce726dd50040ba2bf', value: web3.toWei(1, "ether")})

from是支付方,to是收款方,value是金額和單位,我們這裡是從第一個賬戶中向第二個賬戶中轉賬1個以太幣
終端輸入下面的命令,查詢餘額

eth.getBalance(eth.accounts[1])

結果顯示賬戶中有1個以太幣。

部署

智慧合約寫好了,測試節點建立好了,測試賬戶也都準備好了,接下來就是部署了
找到剛才儲存的智慧合約的編譯結果

var candidateNames = /* var of type bytes32[] here */ ;
var votingContract = web3.eth.contract([{"constant":true,"inputs":[{"name":"candidate","type":"bytes32"}],"name":"totalVotesFor","outputs":[{"name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"candidate","type":"bytes32"}],"name":"validCandidate","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"bytes32"}],"name":"votesReceived","outputs":[{"name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"uint256"}],"name":"candidateList","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"candidate","type":"bytes32"}],"name":"voteForCandidate","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"candidateNames","type":"bytes32[]"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"}]);
var voting = votingContract.new(
   candidateNames,
   {
     from: web3.eth.accounts[0], 
     data: '0x608060405234801561001057600080fd5b506040516102f23803806102f283398101604052805101805161003a906001906020840190610041565b50506100ab565b82805482825590600052602060002090810192821561007e579160200282015b8281111561007e5782518255602090920191600190910190610061565b5061008a92915061008e565b5090565b6100a891905b8082111561008a5760008155600101610094565b90565b610238806100ba6000396000f30060806040526004361061006c5763ffffffff7c01000000000000000000000000000000000000000000000000000000006000350416632f265cf78114610071578063392e66781461009f5780637021939f146100cb578063b13c744b146100e3578063cc9ab2671461010d575b600080fd5b34801561007d57600080fd5b50610089600435610127565b6040805160ff9092168252519081900360200190f35b3480156100ab57600080fd5b506100b7600435610153565b604080519115158252519081900360200190f35b3480156100d757600080fd5b506100896004356101a0565b3480156100ef57600080fd5b506100fb6004356101b5565b60408051918252519081900360200190f35b34801561011957600080fd5b506101256004356101d4565b005b600061013282610153565b151561013d57600080fd5b5060009081526020819052604090205460ff1690565b6000805b60015481101561019557600180548491908390811061017257fe5b600091825260209091200154141561018d576001915061019a565b600101610157565b600091505b50919050565b60006020819052908152604090205460ff1681565b60018054829081106101c357fe5b600091825260209091200154905081565b6101dd81610153565b15156101e857600080fd5b6000908152602081905260409020805460ff8082166001011660ff199091161790555600a165627a7a72305820943f89b031a215cff20e4d7fdadf277223e47612eac75ad93885f5c01a3930240029', 
     gas: '4700000'
   }, function (e, contract){
    console.log(e, contract);
    if (typeof contract.address !== 'undefined') {
         console.log('Contract mined! address: ' + contract.address + ' transactionHash: ' + contract.transactionHash);
    }
 })

我們要關心的只有兩個地方,第一行定義了一個變數,該變數在部署的時候作為引數傳遞給建構函式,我們要自己給變數賦值。
第五行的“from: web3.eth.accounts[0]”是要指定使用哪個賬戶進行部署,我們使用新建立的賬戶進行部署,所以要改成“from: web3.eth.accounts[1]”
修改後的結果如下

var candidateNames = ['Rama','Nick','Jose']/* var of type bytes32[] here */ ;
var votingContract = web3.eth.contract([{"constant":true,"inputs":[{"name":"candidate","type":"bytes32"}],"name":"totalVotesFor","outputs":[{"name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"candidate","type":"bytes32"}],"name":"validCandidate","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"bytes32"}],"name":"votesReceived","outputs":[{"name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"uint256"}],"name":"candidateList","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"candidate","type":"bytes32"}],"name":"voteForCandidate","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"candidateNames","type":"bytes32[]"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"}]);
var voting = votingContract.new(
   candidateNames,
   {
     from: web3.eth.accounts[1], 
     data: '0x608060405234801561001057600080fd5b506040516102f23803806102f283398101604052805101805161003a906001906020840190610041565b50506100ab565b82805482825590600052602060002090810192821561007e579160200282015b8281111561007e5782518255602090920191600190910190610061565b5061008a92915061008e565b5090565b6100a891905b8082111561008a5760008155600101610094565b90565b610238806100ba6000396000f30060806040526004361061006c5763ffffffff7c01000000000000000000000000000000000000000000000000000000006000350416632f265cf78114610071578063392e66781461009f5780637021939f146100cb578063b13c744b146100e3578063cc9ab2671461010d575b600080fd5b34801561007d57600080fd5b50610089600435610127565b6040805160ff9092168252519081900360200190f35b3480156100ab57600080fd5b506100b7600435610153565b604080519115158252519081900360200190f35b3480156100d757600080fd5b506100896004356101a0565b3480156100ef57600080fd5b506100fb6004356101b5565b60408051918252519081900360200190f35b34801561011957600080fd5b506101256004356101d4565b005b600061013282610153565b151561013d57600080fd5b5060009081526020819052604090205460ff1690565b6000805b60015481101561019557600180548491908390811061017257fe5b600091825260209091200154141561018d576001915061019a565b600101610157565b600091505b50919050565b60006020819052908152604090205460ff1681565b60018054829081106101c357fe5b600091825260209091200154905081565b6101dd81610153565b15156101e857600080fd5b6000908152602081905260409020805460ff8082166001011660ff199091161790555600a165627a7a72305820943f89b031a215cff20e4d7fdadf277223e47612eac75ad93885f5c01a3930240029', 
     gas: '4700000'
   }, function (e, contract){
    console.log(e, contract);
    if (typeof contract.address !== 'undefined') {
         console.log('Contract mined! address: ' + contract.address + ' transactionHash: ' + contract.transactionHash);
    }
 })

結果返回錯誤

Error: authentication needed: password or unlock undefined

這個錯誤是因為賬戶被鎖定了,所以我們要先給賬戶進行解鎖

personal.unlockAccount(eth.accounts[1],"123");

然後在執行部署命令,返回

Contract mined! address: 0x9abbc0dc0d688b280e612cb4d1e73a35f61b2895 transactionHash: 0x0d13541db32353e2aba900dc03b2c1de1acd3c1bab267aea9b695043b9958b43

說明部署成功。
我們來查詢一下賬戶1的餘額,發現餘額減少了,說明部署是需要消耗以太幣的。

互動

合約部署完畢,接下來就是進行互動了。
在部署的時候,我們輸入終端的的程式碼辣麼長,但其實只有三句話,每句話定義了一個變數,最後一個變數voting就的定義的合約例項化的物件,可以通過這個變數執行合約中的函式
在合約構造的收,我們傳入了三個人名“[‘Rama’,’Nick’,’Jose’]”,現在我們來檢視一下rama的選票數

voting.totalVotesFor('Rama')

返回的結果是0,因為還沒人給rama投票,我們使用賬戶1給rama投一票

voting.voteForCandidate('Rama', {from: web3.eth.accounts[1]})

然後再看一下rama的選票數,發現rama已經有了一票了
在看一下賬戶1的餘額

eth.getBalance(eth.accounts[1])

發現餘額有少了幾個wei,投票需要向網路上寫資料,所以要消耗以太幣,而查詢不用寫資料,就不用消耗以太幣了。