基于以太坊智能合約的Dapp開發(fā)與實踐

環(huán)境準備

安裝Go-lang
安裝go-ethereum 1.8+
Atom編譯器(optional)安裝solidity插件
安裝python3 + pyCharm(optional)安裝web3.py模塊
安裝Truffle (optional)

1, 定制創(chuàng)世區(qū)塊

1.1 創(chuàng)建工作目錄

創(chuàng)建工作目錄“devnet”,創(chuàng)建兩個節(jié)點目錄

$mkdir devnet
$cd devnet
$mkdir node1 ndoe2

1.2 生成賬戶

初始化兩個節(jié)點的以太坊賬戶與密碼

$geth --datadir node1/ account new
$geth --datadir node2/ account new
$echo {ACCOUNT1} >> accounts.txt
$echo {ACCOUNT2} >> accounts.txt
$echo {PASSWD1} > node1/password.txt
$echo {PASSWD2} > node2/password.txt

1.3 創(chuàng)建創(chuàng)世區(qū)塊

使用以太坊自帶工具puppeth生成創(chuàng)世區(qū)塊

$puppeth
Please specify a network name to administer (no spaces, please)

> devnet

What would you like to do? (default = stats)

1. Show network stats

2. Configure new genesis

3. Track new remote server

4. Deploy network components

> 2

Which consensus engine to use? (default = clique)

1. Ethash - proof-of-work

2. Clique - proof-of-authority

> 2

How many seconds should blocks take? (default = 15)

> 3 // for example

Which accounts are allowed to seal? (mandatory at least one)

> 0x87366ef81db496edd0ea2055ca605e8686eec1e6 //copy paste from account.txt :)

> 0x08a58f09194e403d02a1928a7bf78646cfc260b0

Which accounts should be pre-funded? (advisable at least one)

> {ACCOUNT1} // free ethers !

> {ACCOUNT2}

Specify your chain/network ID if you want an explicit one (default = random)

> {NETWORK_ID} // for example. Do not use anything from 1 to 10

Anything fun to embed into the genesis block? (max 32 bytes)

>

What would you like to do? (default = stats)

1. Show network stats

2. Manage existing genesis

3. Track new remote server

4. Deploy network components

> 2

1. Modify existing fork rules

2. Export genesis configuration

> 2

Which file to save the genesis into? (default = devnet.json)

> genesis.json

INFO [01-23|15:16:17] Exported existing genesis block

What would you like to do? (default = stats)

1. Show network stats

2. Manage existing genesis

3. Track new remote server

4. Deploy network components

> ^C // ctrl+C to quit puppeth

生成了genesis.json到當前路徑

2, 節(jié)點初始化

2.1 用創(chuàng)世文件初始化節(jié)點

$geth —datadir node1/ init genesis.json
$geth —datadir node2/ init genesis.json

2.2 初始化boot節(jié)點

$bootnode -genkey boot.key

3, 開始挖礦

3.1 開啟bootnode

$ bootnode -nodekey boot.key -verbosity 9 -addr :30310 // 請勿使用 30303

3.2 開啟礦工節(jié)點

$geth --datadir node1/ --syncmode 'full' --port 30311 --rpc --rpcaddr 'localhost' --rpcport 8551 --rpcapi 'personal,db,eth,net,web3,txpool,miner' --bootnodes 'enode://3ec4fef2d726c2c01f16f0a0030f15dd5a81e274067af2b2157cafbf76aa79fa9c0be52c6664e80cc5b08162ede53279bd70ee10d024fe86613b0b09e1106c40@127.0.0.1:30310' --networkid 1515 --gasprice '1' --unlock '0x87366ef81db496edd0ea2055ca605e8686eec1e6' --password node1/password.txt --mine

$ geth --datadir node2/ --syncmode 'full' --port 30312 --rpc --rpcaddr 'localhost' --rpcport 8552 --rpcapi 'personal,db,eth,net,web3,txpool,miner' --bootnodes 'enode://3ec4fef2d726c2c01f16f0a0030f15dd5a81e274067af2b2157cafbf76aa79fa9c0be52c6664e80cc5b08162ede53279bd70ee10d024fe86613b0b09e1106c40@127.0.0.1:30310' --networkid 1515 --gasprice '0' --unlock '0x08a58f09194e403d02a1928a7bf78646cfc260b0' --password node2/password.txt --mine

