在Go中構建區塊鏈 第7部分:網路

銀河1號發表於2019-02-26

Introduction

到目前為止,我們已經構建了一個具有所有關鍵功能的區塊鏈:匿名,安全和隨機生成的地址;區塊鏈資料儲存;工作證明制度;儲存交易的可靠方式。雖然這些功能至關重要,但這還不夠。是什麼讓這些功能真正發揮作用,使加密貨幣成為可能的是網路。在單臺計算機上執行這種區塊鏈實現有什麼用?當只有一個使用者時,基於密碼術的功能有什麼用?它的網路使所有這些機制起作用並且有用。

您可以將區塊鏈功能視為規則,類似於人們希望一起生活和繁榮時所建立的規則。一種社會安排。區塊鏈網路是遵循相同規則的程式社群,它遵循使網路活躍的規則。同樣,當人們分享相同的想法時,他們會變得更強大,可以共同創造更好的生活。如果有人遵循不同的規則,他們將生活在一個單獨的社會(州,公社等)。同樣,如果區塊鏈節點遵循不同的規則,它們將形成一個單獨的網路。

這是非常重要的:沒有網路,沒有大多數節點共享相同的規則,這些規則是沒用的!

免責宣告:不幸的是,我沒有足夠的時間來實現真正的P2P網路原型。在本文中,我將演示一個最常見的場景,它涉及不同型別的節點。改善這種情況並使其成為P2P網路對您來說是一個很好的挑戰和實踐!此外,我不能保證除了本文中實現的方案之外的其他方案也可以。抱歉!這部分介紹了重要的程式碼更改,因此在這裡解釋所有這些都沒有意義。請參閱this page檢視自上一篇文章以來的所有更改。

Blockchain Network

區塊鏈網路是分散的,這意味著沒有伺服器可以使用伺服器來獲取或處理資料。在區塊鏈網路中有節點,每個節點都是網路的成熟成員。節點就是一切:它既是客戶端又是伺服器。記住這一點非常重要,因為它與通常的Web應用程式非常不同。

區塊鏈網路是P2P(點對點)網路,這意味著節點彼此直接連線。它的拓撲結構是扁平的,因為節點角色中沒有層次結構。這裡的示意圖如下:

在Go中構建區塊鏈 第7部分:網路

(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_IDs並執行不同的節點。

這種方法還需要具有不同的區塊鏈和錢包檔案。它們現在必須依賴於節點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, &block)
    }

    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
複製程式碼

-mineflag表示該塊將立即被同一節點挖掘。我們必須擁有此標誌,因為最初網路中沒有礦工節點。
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訊息,如比特幣網路協議(連結如下)所述。這是一個非常重要的訊息,因為它允許節點相互發現。我開始實現它,但還沒有完成!

英文原文:jeiwan.cc/posts/build…

更多文章歡迎訪問 http://www.apexyun.com/

聯絡郵箱:public@space-explore.com

(未經同意,請勿轉載)


相關文章