35

在Go中构建区块链 第7部分:网络

 5 years ago
source link: http://www.apexyun.com/zai-gozhong-gou-jian-qu-kuai-lian-di-7bu-fen-wang-luo/?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Introduction

到目前为止,我们已经构建了一个具有所有关键功能的区块链:匿名,安全和随机生成的地址;区块链数据存储;工作证明制度;存储交易的可靠方式。虽然这些功能至关重要,但这还不够。是什么让这些功能真正发挥作用,使加密货币成为可能的是网络。在单台计算机上运行这种区块链实现有什么用?当只有一个用户时,基于密码术的功能有什么用?它的网络使所有这些机制起作用并且有用。

您可以将区块链功能视为规则,类似于人们希望一起生活和繁荣时所建立的规则。一种社会安排。区块链网络是遵循相同规则的程序社区,它遵循使网络活跃的规则。同样,当人们分享相同的想法时,他们会变得更强大,可以共同创造更好的生活。如果有人遵循不同的规则,他们将生活在一个单独的社会(州,公社等)。同样,如果区块链节点遵循不同的规则,它们将形成一个单独的网络。

这是非常重要的:没有网络,没有大多数节点共享相同的规则,这些规则是没用的!

免责声明:不幸的是,我没有足够的时间来实现真正的P2P网络原型。在本文中,我将演示一个最常见的场景,它涉及不同类型的节点。改善这种情况并使其成为P2P网络对您来说是一个很好的挑战和实践!此外,我不能保证除了本文中实现的方案之外的其他方案也可以。抱歉!这部分介绍了重要的代码更改,因此在这里解释所有这些都没有意义。请参阅 this page 查看自上一篇文章以来的所有更改。

Blockchain Network

区块链网络是分散的,这意味着没有服务器可以使用服务器来获取或处理数据。在区块链网络中有节点,每个节点都是网络的成熟成员。节点就是一切:它既是客户端又是服务器。记住这一点非常重要,因为它与通常的Web应用程序非常不同。

区块链网络是P2P(点对点)网络,这意味着节点彼此直接连接。它的拓扑结构是扁平的,因为节点角色中没有层次结构。这里的示意图如下:

umIjQjU.png!web

( Business vector created by Dooder - Freepik.com )

这种网络中的节点更难实现,因为它们必须执行大量操作。每个节点必须与多个其他节点交互,它必须请求其他节点的状态,将其与自己的状态进行比较,并在其过时时更新其状态。

Node Roles

尽管是完整的,区块链节点可以在网络中扮演不同的角色。他们来了:

  1. Miner.这些节点在强大的或专用的硬件(如ASIC)上运行,其唯一目标是尽可能快地挖掘新块。矿工只能在使用工作证明的区块链中使用,因为挖掘实际上意味着解决PoW难题。例如,在Proof-of-Stake区块链中,没有采矿。
  2. Full node.这些节点验证矿工开采的块并验证交易。要做到这一点,他们必须拥有区块链的全部副本。此外,这样的节点执行这样的路由操作,例如帮助其他节点发现彼此。对于网络而言,拥有许多完整节点非常重要,因为正是这些节点做出了决策:他们决定一个块或事务是否有效。
  3. SPV.SPV代表简化付款验证。这些节点不存储区块链的完整副本,但它们仍然能够验证事务(不是所有事务,而是一个子集,例如,发送到特定地址的子集)。 SPV节点依赖于完整节点来获取数据,并且可能有许多SPV节点连接到一个完整节点。 SPV使钱包应用成为可能:一个不需要下载完整的区块链,但仍然可以验证他们的交易。

Network simplification

要在我们的区块链中实现网络,我们必须简化一些事情。问题是我们没有很多计算机来模拟具有多个节点的网络。我们可以使用虚拟机或Docker来解决这个问题,但它可能会使一切变得更加困难:你必须解决可能的虚拟机或Docker问题,而我的目标只是集中在区块链实现上。因此,我们希望在一台机器上运行多个区块链节点,同时我们希望它们具有不同的地址。要实现这一点,我们将使用 端口作为节点标识符 而不是IP地址。例如,将有节点具有地址: 127.0.0.1:3000 , 127.0.0.1:3001 , 127.0.0.1:3002 我们将调用端口节点ID并使用 NODE_ID 环境变量来设置它们。因此,您可以打开多个终端窗口,设置不同 NODE_ID s并运行不同的节点。

这种方法还需要具有不同的区块链和钱包文件。它们现在必须依赖于节点ID并命名为 blockchain_3000.db , blockchain_30001.db and wallet_3000.db , wallet_30001.db , etc.

Implementation

