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