# 前言
本版本區塊鏈不會實現一個P2P網絡,但會實現基于網絡的、bitcoin的一個最常見的場景:
1. 中心節點創建一個區塊鏈。
2. 一個其他(錢包)節點連接到中心節點并下載區塊鏈。
3. 另一個(礦工)節點連接到中心節點并下載區塊鏈。
4. 錢包節點創建一筆交易。
5. 礦工節點接收交易,并將交易保存到內存池中。
6. 當內存池中有足夠的交易時,礦工開始挖一個新塊。
7. 當挖出一個新塊后,將其發送到中心節點。
8. 錢包節點與中心節點進行同步。
9. 錢包節點的用戶檢查他們的支付是否成功。
這就是比特幣中的一般流程,也是比特幣最常見最重要的用戶場景。
# 節點角色
盡管節點具有完備成熟的屬性,但是它們也可以在網絡中扮演不同角色。比如:
1. 礦工 這樣的節點運行于強大或專用的硬件(比如 ASIC)之上,它們唯一的目標是,盡可能快地挖出新塊。礦工是區塊鏈中唯一可能會用到工作量證明的角色,因為挖礦實際上意味著解決 PoW 難題。在權益證明 PoS 的區塊鏈中,沒有挖礦。
2. 全節點 這些節點驗證礦工挖出來的塊的有效性,并對交易進行確認。為此,他們必須擁有區塊鏈的完整拷貝。同時,全節點執行路由操作,幫助其他節點發現彼此。對于網絡來說,非常重要的一段就是要有足夠多的全節點。因為正是這些節點執行了決策功能:他們決定了一個塊或一筆交易的有效性。
3. SPV SPV 表示 Simplified Payment Verification,簡單支付驗證。這些節點并不存儲整個區塊鏈副本,但是仍然能夠對交易進行驗證(不過不是驗證全部交易,而是一個交易子集,比如,發送到某個指定地址的交易)。一個 SPV 節點依賴一個全節點來獲取數據,可能有多個 SPV 節點連接到一個全節點。SPV 使得錢包應用成為可能:一個人不需要下載整個區塊鏈,但是仍能夠驗證他的交易。
# 網絡簡化
我們將使用**端口號作為節點標識符**,而不是使用 IP 地址,比如將會有這樣地址的節點:**127.0.0.1:3000**,**127.0.0.1:3001**,**127.0.0.1:3002**等等。我們叫它端口節點(port node) ID,并使用環境變量`NODE_ID`對它們進行設置。故而,你可以打開多個終端窗口,設置不同的`NODE_ID`運行不同的節點。
這個方法也需要有不同的區塊鏈和錢包文件。它們現在必須依賴于節點 ID 進行命名,比如 blockchain_3000.db, blockchain_30001.db和wallet_3000.db, wallet_30001.db 等等。
# 實現
在 Bitcoin Core 中,硬編碼了[DNS seeds](https://link.jianshu.com/?t=https://bitcoin.org/en/glossary/dns-seed)。雖然這些并不是節點,但是 DNS 服務器知道一些節點的地址。當你啟動一個全新的 Bitcoin Core 時,它會連接到一個種子節點,獲取全節點列表,隨后從這些節點中下載區塊鏈。
不過在我們目前的實現中,無法做到完全的去中心化,因為會出現中心化的特點。我們會有三個節點:
1. 一個中心節點。所有其他節點都會連接到這個節點,這個節點會在其他節點之間發送數據。
2. 一個礦工節點。這個節點會在內存池中存儲新的交易,當有足夠的交易時,它就會打包挖出一個新塊。
3. 一個錢包節點。這個節點會被用作在錢包之間發送幣。但是與 SPV 節點不同,它存儲了區塊鏈的一個完整副本。
節點通過消息(message)進行交流:整個通訊就是:請求--回復--請求--回復...這么一個模式進行,第一個請求可能需要經過請求節點與被請求節點之間多次往返消息(一般為多個成對消息)才能完成。
當一個新的節點開始運行時,它會從一個 DNS 種子獲取幾個節點,給它們發送`version`消息,在我們的實現看起來就像是這樣:
~~~
type version struct {
Version int//區塊鏈版本號
BestHeight int//存儲區塊鏈中節點的高度。
AddrFrom string//存儲發送消息者的地址
}
~~~
接收到`version`消息的節點應該做什么呢?它會響應自己的`version`消息。這是一種握手??:如果沒有事先互相問候,就不可能有其他交流。`version`用于找到一個更長的區塊鏈。當一個節點接收到`version`消息,它會檢查本節點的區塊鏈是否比`BestHeight`的值更大。如果不是,節點就會請求并下載缺失的塊。
為了接收消息,我們需要一個服務器(每個節點都是服務器):
~~~
var nodeAddress string
var knownNodes = []string{"localhost:3000"}//中心節點的地址數組
func StartServer(nodeID, minerAddress string) {//nodeID為當前節點
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)//非中心節點必須向中心節點發送`version`消息來查詢是否自己的區塊鏈已過時
}
//優雅的Go語言
for {//開啟無限循環
conn, err := ln.Accept()//等待連接
go handleConnection(conn, bc)//異步,處理連接
}
}
~~~
非中心節點向中心節點發送消息,查詢自己的區塊鏈是否已經過時:
~~~
func sendVersion(addr string, bc *Blockchain) {
bestHeight := bc.GetBestHeight()
payload := gobEncode(version{nodeVersion, bestHeight, nodeAddress})
request := append(commandToBytes("version"), payload...)//“version”是命令,payload是gob編碼的消息結構
sendData(addr, request)
}
~~~
`commandToBytes`看起來是這樣:
~~~
//創建一個 12 字節的緩沖區,并用命令名進行填充,將剩下的字節置為空
func commandToBytes(command string) []byte {//命令字符串轉為數組切片
var bytes [commandLength]byte//commandLength為12個字節
for i, c := range command {
bytes[i] = byte(c)
}
return bytes[:]//返回切片
}
~~~
解析命令的函數如下:
~~~
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("接收到 %s 命令\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)//發送getblocks消息
} else if myBestHeight > foreignerBestHeight {
sendVersion(payload.AddrFrom, bc)//回復version消息
}
if !nodeIsKnown(payload.AddrFrom) {//如果中心節點不包含本地地址
knownNodes = append(knownNodes, payload.AddrFrom)//將本地地址加入到中心節點地址數組中
}
}
~~~
首先,我們需要對請求進行解碼,提取有效信息。所有的處理器在這部分都類似。
然后節點將從消息中提取的`BestHeight`與自身進行比較。如果自身節點的區塊鏈更長,它會回復`version`消息;否則,它會發送`getblocks`消息。
## getblocks
~~~
type getblocks struct {
AddrFrom string
}
~~~
`getblocks`意為 “給我看一下你有什么區塊”(在比特幣中,這會更加復雜)。注意,它并沒有說“把你全部的區塊給我”,而是請求了一個塊哈希的列表。這是為了減輕網絡負載,因為區塊可以從不同的節點下載,并且我們不想從一個單一節點下載數十 GB 的數據。
處理命令十分簡單:
~~~
func handleGetBlocks(request []byte, bc *Blockchain) {
...
blocks := bc.GetBlockHashes()//接收到getblocks命令的節點獲得本地的區塊哈希列表
sendInv(payload.AddrFrom, "block", blocks)//然后將本地的區塊哈希列表回復給請求者
}
~~~
在我們簡化版的實現中,它會返回**所有塊哈希**。
## inv
~~~
type inv struct {
AddrFrom string
Type string//payload的類型:block或tx
Items [][]byte//當前節點的所有塊的哈希或交易的哈希
}
~~~
比特幣使用`inv`來向其他節點展示當前節點有什么塊和交易。再次提醒,它沒有包含完整的區塊鏈和交易,僅僅是哈希而已。`Type`字段表明了這是塊還是交易。
處理`inv`稍顯復雜:
~~~
func handleInv(request []byte, bc *Blockchain) {//請求區塊者收到回復節點返回的blocks后進行處理
...
fmt.Printf("Recevied inventory with %d %s\n", len(payload.Items), payload.Type)
if payload.Type == "block" {//塊哈希
blocksInTransit = payload.Items
blockHash := payload.Items[0]
//給回復節點發送getdata命令,請求該塊:哦,你有這么一個塊,麻煩給我
//注意,每次只能請求一個block,所以這次請求的是payload里面的第0個
sendGetData(payload.AddrFrom, "block", blockHash)
newInTransit := [][]byte{}
for _, b := range blocksInTransit {//迭代收到的區塊哈希列表
//如果b不等于收到的第0個哈希值(第0個塊在上面getdata命令中已經請求下載),加入到新的newInTransit(尚未下載該塊)
if bytes.Compare(b, blockHash) != 0 {//第0個已經請求下載了啦,所以需要排除,下次不要再請求其他節點下載
newInTransit = append(newInTransit, b)
}
}
//更新blocksInTransit,blocksInTransit用來跟蹤已下載的塊,以后會根據此表請求下載其他的區塊,直到全部下載完畢
blocksInTransit = newInTransit
}
if payload.Type == "tx" {
txID := payload.Items[0]
if mempool[hex.EncodeToString(txID)].ID == nil {//如果本地內存池中不存在這么一個交易
sendGetData(payload.AddrFrom, "tx", txID)//收到回復節點返回的tx,請求該交易:哦,你有這么一個交易,麻煩給我
}
}
}
~~~
## 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 和 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()//最好不要調用Reindex,而是迭代下載的blocks,使用 UTXOSet.Update(block)更新數據庫的UTXO表
}
}
~~~
當接收到一個新塊時,我們把它放到區塊鏈里面。如果還有更多的區塊需要下載,我們繼續從上一個下載的塊的那個節點繼續請求。當最后把所有塊都下載完后,對 UTXO 集進行重新索引。
> TODO:并非無條件信任,我們應該在將每個塊加入到區塊鏈之前對它們進行驗證。
> TODO: 并非運行 UTXOSet.Reindex(), 而是應該使用 UTXOSet.Update(block),因為如果區塊鏈很大,它將需要很多時間來對整個 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 {//如果交易池中的交易大于或者等于2筆,并且當前是礦工節點
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("所有交易均非法,正在等待新的交易到來...")
return
}
cbTx := NewCoinbaseTX(miningAddress, "")//coninbase交易,挖礦獎勵
txs = append(txs, cbTx)
newBlock := bc.MineBlock(txs)//挖礦
UTXOSet := UTXOSet{bc}
UTXOSet.Reindex()//最好不要重新索引,而是執行update
fmt.Println("已挖到新區塊!")
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
}
}
}
}
~~~
# 場景檢驗
## 創建中心節點
在第一個終端窗口中將`NODE_ID`設置為 3000,作為中心節點,3001為錢包節點,3002為礦工節點:
讓我們來回顧一下上面定義的場景。
首先,在第一個終端窗口中將`NODE_ID`設置為 3000(`export NODE_ID=3000`)。為了讓你知道什么節點執行什么操作,我會使用像**NODE 3000**或**NODE 3001**進行標識。
### NODE 3000
創建一個錢包和一個新的區塊鏈:
~~~
$ main createblockchain -address CENTREAL_NODE
~~~
(為了簡潔起見,我會使用假地址。)
然后,會生成一個僅包含創世塊的區塊鏈。我們需要保存塊,并在其他節點使用。創世塊承擔了一條鏈標識符的角色(在 Bitcoin Core 中,創世塊是硬編碼的)
~~~
$ cp blockchain_3000.db blockchain_genesis.db
~~~
### NODE 3001
接下來,打開一個新的終端窗口,將 node ID 設置為 3001。這會作為一個錢包節點。通過`blockchain_go createwallet`生成一些地址,我們把這些地址叫做 WALLET\_1, WALLET\_2, WALLET\_3.
### NODE 3000
向錢包地址發送一些幣:
~~~
$ main send -from CENTREAL_NODE -to WALLET_1 -amount 10 -mine
$ main send -from CENTREAL_NODE -to WALLET_2 -amount 10 -mine
~~~
`-mine`標志指的是塊會立刻被同一節點挖出來。我們必須要有這個標志,因為初始狀態時,網絡中沒有礦工節點。
啟動節點:
~~~
$ main startnode
~~~
這個節點會持續運行,直到本文定義的場景結束。
### NODE 3001
啟動上面保存創世塊節點的區塊鏈:
~~~
$ cp blockchain_genesis.db blockchain_3001.db
~~~
運行節點:
~~~
$ main startnode
~~~
它會從中心節點下載所有區塊。為了檢查一切正常,暫停節點運行并檢查余額:
~~~
$ main getbalance -address WALLET_1
Balance of 'WALLET_1': 10
$ main getbalance -address WALLET_2
Balance of 'WALLET_2': 10
~~~
你還可以檢查`CENTRAL_NODE`地址的余額,因為 node 3001 現在有它自己的區塊鏈:
~~~
$ main getbalance -address CENTRAL_NODE
Balance of 'CENTRAL_NODE': 10
~~~
### NODE 3002
打開一個新的終端窗口,將它的 ID 設置為 3002,然后生成一個錢包。這會是一個礦工節點。初始化區塊鏈:
~~~
$ cp blockchain_genesis.db blockchain_3002.db
~~~
啟動節點:
~~~
$ main startnode -miner MINER_WALLET
~~~
### NODE 3001
發送一些幣:
~~~
$ main send -from WALLET_1 -to WALLET_3 -amount 1
$ main send -from WALLET_2 -to WALLET_4 -amount 1
~~~
### NODE 3002
迅速切換到礦工節點,你會看到挖出了一個新塊!同時,檢查中心節點的輸出。
### NODE 3001
切換到錢包節點并啟動:
~~~
$ main startnode
~~~
它會下載最近挖出來的塊!

