環(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)