环境:Kali Linux + Docker + Geth v1.10.26
目标:用 3 个节点搭一条自己的以太坊私有链,完成创世、互联、挖矿、账户与转账。
1. 实验要做成什么样
以太坊主网是全球共用的一条链。本实验不连主网,而是在本机用 Docker 起 3 个 Geth 进程,让它们共用同一份创世块,互相加为对等节点,自己挖矿出块。
完成后应看到:
- 三个容器
peer1/peer2/peer3同时运行 - 三个节点的创世块 hash 完全相同
admin.peers不为空(每个节点连着另外两个)miner.start(1)之后eth.blockNumber持续增加- 能创建账户、查余额、发转账
一张对应表:
| 概念 | 本实验里的实物 |
|---|---|
| 节点软件 | Geth(镜像 ethereum/client-go:v1.10.26) |
| 一台节点 | 一个 Docker 容器,例如 myethereumnet-peer1-1 |
| 账本数据 | /root/MyEthereumNet/peer1/ 等三个目录 |
| 链的身份证 | genesis.json 里的 chainId: 15 |
| 节点之间打招呼 | admin.addPeer(...) 或 static-nodes.json |
| 出块 | miner.start(1) |
2. 以太坊原理(只讲本实验会碰到的)
2.1 账本、区块、链
以太坊可以看成一台全网共用的记账计算机:
- 每个人有账户(地址 + 私钥)
- 转账、部署合约都是一笔交易
- 交易被打包装进区块
- 区块用
parentHash指向上一块,串成链 - 每个全节点各自存一份账本,靠协议保证看到的是同一条链
创世块是第 0 块,没有父块。三个节点必须用同一份 genesis.json 初始化,得到同一个创世 hash,否则它们会认为对方是另一条链,连上也同步不了。
一块里实验中常看的字段:
| 字段 | 含义 |
|---|---|
number |
高度,创世块为 0 |
hash |
本块哈希 |
parentHash |
上一块哈希,构成链式结构 |
miner / coinbase |
这块是谁挖出来的,奖励打给谁 |
difficulty |
挖矿有多难 |
gasLimit |
本块最多能消耗多少 gas |
transactions |
本块打包的交易列表 |
nonce / mixHash |
Ethash 挖矿的工作量证明材料 |
2.2 账户、密钥、单位
一个账户是一对密钥:
- 私钥:能签名、能花钱。丢了或泄露都不可挽回。
- 地址 :由公钥算出的 20 字节,形如
0xf52cc7096054a62d8da117b40c60cf2c68461b64。
Geth 把私钥加密后放在 keystore/ 目录。personal.newAccount("123456") 里的 123456 是加密 keystore 的口令,不是私钥本身。忘记口令,文件还在也解不开。
余额单位:
- 链上最小单位是 wei
1 ETH = 10^18 wei- 控制台
eth.getBalance(...)默认返回 wei,所以会看到一长串 0 - 换成 ETH:
web3.fromWei(eth.getBalance(eth.accounts[0]), "ether")
本实验的 Ethash 旧规则:每挖出 1 块奖励 5 ETH。所以大约:
余额 ≈ 块高 × 5
例如高度 150 时,挖矿账户约 750 ETH。
2.3 交易
转账本质是:用私钥签一条消息「从 A 转 N wei 给 B」,广播给节点,矿工打包进块。没进块之前只是内存池里的 pending。
转账前必须 unlockAccount,因为签名需要私钥,而私钥躺在加密的 keystore 里。
只有链上有人在挖矿(或出块),交易才会被打包。miner.stop() 之后转账会一直 pending。
2.4 挖矿(Ethash)与为什么不用最新 Geth
挖矿就是不断改区块里的 nonce 做哈希,直到结果小于当前难度目标。谁先算出谁出块。
本实验链:
- 起始难度
0x40000(十进制 262144),很低,笔记本 CPU 就能挖 - 共识是 Ethash 工作量证明
- 控制台用
miner.start(1)启动 1 个挖矿线程
公网以太坊 2022 年已改为权益证明(PoS),没有这种本地 CPU 挖矿。Geth 1.14 之后去掉了 Ethash。若 docker pull ethereum/client-go 拉到最新镜像:
- 这份旧
genesis.json初始化会失败或无法挖矿 miner.start失效
因此实装把镜像钉在 ethereum/client-go:v1.10.26 (2022 年稳定版),并打成 latest 标签,compose 里仍写 image: ethereum/client-go,和课件一致。
2.5 chainId、networkId、enode
-
chainId:写进创世块。交易签名会带上它,防止一条链上的签名拿到另一条链重放(EIP-155)。
-
networkId :P2P 分组号。两边都是 15,才是封闭私链。启动参数
--networkid 15。 -
enode:节点在 P2P 层的身份证,格式:
enode://<公钥>@
:<端口>
admin.addPeer(对方enode) 等于把对方加进好友列表。
admin.peers 为空 = 孤岛;长度为 2 = 三个节点已经两两连上。
--nodiscover 关闭自动发现,避免误连公网。私链里要靠手动 addPeer 或 static-nodes.json。
重启容器后,账本还在,内存里的好友列表会丢 。所以要写 static-nodes.json,或准备重连脚本。
3. 工具分别干什么
3.1 Docker
把「Geth + 它所在的 Linux 环境」打成镜像,再变成容器跑。不必在 Kali 宿主机里直接装 geth。
三个容器互相隔离,但走同一条 Docker 网桥 myethereumnet_default,可以用 IP 或服务名互访。compose 服务名 peer1 / peer2 / peer3 就是 DNS 名,容器里 getent hosts peer2 能解析。
常用:
bash
docker --version
docker ps
docker images
docker logs myethereumnet-peer1-1
docker exec -it myethereumnet-peer1-1 /bin/sh
docker pull ethereum/client-go:v1.10.26
Kali 安装 Docker:
bash
curl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun
# 或
curl -sSL https://get.daocloud.io/docker | sh
本机若已有 Docker,跳过。
3.2 Docker Compose
一份 YAML 同时定义 3 个容器的镜像、命令、端口、数据卷。
docker-compose,新版 Docker 也可能是 docker compose(中间有空格)。Kali Rolling 可用:
bash
sudo apt-get update
sudo apt-get install -y docker-compose
docker-compose --version
工作目录内:
bash
cd /root/MyEthereumNet
docker-compose up -d # 后台启动
docker-compose ps # 看这组服务
docker-compose logs -f peer1 # 跟踪 peer1 日志
docker-compose restart # 重启(P2P 连接可能丢)
docker-compose down # 停并删容器,数据目录保留
command 参数含义:
| 参数 | 含义 |
|---|---|
--ipcpath /root/.ethereum/geth.ipc |
本机 JS 控制台走的 Unix 套接字路径 |
--port 23134 |
P2P 监听端口 |
--nodiscover |
不要去公网找节点 |
--networkid 15 |
私有网络号 |
--maxpeers 25 |
最多连多少个对等节点 |
数据卷 ./peer1/:/root/.ethereum/ 把宿主机目录挂进容器。账本、keystore、IPC 文件都落在磁盘上,删容器不会丢链。
3.3 Geth
Go 语言写的以太坊客户端。一个进程 = 一个节点。它同时:
- 把链存在 LevelDB(
geth/chaindata) - 跟别的节点同步区块
- 执行交易
- 可选:挖矿
- 提供 IPC / HTTP / WS 接口
本实验用 IPC (geth.ipc)进 JavaScript 控制台,没有对外开 8545 JSON-RPC。浏览器、MetaMask 暂时连不上。
控制台是 JS ,不是 bash。正确:eth.blockNumber。错误:eth blockNumber。
4. 目录与文件长什么样
建议工作目录:/root/MyEthereumNet(root 登录 Kali)或自己的家目录。结构:
MyEthereumNet/
├── docker-compose.yml
├── genesis.json
├── reconnect-peers.sh # 可选:重启后重连
├── peer1/
│ ├── genesis.json
│ ├── geth.ipc # 节点起来后出现
│ ├── keystore/ # 账户文件
│ └── geth/
│ ├── chaindata/ # 账本
│ ├── nodekey # 本节点 P2P 私钥,决定 enode
│ └── static-nodes.json # 可选:开机自动加同伴
├── peer2/
│ └── ...
└── peer3/
└── ...
5. 完整部署步骤
5.1 确认 Docker
bash
docker --version
sudo apt-get install -y docker-compose
docker-compose --version
5.2 建目录
bash
mkdir -p /root/MyEthereumNet/{peer1,peer2,peer3}
cd /root/MyEthereumNet
5.3 写 docker-compose.yml
yaml
version: '3'
services:
peer1:
image: ethereum/client-go
command: --ipcpath "/root/.ethereum/geth.ipc" --port 23134 --nodiscover --networkid 15 --maxpeers 25
ports:
- "17450:4535"
- "17460:4536"
- "13303:23134"
- "13303:23134/udp"
- "13304:23135/udp"
volumes:
- /etc/localtime:/etc/localtime
- ./peer1/:/root/.ethereum/
environment:
- TZ=Asia/Beijing
peer2:
image: ethereum/client-go
command: --ipcpath "/root/.ethereum/geth.ipc" --port 23134 --nodiscover --networkid 15 --maxpeers 25
ports:
- "19450:4535"
- "19460:4536"
- "14303:23134"
- "14303:23134/udp"
- "14304:23135/udp"
volumes:
- /etc/localtime:/etc/localtime
- ./peer2/:/root/.ethereum/
environment:
- TZ=Asia/Beijing
peer3:
image: ethereum/client-go
command: --ipcpath "/root/.ethereum/geth.ipc" --port 23134 --nodiscover --networkid 15 --maxpeers 25
ports:
- "29450:4535"
- "29460:4536"
- "24303:23134"
- "24303:23134/udp"
- "24304:23135/udp"
volumes:
- /etc/localtime:/etc/localtime
- ./peer3/:/root/.ethereum/
environment:
- TZ=Asia/Beijing
新版 compose 会提示 version 字段过时,不影响运行。
5.4 写 genesis.json
json
{
"config": {
"chainId": 15,
"homesteadBlock": 0,
"eip150Block": 0,
"eip155Block": 0,
"eip158Block": 0
},
"coinbase" : "0x0000000000000000000000000000000000000000",
"difficulty" : "0x40000",
"extraData" : "",
"gasLimit" : "0xffffffff",
"nonce" : "0x0000000000000042",
"mixhash" : "0x0000000000000000000000000000000000000000000000000000000000000000",
"parentHash" : "0x0000000000000000000000000000000000000000000000000000000000000000",
"timestamp" : "0x00",
"alloc": { }
}
字段说明:
| 字段 | 作用 |
|---|---|
chainId |
链 ID,实装用 15,避免和主网的 1 冲突 |
homesteadBlock 等 |
硬分叉从第 0 块就生效,私链最简单 |
difficulty |
起始挖矿难度,0x40000 很低 |
gasLimit |
单块 gas 上限拉满,方便实验 |
alloc |
预分配余额。空对象表示一开始谁都没钱,靠挖矿挣 |
nonce / mixhash / parentHash |
创世块工作量证明相关;创世块 parent 为全 0 |
复制到三个节点目录,容器内才能读到:
bash
cp genesis.json peer1
cp genesis.json peer2
cp genesis.json peer3
5.5 拉镜像
bash
docker pull ethereum/client-go:v1.10.26
docker tag ethereum/client-go:v1.10.26 ethereum/client-go:latest
docker images | grep client-go
国内 Docker Hub 若超时,可改镜像站后再 tag:
bash
docker pull docker.m.daocloud.io/ethereum/client-go:v1.10.26
docker tag docker.m.daocloud.io/ethereum/client-go:v1.10.26 ethereum/client-go:v1.10.26
docker tag ethereum/client-go:v1.10.26 ethereum/client-go:latest
成功时应看到约 50MB 的 ethereum/client-go:v1.10.26。
5.6 第一次启动(可先确认容器能起来)
bash
cd /root/MyEthereumNet
docker-compose up -d
docker-compose ps
docker ps
三个容器状态为 Up 即可。此时它们还没有按私有创世块初始化(或带着默认空库),下一步专门 init。
5.7 初始化创世块
Geth 正在运行时占用 datadir 锁,不要 在运行中的容器里 rm -rf geth 再 geth init。正确做法:先停,再 init,再启动。
bash
cd /root/MyEthereumNet
docker-compose down
rm -rf peer1/geth peer2/geth peer3/geth
docker run --rm -v /root/MyEthereumNet/peer1:/root/.ethereum ethereum/client-go init /root/.ethereum/genesis.json
docker run --rm -v /root/MyEthereumNet/peer2:/root/.ethereum ethereum/client-go init /root/.ethereum/genesis.json
docker run --rm -v /root/MyEthereumNet/peer3:/root/.ethereum ethereum/client-go init /root/.ethereum/genesis.json
每条成功日志里应出现:
Successfully wrote genesis state hash=a0e580..a5e82e
三个节点的 hash 必须相同。 本次实装为:
0xa0e580c6769ac3dd80894b2a256164a76b796839d2eb7f799ef6b9850ea5e82e
5.8 按创世块重新拉起集群
bash
docker-compose up -d
sleep 3
docker-compose ps
ls -l peer1/geth.ipc peer2/geth.ipc peer3/geth.ipc
IPC 文件出现,说明控制台可以 attach。日志里应有:
IPC endpoint opened url=/root/.ethereum/geth.ipc
Started P2P networking
至此部署完成,下一步进控制台、互联、挖矿。
6. 进入 Geth 控制台
Geth 1.10 的 --exec 是全局参数,必须写在 attach 前面。
6.1 交互式(推荐截图、练手)
bash
docker exec -it myethereumnet-peer1-1 geth attach /root/.ethereum/geth.ipc
提示符变成 >。退出:exit。
6.2 一次性查询(适合脚本)
bash
docker exec myethereumnet-peer1-1 geth --exec 'eth.blockNumber' attach /root/.ethereum/geth.ipc
6.3 宿主机 attach
数据卷已经把 geth.ipc 映射到 ./peer1/geth.ipc。宿主机若安装了同版本 geth:
bash
geth attach ./peer1/geth.ipc
Kali 没装 geth 时,用 6.1 的 docker exec 即可,不必再装一份。
7. 节点互联
7.1 看网络 IP
bash
docker network ls
docker network inspect myethereumnet_default
本次实装示例(重启后 IP 可能变):
| 容器 | IPv4 |
|---|---|
| myethereumnet-peer1-1 | 172.18.0.4 |
| myethereumnet-peer2-1 | 172.18.0.2 |
| myethereumnet-peer3-1 | 172.18.0.3 |
7.2 取 enode
bash
docker exec myethereumnet-peer1-1 geth --exec 'admin.nodeInfo.enode' attach /root/.ethereum/geth.ipc
docker exec myethereumnet-peer2-1 geth --exec 'admin.nodeInfo.enode' attach /root/.ethereum/geth.ipc
docker exec myethereumnet-peer3-1 geth --exec 'admin.nodeInfo.enode' attach /root/.ethereum/geth.ipc
输出里的 @127.0.0.1 是节点自己眼中的地址,加同伴时必须改成对方容器 IP 。--nodiscover 时末尾常为 ?discport=0,addPeer 时可去掉。
示例(公钥以实际输出为准):
enode://<peer2公钥>@172.18.0.2:23134
enode://<peer3公钥>@172.18.0.3:23134
7.3 addPeer
先看应为空:
js
admin.peers
若这里冒出大量公网节点,说明连到了公共网络,前面某步(chainId / nodiscover)有问题。
在 peer1 控制台把 peer2、peer3 加上;再在 peer2 把 peer3 加上,形成全连接:
js
admin.addPeer("enode://<peer2公钥>@172.18.0.2:23134")
admin.addPeer("enode://<peer3公钥>@172.18.0.3:23134")
返回 true 只表示命令被接受。等一两秒再查:
js
admin.peers
net.peerCount
每个节点 peerCount 应为 2。连接是单向发起、双向建立的,不必在每个节点上把另外两个都加一遍;全连接更稳。
宿主机一条命令写法:
bash
docker exec myethereumnet-peer1-1 geth --exec 'admin.addPeer("enode://...@172.18.0.2:23134")' attach /root/.ethereum/geth.ipc
7.4 让重启后自动重连
把对方 enode 写成 static-nodes.json,放进各节点的 geth/ 目录。Docker DNS 可用服务名,IP 变了也能解析:
peer1/geth/static-nodes.json 示例:
json
[
"enode://<peer2公钥>@peer2:23134",
"enode://<peer3公钥>@peer3:23134"
]
公钥来自该节点的 geth/nodekey,只要不删 peerX/geth/nodekey,enode 前半段不变。此文件在下次启动时生效。
也可在目录下放 reconnect-peers.sh:读当前 IP、改写 enode、再 addPeer。重启后若 admin.peers 仍为空就执行它。
8. 账户、挖矿与转账
以下均在 peer1 的 > 控制台中执行。
8.1 创建账户并指定 coinbase
js
personal.newAccount("123456")
eth.accounts
miner.setEtherbase(eth.accounts[0])
eth.coinbase
口令仅用于本实验。本次实装挖矿地址:
0xf52cc7096054a62d8da117b40c60cf2c68461b64
8.2 挖矿
js
miner.start(1)
eth.mining
eth.blockNumber
web3.fromWei(eth.getBalance(eth.coinbase), "ether")
难度 0x40000 时,大约十几秒出一块。三个节点高度应一起涨,说明同步正常:
bash
docker exec myethereumnet-peer1-1 geth --exec 'eth.blockNumber' attach /root/.ethereum/geth.ipc
docker exec myethereumnet-peer2-1 geth --exec 'eth.blockNumber' attach /root/.ethereum/geth.ipc
docker exec myethereumnet-peer3-1 geth --exec 'eth.blockNumber' attach /root/.ethereum/geth.ipc
截图结束后建议:
js
miner.stop()
否则会一直占 CPU。
8.3 看块
js
eth.getBlock(0) // 创世块
eth.getBlock("latest") // 最新一块
eth.getBlock(1).miner
eth.getBlock(1).parentHash
核对:第 1 块的 parentHash 应等于第 0 块的 hash。
8.4 转账练习
再开一个收款账户,从挖矿账户转 1 ETH:
js
personal.newAccount("123456")
personal.unlockAccount(eth.accounts[0], "123456", 300)
eth.sendTransaction({
from: eth.accounts[0],
to: eth.accounts[1],
value: web3.toWei(1, "ether")
})
返回交易 hash。然后:
js
eth.getTransaction("0x交易hash")
eth.pendingTransactions
eth.blockNumber
web3.fromWei(eth.getBalance(eth.accounts[1]), "ether")
value 也可用 wei 直写:1000000000000000000 就是 1 ETH。必须保持 eth.mining === true,否则交易不会进块。
8.5 控制台命令表
| 功能 | 命令 |
|---|---|
| 当前高度 | eth.blockNumber |
| 创世块 | eth.getBlock(0) |
| 最新块 | eth.getBlock("latest") |
| 是否在同步 | eth.syncing |
| 本节点 enode | admin.nodeInfo.enode |
| 已连接同伴 | admin.peers |
| 同伴数量 | net.peerCount |
| 加同伴 | admin.addPeer("enode://...") |
| 本地账户 | eth.accounts |
| 挖矿收益地址 | eth.coinbase |
| 余额(wei) | eth.getBalance(eth.accounts[0]) |
| 余额(ETH) | web3.fromWei(eth.getBalance(eth.accounts[0]), "ether") |
| 新建账户 | personal.newAccount("密码") |
| 解锁 300 秒 | personal.unlockAccount(eth.accounts[0], "密码", 300) |
| 转账 1 ETH | eth.sendTransaction({from: eth.accounts[0], to: "0x...", value: web3.toWei(1,"ether")}) |
| 开始挖矿 | miner.start(1) |
| 停止挖矿 | miner.stop() |
| 指定矿工地址 | miner.setEtherbase(eth.accounts[0]) |
| 是否在挖 | eth.mining |
| 退出 | exit |
9. 常用命令速查
9.1 宿主机 / Docker
bash
cd /root/MyEthereumNet
docker-compose ps
docker-compose logs --tail 50 peer1
docker-compose restart
docker-compose down
docker-compose up -d
docker logs myethereumnet-peer1-1
docker exec -it myethereumnet-peer1-1 /bin/sh
docker network inspect myethereumnet_default
# 重启后重连(若已放置脚本)
/root/MyEthereumNet/reconnect-peers.sh
9.2 一键看三节点高度
bash
for p in peer1 peer2 peer3; do
echo -n "$p "
docker exec myethereumnet-${p}-1 geth --exec 'eth.blockNumber' attach /root/.ethereum/geth.ipc
done
9.3 清理(慎用)
bash
# 只停容器,保留链数据
docker-compose down
# 连账本一起删,等于重做一条链
docker-compose down
rm -rf peer1/geth peer2/geth peer3/geth peer1/keystore peer2/keystore peer3/keystore
# 删镜像
docker rmi ethereum/client-go:v1.10.26 ethereum/client-go:latest
10. 排错
| 现象 | 原因与处理 |
|---|---|
geth init 报 datadir 被锁 |
容器里 geth 还在跑。先 docker-compose down 再 init |
| 三个创世 hash 不一致 | genesis.json 没拷全,或某节点旧 geth/ 没删干净 |
admin.peers 很多公网节点 |
用了 chainId 1 且没加 --nodiscover。改 15 并加 --nodiscover 后重建 |
admin.peers 一直 [] |
enode 里还是 127.0.0.1;应换成对方容器 IP 或服务名。重启后要重新 addPeer |
addPeer 返回 true 但 peers 仍空 |
等 2 秒;检查 networkid 是否同为 15;检查创世 hash 是否相同 |
miner.start 没反应 / 报错 |
镜像太新,Ethash 已移除。改用 v1.10.26 |
| 高度不涨 | 没 miner.start,或 coinbase 没设账户 |
| 三节点高度不一致 | P2P 没连上。查 admin.peers,跑重连脚本 |
| 转账一直 pending | 没有人在挖矿 |
Fatal: invalid command-line: too many arguments |
Geth 1.10 要把 --exec 写在 attach 前面 |
docker: unknown command: docker compose |
安装 docker-compose 包,或用带连字符的命令 |
| IPC 不存在 | 容器还没起来或 geth 崩了。看 docker logs |
| 端口冲突 | ss -tulpn 查 13303/14303/24303,改 compose 或停占用进程 |
日志:
bash
docker logs --tail 80 myethereumnet-peer1-1
关注 IPC endpoint opened、Started P2P networking、Successfully wrote genesis state、Block synchronisation started。