首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

区块链之网络架构详解(下)

我们继续区块链网络下部分内容,不知道大家对上半部分理解如何?

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 网络原型继续展开。希望本文已经回答了关于比特币技术的一些问题,也给读者提出了一些问题,这些问题你可以自行寻找答案。在比特币技术中还有隐藏着很多有趣的事情!

  • 发表于:
  • 原文链接http://kuaibao.qq.com/s/20180429G07Q2000?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券