1. 程式人生 > >用go編寫區塊鏈系列之7--網路

用go編寫區塊鏈系列之7--網路

1 區塊鏈中的網路

區塊鏈是一個分散式資料庫,所謂的分散式就是分散在多臺電腦上面,所以需要具有網路通訊功能。區塊鏈是一個分散式系統,意味著其中沒有客戶端和伺服器,所有的網路節點都是平等的。一個節點兼具伺服器和客戶端倆種角色,這與傳統的網路應用非常不同。

區塊鏈網路使用P2P網路,即網路中的節點都是倆倆直接互聯的。下圖是p2p網路拓撲圖。它的拓撲是扁平化的,因為其中不存在層級。這種網路中的節點實現起來都很有難度,因為它們要執行大量操作。每個節點都需要與大量其它節點進行通訊互動,它需要接收其它節點的狀態,並和自己的狀態進行比較,當自身狀態過時以後需要進行狀態更新。

2 節點角色

儘管區塊鏈中節點都是平等的,但它們還是可以網路中扮演不同的角色。主要有一下幾種:

1. 礦工

這些節點執行在效能強勁或專用硬體上面(比如ASIC),它們的目標就是儘可能快的挖掘出新區快。挖礦只存在於使用PoW共識機制的區塊鏈中,因為挖礦實際上就是在解答PoW謎語。比如在PoS區塊鏈中,就不需要挖礦。

2. 全節點

這些節點同步並驗證由曠工挖掘出的新區快,同時也驗證其中的交易。為了做到這一點,它們必須有整個區塊鏈的全拷貝。同時全節點還扮演著網路路由的角色,幫助其它節點互相發現。區塊鏈網路中存在大量全節點是一件很重要的事情,因為正是這些節點在扮演決策角色:它們決定一個區塊或一筆交易是否有效。

3. SPV

SPV用於做簡單支付驗證(Simplified Payment Verification)。這些節點只儲存區塊鏈部分資料,但是它們還可以驗證交易(SPV只能驗證全部交易的一個子集,只能驗證部分發送到特定地址的交易)。SPV節點需要依靠全節點來獲取資料,可以有很多SPV節點連線到一個全節點。SPV使得錢包應用成為可能:一個人不需要下載全部資料,但是仍然可以驗證它們的交易。

3 網路簡化

為了實現區塊鏈網路,本文中做了一些簡化。我們沒有很多電腦來模擬一個多節點網路。我們可以使用虛擬機器或Docker來解決這個問題,但是這將帶來大量關於虛擬機器和Docker配置相關的工作。由於我的目標只集中於區塊鏈實現,所以我想在一臺電腦上面執行執行多個區塊鏈節點。為了達到這種效果我們使用埠號而不是ip地址來區分不同節點,例如我們將使用地址127.0.0.1:3000127.0.0.1:3001127.0.0.1:3002等。我們將通過設定環境變數NODE_ID來設定埠。我們將開啟多個終端,為每個終端設定一個NODE_ID環境變數,然後再執行多個區塊鏈節點。

這種方案要求有多個不同的區塊鏈和錢包檔案,我們用NODE_ID對應的埠號來區分它們,比如像

blockchain_3000.dbblockchain_30001.db和 wallet_3000.dbwallet_30001.db等。

4 實現

當我們第一次執行一個比特幣節點的時候,節點將連線到其它節點去下載比特幣區塊鏈的最新狀態,但是電腦是怎麼找到其它的節點地址的呢?硬編碼節點地址到比特幣程式碼中將會是一個錯誤,因為這些節點可能會被攻擊會關機,這樣會導致其它的節點連線不到區塊鏈網路。比特幣程式碼中硬編碼有DNS種子。這些種子不是區塊鏈節點而是DNS伺服器,它們知道一些節點的地址。當你啟動比特幣客戶端的時候,它將連線到其中一個節點去獲取一套完整的區塊鏈資料。

在我們的方案中,我們將會採用一種中心化的方式,我們有三種節點型別:

1. 中心節點。其它節點都會連線到中心節點,中心節點會把資料轉發給所有節點。

2. 礦工節點。礦工節點會在記憶體中儲存交易,當交易到達足夠數量的時候,它將挖掘出新區快。

3. 錢包節點。錢包節點會在不同錢包見轉賬。不同於SPV,它們儲存區塊鏈全部資料。

5 方案

本文將實現下面的方案:

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還大時,它就從傳送該訊息的節點處去下載區塊。

為了接收訊息,我們定義一個server:

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

