搭建以太坊私链网络

利用puppeth搭建POA共识的以太坊私链网络

大量过时文章充斥于网络,本文基于官方go-tehereum 1.6.7版本整理而出,在geth1.6之后引入了一个puppeth工具,它就是用来初始一个私链创世块配置的。

准备工具环境

下载go-ethereum代码(go的开发环境准备,不在此文范围)

# 下载源码
go get github.com/ethereum/go-ethereum
cd $GOPATH/src/github.com/ethereum/go-ethereum

# 编译1.6.7版本的代码
git checkout -b v1.6.7 v1.6.7
make all

# 安装
sudo ln -s $PWD/build/bin/* /usr/local/bin/

# 检查是否安装OK
geth version
    Geth
    Version: 1.6.7-stable
    Git Commit: ab5646c532292b51e319f290afccf6a44f874372
    Architecture: amd64
    Protocol Versions: [63 62]
    Network Id: 1
    Go Version: go1.8.3
    Operating System: linux
    GOPATH=/home/xxp/go
    GOROOT=/home/xxp/Software/go

生成私链创世块的配置

  1. 创建账户
# 创建testnet目录
➜  mkdir testnet && cd testnet

# 创建3个普通账户,密码自定
➜  geth --datadir node0 account new
# 把密码记录到文件里,后面会频繁输入echo node0 > node0/password

➜  geth --datadir node1 account new
➜  echo node1 > node1/password
➜  geth --datadir node2 account new
➜  echo node2 > node2/password

如上打印的一串16进制的字符串就代表账户的userID(理解成网络中的IP地址),后面puppeth需要用到。

  1. 生成genesis文件

genesis文件定义了私链的第一个块生成,直接看操作吧(省略了些输出)

➜  testnet puppeth

Please specify a network name to administer (no spaces, please)
> testnet
Sweet, you can set this via --network=testnet next time!

INFO [08-21|23:04:14] Administering Ethereum network           name=testnet
WARN [08-21|23:04:14] No previous configurations found         path=/home/xxp/.puppeth/testnet

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
# 设置5秒出一个块
How many seconds should blocks take? (default = 15)
> 5
# 输入有签名权限的账户
Which accounts are allowed to seal? (mandatory at least one)
> 0x799a8f7796d1d20b8198a587caaf545cdde5de13
> 0x1458eac314d8fc922029095fae20483f55726017
> 0x3ca60eb49314d867ab75a3c7b3a5aa61c3d6ef71
> 0x
# 输入有预留余额的账户
Which accounts should be pre-funded? (advisable at least one)
> 0x799a8f7796d1d20b8198a587caaf545cdde5de13
> 0x1458eac314d8fc922029095fae20483f55726017
> 0x3ca60eb49314d867ab75a3c7b3a5aa61c3d6ef71
> 0x


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

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

What would you like to do? (default = stats)
 1. Show network stats
 2. Save existing genesis
 3. Track new remote server
 4. Deploy network components
> 2

Which file to save the genesis into? (default = testnet.json)
> genesis.json
INFO [08-21|23:05:36] Exported existing genesis block 

最后会在当前目录生成genesis.json文件。

启动私链网络

这里通过单机不同端口模拟多节点,

  • 默认geth一启动就会发出discovery,以发现其他节点,源码里内置了几个初始节点,可以通过--bootnode参数重置。如果真要不同节点互组网络的话,还需要主要时间同步,
  • 还可以通过--nodiscover参数,停掉自动发现,并利用admin.addPeer()或者static-node功能组成网络。
# 启动节点0, 关闭自动发现(防止不希望的节点进入)
geth --datadir node0 init genesis.json
geth --datadir node0 --port 30000 --nodiscover --unlock '0' --password ./node0/password console
# 不加console的话,可以通过geth attach ipc:node0/geth.ipc来访问

# 启动节点1,另起一个终端,通过不同端口模拟
geth --datadir node1 init genesis.json
geth --datadir node1 --port 30001 --nodiscover --unlock '0' --password ./node1/password  console

# 启动节点2,genesis.json里已经指定networkID,启动时无需指定了
geth --datadir node2 init genesis.json
geth --datadir node2 --port 30002 --nodiscover --unlock '0' --password ./node2/password  console

目前启动了三个节点,但都默认关闭了发现功能,需要手动添加peer节点。

在每个console输入admin.nodeInfo.enode, 把输出记录下来到static-nodes.json文件,我这里情况如下:

➜  testnet cat node0/static-nodes.json 
[
"enode://2ffb53ede7de8dabf8f12343a7b2aba6b09263a53d8db5b4669309c5913f72969ce469cf09299f13e9d6cba8a98e18ad43811439326d7152f21d2e03ddc6be17@[::]:30000?discport=0",
"enode://910d1bfcd763bb5157bc62f8b121eb21fb305d17e4e4437c0b094d3d6f2d72f1964b80eb8fa2cf6cd7d4cc2d44cfc1ed9b74275ea7fd42ab89b4d089023fb7d5@[::]:30001?discport=0",
"enode://acab97a2a287b740b5efc3af465ba7330b3d4948b05e26818822d1aee659ec1b8f54ee9501576dc08ea4021d7ede01431691a27310a7dcbda2437bcd3b9c451d@[::]:30002?discport=0"
]

把static-nodes.json文件放入各自(node0/,node1/, node2/)的admin.datadir目录下,并Ctrl+D终止掉console,并重新执行geth --datadir <dir> --prot <port> --nodiscover console,如果觉得这样麻烦的话,或者以后动态添加节点时候,可考虑在每个节点的console里输入admin.addPeer("nodeInfo_encode")来完成。

然后在console中可以如下验证,是否互相发现组成以太坊私链网络了

# 这样可看到其他节点的明细
> admin.peers
[{
    caps: ["eth/63"],
    id: "910d1bfcd763bb5157bc62f8b121eb21fb305d17e4e4437c0b094d3d6f2d72f1964b80eb8fa2cf6cd7d4cc2d44cfc1ed9b74275ea7fd42ab89b4d089023fb7d5",
    name: "Geth/v1.6.7-stable-ab5646c5/linux-amd64/go1.8.3",
    network: {
      localAddress: "[::1]:43928",
      remoteAddress: "[::1]:30001"
    },
    protocols: {
      eth: {
        difficulty: 1,
        head: "0xa43519868915a64d3798abf1867b7bc769d1239442c69ff1eca8e3dfcd13209b",
        version: 63
      }
    }
}, {
    caps: ["eth/63"],
    id: "acab97a2a287b740b5efc3af465ba7330b3d4948b05e26818822d1aee659ec1b8f54ee9501576dc08ea4021d7ede01431691a27310a7dcbda2437bcd3b9c451d",
    name: "Geth/v1.6.7-stable-ab5646c5/linux-amd64/go1.8.3",
    network: {
      localAddress: "[::1]:60310",
      remoteAddress: "[::1]:30002"
    },
    protocols: {
      eth: {
        difficulty: 1,
        head: "0xa43519868915a64d3798abf1867b7bc769d1239442c69ff1eca8e3dfcd13209b",
        version: 63
      }
    }
}]
> 
# 可看到此节点发现了另外两个节点
> net.peerCount
2
> 

目前整个testnet的目录结构如下

➜  tree
.
├── genesis.json
├── node0
│   ├── geth
│   │   ├── chaindata
│   │   │   ├── 000002.ldb
|   |   |   ├── ...
│   │   ├── lightchaindata
│   │   │   ├── 000001.log
│   │   │   ├── ...
│   │   ├── LOCK
│   │   └── nodekey
│   ├── geth.ipc
│   ├── history
│   ├── keystore
│   │   └── UTC--2017-08-21T15-03-25.499242705Z--799a8f7796d1d20b8198a587caaf545cdde5de13
│   └── static-nodes.json
├── node1
│   ├── geth
│   │   ├── chaindata
│   │   │   ├── 000002.ldb
│   │   │   ├── ...
│   │   │   └── MANIFEST-000007
│   │   ├── lightchaindata
│   │   │   ├── ...
│   │   │   └── MANIFEST-000000
│   │   ├── LOCK
│   │   └── nodekey
│   ├── geth.ipc
│   ├── history
│   ├── keystore
│   │   └── UTC--2017-08-21T15-03-35.020270645Z--1458eac314d8fc922029095fae20483f55726017
│   └── static-nodes.json
└── node2
    ├── geth
    │   ├── chaindata
    │   │   ├── 000002.ldb
    │   │   ├── ...
    │   │   └── MANIFEST-000007
    │   ├── lightchaindata
    │   │   ├── ...
    │   │   └── MANIFEST-000000
    │   ├── LOCK
    │   └── nodekey
    ├── geth.ipc
    ├── history
    ├── keystore
    │   └── UTC--2017-08-21T15-03-46.899273318Z--3ca60eb49314d867ab75a3c7b3a5aa61c3d6ef71
    └── static-nodes.json

15 directories, 56 files

开始挖矿

在每个节点的console输入如下,启动挖矿(共识记账):

# 这一步可以不需要,因为我们geth启动的时候,已经传入unlock参数了,因为console里执行unlock是有过期时间机制的,私网直接来。。。。
> personal.unlockAccount(eth.coinbase)
Unlock account 0x799a8f7796d1d20b8198a587caaf545cdde5de13
Passphrase: 
true
> 
# 这一个很重要,为后面的eth设置了默认账户
> eth.defaultAccount = eth.coinbase
"0x799a8f7796d1d20b8198a587caaf545cdde5de13"
> 
> miner.start()

通过钱包交易

通过mist钱包界面,来查看基本信息和进行交易,不过之前需要在console开启RPC, node0的console里如下

> admin.startRPC("127.0.0.1", 8545, "*", "eth,net,web3,admin,personal")

启动的mist钱包

mist --node geth --node-datadir ./node0 --rpc http://localhost:8545
# 不启动rpc的话,也可以如下直接通过ipc通信
# mist  --rpc ./node0/geth.ipc  --node-networkid 378 --node-datadir ./node0

mist界面

需要去node1上通过eth.accounts[0]拿到账号信息,填入红框,点击发送交易,输入密码即可

另外也可以通过console命令行来完成上面操作:

> 
> var sender = eth.accounts[0]
undefined
> var receiver = "0x1458eac314d8fc922029095fae20483f55726017"
undefined
> var amount = web3.toWei(10, "ether")
undefined
> personal.unlockAccount(eth.accounts[0])
Unlock account 0x799a8f7796d1d20b8198a587caaf545cdde5de13
Passphrase: 
true
> eth.sendTransaction({from:sender, to:receiver, value: amount})
"0x97ca1f5fa27df083e14b2ffb82c2a60744aeae0f1a7b5e735ca4d0c05c16f7b6"
> 

写个合约

通过mist界面点击的“开发”->"Open Remix IDE", 会自动打开IDE工具,并且默认集成了一个投票的合约,合约内容不表

“create”按钮后面输入 10 -> 点击, 这时候需要输入密码解锁,因为需要支付一定的gas费用。

  • 这个合约就是个投票程序
  • 第一个部署的默认是主席身份,拥有指定别人投票的权利,因为我这里的mist是连接的node0,所以
  • 合约初始化(也就是界面点击“create”时),需要输入"投案"个数

在每个节点的的console里,通过eth.contract(ABI).at(Address);拿到合约对象,

  • 其中node0的节点需要用giveRightToVote给其他账户授权投票权限
  • ABI的信息,就是remix里Interface框对应的信息
  • 合约地址通过Copy address得到

> var b = eth.contract([{"constant":false,"inputs":[{"name":"to","type":"address"}],"name":"delegate","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"winningProposal","outputs":[{"name":"_winningProposal","type":"uint8"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"voter","type":"address"}],"name":"giveRightToVote","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"proposal","type":"uint8"}],"name":"vote","outputs":[],"payable":false,"type":"function"},{"inputs":[{"name":"_numProposals","type":"uint8"}],"payable":false,"type":"constructor"}]).at('0xc825238a3348f0a679924796fcf1b1b64a8c1706')
undefined

> b.vote(9)
"0x1646e6547606a8ad0e183f1c9145eff08bbdfd860961d6c7d7367f5b70779cbd"
> 
> b.giveRightToVote('0x1458eac314d8fc922029095fae20483f55726017')
"0x2759928ad03a2ed5bc4b9c54531eb83e25c4a468e71682f67b160ad3328c8173"
> 
> b.giveRightToVote('0x3ca60eb49314d867ab75a3c7b3a5aa61c3d6ef71')
"0x46f756e613499f836e392011c8f6d7c23d378fd5a656bae775ecda8bf286c5b6"
> b.winningProposal()
9

其中值得注意的是b.vote() 其中的number不能超过create后传入初始参数10, 其实通过Web3 deploy框的信息也可以直接部署合约。