那么,当您下载比特币核心并首次运行它时会发生什么?它必须连接到某个节点才能下载区块链的最新状态。考虑到您的计算机不知道所有或某些比特币节点,这个节点是什么?

比特币核心中的节点地址硬编码将是一个错误:节点可能受到攻击或关闭,这可能导致新节点无法加入网络。相反,在比特币核心中,有 DNS seeds 硬编码。这些不是节点,而是知道某些节点地址的DNS服务器。当你启动一个干净的比特币核心时,它将连接到其中一个种子并获得一个完整节点列表,然后它将从中下载区块链。

在我们的实施中,将会集中化。我们将有三个节点:

  1. 中心节点。这是所有其他节点将连接到的节点,这是将在其他节点之间发送数据的节点。
  2. 矿工节点。此节点将在mempool中存储新事务,并且当有足够的事务时,它将挖掘新块。
  3. 钱包节点。此节点将用于在钱包之间发送硬币。与SPV节点不同,它会存储区块链的完整副本。

The Scenario

本文的目标是实现以下场景:

  1. 中心节点创建区块链。
  2. 其他(钱包)节点连接到它并下载区块链。
  3. 另一个(矿工)节点连接到中央节点并下载区块链。
  4. 钱包节点创建一个事务。
  5. 矿工节点接收事务并将其保留在其内存池中。
  6. 当内存池中有足够的事务时,矿工开始挖掘新块。
  7. 当开采新块时,它将发送到中央节点。
  8. 钱包节点与中心节点同步。
  9. 钱包节点的用户检查他们的付款是否成功。

这就是比特币的样子。即使我们不打算建立一个真正的P2P网络,我们也将实现一个真实的,比特币的主要和最重要的用例。

version

节点通过消息进行通信。运行新节点时,它会从DNS种子中获取多个节点并发送它们 version 消息,在我们的实现中将如下所示:

type version struct {
    Version    int
    BestHeight int
    AddrFrom   string
}

我们只有一个区块链版本,所以 Version 字段不会保留任何重要信息。 BestHeight 存储节点区块链的长度。 AddFrom 存储发件人的地址。

接收节点的节点应该是什么? version 消息呢?它会以自己的方式回应 version 信息。这是一种握手:没有事先互相问候,就不可能有其他互动。但这不仅仅是礼貌: version 用于查找更长的区块链。当节点收到一个 version 消息它检查节点的区块链是否长于值 BestHeight 。如果不是,节点将请求并下载丢失的块。

为了接收消息,我们需要一个服务器:

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 参数指定接收采矿奖励的地址。这件作品:

if nodeAddress != knownNodes[0] {
    sendVersion(knownNodes[0], bc)
}

意味着如果当前节点不是中心节点,则必须发送 version 消息到中央节点以查明其区块链是否过时。

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 看起来像这样:

func commandToBytes(command string) []byte {
    var bytes [commandLength]byte

    for i, c := range command {
        bytes[i] = byte(c)
    }

    return bytes[:]
}

它创建一个12字节的缓冲区并使用命令名填充它,将rest字节留空。有一个相反的功能:

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("Received %s command\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)
    } else if myBestHeight > foreignerBestHeight {
        sendVersion(payload.AddrFrom, bc)
    }

    if !nodeIsKnown(payload.AddrFrom) {
        knownNodes = append(knownNodes, payload.AddrFrom)
    }
}

首先,我们需要解码请求并提取有效负载。这与所有处理程序类似,因此我将在以后的代码片段中省略这一部分。

然后一个节点比较它 BestHeight 与消息中的那个。如果节点的区块链更长,它将回复 version 信息;否则,它会发送 getblocks message.

getblocks

type getblocks struct {
    AddrFrom string
}

getblocks 意思是“告诉我你有什么块”(在比特币中,它更复杂)。注意,它没有说“给我所有的块”,而是它请求块哈希列表。这样做是为了减少网络负载,因为可以从不同的节点下载块,我们不想从一个节点下载几十GB。

处理命令很简单:

func handleGetBlocks(request []byte, bc *Blockchain) {
    ...
    blocks := bc.GetBlockHashes()
    sendInv(payload.AddrFrom, "block", blocks)
}

在我们的简化实现中,它将返回所有块的hash.

inv

type inv struct {
    AddrFrom string
    Type     string
    Items    [][]byte
}

Bitcoin 使用 inv 向其他节点显示当前节点具有哪些块或事务。同样,它不包含整个块和事务,只包含它们的哈希值。该 Type 字段说这些是块还是交易。

处理 inv 更困难:

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)
        }
    }
}

