go區(qū)塊鏈公鏈實戰(zhàn)0xa1網(wǎng)絡(luò)初窺

前面實現(xiàn)了公鏈的基本結(jié)構(gòu),交易民泵,錢包地址癣丧,數(shù)據(jù)持久化,交易等功能栈妆。但顯然這些功能都是基于單節(jié)點的胁编,我們都知道比特幣網(wǎng)絡(luò)是一個多節(jié)點共存的P2P網(wǎng)絡(luò)。

比特幣網(wǎng)絡(luò)上的節(jié)點主要有以下幾類(圖片來自《精通比特幣》):

比特幣網(wǎng)絡(luò)節(jié)點.png

M:礦工節(jié)點签钩,具備挖礦功能的節(jié)點掏呼。這些節(jié)點一般運行在特殊的硬件設(shè)備以完成復(fù)雜的工作量證明運算坏快。有些礦工節(jié)點同時也是全節(jié)點铅檩。

W:錢包節(jié)點,常見的很多比特幣客戶端屬于錢包節(jié)點莽鸿,它不需要拷貝完整的區(qū)塊鏈昧旨。一般的錢包節(jié)點都是SPV節(jié)點,SPV節(jié)點借助之前講的MerkleTree原理使得不需要下載所有區(qū)塊就能驗證交易成為可能祥得,后面講到錢包開發(fā)再深入理解兔沃。

B:全節(jié)點具有完整的,最新的區(qū)塊鏈拷貝级及∑故瑁可以獨立自主地校驗所有交易。

復(fù)雜問題簡單化

由于P2P網(wǎng)絡(luò)的復(fù)雜性饮焦,為了便于理解區(qū)塊鏈網(wǎng)絡(luò)同步的原理怕吴,我們可以將復(fù)雜的網(wǎng)絡(luò)簡單化為只有三個核心節(jié)點的網(wǎng)絡(luò):

1.中心節(jié)點(全節(jié)點):其他節(jié)點會連接到這個節(jié)點來更新區(qū)塊數(shù)據(jù)
2.錢包節(jié)點:用于錢包之間實現(xiàn)交易,但這里它依舊存儲一個區(qū)塊鏈的完整副本
3.礦工節(jié)點:礦工節(jié)點會在內(nèi)存池中存儲交易并在適當(dāng)時機將交易打包挖出一個新區(qū)塊 但這里它依舊存儲一個區(qū)塊鏈的完整副本

我們在這個簡化基礎(chǔ)上去實現(xiàn)區(qū)塊鏈的網(wǎng)絡(luò)同步县踢。

幾個重要的數(shù)據(jù)結(jié)構(gòu)

要想實現(xiàn)數(shù)據(jù)的同步转绷,必須有兩個節(jié)點間的通訊。那么他們通訊的內(nèi)容和格式是什么樣的呢硼啤?

區(qū)塊鏈同步時兩個節(jié)點的通訊信息并不是單一的议经,不同的情況和不同的階段通訊的格式與處理方式是不同的。這里分析主要用的幾個數(shù)據(jù)結(jié)構(gòu)谴返。

為了區(qū)分節(jié)點發(fā)送的信息煞肾,我們需要定義幾個消息類型來區(qū)別他們。

package BLC

// 采用TCP
const PROTOCOL  = "tcp"
// 發(fā)送消息的前12個字節(jié)指定了命令名(version)
const COMMANDLENGTH  = 12
// 節(jié)點的區(qū)塊鏈版本
const NODE_VERSION  = 1

// 命令
// 版本命令
const COMMAND_VERSION  = "version"
const COMMAND_ADDR  = "addr"
const COMMAND_BLOCK  = "block"
const COMMAND_INV  = "inv"
const COMMAND_GETBLOCKS  = "getblocks"
const COMMAND_GETDATA  = "getdata"
const COMMAND_TX  = "tx"

// 類型
const BLOCK_TYPE  = "block"
const TX_TYPE  = "tx"

Version

Version消息是發(fā)起區(qū)塊同步第一個發(fā)送的消息類型嗓袱,其內(nèi)容主要有區(qū)塊鏈版本籍救,區(qū)塊鏈最大高度,來自的節(jié)點地址索抓。它主要用于比較兩個節(jié)點間誰是最長鏈钧忽。

type Version struct {
    // 區(qū)塊鏈版本
    Version    int64
    // 請求節(jié)點區(qū)塊的高度
    BestHeight int64
    // 請求節(jié)點的地址
    AddrFrom   string
}

組裝發(fā)送Version信息

//發(fā)送COMMAND_VERSION
func sendVersion(toAddress string, blc *Blockchain)  {


    bestHeight := blc.GetBestHeight()
    payload := gobEncode(Version{NODE_VERSION, bestHeight, nodeAddress})

    request := append(commandToBytes(COMMAND_VERSION), payload...)

    sendData(toAddress, request)
}

當(dāng)一個節(jié)點收到Version信息毯炮,會比較自己的最大區(qū)塊高度和請求者的最大區(qū)塊高度。如果自身高度大于請求節(jié)點會向請求節(jié)點回復(fù)一個版本信息告訴請求節(jié)點自己的相關(guān)信息耸黑;否則直接向請求節(jié)點發(fā)送一個GetBlocks信息桃煎。

