Introduction
到目前為止,我們已經構建了一個具有所有關鍵功能的區塊鏈:匿名,安全和隨機生成的地址;區塊鏈資料儲存;工作證明制度;儲存交易的可靠方式。雖然這些功能至關重要,但這還不夠。是什麼讓這些功能真正發揮作用,使加密貨幣成為可能的是網路。在單臺計算機上執行這種區塊鏈實現有什麼用?當只有一個使用者時,基於密碼術的功能有什麼用?它的網路使所有這些機制起作用並且有用。
您可以將區塊鏈功能視為規則,類似於人們希望一起生活和繁榮時所建立的規則。一種社會安排。區塊鏈網路是遵循相同規則的程式社群,它遵循使網路活躍的規則。同樣,當人們分享相同的想法時,他們會變得更強大,可以共同創造更好的生活。如果有人遵循不同的規則,他們將生活在一個單獨的社會(州,公社等)。同樣,如果區塊鏈節點遵循不同的規則,它們將形成一個單獨的網路。
這是非常重要的:沒有網路,沒有大多數節點共享相同的規則,這些規則是沒用的!
免責宣告:不幸的是,我沒有足夠的時間來實現真正的P2P網路原型。在本文中,我將演示一個最常見的場景,它涉及不同型別的節點。改善這種情況並使其成為P2P網路對您來說是一個很好的挑戰和實踐!此外,我不能保證除了本文中實現的方案之外的其他方案也可以。抱歉!這部分介紹了重要的程式碼更改,因此在這裡解釋所有這些都沒有意義。請參閱this page檢視自上一篇文章以來的所有更改。
Blockchain Network
區塊鏈網路是分散的,這意味著沒有伺服器可以使用伺服器來獲取或處理資料。在區塊鏈網路中有節點,每個節點都是網路的成熟成員。節點就是一切:它既是客戶端又是伺服器。記住這一點非常重要,因為它與通常的Web應用程式非常不同。
區塊鏈網路是P2P(點對點)網路,這意味著節點彼此直接連線。它的拓撲結構是扁平的,因為節點角色中沒有層次結構。這裡的示意圖如下:
(Business vector created by Dooder - Freepik.com)
這種網路中的節點更難實現,因為它們必須執行大量操作。每個節點必須與多個其他節點互動,它必須請求其他節點的狀態,將其與自己的狀態進行比較,並在其過時時更新其狀態。
Node Roles
儘管是完整的,區塊鏈節點可以在網路中扮演不同的角色。他們來了:
- Miner.這些節點在強大的或專用的硬體(如ASIC)上執行,其唯一目標是儘可能快地挖掘新塊。礦工只能在使用工作證明的區塊鏈中使用,因為挖掘實際上意味著解決PoW難題。例如,在Proof-of-Stake區塊鏈中,沒有采礦。
- Full node.這些節點驗證礦工開採的塊並驗證交易。要做到這一點,他們必須擁有區塊鏈的全部副本。此外,這樣的節點執行這樣的路由操作,例如幫助其他節點發現彼此。對於網路而言,擁有許多完整節點非常重要,因為正是這些節點做出了決策:他們決定一個塊或事務是否有效。
- SPV.SPV代表簡化付款驗證。這些節點不儲存區塊鏈的完整副本,但它們仍然能夠驗證事務(不是所有事務,而是一個子集,例如,傳送到特定地址的子集)。 SPV節點依賴於完整節點來獲取資料,並且可能有許多SPV節點連線到一個完整節點。 SPV使錢包應用成為可能:一個不需要下載完整的區塊鏈,但仍然可以驗證他們的交易。
Network simplification
要在我們的區塊鏈中實現網路,我們必須簡化一些事情。問題是我們沒有很多計算機來模擬具有多個節點的網路。我們可以使用虛擬機器或Docker來解決這個問題,但它可能會使一切變得更加困難:你必須解決可能的虛擬機器或Docker問題,而我的目標只是集中在區塊鏈實現上。因此,我們希望在一臺機器上執行多個區塊鏈節點,同時我們希望它們具有不同的地址。要實現這一點,我們將使用埠作為節點識別符號而不是IP地址。例如,將有節點具有地址:127.0.0.1:3000
, 127.0.0.1:3001
, 127.0.0.1:3002
我們將呼叫埠節點ID並使用NODE_ID
環境變數來設定它們。因此,您可以開啟多個終端視窗,設定不同NODE_ID
s並執行不同的節點。
這種方法還需要具有不同的區塊鏈和錢包檔案。它們現在必須依賴於節點ID並命名為blockchain_3000.db
, blockchain_30001.db
and wallet_3000.db
, wallet_30001.db
, etc.
Implementation
那麼,當您下載比特幣核心並首次執行它時會發生什麼?它必須連線到某個節點才能下載區塊鏈的最新狀態。考慮到您的計算機不知道所有或某些比特幣節點,這個節點是什麼?
比特幣核心中的節點地址硬編碼將是一個錯誤:節點可能受到攻擊或關閉,這可能導致新節點無法加入網路。相反,在比特幣核心中,有DNS seeds硬編碼。這些不是節點,而是知道某些節點地址的DNS伺服器。當你啟動一個乾淨的比特幣核心時,它將連線到其中一個種子並獲得一個完整節點列表,然後它將從中下載區塊鏈。
在我們的實施中,將會集中化。我們將有三個節點:
- 中心節點。這是所有其他節點將連線到的節點,這是將在其他節點之間傳送資料的節點。
- 礦工節點。此節點將在mempool中儲存新事務,並且當有足夠的事務時,它將挖掘新塊。
- 錢包節點。此節點將用於在錢包之間傳送硬幣。與SPV節點不同,它會儲存區塊鏈的完整副本。
The Scenario
本文的目標是實現以下場景:
- 中心節點建立區塊鏈。
- 其他(錢包)節點連線到它並下載區塊鏈。
- 另一個(礦工)節點連線到中央節點並下載區塊鏈。
- 錢包節點建立一個事務。
- 礦工節點接收事務並將其保留在其記憶體池中。
- 當記憶體池中有足夠的事務時,礦工開始挖掘新塊。
- 當開採新塊時,它將傳送到中央節點。
- 錢包節點與中心節點同步。
- 錢包節點的使用者檢查他們的付款是否成功。
這就是比特幣的樣子。即使我們不打算建立一個真正的P2P網路,我們也將實現一個真實的,比特幣的主要和最重要的用例。
version
節點通過訊息進行通訊。執行新節點時,它會從DNS種子中獲取多個節點併傳送它們version
訊息,在我們的實現中將如下所示:
type version struct {
Version int
BestHeight int
AddrFrom string
}
複製程式碼
我們只有一個區塊鏈版本,所以Version
欄位不會保留任何重要資訊。BestHeight
儲存節點區塊鏈的長度。AddFrom
儲存發件人的地址。
接收節點的節點應該是什麼?version
訊息呢?它會以自己的方式迴應version
資訊。這是一種握手:沒有事先互相問候,就不可能有其他互動。但這不僅僅是禮貌:version
用於查詢更長的區塊鏈。當節點收到一個version
訊息它檢查節點的區塊鏈是否長於值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位元組的緩衝區並使用命令名填充它,將rest位元組留空。有一個相反的功能:
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
message.
getblocks
type getblocks struct {
AddrFrom string
}
複製程式碼
getblocks
意思是“告訴我你有什麼塊”(在比特幣中,它更復雜)。注意,它沒有說“給我所有的塊”,而是它請求塊雜湊列表。這樣做是為了減少網路負載,因為可以從不同的節點下載塊,我們不想從一個節點下載幾十GB。
處理命令很簡單:
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
}
複製程式碼
Bitcoin 使用 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
變數來跟蹤下載的塊。這允許我們從不同節點下載塊。在將塊放入運輸狀態後,我們傳送getdata
命令給發件人inv
訊息和更新blocksInTransit
。在真正的P2P網路中,我們希望從不同節點傳輸塊。
在我們的實施中,我們永遠不會傳送有多個雜湊的inv
。這就是為什麼payload.Type == "tx"
只有第一個雜湊值。然後我們檢查我們的mempool中是否已經有雜湊值,如果沒有,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)
}
}
複製程式碼
處理程式很簡單:如果它們請求塊,則返回塊;如果他們請求交易,則返回交易。請注意,我們不會檢查我們是否確實擁有此塊或事務。這是一個缺陷:)
block and 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()
}
}
複製程式碼
當我們收到一個新區塊時,我們將其放入區塊鏈中。如果要下載更多塊,我們會從我們下載前一個塊的同一節點請求它們。當我們最終下載了所有塊時,重新索引UTXO集。
TODO:我們應該在將其新增到區塊鏈之前驗證每個傳入的塊,而不是無條件地信任。TODO:不應執行UTXOSet.Reindex(),而應使用UTXOSet.Update(塊),因為如果區塊鏈很大,則需要花費大量時間來重新索引整個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 {
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(同樣,事務必須在放入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
僅在礦工節點上設定。噹噹前(礦工)節點的mempool中有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交易。挖掘塊後,重新索引UTXO集。
TODO:同樣,應該使用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
}
複製程式碼
交易完成後,它將從mempool中刪除。當前節點知道,接收的每個其他節點inv
帶有新塊雜湊的訊息。他們可以在處理訊息後請求塊。
Result
讓我們播放我們之前定義的場景。
首先, set NODE_ID
to 3000 (export NODE_ID=3000
)在第一個終端視窗。我會用像徽章一樣的徽章NODE 3000
or NODE 3001
在下一段之前,讓您知道要對哪個節點執行操作。
NODE 3000
建立一個錢包和一個新的區塊鏈:
$ blockchain_go createblockchain -address CENTREAL_NODE
複製程式碼
(為了清晰和簡潔,我將使用虛假地址)
之後,區塊鏈將包含單個發生區塊。我們需要儲存塊並在其他節點中使用它。 Genesis塊用作區塊鏈的識別符號(在比特幣核心中,創世塊是硬編碼的)。
$ cp blockchain_3000.db blockchain_genesis.db
複製程式碼
NODE 3001
接下來,開啟一個新的終端視窗並將節點ID設定為3001.這將是一個錢包節點。生成一些地址blockchain_go createwallet
,我們稱之為這些地址WALLET_1
, WALLET_2
, WALLET_3
.
NODE 3000
將一些硬幣傳送到錢包地址:
$ blockchain_go send -from CENTREAL_NODE -to WALLET_1 -amount 10 -mine
$ blockchain_go send -from CENTREAL_NODE -to WALLET_2 -amount 10 -mine
複製程式碼
-mine
flag表示該塊將立即被同一節點挖掘。我們必須擁有此標誌,因為最初網路中沒有礦工節點。
Start the node:
$ blockchain_go startnode
複製程式碼
節點必須一直執行直到方案結束。
NODE 3001
使用上面儲存的genesis塊啟動節點的區塊鏈:
$ cp blockchain_genesis.db blockchain_3001.db
複製程式碼
Run the node:
$ blockchain_go startnode
複製程式碼
它將從中央節點下載所有塊。要檢查一切正常,請停止節點並檢查餘額:
$ blockchain_go getbalance -address WALLET_1
Balance of 'WALLET_1': 10
$ blockchain_go getbalance -address WALLET_2
Balance of 'WALLET_2': 10
複製程式碼
此外,您可以檢查平衡CENTRAL_NODE
地址,因為節點3001現在有其區塊鏈:
$ blockchain_go getbalance -address CENTRAL_NODE
Balance of 'CENTRAL_NODE': 10
複製程式碼
NODE 3002
開啟一個新的終端視窗並將其ID設定為3002,然後生成一個錢包。這將是一個礦工節點。初始化區塊鏈:
$ cp blockchain_genesis.db blockchain_3002.db
複製程式碼
啟動節點:
$ blockchain_go startnode -miner MINER_WALLET
複製程式碼
NODE 3001
Send some coins:
$ blockchain_go send -from WALLET_1 -to WALLET_3 -amount 1
$ blockchain_go send -from WALLET_2 -to WALLET_4 -amount 1
複製程式碼
NODE 3002
很快!切換到礦工節點並看到它挖掘一個新塊!另外,檢查中央節點的輸出。
NODE 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
複製程式碼
That’s it!
Conclusion
這是該系列的最後一部分。我可以釋出一些實現P2P網路真實原型的帖子,但我沒有時間做這件事。我希望這篇文章回答你關於比特幣技術的一些問題並提出新的問題,你可以自己找到答案。比特幣技術中隱藏著更多有趣的東西!祝好運!
PS您可以通過實施來開始改進網路addr
訊息,如比特幣網路協議(連結如下)所述。這是一個非常重要的訊息,因為它允許節點相互發現。我開始實現它,但還沒有完成!