摘要:盡管我們不會(huì)實(shí)現(xiàn)一個(gè)真實(shí)的網(wǎng)絡(luò),但是我們會(huì)實(shí)現(xiàn)一個(gè)真是,也是比特幣最常見(jiàn)最重要的用戶(hù)場(chǎng)景。不過(guò),這并不是處于禮貌用于找到一個(gè)更長(zhǎng)的區(qū)塊鏈。意為給我看一下你有什么區(qū)塊在比特幣中,這會(huì)更加復(fù)雜。
翻譯的系列文章我已經(jīng)放到了 GitHub 上:blockchain-tutorial,后續(xù)如有更新都會(huì)在 GitHub 上,可能就不在這里同步了。如果想直接運(yùn)行代碼,也可以 clone GitHub 上的教程倉(cāng)庫(kù),進(jìn)入 src 目錄執(zhí)行 make 即可。
到目前為止,我們所構(gòu)建的原型已經(jīng)具備了區(qū)塊鏈所有的關(guān)鍵特性:匿名,安全,隨機(jī)生成的地址;區(qū)塊鏈數(shù)據(jù)存儲(chǔ);工作量證明系統(tǒng);可靠地存儲(chǔ)交易。盡管這些特性都不可或缺,但是仍有不足。能夠使得這些特性真正發(fā)光發(fā)熱,使得加密貨幣成為可能的,是網(wǎng)絡(luò)(network)。如果實(shí)現(xiàn)的這樣一個(gè)區(qū)塊鏈僅僅運(yùn)行在單一節(jié)點(diǎn)上,有什么用呢?如果只有一個(gè)用戶(hù),那么這些基于密碼學(xué)的特性,又有什么用呢?正是由于網(wǎng)絡(luò),才使得整個(gè)機(jī)制能夠運(yùn)轉(zhuǎn)和發(fā)光發(fā)熱。
你可以將這些區(qū)塊鏈特性認(rèn)為是規(guī)則(rule),類(lèi)似于人類(lèi)在一起生活,繁衍生息建立的規(guī)則,一種社會(huì)安排。區(qū)塊鏈網(wǎng)絡(luò)就是一個(gè)程序社區(qū),里面的每個(gè)程序都遵循同樣的規(guī)則,正是由于遵循著同一個(gè)規(guī)則,才使得網(wǎng)絡(luò)能夠長(zhǎng)存。類(lèi)似的,當(dāng)人們都有著同樣的想法,就能夠?qū)⑷^攥在一起構(gòu)建一個(gè)更好的生活。如果有人遵循著不同的規(guī)則,那么他們就將生活在一個(gè)分裂的社區(qū)(州,公社,等等)中。同樣的,如果有區(qū)塊鏈節(jié)點(diǎn)遵循不同的規(guī)則,那么也會(huì)形成一個(gè)分裂的網(wǎng)絡(luò)。
重點(diǎn)在于:如果沒(méi)有網(wǎng)絡(luò),或者大部分節(jié)點(diǎn)都不遵守同樣的規(guī)則,那么規(guī)則就會(huì)形同虛設(shè),毫無(wú)用處!
聲明:不幸的是,我并沒(méi)有足夠的時(shí)間來(lái)實(shí)現(xiàn)一個(gè)真實(shí)的 P2P 網(wǎng)絡(luò)原型。本文我會(huì)展示一個(gè)最常見(jiàn)的場(chǎng)景,這個(gè)場(chǎng)景涉及不同類(lèi)型的節(jié)點(diǎn)。繼續(xù)改進(jìn)這個(gè)場(chǎng)景,將它實(shí)現(xiàn)為一個(gè) P2P 網(wǎng)絡(luò),對(duì)你來(lái)說(shuō)是一個(gè)很好的挑戰(zhàn)和實(shí)踐!除了本文的場(chǎng)景,我也無(wú)法保證在其他場(chǎng)景將會(huì)正常工作。抱歉!區(qū)塊鏈網(wǎng)絡(luò)本文的代碼實(shí)現(xiàn)變化很大,請(qǐng)點(diǎn)擊 這里 查看所有的代碼更改。
區(qū)塊鏈網(wǎng)絡(luò)是去中心化的,這意味著沒(méi)有服務(wù)器,客戶(hù)端也不需要依賴(lài)服務(wù)器來(lái)獲取或處理數(shù)據(jù)。在區(qū)塊鏈網(wǎng)絡(luò)中,有的是節(jié)點(diǎn),每個(gè)節(jié)點(diǎn)是網(wǎng)絡(luò)的一個(gè)完全(full-fledged)成員。節(jié)點(diǎn)就是一切:它既是一個(gè)客戶(hù)端,也是一個(gè)服務(wù)器。這一點(diǎn)需要牢記于心,因?yàn)檫@與傳統(tǒng)的網(wǎng)頁(yè)應(yīng)用非常不同。
區(qū)塊鏈網(wǎng)絡(luò)是一個(gè) P2P(Peer-to-Peer,端到端)的網(wǎng)絡(luò),即節(jié)點(diǎn)直接連接到其他節(jié)點(diǎn)。它的拓?fù)涫潜馄降模驗(yàn)樵诠?jié)點(diǎn)的世界中沒(méi)有層級(jí)之分。下面是它的示意圖:
Business vector created by Dooder - Freepik.com
要實(shí)現(xiàn)這樣一個(gè)網(wǎng)絡(luò)節(jié)點(diǎn)更加困難,因?yàn)樗鼈儽仨殘?zhí)行很多操作。每個(gè)節(jié)點(diǎn)必須與很多其他節(jié)點(diǎn)進(jìn)行交互,它必須請(qǐng)求其他節(jié)點(diǎn)的狀態(tài),與自己的狀態(tài)進(jìn)行比較,當(dāng)狀態(tài)過(guò)時(shí)時(shí)進(jìn)行更新。
節(jié)點(diǎn)角色盡管節(jié)點(diǎn)具有完備成熟的屬性,但是它們也可以在網(wǎng)絡(luò)中扮演不同角色。比如:
礦工
這樣的節(jié)點(diǎn)運(yùn)行于強(qiáng)大或?qū)S玫挠布ū热?ASIC)之上,它們唯一的目標(biāo)是,盡可能快地挖出新塊。礦工是區(qū)塊鏈中唯一可能會(huì)用到工作量證明的角色,因?yàn)橥诘V實(shí)際上意味著解決 PoW 難題。在權(quán)益證明 PoS 的區(qū)塊鏈中,沒(méi)有挖礦。
全節(jié)點(diǎn)
這些節(jié)點(diǎn)驗(yàn)證礦工挖出來(lái)的塊的有效性,并對(duì)交易進(jìn)行確認(rèn)。為此,他們必須擁有區(qū)塊鏈的完整拷貝。同時(shí),全節(jié)點(diǎn)執(zhí)行路由操作,幫助其他節(jié)點(diǎn)發(fā)現(xiàn)彼此。對(duì)于網(wǎng)絡(luò)來(lái)說(shuō),非常重要的一段就是要有足夠多的全節(jié)點(diǎn)。因?yàn)檎沁@些節(jié)點(diǎn)執(zhí)行了決策功能:他們決定了一個(gè)塊或一筆交易的有效性。
SPV
SPV 表示 Simplified Payment Verification,簡(jiǎn)單支付驗(yàn)證。這些節(jié)點(diǎn)并不存儲(chǔ)整個(gè)區(qū)塊鏈副本,但是仍然能夠?qū)灰走M(jìn)行驗(yàn)證(不過(guò)不是驗(yàn)證全部交易,而是一個(gè)交易子集,比如,發(fā)送到某個(gè)指定地址的交易)。一個(gè) SPV 節(jié)點(diǎn)依賴(lài)一個(gè)全節(jié)點(diǎn)來(lái)獲取數(shù)據(jù),可能有多個(gè) SPV 節(jié)點(diǎn)連接到一個(gè)全節(jié)點(diǎn)。SPV 使得錢(qián)包應(yīng)用成為可能:一個(gè)人不需要下載整個(gè)區(qū)塊鏈,但是仍能夠驗(yàn)證他的交易。
為了在目前的區(qū)塊鏈原型中實(shí)現(xiàn)網(wǎng)絡(luò),我們不得不簡(jiǎn)化一些事情。因?yàn)槲覀儧](méi)有那么多的計(jì)算機(jī)來(lái)模擬一個(gè)多節(jié)點(diǎn)的網(wǎng)絡(luò)。當(dāng)然,我們可以使用虛擬機(jī)或是 Docker 來(lái)解決這個(gè)問(wèn)題,但是這會(huì)使一切都變得更復(fù)雜:你將不得不先解決可能出現(xiàn)的虛擬機(jī)或 Docker 問(wèn)題,而我的目標(biāo)是將全部精力都放在區(qū)塊鏈實(shí)現(xiàn)上。所以,我們想要在一臺(tái)機(jī)器上運(yùn)行多個(gè)區(qū)塊鏈節(jié)點(diǎn),同時(shí)希望它們有不同的地址。為了實(shí)現(xiàn)這一點(diǎn),我們將使用端口號(hào)作為節(jié)點(diǎn)標(biāo)識(shí)符,而不是使用 IP 地址,比如將會(huì)有這樣地址的節(jié)點(diǎn):127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002 等等。我們叫它端口節(jié)點(diǎn)(port node) ID,并使用環(huán)境變量 NODE_ID 對(duì)它們進(jìn)行設(shè)置。故而,你可以打開(kāi)多個(gè)終端窗口,設(shè)置不同的 NODE_ID 運(yùn)行不同的節(jié)點(diǎn)。
這個(gè)方法也需要有不同的區(qū)塊鏈和錢(qián)包文件。它們現(xiàn)在必須依賴(lài)于節(jié)點(diǎn) ID 進(jìn)行命名,比如 blockchain_3000.db, blockchain_30001.db and wallet_3000.db, wallet_30001.db 等等。
實(shí)現(xiàn)所以,當(dāng)你下載 Bitcoin Core 并首次運(yùn)行時(shí),到底發(fā)生了什么呢?它必須連接到某個(gè)節(jié)點(diǎn)下載最新?tīng)顟B(tài)的區(qū)塊鏈。考慮到你的電腦并沒(méi)有意識(shí)到所有或是部分的比特幣節(jié)點(diǎn),那么連接到的“某個(gè)節(jié)點(diǎn)”到底是什么?
在 Bitcoin Core 中硬編碼一個(gè)地址,已經(jīng)被證實(shí)是一個(gè)錯(cuò)誤:因?yàn)楣?jié)點(diǎn)可能會(huì)被攻擊或關(guān)機(jī),這會(huì)導(dǎo)致新的節(jié)點(diǎn)無(wú)法加入到網(wǎng)絡(luò)中。在 Bitcoin Core 中,硬編碼了 DNS seeds。雖然這些并不是節(jié)點(diǎn),但是 DNS 服務(wù)器知道一些節(jié)點(diǎn)的地址。當(dāng)你啟動(dòng)一個(gè)全新的 Bitcoin Core 時(shí),它會(huì)連接到一個(gè)種子節(jié)點(diǎn),獲取全節(jié)點(diǎn)列表,隨后從這些節(jié)點(diǎn)中下載區(qū)塊鏈。
不過(guò)在我們目前的實(shí)現(xiàn)中,無(wú)法做到完全的去中心化,因?yàn)闀?huì)出現(xiàn)中心化的特點(diǎn)。我們會(huì)有三個(gè)節(jié)點(diǎn):
一個(gè)中心節(jié)點(diǎn)。所有其他節(jié)點(diǎn)都會(huì)連接到這個(gè)節(jié)點(diǎn),這個(gè)節(jié)點(diǎn)會(huì)在其他節(jié)點(diǎn)之間發(fā)送數(shù)據(jù)。
一個(gè)礦工節(jié)點(diǎn)。這個(gè)節(jié)點(diǎn)會(huì)在內(nèi)存池中存儲(chǔ)新的交易,當(dāng)有足夠的交易時(shí),它就會(huì)打包挖出一個(gè)新塊。
一個(gè)錢(qián)包節(jié)點(diǎn)。這個(gè)節(jié)點(diǎn)會(huì)被用作在錢(qián)包之間發(fā)送幣。但是與 SPV 節(jié)點(diǎn)不同,它存儲(chǔ)了區(qū)塊鏈的一個(gè)完整副本。
場(chǎng)景本文的目標(biāo)是實(shí)現(xiàn)如下場(chǎng)景:
中心節(jié)點(diǎn)創(chuàng)建一個(gè)區(qū)塊鏈。
一個(gè)其他(錢(qián)包)節(jié)點(diǎn)連接到中心節(jié)點(diǎn)并下載區(qū)塊鏈。
另一個(gè)(礦工)節(jié)點(diǎn)連接到中心節(jié)點(diǎn)并下載區(qū)塊鏈。
錢(qián)包節(jié)點(diǎn)創(chuàng)建一筆交易。
礦工節(jié)點(diǎn)接收交易,并將交易保存到內(nèi)存池中。
當(dāng)內(nèi)存池中有足夠的交易時(shí),礦工開(kāi)始挖一個(gè)新塊。
當(dāng)挖出一個(gè)新塊后,將其發(fā)送到中心節(jié)點(diǎn)。
錢(qián)包節(jié)點(diǎn)與中心節(jié)點(diǎn)進(jìn)行同步。
錢(qián)包節(jié)點(diǎn)的用戶(hù)檢查他們的支付是否成功。
這就是比特幣中的一般流程。盡管我們不會(huì)實(shí)現(xiàn)一個(gè)真實(shí)的 P2P 網(wǎng)絡(luò),但是我們會(huì)實(shí)現(xiàn)一個(gè)真是,也是比特幣最常見(jiàn)最重要的用戶(hù)場(chǎng)景。
版本節(jié)點(diǎn)通過(guò)消息(message)進(jìn)行交流。當(dāng)一個(gè)新的節(jié)點(diǎn)開(kāi)始運(yùn)行時(shí),它會(huì)從一個(gè) DNS 種子獲取幾個(gè)節(jié)點(diǎn),給它們發(fā)送 version 消息,在我們的實(shí)現(xiàn)看起來(lái)就像是這樣:
type version struct { Version int BestHeight int AddrFrom string }
由于我們僅有一個(gè)區(qū)塊鏈版本,所以 Version 字段實(shí)際并不會(huì)存儲(chǔ)什么重要信息。BestHeight 存儲(chǔ)區(qū)塊鏈中節(jié)點(diǎn)的高度。AddFrom 存儲(chǔ)發(fā)送者的地址。
接收到 version 消息的節(jié)點(diǎn)應(yīng)該做什么呢?它會(huì)響應(yīng)自己的 version 消息。這是一種握手?:如果沒(méi)有事先互相問(wèn)候,就不可能有其他交流。不過(guò),這并不是處于禮貌:version 用于找到一個(gè)更長(zhǎng)的區(qū)塊鏈。當(dāng)一個(gè)節(jié)點(diǎn)接收到 version 消息,它會(huì)檢查本節(jié)點(diǎn)的區(qū)塊鏈?zhǔn)欠癖?BestHeight 的值更大。如果不是,節(jié)點(diǎn)就會(huì)請(qǐng)求并下載缺失的塊。
為了接收消息,我們需要一個(gè)服務(wù)器:
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) } }
首先,我們對(duì)中心節(jié)點(diǎn)的地址進(jìn)行硬編碼:因?yàn)槊總€(gè)節(jié)點(diǎn)必須知道從何處開(kāi)始初始化。minerAddress 參數(shù)指定了接收挖礦獎(jiǎng)勵(lì)的地址。代碼片段:
if nodeAddress != knownNodes[0] { sendVersion(knownNodes[0], bc) }
這意味著如果當(dāng)前節(jié)點(diǎn)不是中心節(jié)點(diǎn),它必須向中心節(jié)點(diǎn)發(fā)送 version 消息來(lái)查詢(xún)是否自己的區(qū)塊鏈已過(guò)時(shí)。
func sendVersion(addr string, bc *Blockchain) { bestHeight := bc.GetBestHeight() payload := gobEncode(version{nodeVersion, bestHeight, nodeAddress}) request := append(commandToBytes("version"), payload...) sendData(addr, request) }
我們的消息,在底層就是字節(jié)序列。前 12 個(gè)字節(jié)指定了命令名(比如這里的 version),后面的字節(jié)會(huì)包含 gob 編碼的消息結(jié)構(gòu),commandToBytes 看起來(lái)是這樣:
func commandToBytes(command string) []byte { var bytes [commandLength]byte for i, c := range command { bytes[i] = byte(c) } return bytes[:] }
它創(chuàng)建一個(gè) 12 字節(jié)的緩沖區(qū),并用命令名進(jìn)行填充,將剩下的字節(jié)置為空。下面一個(gè)相反的函數(shù):
func bytesToCommand(bytes []byte) string { var command []byte for _, b := range bytes { if b != 0x0 { command = append(command, b) } } return fmt.Sprintf("%s", command) }
當(dāng)一個(gè)節(jié)點(diǎn)接收到一個(gè)命令,它會(huì)運(yùn)行 bytesToCommand 來(lái)提取命令名,并選擇正確的處理器處理命令主體:
func handleConnection(conn net.Conn, bc *Blockchain) { request, err := ioutil.ReadAll(conn) command := bytesToCommand(request[:commandLength]) fmt.Printf("Received %s command ", 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) } }
首先,我們需要對(duì)請(qǐng)求進(jìn)行解碼,提取有效信息。所有的處理器在這部分都類(lèi)似,所以我們會(huì)下面的代碼片段中略去這部分。
然后節(jié)點(diǎn)將從消息中提取的 BestHeight 與自身進(jìn)行比較。如果自身節(jié)點(diǎn)的區(qū)塊鏈更長(zhǎng),它會(huì)回復(fù) version 消息;否則,它會(huì)發(fā)送 getblocks 消息。
getblockstype getblocks struct { AddrFrom string }
getblocks 意為 “給我看一下你有什么區(qū)塊”(在比特幣中,這會(huì)更加復(fù)雜)。注意,它并沒(méi)有說(shuō)“把你全部的區(qū)塊給我”,而是請(qǐng)求了一個(gè)塊哈希的列表。這是為了減輕網(wǎng)絡(luò)負(fù)載,因?yàn)閰^(qū)塊可以從不同的節(jié)點(diǎn)下載,并且我們不想從一個(gè)單一節(jié)點(diǎn)下載數(shù)十 GB 的數(shù)據(jù)。
處理命令十分簡(jiǎn)單:
func handleGetBlocks(request []byte, bc *Blockchain) { ... blocks := bc.GetBlockHashes() sendInv(payload.AddrFrom, "block", blocks) }
在我們簡(jiǎn)化版的實(shí)現(xiàn)中,它會(huì)返回 所有塊哈希。
invtype inv struct { AddrFrom string Type string Items [][]byte }
比特幣使用 inv 來(lái)向其他節(jié)點(diǎn)展示當(dāng)前節(jié)點(diǎn)有什么塊和交易。再次提醒,它沒(méi)有包含完整的區(qū)塊鏈和交易,僅僅是哈希而已。Type 字段表明了這是塊還是交易。
處理 inv 稍顯復(fù)雜:
func handleInv(request []byte, bc *Blockchain) { ... fmt.Printf("Recevied inventory with %d %s ", 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 變量來(lái)跟蹤已下載的塊。這能夠讓我們從不同的節(jié)點(diǎn)下載塊。在將塊置于傳送狀態(tài)時(shí),我們給 inv 消息的發(fā)送者發(fā)送 getdata 命令并更新 blocksInTransit。在一個(gè)真實(shí)的 P2P 網(wǎng)絡(luò)中,我們會(huì)想要從不同節(jié)點(diǎn)來(lái)傳送塊。
在我們的實(shí)現(xiàn)中,我們永遠(yuǎn)也不會(huì)發(fā)送有多重哈希的 inv。這就是為什么當(dāng) payload.Type == "tx" 時(shí),只會(huì)拿到第一個(gè)哈希。然后我們檢查是否在內(nèi)存池中已經(jīng)有了這個(gè)哈希,如果沒(méi)有,發(fā)送 getdata 消息。
getdatatype getdata struct { AddrFrom string Type string ID []byte }
getdata 用于某個(gè)塊或交易的請(qǐng)求,它可以?xún)H包含一個(gè)塊或交易的 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) } }
這個(gè)處理器比較地直觀(guān):如果它們請(qǐng)求一個(gè)塊,則返回塊;如果它們請(qǐng)求一筆交易,則返回交易。注意,我們并不檢查實(shí)際上是否已經(jīng)有了這個(gè)塊或交易。這是一個(gè)缺陷 :)
block 和 txtype block struct { AddrFrom string Block []byte } type tx struct { AddFrom string Transaction []byte }
實(shí)際完成數(shù)據(jù)轉(zhuǎn)移的正是這些消息。
處理 block 消息十分簡(jiǎn)單:
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 ", block.Hash) if len(blocksInTransit) > 0 { blockHash := blocksInTransit[0] sendGetData(payload.AddrFrom, "block", blockHash) blocksInTransit = blocksInTransit[1:] } else { UTXOSet := UTXOSet{bc} UTXOSet.Reindex() } }
當(dāng)接收到一個(gè)新塊時(shí),我們把它放到區(qū)塊鏈里面。如果還有更多的區(qū)塊需要下載,我們繼續(xù)從上一個(gè)下載的塊的那個(gè)節(jié)點(diǎn)繼續(xù)請(qǐng)求。當(dāng)最后把所有塊都下載完后,對(duì) UTXO 集進(jìn)行重新索引。
TODO:并非無(wú)條件信任,我們應(yīng)該在將每個(gè)塊加入到區(qū)塊鏈之前對(duì)它們進(jìn)行驗(yàn)證。TODO: 并非運(yùn)行 UTXOSet.Reindex(), 而是應(yīng)該使用 UTXOSet.Update(block),因?yàn)槿绻麉^(qū)塊鏈很大,它將需要很多時(shí)間來(lái)對(duì)整個(gè) 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 } } } }
首先要做的事情是將新交易放到內(nèi)存池中(再次提醒,在將交易放到內(nèi)存池之前,必要對(duì)其進(jìn)行驗(yàn)證)。下個(gè)片段:
if nodeAddress == knownNodes[0] { for _, node := range knownNodes { if node != nodeAddress && node != payload.AddFrom { sendInv(node, "tx", [][]byte{tx.ID}) } } }
檢查當(dāng)前節(jié)點(diǎn)是否是中心節(jié)點(diǎn)。在我們的實(shí)現(xiàn)中,中心節(jié)點(diǎn)并不會(huì)挖礦。它只會(huì)將新的交易推送給網(wǎng)絡(luò)中的其他節(jié)點(diǎn)。
下一個(gè)很大的代碼片段是礦工節(jié)點(diǎn)“專(zhuān)屬”。讓我們對(duì)它進(jìn)行一下分解:
if len(mempool) >= 2 && len(miningAddress) > 0 {
miningAddress 只會(huì)在礦工節(jié)點(diǎn)上設(shè)置。如果當(dāng)前節(jié)點(diǎn)(礦工)的內(nèi)存池中有兩筆或更多的交易,開(kāi)始挖礦:
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 }
首先,內(nèi)存池中所有交易都是通過(guò)驗(yàn)證的。無(wú)效的交易會(huì)被忽略,如果沒(méi)有有效交易,則挖礦中斷。
cbTx := NewCoinbaseTX(miningAddress, "") txs = append(txs, cbTx) newBlock := bc.MineBlock(txs) UTXOSet := UTXOSet{bc} UTXOSet.Reindex() fmt.Println("New block is mined!")
驗(yàn)證后的交易被放到一個(gè)塊里,同時(shí)還有附帶獎(jiǎng)勵(lì)的 coinbase 交易。當(dāng)塊被挖出來(lái)以后,UTXO 集會(huì)被重新索引。
TODO: 提醒,應(yīng)該使用 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 }
當(dāng)一筆交易被挖出來(lái)以后,就會(huì)被從內(nèi)存池中移除。當(dāng)前節(jié)點(diǎn)所連接到的所有其他節(jié)點(diǎn),接收帶有新塊哈希的 inv 消息。在處理完消息后,它們可以對(duì)塊進(jìn)行請(qǐng)求。
結(jié)果讓我們來(lái)回顧一下上面定義的場(chǎng)景。
首先,在第一個(gè)終端窗口中將 NODE_ID 設(shè)置為 3000(export NODE_ID=3000)。為了讓你知道什么節(jié)點(diǎn)執(zhí)行什么操作,我會(huì)使用像 NODE 3000 或 NODE 3001 進(jìn)行標(biāo)識(shí)。
NODE 3000創(chuàng)建一個(gè)錢(qián)包和一個(gè)新的區(qū)塊鏈:
$ blockchain_go createblockchain -address CENTREAL_NODE
(為了簡(jiǎn)潔起見(jiàn),我會(huì)使用假地址。)
然后,會(huì)生成一個(gè)僅包含創(chuàng)世塊的區(qū)塊鏈。我們需要保存塊,并在其他節(jié)點(diǎn)使用。創(chuàng)世塊承擔(dān)了一條鏈標(biāo)識(shí)符的角色(在 Bitcoin Core 中,創(chuàng)世塊是硬編碼的)
$ cp blockchain_3000.db blockchain_genesis.dbNODE 3001
接下來(lái),打開(kāi)一個(gè)新的終端窗口,將 node ID 設(shè)置為 3001。這會(huì)作為一個(gè)錢(qián)包節(jié)點(diǎn)。通過(guò) blockchain_go createwallet 生成一些地址,我們把這些地址叫做 WALLET_1, WALLET_2, WALLET_3.
NODE 3000向錢(qián)包地址發(fā)送一些幣:
$ 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 標(biāo)志指的是塊會(huì)立刻被同一節(jié)點(diǎn)挖出來(lái)。我們必須要有這個(gè)標(biāo)志,因?yàn)槌跏紶顟B(tài)時(shí),網(wǎng)絡(luò)中沒(méi)有礦工節(jié)點(diǎn)。
啟動(dòng)節(jié)點(diǎn):
$ blockchain_go startnode
這個(gè)節(jié)點(diǎn)會(huì)持續(xù)運(yùn)行,直到本文定義的場(chǎng)景結(jié)束。
NODE 3001啟動(dòng)上面保存創(chuàng)世塊節(jié)點(diǎn)的區(qū)塊鏈:
$ cp blockchain_genesis.db blockchain_3001.db
運(yùn)行節(jié)點(diǎn):
$ blockchain_go startnode
它會(huì)從中心節(jié)點(diǎn)下載所有區(qū)塊。為了檢查一切正常,暫停節(jié)點(diǎn)運(yùn)行并檢查余額:
$ blockchain_go getbalance -address WALLET_1 Balance of "WALLET_1": 10 $ blockchain_go getbalance -address WALLET_2 Balance of "WALLET_2": 10
你還可以檢查 CENTRAL_NODE 地址的余額,因?yàn)?node 3001 現(xiàn)在有它自己的區(qū)塊鏈:
$ blockchain_go getbalance -address CENTRAL_NODE Balance of "CENTRAL_NODE": 10NODE 3002
打開(kāi)一個(gè)新的終端窗口,將它的 ID 設(shè)置為 3002,然后生成一個(gè)錢(qián)包。這會(huì)是一個(gè)礦工節(jié)點(diǎn)。初始化區(qū)塊鏈:
$ cp blockchain_genesis.db blockchain_3002.db
啟動(dòng)節(jié)點(diǎn):
$ blockchain_go startnode -miner MINER_WALLETNODE 3001
發(fā)送一些幣:
$ blockchain_go send -from WALLET_1 -to WALLET_3 -amount 1 $ blockchain_go send -from WALLET_2 -to WALLET_4 -amount 1NODE 3002
迅速切換到礦工節(jié)點(diǎn),你會(huì)看到挖出了一個(gè)新塊!同時(shí),檢查中心節(jié)點(diǎn)的輸出。
NODE 3001切換到錢(qián)包節(jié)點(diǎn)并啟動(dòng):
$ blockchain_go startnode
它會(huì)下載最近挖出來(lái)的塊!
暫停節(jié)點(diǎn)并檢查余額:
$ 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
就是這么多了!
總結(jié)這是本系列的最后一篇文章了。我本可以就實(shí)現(xiàn)一個(gè)真實(shí)的 P2P 網(wǎng)絡(luò)原型繼續(xù)展開(kāi),但是我真的沒(méi)有這么多時(shí)間。我希望本文已經(jīng)回答了關(guān)于比特幣技術(shù)的一些問(wèn)題,也給讀者提出了一些問(wèn)題,這些問(wèn)題你可以自行尋找答案。在比特幣技術(shù)中還有隱藏著很多有趣的事情!好運(yùn)!
后記:你可以從實(shí)現(xiàn) addr 消息來(lái)開(kāi)始改進(jìn)網(wǎng)絡(luò),正如比特幣網(wǎng)絡(luò)協(xié)議中所描述的(鏈接可以下方找到)那樣。這是一個(gè)非常重要的消息,因?yàn)樗试S節(jié)點(diǎn)來(lái)互相發(fā)現(xiàn)彼此。我已經(jīng)開(kāi)始實(shí)現(xiàn)了,不過(guò)還沒(méi)有完成!
鏈接:
Source codes
Bitcoin protocol documentation
Bitcoin network
原文:Building Blockchain in Go. Part 7: Network
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/23934.html
摘要:到目前為止,我們幾乎已經(jīng)實(shí)現(xiàn)了一個(gè)區(qū)塊鏈數(shù)據(jù)庫(kù)的所有元素。使用根據(jù)在區(qū)塊鏈中找到一筆交易。是一個(gè)比特幣輕節(jié)點(diǎn),它不需要下載整個(gè)區(qū)塊鏈,也不需要驗(yàn)證區(qū)塊和交易。到目前為止,我們只是將一個(gè)塊里面的每筆交易哈希連接了起來(lái),將在上面應(yīng)用了算法。 翻譯的系列文章我已經(jīng)放到了 GitHub 上:blockchain-tutorial,后續(xù)如有更新都會(huì)在 GitHub 上,可能就不在這里同步了。如果...
摘要:在區(qū)塊鏈中,存儲(chǔ)有效信息的是區(qū)塊。存儲(chǔ)的是前一個(gè)塊的哈希。正是由于這個(gè)特性,才使得區(qū)塊鏈?zhǔn)前踩摹_@樣的結(jié)構(gòu),能夠讓我們快速地獲取鏈上的最新塊,并且高效地通過(guò)哈希來(lái)檢索一個(gè)塊。 翻譯的系列文章我已經(jīng)放到了 GitHub 上:blockchain-tutorial,后續(xù)如有更新都會(huì)在 GitHub 上,可能就不在這里同步了。如果想直接運(yùn)行代碼,也可以 clone GitHub 上的教程倉(cāng)...
摘要:哈希函數(shù)被廣泛用于檢測(cè)數(shù)據(jù)的一致性。在區(qū)塊鏈中,哈希被用于保證一個(gè)塊的一致性。比特幣使用,一個(gè)最初用來(lái)防止垃圾郵件的工作量證明算法。下面是與前面例子哈希的形式化比較第一個(gè)哈希基于計(jì)算比目標(biāo)要大,因此它并不是一個(gè)有效的工作量證明。 翻譯的系列文章我已經(jīng)放到了 GitHub 上:blockchain-tutorial,后續(xù)如有更新都會(huì)在 GitHub 上,可能就不在這里同步了。如果想直接運(yùn)...
摘要:引言到目前為止,我們已經(jīng)構(gòu)建了一個(gè)有工作量證明機(jī)制的區(qū)塊鏈。在今天的內(nèi)容中,我們會(huì)將區(qū)塊鏈持久化到一個(gè)數(shù)據(jù)庫(kù)中,然后會(huì)提供一個(gè)簡(jiǎn)單的命令行接口,用來(lái)完成一些與區(qū)塊鏈的交互操作。這同樣也意味著,一個(gè)也就是區(qū)塊鏈的一種標(biāo)識(shí)符。 翻譯的系列文章我已經(jīng)放到了 GitHub 上:blockchain-tutorial,后續(xù)如有更新都會(huì)在 GitHub 上,可能就不在這里同步了。如果想直接運(yùn)行代碼...
摘要:引言交易是比特幣的核心所在,而區(qū)塊鏈的唯一目的,也正是為了能夠安全可靠地存儲(chǔ)交易。比特幣使用了一個(gè)更加復(fù)雜的技術(shù)它將一個(gè)塊里面包含的所有交易表示為一個(gè),然后在工作量證明系統(tǒng)中使用樹(shù)的根哈希。 翻譯的系列文章我已經(jīng)放到了 GitHub 上:blockchain-tutorial,后續(xù)如有更新都會(huì)在 GitHub 上,可能就不在這里同步了。如果想直接運(yùn)行代碼,也可以 clone GitHu...
閱讀 1518·2023-04-25 17:41
閱讀 3040·2021-11-22 15:08
閱讀 842·2021-09-29 09:35
閱讀 1605·2021-09-27 13:35
閱讀 3323·2021-08-31 09:44
閱讀 2716·2019-08-30 13:20
閱讀 1939·2019-08-30 13:00
閱讀 2558·2019-08-26 12:12