本篇是"用Go構建區(qū)塊鏈"系列的最后一篇,主要對原文進行翻譯灭将。對應原文如下:
Building Blockchain in Go. Part 7: Network
1漂羊、介紹
到目前為止,我們已經構建了一個包含所有關鍵功能的區(qū)塊鏈:匿名扮碧,安全和隨機生成的地址; 區(qū)塊鏈數據存儲; 工作量證明系統(tǒng); 以可靠的方式來存儲交易。雖然這些功能至關重要杏糙,但這還不夠慎王。什么讓這些功能真正發(fā)揮作用,以及使加密貨幣成為可能的因素是網絡宏侍。只是在一臺計算機上運行這種區(qū)塊鏈實現有什么用處赖淤?當只有一個用戶時,那些基于密碼學的功能有什么好處谅河?是網絡使得這些機制可以工作起來咱旱,而且變得有用嗜愈。
您可以將這些區(qū)塊鏈功能視為規(guī)則,類似于人們想要共同生存和發(fā)展時所制定的規(guī)則莽龟。一種社會規(guī)則蠕嫁。區(qū)塊鏈網絡是遵循相同規(guī)則的程序社區(qū),正式遵循這種規(guī)則使得社區(qū)得以存活毯盈。同樣剃毒,當人們擁有相同的想法時,他們會變得更強大搂赋,并可以共同創(chuàng)造美好的生活赘阀。如果有人遵循不同的規(guī)則,他們將生活在一個單獨的社會(州脑奠,公社等)基公。同樣,如果區(qū)塊鏈節(jié)點遵循不同的規(guī)則宋欺,它們將形成一個單獨的網絡轰豆。
這非常重要:如果沒有網絡,沒有大多數節(jié)點共享相同的規(guī)則齿诞,這些規(guī)則是無用的酸休!
免責聲明:不幸的是,我沒有足夠的時間來實現真正的P2P網絡原型祷杈。在本文中斑司,我將演示一個最常見的場景,涉及不同類型的節(jié)點但汞。改善這種情況并使其成為P2P網絡對您來說可能是一個很好的挑戰(zhàn)和實踐宿刮!另外,我不能保證除了本文中實現的其他場景以外的其他場景都可以使用私蕾。抱歉!
這部分的介紹有重大的代碼更改僵缺,所以在這里解釋它們是沒有意義的。請參閱此頁面以查看自上一篇文章以來的所有更改是目。
2谤饭、區(qū)塊鏈網絡
區(qū)塊鏈網絡是去中心化的,這意味著沒有工作的服務器懊纳,客戶端也不需要使用服務器來獲取或處理數據。區(qū)塊鏈網絡中有節(jié)點亡容,每個節(jié)點都是網絡的正式成員嗤疯。點就是一切:它既是客戶端又是服務器。記住這一點非常重要闺兢,因為它與通常的Web應用程序非常不同茂缚。
區(qū)塊鏈網絡是一種P2P(對等)網絡戏罢,這意味著節(jié)點彼此直接連接。它的拓撲結構是扁平的脚囊,因為節(jié)點角色沒有層次結構龟糕。在這里它的示意圖:
Business vector created by Dooder - Freepik.com
這種的網絡節(jié)點更難以實現,因為它們必須執(zhí)行大量操作悔耘。每個節(jié)點必須與多個其他節(jié)點交互讲岁,它必須請求其他節(jié)點的狀態(tài),將其與自己的狀態(tài)進行比較衬以,并在過期時更新其狀態(tài)缓艳。
3、節(jié)點角色
盡管是全面的看峻,區(qū)塊鏈節(jié)點可以在網絡中扮演不同的角色阶淘。它們分別是:
-
礦工。
這些節(jié)點運行在功能強大或專用的硬件設備(如ASIC)上互妓,其唯一目標是盡快挖掘出新的區(qū)塊溪窒。礦工只能在使用工作證明的區(qū)塊鏈中使用,因為采礦實際上意味著解決PoW難題冯勉。例如霉猛,在證明權益區(qū)塊鏈中,不存在挖掘珠闰。礦工是區(qū)塊鏈中唯一可能使用到工作量證明系統(tǒng)的角色惜浅,因為挖礦實際上就是解決PoW的問題。例如在PoS權益證明的區(qū)塊鏈中伏嗜,沒有挖礦坛悉。
-
全節(jié)點。
這些節(jié)點驗證礦工挖出來的區(qū)塊并驗證交易承绸。要做到這一點裸影,他們必須擁有區(qū)塊鏈的全部副本。而且军熏,這樣的節(jié)點執(zhí)行這種路由操作轩猩,就像幫助其他節(jié)點發(fā)現對方一樣。對于網絡來說荡澎,擁有許多完整節(jié)點非常重要均践,因為由這些節(jié)點來做出決定的:它們決定一個區(qū)塊或一筆交易是否有效。
-
SPV摩幔。
SPV代表簡單支付驗證彤委。這些節(jié)點不存儲完整的區(qū)塊鏈副本,但它們仍然能夠驗證交易(并不是所有交易或衡,而是一個子集焦影,例如發(fā)送到某個特定地址的交易)车遂。一個SPV節(jié)點依賴于完整節(jié)點來獲取數據,并且可能有多個SPV節(jié)點連接到一個完整節(jié)點斯辰。SPV使得錢包應用成為可能:一個不需要下載完整的區(qū)塊鏈舶担,但仍然可以驗證他們的交易。
4彬呻、網絡簡化
為了實現我們區(qū)塊鏈中的網絡衣陶,我們需要簡化一些事情。問題是我們沒有多臺計算機來模擬具有多個節(jié)點的網絡废岂。我們可以使用虛擬機或Docker來解決這個問題祖搓,但它可能會讓一切變得更加困難:您必須解決可能的虛擬機或Docker問題,而我的目標是專注于區(qū)塊鏈實現湖苞。所以拯欧,我們希望在一臺機器上運行多個區(qū)塊鏈節(jié)點,同時我們希望它們擁有不同的地址财骨。為了達到這個目的镐作,我們將使用端口作為節(jié)點標識符,而不是IP地址隆箩。例如该贾,會出現這樣的地址節(jié)點:127.0.0.1:3000,127.0.0.1:3001捌臊,127.0.0.1:3002等杨蛋。我們叫它為端口節(jié)點ID,并使用環(huán)境變量 NODE_ID 對它們進行設置理澎。因此逞力,您可以打開多個終端窗口,設置不同的NODE_ID s并運行不同的節(jié)點糠爬。
這種方法還需要擁有不同的區(qū)塊鏈和錢包文件】苡現在,他們必須依靠節(jié)點ID進行命名执隧,比如 blockchain_3000.db, blockchain_30001.db和wallet_3000.db揩抡,wallet_30001.db等待。
5镀琉、實現
那么峦嗤,當你下載Bitcoin Core并首次運行它時會發(fā)生什么?它必須連接到某他節(jié)點才能下載最新狀態(tài)的區(qū)塊鏈滚粟⊙罢蹋考慮到你的計算機不知道所有的或者部分的比特幣節(jié)點,那么這個節(jié)點是什么凡壤?
在Bitcoin Core中硬編碼一個地址署尤,已經被證實是一個錯誤:節(jié)點可能會受到攻擊或關閉,這可能導致新節(jié)點無法加入網絡亚侠。相反曹体,在Bitcoin Core中,硬編碼了DNS種子(DNS seeds)硝烂。雖然這些不是節(jié)點箕别,但是DNS服務器知道一些節(jié)點的地址。當你啟動一個全新的Bitcoin Core時滞谢,它將連接到其中一個種子節(jié)點上并獲得全節(jié)點的列表串稀,然后它將從中下載區(qū)塊鏈。
在我們的實現中狮杨,雖然還是中心化的母截。我們會有三個節(jié)點:
- 一個中心節(jié)點。這是所有其他節(jié)點將連接到的節(jié)點橄教,并且這是將在其他節(jié)點之間發(fā)送數據的節(jié)點清寇。
- 一個礦工節(jié)點。這個節(jié)點將在內存池中存儲新的交易护蝶,當有足夠的交易時华烟,它會打包挖掘出一個新的區(qū)塊。
- 一個錢包節(jié)點持灰。這個節(jié)點將用于在錢包之間發(fā)送幣盔夜。與SPV節(jié)點不同,它將存儲完整的區(qū)塊鏈副本堤魁。
6喂链、場景
本文的目標是實現以下場景:
- 中心節(jié)點創(chuàng)建一個區(qū)塊鏈。
- 其他(錢包)節(jié)點連接到它并下載區(qū)塊鏈姨涡。
- 另外一個(礦工)節(jié)點連接到中心節(jié)點并下載區(qū)塊鏈衩藤。
- 錢包節(jié)點創(chuàng)建一個交易。
- 礦工節(jié)點接收交易并將它保存在其內存池中涛漂。
- 當內存池中有足夠的交易時赏表,礦工開始挖掘出新的區(qū)塊。
- 當一個新的區(qū)塊被挖掘出來時匈仗,將它發(fā)送到中心節(jié)點瓢剿。
- 錢包節(jié)點與中心節(jié)點同步。
- 錢包節(jié)點的用戶檢查他們的支付是否成功悠轩。
比特幣看起來是這樣的情況间狂。即使我們不打算建立一個真正的P2P網絡,我們將實現一個真正的火架,也是最重要的比特幣用戶場景鉴象。
7忙菠、版本
節(jié)點通過消息的方式進行通信。當一個新節(jié)點運行時纺弊,它從DNS種子中獲得幾個節(jié)點牛欢,并向它們發(fā)送版本(version)消息,在我們的實現中淆游,看起來就像是這樣:
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é)點將請求并下載缺失的區(qū)塊传趾。
為了接收消息,我們需要一個服務器:
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參數指定接收挖礦獎勵的地址。這一部分:
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é)為空轿腺。有一個相反的函數:
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\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)
}
}
首先族壳,我們需要解碼請求并提取有效載荷。這與所有處理器類似趣些,所以我將在后面的代碼片段中省略這一部分仿荆。
然后一個節(jié)點將其BestHeight與消息中的一個進行比較。如果節(jié)點的區(qū)塊鏈更長,它會回復version消息; 否則拢操,它會發(fā)送getblocks消息锦亦。
8、getblocks
type getblocks struct {
AddrFrom string
}
getblocks意味著"向我展示你擁有的塊"(在比特幣中庐冯,它更復雜)孽亲。注意坎穿,它不會說"給我所有的區(qū)塊"展父,而是要求一個區(qū)塊哈希列表。這樣做是為了減少網絡負載玲昧,因為可以從不同的節(jié)點下載區(qū)塊栖茉,我們不希望從一個節(jié)點下載幾十GB的數據。
處理命令如下所示:
func handleGetBlocks(request []byte, bc *Blockchain) {
...
blocks := bc.GetBlockHashes()
sendInv(payload.AddrFrom, "block", blocks)
}
在我們的簡化實現中孵延,它將返回所有區(qū)塊哈希吕漂。
9、inv
type inv struct {
AddrFrom string
Type string
Items [][]byte
}
比特幣使用inv向其他節(jié)點顯示當前節(jié)點具有哪些區(qū)塊或交易尘应。再次提示惶凝,它不包含整個區(qū)塊和交易,僅僅是它們的哈希值犬钢。該Type字段表示這些是區(qū)塊還是交易苍鲜。
處理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變量中以跟蹤下載的區(qū)塊玷犹。這允許我們從不同節(jié)點下載塊混滔。在將塊放入傳輸狀態(tài)之后,我們將getdata命令發(fā)送給inv消息的發(fā)送者并進行更新blocksInTransit歹颓。在真實的P2P網絡中坯屿,我們希望從不同節(jié)點傳輸塊。
在我們的實現中巍扛,我們永遠不會發(fā)送inv多個哈希值领跛。這就是為什么payload.Type == "tx"只有第一個哈希被采用時。然后我們檢查我們的內存池中是否已經有這個哈希撤奸,如果沒有吠昭,就發(fā)送 getdata消息。
10寂呛、getdata
type getdata struct {
AddrFrom string
Type string
ID []byte
}
getdata 用于某個區(qū)塊或交易的請求怎诫,并且它僅包含一個區(qū)塊或交易的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)
}
}
該處理程序很簡單:如果他們請求一個區(qū)塊贷痪,則返回這個區(qū)塊; 如果請求一筆交易幻妓,則返回交易。請注意,我們不檢查我們是否確的有這個區(qū)塊或交易肉津。這是一個缺陷:)
11强胰、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()
}
}
當我們收到一個新的區(qū)塊時妹沙,我們將其放入我們的區(qū)塊鏈中偶洋。如果有更多的區(qū)塊要下載,我們會從我們下載前一個區(qū)塊的同一節(jié)點請求它們距糖。當我們最終下載所有區(qū)塊時玄窝,UTXO集就會被重新索引。
TODO:并非無條件信任悍引,我們應該在將每個區(qū)塊加入到區(qū)塊鏈之前對它們進行驗證恩脂。
TODO:應該使用UTXOSet.Update(block),而不是運行UTXOSet.Reindex()趣斤,因為如果區(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é)點。在我們的實現中联贩,中心節(jié)點不會挖掘區(qū)塊漫仆。相反,它會將新的交易轉發(fā)到網絡中的其他節(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!")
已驗證的交易正被放入一個區(qū)塊微王,以及一個帶有獎勵的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消息。他們可以在處理消息后請求該區(qū)塊涨醋。
12瓜饥、結果
讓我們來回顧下我們之前定義的場景。
首先浴骂,在第一個終端窗口中設置NODE_ID為3000(export NODE_ID=3000)乓土。在下一節(jié)之前我會用類似于 NODE 3000 或 NODE 3001來代替,你要了解哪個節(jié)點做什么溯警。
NODE 3000
創(chuàng)建一個錢包和一個新的區(qū)塊鏈:
$ blockchain_go createblockchain -address CENTREAL_NODE
(為了清晰和簡潔趣苏,我將使用假地址)
之后,區(qū)塊鏈會包含一個創(chuàng)世區(qū)塊愧膀。我們需要保存塊并將其用于其他節(jié)點拦键。創(chuàng)世區(qū)塊作為區(qū)塊鏈的標識符(在比特幣核心中,創(chuàng)世區(qū)塊是硬編碼的)檩淋。
$ cp blockchain_3000.db blockchain_genesis.db
NODE 3001
接下來,打開一個新的終端窗口并將節(jié)點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標志表示該區(qū)塊將立即被同一節(jié)點挖掘。我們必須有這個標志孙乖,因為最初網絡中沒有礦工節(jié)點浙炼。
啟動節(jié)點:
$ blockchain_go startnode
節(jié)點必須運行,直到場景結束唯袄。
NODE 3001
用上面保存的創(chuàng)始區(qū)塊啟動節(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地址的余額资厉,因為節(jié)點3001現在有它的區(qū)塊鏈:
$ blockchain_go getbalance -address CENTRAL_NODE
Balance of 'CENTRAL_NODE': 10
NODE 3002
打開一個新的終端窗口并將其ID設置為3002,并生成一個錢包蔬顾。這將是一個礦工節(jié)點宴偿。初始化區(qū)塊鏈:
$ cp blockchain_genesis.db blockchain_3002.db
并啟動節(jié)點:
$ blockchain_go startnode -miner MINER_WALLET
NODE 3001
發(fā)送一些幣:
$ blockchain_go send -from WALLET_1 -to WALLET_3 -amount 1
$ blockchain_go send -from WALLET_2 -to WALLET_4 -amount 1
NODE 3002
快速切換到礦工節(jié)點,你會看到它挖出了一個新的區(qū)塊诀豁!另外窄刘,檢查中心節(jié)點的輸出。
NODE 3001
切換到錢包節(jié)點并啟動它:
$ blockchain_go startnode
它會下載新被挖出的區(qū)塊舷胜!
停止并檢查余額:
$ 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
搞定娩践,收工!
13、總結
這是該系列的最后一部分欺矫。我本可以發(fā)布更多的文章來實現P2P網絡的真實原型纱新,但我沒有時間這樣做。我希望這篇文章能夠回答您關于比特幣技術的一些問題穆趴,并提出新的問題脸爱,您可以自己找到答案。比特幣技術中隱藏著更多有趣的東西未妹!祝你好運簿废!
PS:你可以從實現addr消息來開始優(yōu)化這個網絡,就像比特幣網絡協(xié)議中所描述的(鏈接在下面)那樣络它。這是一個非常重要的信息族檬,因為它允許節(jié)點相互發(fā)現。我開始著手實現它化戳,但還沒有完成单料!
鏈接:
由于水平有限,翻譯質量不太好点楼,歡迎大家拍磚扫尖。