控制臺命令參數(shù)解析:

--datadir 指定鏈的數(shù)據(jù)位置

--syncmode 

    ‘full’:節(jié)點同步所有區(qū)塊頭 區(qū)塊數(shù)據(jù)以及簽名 ,并驗證區(qū)塊

    ‘fast’:節(jié)點同步區(qū)塊頭和區(qū)塊數(shù)據(jù),不做任何校驗,類似于“快照”

    ‘light’:節(jié)點直接從其他‘full’節(jié)點同步當前狀態(tài)

--rpcapi 會使用那些api與此節(jié)點交互

--rpcaddr 這個節(jié)點監(jiān)聽從哪里發(fā)來的rpc請求

--rpcport 這個節(jié)點監(jiān)聽從哪個端口發(fā)來的rpc請求

--bootnodes 指定bootnode ID

--networkid 同一個鏈上節(jié)點networkid相同

--gasprice 這個礦工節(jié)點可以接受的最小gas

--mine 開始挖礦

3.3 創(chuàng)建第三個節(jié)點

$mkdir node3
$geth --datadir node3/ account new
$echo {ACCOUNT3} >> accounts.txt
$echo {PASSWD3} > node3/password.txt
$geth --datadir node3/ init genesis.json

3.4 已存在節(jié)點投票加入新節(jié)點

進入node1的控制臺

$geth attach node1/geth.ipc

投票加入node3的地址

>clique.propose( {ACCOUNT3}, true)

>exit

同上

$geth attach node2/geth.ipc

>clique.propose( {ACCOUNT}, true)

>exit

$ geth --datadir node3/ --syncmode 'full' --port 30313 --rpc --rpcaddr 'localhost' --rpcport 8553 --rpcapi 'personal,db,eth,net,web3,txpool,miner' --bootnodes 'enode://3ec4fef2d726c2c01f16f0a0030f15dd5a81e274067af2b2157cafbf76aa79fa9c0be52c6664e80cc5b08162ede53279bd70ee10d024fe86613b0b09e1106c40@127.0.0.1:30310' --networkid 1515 --gasprice '0' --unlock '0x08a58f09194e403d02a1928a7bf78646cfc11ab3' --password node3/password.txt --mine

4,智能合約

4.1 EVM基礎(chǔ)

賬戶

以太坊中有兩種不同類型但是共享同一地址空間的賬戶:外部賬戶由一對公私鑰控制,合約賬戶由賬戶內(nèi)部的合約代碼控制。

外部賬戶的地址是由公鑰(經(jīng)過hash運算)決定的,而合約賬戶的地址在此合約被創(chuàng)建的時候決定的(由合約創(chuàng)建者的地址和發(fā)送到此合約地址的交易數(shù)決定,這就是所謂的“nonce”)不管是哪種類型的賬戶,EVM的處理方式是一樣的
每個賬戶都有一個持久的key-value類型的存儲,把256字節(jié)的key映射到256字節(jié)的value
此外,每個賬戶都有以“Wei”為單位,在交易過程中會被修改的資產(chǎn)(balance)信息

交易

交易是一個從賬戶發(fā)往另一個賬戶(可以是同一個賬戶或者是special zero-account)的消息。它包含二進制數(shù)據(jù)(交易相關(guān)的數(shù)據(jù))and Ether。
如果目標賬戶包含代碼,代碼會被執(zhí)行,交易相關(guān)的數(shù)據(jù)將作為參數(shù)
如果目標賬戶是地址為0的賬戶zero-account, 交易會創(chuàng)建一個新的合約。如上文提到的,合約地址不是一個地址為0的地址,而是一個由交易發(fā)送者和交易數(shù)來決定的地址。這樣的一筆(到zero-account)交易的相關(guān)參數(shù)會被轉(zhuǎn)化為EVM字節(jié)碼然后被執(zhí)行,輸出結(jié)果就是被永久存儲的合約代碼。這意味著為了創(chuàng)建一個合約,并不需要發(fā)送真實的合約代碼,代碼可以被自動創(chuàng)建