// Version命令處理器
func handleVersion(request []byte, blc *Blockchain)  {

    var buff bytes.Buffer
    var payload Version

    dataBytes := request[COMMANDLENGTH:]

    // 反序列化
    buff.Write(dataBytes)
    dec := gob.NewDecoder(&buff)
    err := dec.Decode(&payload)
    if err != nil {

        log.Panic(err)
    }

    // 提取最大區(qū)塊高度作比較
    bestHeight := blc.GetBestHeight()
    foreignerBestHeight := payload.BestHeight

    if bestHeight > foreignerBestHeight {

        // 向請求節(jié)點回復(fù)自身Version信息
        sendVersion(payload.AddrFrom, blc)
    } else if bestHeight < foreignerBestHeight {

        // 向請求節(jié)點要信息
        sendGetBlocks(payload.AddrFrom)
    }

// 添加到已知節(jié)點中
    if !nodeIsKnown(payload.AddrFrom) {

        knowedNodes = append(knowedNodes, payload.AddrFrom)
    }
}

Blockchain獲取自身最大區(qū)塊高度的方法:

// 獲取區(qū)塊鏈最大高度
func (blc *Blockchain) GetBestHeight() int64 {

    block := blc.Iterator().Next()

    return block.Height
}

GetBlocks

當(dāng)一個節(jié)點知道對方節(jié)點區(qū)塊鏈最新,就需要發(fā)送一個GetBlocks請求來請求對方節(jié)點所有的區(qū)塊哈希大刊。這里有人覺得為什么不直接返回對方節(jié)點所有新區(qū)塊呢为迈,可是萬一兩個節(jié)點區(qū)塊數(shù)據(jù)相差很大,在一次請求中發(fā)送相當(dāng)大的數(shù)據(jù)肯定會使通訊出問題缺菌。

// 表示向節(jié)點請求一個塊哈希的表葫辐,該請求會返回所有塊的哈希
type GetBlocks struct {
    //請求節(jié)點地址
    AddrFrom string
}

組裝發(fā)送GetBlocks消息

//發(fā)送COMMAND_GETBLOCKS
func sendGetBlocks(toAddress string)  {

    payload := gobEncode(GetBlocks{nodeAddress})

    request := append(commandToBytes(COMMAND_GETBLOCKS), payload...)

    sendData(toAddress, request)
}

當(dāng)一個節(jié)點收到一個GetBlocks消息,會將自身區(qū)塊鏈所有區(qū)塊哈希算出并組裝在Inv消息中發(fā)送給請求節(jié)點伴郁。一般收到GetBlocks消息的節(jié)點為較新區(qū)塊鏈耿战。

func handleGetblocks(request []byte, blc *Blockchain)  {

    var buff bytes.Buffer
    var payload GetBlocks

    dataBytes := request[COMMANDLENGTH:]

    // 反序列化
    buff.Write(dataBytes)
    dec := gob.NewDecoder(&buff)
    err := dec.Decode(&payload)
    if err != nil {
        log.Panic(err)
    }

    blocks := blc.GetBlockHashes()

    sendInv(payload.AddrFrom, BLOCK_TYPE, blocks)
}

Blockchain獲得所有區(qū)塊哈希的方法:

// 獲取區(qū)塊所有哈希
func (blc *Blockchain) GetBlockHashes() [][]byte {

    blockIterator := blc.Iterator()

    var blockHashs [][]byte

    for {

        block := blockIterator.Next()
        blockHashs = append(blockHashs, block.Hash)

        var hashInt big.Int
        hashInt.SetBytes(block.PrevBlockHash)
        if hashInt.Cmp(big.NewInt(0)) == 0 {

            break
        }
    }

    return blockHashs
}

Inv消息

Inv消息用于收到GetBlocks消息的節(jié)點向其他節(jié)點展示自己擁有的區(qū)塊或交易信息。其主要結(jié)構(gòu)包括自己的節(jié)點地址焊傅,展示信息的類型剂陡,是區(qū)塊還是交易,當(dāng)用于節(jié)點請求區(qū)塊同步時是區(qū)塊信息狐胎;當(dāng)用于節(jié)點向礦工節(jié)點轉(zhuǎn)發(fā)交易時是交易信息鸭栖。

// 向其他節(jié)點展示自己擁有的區(qū)塊和交易
type Inv struct {
    // 自己的地址
    AddrFrom string
    // 類型 block tx
    Type     string
    // hash二維數(shù)組
    Items    [][]byte
}

組裝發(fā)送Inv消息:

//COMMAND_Inv
func sendInv(toAddress string, kind string, hashes [][]byte) {

    payload := gobEncode(Inv{nodeAddress,kind,hashes})

    request := append(commandToBytes(COMMAND_INV), payload...)

    sendData(toAddress, request)
}

當(dāng)一個節(jié)點收到Inv消息后,會對Inv消息的類型做判斷分別采取處理握巢。
如果是Block類型晕鹊,它會取出最新的區(qū)塊哈希并組裝到一個GetData消息返回給來源節(jié)點,這個消息才是真正向來源節(jié)點請求新區(qū)塊的消息暴浦。

