1. 环境
Win 10 Home
CentOS 7 (3.10.0-693.el7.x86_64, 在VirtualBox虚机)
GETH 1.8.2 (1.9.4)
2. 安装
https://geth.ethereum.org/downloads/
注:此页面由于从gethstore.blob.core.windows.net请求一个很大的list文件,容易打不开。记录的1.8.2的下载地址是:
https://gethstore.blob.core.windows.net/builds/geth-alltools-windows-amd64-1.8.12-37685930.zip
https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.8.12-37685930.tar.gz
2.1 Win10,bootnode安装
bootnode是一个简化版的节点,仅用于其他节点之间互相发现。
从下载包里解压bootnode.exe到例如F:\tmp\ethereum\node0
2.2 CentOS, member node安装
三台VirtualBox虚机,分别建测试目录/tmp/ethereum/node1, /tmp/ethereum/node2, /tmp/ethereum/node3.
从下载包解压geth可执行文件到三个目录。
在三个目录建立文件genesis.json,内容:
{
"config": {
"chainId": 15,
"homesteadBlock": 0,
"eip150Block": 0,
"eip155Block": 0,
"eip158Block": 0,
"byzantiumBlock": 0,
"constantinopleBlock": 0,
"petersburgBlock": 0
},
"alloc": {
"0x0000000000000000000000000000000000000001": {
"balance": "111111111"
},
"0x0000000000000000000000000000000000000002": {
"balance": "222222222"
},
"0x0000000000000000000000000000000000000003": {
"balance": "333333333"
}
},
"coinbase": "0x0000000000000000000000000000000000000000",
"difficulty": "0x20000",
"extraData": "",
"gasLimit": "0xf12345",
"nonce": "0x4510809143055965",
"mixhash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"timestamp": "0x00"
}
注:
- "chainId": identifies the current chain and is used for replay protection. You should set it to a unique value for your private chain (see https://ethereum.stackexchange.com/questions/15682/the-meaning-specification-of-config-in-genesis-json)
- "alloc" 预先给一些账号充值,可以没内容("alloc":{})
- "difficulty": "0x20000" 此难度值较低
- "gasLimit": "0xf12345":一个区块最大允许的gas。随着以太坊升级,某些指令所需gas会增加。后续测试如果创建/调用合约时提示gas不够,可以调高此值从头再来(see https://blog.csdn.net/weixin_34390105/article/details/89697561)
- "nonce" 据说要用随机值
3. 转账
3.1 卸载删除
bootnode:
del boot.key
member node:
rm -rf ~/.ethash/ ~/.ethereum/
注:Linux下数据默认存放在这2个目录。
3.2
# 生成 boot.key 文件
bootnode --genkey=boot.key
# 启动boot node
bootnode --nodekey=boot.key
启动后会打印一个url如:
node://fa5e5f9df4e1436e89465c798e81e7682cd8fc6840c80fbbc88e1f5a3a47103802b0f0dff9042c9f8d32c5259946462b891aaa099cf9447459a154523f494af4@[::]:30301
将其中的ip改成三个member node能连接到的ip,后面要用。例如:
node://fa5e5f9df4e1436e89465c798e81e7682cd8fc6840c80fbbc88e1f5a3a47103802b0f0dff9042c9f8d32c5259946462b891aaa099cf9447459a154523f494af4@172.16.45.228:30301
(注:1.9.4的格式是“......@127.0.0.1:0?discport=30301”)
3.3 初始化三个节点
./geth init genesis.json
3.4 在三个节点各建立一个账号(为简化测试,密码可分别设为例如1、2、3)。记录产生的账号地址
./geth account new
node1: c34af3b19a1adc89a817e44991934dcf6a3fdfac
node2: 266aed6dbe9cd172a5121a1c8b32519fb657ab23
node3: a2b9c13157e7a5b3220876322a7da3b852710357
3.5 分别启动三个节点
./geth --networkid 15 --bootnodes "enode://fa5e5f9df4e1436e89465c798e81e7682cd8fc6840c80fbbc88e1f5a3a47103802b0f0dff9042c9f8d32c5259946462b891aaa099cf9447459a154523f494af4@172.16.45.228:30301" console
注-"networkid":
- Since connections between nodes are valid only if peers have identical protocol version and network ID, you can effectively isolate your network by setting either of these to a non default value (see https://geth.ethereum.org/doc/Private-network)
- Network identifier (integer, 1=Frontier, 2=Morden (disused), 3=Ropsten, 4=Rinkeby) (default: 1)
启动后在命令行输入“net.peerCount”,应该输出2,验证这三个节点已经互相发现。
3.6 node1
# 查询账户余额。应该输出0
eth.getBalance(eth.coinbase)
(eth.coinbase=0xc34af3b19a1adc89a817e44991934dcf6a3fdfac)
personal.unlockAccount(eth.coinbase)
解锁当前账号,需输入账号的密码。解锁后过段时间会自动关闭 - 下次再执行transaction又要解锁。
eth.sendTransaction({from:eth.coinbase, to:"0x266aed6dbe9cd172a5121a1c8b32519fb657ab23", value:web3.toWei(0.0005, "ether")})
尝试往账号2(node2)转账。因为本账号目前没钱,会提示错误:
Error: insufficient funds for gas * price + value
miner.start(8)
开始挖矿,8=线程数:
Generating DAG in progress ......
经过10分钟左右会提示挖到矿:
Successfully sealed new block ......
此时查询余额就有钱了(挖矿的奖励):
38000000000000000000
38后18个零,即38 ether/以太币(计量单位see https://blog.csdn.net/wo541075754/article/details/79049425)。按当前行情171.38 * 38 = 6512.44美元。注意:一定要区分生产网络账号和私网账号(https://github.com/ethereum/go-ethereum: you should make sure to always use separate accounts for play-money and real-money ......)
注:按上述配置应该不用等太久就可挖到矿,否则就是出问题了。
# 解锁后给node2 转1 ether
personal.unlockAccount(eth.coinbase)
eth.sendTransaction({from:eth.coinbase, to:"0x266aed6dbe9cd172a5121a1c8b32519fb657ab23", value:web3.toWei(1, "ether")})
看到输出“Successfully sealed new block” 应该交易已成功了。
# 在node2 查账:
eth.getBalance(eth.coinbase)
输出1000000000000000000,已到账。
注:有时解锁较慢。需要等到输出“true”才完成。
# 在node2
# node2 转0.0005 ether给node3:
personal.unlockAccount(eth.coinbase)
eth.sendTransaction({from:eth.coinbase, to:"0xa2b9c13157e7a5b3220876322a7da3b852710357", value:web3.toWei(0.0005, "ether")})
注:由于是node1 挖矿,挖到矿后新的区块同步到node2,node2会输出“Imported new chain segment ......”
node3 查到:500000000000000(即5e14)。
再查node2:999122000000000000。
即手续费/gas = 1000000000000000000 - 999122000000000000 - 500000000000000 = 378000000000000。web3.fromWei('378000000000000', 'ether') = 0.000378 ether。0.000378 * 171.38 = 0.06478164美元。
注:经试验,转账1 ether的手续费也是0.000378 ether。
4. 合约
pragma solidity >=0.4.0 <0.7.0;
contract SimpleStorage {
uint storedData;
function set(uint x) public {
storedData = x;
}
function get() public view returns (uint) {
return storedData;
}
}
这个最简单的合约也有一个‘坑’(花费了一天时间),就是get方法的modifier "view" 在GETH 1.9.4运行时总是返回0,即调用合约的set方法总是好像没有实际调用到一样。经试验将"view" 改成 "constant",并改用GETH 1.8.2 + Solidity 0.4版本编译(当前最新0.5)可行。
remix.ethereum.org用来在线编译上面以Solidity语言编写的以太坊智能合约。在其中新建文件"SimpleStorage.sol",录入程序内容(注意"view"=>"constant");Compiler选择0.4.26;其他选项不变。点击“Compile SimpleStorage.sol”按钮编译。应该会编译成功。之后点击下方的“Compilation Details”,在弹框里找到自动生成的“WEB3DEPLOY”部署代码:
var simplestorageContract = web3.eth.contract([{"constant":false,"inputs":[{"name":"x","type":"uint256"}],"name":"set","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"get","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"}]);
var simplestorage = simplestorageContract.new(
{
from: web3.eth.accounts[0],
data: '0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a72305820cbdbd7f507adeac3b8bbabf01359315da795777ddf17a0bd5f81f3a9ccaeb5250029',
gas: '4700000'
}, function (e, contract){
console.log(e, contract);
if (typeof contract.address !== 'undefined') {
console.log('Contract mined! address: ' + contract.address + ' transactionHash: ' + contract.transactionHash);
}
})
上面“[{"constant" ......]”是合约JSON格式的ABI (application binary interface),描述合约的对外接口。“data: '0x ......'”是合约内容的编译结果。gas是编译器估算创建合约所需费用。
4.1 node1窗口
# 先解锁
personal.unlockAccount(eth.coinbase)
再粘贴上面这2段代码,回车。
等1~2分钟node1会挖到矿(执行创建合约的代码)。输出:
Contract mined! address: 0x71763e8bd5af4cf3db13460177cddceabda12833 transactionHash: 0xe9712292e73fd55b24b36ec4cb488c22bf904ce49ff2310b325e69206f936bf1
"0x71763e8bd5af4cf3db13460177cddceabda12833"是合约地址 - 现在该合约已经存在于node1,而且随着node1将挖矿新产生的区块广播给其他节点,node2、node3也会更新它们本地存储,最终全网达成共识接受创建这个新合约。
输入“simplestorage” 查看这个合约对象。输入“eth.getTransaction("0xe9712292e73fd55b24b36ec4cb488c22bf904ce49ff2310b325e69206f936bf1")” 查看创建合约的这个交易。
simplestorage.get()
输出0,说明合约的字段“storedData” 目前还没有赋值(由于get方法不改变链上内容,可以直接调用不需要transaction - 此方法直接查询当前节点的本地存储)。
simplestorage.set.sendTransaction(3, {from: eth.accounts[0]})
调用合约set方法,设置字段storedData=3。由于会改变链上内容,需要调用交易(并支付一定gas给矿工)。
等待提示挖矿成功后再查询:
simplestorage.get()
输出3,说明调用set成功。
4.2 node2窗口
var simplestorage = eth.contract([{"constant":false,"inputs":[{"name":"x","type":"uint256"}],"name":"set","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"get","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"}]).at("0x71763e8bd5af4cf3db13460177cddceabda12833")
simplestorage
simplestorage.get()
输出3 - 在node2查到正确的合约字段值。
personal.unlockAccount(eth.coinbase)
simplestorage.set.sendTransaction(4, {from: eth.accounts[0]})
node2修改合约字段值=4。等待node1挖矿后再查询应该修改成功变成4。
注:由于node1在不停挖矿,node2可能会先查到旧值再查到新值例如:
Submitted transaction
Imported new chain segment
> simplestorage.get()
3
Imported new chain segment
> simplestorage.get()
4
Imported new chain segment
回到node1窗口,确认字段值也是4。
4.3 退出
# node1
miner.stop()
exit
# node2
exit
# node3
exit
# bootnode (win10)
# Ctrl-C 退出
4.4 重新进入
# bootnode
bootnode --nodekey=boot.key
# node1|2|3
./geth --networkid 15 --bootnodes "enode://fa5e5f9df4e1436e89465c798e81e7682cd8fc6840c80fbbc88e1f5a3a47103802b0f0dff9042c9f8d32c5259946462b891aaa099cf9447459a154523f494af4@172.16.45.228:30301" console
验证合约的字段值仍在。
5. 补充
5.1 简化版的genesis.json 应该也可用:
{
"config": {
"chainId": 15,
"homesteadBlock": 0,
"eip155Block": 0,
"eip158Block": 0
},
"difficulty": "0x20000",
"gasLimit": "0xf12345",
"alloc": {
"0x0000000000000000000000000000000000000001": { "balance": "111111111" },
"0x0000000000000000000000000000000000000002": { "balance": "222222222" }
}
}
5.2
查看区块高度
eth.blockNumber
查看未打包的交易
txpool.status
查看一个区块
eth.getBlock(eth.blockNumber)
专门的挖矿节点好像没效:
./geth --networkid 15 --bootnodes "enode://......" --mine --minerthreads=2 --etherbase=0x...... console
估算gas
var gasValue = eth.estimateGas({data: binXXXXXX......})
指定gasPrice
eth.contract(ABI......)..new({from: xx, data: bin, gas: gasValue, gasPrice: 1e12}, function...)
SimpleStorage.sol
"constant"在Solidity 0.5.0时删除了。"view"+0.5对比 "constant"+0.4,编译出来的ABI和data 均稍有差异。