1. 程式人生 > >以太坊教程:學習以太坊Dapp開發

以太坊教程:學習以太坊Dapp開發

以太坊教程 以太坊 以太坊開發 智能合約 區塊鏈 dapp

一、區塊鏈

1. 分布式去中心化

比特幣設計的初衷就是要避免依賴中心化的機構,沒有發行機構,也不可能操縱發行數量。既然沒有中心化的信用機構,在電子貨幣運行的過程中,也勢必需要一種機制來認可運行在區塊鏈上的行為(包括比特幣的運營,亦或是運行在區塊鏈上的其他業務),這種機制就是共識機制。在完全去中心化的區塊鏈上運行的比特幣,采用的是PoW(Proof of Work,工作量證明),該機制完美的解決了拜占庭將軍問題(存在異常的情況下仍能達成一致)。因為基礎網絡架構為分布式,對單獨一個節點是無法控制或破壞整個網絡,掌握網內51%的運算能力(非節點數)才有可能操作交易,而這個代價大概要超過270億美元。

2. 無須信任

整個區塊鏈網絡中的數據是公開透明的,每個節點(參與者)都可自由加入該網絡中,下載到所有的數據。任意兩個節點間的數據交換無需互相信任,完全依靠區塊鏈中的交易歷史和數據的可追溯,以及共識機制來保證數據交換的正確且不可逆的執行。

3. 不可篡改和加密安全性

跟當前銀行網銀系統(特別是公司網銀系統)的加密機制類似,區塊鏈的數據結構和交易流程中大量的使用了公私鑰來加解密,保證數據的安全性。基於該技術基礎,甚至可以應用群組簽名來保證共有數據的安全性。任何事物既然有優點,也同時會存在不足之處。根源於分布式網絡架構和共識機制,在區塊鏈上運行的交易確認時間會比較長(比特幣的確認時間大概是15分鐘),交易並發數受限(比特幣的每秒交易數為7筆,而淘寶的每秒並發數能達到10萬左右),區塊的容量限制(當前為1M,區塊鏈的擴容一直在討論中),監管難以介入,基於工作量證明的共識機制存在浪費系統資源和帶寬的問題。

4. 區塊鏈技術

a. 區塊

區塊是一個包含在區塊鏈(公開賬簿)裏的聚合了交易信息的容器。它由一個包含元數據的區塊頭和緊跟其後的構成區塊主體的一長串交易組成。區塊頭是80字節,而平均每個交易至少是250字節,而且平均每個區塊至少包含超過500個交易。
區塊結構如下圖


交易(Tx)詳情中的結構如下圖


b. 區塊鏈

當一個節點從網絡接受到傳入的區塊時,它會驗證這些區塊,然後鏈接到現有的區塊鏈上,鏈接的形態如下圖:


由於每個區塊包含前一個區塊的HASH值,這就使得從創世塊到當前塊形成了一條塊鏈,每個區塊必定按時間順序跟隨在前一個區塊之後,因為如果不知道前一塊區塊的HASH值就沒法生成當前區塊。要改變一個已經在塊鏈中存在一段時間的區塊,從計算上來說是不可行的,因為如果它被改變,它之後的每個區塊必須隨之改變。這些特性使得雙花比特幣非常困難,區塊鏈是比特幣的最大創新。

5. 比特幣錢包

a. 比特幣錢包的生成

  1. 首先使用隨機數發生器生成一個 私鑰 。一般來說這是一個256bits的數,擁有了這串數字就可以對相應 錢包地址 中的比特幣進行操作,所以必須被安全地保存起來。
  2. 私鑰經過SECP256K1算法處理生成了公鑰。SECP256K1是一種橢圓曲線算法,通過一個已知私鑰時可以算得公鑰,而公鑰已知時卻無法反向計算出私鑰。這是保障比特幣安全的算法基礎。
  3. 同SHA256一樣,RIPEMD160也是一種Hash算法,由公鑰可以計算得到公鑰哈希,而反過來是行不通的。
  4. 將一個字節的地址版本號連接到公鑰哈希頭部(對於比特幣網絡的pubkey地址,這一字節為“0”),然後對其進行兩次SHA256運算,將結果的前4字節作為公鑰哈希的校驗值,連接在其尾部。
  5. 將上一步結果使用BASE58進行編碼(比特幣定制版本),就得到了錢包地址。

