用 Go 構建一個區塊鏈 -- Part 7: 網路__Go

來源:互聯網
上載者:User

翻譯的系列文章我已經放到了 GitHub 上:blockchain-tutorial,後續如有更新都會在 GitHub 上,可能就不在這裡同步了。如果想直接運行代碼,也可以 clone GitHub 上的教程倉庫,進入 src 目錄執行 make 即可。 引言

到目前為止,我們所構建的原型已經具備了區塊鏈所有的關鍵特性:匿名,安全,隨機產生的地址;區塊鏈資料存放區;工作量證明系統;可靠地儲存體交易。儘管這些特性都不可或缺,但是仍有不足。能夠使得這些特性真正發光發熱,使得加密貨幣成為可能的,是網路(network)。如果實現的這樣一個區塊鏈僅僅運行在單一節點上,有什麼用呢。如果只有一個使用者,那麼這些基於密碼學的特性,又有什麼用呢。正是由於網路,才使得整個機制能夠運轉和發光發熱。

你可以將這些區塊鏈特性認為是規則(rule),類似於人類在一起生活,繁衍生息建立的規則,一種社會安排。區塊鏈網路就是一個程式社區,裡面的每個程式都遵循同樣的規則,正是由於遵循著同一個規則,才使得網路能夠長存。類似的,當人們都有著同樣的想法,就能夠將拳頭攥在一起構建一個更好的生活。如果有人遵循著不同的規則,那麼他們就將生活在一個分裂的社區(州,公社,等等)中。同樣的,如果有區塊鏈節點遵循不同的規則,那麼也會形成一個分裂的網路。

重點在於:如果沒有網路,或者大部分節點都不遵守同樣的規則,那麼規則就會形同虛設,毫無用處。 聲明:不幸的是,我並沒有足夠的時間來實現一個真實的 P2P 網路原型。本文我會展示一個最常見的情境,這個情境涉及不同類型的節點。繼續改進這個情境,將它實現為一個 P2P 網路,對你來說是一個很好的挑戰和實踐。除了本文的情境,我也無法保證在其他情境將會正常工作。抱歉。

本文的代碼實現變化很大,請點擊 這裡 查看所有的代碼更改。 區塊鏈網路

區塊鏈網路是去中心化的,這意味著沒有伺服器,用戶端也不需要依賴伺服器來擷取或處理資料。在區塊鏈網路中,有的是節點,每個節點是網路的一個完全(full-fledged)成員。節點就是一切:它既是一個用戶端,也是一個伺服器。這一點需要牢記於心,因為這與傳統的網頁應用非常不同。

區塊鏈網路是一個 P2P(Peer-to-Peer,端到端)的網路,即節點直接連接到其他節點。它的拓撲是扁平的,因為在節點的世界中沒有層級之分。下面是它的示意圖:

Business vector created by Dooder - Freepik.com

要實現這樣一個網路節點更加困難,因為它們必須執行很多操作。每個節點必須與很多其他節點進行互動,它必須請求其他節點的狀態,與自己的狀態進行比較,當狀態過時時進行更新。 節點角色

儘管節點具有完備成熟的屬性,但是它們也可以在網路中扮演不同角色。比如: 礦工
這樣的節點運行於強大或專用的硬體(比如 ASIC)之上,它們唯一的目標是,儘可能快地挖出新塊。礦工是區塊鏈中唯一可能會用到工作量證明的角色,因為挖礦實際上意味著解決 PoW 難題。在權益證明 PoS 的區塊鏈中,沒有挖礦。 全節點
這些節點驗證礦工挖出來的塊的有效性,並對交易進行確認。為此,他們必須擁有區塊鏈的完整拷貝。同時,全節點執行路由操作,協助其他節點發現彼此。對於網路來說,非常重要的一段就是要有足夠多的全節點。因為正是這些節點執行了決策功能:他們決定了一個塊或一筆交易的有效性。 SPV
SPV 表示 Simplified Payment Verification,簡單支付驗證。這些節點並不儲存整個區塊鏈副本,但是仍然能夠對交易進行驗證(不過不是驗證全部交易,而是一個交易子集,比如,發送到某個指定地址的交易)。一個 SPV 節點依賴一個全節點來擷取資料,可能有多個 SPV 節點串連到一個全節點。SPV 使得錢包應用成為可能:一個人不需要下載整個區塊鏈,但是仍能夠驗證他的交易。 網路簡化