由于這里將源節(jié)點(比當(dāng)前節(jié)點擁有更新區(qū)塊鏈的節(jié)點)所有區(qū)塊的哈希都知道了溅话,所以需要每處理一次Inv消息后將剩余的區(qū)塊哈希緩存到unslovedHashes數(shù)組,當(dāng)unslovedHashes長度為零表示處理完畢肉渴。

這里可能有人會有疑問公荧,我們更新的應(yīng)該是源節(jié)點擁有的新區(qū)塊(自身節(jié)點沒有),這里為啥請求的是全部呢同规?這里的邏輯是這樣的循狰,請求的時候是請求的全部,后面在真正更新自身數(shù)據(jù)庫的時候判斷是否為新區(qū)塊并保存到數(shù)據(jù)庫券勺。其實绪钥,我們都知道兩個節(jié)點的區(qū)塊最大高度,這里也可以完全請求源節(jié)點的所有新區(qū)塊哈希关炼。為了簡單程腹,這里先暫且這樣處理。

如果收到的Inv是交易類型儒拂,取出交易哈希寸潦,如果該交易不存在于交易緩沖池色鸳,添加到交易緩沖池。這里的交易類型Inv一般用于有礦工節(jié)點參與的通訊见转。因為在網(wǎng)絡(luò)中命雀,只有礦工節(jié)點才需要去處理交易。

func handleInv(request []byte, blc *Blockchain)  {

    var buff bytes.Buffer
    var payload Inv

    dataBytes := request[COMMANDLENGTH:]

    // 反序列化
    buff.Write(dataBytes)
    dec := gob.NewDecoder(&buff)
    err := dec.Decode(&payload)
    if err != nil {
        log.Panic(err)
    }

    // Ivn 3000 block hashes [][]
    if payload.Type == BLOCK_TYPE {

        fmt.Println(payload.Items)

        blockHash := payload.Items[0]
        sendGetData(payload.AddrFrom, BLOCK_TYPE , blockHash)

        if len(payload.Items) >= 1 {

            unslovedHashes = payload.Items[1:]
        }
    }

    if payload.Type == TX_TYPE {

        txHash := payload.Items[0]

        // 添加到交易池
        if mempool[hex.EncodeToString(txHash)].TxHAsh == nil {

            sendGetData(payload.AddrFrom, TX_TYPE, txHash)
        }
    }
}

GetData消息

GetData消息是用于真正請求一個區(qū)塊或交易的消息類型斩箫,其主要結(jié)構(gòu)為:

// 用于請求區(qū)塊或交易
type GetData struct {
    // 節(jié)點地址
    AddrFrom string
    // 請求類型  是block還是tx
    Type     string
    // 區(qū)塊哈侠羯埃或交易哈希
    Hash       []byte
}

組裝并發(fā)送GetData消息。

func sendGetData(toAddress string, kind string ,blockHash []byte) {

    payload := gobEncode(GetData{nodeAddress,kind,blockHash})

    request := append(commandToBytes(COMMAND_GETDATA), payload...)

    sendData(toAddress, request)
}

當(dāng)一個節(jié)點收到GetData消息乘客,如果是請求區(qū)塊狐血,節(jié)點會根據(jù)區(qū)塊哈希取出對應(yīng)的區(qū)塊封裝到BlockData消息中發(fā)送給請求節(jié)點;如果是請求交易易核,同理會根據(jù)交易哈希取出對應(yīng)交易封裝到TxData消息中發(fā)送給請求節(jié)點匈织。

func handleGetData(request []byte, blc *Blockchain)  {

    var buff bytes.Buffer
    var payload GetData

    dataBytes := request[COMMANDLENGTH:]

    // 反序列化
    buff.Write(dataBytes)
    dec := gob.NewDecoder(&buff)
    err := dec.Decode(&payload)
    if err != nil {

        log.Panic(err)
    }

    if payload.Type == BLOCK_TYPE {

        block, err := blc.GetBlock([]byte(payload.Hash))
        if err != nil {

            return
        }

        sendBlock(payload.AddrFrom, block)
    }

    if payload.Type == TX_TYPE {

        // 取出交易
        txHash := hex.EncodeToString(payload.Hash)
        tx := mempool[txHash]

        sendTx(payload.AddrFrom, &tx)
    }
}

Blockchain的GetBlock方法:

// 獲取對應(yīng)哈希的區(qū)塊
func (blc *Blockchain) GetBlock(bHash []byte) ([]byte, error)  {

    //blcIterator := blc.Iterator()
    //var block *Block = nil
    //var err error = nil
    //
    //for {
    //
    //  block = blcIterator.Next()
    //  if bytes.Compare(block.Hash, bHash) == 0 {
    //
    //      break
    //  }
    //}
    //
    //if block == nil {
    //
    //  err = errors.New("Block is not found")
    //}
    //
    //return block, err

    var blockBytes []byte

    err := blc.DB.View(func(tx *bolt.Tx) error {

        b := tx.Bucket([]byte(blockTableName))

        if b != nil {

            blockBytes = b.Get(bHash)
        }

        return nil
    })

    return blockBytes, err
}

BlockData

BlockData消息用于一個節(jié)點向其他節(jié)點發(fā)送一個區(qū)塊,到這里才真正完成區(qū)塊的發(fā)送耸成。

// 用于節(jié)點間發(fā)送一個區(qū)塊
type BlockData struct {
    // 節(jié)點地址
    AddrFrom string
    // 序列化區(qū)塊
    BlockBytes []byte
}

BlockData的發(fā)送:

func sendBlock(toAddress string, blockBytes []byte)  {


    payload := gobEncode(BlockData{nodeAddress,blockBytes})

    request := append(commandToBytes(COMMAND_BLOCK), payload...)

    sendData(toAddress, request)
}

當(dāng)一個節(jié)點收到一個Block信息报亩,它會首先判斷是否擁有該Block,如果數(shù)據(jù)庫沒有就將其添加到數(shù)據(jù)庫中(AddBlock方法)井氢。然后會判斷unslovedHashes(之前緩存所有主節(jié)點未發(fā)送的區(qū)塊哈希數(shù)組)數(shù)組的長度,如果數(shù)組長度不為零表示還有未發(fā)送處理的區(qū)塊岳链,節(jié)點繼續(xù)發(fā)送GetData消息去請求下一個區(qū)塊花竞。否則,區(qū)塊同步完成掸哑,重置UTXO數(shù)據(jù)庫约急。

func handleBlock(request []byte, blc *Blockchain)  {

    //fmt.Println("handleblock:\n")
    //blc.Printchain()

    var buff bytes.Buffer
    var payload BlockData

    dataBytes := request[COMMANDLENGTH:]

    // 反序列化
    buff.Write(dataBytes)
    dec := gob.NewDecoder(&buff)
    err := dec.Decode(&payload)
    if err != nil {

        log.Panic(err)
    }

    block := DeSerializeBlock(payload.BlockBytes)
    if block == nil {

        fmt.Printf("Block nil")
    }

    err = blc.AddBlock(block)
    if err != nil {

        log.Panic(err)
    }
    fmt.Printf("add block %x succ.\n", block.Hash)
    //blc.Printchain()

    if len(unslovedHashes) > 0 {

        sendGetData(payload.AddrFrom, BLOCK_TYPE, unslovedHashes[0])
        unslovedHashes = unslovedHashes[1:]
    }else {

        //blc.Printchain()

        utxoSet := &UTXOSet{blc}
        utxoSet.ResetUTXOSet()
    }
}

TxData消息

TxData消息用于真正地發(fā)送一筆交易。當(dāng)對方節(jié)點發(fā)送的GetData消息為Tx類型苗分,相應(yīng)地會回復(fù)TxData消息厌蔽。

// 同步中傳遞的交易類型
type TxData struct {
    // 節(jié)點地址
    AddFrom string
    // 交易
    TransactionBytes []byte
}

TxData消息的發(fā)送:

func sendTx(toAddress string, tx *Transaction)  {

    data := TxData{nodeAddress, tx.Serialize()}
    payload := gobEncode(data)
    request := append(commandToBytes(COMMAND_TX), payload...)

    sendData(toAddress, request)
}

當(dāng)一個節(jié)點收到TxData消息,這個節(jié)點一般為礦工節(jié)點摔癣,如果不是他會以Inv消息格式繼續(xù)轉(zhuǎn)發(fā)該交易信息到礦工節(jié)點奴饮。礦工節(jié)點收到交易,當(dāng)交易池滿足一定數(shù)量時開始打包挖礦择浊。

當(dāng)生成新的區(qū)塊并打包到區(qū)塊鏈上時戴卜,礦工節(jié)點需要以BlockData消息向其他節(jié)點轉(zhuǎn)發(fā)該新區(qū)塊。

func handleTx(request []byte, blc *Blockchain)  {

    var buff bytes.Buffer
    var payload TxData

    dataBytes := request[COMMANDLENGTH:]
    buff.Write(dataBytes)
    dec := gob.NewDecoder(&buff)
    err := dec.Decode(&payload)
    if err != nil {

        log.Panic(err)
    }

    tx := DeserializeTransaction(payload.TransactionBytes)
    memTxPool[hex.EncodeToString(tx.TxHAsh)] = tx

    // 自身為主節(jié)點琢岩,需要將交易轉(zhuǎn)發(fā)給礦工節(jié)點
    if nodeAddress == knowedNodes[0] {

        for _, node := range knowedNodes {

            if node != nodeAddress && node != payload.AddFrom {

                sendInv(node, TX_TYPE, [][]byte{tx.TxHAsh})
            }
        }
    } else {

        //fmt.Println(len(memTxPool), len(miningAddress))
        if len(memTxPool) >= minMinerTxCount && len(miningAddress) > 0 {

        MineTransactions:

            var txs []*Transaction
            // 創(chuàng)幣交易投剥,作為挖礦獎勵
            coinbaseTx := NewCoinbaseTransaction(miningAddress)
            txs = append(txs, coinbaseTx)

            var _txs []*Transaction

            for id := range memTxPool {

                tx := memTxPool[id]
                _txs = append(_txs, &tx)
                //fmt.Println("before")
                //tx.PrintTx()
                if blc.VerifyTransaction(&tx, _txs) {

                    txs = append(txs, &tx)
                }
            }

            if len(txs) == 1 {

                fmt.Println("All transactions invalid!\n")

            }

            fmt.Println("All transactions verified succ!\n")


            // 建立新區(qū)塊
            var block *Block
            // 取出上一個區(qū)塊
            err = blc.DB.View(func(tx *bolt.Tx) error {

                b := tx.Bucket([]byte(blockTableName))
                if b != nil {

                    hash := b.Get([]byte(newestBlockKey))
                    block = DeSerializeBlock(b.Get(hash))
                }

                return nil
            })
            if err != nil {

                log.Panic(err)
            }

            //構(gòu)造新區(qū)塊
            block = NewBlock(txs, block.Height+1, block.Hash)

            fmt.Println("New block is mined!")

            // 添加到數(shù)據(jù)庫
            err = blc.DB.Update(func(tx *bolt.Tx) error {

                b := tx.Bucket([]byte(blockTableName))
                if b != nil {

                    b.Put(block.Hash, block.Serialize())
                    b.Put([]byte(newestBlockKey), block.Hash)
                    blc.Tip = block.Hash

                }
                return nil
            })
            if err != nil {

                log.Panic(err)
            }

            utxoSet := UTXOSet{blc}
            utxoSet.Update()

            // 去除內(nèi)存池中打包到區(qū)塊的交易
            for _, tx := range txs {

                fmt.Println("delete...")
                txHash := hex.EncodeToString(tx.TxHAsh)
                delete(memTxPool, txHash)
            }

            // 發(fā)送區(qū)塊給其他節(jié)點
            sendBlock(knowedNodes[0], block.Serialize())
            //for _, node := range knownNodes {
            //  if node != nodeAddress {
            //      sendInv(node, "block", [][]byte{newBlock.Hash})
            //  }
            //}

            if len(memTxPool) > 0 {

                goto MineTransactions
            }
        }
    }
}