流程圖如下


b .轉賬

比特幣錢包間的轉賬是通過交易(Transaction)實現的。交易數據是由轉出錢包私鑰的所有者生成,也就是說有了私鑰就可以花費該錢包的比特幣余額。生成交易的過程如下:


  1. 交易的原始數據包括“轉賬數額”和“轉入錢包地址”,但是僅有這些是不夠的,因為無法證明交易的生成者對“轉出錢包地址”余額有動用的權利。所以需要用私鑰對原始數據進行簽名。
  2. 生成“轉出錢包公鑰”,這一過程與生成錢包地址的第2步是一樣的。
  3. 將“轉出簽名”和“轉出公鑰”添加到原始交易數據中,生成了正式的交易數據,這樣它就可以被廣播到比特幣網絡進行轉賬了。

二、以太坊Ethereum

1. 概念

a. 什麽是以太坊

簡單來說,以太坊是一種新的法律形式。現行法律的本質是一種合約。它是由(生活於某一社群的)人和他們的領導者之間所締結的,一種關於彼此該如何行動的共識。個體之間也存在著一些合約,這些合約可以理解為一種私法,相應的,這種私法僅對合約的參與者生效。

例如,你和一個人訂立合約,借給他一筆錢,但他最後毀約了,不打算還這筆錢。此時你多半會將對方告上法庭。在現實生活中,打官司這種事情常常混亂不堪並且充滿了不確定性。將對方告上法庭,也通常意味著你需要支付高昂的費用聘請律師,來幫你在法庭上針對法律條文展開辯論,而且這一過程一般都曠日持久。而且,即使你最終贏了官司,你依然可能會遇到問題(比如,對方拒不執行法庭判決)。
令人欣慰的是,當初你和借款人把條款寫了下來,訂立了合約。但法律的制定者和合約的起草者們都必須面對一個不容忽視的挑戰:那就是,理想情況下,法律或者合約的內容應該是明確而沒有歧義的,但現行的法律和合約都是由語句構成的,而語句,則是出了名的充滿歧義。
因此,一直以來,現行的法律體系都存在著兩個巨大的問題:首先,合約或法律是由充滿歧義的語句定義的,第二,強制執行合約或法律的代價非常大。
而以太坊,通過數字貨幣和編程語言的結合,解決了現行法律體系的這兩大問題。
以太坊系統自身帶有一種叫做以太幣(Ether)的數字貨幣。以太幣和著名的數字貨幣比特幣(Bitcoin)有著非常多的相似之處。兩者均為數字儲值貨幣,且無法偽造,都以去中心化的方式運行來保證貨幣供應不被某一方所控制。兩者都可以像電子郵件一樣,作為貨幣自由地在全世界流通。而且,由於它們可以做到傳統貨幣做不到的事情,因此用戶對它們未來的價值充滿期待 。

另外
1.詳情請閱讀以太坊白皮書 (中文, 英文)。
2.以太坊教程

