前面實現(xiàn)了公鏈的基本結(jié)構(gòu),交易民泵,錢包地址癣丧,數(shù)據(jù)持久化,交易等功能栈妆。但顯然這些功能都是基于單節(jié)點的胁编,我們都知道比特幣網(wǎng)絡(luò)是一個多節(jié)點共存的P2P網(wǎng)絡(luò)。
比特幣網(wǎng)絡(luò)上的節(jié)點主要有以下幾類(圖片來自《精通比特幣》):
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)客戶端迫筑。
- A.StartServer 等待接收其他節(jié)點發(fā)來的消息
- B.StartServer 啟動同步服務(wù)
- B != 中心節(jié)點,向中心節(jié)點發(fā)請求:B.sendVersion(A, B.blc)
- A.Handle(B.Versin) :A收到B的Version消息
4.1 A.blc.Height > B.blc.Height(3>1) A.sendVersion(B, A.blc)
- B.Handle(A.Version):B收到A的Version消息
5.1 B.blc.Height < A.blc.Height(1<3) B向A請求其所有的區(qū)塊哈希:B.sendGetBlocks(B)
- A.Handle(B.GetBlocks) A將其所有的區(qū)塊哈希返回給B:A.sendInv(B, "block",blockHashes)
- B.Handle(A.Inv) B收到A的Inv消息
7.1取第一個哈希鹤树,向A發(fā)送一個消息請求該哈希對應(yīng)的區(qū)塊:B.sendGetData(A, blockHash)
7.2在收到的blockHashes去掉請求的blockHash后铣焊,緩存到一個數(shù)組unslovedHashes中
- A.Handle(B.GetData) A收到B的GetData請求,發(fā)現(xiàn)是在請求一個區(qū)塊
8.1 A取出對應(yīng)得區(qū)塊并發(fā)送給B:A.sendBlock(B, block)
- 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ù)庫
- 大功告成
挖礦節(jié)點參與的同步邏輯
上面的同步并沒有礦工挖礦的工作岛蚤,那么由礦工節(jié)點參與挖礦時的同步邏輯又是怎樣的呢?
- A.StartServer 等待接收其他節(jié)點發(fā)來的消息
- C.StartServer 啟動同步服務(wù)懈糯,并指定自己為挖礦節(jié)點涤妒,指定挖礦獎勵接收地址
- C != 中心節(jié)點,向中心節(jié)點發(fā)請求:C.sendVersion(A, C.blc)
- A.Handle(C.Version),該步驟如果有更新同上面的分析相同
- 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)
- 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)
- 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ū)塊同步的過程中需要的幾次通訊掸绞。
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) | -- |
節(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
切換到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
太坎坷了虽画,今天終于把網(wǎng)絡(luò)同步的筆記寫完了舞蔽。從寫代碼到Debug,再到筆記成稿码撰,說多了都是淚啊渗柿。
這次代碼修改了之前交易簽名和驗簽時候的代碼,具體就不多說了,詳見源碼吧朵栖。
源代碼在這,喜歡的朋友記得給個小star颊亮,或者fork.也歡迎大家一起探討區(qū)塊鏈相關(guān)知識,一起進步陨溅!
.
.
.
.
互聯(lián)網(wǎng)顛覆世界终惑,區(qū)塊鏈顛覆互聯(lián)網(wǎng)!
---------------------------------------------20180717 22:35