首先我們硬編碼中心節點的地址,就是knowNodes[0]。miningAddress定義了挖礦節點接收挖礦獎勵的地址。下面這句:

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",接下來的位元組是用godEcode序列化以後的訊息體結構。commandToBytes長成這樣:

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

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

    return bytes[:]
}

它建立了一個12位元組的陣列,然後將命令名稱複製進來。它有一個對應的反操作函式:

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訊息中的高度,如果它自身的更長,它就回應一個version訊息給對方,否則,它就向對方傳送getblocks訊息去請求區塊資料。

getblocks

type getblocks struct {
    AddrFrom string
}

getblocks訊息的意思就是“告訴我你有哪些區塊”。注意,它並沒有說“給我你所有的區塊”,它請求一個區塊雜湊列表。這樣做是為了減小網路負載,因為區塊可以從各個不同的節點去下載,我們不想從一個節點去下載好多G的資料。

這個訊息處理函式非常簡單:

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
}

比特幣使用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陣列中以便跟蹤下載區塊,這允許我們可以從不同節點下載區塊。一旦我們將區塊雜湊儲存後,我們馬上向inv訊息傳送者傳送一個getdata訊息去請求一個區塊資料,然後我們再更新blocksInTransit陣列。

在我們的實現中,我們用inv傳送交易訊息時只發送一條,所以這也為什麼在"payload.Type == "tx"中我們只提取payload.Item[0]。我們檢查我們的記憶體池中是否已經有這條交易雜湊,如果沒有,則會發送getdata訊息去獲取。

getdata

type getdata struct {
    AddrFrom string
    Type     string
    ID       []byte
}

getdata用來請求區塊或交易,它只能帶一個區塊或交易雜湊做引數。

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

getdata訊息的處理函式很簡單,如果請求區塊,就返回區塊;如果請求交易,就返回交易。注意,我們在這裡並沒有檢查區塊或交易是否存在,這是一個漏洞。

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

當節點接收到一個新區快,就將它加入到區塊鏈中,如果還存在更多需要下載的區塊,我們將再次向之前的區塊傳送方去請求它們。當我們終於下載了所有區塊,UTXOSet將會重新索引。

todo:我們在將每一個新到來的區塊加入到區塊鏈中前需要驗證它,而不是無條件的信任它。

todo:我們應該使用UTXOSet.update(block),而不是執行UTXOSet.Reindex()。因為區塊鏈很大,重新索引整個UTXOSet是一件很耗時的事情。

處理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中(注意,這裡也還需要對交易進行驗證才能加入)。下一段:

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,當記憶體池中有不少於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交易進來,為了給礦工傳送獎勵。然後挖礦,當挖礦成功後產生新區快,UTXOSet會被重新索引。

todo:UTXOSet.Reindex應該被UTXOSet.Update來替換掉。

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中移除。然後礦工節點向所有已知節點廣播這個新區快。其它節點可以在收到訊息後向礦工節點請求新區塊。

5 實驗

程式碼見https://github.com/Jeiwan/blockchain_go

準備工作

我們先生成可執行檔案。然後再在本地新建3個資料夾,命名為Node1,Node2,Node3。將可執行檔案拷貝到3個目錄中。然後在這三個資料夾中開啟3個3個終端。對於第一個終端,設定一個環境變數NODE_ID為3000,這可以通過命令:

C:\Users\lzj\Desktop\Node1>set NODE_ID=3000

對於第二個終端,設定NODE_ID為3001,第三個終端設定NODE_ID為3002。我們將在第1個資料夾中建立中心節點,再第2個資料夾中建立錢包節點,在第3個資料夾中建立礦工節點。

先在第1個資料夾中新建4個錢包:

C:\Users\lzj\Desktop\Node1>go_build_blockchain_go_master.exe createwallet
Your new address: 1EYiRBrcoaG31EmciWPa9Bq5FFX4NJAN6s

C:\Users\lzj\Desktop\Node1>go_build_blockchain_go_master.exe createwallet
Your new address: 19bDbkDETDRrkjKSP5kSm5ynjgKgeUNo3Z

C:\Users\lzj\Desktop\Node1>go_build_blockchain_go_master.exe createwallet
Your new address: 1ALJkuHPXvWUniARtJgajEEUL5FP9pda4p

C:\Users\lzj\Desktop\Node1>go_build_blockchain_go_master.exe createwallet
Your new address: 1HcEXXGmYhrV1rkjvtEhomaRvFFtTLycMR