如果传输了块哈希,我们希望将它们保存在 blocksInTransit 变量来跟踪下载的块。这允许我们从不同节点下载块。在将块放入运输状态后,我们发送 getdata 命令给发件人 inv 消息和更新 blocksInTransit 。在真正的P2P网络中,我们希望从不同节点传输块。

在我们的实施中,我们永远不会发送有多个哈希的 inv 。这就是为什么 payload.Type == "tx" 只有第一个哈希值。然后我们检查我们的mempool中是否已经有哈希值,如果没有, getdata 消息就被发送出去了.

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, █)
    }

    if payload.Type == "tx" {
        txID := hex.EncodeToString(payload.ID)
        tx := mempool[txID]

        sendTx(payload.AddrFrom, &tx)
    }
}

处理程序很简单:如果它们请求块,则返回块;如果他们请求交易,则返回交易。请注意,我们不会检查我们是否确实拥有此块或事务。这是一个缺陷:)

block and 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()
    }
}

当我们收到一个新区块时,我们将其放入区块链中。如果要下载更多块,我们会从我们下载前一个块的同一节点请求它们。当我们最终下载了所有块时,重新索引UTXO集。

TODO:我们应该在将其添加到区块链之前验证每个传入的块,而不是无条件地信任。TODO:不应运行UTXOSet.Reindex(),而应使用UTXOSet.Update(块),因为如果区块链很大,则需要花费大量时间来重新索引整个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 {
        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
            }
        }
    }
}

首先要做的是将新事务放入mempool(同样,事务必须在放入mempool之前进行验证)。下一件:

if nodeAddress == knownNodes[0] {
    for _, node := range knownNodes {
        if node != nodeAddress && node != payload.AddFrom {
            sendInv(node, "tx", [][]byte{tx.ID})
        }
    }
}

检查当前节点是否为中心节点。在我们的实现中,中心节点不会挖掘块。相反,它会将新事务转发到网络中的其他节点。

下一个重要部分仅适用于矿工节点。让我们将它分成更小的部分:

if len(mempool) >= 2 && len(miningAddress) > 0 {

miningAddress 仅在矿工节点上设置。当当前(矿工)节点的mempool中有2个或更多事务时,开始挖掘。

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
}

首先,验证mempool中的所有事务。忽略无效的事务,如果没有有效的事务,则中断挖掘。

cbTx := NewCoinbaseTX(miningAddress, "")
txs = append(txs, cbTx)

newBlock := bc.MineBlock(txs)
UTXOSet := UTXOSet{bc}
UTXOSet.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{newBlock.Hash})
    }
}

if len(mempool) > 0 {
    goto MineTransactions
}

交易完成后,它将从mempool中删除。当前节点知道,接收的每个其他节点 inv 带有新块哈希的消息。他们可以在处理消息后请求块。

Result

让我们播放我们之前定义的场景。

首先, set NODE_ID to 3000 ( export NODE_ID=3000 )在第一个终端窗口。我会用像徽章一样的徽章 NODE 3000 or NODE 3001 在下一段之前,让您知道要对哪个节点执行操作。

NODE 3000
创建一个钱包和一个新的区块链:

$ blockchain_go createblockchain -address CENTREAL_NODE

(为了清晰和简洁,我将使用虚假地址)

之后,区块链将包含单个发生区块。我们需要保存块并在其他节点中使用它。 Genesis块用作区块链的标识符(在比特币核心中,创世块是硬编码的)。

$ cp blockchain_3000.db blockchain_genesis.db

NODE 3001
接下来,打开一个新的终端窗口并将节点ID设置为3001.这将是一个钱包节点。生成一些地址 blockchain_go createwallet ,我们称之为这些地址 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

-mine flag表示该块将立即被同一节点挖掘。我们必须拥有此标志,因为最初网络中没有矿工节点。

Start the node:

$ blockchain_go startnode

节点必须一直运行直到方案结束。

NODE 3001
使用上面保存的genesis块启动节点的区块链:

$ cp blockchain_genesis.db blockchain_3001.db

Run the node:

$ 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

此外,您可以检查平衡 CENTRAL_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
Send some coins:

$ 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

That’s it!

Conclusion

这是该系列的最后一部分。我可以发布一些实现P2P网络真实原型的帖子,但我没有时间做这件事。我希望这篇文章回答你关于比特币技术的一些问题并提出新的问题,你可以自己找到答案。比特币技术中隐藏着更多有趣的东西!祝好运!

PS您可以通过实施来开始改进网络 addr 消息,如比特币网络协议(链接如下)所述。这是一个非常重要的消息,因为它允许节点相互发现。我开始实现它,但还没有完成!

英文原文: https://jeiwan.cc/posts/building-blockchain-in-go-part-7/


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK