最近在學習區(qū)塊鏈的技術(shù),初步打算從go-ethereum入手,學習一下以太坊的設(shè)計思想順便把GoLang入個門(簡單、直接明了,是目前這個階段需要追求的東西)。
手上有一個臺阿里云的服務(wù)器(單核1G),性能一般,平時就用來掛著自己的個站,工作不飽和,打算在上面搭一條以太坊的私鏈,方便學習。
環(huán)境
服務(wù)器:
- 單核CPU,1G內(nèi)存,40G磁盤
- OS:CentOS
- OS內(nèi)核:3.10.0-693.11.1.el7.x86_64
- 軟件包管理工具:yum
go-ethereum源碼下載
以太坊的節(jié)點有兩個版本,基于c++編寫的cpp-ethereum和基于go編寫的go-ethereum,這里選擇go語言版本,主要還是希望能順便熟悉一下這門語言
選擇一個最新的release版本1.8,下載到服務(wù)器,位置隨意,這里我們選擇放在/usr/src(存放源碼)下。
cd /usr/src/
wget https://github.com/ethereum/go-ethereum/archive/release/1.8.zip

解壓zip包得到我們需要的源碼文件
unzip 1.8.zip

安裝go語言環(huán)境
yum install golang

編譯go-ethereum源碼
cd go-ethereum-release-1.8/
make geth

到這里我們就得到了需要的以太坊節(jié)點程序/go-ethereum-release-1.8/build/bin/geth
可以把編譯好的程序拷貝到/usr/bin目錄下,方便運行
創(chuàng)建初始區(qū)塊
以太坊私鏈的初始區(qū)塊需要手動創(chuàng)建起來,否則整個區(qū)塊鏈沒法持續(xù)運行,我們從官網(wǎng)得到一段初始配置
{
"config": {
"chainId": 0,
"homesteadBlock": 0,
"eip155Block": 0,
"eip158Block": 0
},
"alloc" : {},
"coinbase" : "0x0000000000000000000000000000000000000000",
"difficulty" : "0x20000",
"extraData" : "",
"gasLimit" : "0x2fefd8",
"nonce" : "0x0000000000000042",
"mixhash" : "0x0000000000000000000000000000000000000000000000000000000000000000",
"parentHash" : "0x0000000000000000000000000000000000000000000000000000000000000000",
"timestamp" : "0x00"
}
創(chuàng)建一個文件,并復(fù)制初始配置進去
touch init.json
接下來就可以完成初始區(qū)塊的配置了
geth --datadir chaindata init init.json
--datadir 參數(shù)指定區(qū)塊鏈的數(shù)據(jù)存儲位置,可以根據(jù)需求自己指定
啟動區(qū)塊鏈并開放RPC接口
geth --rpc --rpcaddr "*.*.*.*" --rpccorsdomain "*" --datadir "chaindata" --rpcport "8545" --rpcapi "db,eth,net,web3" --networkid 31415926 console 2>>log.txt
--rpc 啟動RPC協(xié)議
--rpcaddr 指定服務(wù)器ip地址
--rpccorsdomain 設(shè)置允許訪問的域名
--datadir 區(qū)塊鏈數(shù)據(jù)存儲位置,要與初始化時保持一致
--rpcport PRC協(xié)議接口
--rpcapi RPC支持的API
--networkid 區(qū)塊鏈網(wǎng)絡(luò)ID,用于發(fā)現(xiàn)節(jié)點
最后把日志重定向到log.txt文件中,方便我們查看
當然還需要去阿里云的服務(wù)器配置中把TCP的8545端口打開

到這里節(jié)點就已經(jīng)啟動了
創(chuàng)建賬戶并啟動挖礦
創(chuàng)建賬戶,指定密碼
personal.newAccount('pwd')
綁定賬戶到挖礦程序
miner.setEtherbase(eth.accounts[0])
開啟挖礦,指定線程數(shù)
miner.start(2)
停止挖礦
miner.stop()
如果挖礦啟動失敗,可以檢查一下是否綁定過賬戶miner.setEtherbase(eth.accounts[0])
這里miner.start(2)可以根據(jù)機器自身的性能指定需要啟動的線程數(shù)。但是我在啟動miner之后并沒有開始挖礦,也沒有報錯,仔細閱讀了以太坊的相關(guān)資料之后發(fā)現(xiàn)正常情況下以太坊節(jié)點是通過POW(proof-of-work)的方式產(chǎn)生新的區(qū)塊,如果機器性能比較低可能并不會產(chǎn)生新的區(qū)塊,或者生成新區(qū)塊的速度會非常慢。
如果單純是為了測試開發(fā)使用,可以通過--dev參數(shù)初始化一條測試私鏈。--dev參數(shù)會創(chuàng)建一個使用POA(proof-of-authority)的共識網(wǎng)絡(luò),默認預(yù)分配一個開發(fā)者賬戶并且會自動開啟挖礦。
可以通過下面的方式直接創(chuàng)建:
geth --rpc --rpcaddr "*.*.*.*" --rpccorsdomain "*" --datadir "chaindata" --rpcport "8545" --rpcapi "db,eth,net,web3" --networkid 31415926 --dev console 2>>log.txt
用pm2監(jiān)控geth
為了保證終端回話關(guān)閉之后geth還能正常運行,并能處理RPC請求必須要以守護進程(daemon)的方式啟動,這里有幾種方式:
- nohup 命令
- Systemd 工具
- pm2 工具
當然還有其他的方式,這三種是我比較常用的,因為機器上有個node的服務(wù)正在用pm2管理,這里也正好借用pm2工具管理一下geth
首先創(chuàng)建一個pm2啟動配置文件:
touch start.json
配置參數(shù)
{
"name": "geth",
"script": "geth",
"args": "--rpc --rpcaddr '*.*.*.*' --rpccorsdomain '*' --datadir 'chaindata' --rpcport '8545' --rpcapi 'db,eth,net,web3' --networkid 31415926 --dev",
"log_date_format": "YYYY-MM-DD HH:mm Z",
"merge_logs": false,
"watch": false,
"max_restarts": 10,
"exec_interpreter": "none",
"exec_mode": "fork_mode"
}
pm2 start start.json