費用

創(chuàng)建之后,每筆交易都需要一定數(shù)量的費用,用于限制交易所消耗的工作量,即交易是需要付出代價的(避免DDoS攻擊)。EVM執(zhí)行交易的過程中,費用會按一個特殊規(guī)則逐漸減少
費用的多少是由交易發(fā)起者設(shè)置,至少需要從發(fā)起賬戶支付gas_price * gas用費。如果交易執(zhí)行完畢費用還有剩余的,將退回到發(fā)起賬戶。
如果交易完成之前費用耗盡,將會拋出一個out-of-gas的異常,所有的修改都會被回滾

Storage,Memory,Stack

每個賬戶都有一個持久的內(nèi)存空間,稱之為storage,storage以key-value形式存儲,256字節(jié)的key映射到256字節(jié)value,合約內(nèi)部不可能枚舉storage(內(nèi)部元素),讀取或者修改storage操作消耗都很大(原文是 It is not possible to enumerate storage from within a contract and it is comparatively costly to read and even more so, to modify storage. )。 合約只能讀取和修改自己的storage里的數(shù)據(jù)。
第二種內(nèi)存空間稱之為memory,里面存儲著每個消息調(diào)用時合約創(chuàng)建的實例。memory是線型的,可以以字節(jié)級別來處理,但是限制為256字節(jié)寬度,寫入可以是8或256字節(jié)寬度。當讀取或?qū)懭胍粋€預(yù)先未觸發(fā)的指令的時候會消耗memory的空間,消耗空間的同時,必須支付費用(gas)。memory消耗的越多,手續(xù)費越多(按平方級增長)
EVM不是一個注冊的機器而是一個堆棧機器,所以所有的計算指令都在stack空間里面執(zhí)行。stack最多只能容納1024個長度不超過256字節(jié)的指令元素。只能用下述方法,從頂部訪問stack:可以拷貝最頂部的16個元素中的一個到stack的最頂部,或者將最頂部的那個元素與其下面的16個元素之一互換。所有其它操作從stack最頂部取出兩個(或一個,或更多,取決于操作)元素,然后把結(jié)果push到stack頂端。當然將stack中的元素移到memory或者storage也是可以的,但是不能直接訪問stack中間的元素(必須從頭部開始訪問)

4.2 Solidity代碼實例

pragma solidity ^0.4.18;
contract Ballot{

    address founder;  // 用于記錄誰部署的合約
    struct Proposal{
        string name;    //提案的名稱
        uint count;     // 用于統(tǒng)計票數(shù)
    }
    Proposal[] public proposals;  // 數(shù)組,用來保存所有提案
    mapping(address => uint8) voters;  // 映射,用來保存所有投票者

    function Ballot() public {    // 構(gòu)造函數(shù),在合同部署時調(diào)用一次
        founder = msg.sender;     //記錄構(gòu)造函數(shù)的調(diào)用者,也就是合同部署人
    }

    
    function activeVoter(address voterAddr) public{
        if (founder != msg.sender){  // 判斷,只允許部署合同的管理員新增投票者
            return;
        }
        if(voters[voterAddr] == 0){
            voters[voterAddr] = 1;
        }
        return;
    }

    function activeProposal(string proposalName) public{
        if (founder != msg.sender){    // 判斷,只允許部署合同的管理員增加新提議
            return;
        }
        for (uint index = 0; index < proposals.length; index++){
            if(keccak256(proposals[index].name) == keccak256(proposalName)){    //  keccak256 就是 sha256
                return;
            }
        }
        proposals.push(Proposal({
            name: proposalName,
            count: 0 }));
        return;
    }

    // 所有被“激活”的投票者都可以投出一票 給指定提案
    function vote(string proposalName) public {
        address voter = msg.sender;
        if(voters[voter] != 1){
            return;
        }
        for(uint index = 0; index < proposals.length; index ++){  
            if(keccak256(proposals[index].name) == keccak256(proposalName)){   // 判斷指定提案是否存在
                proposals[index].count++;
                voters[voter] = 2;
                return;
            }
        }
        return;
    }

    // 任何人都可以查詢哪項提案獲得的票數(shù)最高
    function getWinner() public constant returns(string winnerName, uint winnerCount) {
        winnerCount = 0;
        winnerName = "";
        for(uint index = 0; index < proposals.length; index ++){ 
            if(proposals[index].count > winnerCount){
                winnerName = proposals[index].name;
                winnerCount = proposals[index].count;
            }
        }
    }
}