好累啊,終于將一次網(wǎng)絡(luò)同步需要通訊的消息類型寫完了担孔。是不是覺得好復(fù)雜江锨,其實不然吃警,一會結(jié)合實際??看過程就好理解多了。

Server服務(wù)器端

由于我們是在本地模擬網(wǎng)絡(luò)環(huán)境啄育,所以采用不同的端口號來模擬節(jié)點IP地址汤徽。eg:localhost:8000代表一個節(jié)點,eg:localhost:8001代表一個不同的節(jié)點灸撰。

寫一個啟動Server服務(wù)的方法:


func StartServer(nodeID string, minerAdd string) {

    // 當(dāng)前節(jié)點IP地址
    nodeAddress = fmt.Sprintf("localhost:%s", nodeID)
    // 挖礦節(jié)點設(shè)置
    if len(minerAdd) > 0 {

        miningAddress = minerAdd
    }

    // 啟動網(wǎng)絡(luò)監(jiān)聽服務(wù)
    ln, err := net.Listen(PROTOCOL, nodeAddress)
    if err != nil {

        log.Panic(err)
    }
    defer ln.Close()

    blc := GetBlockchain(nodeID)
    //fmt.Println("startserver\n")
    //blc.Printchain()

    // 第一個終端:端口為3000,啟動的就是主節(jié)點
    // 第二個終端:端口為3001谒府,錢包節(jié)點
    // 第三個終端:端口號為3002,礦工節(jié)點
    if nodeAddress != knowedNodes[0] {

        // 該節(jié)點不是主節(jié)點浮毯,錢包節(jié)點向主節(jié)點請求數(shù)據(jù)
        sendVersion(knowedNodes[0], blc)
    }

    for {

        // 接收客戶端發(fā)來的數(shù)據(jù)
        connc, err := ln.Accept()
        if err != nil {

            log.Panic(err)
        }

        // 不同的命令采取不同的處理方式
        go handleConnection(connc, blc)
    }
}

針對不同的命令要采取不同的處理方式(上面已經(jīng)講了具體命令對應(yīng)的實現(xiàn))完疫,所以需要實現(xiàn)一個命令解析器:

// 客戶端命令處理器
func handleConnection(conn net.Conn, blc *Blockchain) {

    //fmt.Println("handleConnection:\n")
    //blc.Printchain()

    // 讀取客戶端發(fā)送過來的所有的數(shù)據(jù)
    request, err := ioutil.ReadAll(conn)
    if err != nil {

        log.Panic(err)
    }

    fmt.Printf("Receive a Message:%s\n", request[:COMMANDLENGTH])

    command := bytesToCommand(request[:COMMANDLENGTH])

    switch command {

    case COMMAND_VERSION:
        handleVersion(request, blc)

    case COMMAND_ADDR:
        handleAddr(request, blc)

    case COMMAND_BLOCK:
        handleBlock(request, blc)

    case COMMAND_GETBLOCKS:
        handleGetblocks(request, blc)

    case COMMAND_GETDATA:
        handleGetData(request, blc)

    case COMMAND_INV:
        handleInv(request, blc)

    case COMMAND_TX:
        handleTx(request, blc)

    default:
        fmt.Println("Unknown command!")
    }
    defer conn.Close()
}

Server需要的一些全局變量:

//localhost:3000 主節(jié)點的地址
var knowedNodes = []string{"localhost:8000"}
var nodeAddress string //全局變量,節(jié)點地址
// 存儲擁有最新鏈的未處理的區(qū)塊hash值
var unslovedHashes [][]byte
// 交易內(nèi)存池
var memTxPool = make(map[string]Transaction)
// 礦工地址
var miningAddress string
// 挖礦需要滿足的最小交易數(shù)
const minMinerTxCount = 1