C:\Users\lzj\Desktop\Node1>go_build_blockchain_go_master.exe listaddresses
19bDbkDETDRrkjKSP5kSm5ynjgKgeUNo3Z
1ALJkuHPXvWUniARtJgajEEUL5FP9pda4p
1EYiRBrcoaG31EmciWPa9Bq5FFX4NJAN6s
1HcEXXGmYhrV1rkjvtEhomaRvFFtTLycMR

第1個資料夾中會出現wallet_3000.dat錢包檔案。然後將該錢包檔案拷貝到第2個資料夾中,重新命名為wallet_3001.dat,拷貝到第3個資料夾中,重新命名為wallet_3002.dat。現在,三個節點都能訪問這些錢包了。

我們現在第1個資料夾中新建區塊鏈節點:

C:\Users\lzj\Desktop\Node1>go_build_blockchain_go_master.exe createblockchain -a
ddress 19bDbkDETDRrkjKSP5kSm5ynjgKgeUNo3Z
41371b1df93a5d88c7bfe7deced007ef658160ab1c8961bf342a44a30b0c859e

Done!

C:\Users\lzj\Desktop\Node1>go_build_blockchain_go_master.exe getbalance -address
 19bDbkDETDRrkjKSP5kSm5ynjgKgeUNo3Z
Balance of '19bDbkDETDRrkjKSP5kSm5ynjgKgeUNo3Z': 10

這個時候第1個資料夾中會出現一個名為blockchain_3000.db的區塊鏈資料庫檔案。它包括了創世區塊。我們將這個資料庫拷貝到第2個資料夾中,重新命名為blockchain_3001.db,拷貝到第3個資料夾中,重新命名為blockchain_3002.db。現在,這3個節點都擁有共同的創世區塊了。

啟動中心節點

在節點1中執行轉賬:

C:\Users\lzj\Desktop\Node1>go_build_blockchain_go_master.exe send -from 19bDbkDE
TDRrkjKSP5kSm5ynjgKgeUNo3Z -to 1ALJkuHPXvWUniARtJgajEEUL5FP9pda4p -amount 4 -mine
e55627437bf39553e7305117cc8aba3e5ba695679d189fe494558fe6d1256047

Success!

C:\Users\lzj\Desktop\Node1>go_build_blockchain_go_master.exe getbalance -address 1ALJkuHPXvWUniARtJgajEEUL5FP9pda4p
Balance of '1ALJkuHPXvWUniARtJgajEEUL5FP9pda4p': 4

C:\Users\lzj\Desktop\Node1>go_build_blockchain_go_master.exe send -from 19bDbkDE
TDRrkjKSP5kSm5ynjgKgeUNo3Z -to 1EYiRBrcoaG31EmciWPa9Bq5FFX4NJAN6s -amount 4 -mine
9eda2cb402dc79039a2b263bf2d98e45bb0eef46f25487e171bcd8bd942d2956

Success!

C:\Users\lzj\Desktop\Node1>go_build_blockchain_go_master.exe getbalance -address1EYiRBrcoaG31EmciWPa9Bq5FFX4NJAN6s
Balance of '1EYiRBrcoaG31EmciWPa9Bq5FFX4NJAN6s': 4

C:\Users\lzj\Desktop\Node1>go_build_blockchain_go_master.exe getbalance -address 19bDbkDETDRrkjKSP5kSm5ynjgKgeUNo3Z
Balance of '19bDbkDETDRrkjKSP5kSm5ynjgKgeUNo3Z': 22

這裡使用 -mine 標記去讓節點立即去挖掘新區快。由於一開始還沒有礦工節點,所以我們加入這個標記。現在可以看到19bDbkDETDRrkjKSP5kSm5ynjgKgeUNo3Z的餘額為22,1ALJkuHPXvWUniARtJgajEEUL5FP9pda4p的餘額為4,。由於中心節點原來有10個,轉出了8個,但是由於挖出了2個區塊獎勵了20,所以現在餘額是22。

現在我們啟動節點1:

C:\Users\lzj\Desktop\Node1>go_build_blockchain_go_master.exe startnode

節點1作為中心節點,服務啟動後就讓它一直保持執行知道實驗停止。

啟動錢包節點

在資料夾2中執行:

C:\Users\lzj\Desktop\Node2>go_build_blockchain_go_master.exe startnode
Starting node 3001
Received version command
Received inv command
Recevied inventory with 3 block
Received block command
Recevied a new block!
Added block 0000b2cdb0fd731118694e297b7eb51125a8ce7297ed1fae1d4e13cdf46ac9d9
Received block command
Recevied a new block!
Added block 0000c24f671f0582e4d3777d20929fa5ebc240e3a7bcb75cd12b902c51b09880
Received block command
Recevied a new block!
Added block 0000920773da4ee647ee4bb37ae3a71f8e97a49d334a74963da91bc42511cfc9