b. 基本知識

  • 公鑰加密系統。 Alice有一把公鑰和一把私鑰。她可以用她的私鑰創建數字簽名,而Bob可以用她的公鑰來驗證這個簽名確實是用Alice的私鑰創建的,也就是說,確實是Alice的簽名。當你創建一個以太坊或者比特幣錢包的時候,那長長的0xdf...5f地址實質上是個公鑰,對應的私鑰保存某處。類似於Coinbase的在線錢包可以幫你保管私鑰,你也可以自己保管。如果你弄丟了存有資金的錢包的私鑰,你就等於永遠失去了那筆資金,因此你最好對私鑰做好備份。
  • 點對點網絡。 就像BitTorrent, 以太坊分布式網絡中的所有節點都地位平等,沒有中心服務器。
  • 區塊鏈。 區塊鏈就像是一個全球唯一的帳簿,或者說是數據庫,記錄了網絡中所有交易歷史。
  • 以太坊虛擬機(EVM)。 它讓你能在以太坊上寫出更強大的程序(比特幣上也可以寫腳本程序)。它有時也用來指以太坊區塊鏈,負責執行智能合約以及一切。
  • 節點。 你可以運行節點,通過它讀寫以太坊區塊鏈,也即使用以太坊虛擬機。完全節點需要下載整個區塊鏈。輕節點仍在開發中。
  • 礦工。 挖礦,也就是處理區塊鏈上的區塊的節點。這個網頁可以看到當前活躍的一部分以太坊礦工:stats.ethdev.com。
  • 工作量證明。 礦工們總是在競爭解決一些數學問題。第一個解出答案的(算出下一個區塊)將獲得以太幣作為獎勵。然後所有節點都更新自己的區塊鏈。所有想要算出下一個區塊的礦工都有與其他節點保持同步,並且維護同一個區塊鏈的動力,因此整個網絡總是能達成共識。(註意:以太坊正計劃轉向沒有礦工的權益證明系統(POS),不過那不在本文討論範圍之內。)
  • 以太幣。 縮寫ETH。一種你可以購買和使用的真正的數字貨幣。這裏是可以交易以太幣的其中一家交易所的走勢圖。在寫這篇文章的時候,1個以太幣價值65美分。
  • Gas. 在以太坊上執行程序以及保存數據都要消耗一定量的以太幣,Gas是以太幣轉換而成。這個機制用來保證效率。
  • DApp. 以太坊社區把基於智能合約的應用稱為去中心化的應用程序(Decentralized App)。DApp的目標是(或者應該是)讓你的智能合約有一個友好的界面,外加一些額外的東西,例如IPFS(可以存儲和讀取數據的去中心化網絡,不是出自以太坊團隊但有類似的精神)。DApp可以跑在一臺能與以太坊節點交互的中心化服務器上,也可以跑在任意一個以太坊平等節點上。這裏分享一個以太坊DApp教程,可以高效的學習如何開發一個DApp,很適合入門。

2. 工作流程

a. 環境搭建

建議使用Mac OS環境,不然可能會出現各種坑。

  1. 安裝NodeJS,安裝Python。
  2. 安裝testrpc(測試環境中使用),安裝go-ethereum(真實環境中使用)。
  3. 安裝solc。
  4. 安裝truffle。

如果是windows的話建議用工具ethbox可以一鍵安裝以太坊開發環境的工具: ethbox

b. Solidity語言簡介

下面是官網上面的一段關於智能投票合約的示例代碼

contract Ballot { //一個選民的構造體 struct Voter {
        uint weight; // 權重(即他可以投幾票) bool voted; //是否已經投過票 address delegate; // 代表地址(他可以代表某個人進行投票) uint vote; // index of the voted proposal } // 投票的提案的構造體 struct Proposal
    {
        bytes32 name; // 提案名稱 uint voteCount; //獲得的票數 }

    address public chairperson;//會議主席 //地址 -選民 的map mapping(address => Voter) public voters; // 投票種類的動態數組 Proposal[] public proposals; ///構造函數 function Ballot(bytes32[] proposalNames) {
        chairperson = msg.sender;//初始化會議主席 voters[chairperson].weight = 1; //初始化所有的提案 for (uint i = 0; i < proposalNames.length; i++) {

            proposals.push(Proposal({
                name: proposalNames[i],
                voteCount: 0 }));
        }
    } // 給予投票權 function giveRightToVote(address voter) returns (bool b) { if (msg.sender != chairperson || voters[voter].voted) { //對於會議主席和已經投過票的選民這裏不處理 return false;;
        }
        voters[voter].weight = 1; return true;
    } /// 投票權轉移函數 function delegate(address to) { // 投票權轉移的發起人 Voter sender = voters[msg.sender]; if (sender.voted) throw; //遞歸找到沒有轉移投票權的  選民 while (
            voters[to].delegate != address(0) &&
            voters[to].delegate != msg.sender
        ) {
            to = voters[to].delegate;
        } if (to == msg.sender) { throw;
        } //將發起人設置為已經投過票的狀態 sender.voted = true; //將代表設置為剛才遞歸獲取的選民 sender.delegate = to;
        Voter delegate = voters[to]; if (delegate.voted) { //如果代表已經投過票就在他投票的提案的票數增加 proposals[delegate.vote].voteCount += sender.weight;
        } else { //將代表的的票數增加 delegate.weight += sender.weight;
        }
    } /// 投票函數 function vote(uint proposal) {
        Voter sender = voters[msg.sender]; if (sender.voted) throw;
        sender.voted = true;
        sender.vote = proposal; //將投的提案票數增加 proposals[proposal].voteCount += sender.weight;
    } ///獲得票數最多的提案 function winningProposal() constant returns (uint winningProposal) {
        uint winningVoteCount = 0; for (uint p = 0; p < proposals.length; p++) { if (proposals[p].voteCount > winningVoteCount) {
                winningVoteCount = proposals[p].voteCount;
                winningProposal = p;
            }
        }
    }
}