為了能使礦工節(jié)點執(zhí)行挖礦的責(zé)任债蓝,修改啟動服務(wù)的CLI代碼壳鹤。當(dāng)帶miner參數(shù)且不為空時,該參數(shù)為礦工獎勵地址饰迹。

startNodeCmd := flag.NewFlagSet("startnode", flag.ExitOnError)
flagMiner := startNodeCmd.String("miner","","定義挖礦獎勵的地址......")

func (cli *CLI) startNode(nodeID string, minerAdd string)  {

    fmt.Printf("start Server:localhost:%s\n", nodeID)
    // 挖礦地址判斷
    if len(minerAdd) > 0 {

        if IsValidForAddress([]byte(minerAdd)) {

            fmt.Printf("Miner:%s is ready to mining...\n", minerAdd)
        }else {

            fmt.Println("Server address invalid....\n")
            os.Exit(0)
        }
    }

    // 啟動服務(wù)器
    StartServer(nodeID, minerAdd)
}

除此之外芳誓,轉(zhuǎn)賬的send命令也需要稍作修改。帶有mine參數(shù)表示立即挖礦啊鸭,由交易的第一個轉(zhuǎn)賬方地址進行挖礦锹淌;如果沒有該參數(shù),表示由啟動服務(wù)的礦工進行挖礦赠制。

flagSendBlockMine := sendBlockCmd.Bool("mine",false,"是否在當(dāng)前節(jié)點中立即驗證....")
//轉(zhuǎn)賬
func (cli *CLI) send(from []string, to []string, amount []string, nodeID string, mineNow bool)  {

    blc := GetBlockchain(nodeID)
    defer blc.DB.Close()

    utxoSet := &UTXOSet{blc}

    // 由交易的第一個轉(zhuǎn)賬地址進行打包交易并挖礦
    if mineNow {

        blc.MineNewBlock(from, to, amount, nodeID)

        // 轉(zhuǎn)賬成功以后赂摆,需要更新UTXOSet
        utxoSet.Update()
    }else {
        
        // 把交易發(fā)送到礦工節(jié)點去進行驗證
        fmt.Println("miner deal with the Tx...")

        // 遍歷每一筆轉(zhuǎn)賬構(gòu)造交易
        var txs []*Transaction
        for index, address := range from {

            value, _ := strconv.Atoi(amount[index])
            tx := NewTransaction(address, to[index], int64(value), utxoSet, txs, nodeID)
            txs = append(txs, tx)

            // 將交易發(fā)送給主節(jié)點
            sendTx(knowedNodes[0], tx)
        }
    }
}

網(wǎng)絡(luò)同步??詳解

假設(shè)現(xiàn)在的情況是這樣的:

  • A節(jié)點(中心節(jié)點),擁有3個區(qū)塊的區(qū)塊鏈
  • B節(jié)點(錢包節(jié)點)钟些,擁有1個區(qū)塊的區(qū)塊鏈
  • C節(jié)點(挖礦節(jié)點)烟号,擁有1個區(qū)塊的區(qū)塊鏈

很明顯,B節(jié)點需要向A節(jié)點請求2個區(qū)塊更新到自己的區(qū)塊鏈上政恍。那么汪拥,實際的代碼邏輯是怎樣處理的?

中心節(jié)點與錢包節(jié)點的同步邏輯

A和B都是既可以充當(dāng)服務(wù)端篙耗,也可以充當(dāng)客戶端迫筑。

  1. A.StartServer 等待接收其他節(jié)點發(fā)來的消息
  1. B.StartServer 啟動同步服務(wù)
  1. B != 中心節(jié)點,向中心節(jié)點發(fā)請求:B.sendVersion(A, B.blc)
  1. A.Handle(B.Versin) :A收到B的Version消息
    4.1 A.blc.Height > B.blc.Height(3>1) A.sendVersion(B, A.blc)
  1. B.Handle(A.Version):B收到A的Version消息
    5.1 B.blc.Height < A.blc.Height(1<3) B向A請求其所有的區(qū)塊哈希:B.sendGetBlocks(B)
  1. A.Handle(B.GetBlocks) A將其所有的區(qū)塊哈希返回給B:A.sendInv(B, "block",blockHashes)
  1. B.Handle(A.Inv) B收到A的Inv消息
    7.1取第一個哈希鹤树,向A發(fā)送一個消息請求該哈希對應(yīng)的區(qū)塊:B.sendGetData(A, blockHash)
    7.2在收到的blockHashes去掉請求的blockHash后铣焊,緩存到一個數(shù)組unslovedHashes中
  1. A.Handle(B.GetData) A收到B的GetData請求,發(fā)現(xiàn)是在請求一個區(qū)塊
    8.1 A取出對應(yīng)得區(qū)塊并發(fā)送給B:A.sendBlock(B, block)
  1. B.Handle(A.Block) B收到A的一個Block
    9.1 B判斷該Block自己是否擁有罕伯,如果沒有加入自己的區(qū)塊鏈
    9.2 len(unslovedHashes) != 0曲伊,如果還有區(qū)塊未處理,繼續(xù)發(fā)送GetData消息,相當(dāng)于回7.1:B.sendGetData(A,unslovedHashes[0])
    9.3 len(unslovedHashes) == 0,所有A的區(qū)塊處理完畢坟募,重置UTXO數(shù)據(jù)庫
  1. 大功告成

