8btm.com-新币圈
8btm.com-新币圈
8btm.com-新币圈
8btm.com-新币圈
8btm.com-新币圈
8btm.com-新币圈作者: Ivan Kuznetsov 吴寿鹤等8btm.com-新币圈
8btm.com-新币圈著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
8btm.com-新币圈
8btm.com-新币圈8btm.com-新币圈
8btm.com-新币圈本文的目标是实现如下场景:
8btm.com-新币圈
8btm.com-新币圈
8btm.com-新币圈
8btm.com-新币圈- 中心节点创建一个区块链。
8btm.com-新币圈
8btm.com-新币圈 - 一个其他(钱包)节点连接到中心节点并下载区块链。
8btm.com-新币圈
8btm.com-新币圈 - 另一个(矿工)节点连接到中心节点并下载区块链。
8btm.com-新币圈
8btm.com-新币圈 - 钱包节点创建一笔交易。
8btm.com-新币圈
8btm.com-新币圈 - 矿工节点接收交易,并将交易保存到内存池中。
8btm.com-新币圈
8btm.com-新币圈 - 当内存池中有足够的交易时,矿工开始挖一个新块。
8btm.com-新币圈
8btm.com-新币圈 - 当挖出一个新块后,将其发送到中心节点。
8btm.com-新币圈
8btm.com-新币圈 - 钱包节点与中心节点进行同步。
8btm.com-新币圈
8btm.com-新币圈 - 钱包节点的用户检查他们的支付是否成功。
8btm.com-新币圈
8btm.com-新币圈
这就是比特币中的一般流程。尽管我们不会实现一个真实的 P2P 网络,但是我们会实现一个真是,也是比特币最常见最重要的用户场景。
8btm.com-新币圈
8btm.com-新币圈版本
8btm.com-新币圈
8btm.com-新币圈节点通过消息(message)进行交流。当一个新的节点开始运行时,它会从一个 DNS 种子获取几个节点,给它们发送 version 消息,在我们的实现看起来就像是这样:
8btm.com-新币圈
8btm.com-新币圈type version
struct { Version int BestHeight int AddrFrom string
}由于我们仅有一个区块链版本,所以 Version 字段实际并不会存储什么重要信息。BestHeight 存储区块链中节点的高度。AddFrom 存储发送者的地址。
8btm.com-新币圈
8btm.com-新币圈接收到 version 消息的节点应该做什么呢?它会响应自己的 version 消息。这是一种握手:如果没有事先互相问候,就不可能有其他交流。不过,这并不是处于礼貌:version 用于找到一个更长的区块链。当一个节点接收到 version 消息,它会检查本节点的区块链是否比 BestHeight 的值更大。如果不是,节点就会请求并下载缺失的块。
8btm.com-新币圈
8btm.com-新币圈为了接收消息,我们需要一个服务器:
8btm.com-新币圈
8btm.com-新币圈var nodeAddress string
var knownNodes
= []string
{"localhost:3000"}func StartServer
(nodeID
, minerAddress string
) { 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
) } for { conn
, err
:= ln
.Accept
() go handleConnection
(conn
, bc
) }}首先,我们对中心节点的地址进行硬编码:因为每个节点必须知道从何处开始初始化。minerAddress 参数指定了接收挖矿奖励的地址。代码片段:
8btm.com-新币圈
8btm.com-新币圈if nodeAddress
!= knownNodes
[0] { sendVersion
(knownNodes
[0], bc
)}这意味着如果当前节点不是中心节点,它必须向中心节点发送 version 消息来查询是否自己的区块链已过时。8btm.com-新币圈
8btm.com-新币圈func sendVersion
(addr string
, bc
*Blockchain
) { bestHeight
:= bc
.GetBestHeight
() payload
:= gobEncode
(version
{nodeVersion
, bestHeight
, nodeAddress
}) request
:= append
(commandToBytes
("version"), payload
...) sendData
(addr
, request
)}我们的消息,在底层就是字节序列。前 12 个字节指定了命令名(比如这里的 version),后面的字节会包含
gob 编码的消息结构,commandToBytes 看起来是这样:
8btm.com-新币圈
8btm.com-新币圈func commandToBytes
(command string
) []byte
{ var bytes
[commandLength
]byte
for i
, c
:= range command
{ bytes
[i
] = byte
(c
) } return bytes
[:]}它创建一个 12 字节的缓冲区,并用命令名进行填充,将剩下的字节置为空。下面一个相反的函数:
8btm.com-新币圈
8btm.com-新币圈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 来提取命令名,并选择正确的处理器处理命令主体:
8btm.com-新币圈
8btm.com-新币圈func handleConnection
(conn net
.Conn
, bc
*Blockchain
) { request
, err
:= ioutil
.ReadAll
(conn
) command
:= bytesToCommand
(request
[:commandLength
]) fmt
.Printf
("Received %s commandn", command
) switch command
{ ... case "version": handleVersion
(request
, bc
) default: fmt
.Println
("Unknown command!") } conn
.Close
()}下面是 version 命令处理器:
8btm.com-新币圈
8btm.com-新币圈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
</span foreignerBestHeight span style="font-size:inherit;color:rgb(94,102,135);"{/span span style="font-size:inherit;"sendGetBlocks/spanspan style="font-size:inherit;color:rgb(94,102,135);"(/spanpayloadspan style="font-size:inherit;color:rgb(94,102,135);"./spanAddrFromspan style="font-size:inherit;color:rgb(94,102,135);")/span span style="font-size:inherit;color:rgb(94,102,135);"}/span span style="font-size:inherit;color:rgb(172,151,57);"else/span span style="font-size:inherit;color:rgb(172,151,57);"if/span myBestHeight span style="font-size:inherit;color:rgb(199,107,41);"> foreignerBestHeight
{ sendVersion
(payload
.AddrFrom
, bc
) } if !nodeIsKnown
(payload
.AddrFrom
) { knownNodes
= append
(knownNodes
, payload
.AddrFrom
) }}首先,我们需要对请求进行解码,提取有效信息。所有的处理器在这部分都类似,所以我们会下面的代码片段中略去这部分。
8btm.com-新币圈
8btm.com-新币圈然后节点将从消息中提取的 BestHeight 与自身进行比较。如果自身节点的区块链更长,它会回复 version 消息;否则,它会发送 getblocks 消息。
8btm.com-新币圈
8btm.com-新币圈getblocks
8btm.com-新币圈
8btm.com-新币圈type getblocks
struct { AddrFrom string
}getblocks 意为 “给我看一下你有什么区块”(在比特币中,这会更加复杂)。注意,它并没有说“把你全部的区块给我”,而是请求了一个块哈希的列表。这是为了减轻网络负载,因为区块可以从不同的节点下载,并且我们不想从一个单一节点下载数十 GB 的数据。
8btm.com-新币圈
8btm.com-新币圈处理命令十分简单:
8btm.com-新币圈
8btm.com-新币圈func handleGetBlocks
(request
[]byte
, bc
*Blockchain
) { ... blocks
:= bc
.GetBlockHashes
() sendInv
(payload
.AddrFrom
, "block", blocks
)}在我们简化版的实现中,它会返回
所有块哈希。
8btm.com-新币圈
8btm.com-新币圈inv
8btm.com-新币圈
8btm.com-新币圈type inv
struct { AddrFrom string Type string Items
[][]byte
}比特币使用 inv 来向其他节点展示当前节点有什么块和交易。再次提醒,它没有包含完整的区块链和交易,仅仅是哈希而已。Type 字段表明了这是块还是交易。
8btm.com-新币圈
8btm.com-新币圈处理 inv 稍显复杂:
8btm.com-新币圈
8btm.com-新币圈func handleInv
(request
[]byte
, bc
*Blockchain
) { ... fmt
.Printf
("Recevied inventory with %d %sn", len
(payload
.Items
), payload
.Type
) if payload
.Type
== "block" { blocksInTransit
= payload
.Items blockHash
:= payload
.Items
[0] sendGetData
(payload
.AddrFrom
, "block", blockHash
) newInTransit
:= [][]byte
{} for _, b
:= range blocksInTransit
{ if bytes
.Compare
(b
, blockHash
) != 0 { newInTransit
= append
(newInTransit
, b
) } } blocksInTransit
= newInTransit
} if payload
.Type
== "tx" { txID
:= payload
.Items
[0] if mempool
[hex
.EncodeToString
(txID
)].ID
== nil { sendGetData
(payload
.AddrFrom
, "tx", txID
) } }}如果收到块哈希,我们想要将它们保存在 blocksInTransit 变量来跟踪已下载的块。这能够让我们从不同的节点下载块。在将块置于传送状态时,我们给 inv 消息的发送者发送 getdata 命令并更新 blocksInTransit。在一个真实的 P2P 网络中,我们会想要从不同节点来传送块。
8btm.com-新币圈
8btm.com-新币圈在我们的实现中,我们永远也不会发送有多重哈希的 inv。这就是为什么当 payload.Type == "tx"时,只会拿到第一个哈希。然后我们检查是否在内存池中已经有了这个哈希,如果没有,发送 getdata 消息。
8btm.com-新币圈
8btm.com-新币圈getdata
8btm.com-新币圈
8btm.com-新币圈type getdata
struct { AddrFrom string Type string ID
[]byte
}getdata 用于某个块或交易的请求,它可以仅包含一个块或交易的 ID。
8btm.com-新币圈
8btm.com-新币圈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
) }}这个处理器比较地直观:如果它们请求一个块,则返回块;如果它们请求一笔交易,则返回交易。注意,我们并不检查实际上是否已经有了这个块或交易。这是一个缺陷 :)
8btm.com-新币圈
8btm.com-新币圈block 和 tx
8btm.com-新币圈
8btm.com-新币圈type block
struct { AddrFrom string Block
[]byte
}type tx
struct { AddFrom string Transaction
[]byte
}实际完成数据转移的正是这些消息。
8btm.com-新币圈
8btm.com-新币圈处理 block 消息十分简单:
8btm.com-新币圈
8btm.com-新币圈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 %xn", block
.Hash
) if len
(blocksInTransit
) > 0 { blockHash
:= blocksInTransit
[0] sendGetData
(payload
.AddrFrom
, "block", blockHash
) blocksInTransit
= blocksInTransit
[1:] } else { UTXOSet
:= UTXOSet
{bc
} UTXOSet
.Reindex
() }}当接收到一个新块时,我们把它放到区块链里面。如果还有更多的区块需要下载,我们继续从上一个下载的块的那个节点继续请求。当最后把所有块都下载完后,对 UTXO 集进行重新索引。
8btm.com-新币圈
8btm.com-新币圈TODO:并非无条件信任,我们应该在将每个块加入到区块链之前对它们进行验证。
8btm.com-新币圈
8btm.com-新币圈TODO: 并非运行 UTXOSet.Reindex(), 而是应该使用 UTXOSet.Update(block),因为如果区块链很大,它将需要很多时间来对整个 UTXO 集重新索引。
8btm.com-新币圈
8btm.com-新币圈处理 tx 消息是最困难的部分:
8btm.com-新币圈
8btm.com-新币圈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 { 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
("All transactions are invalid! Waiting for new ones...") return } cbTx
:= NewCoinbaseTX
(miningAddress
, "") txs
= append
(txs
, cbTx
) newBlock
:= bc
.MineBlock
(txs
) UTXOSet
:= UTXOSet
{bc
} UTXOSet
.Reindex
() fmt
.Println
("New block is mined!") 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
} } }}未完待续……
8btm.com-新币圈
8btm.com-新币圈上一篇:我的区块链技术学习笔记(十九):认识区块链网络
8btm.com-新币圈
8btm.com-新币圈下一篇:我的区块链技术学习笔记(二十):比特币场景化实践(2)
8btm.com-新币圈
8btm.com-新币圈8btm.com-新币圈
8btm.com-新币圈
8btm.com-新币圈
8btm.com-新币圈
8btm.com-新币圈
8btm.com-新币圈
8btm.com-新币圈
8btm.com-新币圈