解讀

  • address. 地址類型。chairperson是會議主席的錢包地址。這個地址會在合約的構造函數function Ballot()中被賦值。很多時候也稱呼這種地址為’owner’(所有人)。
  • public. 這個關鍵字表明變量可以被合約之外的對象使用。private修飾符則表示變量只能被本合約(或者衍生合約)內的對象使用。如果你想要在測試中通過web3.js使用合約中的某個變量,記得把它聲明為public。
  • Mapping或數組。mapping(address => Voter)為選民錢包地址和選民構造體的鍵值對。Proposal[] public proposals是一個提案構造體的數組。
  • 有特殊的變量和函數總是在全局命名空間存在,主要用於提供有關blockchain信息,例如msg,block,tx,其中msg.sender為發起人的地址。

solidity語言更深入的理解可以閱讀官方文檔。

c. 使用geth部署合約

  1. 啟動一個測試節點

geth --testnet --fast --cache=512 --genesis CustomGenesis.json console

這裏的CustomGenesis.json是為了給測試的賬戶分配以太幣

{ "coinbase": "0x0000000000000000000000000000000000000000", "difficulty": "0x20000", "extraData": "", "gasLimit": "0x2fefd8", "nonce": "0x0000000000000042", "mixhash": "0x0000000000000000000000000000000000000000000000000000000000000000", "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", "timestamp": "0x00", "alloc": { "0xe49c283bc6bf92c5833cc981b97679238dd3b5da": { "balance": "111111111000000000000000000000000000" }, "0xd8927c296b3ebe454a6409770a0c323ec4ed23ba": { "balance": "222222222000000000000000000000000000" }
    }
}

solc下的內容要替換成你的測試賬戶地址。具體geth的用法請查看官方文檔和源碼介紹。

  1. 使用solc編譯智能合約,獲得二進制代碼
    例如以下代碼
contract test { function multiply(uint a) returns(uint d) { return a * 7; 
      }
}

在geth中輸入

source = "contract test { function multiply(uint a) returns(uint d) { return a * 7; } }"

clientContract = eth.compile.solidity(source).test

編譯返回的結果的JSON格式如下


其中,

  • code:編譯後的EVM字節碼
  • info:編譯器返回的metadata
  • abiDefination:Application Binary Interface定義。具體接口規則參見這裏
  • compilerVersion:編譯此代碼的solidity編譯器版本
  • developerDoc:針對開發者的Natural Specification Format,類似於Doxygen
  • language:合約語言
  • languageVersion:合約語言版本
  • source:源代碼
  • userDoc:針對用戶的Ethereum的Natural Specification Format。

編譯器返回的JSON結構反映了合約部署的兩種不同的路徑。info信息真實的存在於區中心化的雲中,作為metadata信息來公開驗證Blockchain中合約代碼的實現。而code信息通過創建交易的方式部署到區塊鏈中。

  1. 使用solc編譯智能合約,獲得二進制代碼
    部署合約前,確保你有一個解鎖的賬戶並且賬戶中有余額,因為部署合約得過程中會消耗以太幣。輸入web3.fromWei(eth.getBalance(eth.accounts[0]),"ether")可以查看賬戶余額。
    解鎖一個賬戶

personal.unlockAccount(eth.accounts[0])

獲得賬戶

primaryAddress = eth.accounts[0]

定義一個abi (abi是個js的數組,否則不成功)

abi = [{ constant: false, inputs: [{ name: ’a’, type: ’uint256’ } ]}]

創建智能合約

MyContract = eth.contract(abi)

發送交易部署合約

contract = MyContract.new({from: primaryAddress, data:"0x6060604052602a8060106000396000f3606060405260e060020a6000350463c6888fa18114601a575b005b6007600435026060908152602090f3"})


如果交易被pending,如圖說明你的miner沒有在挖礦


啟動一個礦工
miner.setEtherbase(eth.primaryAddress) //設定開采賬戶
miner.start(8)

eth.getBlock("pending", true).transactions
這時候發現交易已經在區塊中


不過會發現,交易還是pending,這是因為該交易區塊沒有人協助進行運算驗證,這時候只需要再啟動一個礦工就行了
miner.start(8)

  1. 與合約進行交互