挖礦節(jié)點參與的同步邏輯

上面的同步并沒有礦工挖礦的工作岛蚤,那么由礦工節(jié)點參與挖礦時的同步邏輯又是怎樣的呢?

  1. A.StartServer 等待接收其他節(jié)點發(fā)來的消息
  1. C.StartServer 啟動同步服務(wù)懈糯,并指定自己為挖礦節(jié)點涤妒,指定挖礦獎勵接收地址
  1. C != 中心節(jié)點,向中心節(jié)點發(fā)請求:C.sendVersion(A, C.blc)
  1. A.Handle(C.Version),該步驟如果有更新同上面的分析相同
  1. B.Send(B, C, amount) B給C的地址轉(zhuǎn)賬形成一筆交易
    5.1 B.sendTx(A, tx) B節(jié)點將該交易tx轉(zhuǎn)發(fā)給主節(jié)點做處理
    5.2 A.Handle(B.tx) A節(jié)點將其信息分裝到Inv發(fā)送給其他節(jié)點:A.SendInv(others, txInv)
  1. C.Handle(A.txInv),C收到轉(zhuǎn)發(fā)的交易將其放到交易緩沖池memTxPool赚哗,當(dāng)memTxPool內(nèi)Tx達到一定數(shù)量就進行打包挖礦產(chǎn)生新區(qū)塊并發(fā)送給其他節(jié)點:C.sendBlock(others, blockData)
  1. A(B).HandleBlock(C. blockData) A和B都會收到C產(chǎn)生的新區(qū)塊并添加到自己的區(qū)塊鏈上

8.大功告成

幾個命令總結(jié)

從上面的??可以看出她紫,這幾個命令總是兩兩對應(yīng)的。而且很明顯有些命令用于低高度節(jié)點請求屿储,有些命令用于最新鏈節(jié)點對請求節(jié)點的回復(fù)贿讹。

A & B

還是以上面的情形為例,A為主節(jié)點(3個區(qū)塊高度),B(1個區(qū)塊)。這里最開始是由B先發(fā)起的Version_B消息躁绸。

命令對 A(回復(fù)) B(發(fā)起)
CmdPair0 Version_A Version_B
CmdPair1 Inv GetBlocks
CmdPair2 BlockData/TxData GetData
Action -- HandleBlock/HandleTx

我們將表格添加一些流程走向,就能直觀地看出整個區(qū)塊同步的過程中需要的幾次通訊掸绞。

區(qū)塊同步通訊流程圖1.png

A & C

CMD A B C
CmdPair0 Version_A Version_C
(同A&B同步流程) ... -- 區(qū)塊同步到最新
sendAction -- send(B,C,amount) --
sendTx -- TxData --
sendInv txInv -- Handle(tx)
sendBlock -- -- BlockData
handleBlock Handle(BlockData) Handle(BlockData) --
區(qū)塊同步通訊流程圖2.png

節(jié)點設(shè)置

我們通過設(shè)置一個環(huán)境變量NODE_ID來區(qū)別不同的節(jié)點。通過“export NODE_ID=8888”命令來在終端設(shè)置節(jié)點,通過以下方式在代碼CLI.RUN中獲取到節(jié)點的端口號:

//獲取節(jié)點
    //在命令行可以通過 export NODE_ID=8888 設(shè)置節(jié)點ID
    nodeID := os.Getenv("NODE_ID")
    if nodeID == "" {

        fmt.Printf("NODE_ID env var is not set!\n")
        os.Exit(1)
    }
    fmt.Printf("NODE_ID:%s\n", nodeID)

有了節(jié)點的概念,在這里為了模擬不同節(jié)點的區(qū)塊鏈哭廉,我們需要給相關(guān)方法加入節(jié)點作為參數(shù)。

例如創(chuàng)建區(qū)塊鏈(CreateBlockchainWithGensisBlock)方法中加入節(jié)點參數(shù)來表示該區(qū)塊鏈屬于哪一個節(jié)點期丰,錢包創(chuàng)建(NewWallets)群叶,交易挖礦(MineNewBlock)等。相應(yīng)地在CLI中的調(diào)用也要做相應(yīng)的修改钝荡。這里只以CreateBlockchainWithGensisBlock為例,其他參照源代碼修改下舶衬。

修改數(shù)據(jù)庫名字宏定義(錢包文件名同理)

//相關(guān)數(shù)據(jù)庫屬性
const dbName = "chaorsBlockchain_"

創(chuàng)建區(qū)塊鏈

//1.創(chuàng)建創(chuàng)世區(qū)塊
func CreateBlockchainWithGensisBlock(address string, nodeID string) *Blockchain {

    //格式化數(shù)據(jù)庫名字埠通,表示該鏈屬于哪一個節(jié)點
    dbName := fmt.Sprintf(dbName, nodeID)
        ……
        .......
}

CLI調(diào)用

