Sergio Chan

Full Stack, Born hacker, Professional Manager

Crazy fan of Hackathons all around the world.
Founded Hackathon team hACKbUSTER.


如何创建自己的以太坊私有链(1)CentOS + Geth

这两天根据网上能搜到的所有的教程和文档,试着搭建了一个以太坊私有链 + iOS 钱包 / 应用的客户端,踩了一些坑,一来要找到这些坑的解决办法比较曲折,二来这类教程的同质化太强,我就着重记录一下这些坑的原因和解决办法吧,帮助看到的人可以吸取一些经验。

准备工作

Ethereum 客户端目前有几种语言的实现是开源的(这里只列出几个最主要的,其他的可以在 Ethereum 的官方 wiki 上找到):

  • go-ethereum(GETH),这也是目前比较容易上手且普遍流行的实现,对应的工具和文档都比较齐全
  • pyethapp,基于 py-ethereum,但是目前已经停止继续维护,基于 py-ethereum 有了新的 py-evm,并且推出了基于 py-evm 的新的客户端 Trinity
  • Parity(Rust)
  • cpp-ethereum(C++)

Installation

各个平台的安装过程可以见以太坊官方的各平台安装指引。以 CentOS 为例,安装过程其实并不复杂。

首先确保 cmake, gcc, wget 这些基本的组件都已经安装完毕。然后是安装 golang 环境:

yum install golang

安装完 golang 环境后,可以 wget 下载 github 上最新版本的 release 包,查看地址在这。例如我就是下载的最新的 1.8.12 的包。

wget https://github.com/ethereum/go-ethereum/archive/v1.8.12.zip

下载完之后解压,解压后进入目录下运行 make。成功后即编译完成。geth 的可执行文件在 build/bin 下。切换到这个目录就可以在命令行调用 geth 命令了。

创世区块的简单配置以及需要注意的一个坑

{
"config": {
"chainId": 10,
"homesteadBlock": 0,
"eip150Block": 0,
"eip150Hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"eip155Block": 0,
"eip158Block": 0,
"byzantiumBlock": 0,
}
"nonce": "0×0000000000000042",
"mixhash": "0×0000000000000000000000000000000000000000000000000000000000000000",
"difficulty": "0×4000",
"alloc": {},
"coinbase": "0×0000000000000000000000000000000000000000",
"timestamp": "0×00",
"parentHash": "0×0000000000000000000000000000000000000000000000000000000000000000",
"extraData": "GenesisBlock",
"gasLimit": "0xffffffff"
}

在启动链之前首先要初始化创世区块,创世区块的几个基本参数的说明在网上比较好找,在这就不详细展开了。如果需要预先分配好几个账户及其对应的 balance,可以在 alloc 里面声明。

一个巨大的坑在于 chainID 的设置,如果设置为 0 则会在之后产生任何事务的时候报错 invlid sender,并且这里的 chainID 必须和命令行启动客户端的时候指定的 networkID 一致,这两者实际上是一个东西。如果不一致,也会导致后面再发送事务的时候验证失败。

为了在本地启动两个节点并相互连接,我们需要初始化两次创世节点到两份不同的数据目录下。

./geth --datadir ~/chain2/ init init.json
./geth --datadir ~/chain/ init init.json

Command Line 运行参数解析

官方的完整参数说明在这

./geth --port "30303" --rpc --rpccorsdomain "*" --datadir "~/chain" --rpcaddr "0.0.0.0" --rpcport "8545" --rpcapi "db,eth,net,web3,admin,personal,txpool" --networkid 10 --verbosity 3 --syncmode fast --gasprice 0 -gcmode archive console
./geth --port "30304" --rpc --rpccorsdomain "*" --datadir "~/chain2" --rpcaddr "0.0.0.0" --rpcport "8546" --rpcapi "db,eth,net,web3,admin,personal,txpool" --networkid 10 --verbosity 3 --syncmode fast --gasprice 0 -gcmode archive console

