此文是区块链项目的期末 🔥 热身报告,用来帮助我们熟悉以太坊开发环境。实验主要包括:
- 以太坊的安装、私有链创世区块的搭建;
- block 各个字段和日志的输出解释;
- 在私有链部署简单的智能合约;
- 对交易字段进行解释。
以太坊的安装
我是在 Mac 上搭建的 Ethereum 环境,搭建的过程很简单:
brew tap ethereum/ethereum
brew install ethereum
geth --help
如果能成功显示输出帮助,则表示已经成功安装。
私有链环境搭建
配置文件
以太坊支持自定义创世区块,要运行私有链,我们就需要定义自己的创世区块,创世区块信息写在一个 json 格式的配置文件中。首先将下面的内容保存到一个 json 文件中,例如 genesis.json。(直接从官网复制,“chainId” 为 0, 但是会在合约部署出现问题,所以在这里改为 666)
{
"config": {
"chainId": 666,
"homesteadBlock": 0,
"eip155Block": 0,
"eip158Block": 0
},
"alloc" : {},
"coinbase" : "0x0000000000000000000000000000000000000000",
"difficulty" : "0x00000001",
"extraData" : "",
"gasLimit" : "0xffffff",
"nonce" : "0x0000000000000042",
"mixhash" : "0x0000000000000000000000000000000000000000000000000000000000000000",
"parentHash" : "0x0000000000000000000000000000000000000000000000000000000000000000",
"timestamp" : "0x00"
}
初始化私有链
准备好创世区块配置文件后,需要初始化区块链,将上面的创世区块信息写入到区块链中。首先要新建一个目录用来存放区块链数据,假设新建的数据目录为~/nino/shuju
,genesis.json 保存在~/nino
中,此时目录结构应该是这样的:
nino
├── shuju
└── genesis.json
进入 ~/nino
目录下,执行命令:
geth -datadir shuju init genesis.json
上面的命令的主体是 geth init,表示初始化区块链,命令可以带有选项和参数,其中--datadir
选项后面跟一个目录名,这里为 shuju
,表示指定数据存放目录为 shuju
, genesis.json
是init
命令的参数。运行上面的命令,会读取 genesis.json 文件,根据其中的内容,将创世区块写入到区块链中。如果看到以下的输出内容,说明初始化成功了。
WARN [11-04|10:34:14] No etherbase set and no accounts found as default
INFO [11-04|10:34:14] Allocated cache and file handles database=/Users/nino/shuju/geth/chaindata cache=16 handles=16
INFO [11-04|10:34:14] Writing custom genesis block
INFO [11-04|10:34:14] Successfully wrote genesis state database=chaindata hash=5e1fc7…d790e0
INFO [11-04|10:34:14] Allocated cache and file handles database=/Users/nino/shuju/geth/lightchaindata cache=16 handles=16
INFO [11-04|10:34:14] Writing custom genesis block
INFO [11-04|10:34:14] Successfully wrote genesis state database=lightchaindata hash=5e1fc7…d790e0
初始化成功后,会在数据目录~/nino/shuju
中生成 geth 和 keystore 两个文件夹。(其中geth/chaindata中存放的是区块数据,keystore中存放的是账户数据)
nino
├── shuju
│ ├── geth
│ │ └── chaindata
│ │ ├── 000002.ldb
│ │ ├── 000003.log
│ │ ├── CURRENT
│ │ ├── LOCK
│ │ ├── LOG
│ │ └── MANIFEST-000004
│ └── keystore
└── genesis.json
启动私有链
初始化完成后,就有了一条自己的私有链,之后就可以启动自己的私有链节点并做一些操作,在终端中输入以下命令即可启动节点:
geth -datadir shuju -networkid 2018 -rpc -rpcaddr 你的IP -rpccorsdomain "*" console
上面命令的主体是geth console
,表示启动节点并进入交互式控制台,--datadir
选项指定使用data0
作为数据目录,--networkid
选项后面跟一个数字 2018,表示指定这个私有链的网络id为2018。网络id在连接到其他节点的时候会用到,以太坊公网的网络 id 是1,为了不与公有链网络冲突,运行私有链节点的时候要指定自己的网络 id。运行上面的命令后,就启动了区块链节点并进入了Javascript Console:
INFO [11-04|16:51:13.102] Starting P2P networking
INFO [11-04|16:51:15.224] UDP listener up self=enode://1d464c3eeff748edaf49ff582c4face5a42be81ec6be37d2044c408935dbb76d95a2bb66b38fcf874cb59a124238cd00c86ce0fad96499490ef24f573272cee5@[::]:30303
INFO [11-04|16:51:15.225] RLPx listener up self=enode://1d464c3eeff748edaf49ff582c4face5a42be81ec6be37d2044c408935dbb76d95a2bb66b38fcf874cb59a124238cd00c86ce0fad96499490ef24f573272cee5@[::]:30303
INFO [11-04|16:51:15.227] IPC endpoint opened url=/Users/nino/shuju/geth.ipc
INFO [11-04|16:51:15.227] HTTP endpoint opened url=http://172.19.93.219:8545 cors=* vhosts=localhost
Welcome to the Geth JavaScript console!
instance: Geth/v1.8.14-stable/darwin-amd64/go1.11
INFO [11-04|16:51:15.314] Etherbase automatically configured address=0x78d2E183cb87bEb22fb8d09ef34675F8bbAc8e15
coinbase: 0x78d2e183cb87beb22fb8d09ef34675f8bbac8e15
at block: 95 (Sun, 04 Nov 2018 16:27:03 CST)
datadir: /Users/nino/shuju
modules: admin:1.0 debug:1.0 eth:1.0 ethash:1.0 miner:1.0 net:1.0 personal:1.0 rpc:1.0 txpool:1.0 web3:1.0
这是一个交互式的 Javascript 执行环境,在这里面可以执行 Javascript 代码,其中 > 是命令提示符。在这个环境里也内置了一些用来操作以太坊的 Javascript 对象,可以直接使用这些对象。这些对象主要包括:
对象 | 用途 |
---|---|
eth | 包含一些跟操作区块链相关的方法 |
net | 包含以下查看 P2P 网络状态的方法 |
admin | 包含一些与管理节点相关的方法 |
miner | 包含启动、停止挖矿的一些方法 |
personal | 主要包含一些管理账户的方法 |
txpool | 包含一些查看交易内存池的方法 |
web3 | 包含了以上对象,还包含一些单位换算的方法 |
私有链节点操作
创建账户
刚搭建好的私有链是没有账户的,输入:
> eth.accounts
发现账户为空,接下来使用personal
对象来创建一个账户:
> personal.newAccount("账户密码")
我们通过这种方法再创建账户:
> eth.accounts
["0x78d2e183cb87beb22fb8d09ef34675f8bbac8e15", "0x6f1f96c9131ad12f1ddcd9d195fdce6d0790b063"]
账户默认会保存在数据目录的 keystore 文件夹中。查看目录结构,发现shuju/keystore
中多了两个文件(分别对应于两个账户),这两个文件就对应刚才创建的两个账户,这是 json 格式的文本文件,可以打开查看,里面存的是私钥经过密码加密后的信息。比如 “…8e15” :
{"address":"78d2e183cb87beb22fb8d09ef34675f8bbac8e15","crypto":{"cipher":"aes-128-ctr","ciphertext":"c1a6c65452655416e0dde61ac1e45c7a98c36d224d9a6ce2b688bb64d44c00f0","cipherparams":{"iv":"57cc60fb258e5e2bfc0adef32ed18324"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"fb65ff0c0dc5bf41f27897b12ce617c5cc971ea3ab809bb0105a33c374d7f3bc"},"mac":"aee378c2c16e566eb569f8da2ea2d2edc75f6d772e9dd276d6e1507e402494da"},"id":"515b2084-8f92-4053-9e5b-15ce917a66a3","version":3}
此时的目录结构如下:
shuju
├── geth
│ ├── chaindata
│ ├── LOCK
│ ├── nodekey
│ └── nodes
├── geth.ipc
├── history
└── keystore
├── UTC--2018-11-04T08-23-15.866675000Z--78d2e183cb87beb22fb8d09ef34675f8bbac8e15
└── UTC--2018-11-04T09-03-50.634292000Z--6f1f96c9131ad12f1ddcd9d195fdce6d0790b063
查看余额
查看账户余额:
> eth.getBalance(eth.accounts[0])
0
> eth.getBalance(eth.accounts[1])
0
目前两个账户的以太币余额都是0,要使账户有余额,可以从其他账户转账过来,或者通过挖矿来获得以太币奖励。
节点挖矿
启动挖矿:
> miner.start(1)
其中start
的参数表示挖矿使用的线程数。第一次启动挖矿会先生成挖矿所需的 DAG 文件,这个过程有点慢,等进度达到100%后,就会开始挖矿,此时屏幕会被挖矿信息刷屏。
如果想停止挖矿,在 js console中输入:
> miner.stop()
输入的字符会被挖矿刷屏信息冲掉,没有关系,只要输入完整的miner.stop()
之后回车,即可停止挖矿。
挖到一个区块会奖励5个以太币,挖矿所得的奖励会进入矿工的账户,这个账户叫做coinbase,默认情况下 coinbase 是本地账户中的第一个账户:
> eth.coinbase
"0x78d2e183cb87beb22fb8d09ef34675f8bbac8e15"
挖到区块以后,账户 0 里面应该就有余额了:
> eth.getBalance(eth.accounts[0])
475000000000000000000
getBalance()
返回值的单位是wei
,wei
是以太币的最小单位,1个以太币=10的18次方个wei
。要查看有多少个以太币,可以用web3.fromWei()
将返回值换算成以太币:
> web3.fromWei(eth.getBalance(eth.accounts[0]),'ether')
475
发起交易
账户每隔一段时间就会被锁住,要发送交易,必须先解锁账户。由于我们要从账户 0 发送交易,所以要解锁账户 0:
> personal.unlockAccount(eth.accounts[0])
Unlock account 0x78d2e183cb87beb22fb8d09ef34675f8bbac8e15
Passphrase:
true
可以通过发送一笔交易,从账户 0 转移 5 个以太币到账户 1(⚠️ 最后显示的交易 ID 十分重要,我们将会用这个 ID 来查找交易):
> amount = web3.toWei(5,'ether')
"5000000000000000000"
> eth.sendTransaction({from:eth.accounts[0],to:eth.accounts[1],value:amount})
INFO [11-04|17:28:06.737] Setting new local account address=0x78d2E183cb87bEb22fb8d09ef34675F8bbAc8e15
INFO [11-04|17:28:06.738] Submitted transaction fullhash=0x67ef39ca8d8c8bd3dec2e96342a3ccffaaa3385140a0d8ded4c24b20b3be854d recipient=0x6F1f96c9131aD12f1dDcd9D195Fdce6d0790b063
"0x67ef39ca8d8c8bd3dec2e96342a3ccffaaa3385140a0d8ded4c24b20b3be854d"
此时交易已经提交到区块链,返回了交易的 hash,但还未被处理,这可以通过查看 txpool
来验证:
> txpool.status
{
pending: 1,
queued: 0
}
其中有一条 pending 的交易,pending 表示已提交但还未被处理的交易。
确认交易
要使交易被处理,必须要挖矿。这里我们启动挖矿,然后等待挖到一个区块之后就停止挖矿:
> miner.start(1); admin.sleepBlocks(1); miner.stop();
如下显示表示挖矿成功:
INFO [11-04|17:31:12.566] Updated mining threads threads=1
INFO [11-04|17:31:12.566] Transaction pool price threshold updated price=18000000000
INFO [11-04|17:31:12.572] Commit new mining work number=96 uncles=0 txs=0 gas=0 fees=0 elapsed=6.319ms
INFO [11-04|17:31:12.573] Commit new mining work number=96 uncles=0 txs=1 gas=21000 fees=0.000378 elapsed=6.867ms
INFO [11-04|17:31:16.781] Successfully sealed new block number=96 hash=ddc2bb…215191 elapsed=4.208s
INFO [11-04|17:31:16.782] 🔨 mined potential block number=96 hash=ddc2bb…215191
INFO [11-04|17:31:16.782] Commit new mining work number=97 uncles=0 txs=0 gas=0 fees=0 elapsed=171.356µs
true
txpool
中pending的交易数量应该为0了,说明交易已经被处理了:
> txpool.status
{
pending: 0,
queued: 0
}
此时,交易已经生效,账户一应该已经收到了5个以太币了:
> web3.fromWei(eth.getBalance(eth.accounts[1]),'ether')
5
区块数量
查看当前区块总数:
> eth.blockNumber
96
所以说目前我们的私有链上有 96 个区块。
查看交易
通过交易hash查看交易:
> eth.getTransaction("0x67ef39ca8d8c8bd3dec2e96342a3ccffaaa3385140a0d8ded4c24b20b3be854d")
{
blockHash: "0xddc2bbbeaa686cc5c9c6b2d3d8a9f942a53a9051bc06e9d649e4de1e3a215191",
blockNumber: 96,
from: "0x78d2e183cb87beb22fb8d09ef34675f8bbac8e15",
gas: 90000,
gasPrice: 18000000000,
hash: "0x67ef39ca8d8c8bd3dec2e96342a3ccffaaa3385140a0d8ded4c24b20b3be854d",
input: "0x",
nonce: 0,
r: "0xb852d78a0483ba05a21e3701c74bd86ed2dd52867b59258170e7feb8ba3f0414",
s: "0x3c752d3d485e1f7ed16bec78629be07bf377c4de9b041b11388263d943a66fd3",
to: "0x6f1f96c9131ad12f1ddcd9d195fdce6d0790b063",
transactionIndex: 0,
v: "0x557",
value: 5000000000000000000
}
交易中各个字段的含义:
hash
是交易的哈希值,nonce
用来区别同一用户发出的不同交易的标记;blockHash
是封装事务(交易)的区块哈希值,blockNumber
是该区块在我们构建的私有链的序号;from
和to
字段规定了交易的双方,value
字段规定了交易的数额;gas
表示这个交易允许消耗的最大 Gas 数量,gasPrice
表示发送者愿意支付给矿工的 Gas 价格;r
、s
、V
为交易签名的三个部分,由发送者的私钥对交易哈希进行签名生成。
查看区块
通过区块号查看区块:
> eth.getBlock(96)
> eth.getBlock("0xddc2bbbeaa686cc5c9c6b2d3d8a9f942a53a9051bc06e9d649e4de1e3a215191")
{
difficulty: 131072,
extraData: "0xd78301080e846765746886676f312e31318664617277696e",
gasLimit: 15275265,
gasUsed: 21000,
hash: "0xddc2bbbeaa686cc5c9c6b2d3d8a9f942a53a9051bc06e9d649e4de1e3a215191",
logsBloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
miner: "0x78d2e183cb87beb22fb8d09ef34675f8bbac8e15",
mixHash: "0x4b72e2f521d9a45d53920d0af22c575eeb2ad489bab41d0b509584b86bd096ff",
nonce: "0x0dc5a55c24b91e34",
number: 96,
parentHash: "0xff22f6f888c08e290b292ad105b9c72b4ff206c57e78a8393d38a94b4ea6ddd3",
receiptsRoot: "0xa45d92ea3058f83814c424a8d4bdf5c3cd65742545569c3f3ede75b2ca2e6be1",
sha3Uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
size: 651,
stateRoot: "0x346e3b94dfaebfb891f5d9d41204b83ed173230fa9d6e62016e9f3453f7cbcaf",
timestamp: 1541323872,
totalDifficulty: 12844361,
transactions: ["0x67ef39ca8d8c8bd3dec2e96342a3ccffaaa3385140a0d8ded4c24b20b3be854d"],
transactionsRoot: "0x9fae89d7136c768294f0eb495f140308ee858f2e54de9b537f89970a4e917fee",
uncles: []
}
区块中各个字段的含义:
difficulty
是挖出该区块的难度,totalDifficulty
是目前挖矿的难度积累,nonce
是用来 PoW 的无意义6位哈希数;hash
是区块的哈希值,mixHash
256位 Hash 值,当与nonce
组合时,证明此区块已经执行了足够的计算;size
为区块的大小,miner
挖掘出这个区块的作者地址;number
区块的序号,等于其父区块的number
+1,timestamp
区块应被创建的时间戳,由共识算法确定;-
gasLimit
区块的gas
消耗的理论上限,gasUsed
是实际消耗的gas
总和; parentHash
父区块的指针,uncles
叔区块 是用来对孤立区块进行奖励的一个 Header 数组,sha3Uncles
是 叔区块 们的安全散列;receiptsRoot
Block 中的 “Receipt Trie”的根节点的 RLP 哈希值,Block 的所有 Transaction 执行完后会生成一个 Receipt 数组,这个数组中的所有 Receipt 被逐个插入一个 MPT 结构中,形成”Receipt Trie”;stateRoot
StateDB 中的“state Trie”的根节点的 RLP 哈希值,每个账户以 stateObject 对象表示,账户以 Address 为唯一标示,其信息在相关交易的执行中被修改,所有账户对象可以逐个插入一个Merkle Patrica Trie(MPT)结构里,形成“state Trie”;transactionRoot
Block 中 “tx Trie”的根节点的 RLP 哈希值,Block 的成员变量 transactions 中所有的 tx 对象,被逐个插入一个 MPT 结构,形成“tx Trie”;logsBloom
Bloom 过滤器,用来快速判断一个参数 Log 对象是否存在于一组已知的 Log 集合中;extraData
区块创建者可以记录一些与该区块有关的信息,长度小于等于32字节即可。
智能合约
合约定义
用 Solidity 定义一个智能合约 Nino.propose()
:
pragma solidity ^0.4.18;
contract Nino {
string word;
function Nino(string _word) public {
word = _word;
}
function propose() constant public returns (string) {
return word;
}
}
在 Remix 文本框复制这段代码,进行调试,编译无误后点击 Detail,
可以从弹出窗口找到 WEB3DEPLOY
,把红线部分的话换成 Nino 的 propose:
部署合约
复制以上内容,因为部署合约本质上也是一种交易,在部署前需要把部署者 account[1] 解锁(Unlock):
> personal.unlockAccount(eth.accounts[1])
Unlock account 0x6f1f96c9131ad12f1ddcd9d195fdce6d0790b063
Passphrase:
true
并把其输入 JS Console:
var _word = "I'm 16340154, please give me a high score";
var ninoContract = web3.eth.contract([{"constant":true,"inputs":[],"name":"propose","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[{"name":"_word","type":"string"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"}]);
var nino = ninoContract.new(
_word,
{
from: web3.eth.accounts[1],
data: '0x608060405234801561001057600080fd5b506040516102a83803806102a8833981018060405281019080805182019291905050508060009080519060200190610049929190610050565b50506100f5565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061009157805160ff19168380011785556100bf565b828001600101855582156100bf579182015b828111156100be5782518255916020019190600101906100a3565b5b5090506100cc91906100d0565b5090565b6100f291905b808211156100ee5760008160009055506001016100d6565b5090565b90565b6101a4806101046000396000f300608060405260043610610041576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063c198f8ba14610046575b600080fd5b34801561005257600080fd5b5061005b6100d6565b6040518080602001828103825283818151815260200191508051906020019080838360005b8381101561009b578082015181840152602081019050610080565b50505050905090810190601f1680156100c85780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b606060008054600181600116156101000203166002900480601f01602080910402602001604051908101604052809291908181526020018280546001816001161561010002031660029004801561016e5780601f106101435761010080835404028352916020019161016e565b820191906000526020600020905b81548152906001019060200180831161015157829003601f168201915b50505050509050905600a165627a7a7230582001962db1111855ac0f746def769de3d5475e132807426af7c33f7abe7c417a060029',
gas: '4700000'
}, function (e, contract){
console.log(e, contract);
if (typeof contract.address !== 'undefined') {
console.log('Contract mined! address: ' + contract.address + ' transactionHash: ' + contract.transactionHash);
}
})
即使复制上去了之后,我们还不能着急运行,我们还要进行挖矿把 pending 的交易封装成块miner.start()
让交易得到确认才能运行合约:
> miner.start(1); admin.sleepBlocks(1); miner.stop();
INFO [11-04|20:06:17.040] Updated mining threads threads=1
INFO [11-04|20:06:17.040] Transaction pool price threshold updated price=18000000000
INFO [11-04|20:06:17.040] Commit new mining work number=162 uncles=0 txs=0 gas=0 fees=0 elapsed=124.139µs
INFO [11-04|20:06:17.041] Commit new mining work number=162 uncles=0 txs=1 gas=242793 fees=0.004370274 elapsed=517.207µs
INFO [11-04|20:06:18.184] Successfully sealed new block number=162 hash=3bae74…9bb332 elapsed=1.143s
INFO [11-04|20:06:18.192] 🔗 block reached canonical chain number=157 hash=787e98…9ebcc5
INFO [11-04|20:06:18.192] 🔨 mined potential block number=162 hash=3bae74…9bb332
INFO [11-04|20:06:18.192] Commit new mining work number=163 uncles=0 txs=0 gas=0 fees=0 elapsed=161.24µs
true
> null [object Object]
终于成功了 ✌️:
Contract mined! address: 0x0ea4925971b634f91310d8d1d61fad8734b9a09f transactionHash: 0xb2ac1920b1fddd2d6b032ac9204ac963a8eaf0e793a0b8803ce5433e8c85ae68
那么 Nino 的 proposal 到底是什么呢???🤔️
> nino.propose()
"I'm 16340154, please give me a high score"
*希望它能够实现,因为它为了部署这个合约可是花了钱呢 *💰:
> eth.getBalance(eth.accounts[1])
4979037452000000000 // < 5000000000000000000
参考&鸣谢
“喝水不忘挖井人”,这里特别感谢一下几个博主的优秀博文: