以太坊搭建私有链和部署智能合约
[TOC]
1. 实验环境
VMWare虚拟机 + CentOs 7 + Golang
2. 以太坊安装
2.1 安装相关组件
为了避免后续安装过程出错,这里提前安装一些需要用到的组件和依赖。如果不确定之前有没有安装过这些组件也没关系,检测到安装就不会重复安装。-y表示安装过程中弹出的问题一律回答yes。
yum update -y && yum install git wget bzip2 vim gcc-c++ ntp epel-release nodejs cmake -y
- 相关组件的作用
组件 | 作用 |
---|---|
git | 版本管理工具,用于后面从github克隆geth |
wget | Linux命令行的下载工具,用于后面下载cmake |
bzip2 | 无损压缩软件 |
vim | 编辑工具,用于后面编辑一些环境配置文件 |
gcc-c++ | c/c++编译工具,用于后面geth的编译 |
ntp | 网络时钟同步组件 |
nodejs | 用于前端js开发的包管理软件 |
epel | 网络第三方的linux安装包源 |
2.2 安装及配置Golang环境
- 我的虚拟机之前已经装过Golang环境,因此这里不再重现和赘述。
- 贴一篇我当时配golang环境时写的博客:golang开发环境配置
2.3 克隆并编译geth
- 从github上clone下来go-ethereum(简称geth)
git clone https://github.com/ethereum/go-ethereum.git
- 进入geth根目录并编译
cd go-ethereum && make all
- 配置环境变量
# 打开环境配置文件
vim ~/.profile
# 新增path
export PATH=$PATH:$GOPATH/bin:$HOME/go-ethereum/build/bin
# 让更改生效
source ~/.profile
- 验证是否配置成功
在终端输入geth -h有一堆帮助信息出来即可
2.4 安装及配置cmake环境
智能合约编译需要cmake,之前yum install cmake的版本只有2.8不够,这里需要去官网下载其独立安装包。这里我用的是最新版本3.12.3。
- 下载到用户根目录$HOME
cd && wget https://cmake.org/files/v3.12/cmake-3.12.3.tar.gz
- 解压
tar -xzvf cmake-3.12.3.tar.gz
- 进入cmake根目录并编译安装
cd cmake-3.12.3
./bootstrap && make && make install
- 配置环境变量
# 打开环境配置文件
vim ~/.profile
# 新增path
export PATH=$PATH:$GOPATH/bin:$HOME/go-ethereum/build/bin:$HOME/cmake/bin
# 让更改生效
source ~/.profile
- 验证是否配置成功
在终端输入cmake -h或cmake -version有一堆帮助信息出来或者有版本信息出来均可
2.5 防火墙
因为之前在配置golang私有云桌面的时候把防火墙关闭了,我在这里就不需要考虑。
- 你可以选择关闭防火墙
systemctl stop firewalld systemctl disable firewalld
- 也可以只添加防火墙规则,允许某几个geth使用一些默认端口。
firewall-cmd --zone=public --add-port=8087/tcp --permanent firewall-cmd --zone=public --add-port=30303/tcp --permanent
- mark一下:启用防火墙的命令
systemctl enable firewalld systemctl start firewalld
2.6 时钟同步
因为区块链需要同步网络时间,所以需要启用网络时间同步
systemctl enable ntpd
systemctl start ntpd
3. 私有链创世区块搭建
- 编写创世区块的配置文件genesis.json。里面可以设置一些挖矿难度,默认的挖矿账户和最大手续费等。
{ "nonce": "0x0000000000000042", "timestamp": "0x00", "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", "extraData": "0x00", "gasLimit": "0x80000000", "difficulty": "0x400", "mixhash": "0x0000000000000000000000000000000000000000000000000000000000000000", "coinbase": "0x2D356ee3F5b8718d8690AFCD31Fe2CB5E602677e", "alloc": {}, "config": { "chainId": 15, "homesteadBlock": 0, "eip155Block": 0, "eip158Block": 0 } }
- 初始化创世区块节点信息
geth --datadir node0 init genesis.json
- 得到如下信息,说明初始化成功
- 执行geth的控制台,将输出重定向到geth.log中
geth --datadir node0 console 2>> geth.log
- 一些基本的操作
- 新建账户、查看账户及账户余额
- 查看挖矿的账户、当前区块数等
- 发起交易、查看交易池状态等
- 挖矿使交易池中待处理的交易完成(可以看见pending从1变成0,目标账户的余额从0变成15个以太币——注意单位换算fromWei),sleepBlocks(n)可以监控挖矿过程,指定在挖到第n个新块的时候的时候执行下条命令,如stop()。
- 注:只要实际上有在挖矿,start返回null或true都无所谓。
- 新建账户、查看账户及账户余额
- 另开一个终端查看log内容
tail -f geth.log
4. 私有链节点加入
- 新建一个节点并初始化
geth --datadir node1 init genesis.json
- 启动上一个节点的rpc服务,并把网络id设置成2018,其他不设置(默认rpc端口8545,监听端口30303,ip地址127.0.0.1)
geth --datadir node0 -networkid 2018 -rpc console 2>> geth.log
- 启动新节点的rpc服务,把网络id设置成2018(要相同),rpc的端口默认8545,节点监听的端口默认是30303,这两个端口都被上一个节点占用了(因为我是在一台机器上),因此这个新节点的这两个端口都要手动设置(我这里分别设成8546和30304),ip地址仍是默认
geth --datadir node1 -networkid 2018 -rpc -rpcport 8546 -port 30304 console 2>> geth.log
- 查看新节点的enode,并拷贝
admin.nodeInfo.enode
- 在另一个终端上,进入上一个节点的console,加入新节点
admin.addPeer(上面拷贝的enode)
连接成功
有个小小的疑惑。当时忘记设置网络id,默认是1,网上说networkid是1的时候是接入公网?然后确实是会莫名其妙偶尔连到一些外国的节点,第一个193开头的是乌克兰,第一个52开头的是爱尔兰的。但是为何私链可以接到公网上面?后面发现加上-nodiscover就可以不被别的节点发现。
5. 区块字段解释
5.1 最新区块
5.2 各个字段解释
字段 | 意义 |
---|---|
difficulty | 挖到当前区块的难度 |
extraData | 额外数据 |
gasLimit | 该区块允许使用的手续费上限 |
gasUsed | 该区块实际包含的所有手续费 |
hash | 该区块的唯一哈希值标识,32字节 |
logsBloom | 区块日志的过滤器,256字节 |
miner | 打包该区块的矿工地址,20字节 |
mixHash | 混合哈希 |
nonce | 一个符合PoW难题的随机值,8字节 |
number | 当前区块的序号 |
parentHash | 父区块的哈希值,32字节 |
receiptRoot | 收据的唯一哈希值标识 |
sha3Uncles | 叔区块的SHA3哈希值 |
size | 区块的大小,单位是字节 |
stateRoot | 该区块最终前缀树的根哈希 |
timeStamp | 该区块被发现时的Unix时间戳 |
totalDifficulty | 挖到当前区块前所有区块的难度总和 |
transactions | 所有交易的哈希值集合 |
transactionsRoot | 该区块交易前缀树的根哈希 |
uncles | 叔哈希 |
5.3 补充
- difficulty由父区块的难度及其时间戳与本区块时间戳的差计算得到。
- mixHash是一个与随机数 (nonce)相关的 256 位哈希计算,用于证明针对当前区块已经完成了足够的计算,相当于签名。
- 创世区块的parentHash为0
- 当前区块的哈希由区块头唯一决定。意味着,区块头不同,产生的哈希才会不同。区块头里面所要记录的信息都是确定的、不变的,为了让区块头产生变化,引入了一个随机值 Nonce,作为一个唯一的变量,在密码学中Nonce是一个只被使用一次的任意或非重复的随机数值。矿工的作用其实就是不断重复 变化 Nonce,计算哈希的过程,直到碰到一个 Nonce 的值,使得区块头的哈希可以小于目标值,从而能够写入区块链。
- 一个块奖励5个以太币
6. 日志输出解释(默认日志等级)
6.1 挖矿
可以观察到每次挖到一个新的区块都需要经历四个阶段。分别是commit new mining work => successfully sealed new block => block reached canonical chain => mined potential block,即先提交要打包某个区块的任务,然后成功打包该区块,该区块接到主链上(主链最后一个区块号一般会比当前的目标区块小一些,因为有分叉),成功完成挖矿。每条记录后面的number指明的是当前挖矿的目标区块序号。
6.2 节点记录
- new local node record这条记录在网上搜了很久没有发现对应的解释,我猜测可能是因为我关闭了防火墙,同时没有设置nodiscover,也许会从各个udp端口或30303tcp端口收到来自公网的包,从而进行同步,猜测这也是之前为什么会连接到公网节点的原因之一,当然可能也不仅仅只包含公网的同步,应该也还有私链上的多节点同步。每条该类记录后面会有一个seq标识当前记录是第几个的记录。
- 然后每经过一定量的日志记录后,会出现regenerated local transaction journal,这个是日志记录的回转,为了防止日志记录过多。日志轮换是系统管理中使用的自动过程,对过时的日志文件进行存档。运行大型应用程序的服务器通常会记录每个请求,面对庞大的日志,日志轮换提供了一种限制保留日志总大小的方法,同时仍允许分析最近的事件。
6.3 添加节点
- 这段日志里的各种closed和stopped是因为退出了某个节点的控制台,需要关闭所有服务,当然包括数据库及各种协议。
- 其他的像添加节点后便会出现从starting peer-to-peer node到started P2P networking等一系列节点连接的过程,包括ipc服务的open,存储空间的确认,第一次连接的话可能中间还会有一些初始化同一创世区块的步骤。
7. 部署智能合约
7.1 编写合约
通过remix,写了一个最简单的合约,有读操作也有写操作。
pragma solidity ^0.4.18;
contract Test{
string public name;
function Test() public {
name = "xietao";
}
function getName() public view returns (string) {
return name;
}
function setName(string _name) public {
name = _name;
}
}
7.2 编译并部署合约
- 右侧点击start compile之后点击detail可以看见合约详情。
- 找到WEB3DEPLOY一项,拷贝里面的代码
var testContract = web3.eth.contract([{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getName","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_name","type":"string"}],"name":"setName","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"inputs":[],"payable":false,"stateMutability":"nonpayable","type":"constructor"}]); var test = testContract.new( { from: web3.eth.accounts[0], data: '0x6060604052341561000f57600080fd5b60408051908101604052600681527f78696574616f00000000000000000000000000000000000000000000000000006020820152600090805161005692916020019061005c565b506100f7565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061009d57805160ff19168380011785556100ca565b828001600101855582156100ca579182015b828111156100ca5782518255916020019190600101906100af565b506100d69291506100da565b5090565b6100f491905b808211156100d657600081556001016100e0565b90565b61037f806101066000396000f3006060604052600436106100565763ffffffff7c010000000000000000000000000000000000000000000000000000000060003504166306fdde03811461005b57806317d7de7c146100e5578063c47f0027146100f8575b600080fd5b341561006657600080fd5b61006e61014b565b60405160208082528190810183818151815260200191508051906020019080838360005b838110156100aa578082015183820152602001610092565b50505050905090810190601f1680156100d75780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34156100f057600080fd5b61006e6101e9565b341561010357600080fd5b61014960046024813581810190830135806020601f8201819004810201604051908101604052818152929190602084018383808284375094965061029295505050505050565b005b60008054600181600116156101000203166002900480601f0160208091040260200160405190810160405280929190818152602001828054600181600116156101000203166002900480156101e15780601f106101b6576101008083540402835291602001916101e1565b820191906000526020600020905b8154815290600101906020018083116101c457829003601f168201915b505050505081565b6101f16102a9565b60008054600181600116156101000203166002900480601f0160208091040260200160405190810160405280929190818152602001828054600181600116156101000203166002900480156102875780601f1061025c57610100808354040283529160200191610287565b820191906000526020600020905b81548152906001019060200180831161026a57829003601f168201915b505050505090505b90565b60008180516102a59291602001906102bb565b5050565b60206040519081016040526000815290565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f106102fc57805160ff1916838001178555610329565b82800160010185558215610329579182015b8281111561032957825182559160200191906001019061030e565b50610335929150610339565b5090565b61028f91905b80821115610335576000815560010161033f5600a165627a7a72305820ca52a32829a7341ced9c589fa39647854f065bcec7bf1d7b4a2aed5185b579900029', gas: '4700000' }, function (e, contract){ console.log(e, contract); if (typeof contract.address !== 'undefined') { console.log('Contract mined! address: ' + contract.address + ' transactionHash: ' + contract.transactionHash); } })
- 将上述代码直接黏贴进console,或者可以保存在某个js文件中再load进来(比较优雅)。如果报错说没有unlock则需要先解锁一个账户。
personal.unlockAccount(eth.accounts[0])
- 然后开始挖矿进行部署,否则合约实例的address是undefined。
miner.start();admin.sleepBlocks(1);miner.stop()
- 提示contract mined则正式部署成功!
Contract mined! address: 0x7e8109387a54814fcc61a48729b7a891f24cd291 transactionHash: 0x656fc28b69415dab65edbb5e62f2034daa6f2402f5db052aaba72da3269a1707
- 再次查看合约实例——test的address字段,已经不再是undefined了。
7.3 调用合约
- 读操作。读操作很简单,直接调用就行,不消耗gas。
- 写操作。写操作遇到一点坑,一开始总是set没有更改到。虽然知道要扣费,但一开始不知道没搞懂怎么扣除指定账户的gas。后面发现其实合约也是一个账户,和交易类似。有两种设置付款账户的方法。
- 为该节点设置默认账户,这样在调用set函数时就会自动让你付款。这种方法适用于只有一个账户的节点或者只有一个账户经常使用的多账户节点。
eth.defaultAccount = eth.accouts[0]
- 每次调用set时顺便指定付款账户。这种方法适用于每个账户都经常使用的多账户节点。
test.setName("xietao1", {from:eth.accounts[0]}");
- 其他操作和交易时是一样的,交易前记得解锁账户,产生交易后记得挖矿。
- 为该节点设置默认账户,这样在调用set函数时就会自动让你付款。这种方法适用于只有一个账户的节点或者只有一个账户经常使用的多账户节点。
- 注意到读操作和写操作是有区别的——写操作要消耗gas,读操作不用。因为写操作需要通过矿工对链做出更改和同步,有代价。
8. 交易字段解释
8.1 交易信息
8.2 各个字段解释
字段 | 意义 |
---|---|
blockHash | 该交易所在区块的哈希,32字节 |
blockNumber | 该交易所在的区块序号 |
from | 该交易转账方的地址 |
gas | 转账方愿意支付的手续费 |
gasPrice | gas的单价,单位是Wei |
hash | 该交易的哈希 |
input | 交易过程中用到的数据 |
nonce | 转账方在该交易前发起的交易数 |
r,s | 交易的ECDSA签名 |
to | 该交易收款方的地址 |
v | 公钥恢复id |
transactionIndex | 该交易在所在区块交易集合的下标 |
value | 交易的金额,单位是Wei |
8.3 补充
- 区别(结合例子解释)
- gas。在交易信息中的gas字段,表示交易发起方(转账方)愿意支付的gas上限。本例是发起方愿意支付90000单位的gas。
- gas price。由交易发起方(转账方)设置,表示gas的单位价格,单价越高交易越优先处理。本例中发起方愿意以每单位1000000000Wei的价格支付gas。gas * gas price = 发起方最多愿意支付的手续费。
- gas limit。在交易信息中和gas是一个意思,但是在区块信息中表示该区块最多可以消耗的gas上限。也就是该区块中所有交易实际消耗gas(gas used)的总和不能超过该区块的gas limit。
- gas used。表示一个交易实际上消耗的gas。看看该交易对应的区块,可以发现gas实际上只消耗了21000,没有超过交易发起方愿意发起的gas上限(交易发起方的gas limit,即90000),交易正常进行。gas used * gas price = 发起方实际支付的手续费,多余的gas会被返还。
- 注意如果发起方愿意支付的gas小于实际消耗的gas,那么交易会被撤销,但仍然会扣除发起方支付的gas,因为不管交易是否可以顺利进行,矿工都已经进行了工作。
- 区别
- 交易的nonce。交易信息中的nonce字段是通过计数来用来唯一标识每一个交易。nonce越小会越先被处理。可以用来防止一些双花等恶意消费动机。
- 挖矿的nonce。即区块中的nonce,这个前面解释过,是用来调整大小以产生符合难题的哈希的一个无意义的值。
- Wei是以太币的最小计量单位,$一个以太币=10^{18}Wei$