分别在不同的 terminal 窗口下运行这两条命令。注意最后的 console 参数可以不需要或用 >>将日志输出到文件 ,即不进入交互式的命令界面,然后在另外的命令行里通过 geth attach xxx回到交互式界面。

--rpc 钱包和应用与链的交互都是通过 JSON RPC 来完成的,因此在这里需要明确 rpc 服务器的地址、端口以及开放调用的 api 命名空间。这里如果希望 rpc 服务器可以被外网访问的话,需要指定 ip 为 0.0.0.0,内网访问的话就指定内网 ip,本地访问的话实际上指定 127.0.0.1即可,这样可以避免被不知名的其他节点不停地尝试连接。rpc 的 api 可以参考这里,如果是开发网页或 native 的应用的话可以直接使用包装好的 web3 库来调用。

—networkid这里需要和创世区块里定义的 chainID 一致。

—verbosity这是日志输出的等级,3 是到 INFO,4 是到 DEBUG,一般情况 3 就足够了,DEBUG 模式下日志刷新的太快,命令行交互基本没法使用。

—syncmode节点之间同步区块和事务的模式,在节点数少的情况下,尽量选择完整同步,避免因为不同步而产生冲突从而导致一些已经被认为发生的事务给丢弃了。但是这里最好选择 fast,在实际运行中,如果选择完整同步,对 CPU 的消耗非常大,因此如果配置不够的话,经常出现被系统杀死进程的情况,在这里最好选择默认的。

P.S 中间我还另外尝试了一下 iOS 上的 Geth 库,如果你的节点希望能被 iOS 或 Android 上的客户端(不是钱包或应用,是客户端)连接,这里需要将同步模式去掉,加上 —lightserver xxx 的参数,因为目前实现在移动平台上的节点客户端都是 light 版本的,因此连接的链节点也需要相应的配置这个参数。

—gcmode这个参数设置的问题见这里,我也遇到了同样的问题,退出命令行重启客户端,有的时候就发现刚才发生的 transaction 都被回滚了,这是因为默认情况下最近的 128 个区块都是存在内存中的,命令行结束了这个进程,这部分内存也就丢了,因此需要改成 archive,会将所有的区块存下来,避免中止客户端进程影响区块的数据。

Geth 基本交互操作

首先每个节点都要有一个默认的 coinbase 账户,而刚启动的时候除非你在启动的时候导入一个账户的 private key,否则就可以在命令行里输入

personal.newAccount("xxx")

创建一个新的地址。挖矿挖到的 ETH 也会存入这个账户里。

查看余额

eth.getBalance('xxxx')

是获取指定地址(包括合约)上的 ETH 数量,单位为 wei。

ETH 最小的单位是 wei,也是命令行默认的单位, 然后每 1000 进一个单位,依次是

  • kwei (1000 wei)
  • mwei (1000 kwei)
  • gwei (1000 mwei)
  • szabo (1000 gwei)
  • finney (1000 szabo)
  • ether (1000 finney)

在命令行里,可以输入

web3.fromWei(eth.getBalance(eth.coinbase))

来简便的查看当前矿工账户的 ether 余额。

开始挖矿 & 停止挖矿

这个就比较简单了。

miner.start()
miner.stop()

在挖矿的过程中,矿工不停地确认和打包新的区块,其他同步的节点上应该显示

Importer new chain segment               blocks=1 txs=0 mgas=0.000 elapsed=17.040ms mgasp

表示区块同步是正常进行的。

连接其他的节点

在启动的时候输出的一些信息中能够找到 enode 信息,如果错过了也可以通过

admin.nodeinfo.enode

拿到当前节点的连接地址,将其中的 @[::]:30303中间括号间的两个冒号改为你希望另一个节点访问到的 ip 地址,例如内网访问就填内网地址,外网访问就填公网 ip,然后在另一个节点的控制台里输入

admin.addPeer("xxx@xx:xx:xx:xx:xxxx")
admin.peers // 查看是否连接成功

即可使两个节点 P2P 连接。同时,如果一个节点是长期节点,即作为基础的节点在运转的话,在其他连接节点启动的时候,可以在命令行里设置 —bootnodes 来使新的节点启动的时候就去尝试连接这个已经在线的节点。

交易

挖矿产生了 ETH,就可以进行交易了。交易(调用智能合约也是)需要有至少一个矿工在挖矿,才能够完成,否则就没人处理了。在交易前,所有涉及产生事务,即 sendTransaction 的操作(调用智能合约也是),都需要 unlock 账户地址。

personal.unlockAccount(eth.coinbase,"{your password}",3000)

最后的 3000 单位为秒,即接下来的多少秒内保持账户可以提交事务的状态,可以自行调节。接下来就是具体的转账操作:

eth.sendTransaction({from:'xxx',to:'xxx',value:web3.toWei(1)})

这里的 value 单位仍然是 wei,因此输入的是 ether 为单位的话需要转换。操作成功后等待矿工确认和打包完成,就可以查看地址对应的余额了。

查看事务的状态

由于矿工有些时候不是正好就能处理到刚提交的事务并打包,因此你也可以通过

txpool.content

来查看当前事务池子里处于等待状态的事务,可以确认你的调用是否成功。

实现一个龙头智能合约

在 Ethereum 的测试网络上,都有龙头这样一个地址,你可以请求这个地址,他会发放一定量的测试 ETH 给开发者以供使用。并且用户用完后可以继续捐赠给它,例如 Ropsten Network。因此我也在这里很简单的实现一个龙头合约。

为了开发方便,目前已经有 Solidity 的 IDE 可以使用了,我使用的是 Remix,只要在 environment 里设置为 web3 provider 并填上自己的 rpc 地址,就可以连上你刚刚新建的私有链进行测试了。

pragma solidity ^0.4.20;
contract faucet {
uint public totalEther;
address public _owner;

constructor () public {
totalEther = 0;
_owner = msg.sender;
}

function donate() public payable {
totalEther += msg.value;
}

function requestSomeEther(uint amount) public {
totalEther -= amount;
if (totalEther < 0) {
totalEther = 0;
} else {
msg.sender.transfer(amount);
}
}
}

deploy 以上合约,拷贝地址并加载。通过大的挖矿节点调用合约的 donate 方法将 ether 转到合约的地址上,其他新的测试账户可以直接调用 requestSomeEther来获取指定数量的 ether,当然这个单位是 wei。

下面我会持续补充一些陆陆续续积累的经验。

续 0x0001:Geth 的内存泄漏

在初始几天的实际运行中,我遇到了一个非常严重的问题,以至于后悔入了这个坑。Geth 在长时间运行后,有的时候会自动报错 crash,有的时候则是直接在命令行显示 已杀死,这是典型的占用系统资源过多或 IO 时间太长被系统强制结束的情况。报的错非常非常长,但是追溯到源头就是 out of memory,观察服务器的监控内存曲线就能发现,每过一段时间,内存就会逐渐增加,直到占满 100%,在满负荷运行不久后,要么被系统强制结束,要么自己就 gg 了。于是开始了漫长的查找问题解决办法之路。然而我绝望的发现,无论是 SO 还是 Github 的 issue,都有大量的开发者提了相同的问题,在 Geth 最新的 1.8.12 release 版本中,这个内存泄漏的问题仍然没有找到根源。因此如果你也遇到了,不用大惊小怪的怀疑自己姿势不对,老老实实的上 supervisor 来保护这个进程即可。目前没有更好的解决办法,问题很早就已经被许多人发现了,到现在也没人能解决,实乃坑爹。

这里提供其中一个问题的地址,有很多相关的可以查。

令人惊奇的是,无论是其他人还是我的经历,在 supervisor 监控重启了几十回进程之后,Geth 对于内存的占用竟然稳定维持在了我设置的 2G 以内,目前为止连续运行了一周,没有发生一次崩溃。Hell knows,🤦‍♀️。