//新建區(qū)塊鏈
func (cli *CLI)creatBlockchain(address string, nodeID string)  {

    blockchain := CreateBlockchainWithGensisBlock(address, nodeID)
    defer blockchain.DB.Close()
}

擼起袖子就是干

主節(jié)點:8000
錢包節(jié)點:8001
礦工節(jié)點:8002

打開終端1

// 1.設(shè)置節(jié)點端口為8000
export NODE_ID=8000

// 2.編譯項目
go build main.go

// 3.創(chuàng)建錢包
./main createWallet

// 4.創(chuàng)建區(qū)塊鏈
./main createBlockchain -address

// 5.備份創(chuàng)世區(qū)塊鏈(因為后面要改變這個區(qū)塊鏈)
cp chaorsBlockchain_8000.db chaorsBlockchain_genesis.db

打開終端2

// 6.設(shè)置節(jié)點端口為8001
export NODE_ID=8001

// 7.創(chuàng)建兩個錢包地址
./main creatWallet
./main creatWallet

切換到終端1

// 8. 進行兩次轉(zhuǎn)賬 額度分別為22,11
./main send -from...  -mine

// 9.啟動同步服務(wù)
./main startnode

切換到終端2

// 10.啟動同步服務(wù)
./main startnode

// 11.查詢余額
./main getBalance -address
Node8000_0.png
Node8001_0.png

切換到8000(終端1)逛犹,8002(終端3)

// 12. 啟動8001端辱,8002節(jié)點網(wǎng)絡(luò)服務(wù)。并將8002節(jié)點設(shè)置為礦工節(jié)點
//8000
./main startnode
//8002
./main startnode -miner

切換到8001(終端2)

// 13.從8001的錢包給8002轉(zhuǎn)賬11
send -from ... 

// 14.啟動節(jié)點同步服務(wù)
./main startnode 
Node8000_1.png
Node8001_1.png
Node8002.png

太坎坷了虽画,今天終于把網(wǎng)絡(luò)同步的筆記寫完了舞蔽。從寫代碼到Debug,再到筆記成稿码撰,說多了都是淚啊渗柿。

這次代碼修改了之前交易簽名和驗簽時候的代碼,具體就不多說了,詳見源碼吧朵栖。

源代碼在這,喜歡的朋友記得給個小star颊亮,或者fork.也歡迎大家一起探討區(qū)塊鏈相關(guān)知識,一起進步陨溅!

.
.
.
.

互聯(lián)網(wǎng)顛覆世界终惑,區(qū)塊鏈顛覆互聯(lián)網(wǎng)!

---------------------------------------------20180717 22:35
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市门扇,隨后出現(xiàn)的幾起案子雹有,更是在濱河造成了極大的恐慌,老刑警劉巖臼寄,帶你破解...
    沈念sama閱讀 221,695評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件霸奕,死亡現(xiàn)場離奇詭異,居然都是意外死亡脯厨,警方通過查閱死者的電腦和手機铅祸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,569評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來合武,“玉大人临梗,你說我怎么就攤上這事〖谔” “怎么了盟庞?”我有些...
    開封第一講書人閱讀 168,130評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長汤善。 經(jīng)常有香客問我什猖,道長,這世上最難降的妖魔是什么红淡? 我笑而不...
    開封第一講書人閱讀 59,648評論 1 297
  • 正文 為了忘掉前任不狮,我火速辦了婚禮,結(jié)果婚禮上在旱,老公的妹妹穿的比我還像新娘摇零。我一直安慰自己,他們只是感情好桶蝎,可當(dāng)我...
    茶點故事閱讀 68,655評論 6 397
  • 文/花漫 我一把揭開白布驻仅。 她就那樣靜靜地躺著,像睡著了一般登渣。 火紅的嫁衣襯著肌膚如雪噪服。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,268評論 1 309
  • 那天胜茧,我揣著相機與錄音粘优,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛敬飒,可吹牛的內(nèi)容都是我干的邪铲。 我是一名探鬼主播,決...
    沈念sama閱讀 40,835評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼无拗,長吁一口氣:“原來是場噩夢啊……” “哼带到!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起英染,我...
    開封第一講書人閱讀 39,740評論 0 276
  • 序言:老撾萬榮一對情侶失蹤揽惹,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后四康,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體搪搏,經(jīng)...
    沈念sama閱讀 46,286評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,375評論 3 340
  • 正文 我和宋清朗相戀三年闪金,在試婚紗的時候發(fā)現(xiàn)自己被綠了疯溺。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,505評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡哎垦,死狀恐怖囱嫩,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情漏设,我是刑警寧澤墨闲,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站郑口,受9級特大地震影響鸳碧,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜犬性,卻給世界環(huán)境...
    茶點故事閱讀 41,873評論 3 333
  • 文/蒙蒙 一瞻离、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧乒裆,春花似錦琐脏、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,357評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽吹艇。三九已至惰蜜,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間受神,已是汗流浹背抛猖。 一陣腳步聲響...
    開封第一講書人閱讀 33,466評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人财著。 一個月前我還...
    沈念sama閱讀 48,921評論 3 376
  • 正文 我出身青樓联四,卻偏偏與公主長得像,于是被迫代替她去往敵國和親撑教。 傳聞我的和親對象是個殘疾皇子朝墩,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,515評論 2 359

推薦閱讀更多精彩內(nèi)容