可以看到錢包節點會從中心節點同步區塊鏈資料過來,由於中心節點發起了倆筆轉賬挖掘了2個區塊,所以這裡一共有3個區塊。

終端錢包節點來檢視一下餘額是不是對的:

C:\Users\lzj\Desktop\Node2>go_build_blockchain_go_master.exe getbalance -address
 19bDbkDETDRrkjKSP5kSm5ynjgKgeUNo3Z
Balance of '19bDbkDETDRrkjKSP5kSm5ynjgKgeUNo3Z': 22

正確!說明區塊確實同步成功了。

啟動挖礦節點

我們使用下面命令啟動錢包節點,它使用 -miner 標籤來設定基準賬戶1HcEXXGmYhrV1rkjvtEhomaRvFFtTLycMR:

C:\Users\lzj\Desktop\Node3>go_build_blockchain_go_master.exe startnode -miner 1H
cEXXGmYhrV1rkjvtEhomaRvFFtTLycMR
Starting node 3002
Mining is on. Address to receive rewards:  1HcEXXGmYhrV1rkjvtEhomaRvFFtTLycMR
Received version command
Received inv command
Recevied inventory with 3 block
Received block command
Recevied a new block!
Added block 0000b2cdb0fd731118694e297b7eb51125a8ce7297ed1fae1d4e13cdf46ac9d9
Received block command
Recevied a new block!
Added block 0000c24f671f0582e4d3777d20929fa5ebc240e3a7bcb75cd12b902c51b09880
Received block command
Recevied a new block!
Added block 0000920773da4ee647ee4bb37ae3a71f8e97a49d334a74963da91bc42511cfc9

一開始也是同步資料。然後在等待交易到來。現在是有挖礦節點來接手挖礦任務了。

使用錢包節點發起交易

C:\Users\lzj\Desktop\Node2>go_build_blockchain_go_master.exe send -from 19bDbkDE
TDRrkjKSP5kSm5ynjgKgeUNo3Z -to 1ALJkuHPXvWUniARtJgajEEUL5FP9pda4p -amount 2
Success!

C:\Users\lzj\Desktop\Node2>go_build_blockchain_go_master.exe send -from 19bDbkDE
TDRrkjKSP5kSm5ynjgKgeUNo3Z -to 1EYiRBrcoaG31EmciWPa9Bq5FFX4NJAN6s -amount 5
Success!

挖礦節點顯示:

Received inv command
Recevied inventory with 1 tx
Received tx command
Received inv command
Recevied inventory with 1 tx
Received tx command
283f49596c43c11bda8ef260aeb95dc435ab2d62baa797fae21472cb933f8547

New block is mined!
Received getdata command

挖礦節點接收到2筆交易,然後開始挖礦並挖掘除了一個新區快,然後中心伺服器向它請求區塊資料。

中心節點顯示:

Received getdata command
Received tx command
Received getdata command
Received inv command
Recevied inventory with 1 block
Received block command
Recevied a new block!
Added block 00005247783988340d2c60904c81fd61095febc9dea1ca425d07cf65e844d816

中心節點葉也收到了那倆筆交易,但是它只是順手轉發。等礦工節點挖掘並廣播新區快後,它就請求新區快並將其加入區塊鏈中。

重新開啟錢包節點並同步完資料,再關閉,然後查詢餘額:

C:\Users\lzj\Desktop\Node2>go_build_blockchain_go_master.exe getbalance -address
 1ALJkuHPXvWUniARtJgajEEUL5FP9pda4p
Balance of '1ALJkuHPXvWUniARtJgajEEUL5FP9pda4p': 6

C:\Users\lzj\Desktop\Node2>go_build_blockchain_go_master.exe getbalance -address
 1EYiRBrcoaG31EmciWPa9Bq5FFX4NJAN6s
Balance of '1EYiRBrcoaG31EmciWPa9Bq5FFX4NJAN6s': 9

C:\Users\lzj\Desktop\Node2>go_build_blockchain_go_master.exe getbalance -address
 1HcEXXGmYhrV1rkjvtEhomaRvFFtTLycMR
Balance of '1HcEXXGmYhrV1rkjvtEhomaRvFFtTLycMR': 10

可以看到錢包轉賬成功了。

6 結論

這是這系列文章最後一篇了,我想實現一個P2P網路,但是我沒有時間。我希望這些文章能夠回答你們關於比特幣技術的一些問題。在比特幣中還存在許多有趣的其它技術,你可以自己去找到答案!好運!