- 重要更新說明
- linechain發布
- linechain新版設計
- 引言一
- 引言二
- 引言三
- vs-code設置及開發環境設置
- BoltDB數據庫應用
- 關于Go語言、VS-code的一些Tips
- 區塊鏈的架構
- 網絡通信與區塊鏈
- 單元測試
- 比特幣腳本語言
- 關于區塊鏈的一些概念
- 區塊鏈組件
- 區塊鏈第一版:基本原型
- 區塊鏈第二版:增加工作量證明
- 區塊鏈第三版:持久化
- 區塊鏈第四版:交易
- 區塊鏈第五版:實現錢包
- 區塊鏈第六版:實現UTXO集
- 區塊鏈第七版:網絡
- 階段小結
- 區塊鏈第八版:P2P
- P2P網絡架構
- 區塊鏈網絡層
- P2P區塊鏈最簡體驗
- libp2p建立P2P網絡的關鍵概念
- 區塊鏈結構層設計與實現
- 用戶交互層設計與實現
- 網絡層設計與實現
- 建立節點發現機制
- 向區塊鏈網絡請求區塊信息
- 向區塊鏈網絡發布消息
- 運行區塊鏈
- LineChain
- 系統運行流程
- Multihash
- 區塊鏈網絡的節點發現機制深入探討
- DHT
- Bootstrap
- 連接到所有引導節點
- Advertise
- 搜索其它peers
- 連接到搜到的其它peers
- 區塊鏈網絡的消息訂發布-訂閱機制深入探討
- LineChain:適用于智能合約編程的腳本語言支持
- LineChain:解決分叉問題
- LineChain:多重簽名
- libp2p升級到v0.22版本
- 以太坊基礎
- 重溫以太坊的樹結構
- 世界狀態樹
- (智能合約)賬戶存儲樹
- 交易樹
- 交易收據樹
- 小結
- 以太坊的存儲結構
- 以太坊狀態數據庫
- MPT
- 以太坊POW共識算法
- 智能合約存儲
- Polygon Edge
- block結構
- transaction數據結構
- 數據結構小結
- 關于本區塊鏈的一些說明
- UML工具-PlantUML
- libp2p介紹
- JSON-RPC
- docker制作:啟動多個應用系統
- Dockerfile
- docker-entrypoint.sh
- supervisord.conf
- docker run
- nginx.conf
- docker基礎操作整理
- jupyter計算交互環境
- git技巧一
- git技巧二
- 使用github項目的最佳實踐
- windows下package管理工具