Multiply7 = eth.contract(clientContract.info.abiDefinition);
var myMultiply7 = Multiply7.at(contract.address);
myMultiply7.multiply.call(3)

myMultiply7.multiply.sendTransaction(3, {from: contract.address})

d. 使用truffle框架

使用truffle部署智能合約的步驟:

  1. truffle init (在新目錄中) => 創建truffle項目目錄結構,
  2. 編寫合約代碼,保存到contracts/YourContractName.sol文件。
    例如Ballot .sol,此時要找到migrations文件夾,在deploy_contracts.js文件中添加deployer.deploy(Ballot);
  3. truffile compile 編譯合約代碼。
  4. 啟動以太坊節點(例如在另一個終端裏面運行testrpc)。
  5. truffle migrate(在truffle項目目錄中)。

1. 編寫單元測試

test文件夾中新建ballot.js文件

contract(’Ballot’function(accounts)){ //accounts是所以賬戶得數值 it("獲取投票權"function(){ var meta = Ballot.deployed(); return meta.giveRightToVote(accounts[1]).then(function(b){
           assert.equal(Boolean(b),true,"獲取投票權失敗");
        });
    });

}

在項目根目錄下運行truffle test,你應該看到測試通過,如果使用自己構造的ballot對象,可以這樣寫:

contract(’Ballot’function(accounts)){ //accounts是所以賬戶得數值 it("獲取投票權"function(){ var proposals = [];
       proposals.push("proposal0");
       Ballot.new(proposals).then(function(meta){ return meta.giveRightToVote(accounts[1]).then(function(b){
               assert.equal(Boolean(b),true,"獲取投票權失敗");
            });
       });

    });

}
  • 合約中發送以太幣。 this是合約實例的地址,以變接下來檢查這個地址的余額(或者直接使用this.balance)
  • 當你通過web3.js調用交易函數時(使用web3.eth.sendTransaction),交易並不會立即執行。事實上交易會被提交到礦工網絡中,交易代碼直到其中一位礦工產生一個新區塊把交易記錄進區塊鏈之後才執行。因此你必須等交易進入區塊鏈並且同步回本地節點之後才能驗證交易執行的結果。用testrpc的時候可能看上去是實時的,因為測試環境很快,但是正式網絡會比較慢。
  • Gas. (譯註:以太坊上的燃料,因為代碼的執行必須消耗Gas。直譯為汽油比較突兀,故保留原文做專有名詞。)直到現在我們都沒有涉及Gas的概念,因為在使用testrpc時通常不需要顯式的設置。當你轉向geth和正式網絡時會需要。在交易函數調用中可以在{ from: _, value: _, gas: _ } 對象內設置Gas參數。Web3.js提供了web3.eth.gasPrice調用來獲取當前Gas的價格,Solidity編譯器也提供了一個參數讓你可以從命令行獲取合約的Gas開銷概要:solc --gas YouContract.sol.

2. 為合約創建一個界面

app目錄中,可以編寫自己的html和js文件,js與智能合約的交互與單元測試基本一致,例如一個界面上有一個輸入框和一個按鈕,獲得選民的投票權。

<!DOCTYPE html> <html> <head> <title>Ballot App</title> <link href=’https://fonts.googleapis.com/css?family=Open+Sans:400,700’ rel=’stylesheet’ type=’text/css’> <link href="./app.css" rel=’stylesheet’ type=’text/css’> <script src="./app.js"></script> </head> <body> <h1>Ballot</h1> <h2>Example Truffle Dapp</h2> <br> <h1>Send</h1> <br><label for="amount">Account:</label><input type="text" id="account" placeholder="e.g., 0x453468394hdfg84858345348"></input> <br><br><button id="getRightVote" onclick="getRight()">Get Right Vote</button> <br><br> <span id="status"></span> </body> </html> 

app.js中的代碼為

function getRight() { var account = document.getElementById("account").value; var meta = Ballot.deployed();

   meta.giveRightToVote(account).then(function(b){ if(Boolean(b)){
          setStatus("Get Right Vote Success");
        }else{
          setStatus("Get Right Vote Error");
        }
  }).catch(function(e){
    setStatus("Get Right Vote Error"); console.log(e);
  });

};

	

以太坊教程:學習以太坊Dapp開發