- 你适合使用本教程吗?
- 环境要求
- 安装 Geth
- 我们需要做什么?
- 等等,在开始之前……
- Step 1: 创建创世区块并初始化区块链
- Step 2: 启动区块链!
- Step 3: 部署智能合约并调用
- Step 4: 多节点的私有链
你适合使用本教程吗?
本教程将带领你从零开始在 MacOS 上搭建一个以太坊环境,并且设置一个你自己的私有节点,然后完成挖矿和交易。在它上面,你可以测试你的智能合约代码,或是干任何你想做的事情~在最后,我们也会带领你在上面撰写一个你自己的智能合约,并尝试运行它。
环境要求
- 运行在 MacOS 上的计算机
- 配置好的 Homebrew 环境
- 一些计算机和以太坊的知识
安装 Geth
Geth 是 ‘Go Ethereum’ 的缩写。顾名思义,他是以太坊节点客户端的 go 语言版本。为了搭建我们的以太坊环境,Geth 使我们需要的第一件东西。
打开你的终端,并且运行以下命令来安装 Geth
1 | brew update |
在 Homebrew 的工作完成后,Geth 就已经被成功安装到你的 Mac 上了。接下来就让我们正式走入以太坊的世界吧!
我们需要做什么?
从搭建一个私有的以太坊节点到运行智能合约,我们需要经过以下几步:
- 创建创世区块并初始化区块链
- 启动区块链并尝试挖矿和交易
- 部署智能合约并调用
那么让我们来一步步配置吧~
等等,在开始之前……
在我们开始配置前,我推荐你为你的节点创建一个文件夹,他将被用于存储以太坊的账号信息和数据库。傻瓜式代码如下:
1 | cd ~ |
Step 1: 创建创世区块并初始化区块链
什么是创世区块?
创世区块是一个区块链的第一个区块。同时,创世区块也会初始化节点的行为。
配置创世区块
因而要开启一条新的区块链,首先我们需要配置好我们创世区块,新建一个配置文件 genesis.json
,并用 vim 打开它:
1 | touch genesis.json && vim genesis.json |
将以下内容复制进区块,并且按 ESC 后输入 :wq
退出 vim 并保存文件:
1 | { |
你可能注意到这个 JSON 文件有许多 字段,让我们来一个个解释一下吧:
- chainId: 用于唯一标识整条区块链的编号
- homesteadBlock: 以太坊的早期版本需要的配置值,可留为 0。
- eip155Block/eip158Block: EIP 是指 “Ethereum Improvement Proposals”,他们被设计来代替
homesteadBlock
处理硬分叉等事宜,在私有链上我们无需设置,同样设置为 0 即可。 - difficulty: 顾名思义,挖矿的初始难度。其的含义是“平均每
difficulty
次 hash 值的尝试就会生成一个有效的区块” - gasLimit: 智能合约的消耗上限
- alloc: 允许将以太币分配到一个特定的地址
之后,以太坊程序会根据这个文件中的配置信息来确定链上的创世区块。
初始化区块链
那么让我们初始化这条区块链吧,在终端中使用以下命令:
1 | geth --datadir private init genesis.json |
你将看到如下输出:
INFO [11-01|23:00:31.705] Maximum peer count ETH=25 LES=0 total=25
INFO [11-01|23:00:31.712] Allocated cache and file handles database=/Users/siskon/ethereum/private/geth/chaindata cache=16 handles=16
INFO [11-01|23:00:31.721] Writing custom genesis block
INFO [11-01|23:00:31.722] Persisted trie from memory database nodes=0 size=0.00B time=21.494µs gcnodes=0 gcsize=0.00B gctime=0s livenodes=1 livesize=0.00B
INFO [11-01|23:00:31.723] Successfully wrote genesis state database=chaindata hash=248492…15aeed
INFO [11-01|23:00:31.723] Allocated cache and file handles database=/Users/siskon/ethereum/private/geth/lightchaindata cache=16 handles=16
INFO [11-01|23:00:31.725] Writing custom genesis block
INFO [11-01|23:00:31.725] Persisted trie from memory database nodes=0 size=0.00B time=3.118µs gcnodes=0 gcsize=0.00B gctime=0s livenodes=1 livesize=0.00B
INFO [11-01|23:00:31.725] Successfully wrote genesis state database=lightchaindata hash=248492…15aeed
这些信息分别表示:当前允许的最大 peer 数量,分配的缓存和文件处理者(为一个 database),写入自定义的创世区块,从内存数据库中读取持久化的 trie,成功写入了创世区块。
Geth 将在 private
目录下初始化区块链,并且以 genesis.json
作为创世区块的配置文件。之后该区块链产生的所有账号信息以及数据库信息都会被存储在该文件夹中。
Step 2: 启动区块链!
服务端,启动!
运行以下命令:
1 | geth --port 3000 --networkid 58343 --nodiscover --datadir=./private --maxpeers=1 --rpc --rpcport 8543 --rpcaddr 127.0.0.1 --rpccorsdomain "*" --rpcapi "eth,net,web3,personal,miner" |
这次也是一堆参数呢~不要慌,让我们一段段来解读:
- port: 以太坊服务所运行在的网络端口号
- networkid: 你的以太坊网络的唯一标识,你可以设置为任意值,但是不能和以太坊的保留值冲突。因而我们建议您在 100 ~ 10000 内随便选一个数比较好~
- nodiscover: 隐身!(无法被探查,只能手动添加用户)
- datadir: 区块链数据存储目录
- maxpeers: 最大网络用户数,设为 0 时代表禁用网络。
- rpc: 启动 HTTP-RPC 服务器(后面的几个参数都是在配置这个服务器)
- rpcapi: 需要使用的 RPC API,你会在稍后看到他们的身影。
如果一切正常,你会看到如下输出:
INFO [11-01|23:00:43.170] Maximum peer count ETH=0 LES=0 total=0
INFO [11-01|23:00:43.178] Starting peer-to-peer node instance=Geth/v1.8.14-stable/darwin-amd64/go1.11
INFO [11-01|23:00:43.178] Allocated cache and file handles database=/Users/siskon/ethereum/private/geth/chaindata cache=768 handles=128
INFO [11-01|23:00:43.190] Initialised chain configuration config="{ChainID: 143 Homestead: 0 DAO: <nil> DAOSupport: false EIP150: <nil> EIP155: 0 EIP158: 0 Byzantium: <nil> Constantinople: <nil> Engine: unknown}"
INFO [11-01|23:00:43.190] Disk storage enabled for ethash caches dir=/Users/siskon/ethereum/private/geth/ethash count=3
INFO [11-01|23:00:43.190] Disk storage enabled for ethash DAGs dir=/Users/siskon/.ethash count=2
INFO [11-01|23:00:43.190] Initialising Ethereum protocol versions="[63 62]" network=58343
INFO [11-01|23:00:43.190] Loaded most recent local header number=0 hash=248492…15aeed td=80
INFO [11-01|23:00:43.190] Loaded most recent local full block number=0 hash=248492…15aeed td=80
INFO [11-01|23:00:43.190] Loaded most recent local fast block number=0 hash=248492…15aeed td=80
INFO [11-01|23:00:43.192] Regenerated local transaction journal transactions=0 accounts=0
INFO [11-01|23:00:43.193] Starting P2P networking
INFO [11-01|23:00:43.195] RLPx listener up self="enode://354d42e030694bf418e58ab30bd4afaeb48a7d922e0d61cc81b4b1fe1e45e8335ed6fb9fe0611ef7369172e6c8dcda2f4c4646a3676b1cec5d5e8006112166d5@[::]:3000?discport=0"
INFO [11-01|23:00:43.198] IPC endpoint opened url=/Users/siskon/ethereum/private/geth.ipc
INFO [11-01|23:00:43.199] HTTP endpoint opened url=http://127.0.0.1:8543 cors=* vhosts=localhost
这些信息分别表示:当前最大 peer 数量,启动 P2P 节点,分配缓存和文件管理者,初始化链的配置,允许硬盘存储上的缓存和 DAG,初始化以太坊协议,加在最近的头部、全区块、快速区块,重新生成本地交易记录,开启 P2P 网络,RLPx 监听启动,IPC 和 HTTP 启动。
建立 Geth JavaScript 客户端
那么到目前为止,我们已经成功地启动了区块链服务,但是我们尚未真正地开始我们的挖矿大业(没有创建客户端,或者说矿工)。那么我们现在就开始吧!因为当前的终端窗口正在执行服务端的进程,所以我们按 Command + N 创建一个新的终端窗口,并在其中运行命令:
1 | geth attach http://127.0.0.1:8543 |
通过这个命令,我们建立了一个 Geth JavaScript 客户端,并且连接到了位于 8543 端口的以太坊 RPC-HTTP 服务器,并且启动一个 JavaScript 终端来接收命令。如果一切顺利,那么你会看到输出如下:
Welcome to the Geth JavaScript console!
instance: Geth/v1.8.14-stable/darwin-amd64/go1.11
modules: eth:1.0 miner:1.0 net:1.0 personal:1.0 rpc:1.0 web3:1.0
>
开始挖矿吧!
personal.addAccount('password')
miner.start()
我们首先通过 personal.addAccount(‘password’) 向区块链注册了一个密码为 password 的账号,然后通过调用 miner.start()
,我们就开始了挖矿。耐心等待片刻,你就可以看到服务器的状态栏里开始出现挖矿成功的提示了:
INFO [11-01|21:47:34.729] Successfully sealed new block number=1 hash=ff0b6d…f94c77 elapsed=47.040s
INFO [11-01|21:47:34.734] 🔨 mined potential block number=1 hash=ff0b6d…f94c77
如果要查看当前矿工进程挖到的以太币数量,你可以运行:
web3.fromWei(eth.getBalance(eth.coinbase), "ether")
而要停止挖矿,则运行:
miner.stop()
等等!eth.coinbase
是个啥?!因为大多数矿工可能同时拥有多个账号,而只希望将挖到的以太币存储在其中一个账号中,所以 coinbase 应运而生。它作为矿工节点的默认账号而存在,也是你通过 eth.accounts[0] 获取到的账号,同时也是你之前创建的那个账号。如果需要多个账号,继续通过 newAccount 创建即可。
既然挖到了区块,不如看看这个区块长什么样子吧!
区块字段分析
1 | { |
逐字段分析如下:
- difficulty: 当前区块的设定难度
- extraData: 区块的额外数据
- gasLimit: 区块最多允许使用的 gas 量
- gasUsed: 区块中交易使用掉的 gas 量
- hash: 区块的哈希值
- logsBloom: 当前区块的 bloom filter
- miner: 挖出该区块的矿工账号
- mixHash: 由 nonce 值得出,参与验证 PoW
- nonce: 用于验证 PoW 的随机值
- number: 区块编号
- parentHash: 上一个区块的哈希值
- receiptsRoot: 当前区块的回执的哈希根值
- sha3Uncles: 区块的 uncle 数据的 SHA3
- size: 区块大小
- stateRoot: 区块的最终状态的哈希根值
- timestamp: 时间戳
- totalDifficulty: 整个链耗费在当前区块的时间
- transactions: 区块所包含的所有交易
- transactionsRoot: 所有交易的哈希根值
- uncles: uncle 哈希的数组
进行交易吧!
为了完成一笔交易,我们得拥有复数的账号才可以。因此我们再创建一个账号:
personal.addAccount('password')
为了完成支付,我们必须先将付款账号(也就是初始的账号)解锁:
personal.unlockAccount(eth.accounts[0], 'password')
然后,我们就可以申请一笔交易啦:
eth.sendTransaction({from: eth.accounts[0], to: eth.accounts[1], value: web3.toWei(10,"ether")})
申请完成后,我们可以得到注意交易需要由矿工来确认,所以如果没在挖矿的话交易是没办法被真的写到链上的。在至少挖到了一个区块后,再查询第二个账户的余额:
> web3.fromWei(eth.getBalance(eth.accounts[1]), "ether")
10
已经到账啦~那么我们再根据之前 sendTransaction
方法返回的交易号来查询一下我们的交易吧!
> eth.getTransaction("0xf11705da1551866649f2bebca1b43516beb9a4908d1af8a3a6887bba4b7d89b9")
{
blockHash: "0x581b5d0f2e5796ffceb87e7bae4a50a53c59359ce8aaffcf0b33c892a5c67b84",
blockNumber: 10,
from: "0x0eea1b6dda7118e54b2e47763f5a15cec6fd3f59",
gas: 90000,
gasPrice: 18000000000,
hash: "0xf11705da1551866649f2bebca1b43516beb9a4908d1af8a3a6887bba4b7d89b9",
input: "0x",
nonce: 0,
r: "0x7223003484c508eb9dd33dbbe8f0d5ec9597d7bc7ee4d7ab0f7de588ebc3462b",
s: "0x26058effb045702385bc37f569f20734be7f6861df8e56c7537e595fee994263",
to: "0xac720f92b431049a8b3dac8e043bbc3d6a72166e",
transactionIndex: 0,
v: "0x141",
value: 10000000000000000000
}
其他都无需多言,需要注意的是 value 虽然是本次交易传送的以太币数量,但是由于是 Wei 作为计价单位,所以看上去会很多,实际上换算成以太币需要去掉 18 个 0!
Step 3: 部署智能合约并调用
智能合约可是区块链的精髓所在。有了智能合约,区块链就从原来冷冰冰的账本,变成了一个能够自动处理事务的银行私人管家!那么我们要如何开始我们的智能合约之旅呢?
在以太坊上,智能合约最主要的语言形式是 Solidity,我们可以通过这个语言写出我们需要的合约并将其部署在区块链上的交易中以执行和调用。那么我们要怎么将代码写到交易中呢?
事实上我们并非直接部署 Solidity 到交易中,而是通过编译器将其转换为字节码(类似于 JAVA 文件转换为 Class 文件一样),然后写到交易中由矿工代为执行。那么……我们是不是要大费周章地去新装一堆环境然后搞个编译器呢?不!自从官方推出了 Remix 在线编译器 后,我们只需在网页上动动鼠标就可以完成编译的过程了!
我们的第一个智能合约
在这里我们就不详细地讲解 Solidity 的语法了,直接给出一个可用的小程序:
1 | contract Mortal { |
这个程序就是 Hello World!。通过 Remix 编译,我们可以获取到这段代码的字节码和 ABI……不过我们甚至可以更方便,直接获取到部署的命令!不过在尝试部署之前,我们可以先通过 Remix 进行测试和调试、运行。
在线 Remix 调试
在右侧 Deploy 边栏写入 “Hello World” 之后点击按钮即可部署一个智能合约到测试用的区块链上:
然后下方的控制台会显示合约所部署到的交易:
复制 transaction hash 到 Debugger 下,我们就可以进行编译生成类汇编语言的 Debug 了:
当然,我们也可以通过点击右侧区块中的函数名来快速执行我们的合约并查看执行结果:
部署智能合约
说回部署的事情,将 Remix 所生成的 JS 代码复制下来:
1 | var _greeting = 'Hello World!' ; |
把这段代码写入客户端终端中,然后令客户端继续挖矿,并在挖到至少一个矿后停止。我们就可以调用我们部署的代码了!由于 Remix 帮我们设置了一个部署完成后通知的回调函数,我们甚至可以看到:
> null [object Object]
Contract mined! address: 0xa84ea2e3143ee1d5c402e2c99cc8cfe560aba926 transactionHash: 0x2709ca5ecd874090be80fba698db565a454d02ad1f092752ac4916c6b22f28ba
然后我们可以尝试调用:
> greeter.greet()
"Hello World!"
大功告成~
交易字段分析
我们注意到,智能合约实际上是部署在交易上的。那么在部署之后,交易的字段变成什么样子了呢?不如我们通过交易号看一看吧。
1 | { |
逐字段分析如下:
- blockHash: 交易所属区块的哈希值
- blockNumber: 交易所属区块的编号
- from: 交易发起者
- gas: 交易发起者为本交易支出的 gas 量,多余的会返还给发起者
- gasPrice: 交易发起者为每个 gas 付出的 Wei 量
- hash: 交易的哈希值
- input: 包含 data 的字节码
- r|s|v: 用于校验 ECDSA 签名
- nonce: 该交易是发起者的第几笔交易
- to: 以太币接收方,为 null 表示这个交易用于部署智能合约
- value: 交易的以太币个数,为 0 表示这个交易用于部署智能合约
Step 4: 多节点的私有链
多节点环境搭建
为了统一环境,我们把之前的 private
文件夹直接删除吧~新建两个文件夹:node1
和 node2
。然后我们开启两个终端,分别在两个文件夹内,以相同的网络 id 和不同的端口号创建区块链节点,并以控制台模式启动节点:
1 | # Node1 |
1 | # Node2 |
然后我们在第一个节点的控制台中运行以下命令获得节点信息:
1 | > admin.nodeInfo |
注意到 enode
这个字段了吗,它是一个可以标识该节点的协议链接。将他复制,在第二个节点的终端中运行如下命令,参数就是第一个节点的 enode
:
1 | > admin.addPeer("enode://e7c783dee74c2c83e9f8242b511f0c56d218822bd0ed65380232da0f8fb13bdbff1b54122d75d689fd0c8cac8aed9ae6042cdde161212a6753e36b586a0db123@[::]:30123?discport=0") |
之后,我们回到第一个节点,运行如下命令,可以看到两个节点已经建立了连接:
1 | > admin.peers |
节点间交易
接下来,我们尝试在两个节点间进行交易。首先,我们需要在两个节点上分别创建账号,然后在第一个节点上开始挖矿,来获取交易所需的资金。
1 | > personal.newAccount('password') |
如上所示,我们已经得到了 50 个以太币。那么接下来我们尝试将其中的 10 个转账给另一个节点上的账户吧!
1 | personal.unlockAccount(eth.accounts[0], 'password') |
注意,这里的 to
字段是第二个节点的账号哈希,可以通过在第二个终端上执行 eth.account[0]
获取到。然后我们可以在第二个终端上运行如下命令看到我们转账的结果:
1 | > web3.fromWei(eth.getBalance(eth.accounts[0]), "ether") |
至此,多节点环境的搭建和交易的执行完毕。