# 前言
本版本区块链不会实现一个P2P网络,但会实现基于网络的、bitcoin的一个最常见的场景:
1. 中心节点创建一个区块链。
2. 一个其他(钱包)节点连接到中心节点并下载区块链。
3. 另一个(矿工)节点连接到中心节点并下载区块链。
4. 钱包节点创建一笔交易。
5. 矿工节点接收交易,并将交易保存到内存池中。
6. 当内存池中有足够的交易时,矿工开始挖一个新块。
7. 当挖出一个新块后,将其发送到中心节点。
8. 钱包节点与中心节点进行同步。
9. 钱包节点的用户检查他们的支付是否成功。
这就是比特币中的一般流程,也是比特币最常见最重要的用户场景。
# 节点角色
尽管节点具有完备成熟的属性,但是它们也可以在网络中扮演不同角色。比如:
1. 矿工 这样的节点运行于强大或专用的硬件(比如 ASIC)之上,它们唯一的目标是,尽可能快地挖出新块。矿工是区块链中唯一可能会用到工作量证明的角色,因为挖矿实际上意味着解决 PoW 难题。在权益证明 PoS 的区块链中,没有挖矿。
2. 全节点 这些节点验证矿工挖出来的块的有效性,并对交易进行确认。为此,他们必须拥有区块链的完整拷贝。同时,全节点执行路由操作,帮助其他节点发现彼此。对于网络来说,非常重要的一段就是要有足够多的全节点。因为正是这些节点执行了决策功能:他们决定了一个块或一笔交易的有效性。
3. SPV SPV 表示 Simplified Payment Verification,简单支付验证。这些节点并不存储整个区块链副本,但是仍然能够对交易进行验证(不过不是验证全部交易,而是一个交易子集,比如,发送到某个指定地址的交易)。一个 SPV 节点依赖一个全节点来获取数据,可能有多个 SPV 节点连接到一个全节点。SPV 使得钱包应用成为可能:一个人不需要下载整个区块链,但是仍能够验证他的交易。
# 网络简化
我们将使用**端口号作为节点标识符**,而不是使用 IP 地址,比如将会有这样地址的节点:**127.0.0.1:3000**,**127.0.0.1:3001**,**127.0.0.1:3002**等等。我们叫它端口节点(port node) ID,并使用环境变量`NODE_ID`对它们进行设置。故而,你可以打开多个终端窗口,设置不同的`NODE_ID`运行不同的节点。
这个方法也需要有不同的区块链和钱包文件。它们现在必须依赖于节点 ID 进行命名,比如 blockchain_3000.db, blockchain_30001.db和wallet_3000.db, wallet_30001.db 等等。
# 实现
在 Bitcoin Core 中,硬编码了[DNS seeds](https://link.jianshu.com/?t=https://bitcoin.org/en/glossary/dns-seed)。虽然这些并不是节点,但是 DNS 服务器知道一些节点的地址。当你启动一个全新的 Bitcoin Core 时,它会连接到一个种子节点,获取全节点列表,随后从这些节点中下载区块链。
不过在我们目前的实现中,无法做到完全的去中心化,因为会出现中心化的特点。我们会有三个节点:
1. 一个中心节点。所有其他节点都会连接到这个节点,这个节点会在其他节点之间发送数据。
2. 一个矿工节点。这个节点会在内存池中存储新的交易,当有足够的交易时,它就会打包挖出一个新块。
3. 一个钱包节点。这个节点会被用作在钱包之间发送币。但是与 SPV 节点不同,它存储了区块链的一个完整副本。
节点通过消息(message)进行交流:整个通讯就是:请求--回复--请求--回复...这么一个模式进行,第一个请求可能需要经过请求节点与被请求节点之间多次往返消息(一般为多个成对消息)才能完成。
当一个新的节点开始运行时,它会从一个 DNS 种子获取几个节点,给它们发送`version`消息,在我们的实现看起来就像是这样:
~~~
type version struct {
Version int//区块链版本号
BestHeight int//存储区块链中节点的高度。
AddrFrom string//存储发送消息者的地址
}
~~~
接收到`version`消息的节点应该做什么呢?它会响应自己的`version`消息。这是一种握手🤝:如果没有事先互相问候,就不可能有其他交流。`version`用于找到一个更长的区块链。当一个节点接收到`version`消息,它会检查本节点的区块链是否比`BestHeight`的值更大。如果不是,节点就会请求并下载缺失的块。
为了接收消息,我们需要一个服务器(每个节点都是服务器):
~~~
var nodeAddress string
var knownNodes = []string{"localhost:3000"}//中心节点的地址数组
func StartServer(nodeID, minerAddress string) {//nodeID为当前节点
nodeAddress = fmt.Sprintf("localhost:%s", nodeID)
miningAddress = minerAddress//接收挖矿奖励的地址
ln, err := net.Listen(protocol, nodeAddress)//当前节点开启监听
defer ln.Close()
bc := NewBlockchain(nodeID)//将区块链读取到内存
if nodeAddress != knownNodes[0] {//如果当前节点不是中心节点
sendVersion(knownNodes[0], bc)//非中心节点必须向中心节点发送`version`消息来查询是否自己的区块链已过时
}
//优雅的Go语言
for {//开启无限循环
conn, err := ln.Accept()//等待连接
go handleConnection(conn, bc)//异步,处理连接
}
}
~~~
非中心节点向中心节点发送消息,查询自己的区块链是否已经过时:
~~~
func sendVersion(addr string, bc *Blockchain) {
bestHeight := bc.GetBestHeight()
payload := gobEncode(version{nodeVersion, bestHeight, nodeAddress})
request := append(commandToBytes("version"), payload...)//“version”是命令,payload是gob编码的消息结构
sendData(addr, request)
}
~~~
`commandToBytes`看起来是这样:
~~~
//创建一个 12 字节的缓冲区,并用命令名进行填充,将剩下的字节置为空
func commandToBytes(command string) []byte {//命令字符串转为数组切片
var bytes [commandLength]byte//commandLength为12个字节
for i, c := range command {
bytes[i] = byte(c)
}
return bytes[:]//返回切片
}
~~~
解析命令的函数如下:
~~~
func bytesToCommand(bytes []byte) string {
var command []byte
for _, b := range bytes {
if b != 0x0 {
command = append(command, b)
}
}
return fmt.Sprintf("%s", command)
}
~~~
当一个节点接收到一个命令,它会运行`bytesToCommand`来提取命令名,并选择正确的处理器处理命令主体:
~~~
func handleConnection(conn net.Conn, bc *Blockchain) {
request, err := ioutil.ReadAll(conn)
command := bytesToCommand(request[:commandLength])
fmt.Printf("接收到 %s 命令\n", command)
switch command {
...
case "version":
handleVersion(request, bc)
default:
fmt.Println("Unknown command!")
}
conn.Close()
}
~~~
下面是version命令的处理函数:
~~~
func handleVersion(request []byte, bc *Blockchain) {
var buff bytes.Buffer
var payload verzion
buff.Write(request[commandLength:])
dec := gob.NewDecoder(&buff)
err := dec.Decode(&payload)
myBestHeight := bc.GetBestHeight()//本地区块链高度
foreignerBestHeight := payload.BestHeight//收到的区块链高度
if myBestHeight < foreignerBestHeight {//本地区块链高度小于收到的区块链高度
sendGetBlocks(payload.AddrFrom)//发送getblocks消息
} else if myBestHeight > foreignerBestHeight {
sendVersion(payload.AddrFrom, bc)//回复version消息
}
if !nodeIsKnown(payload.AddrFrom) {//如果中心节点不包含本地地址
knownNodes = append(knownNodes, payload.AddrFrom)//将本地地址加入到中心节点地址数组中
}
}
~~~
首先,我们需要对请求进行解码,提取有效信息。所有的处理器在这部分都类似。
然后节点将从消息中提取的`BestHeight`与自身进行比较。如果自身节点的区块链更长,它会回复`version`消息;否则,它会发送`getblocks`消息。
## getblocks
~~~
type getblocks struct {
AddrFrom string
}
~~~
`getblocks`意为 “给我看一下你有什么区块”(在比特币中,这会更加复杂)。注意,它并没有说“把你全部的区块给我”,而是请求了一个块哈希的列表。这是为了减轻网络负载,因为区块可以从不同的节点下载,并且我们不想从一个单一节点下载数十 GB 的数据。
处理命令十分简单:
~~~
func handleGetBlocks(request []byte, bc *Blockchain) {
...
blocks := bc.GetBlockHashes()//接收到getblocks命令的节点获得本地的区块哈希列表
sendInv(payload.AddrFrom, "block", blocks)//然后将本地的区块哈希列表回复给请求者
}
~~~
在我们简化版的实现中,它会返回**所有块哈希**。
## inv
~~~
type inv struct {
AddrFrom string
Type string//payload的类型:block或tx
Items [][]byte//当前节点的所有块的哈希或交易的哈希
}
~~~
比特币使用`inv`来向其他节点展示当前节点有什么块和交易。再次提醒,它没有包含完整的区块链和交易,仅仅是哈希而已。`Type`字段表明了这是块还是交易。
处理`inv`稍显复杂:
~~~
func handleInv(request []byte, bc *Blockchain) {//请求区块者收到回复节点返回的blocks后进行处理
...
fmt.Printf("Recevied inventory with %d %s\n", len(payload.Items), payload.Type)
if payload.Type == "block" {//块哈希
blocksInTransit = payload.Items
blockHash := payload.Items[0]
//给回复节点发送getdata命令,请求该块:哦,你有这么一个块,麻烦给我
//注意,每次只能请求一个block,所以这次请求的是payload里面的第0个
sendGetData(payload.AddrFrom, "block", blockHash)
newInTransit := [][]byte{}
for _, b := range blocksInTransit {//迭代收到的区块哈希列表
//如果b不等于收到的第0个哈希值(第0个块在上面getdata命令中已经请求下载),加入到新的newInTransit(尚未下载该块)
if bytes.Compare(b, blockHash) != 0 {//第0个已经请求下载了啦,所以需要排除,下次不要再请求其他节点下载
newInTransit = append(newInTransit, b)
}
}
//更新blocksInTransit,blocksInTransit用来跟踪已下载的块,以后会根据此表请求下载其他的区块,直到全部下载完毕
blocksInTransit = newInTransit
}
if payload.Type == "tx" {
txID := payload.Items[0]
if mempool[hex.EncodeToString(txID)].ID == nil {//如果本地内存池中不存在这么一个交易
sendGetData(payload.AddrFrom, "tx", txID)//收到回复节点返回的tx,请求该交易:哦,你有这么一个交易,麻烦给我
}
}
}
~~~
## getdata
~~~
type getdata struct {
AddrFrom string
Type string
ID []byte
}
~~~
`getdata`用于某个块或交易的请求,它可以仅包含一个块或交易的 ID。
~~~
func handleGetData(request []byte, bc *Blockchain) {
...
if payload.Type == "block" {
block, err := bc.GetBlock([]byte(payload.ID))
sendBlock(payload.AddrFrom, &block)
}
if payload.Type == "tx" {
txID := hex.EncodeToString(payload.ID)
tx := mempool[txID]//从本地内存池中取这个交易
sendTx(payload.AddrFrom, &tx)
}
}
~~~
这个处理器比较地直观:如果它们请求一个块,则返回块;如果它们请求一笔交易,则返回交易。注意,我们并没有检查实际上是否已经有了这个块或交易。这是一个缺陷 :),需要修复。
## block 和 tx
~~~
type block struct {
AddrFrom string
Block []byte
}
type tx struct {
AddFrom string
Transaction []byte
}
~~~
实际完成数据转移的正是这些消息。
处理`block`消息十分简单:
~~~
func handleBlock(request []byte, bc *Blockchain) {
...
blockData := payload.Block
block := DeserializeBlock(blockData)
fmt.Println("Recevied a new block!")
bc.AddBlock(block)//加入下载的区块到本地区块链
fmt.Printf("Added block %x\n", block.Hash)
if len(blocksInTransit) > 0 {//迭代需要下载的区块哈希列表
blockHash := blocksInTransit[0]
sendGetData(payload.AddrFrom, "block", blockHash)//这里是从回复哈希列表的节点处,请求其一一给下区块
blocksInTransit = blocksInTransit[1:]//排除第一个,更新待下载区块列表
} else {//区块下载完毕
UTXOSet := UTXOSet{bc}
UTXOSet.Reindex()//最好不要调用Reindex,而是迭代下载的blocks,使用 UTXOSet.Update(block)更新数据库的UTXO表
}
}
~~~
当接收到一个新块时,我们把它放到区块链里面。如果还有更多的区块需要下载,我们继续从上一个下载的块的那个节点继续请求。当最后把所有块都下载完后,对 UTXO 集进行重新索引。
> TODO:并非无条件信任,我们应该在将每个块加入到区块链之前对它们进行验证。
> TODO: 并非运行 UTXOSet.Reindex(), 而是应该使用 UTXOSet.Update(block),因为如果区块链很大,它将需要很多时间来对整个 UTXO 集重新索引。
处理`tx`消息是最困难的部分:
~~~
func handleTx(request []byte, bc *Blockchain) {
...
txData := payload.Transaction
tx := DeserializeTransaction(txData)
mempool[hex.EncodeToString(tx.ID)] = tx//将收到的新交易放到交易内存池之中(这里最好在放到内存池之前,对交易进行验证)
if nodeAddress == knownNodes[0] {//如果当前节点是中心节点(这里中心节点不挖矿)
for _, node := range knownNodes {
if node != nodeAddress && node != payload.AddFrom {//本地节点和发送此交易的节点已经有了此交易
sendInv(node, "tx", [][]byte{tx.ID})//告知其他所有节点,中心节点处新增了此交易
}
}
} else {
if len(mempool) >= 2 && len(miningAddress) > 0 {//如果交易池中的交易大于或者等于2笔,并且当前是矿工节点
MineTransactions:
var txs []*Transaction
for id := range mempool {
tx := mempool[id]
if bc.VerifyTransaction(&tx) {//首先验证交易
txs = append(txs, &tx)//加入到待挖矿的交易列表中
}
}
if len(txs) == 0 {
fmt.Println("所有交易均非法,正在等待新的交易到来...")
return
}
cbTx := NewCoinbaseTX(miningAddress, "")//coninbase交易,挖矿奖励
txs = append(txs, cbTx)
newBlock := bc.MineBlock(txs)//挖矿
UTXOSet := UTXOSet{bc}
UTXOSet.Reindex()//最好不要重新索引,而是执行update
fmt.Println("已挖到新区块!")
for _, tx := range txs {
txID := hex.EncodeToString(tx.ID)
delete(mempool, txID)//从内存池中清除已完成挖矿的交易
}
for _, node := range knownNodes {
if node != nodeAddress {
sendInv(node, "block", [][]byte{newBlock.Hash})//告知其他节点,已经有了新的区块
}
}
if len(mempool) > 0 {//如果交易池还存在未处理交易,继续处理
goto MineTransactions
}
}
}
}
~~~
# 场景检验
## 创建中心节点
在第一个终端窗口中将`NODE_ID`设置为 3000,作为中心节点,3001为钱包节点,3002为矿工节点:
让我们来回顾一下上面定义的场景。
首先,在第一个终端窗口中将`NODE_ID`设置为 3000(`export NODE_ID=3000`)。为了让你知道什么节点执行什么操作,我会使用像**NODE 3000**或**NODE 3001**进行标识。
### NODE 3000
创建一个钱包和一个新的区块链:
~~~
$ main createblockchain -address CENTREAL_NODE
~~~
(为了简洁起见,我会使用假地址。)
然后,会生成一个仅包含创世块的区块链。我们需要保存块,并在其他节点使用。创世块承担了一条链标识符的角色(在 Bitcoin Core 中,创世块是硬编码的)
~~~
$ cp blockchain_3000.db blockchain_genesis.db
~~~
### NODE 3001
接下来,打开一个新的终端窗口,将 node ID 设置为 3001。这会作为一个钱包节点。通过`blockchain_go createwallet`生成一些地址,我们把这些地址叫做 WALLET\_1, WALLET\_2, WALLET\_3.
### NODE 3000
向钱包地址发送一些币:
~~~
$ main send -from CENTREAL_NODE -to WALLET_1 -amount 10 -mine
$ main send -from CENTREAL_NODE -to WALLET_2 -amount 10 -mine
~~~
`-mine`标志指的是块会立刻被同一节点挖出来。我们必须要有这个标志,因为初始状态时,网络中没有矿工节点。
启动节点:
~~~
$ main startnode
~~~
这个节点会持续运行,直到本文定义的场景结束。
### NODE 3001
启动上面保存创世块节点的区块链:
~~~
$ cp blockchain_genesis.db blockchain_3001.db
~~~
运行节点:
~~~
$ main startnode
~~~
它会从中心节点下载所有区块。为了检查一切正常,暂停节点运行并检查余额:
~~~
$ main getbalance -address WALLET_1
Balance of 'WALLET_1': 10
$ main getbalance -address WALLET_2
Balance of 'WALLET_2': 10
~~~
你还可以检查`CENTRAL_NODE`地址的余额,因为 node 3001 现在有它自己的区块链:
~~~
$ main getbalance -address CENTRAL_NODE
Balance of 'CENTRAL_NODE': 10
~~~
### NODE 3002
打开一个新的终端窗口,将它的 ID 设置为 3002,然后生成一个钱包。这会是一个矿工节点。初始化区块链:
~~~
$ cp blockchain_genesis.db blockchain_3002.db
~~~
启动节点:
~~~
$ main startnode -miner MINER_WALLET
~~~
### NODE 3001
发送一些币:
~~~
$ main send -from WALLET_1 -to WALLET_3 -amount 1
$ main send -from WALLET_2 -to WALLET_4 -amount 1
~~~
### NODE 3002
迅速切换到矿工节点,你会看到挖出了一个新块!同时,检查中心节点的输出。
### NODE 3001
切换到钱包节点并启动:
~~~
$ main startnode
~~~
它会下载最近挖出来的块!
![](https://img.kancloud.cn/bb/4b/bb4bc4f4ca70376fef356427bf6e5f8c_2130x1544.png)
- 重要更新说明
- linechain发布
- linechain新版设计
- 引言一
- 引言二
- 引言三
- vs-code设置及开发环境设置
- BoltDB数据库应用
- 关于Go语言、VS-code的一些Tips
- 区块链的架构
- 网络通信与区块链
- 单元测试
- 比特币脚本语言
- 关于区块链的一些概念
- 区块链组件
- 区块链第一版:基本原型
- 区块链第二版:增加工作量证明
- 区块链第三版:持久化
- 区块链第四版:交易
- 区块链第五版:实现钱包
- 区块链第六版:实现UTXO集
- 区块链第七版:网络
- 阶段小结
- 区块链第八版:P2P
- P2P网络架构
- 区块链网络层
- P2P区块链最简体验
- libp2p建立P2P网络的关键概念
- 区块链结构层设计与实现
- 用户交互层设计与实现
- 网络层设计与实现
- 建立节点发现机制
- 向区块链网络请求区块信息
- 向区块链网络发布消息
- 运行区块链
- LineChain
- 系统运行流程
- Multihash
- 区块链网络的节点发现机制深入探讨
- DHT
- Bootstrap
- 连接到所有引导节点
- Advertise
- 搜索其它peers
- 连接到搜到的其它peers
- 区块链网络的消息订发布-订阅机制深入探讨
- LineChain:适用于智能合约编程的脚本语言支持
- LineChain:解决分叉问题
- LineChain:多重签名
- libp2p升级到v0.22版本
- 以太坊基础
- 重温以太坊的树结构
- 世界状态树
- (智能合约)账户存储树
- 交易树
- 交易收据树
- 小结
- 以太坊的存储结构
- 以太坊状态数据库
- MPT
- 以太坊POW共识算法
- 智能合约存储
- Polygon Edge
- block结构
- transaction数据结构
- 数据结构小结
- 关于本区块链的一些说明
- UML工具-PlantUML
- libp2p介绍
- JSON-RPC
- docker制作:启动多个应用系统
- Dockerfile
- docker-entrypoint.sh
- supervisord.conf
- docker run
- nginx.conf
- docker基础操作整理
- jupyter计算交互环境
- git技巧一
- git技巧二
- 使用github项目的最佳实践
- windows下package管理工具