我们继续区块链网络下部分内容,不知道大家对上半部分理解如何?
getblocks
type getblocks struct {
AddrFrom string}
意为 “给我看一下你有什么区块”(在比特币中,这会更加复杂)。注意,它并没有说“把你全部的区块给我”,而是请求了一个块哈希的列表。这是为了减轻网络负载,因为区块可以从不同的节点下载,并且我们不想从一个单一节点下载数十 GB 的数据。
处理命令十分简单:
func handleGetBlocks(request []byte, bc *Blockchain) {
...
blocks := bc.GetBlockHashes()
sendInv(payload.AddrFrom, "block", blocks)}
在我们简化版的实现中,它会返回所有块哈希。
inv
type inv struct {
AddrFrom string
Type string
Items [][]byte}
比特币使用来向其他节点展示当前节点有什么块和交易。再次提醒,它没有包含完整的区块链和交易,仅仅是哈希而已。字段表明了这是块还是交易。
处理稍显复杂:
func handleInv(request []byte, bc *Blockchain) {
...
fmt.Printf("Recevied inventory with %d %s\n", 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)
}
}}
如果收到块哈希,我们想要将它们保存在变量来跟踪已下载的块。这能够让我们从不同的节点下载块。在将块置于传送状态时,我们给消息的发送者发送命令并更新。在一个真实的 P2P 网络中,我们会想要从不同节点来传送块。
在我们的实现中,我们永远也不会发送有多重哈希的。这就是为什么当时,只会拿到第一个哈希。然后我们检查是否在内存池中已经有了这个哈希,如果没有,发送消息。
getdata
type getdata struct {
AddrFrom string
Type string
ID []byte}
用于某个块或交易的请求,它可以仅包含一个块或交易的 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}
实际完成数据转移的正是这些消息。
处理消息十分简单:
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
UTXOSet.Reindex()
}}
当接收到一个新块时,我们把它放到区块链里面。如果还有更多的区块需要下载,我们继续从上一个下载的块的那个节点继续请求。当最后把所有块都下载完后,对 UTXO 集进行重新索引。
TODO:并非无条件信任,我们应该在将每个块加入到区块链之前对它们进行验证。
TODO: 并非运行 UTXOSet.Reindex(), 而是应该使用 UTXOSet.Update(block),因为如果区块链很大,它将需要很多时间来对整个 UTXO 集重新索引。
处理消息是最困难的部分:
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)
}
}
} 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
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)
}
}
if len(mempool) > 0 {
goto MineTransactions }
}
}}
首先要做的事情是将新交易放到内存池中(再次提醒,在将交易放到内存池之前,必要对其进行验证)。下个片段:
if nodeAddress == knownNodes[0] {
for _, node := range knownNodes {
if node != nodeAddress && node != payload.AddFrom {
sendInv(node, "tx", [][]byte)
}
}}
检查当前节点是否是中心节点。在我们的实现中,中心节点并不会挖矿。它只会将新的交易推送给网络中的其他节点。
下一个很大的代码片段是矿工节点“专属”。让我们对它进行一下分解:
if len(mempool) >= 2 && len(miningAddress) > 0 {
只会在矿工节点上设置。如果当前节点(矿工)的内存池中有两笔或更多的交易,开始挖矿:
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 := UTXOSetUTXOSet.Reindex()fmt.Println("New block is mined!")
验证后的交易被放到一个块里,同时还有附带奖励的 coinbase 交易。当块被挖出来以后,UTXO 集会被重新索引。
TODO: 提醒,应该使用 UTXOSet.Update 而不是 UTXOSet.Reindex.
for _, tx := range txs {
txID := hex.EncodeToString(tx.ID)
delete(mempool, txID)}for _, node := range knownNodes {
if node != nodeAddress {
sendInv(node, "block", [][]byte)
}}if len(mempool) > 0 {
goto MineTransactions}
当一笔交易被挖出来以后,就会被从内存池中移除。当前节点所连接到的所有其他节点,接收带有新块哈希的消息。在处理完消息后,它们可以对块进行请求。
结果
让我们来回顾一下上面定义的场景。
首先,在第一个终端窗口中将设置为 3000()。为了让你知道什么节点执行什么操作,我会使用像NODE 3000或NODE 3001进行标识。
NODE 3000
创建一个钱包和一个新的区块链:
$ blockchain_go createblockchain -address CENTREAL_NODE
(为了简洁起见,我会使用假地址。)
然后,会生成一个仅包含创世块的区块链。我们需要保存块,并在其他节点使用。创世块承担了一条链标识符的角色(在 Bitcoin Core 中,创世块是硬编码的)
$ cp blockchain_3000.db blockchain_genesis.db
NODE 3001
接下来,打开一个新的终端窗口,将 node ID 设置为 3001。这会作为一个钱包节点。通过生成一些地址,我们把这些地址叫做 WALLET_1, WALLET_2, WALLET_3.
NODE 3000
向钱包地址发送一些币:
$ blockchain_go send -from CENTREAL_NODE -to WALLET_1 -amount 10 -mine
$ blockchain_go send -from CENTREAL_NODE -to WALLET_2 -amount 10 -mine
标志指的是块会立刻被同一节点挖出来。我们必须要有这个标志,因为初始状态时,网络中没有矿工节点。
启动节点:
$ blockchain_go startnode
这个节点会持续运行,直到本文定义的场景结束。
NODE 3001
启动上面保存创世块节点的区块链:
$ cp blockchain_genesis.db blockchain_3001.db
运行节点:
$ blockchain_go startnode
它会从中心节点下载所有区块。为了检查一切正常,暂停节点运行并检查余额:
$ blockchain_go getbalance -address WALLET_1
Balance of 'WALLET_1': 10
$ blockchain_go getbalance -address WALLET_2
Balance of 'WALLET_2': 10
你还可以检查地址的余额,因为 node 3001 现在有它自己的区块链:
$ blockchain_go getbalance -address CENTRAL_NODE
Balance of 'CENTRAL_NODE': 10
NODE 3002
打开一个新的终端窗口,将它的 ID 设置为 3002,然后生成一个钱包。这会是一个矿工节点。初始化区块链:
$ cp blockchain_genesis.db blockchain_3002.db
启动节点:
$ blockchain_go startnode -miner MINER_WALLET
NODE 3001
发送一些币:
$ blockchain_go send -from WALLET_1 -to WALLET_3 -amount 1
$ blockchain_go send -from WALLET_2 -to WALLET_4 -amount 1
NODE 3002
迅速切换到矿工节点,你会看到挖出了一个新块!同时,检查中心节点的输出。
NODE 3001
切换到钱包节点并启动:
$ blockchain_go startnode
它会下载最近挖出来的块!
暂停节点并检查余额:
$ blockchain_go getbalance -address WALLET_1
Balance of 'WALLET_1': 9
$ blockchain_go getbalance -address WALLET_2
Balance of 'WALLET_2': 9
$ blockchain_go getbalance -address WALLET_3
Balance of 'WALLET_3': 1
$ blockchain_go getbalance -address WALLET_4
Balance of 'WALLET_4': 1
$ blockchain_go getbalance -address MINER_WALLET
Balance of 'MINER_WALLET': 10
就是这么多了!
总结
这是本系列的最后一篇文章了。本可以就实现一个真实的 P2P 网络原型继续展开。希望本文已经回答了关于比特币技术的一些问题,也给读者提出了一些问题,这些问题你可以自行寻找答案。在比特币技术中还有隐藏着很多有趣的事情!
领取专属 10元无门槛券
私享最新 技术干货