閱讀完本文,你將可以在一臺(tái)物理機(jī)上搭建起一個(gè)完整的以太坊私鏈,并且部署自己的合約。用以開(kāi)發(fā)、調(diào)試、學(xué)習(xí)以太坊。
本文涉及的知識(shí)和技術(shù)有:
- Docker:一種時(shí)下流行的容器
- geth:以太坊客戶(hù)端的go實(shí)現(xiàn)
- truffle:以太坊合約
Docker環(huán)境搭建
關(guān)于如何搭建Docker環(huán)境,可以參考我之前的一篇文章。以下的內(nèi)容都是基于該文搭建的環(huán)境。如果你已經(jīng)有了可以運(yùn)行的Docker環(huán)境,可以忽略。
文章地址:http://m.itdecent.cn/p/7ca57e4f66c5
主機(jī)配置
此處需要補(bǔ)充一點(diǎn)的是,如果您使用的是虛擬機(jī)、云主機(jī)或者老破小機(jī)器,需要確保分配給主機(jī)的內(nèi)存至少2GB或以上。因?yàn)橐蕴粸榱说钟忍卮箨戇@樣的ASIC礦機(jī)對(duì)算力的壟斷,采用了和比特幣完全不同的PoW算法——ethash。該算法的特點(diǎn)是算力不敏感,內(nèi)存敏感。該算法目前需要在內(nèi)存創(chuàng)建大約1GB的DAG用來(lái)做PoW運(yùn)算,且DAG會(huì)隨著區(qū)塊的增加呈階梯狀增長(zhǎng),因此建議至少給挖礦節(jié)點(diǎn)的宿主機(jī)器分配至少2GB的內(nèi)存,且保留擴(kuò)大內(nèi)存容量的靈活性。
獲取geth鏡像
docker hub上有現(xiàn)成的geth鏡像。直接獲取。
docker pull ethereum/client-go:v1.8.12
試運(yùn)行一下
docker run -it --rm -v /workspace:/workspace --entrypoint /bin/sh ethereum/client-go:v1.8.12
此處修改鏡像默認(rèn)的entrypoint是為了不讓節(jié)點(diǎn)自動(dòng)運(yùn)行。我們稍后會(huì)對(duì)節(jié)點(diǎn)進(jìn)行自定義配置使其成為私有鏈節(jié)點(diǎn)。
-v將本地的/workspace目錄掛載成容器的/workspace目錄,用來(lái)在容器和宿主機(jī)器之間共享文件
創(chuàng)建Docker網(wǎng)絡(luò)
舊版本的docker容器相互之間是依靠link建立關(guān)系。
新版本docker推薦創(chuàng)建自有網(wǎng)路,再將需要互聯(lián)的容器配置到相同的網(wǎng)絡(luò)中。
于是,我們創(chuàng)建一個(gè)名為“ethnet“的網(wǎng)絡(luò)。該網(wǎng)絡(luò)配置如下:
- 子網(wǎng)172.18.0.0/16
- IP段172.18.0.0
- 掩碼255.255.0.0
- IP范圍172.18.0.1~172.18.255.254
- IP廣播172.18.255.255
docker network create -d bridge --subnet=172.18.0.0/16 ethnet
docker network ls
配置以太坊網(wǎng)絡(luò)
運(yùn)行如下命令進(jìn)入一個(gè)容器:
docker run -it --rm --network ethnet --ip 172.18.0.50 -v /workspace:/workspace --entrypoint /bin/sh ethereum/client-go:v1.8.12
--network ethnet參數(shù)指定了該容器加入剛才創(chuàng)建的ethnet網(wǎng)絡(luò)
--ip 172.18.0.5指定了一個(gè)固定IP給該容器。
創(chuàng)建賬戶(hù)
首先,在容器內(nèi)的/workspace目錄創(chuàng)建如下目錄結(jié)構(gòu)和文件:
dapp\
dapp\miner\
dapp\data\
dapp\genesis.json
然后運(yùn)行如下命令創(chuàng)建賬戶(hù):
cd /workspace/dapp/miner
geth -datadir ./data account new
輸入兩次password,獲得地址。將地址記錄下來(lái),后面要用到。
重復(fù)如上步驟可以創(chuàng)建多個(gè)賬戶(hù)。
創(chuàng)建創(chuàng)世區(qū)塊
編輯剛才創(chuàng)建的文件dapp\data\genesis.json
{
"config": {
"chainId": 88,
"homesteadBlock": 0,
"eip155Block": 0,
"eip158Block": 0
},
"alloc" : {
"0x4c283287839fd441b8c8d18771321bc06a81edae": {"balance": "100000000000000000000"},
"0x8dadb9ec6a82413c389e677baa9e7b589fe92b68": {"balance": "1000000000000000000"},
"0x39c70ad710e07953dea150b1a5f1d0534bf0f135": {"balance": "1000000000000000000"},
"0xb9592b51d40d66bc583f8ed2755861372fb4afb0": {"balance": "1000000000000000000"}
},
"coinbase" : "0x0000000000000000000000000000000000000000",
"difficulty" : "0x400",
"extraData" : "",
"gasLimit" : "0x2fefd8",
"nonce" : "0x0000000000000000",
"mixhash" :
"0x0000000000000000000000000000000000000000000000000000000000000000",
"parentHash" :
"0x0000000000000000000000000000000000000000000000000000000000000000",
"timestamp" : "0x00"
}
- genesis.json是用來(lái)創(chuàng)建創(chuàng)世區(qū)塊的配置文件
- 加入同一私鏈的節(jié)點(diǎn)必須使用同一配置文件
- chainid是私鏈網(wǎng)絡(luò)的標(biāo)識(shí),可以是任意數(shù)字。
- 即使chainid相同,如果genesis.json配置不一樣,也將是兩個(gè)不兼容的網(wǎng)絡(luò)
- alloc下面列舉了4個(gè)賬戶(hù)地址,分別是上一步驟創(chuàng)建并記錄下來(lái)的地址。
- balance是創(chuàng)世區(qū)塊為每個(gè)賬戶(hù)分配的初始以太幣。這里看似分配了很多,其實(shí)單位是wei。1eth=10^18wei。也就是除了第一個(gè)賬戶(hù)給了100eth外,其它幾個(gè)賬戶(hù)分別只擁有1eth。這里給第一個(gè)賬戶(hù)多分點(diǎn),是因?yàn)槲覀冎笮枰盟鼇?lái)部署合約。
完成以太坊網(wǎng)絡(luò)配置
此時(shí)可以退出剛才的容器。由于我們運(yùn)行容器是加了--rm參數(shù),剛才的容器會(huì)被刪除,但宿主機(jī)的/workspace下的文件會(huì)被保存下來(lái)。
挖礦
上述步驟只是配置好了一個(gè)以太坊私有網(wǎng)絡(luò),并沒(méi)有真正創(chuàng)建網(wǎng)絡(luò)。我們知道,以太坊網(wǎng)絡(luò)是一個(gè)分布式的網(wǎng)絡(luò),有了礦工,才有的網(wǎng)絡(luò)。于是,我們首先得有一個(gè)礦工。
創(chuàng)建“主”礦工節(jié)點(diǎn)
我們接下來(lái)打算創(chuàng)建的礦工節(jié)點(diǎn),成為“主”礦工,因?yàn)樗枰獡碛腥缦绿匦裕?/p>
- 它是一個(gè)容器,并且是持久的容器
- 它會(huì)自動(dòng)讀取genesis.json文件,并初始化以太坊網(wǎng)絡(luò)
- 它能夠連接其它節(jié)點(diǎn)(容器)
- 它能夠接受各種rpc調(diào)用,并能夠部署合約
- 它已經(jīng)配置好挖礦賬戶(hù),可以一鍵挖礦
于是,我們按照這個(gè)要求,開(kāi)始一步步創(chuàng)建礦工節(jié)點(diǎn)。
創(chuàng)建entrypoint腳本
創(chuàng)建一個(gè)文件:/workspace/dapp/init.sh
文件內(nèi)容如下:
#!/bin/sh
geth -datadir ~/data/ init /workspace/traceability/genesis.json
if [ $# -lt 1 ]; then
exec "/bin/sh"
else
exec /bin/sh -c "$@"
fi
該腳本的功能是讓以太坊節(jié)點(diǎn)(容器)自動(dòng)初始化以太坊網(wǎng)絡(luò),并且接受一個(gè)自動(dòng)運(yùn)行腳本作為輸入。
創(chuàng)建自動(dòng)運(yùn)行腳本
創(chuàng)建一個(gè)文件:/workspace/dapp/mine.sh
內(nèi)容如下:
#!/bin/sh
cp -r /workspace/dapp/miner/data/keystore/* ~/data/keystore/
geth -datadir ~/data/ --networkid 88 --rpc --rpcaddr "172.18.0.50" --rpcapi admin,eth,miner,web3,personal,net,txpool --unlock "0x4c283287839fd441b8c8d18771321bc06a81edae" --etherbase "0x4c283287839fd441b8c8d18771321bc06a81edae" console
- 第一行命令是將剛才生成的賬戶(hù)私鑰文件拷貝到容器的home目錄下。因?yàn)?workspace是宿主目錄掛載的,并不是linux文件系統(tǒng),直接將datadir指定到該目錄會(huì)導(dǎo)致geth報(bào)錯(cuò)。
- 第二行命令是啟動(dòng)以太坊節(jié)點(diǎn)的命令。
- --networkid 88指定了networkid,這個(gè)必須與genesis.json內(nèi)設(shè)置保持一致
- --rpc --rpcaddr "172.18.0.50" --rpcapi .... 這些參數(shù)表示該節(jié)點(diǎn)接受rpc,并且指定了rpc的協(xié)議
- --unlock "0x..." 加入該參數(shù)會(huì)需要用戶(hù)輸入賬戶(hù)密碼。密碼校驗(yàn)后會(huì)解鎖該賬戶(hù)。賬戶(hù)解鎖后,該節(jié)點(diǎn)就能使用此賬戶(hù)的私鑰進(jìn)行簽名加密等動(dòng)作,用以進(jìn)行交易、發(fā)布合約等。
- --etherbase 參數(shù)指定了挖礦收益賬戶(hù)
創(chuàng)建容器
docker run -it --name=miner --network ethnet --ip 172.18.0.50 --hostname node -v /workspace:/workspace --entrypoint /workspace/dapp/init.sh ethereum/client-go:v1.8.12 /workspace/dapp/mine.sh
該命令會(huì)創(chuàng)建一個(gè)持久化的容器。容器的entrypoint和自動(dòng)運(yùn)行腳本指定為我們剛創(chuàng)建的那兩個(gè)腳本。
創(chuàng)建“從”礦工節(jié)點(diǎn)
只有一個(gè)節(jié)點(diǎn)的網(wǎng)絡(luò),怎么看都不像“分布式”網(wǎng)絡(luò)。所以我們需要?jiǎng)?chuàng)建更多的節(jié)點(diǎn)來(lái)形成一個(gè)“分布式網(wǎng)絡(luò)”。我們稱(chēng)這些節(jié)點(diǎn)叫做“從”礦工。
這類(lèi)礦工不需要交易,不需要發(fā)布合約,因此不需要unlock賬戶(hù),也不需要接受rpc。它們只知道埋頭挖礦。
創(chuàng)建自動(dòng)運(yùn)行腳本
“從”礦工節(jié)點(diǎn)和“主”礦工節(jié)點(diǎn)共享entrypoint,以保證它們創(chuàng)建出完全相同的網(wǎng)絡(luò)。
只有自動(dòng)運(yùn)行腳本不太一樣,/workspace/dapp/node.sh:
#!/bin/sh
cp -r /workspace/dapp/miner/data/keystore/* ~/data/keystore/
geth -datadir ~/data/ --networkid 88 console
創(chuàng)建容器
docker run -it --name=node1 --network ethnet --ip 172.18.0.51 --hostname node1 -v /workspace:/workspace --entrypoint /workspace/dapp/init.sh ethereum/client-go:v1.8.12 /workspace/dapp/node.sh
操作節(jié)點(diǎn)
以上創(chuàng)建出了多個(gè)以太坊節(jié)點(diǎn),運(yùn)行在同一網(wǎng)絡(luò)下。每個(gè)節(jié)點(diǎn)都可以執(zhí)行如下操作。供參考。
節(jié)點(diǎn)發(fā)現(xiàn)
查看節(jié)點(diǎn)信息
geth -datadir ~/data/ --networkid 88 console
>admin.nodeInfo.enode
配置靜態(tài)節(jié)點(diǎn)文件
~/data/geth/static-nodes.json
[
"enode://<node public key>@<node IP address>:<node port>",
...
]
查看連接上的節(jié)點(diǎn)
geth -datadir ~/data/ --networkid 88 console
>admin.peers
動(dòng)態(tài)添加節(jié)點(diǎn)
>admin.addPeer("enode://<node public key>@<node IP address>:<node port>")
挖礦
啟動(dòng)miner容器
>miner.start(1)
- 參數(shù)1指定了挖礦的線程數(shù)。
- 首次啟動(dòng)節(jié)點(diǎn)會(huì)消耗大約20~30分鐘產(chǎn)生DAG
- 某開(kāi)始挖礦后,其它節(jié)點(diǎn)將會(huì)收到新區(qū)塊并打印
部署合約
創(chuàng)建truffle鏡像
由于沒(méi)有找到好用的truffle鏡像,我自己創(chuàng)建了一個(gè)。Dockerfile內(nèi)容如下:
FROM alpine:3.8
MAINTAINER Cary Tan tx-cary@163.com
ENV PS1='[truffle@docker $PWD]\$ '
RUN echo "http://mirrors.tuna.tsinghua.edu.cn/alpine/v3.8/main" > /etc/apk/repositories \
&& echo "http://mirrors.tuna.tsinghua.edu.cn/alpine/v3.8/community" >> /etc/apk/repositories \
&& apk update \
&& apk add npm \
&& mkdir -p /workspace \
&& npm config set registry https://registry.npm.taobao.org \
&& npm install -g truffle
WORKDIR /workspace
CMD /bin/sh
該鏡像我已經(jīng)上傳到dockerhub了,您也可以直接下載使用:
docker pull txcary/truffle:180806
啟動(dòng)truffle容器
docker run -it --rm -v /workspace:/workspace --network ethnet txcary/truffle:180806
測(cè)試節(jié)點(diǎn)RPC
curl 172.18.0.50:8545 -X POST --data '{"id":1,"jsonrpc":"2.0","method":"eth_accounts", "params":[]}' -H "Content-Type: application/json"
curl 172.18.0.50:8545 -X POST --data '{"id":1,"jsonrpc":"2.0","method":"eth_getBalance", "params":["0x4c2832
87839fd441b8c8d18771321bc06a81edae","latest"]}' -H "Content-Type: application/json"
- 第一條命令獲取節(jié)點(diǎn)上的賬戶(hù)
- 第二條命令獲取賬戶(hù)余額
- 如果這兩條命令成功了,說(shuō)明以太坊私有網(wǎng)絡(luò)搭建成功,并且節(jié)點(diǎn)rpc調(diào)用成功
新建truffle工程
truffle init
修改truffle.js
module.exports = {
networks: {
development: {
host: "172.18.0.50",
port: 8545,
network_id: 88,
gas: 2900000,
gasPrice: 10000000000
}
}
};
- gas使用默認(rèn)值會(huì)導(dǎo)致超限錯(cuò)誤,研究半天不知道為什么。要是搞明白的可以給我留言,謝謝!
- gasPrice為默認(rèn)值
編寫(xiě)合約Yourname.sol
- 合約名稱(chēng)必須和文件名一致,不然會(huì)部署失敗
如果你還沒(méi)有自己的合約,可以上truffle官網(wǎng)下載一個(gè)做練習(xí)。
我也做了一個(gè)供應(yīng)鏈追蹤的合約放在github上:https://github.com/txcary/traceability。該合約通過(guò)remix調(diào)試并成功部署在我的私鏈上,供參考。
新建部署腳本
2_deploy_contracts.js
var Yourname = artifacts.require("./Yourname.sol");
module.exports = function(deployer) {
deployer.deploy(Yourname);
};
編譯合約
truffle compile
部署合約
truffle migrate --network development --verbose-rpc