geth以daemon的方式啟動并通過pm2進行監(jiān)控,接下來就可以通過RPC的方式通信了
RPC通信
以太坊RPC的接口列表可以參考:https://ethereum.gitbooks.io/frontier-guide/content/rpc.html
這里遇到一個巨大的坑,感謝安全工程師何處不可憐系統(tǒng)化的方法的幫助,讓我能一步步定位到問題。
我們以eth_accounts試一下:
因為服務(wù)器IP地址綁定了域名,然后調(diào)用方式可以這樣寫:
curl -X POST --data '{"jsonrpc":"2.0","method":"eth_accounts","params":[],"id":1}' http://domain:8545
然后就不正常了,直接報錯
invalid host specified
感覺應(yīng)該是域名解析的問題,但是試了一下其他端口的服務(wù)一切正常,去DNS解析服務(wù)器看過也沒問題。然后更換服務(wù)端口依舊報錯。似乎問題不在DNS解析和端口上
前端抓包顯示403,
后端tcptump監(jiān)聽結(jié)果
14:54:50.651476 IP *.*.*.*.55747 > dawn.8545: Flags [P.], seq 2542259482:2542260075, ack 1963893059, win 4100, options [nop,nop,TS val 1412772142 ecr 4250673300], length 593
14:54:50.652009 IP dawn.8545 > *.*.*.*.55747: Flags [P.], seq 1:181, ack 593, win 255, options [nop,nop,TS val 4250708302 ecr 1412772142], length 180
14:54:50.657020 IP *.*.*.*.55747 > dawn.8545: Flags [.], ack 181, win 4094, options [nop,nop,TS val 1412772147 ecr 4250708302], length 0
14:54:50.685458 IP *.*.*.*.55747 > dawn.8545: Flags [P.], seq 593:1161, ack 181, win 4096, options [nop,nop,TS val 1412772176 ecr 4250708302], length 568
14:54:50.685546 IP dawn.8545 > *.*.*.*.55747: Flags [P.], seq 181:361, ack 1161, win 264, options [nop,nop,TS val 4250708336 ecr 1412772176], length 180
14:54:50.689829 IP *.*.*.*.55747 > dawn.8545: Flags [.], ack 361, win 4090, options [nop,nop,TS val 1412772179 ecr 4250708336], length
有來有往,似乎又沒啥問題。
修改請求地址,換ip直接訪問,200,居然通了??偨Y(jié)一下嘗試的結(jié)果:
- DNS解析正常
- 端口正常
- IP訪問正常(服務(wù)正常)
- 域名訪問異常
在服務(wù)正常DNS正常的情況想通過域名請求不到,問題很可能是服務(wù)本身的限制。參考wiki,---rpccorsdomain參數(shù)配置為“*”沒有問題,允許所有域名訪問。然后再找似乎也沒有可用的參數(shù)了。這個時候只能讀一下源碼,看看是不是能找到思路。之后就是找到go-ethereumRPC模塊的代碼,一點點讀。最后發(fā)現(xiàn)了這么一段代碼
// ServeHTTP serves JSON-RPC requests over HTTP, implements http.Handler
func (h *virtualHostHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// if r.Host is not set, we can continue serving since a browser would set the Host header
if r.Host == "" {
h.next.ServeHTTP(w, r)
return
}
host, _, err := net.SplitHostPort(r.Host)
if err != nil {
// Either invalid (too many colons) or no port specified
host = r.Host
}
if ipAddr := net.ParseIP(host); ipAddr != nil {
// It's an IP address, we can serve that
h.next.ServeHTTP(w, r)
return
}
// Not an ip address, but a hostname. Need to validate
if _, exist := h.vhosts["*"]; exist {
h.next.ServeHTTP(w, r)
return
}
if _, exist := h.vhosts[host]; exist {
h.next.ServeHTTP(w, r)
return
}
http.Error(w, "invalid host specified", http.StatusForbidden)
}
// DefaultConfig contains reasonable default settings.
var DefaultConfig = Config{
DataDir: DefaultDataDir(),
HTTPPort: DefaultHTTPPort,
HTTPModules: []string{"net", "web3"},
HTTPVirtualHosts: []string{"localhost"},
WSPort: DefaultWSPort,
WSModules: []string{"net", "web3"},
P2P: p2p.Config{
ListenAddr: ":30303",
MaxPeers: 25,
NAT: nat.Any(),
},
}
到這里應(yīng)該差不多能猜到,有設(shè)置vhosts的地方,然后用了一個比較笨的辦法
[dawn@dawn ~]$ geth --help|grep vhosts
--rpcvhosts value Comma separated list of virtual hostnames from which to accept requests (server enforced). Accepts '*' wildcard. (default: "localhost")
[dawn@dawn ~]$
果然是有--rpcvhosts參數(shù)可以設(shè)置,修改配置
"args": "--rpc --rpcaddr '*.*.*.*' --rpccorsdomain '*' --datadir 'chaindata' --rpcport '8545' --rpcapi 'db,eth,net,web3' --networkid 31415926 --dev --rpcvhosts '*'"
重啟服務(wù)
pm2 restart start.json
再次通過域名訪問,200,果然通了,但是返回:
invalid content type, only application/json is supported
這就好處理了,增加content-type設(shè)置
請求
curl -H "Content-Type: application/json" -X POST --data '{"jsonrpc":"2.0","method":"eth_accounts","params":[],"id":1}' http://domain:8545
返回數(shù)據(jù)
{"jsonrpc":"2.0","id":1,"result":["0x8018e73d7efc27297ea313e8bd250a02c6ca9f14","0xe3207f6fb2816fead3ccba99ebd2ea9f3ff22231"]}
到這里一切就都調(diào)通了,后續(xù)的鏈上操作就可以通過RPC服務(wù)直接操作了。