2.8 網路
到目前為止,前面我們構造了擁有全部關鍵功能的區塊鏈,匿名、安全、隨機的地址;區塊鏈資料儲存;工作量證明系統;可靠的交易儲存。這些特性都很重要,但還不夠。能夠讓這些能力發出耀眼光芒的,讓加密貨幣成為可能,是網路。這樣實現的區塊鏈,只能執行在一臺計算機上的能有什麼卵用?那些基於密碼學的特性有什麼用,什麼時候又只會有一個人使用?是網路讓所有的機制執行起來且變得有用。
可以把區塊鏈中的我當成規則,類似於人們在彼此的生活成長中而建立的規則。一種社會秩序。區塊鏈網路是一個遵從相同的規則的程式生態社群,也是因為遵從這些規則而賦予區塊鏈網路生命。類似地,當人們分享相同的想法時,就會變得更強也會一起建立更好的生活。有的人遵從不同的規則時,這些人就會被社會隔離(國家、公社,等等)。同樣,如果區塊鏈節點都使用遵守不同的規則,那麼它們只會在一個隔離的網路中生長。
這點非常重要:不骨網路沒有大量的節點共享相同的規則,這些規則一點用也沒有。
免責宣告:非常不幸,我沒有足夠的時間來實現真正的P2P網路。這篇文章我會闡明最常用的場景,涉及不同型別的節點。改善這一場景並使之成為P2P網路對你來說是一個非常不錯的挑戰和嘗試。並且,我不保證其它非本章的實現方式可以執行。抱歉!
本篇文章的程式碼改動比較大,就不詳細解釋了。可以到這裡來看所有的改變。看他們的區別。
區塊鏈網路
區塊鏈網路是去中心化的,也就是說是沒有一箇中央伺服器作為伺服,也沒有客戶端向伺服器獲取或傳送資料。在區塊鏈網路中有節點,每一個節點都是該網路中完整的成員。一個節點就是一切,即是伺服器也是客戶端。記住這點很重要,和Web應用是不同的。
區塊鏈網路是P2P(Peer-to-Peer)網路,也就是說節點之間是彼此直接相連的。這個拓撲非常龐大,因為節點角色之間沒有層級的之分。下圖是P2P網路的圖解:
節點在這樣的網路是非常難實現的,因為他們必須執行很多操作。每個節點都肯定會和其它很多節點互動,也會請求其它節點的狀態,與自己的狀態比較,如果自己的狀態過期時就要更新狀態。
節點規則
儘管是成熟的,區塊鏈節點在網路中可以充當不同的角色:
1. 礦工
有些節點執行強大或特製的硬體(比如ASIC),它們的目的就是儘可能快地挖出新的區塊。礦機可能是僅有的在區塊鏈裡使用工作量證明的(程式),因為挖礦意味著要解釋PoW問題。而舉個例子,在權益證明(Proof-of-Stake)區塊鏈中是沒有挖礦的。
2. 全功能節點
這些節點驗證礦工挖出來的區塊和核實交易。為了完成這點,它們必須握有整個區塊鏈的副本。並且,這些節點執行路由操作,就像幫助其它節點互相發現。
網路中擁有全功能節點非常重要,這些節點可以執行決策:它們能裁定是否區塊或者交易是合法的。
3. SPV
SPV代表Simplified Payment Verification,簡化交易驗證。這些節點並不會去儲存區塊鏈的完整副本,但是卻能夠核實交易(並不是全部,而是子集,比如,那些會傳送到特殊地址的交易)。SPV節點依賴全功能節點提供資料,也可以有多個SPV節點連線到一個全功能節點。SPV使得錢包應用成為可能:錢包不需要下載整個區塊鏈,但是能夠核實交易。
網路簡化
為了在我們的區塊鏈中實現網路,我們必須得簡化一點東西。問題在於我們沒有太多的電腦來模擬有多個節點的網路。我們過去是可以使用虛擬機器或者Docker來解決這個問題的,但是這會讓每件事都變得複雜,我們得解決虛擬機器或者Docker的問題,而我們的目標僅是集中精力到區塊鏈的實現上。所以,我們需要執行多個區塊鏈節點在一臺機器上,以此同時,我們還要讓它們有不同的地址。為了實現這點,我們使用埠作為節點的標識,而不是IP地址,等等。下面將會有節點擁有這些地址,127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002等等。我們會呼叫埠的節點id,然後用NODE_ID環境變數設定它們。因此,你可以開啟多個終端視窗,設定不同的NODE_ID,就可以執行不同的節點了。
這波操作同樣需要不同的區塊鏈和錢包檔案。它們現在必須依賴節點id,並被命名像:blockchain_3000.db, blockchain_30001.db和wallet_3000.db, wallet_30001.db等等
實現
那麼,當下載時發生了什麼,是指下載Bitcoin Core然後第一次執行?答案是必須連線到一些節點下載區塊鏈最後的狀態。考慮到你們計算機並不知道全部或者部分的比特幣節點,到底這個節點是什麼呢。
在Bitcoin Core裡使用硬編碼地址可能會出錯,節點會被攻擊或者關掉,會導致新的節點不能加入到網路中。相反,在Bitcoin Core中,有使用DNS seeds硬編碼。它們不是節點,而是存放了一些節點地址的DNS伺服器。當你開始執行一個純淨的Bitcoin Core時,它會連線到一個seed然後獲取上面記錄的所有節點列表,根據這個列表下載區塊鏈。
不過,在我們的實現中,還是會中心化。會用到三個節點:
1. 中心節點:這個節點會被其它節點連線。該節點會在其它節點之間傳送資料。
2. 礦工節點:這個節點會儲存新的交易到快取池中,當有足夠的交易時,它就會挖出新的區塊。
3. 錢包節點:這個節點會用來在錢包之間傳送錢幣。但是和SPV節點不同,它會儲存區塊鏈的完整副本。
場景
本篇的目標是實現下面的場景:
1. 中心節點生成新的區塊鏈
2. 錢包節點連線到中心節點然後下載區塊鏈
3. 礦工節點連線到中心節點然後下載區塊鏈
4. 錢包節點建立交易
5. 礦工節點接收交易並把它快取在快取池中
6. 當快取池中有足夠的交易時,礦工開始挖新的區塊
7. 當新的區塊被挖出來時,會被髮送到中心節點。
8. 錢包節點與中心節點同步
9. 錢包使用者檢測他們支付是否成功
這個場景看起來和比特幣很像。儘管我們沒有構建一個真正的P2P網路,我們準備實現一個真實的,比特幣的主要、最重要的使用案例。
原文(略有刪改)
本節的闡述會有重大的程式碼改變,如果在這裡講就有點麻煩了。請跳到這裡來看所有的改變。
版本
節點通過訊息的含義進行溝通。當新的節點執行時,它會從DNS種子獲取節點的資訊,然後向它們傳送版本資訊,在我們的實現中,版本的結構如下:
type version struct {
Version int
BestHeight int
AddrFrom string
}
我們只有一個區塊鏈版本號,所有Version欄位不能含有任何重要的資訊。BestHeight存放節點的區塊鏈長度。AddFrom儲存傳送者的地址。
節點接收版本訊息做什麼呢?它會回覆它自己的版本資訊。這是握手的一種型別,除了先去彼此打招呼別無其它的互動可能。但是這並不僅僅是有禮貌,版本用於找到更長的區塊鏈。當一個節點接收到版本資訊時,它會檢測是否節點的區塊鏈比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位元組的快取區,使用命令名來填充,把餘留的位元組置空。上面是它的反向函式:
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(獲取區塊)訊息。
getblocks
type getblocks struct {
AddrFrom string
}
getblocks的意思是亮出你有的區塊(在比特幣中,會更復雜)。注意,不是扔你所有的區塊過來,相反它是請求區塊hash的列表。這麼做是為了降低網路負載,因為區塊可以從不同的節點下載,我們也不用到一個節點去下載上千兆的資料。
處理這個命令比較簡單:
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來向其它節點展示當前節點有哪些區塊或者交易。再說一遍,它並不包含所有的區塊和交易,只儲存有它們的hash值。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)
}
}
}
當區塊的hash轉移好後,需要把它們儲存到blocksInTransit變數中來跟蹤下載過的區塊。這允許我們可以從不同的節點下載區塊。在區塊進入傳輸狀態後,傳送getData指令給inv的傳送者然後更新blocksInTransit。在真正的P2P網路中,得在不同的區塊之間傳辦理區塊。
在實現中,還永不會傳送inv時帶上多個hash。這也是為什麼當payload.Type == “tx”時,只用到陣列中獲取第一個hash。然後檢測是否剛剛的txID是否存在,如果不存在,那麼傳送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)
}
}
這個getdata處理函式比較簡單。當請求的是區塊,則返回區塊;如果是交易,則返回交易。注意,這裡有個缺陷,就是沒有去檢測是否存在指定的區塊或者交易。
區塊和交易
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就需要更新了。
備註:並不是要無條件相信,我們應該在把每一個傳來的區塊加入區塊鏈之前得驗證它們。
備註:並不需要執行UTXOSet.Reindex()方法,應該用UTXOSet.Update(block),因為區塊鏈太大了,重置索引會花費太多時間。
處理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
}
}
}
}
第一件要做的事就是把新的交易放到快取池中(再強調一次,交易在被放到快取池前一定要核實),下一塊程式碼:
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
}
首先,快取池中所有的交易都是核實過的。不合法的交易會被忽略掉,如果沒有合法的交易,挖坑就會中斷。
cbTx := NewCoinbaseTX(miningAddress, "")
txs = append(txs, cbTx)
newBlock := bc.MineBlock(txs)
UTXOSet := UTXOSet{bc}
UTXOSet.Reindex()
fmt.Println("New block is mined!")
核實過的交易正被放到區塊中,還有帶有獎勵的coinbase交易。當挖出區塊後,UTXO集合就會被重置索引。
備忘:再一次說明,要用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
}
在交易被挖時,它就會從快取池中移除。其它被當前節點通知到的節點都會收到帶有新區塊hash的inv訊息。在收到訊息後,它們可以請求該剛被挖出的新區塊。
成果
現在演示上面定義的場景。
首先,在第一個終端視窗中設定環境變數NODE_ID為3000(export NODE_ID=3000)。我們在下一段中會使用像NODE 3000或者NODE 3001這樣的標識,以便在大家能知道列印出的活動是哪個節點的。
下面的分段title出是切到指定的視窗或開啟新視窗
節點 3000
建立新的錢包和新的區塊鏈
$ blockchain_go createblockchain -address CENTREAL_NODE
(這裡使用假的地址,這樣可以簡單明瞭些)
然後,這個區塊鏈只包含有創世區塊。我們需要去儲存這個區塊然後在其它節點中使用它。創世區塊作為區塊鏈的標識(在比特幣中,創世區塊是硬編碼的)。
$ cp blockchain_3000.db blockchain_genesis.db
節點 3001
下一步,開啟新的終端視窗,把node ID設定為3001。這個節點是錢包節點。用blockchain_go createwallet來生成幾個地址,定義這些地址為WALLET_1、WALLET_2、WALLET_3。
節點 3000
傳送一些幣到錢包地址中
blockchaingosend−fromCENTREALNODE−toWALLET1−amount10−mineblockchaingosend−fromCENTREALNODE−toWALLET1−amount10−mine blockchain_go send -from CENTREAL_NODE -to WALLET_2 -amount 10 -mine
-mine指令是說區塊會在相同的節點中立馬挖出來。我們加個標記是因為在初始時網路中沒有礦機節點。
執行這個節點:
$ blockchain_go startnode
這個節點會一直執行直到場景結束。
節點 3001
開始這個節點的區塊鏈,帶著上面說到的創世區塊。
$ cp blockchain_genesis.db blockchain_3001.db
執行節點:
$ blockchain_go startnode
它會去中央節點裡下載所有的區塊。檢測所有事情都好了之後,停止節點然後檢測餘額。
$ blockchain_go getbalance -address WALLET_1
Balance of ‘WALLET_1’: 10
$ blockchain_go getbalance -address WALLET_2Balance of ‘WALLET_2’: 10
當然,也可以檢測CENTRAL_NODE中央節點的餘額,因為3001節點已經有它自己的區塊鏈了:
$ blockchain_go getbalance -address CENTRAL_NODE
Balance of ‘CENTRAL_NODE’: 10
節點 3002
開啟新的視窗,設定ID為3002,然後生成錢包,這個是個礦機節點。初始化區塊鏈:
$ cp blockchain_genesis.db blockchain_3002.db
啟動節點
$ blockchain_go startnode -miner MINER_WALLET
節點 3001
傳送幣:
blockchaingosend−fromWALLET1−toWALLET3−amount1blockchaingosend−fromWALLET1−toWALLET3−amount1
blockchain_go send -from WALLET_2 -to WALLET_4 -amount 1
節點 3002
很快,轉到礦機節點後,可以看到它挖出了新的區塊。也檢測了中央節點的output。
節點 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
這裡就是全部了!
結論
這是我們這個系列文章的最後一篇了。應該要實現真正的P2P原型網路,但是確實沒有這麼多的時間。希望這篇文章能解答一些你關於比特幣技術的疑問,並且獲得新姿勢,你也可以自己去找到答案。還有很多有趣的知識藏在比特幣技術中。祝你開車愉快!
P.S. 你可以開始通過實現addr訊息來完善網路,就如比特幣網路協議中描述的一樣。這是訊息非常重要,因為它可以讓節點互相發現彼此。我已經開始實現它了,但是還沒有完成。
-
學院Go語言視訊主頁
https://edu.csdn.net/lecturer/1928 -
掃碼獲取海量視訊及原始碼 QQ群:721929980
相關文章
- 2.8檔案操作
- 2.8(學號:3025)
- sicp每日一題[2.8]每日一題
- Flutter 2.8 更新詳解Flutter
- Flutter 2.8 正式釋出Flutter
- Kafka2.8安裝Kafka
- 【運籌學】P62 2.8
- kvm網路,docker網路,,vm網路Docker
- TypeScript 2.8下的終極React元件模式TypeScriptReact元件模式
- Windows下Kafka2.8環境搭建教程WindowsKafka
- 在 Fedora 中獲取最新的 Ansible 2.8
- 上週熱點回顧(2.8-2.14)
- 1.2網際網路的網路結構
- [網際網路]網際網路公司的種類
- 世界網際網路大會|網路安全點亮烏鎮“網際網路之光”
- 網際網路+
- (四)卷積神經網路 -- 8 網路中的網路(NiN)卷積神經網路
- 網路
- 網際網路如何推廣 網際網路推廣
- 網路攻防路2
- 計算機網路之網路層計算機網路
- [計算機網路]網路攻擊計算機網路
- 【網路流】網路流基本概念
- 計算機網路(一) --網路模型計算機網路模型
- 網路分流器-網路分流器TAP網路流量分析
- 網路分流器|網路分流器|移動網際網路採集方案
- 【網際網路】在網際網路中隱私在何方?
- 網路組網方式
- NFT House:調查顯示三成網友認識NFT但擁有者僅佔2.8%
- 網路分流器-網路分流器-網路安全評估探討
- 網路分流器-淺談網路資料中心IDC網路技術
- 網路故障排除工具 | 快速定位網路故障
- 計算機網路之網路介面層計算機網路
- 網路虛擬化VXLAN網路架構架構
- 深入淺出Kubernetes網路:容器網路初探
- 計算機網路總結(網路層)計算機網路
- 網際網路怎樣推廣 網際網路如何推廣
- redhat 6.8升級預設cmake 2.8到cmake 3.9Redhat