5,Dapp的部署與交互

5.1 使用traffle 部署合約

進入一個空目錄,執(zhí)行

$truffle init

在contract目錄下,新增.sol文件用來寫合約代碼
在migration目錄下新增2_deploy_contracts.js文件,內(nèi)容如下:

var YourContract = artifacts.require(“./你的合約文件名.sol");
module.exports = function(deployer) {
    deployer.deploy(YourContract);
};

本目錄下執(zhí)行

$truffle compile

在truffle項目下獲取package.json

$npm init -f
安裝web3

$npm install —save web3@0.20

編輯 truffle.js 加入步驟1的node11節(jié)點信息

module.exports = {
    networks: {
    nodeth: {         // “nodeth” 是我給網(wǎng)絡(luò)起的名字
        network_id: 999, // network id associated with your node
        host:'127.0.0.1',
        port:8811,   // same with node11 supported
        gas: 400000000,
        from: “0x6875483cd851990ddfcd5fd49f6732d71cbedb46”. // coinbase  for node11
        }
    }
};

Truffle 目錄下執(zhí)行命令

$truffle deploy — network nodeth        // “nodeth” 對應(yīng)truffle.js 配置里的網(wǎng)絡(luò)名 

第二次部署可以用· truffle migrate —reset —network {you Network}
在geth控制臺終端執(zhí)行挖礦,將會看到新合約被部署了,合約地址被返回

5.2 使用python部署合約 (推薦)

新建區(qū)塊鏈部署模塊deploy.py
引入如下模塊

import time
import sys
from web3 import Web3, HTTPProvider
from solc import compile_source

將solidity 合約代碼復(fù)制到字符串變量

_contract_source_code_ballot = '''
pragma solidity ^0.4.18;
contract Ballot{

    address founder;  // who found this contract
    struct Proposal{
        string name;
        uint count;  // count ballot
    }
    Proposal[] public proposals;  // All proposals
    mapping(address => uint8) voters;

    function Ballot() public {
        founder = msg.sender;
    }

    // Only founder could active voter
    function activeVoter(address voterAddr) public{
        if (founder != msg.sender){
            return;
        }
        if(voters[voterAddr] == 0){
            voters[voterAddr] = 1;
        }
        return;
    }

    // Only founder could active proposal
    function activeProposal(string proposalName) public{
        if (founder != msg.sender){
            return;
        }
        for (uint index = 0; index < proposals.length; index++){
            if(keccak256(proposals[index].name) == keccak256(proposalName)){
                return;
            }
        }
        proposals.push(Proposal({
            name: proposalName,
            count: 0 }));
        return;
    }

    // Any activated voter could vode
    function vote(string proposalName) public {
        address voter = msg.sender;
        if(voters[voter] != 1){
            return;
        }
        for(uint index = 0; index < proposals.length; index ++){
            if(keccak256(proposals[index].name) == keccak256(proposalName)){
                proposals[index].count++;
                voters[voter] = 2;
                return;
            }
        }
        return;
    }

    // Anyone could check who is getting the most votes
    function getWinner() public constant returns(string winnerName, uint winnerCount) {
        winnerCount = 0;
        winnerName = "";
        for(uint index = 0; index < proposals.length; index ++){
            if(proposals[index].count > winnerCount){
                winnerName = proposals[index].name;
                winnerCount = proposals[index].count;
            }
        }
    }
}

'''
編譯合約源代碼

# ------------- compile contract -------------
compiled_sol = compile_source(_contract_source_code_ballot)   # Compiled source code
contract_interface = compiled_sol['<stdin>:Ballot']

使用web3調(diào)用區(qū)塊鏈服務(wù)

w3 = Web3(HTTPProvider('http://127.0.0.1:8501'))   #指定了調(diào)用哪個節(jié)點

部署合約上區(qū)塊鏈

# 編譯后會生成 abi 和 字節(jié)碼。
contract = w3.eth.contract(abi=contract_interface['abi'], bytecode=contract_interface['bin'])
account = w3.eth.accounts[0]
print(account)
tx_hash = contract.deploy(transaction={'from': account, 'gas': 4000000})
time.sleep(6)  // Wait new block confirmation
tx_receipt = w3.eth.getTransactionReceipt(tx_hash)
contract_address = tx_receipt['contractAddress']
print(contract_address)

提問:為什么要sleep(6)?

6 與區(qū)塊鏈的交互

新建與鏈交互的模塊 contactchain.py
引入如下模塊

import time
from web3 import Web3, HTTPProvider
from solc import compile_source
from web3.contract import ConciseContract

與合約交互需要:1,合約在鏈上的地址;2,合約代碼

# Solidity source code
_contract_source_code_ballot = '''
pragma solidity ^0.4.18;
contract Ballot{
    address founder;
    struct Proposal{
...
...

_contract_source_address_ballot = "0x580c04396aa683214034614e428935ad48De39A9"

現(xiàn)在我們模擬兩位員工,主機上分別跑著一個以太坊節(jié)點

_web3_provider_employee1 = 'http://127.0.0.1:8501'
_web3_provider_employee2 = 'http://127.0.0.1:8502'

定義一個類 ContactChainBallot,其中聲明active_voter, active_proposal, vote, get_winner, get_proposal 方法與鏈交互,在以上方法內(nèi)部,分別調(diào)用了智能合約里定義的方法。

class ContactChainBallot:

    # Find the contract from chain
    def __init__(self, employee = "employee1"):
        if(employee == 'employee2'):
            web3_provider = _web3_provider_employee2
        else:
            web3_provider = _web3_provider_employee1
        compiled_sol = compile_source(_contract_source_code_ballot)
        contract_interface = compiled_sol['<stdin>:Ballot']
        w3 = Web3(HTTPProvider(web3_provider))
        self.account = w3.eth.accounts[0]
        self.contract_instance = w3.eth.contract(contract_interface['abi'], _contract_source_address_ballot, ContractFactoryClass=ConciseContract)

    # -------------- interactive with contract -------------
    def active_voter(self, voter_address):
        self.contract_instance.activeVoter(voter_address, transact={'from': self.account, 'gas': 4000000})
        time.sleep(6)

    def active_proposal(self, proposal_name):
        self.contract_instance.activeProposal(proposal_name, transact={'from': self.account, 'gas': 4000000})
        time.sleep(6)

    def vote(self, proposal_name):
        return self.contract_instance.vote(proposal_name, transact={'from': self.account, 'gas': 4000000})

    def get_winner(self):
        return self.contract_instance.getWinner()

    def get_proposal(self, index):
        return self.contract_instance.proposals(index)

引用文章:
Go Ethereum
Proof Of Authority
智能合約介紹

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 本文是對以太坊文檔 Ethereum Frontier Guide 和 Ethereum Homestead 的整...
    趁風卷閱讀 9,775評論 0 16
  • 以太坊(Ethereum ):下一代智能合約和去中心化應(yīng)用平臺 翻譯:巨蟹 、少平 譯者注:中文讀者可以到以太坊愛...
    車圣閱讀 3,928評論 1 7
  • 1. 安裝以太坊節(jié)點客戶端 推薦 go-ethereum, 這是 go 語言實現(xiàn)的以太坊客戶端,可在這里下載二進...
    yanging閱讀 3,817評論 0 6
  • 從早八點到晚八點,兩節(jié)課,兩頓飯,第二名。真的挺難忘的。也挺辛苦的。不過還好結(jié)局是好的,因為前三名都有實習(xí)面試的機...
    Sanity娜娜閱讀 212評論 1 1
  • 我是一個特別簡單的人。 上大學(xué)時,第一次和另外七個女生住在一間不大的宿舍里,我不大會和別人相處,生活上的自理能力也...
    那只毛蟲閱讀 625評論 1 0

友情鏈接更多精彩內(nèi)容