為了在目前的區塊鏈原型中實現網路,我們不得不簡化一些事情。因為我們沒有那麼多的電腦來類比一個多節點的網路。當然,我們可以使用虛擬機器或是 Docker 來解決這個問題,但是這會使一切都變得更複雜:你將不得不先解決可能出現的虛擬機器或 Docker 問題,而我的目標是將全部精力都放在區塊鏈實現上。所以,我們想要在一台機器上運行多個區塊鏈節點,同時希望它們有不同的地址。為了實現這一點,我們將使用連接埠號碼作為節點標識符,而不是使用 IP 位址,比如將會有這樣地址的節點:127.0.0.1:3000127.0.0.1:3001127.0.0.1:3002 等等。我們叫它連接埠節點(port node) ID,並使用環境變數 NODE_ID 對它們進行設定。故而,你可以開啟多個終端視窗,設定不同的 NODE_ID 運行不同的節點。

這個方法也需要有不同的區塊鏈和錢包檔案。它們現在必須依賴於節點 ID 進行命名,比如 blockchain_3000.db, blockchain_30001.db and wallet_3000.db, wallet_30001.db 等等。 實現

所以,當你下載 Bitcoin Core 並首次運行時,到底發生了什麼呢。它必須串連到某個節點下載最新狀態的區塊鏈。考慮到你的電腦並沒有意識到所有或是部分的比特幣節點,那麼串連到的“某個節點”到底是什麼。

在 Bitcoin Core 中寫入程式碼一個地址,已經被證實是一個錯誤:因為節點可能會被攻擊或關機,這會導致新的節點無法加入到網路中。在 Bitcoin Core 中,寫入程式碼了 DNS seeds。雖然這些並不是節點,但是 DNS 伺服器知道一些節點的地址。當你啟動一個全新的 Bitcoin Core 時,它會串連到一個種子節點,擷取全節點列表,隨後從這些節點中下載區塊鏈。

不過在我們目前的實現中,無法做到完全的去中心化,因為會出現中心化的特點。我們會有三個節點: 一個中心節點。所有其他節點都會串連到這個節點,這個節點會在其他節點之間發送資料。 一個礦工節點。這個節點會在記憶體池中儲存新的交易,當有足夠的交易時,它就會打包挖出一個新塊。 一個錢包節點。這個節點會被用作在錢包之間發送幣。但是與 SPV 節點不同,它儲存了區塊鏈的一個完整副本。 情境

本文的目標是實現如下情境: 中心節點建立一個區塊鏈。 一個其他(錢包)節點串連到中心節點並下載區塊鏈。 另一個(礦工)節點串連到中心節點並下載區塊鏈。 錢包節點建立一筆交易。 礦工節點接收交易,並將交易儲存到記憶體池中。 當記憶體池中有足夠的交易時,礦工開始挖一個新塊。 當挖出一個新塊後,將其發送到中心節點。 錢包節點與中心節點進行同步。 錢包節點的使用者檢查他們的支付是否成功。

這就是比特幣中的一般流程。儘管我們不會實現一個真實的 P2P 網路,但是我們會實現一個真是,也是比特幣最常見最重要的使用者情境。 版本

節點通過訊息(message)進行交流。當一個新的節點開始運行時,它會從一個 DNS 種子擷取幾個節點,給它們發送 version 訊息,在我們的實現看起來就像是這樣:

type version struct {    Version    int    BestHeight int    AddrFrom   string}

由於我們僅有一個區塊鏈版本,所以 Version 欄位實際並不會儲存什麼重要訊息。BestHeight 儲存區塊鏈中節點的高度。AddFrom 儲存寄件者的地址。

接收到 version 訊息的節點應該做什麼呢。它會響應自己的 version 訊息。這是一種握手?:如果沒有事先互相問候,就不可能有其他交流。不過,這並不是處於禮貌:version 用於找到一個更長的區塊鏈。當一個節點接收到 version 訊息,它會檢查本節點的區塊鏈是否比 BestHeight 的值更大。如果不是,節點就會請求並下載缺失的塊。

為了接收訊息,我們需要一個伺服器:

var nodeAddress stringvar 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 位元組的緩衝區,並用命令名進行填充,將剩下的位元組置為空白。下面一個相反的函數:

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 訊息。 getblocks

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.