本文是為了后面講解 區(qū)塊鏈的分布式系統(tǒng)以及共識(shí)機(jī)制作為鋪墊的。有了實(shí)際的開(kāi)發(fā)基礎(chǔ),理解理論的知識(shí)就會(huì)容易很多。本文分為三個(gè)部分,第一部分用Go語(yǔ)言編寫一個(gè)小型的區(qū)塊鏈,第二部分在第一部分的基礎(chǔ)上加入Proof of Work(工作量共識(shí)機(jī)制),最后一部分在第一部分的基礎(chǔ)上添加可以加入節(jié)點(diǎn)的功能。
Part 1 Go語(yǔ)言編寫一個(gè)小型區(qū)塊鏈
1.1 開(kāi)發(fā)環(huán)境準(zhǔn)備
首先安裝Go語(yǔ)言
安裝完成后加入一下的packages:
go get github.com/davecgh/go-spew/spew
Spew 可以格式化 structs 和 slices,以便我們?cè)赾onsole能清晰明了的看這些數(shù)據(jù)。
go get github.com/gorilla/mux
Gorilla/mux 是一個(gè)很流行的處理http請(qǐng)求的包
go get github.com/joho/godotenv
Godotenv 可以讓我們讀取 .env 文件里面的環(huán)境變量。
在根目錄創(chuàng)建一個(gè) .env 文件,寫入下面內(nèi)容:
PORT=8080
創(chuàng)建一個(gè) main.go 文件,然后就可以開(kāi)始編寫我們的小型區(qū)塊鏈了。
1.2 小型區(qū)塊鏈
1. Imports(引用包)
首先,引用我們會(huì)用到的包以及前面安裝的包。
package main
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"io"
"log"
"net/http"
"os"
"time"
"github.com/davecgh/go-spew/spew"
"github.com/gorilla/mux"
"github.com/joho/godotenv"
)
2. 數(shù)據(jù)原型
用 struct 來(lái)定義一個(gè) block
type Block struct {
Index int
Timestamp string
Data int
Hash string
PrevHash string
}
- Index 是標(biāo)識(shí)區(qū)塊在區(qū)塊鏈里的位置
- Timestamp 是生成區(qū)塊的時(shí)間戳
- Data 是要寫入?yún)^(qū)塊的數(shù)據(jù)
- Hash 整個(gè)區(qū)塊數(shù)據(jù) SHA256 的哈希值
- PrevHash 是一個(gè)區(qū)塊的哈希值
var Blockchain []Block
創(chuàng)建一個(gè) Block 的 slice。
為什么會(huì)用到哈希?
- 節(jié)省空間。區(qū)塊數(shù)據(jù)的哈希值在區(qū)塊鏈里可以代表這個(gè)區(qū)塊的數(shù)據(jù),當(dāng)需要驗(yàn)證區(qū)塊的完整以及真實(shí)性時(shí)候,儲(chǔ)存哈希值比起儲(chǔ)存所有數(shù)據(jù)方便很多。
- 區(qū)塊鏈的完整性。每一個(gè)區(qū)塊都保存上一個(gè)區(qū)塊的哈希值,確保區(qū)塊鏈的每個(gè)區(qū)塊的數(shù)據(jù)都是真實(shí)且完整的,也防止不誠(chéng)實(shí)節(jié)點(diǎn)篡改數(shù)據(jù)。
接下來(lái),寫一個(gè)計(jì)算區(qū)塊哈希值的函數(shù):
func calculateHash(block Block) string {
record := string(block.Index) + block.Timestamp + string(block.Data) + block.PrevHash
h := sha256.New()
h.Write([]byte(record))
hashed := h.Sum(nil)
return hex.EncodeToString(hashed)
}
calculateHash 這個(gè)方法把區(qū)塊里的必要數(shù)據(jù)以字符串拼接起來(lái),并計(jì)算他們的哈希值。
然后,生成新區(qū)塊的方法:
func generateBlock(oldBlock Block, Data int) (Block, error) {
var newBlock Block
t := time.Now()
newBlock.Index = oldBlock.Index + 1
newBlock.Timestamp = t.String()
newBlock.Data = Data
newBlock.PrevHash = oldBlock.Hash
newBlock.Hash = calculateHash(newBlock)
return newBlock, nil
}
每次生成新的區(qū)塊 Index 增加1,新區(qū)塊的 PrevHash 哈希值要等于 上一個(gè)區(qū)塊的哈希值。
接下來(lái),驗(yàn)證新區(qū)塊是否valid的方法:
func isBlockValid(newBlock, oldBlock Block) bool {
if oldBlock.Index+1 != newBlock.Index {
return false
}
if oldBlock.Hash != newBlock.PrevHash {
return false
}
if calculateHash(newBlock) != newBlock.Hash {
return false
}
return true
}
通過(guò)比對(duì)新舊區(qū)塊的 Index, 哈希值來(lái)決定新生成的區(qū)塊是否 Valid。
區(qū)塊鏈會(huì)發(fā)生分叉,我們?nèi)「L(zhǎng)的鏈作為正確的鏈:
func replaceChain(newBlocks []Block) {
if len(newBlocks) > len(Blockchain) {
Blockchain = newBlocks
}
}

到這一步,整個(gè)區(qū)塊鏈的基本功能就編寫完成了。下一步,我們開(kāi)始web server的編寫。
首先,寫一個(gè) run 方法來(lái)跑我們的server, 后面會(huì)引用。
func run() error {
mux := makeMuxRouter()
httpAddr := os.Getenv("PORT")
log.Println("Listening on ", os.Getenv("PORT"))
s := &http.Server{
Addr: ":" + httpAddr,
Handler: mux,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
}
if err := s.ListenAndServe(); err != nil {
return err
}
return nil
}
os.Getenv("PORT") 就是我們前面用.env 文件的端口號(hào)。
接著,寫一個(gè)處理http請(qǐng)求的方法:
func makeMuxRouter() http.Handler {
muxRouter := mux.NewRouter()
muxRouter.HandleFunc("/", handleGetBlockchain).Methods("GET")
muxRouter.HandleFunc("/", handleWriteBlock).Methods("POST")
return muxRouter
}
一個(gè)GET請(qǐng)求,一個(gè)POST請(qǐng)求,都是到"/"這個(gè)路由。
我們的GET請(qǐng)求方法:
func handleGetBlockchain(w http.ResponseWriter, r *http.Request) {
bytes, err := json.MarshalIndent(Blockchain, "", " ")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
io.WriteString(w, string(bytes))
}
當(dāng)瀏覽 localhost:8080時(shí)候,我們把整個(gè)區(qū)塊鏈數(shù)據(jù)以JSON的格式返回并顯示在頁(yè)面上。
在寫 POST 請(qǐng)求之前,先加入一個(gè)新的 struct:
type Message struct {
Data int
}
方面我們后面POST請(qǐng)求寫入Data數(shù)據(jù)。
我們的POST請(qǐng)求方法:
func handleWriteBlock(w http.ResponseWriter, r *http.Request) {
var m Message
decoder := json.NewDecoder(r.Body)
if err := decoder.Decode(&m); err != nil {
respondWithJSON(w, r, http.StatusBadRequest, r.Body)
return
}
defer r.Body.Close()
newBlock, err := generateBlock(Blockchain[len(Blockchain)-1], m.Data)
if err != nil {
respondWithJSON(w, r, http.StatusInternalServerError, m)
return
}
if isBlockValid(newBlock, Blockchain[len(Blockchain)-1]) {
newBlockchain := append(Blockchain, newBlock)
replaceChain(newBlockchain)
spew.Dump(Blockchain)
}
respondWithJSON(w, r, http.StatusCreated, newBlock)
}
這樣我們就可以用 {"Data": 77} 這樣的方式通過(guò)POST請(qǐng)求寫入新的數(shù)據(jù)。
然后,寫一個(gè)統(tǒng)一的 JSON數(shù)據(jù) 返回方法:
func respondWithJSON(w http.ResponseWriter, r *http.Request, code int, payload interface{}) {
response, err := json.MarshalIndent(payload, "", " ")
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("HTTP 500: Internal Server Error"))
return
}
w.WriteHeader(code)
w.Write(response)
}
我們封裝了一個(gè) 返回 JSON數(shù)據(jù)的方法來(lái)更好的告知所有的請(qǐng)求的發(fā)生詳細(xì)過(guò)程。
最后,我們的 main 方法:
func main() {
err := godotenv.Load()
if err != nil {
log.Fatal(err)
}
go func() {
t := time.Now()
genesisBlock := Block{0, t.String(), 0, "", ""}
spew.Dump(genesisBlock)
Blockchain = append(Blockchain, genesisBlock)
}()
log.Fatal(run())
}
Godotenv.Load()這里獲取我們前面在 .env 文件定義的port 環(huán)境變量。
genesisBlock 是創(chuàng)世區(qū)塊,也就是區(qū)塊鏈的第一個(gè)區(qū)塊。
現(xiàn)在可以在命令行用 go run main.go 來(lái)跑一下試試看。

可以試著往 localhost:8080 傳入 {"Data": 55} 來(lái)寫入新的區(qū)塊:

新區(qū)塊的PrevHash的哈希值與上一個(gè)區(qū)塊的Hash 哈希值是相同的。
Index 也對(duì)應(yīng)的增加1。

訪問(wèn)
localhost:8080 顯示了當(dāng)前區(qū)塊鏈的所有區(qū)塊數(shù)據(jù)。
這樣我們就完成了一個(gè)小型的區(qū)塊鏈,下一部分我們會(huì)加入Proof of Work(區(qū)塊鏈的工作量證明機(jī)制)。
Part2 工作量共識(shí)機(jī)制(Proof of Work)
1.1 基礎(chǔ)設(shè)置以及基礎(chǔ)概念
首先,先復(fù)習(xí)一下我們?cè)诘谝徊糠滞瓿傻拇a,第二部分是基于第一部分的基礎(chǔ)上再繼續(xù)開(kāi)發(fā)的。
package main
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"io"
"log"
"net/http"
"os"
"sync"
"time"
"github.com/davecgh/go-spew/spew"
"github.com/gorilla/mux"
"github.com/joho/godotenv"
)
type Block struct {
Index int
Timestamp string
Data int
Hash string
PrevHash string
}
type Message struct {
Data int
}
var mutex = &sync.Mutex{}
var Blockchain []Block
func calculateHash(block Block) string {
record := string(block.Index) + block.Timestamp + string(block.Data) + block.PrevHash
h := sha256.New()
h.Write([]byte(record))
hashed := h.Sum(nil)
return hex.EncodeToString(hashed)
}
func generateBlock(oldBlock Block, Data int) (Block, error) {
var newBlock Block
t := time.Now()
newBlock.Index = oldBlock.Index + 1
newBlock.Timestamp = t.String()
newBlock.Data = Data
newBlock.PrevHash = oldBlock.Hash
newBlock.Hash = calculateHash(newBlock)
return newBlock, nil
}
func isBlockValid(newBlock, oldBlock Block) bool {
if oldBlock.Index+1 != newBlock.Index {
return false
}
if oldBlock.Hash != newBlock.PrevHash {
return false
}
if calculateHash(newBlock) != newBlock.Hash {
return false
}
return true
}
func replaceChain(newBlocks []Block) {
if len(newBlocks) > len(Blockchain) {
Blockchain = newBlocks
}
}
func run() error {
mux := makeMuxRouter()
httpAddr := os.Getenv("PORT")
log.Println("Listening on ", os.Getenv("PORT"))
s := &http.Server{
Addr: ":" + httpAddr,
Handler: mux,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
}
if err := s.ListenAndServe(); err != nil {
return err
}
return nil
}
func makeMuxRouter() http.Handler {
muxRouter := mux.NewRouter()
muxRouter.HandleFunc("/", handleGetBlockchain).Methods("GET")
muxRouter.HandleFunc("/", handleWriteBlock).Methods("POST")
return muxRouter
}
func handleGetBlockchain(w http.ResponseWriter, r *http.Request) {
bytes, err := json.MarshalIndent(Blockchain, "", " ")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
io.WriteString(w, string(bytes))
}
func handleWriteBlock(w http.ResponseWriter, r *http.Request) {
var m Message
decoder := json.NewDecoder(r.Body)
if err := decoder.Decode(&m); err != nil {
respondWithJSON(w, r, http.StatusBadRequest, r.Body)
return
}
defer r.Body.Close()
mutex.Lock()
newBlock, err := generateBlock(Blockchain[len(Blockchain)-1], m.Data)
mutex.Unlock()
if err != nil {
respondWithJSON(w, r, http.StatusInternalServerError, m)
return
}
if isBlockValid(newBlock, Blockchain[len(Blockchain)-1]) {
newBlockchain := append(Blockchain, newBlock)
replaceChain(newBlockchain)
spew.Dump(Blockchain)
}
respondWithJSON(w, r, http.StatusCreated, newBlock)
}
func respondWithJSON(w http.ResponseWriter, r *http.Request, code int, payload interface{}) {
response, err := json.MarshalIndent(payload, "", " ")
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("HTTP 500: Internal Server Error"))
return
}
w.WriteHeader(code)
w.Write(response)
}
func main() {
err := godotenv.Load()
if err != nil {
log.Fatal(err)
}
go func() {
t := time.Now()
genesisBlock := Block{}
genesisBlock = Block{0, t.String(), 0, calculateHash(genesisBlock), ""}
spew.Dump(generateBlock)
mutex.Lock()
Blockchain = append(Blockchain, genesisBlock)
mutex.Unlock()
}()
log.Fatal(run())
}
熟悉完第一部分的代碼之后,我們來(lái)慢慢加入一些功能來(lái)實(shí)現(xiàn)Proof of Work。
在進(jìn)行開(kāi)發(fā)之前,先講解一下什么是挖礦,Proof of Work。
1.2 加密貨幣的挖礦
挖擴(kuò)其實(shí)是通過(guò)解答數(shù)學(xué)難題,讓礦工在區(qū)塊鏈中獲得生成一個(gè)新區(qū)塊的權(quán)利,并且因此而得到對(duì)應(yīng)的獎(jiǎng)勵(lì)(一般都是對(duì)應(yīng)的貨幣獎(jiǎng)勵(lì),比如比特幣,以太幣)。
那上面所說(shuō)的解答數(shù)學(xué)難題的過(guò)程,其實(shí)就是PoW。要理解PoW前,先了解什么是哈希函數(shù) 請(qǐng)翻看我之前寫的密碼學(xué)系統(tǒng)介紹的第5部分,哈希函數(shù)加密
PoW原理上就是要找出一個(gè) Nonce 的值,當(dāng)這個(gè) Nonce 值加上區(qū)塊的其他數(shù)據(jù)的SHA256的哈希值擁有我們所指定的難度的帶有多少個(gè)以0為開(kāi)頭的哈希值,那么這個(gè)數(shù)學(xué)難題就算是成功解答了。
1.3 PoW開(kāi)發(fā)
這部分比第一部分新增加了一些包
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"strconv"
"strings"
"sync"
"time"
"github.com/davecgh/go-spew/spew"
"github.com/gorilla/mux"
"github.com/joho/godotenv"
)
增加一個(gè)difficulty 常量,定義PoW的解答難度,即哈希值要有多少個(gè)0開(kāi)頭才能算是解答成功。
const difficulty = 1
type Block struct {
Index int
Timestamp string
Data int
Hash string
PrevHash string
Difficulty int
Nonce string
}
var mutex = &sync.Mutex{}
Block的struct 加入 Difficulty,以及 Nonce, 用來(lái)計(jì)算哈希值。
mutex 在后面用來(lái),防止有同時(shí)的寫入請(qǐng)求造成的錯(cuò)誤。
新的 calculateHash 用 strconv.Itoa方法來(lái)轉(zhuǎn)換成字符串,以及加入了Nonce值來(lái)計(jì)算哈希值
func calculateHash(block Block) string {
record := strconv.Itoa(block.Index) + block.Timestamp + strconv.Itoa(block.Data) + block.PrevHash + block.Nonce
h := sha256.New()
h.Write([]byte(record))
hashed := h.Sum(nil)
return hex.EncodeToString(hashed)
}
isHashValid方法用來(lái)驗(yàn)證哈希值是否以difficulty定義的個(gè)數(shù)的0開(kāi)頭。
func isHashValid(hash string, difficulty int) bool {
prefix := strings.Repeat("0", difficulty)
return strings.HasPrefix(hash, prefix)
}
generateBlock方法我們加入for loop 來(lái)處理如何生成一個(gè)新的block。以nonce轉(zhuǎn)換成hex值來(lái)加入哈希值的計(jì)算,從0開(kāi)始,一旦滿足了isHashValid方法的要求,就可以視為解題成功。
func generateBlock(oldBlock Block, Data int) (Block, error) {
var newBlock Block
t := time.Now()
newBlock.Index = oldBlock.Index + 1
newBlock.Timestamp = t.String()
newBlock.Data = Data
newBlock.PrevHash = oldBlock.Hash
newBlock.Difficulty = difficulty
for i := 0; ; i++ {
hex := fmt.Sprintf("%x", i)
newBlock.Nonce = hex
if !isHashValid(calculateHash(newBlock), newBlock.Difficulty) {
fmt.Println(calculateHash(newBlock), " do more work!!")
time.Sleep(time.Second)
continue
} else {
fmt.Println(calculateHash(newBlock), " work done!!")
newBlock.Hash = calculateHash(newBlock)
break
}
}
return newBlock, nil
}
最后,我們 main 方法也加入了mutex的處理。創(chuàng)世區(qū)塊的創(chuàng)建也加入了對(duì)應(yīng)的新屬性。
func main() {
err := godotenv.Load()
if err != nil {
log.Fatal(err)
}
go func() {
t := time.Now()
genesisBlock := Block{}
genesisBlock = Block{0, t.String(), 0, calculateHash(genesisBlock), "", difficulty, ""}
spew.Dump(genesisBlock)
mutex.Lock()
Blockchain = append(Blockchain, genesisBlock)
mutex.Unlock()
}()
log.Fatal(run())
}
到這里,我們就在第一部分的區(qū)塊鏈上面實(shí)現(xiàn)了PoW的功能了。
以防有某些地方?jīng)]有看懂,下面是第二部分的完整代碼。以供參考。
package main
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"strconv"
"strings"
"sync"
"time"
"github.com/davecgh/go-spew/spew"
"github.com/gorilla/mux"
"github.com/joho/godotenv"
)
const difficulty = 1
type Block struct {
Index int
Timestamp string
Data int
Hash string
PrevHash string
Difficulty int
Nonce string
}
var mutex = &sync.Mutex{}
type Message struct {
Data int
}
var Blockchain []Block
func calculateHash(block Block) string {
record := strconv.Itoa(block.Index) + block.Timestamp + strconv.Itoa(block.Data) + block.PrevHash + block.Nonce
h := sha256.New()
h.Write([]byte(record))
hashed := h.Sum(nil)
return hex.EncodeToString(hashed)
}
func generateBlock(oldBlock Block, Data int) (Block, error) {
var newBlock Block
t := time.Now()
newBlock.Index = oldBlock.Index + 1
newBlock.Timestamp = t.String()
newBlock.Data = Data
newBlock.PrevHash = oldBlock.Hash
newBlock.Difficulty = difficulty
for i := 0; ; i++ {
hex := fmt.Sprintf("%x", i)
newBlock.Nonce = hex
if !isHashValid(calculateHash(newBlock), newBlock.Difficulty) {
fmt.Println(calculateHash(newBlock), " do more work!!")
time.Sleep(time.Second)
continue
} else {
fmt.Println(calculateHash(newBlock), " work done!!")
newBlock.Hash = calculateHash(newBlock)
break
}
}
return newBlock, nil
}
func isBlockValid(newBlock, oldBlock Block) bool {
if oldBlock.Index+1 != newBlock.Index {
return false
}
if oldBlock.Hash != newBlock.PrevHash {
return false
}
if calculateHash(newBlock) != newBlock.Hash {
return false
}
return true
}
func replaceChain(newBlocks []Block) {
if len(newBlocks) > len(Blockchain) {
Blockchain = newBlocks
}
}
func run() error {
mux := makeMuxRouter()
httpAddr := os.Getenv("PORT")
log.Println("Listening on ", os.Getenv("PORT"))
s := &http.Server{
Addr: ":" + httpAddr,
Handler: mux,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
}
if err := s.ListenAndServe(); err != nil {
return err
}
return nil
}
func makeMuxRouter() http.Handler {
muxRouter := mux.NewRouter()
muxRouter.HandleFunc("/", handleGetBlockchain).Methods("GET")
muxRouter.HandleFunc("/", handleWriteBlock).Methods("POST")
return muxRouter
}
func handleGetBlockchain(w http.ResponseWriter, r *http.Request) {
bytes, err := json.MarshalIndent(Blockchain, "", " ")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
io.WriteString(w, string(bytes))
}
func handleWriteBlock(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
var m Message
decoder := json.NewDecoder(r.Body)
if err := decoder.Decode(&m); err != nil {
respondWithJSON(w, r, http.StatusBadRequest, r.Body)
return
}
defer r.Body.Close()
mutex.Lock()
newBlock, err := generateBlock(Blockchain[len(Blockchain)-1], m.Data)
mutex.Unlock()
if err != nil {
respondWithJSON(w, r, http.StatusInternalServerError, m)
return
}
if isBlockValid(newBlock, Blockchain[len(Blockchain)-1]) {
newBlockchain := append(Blockchain, newBlock)
replaceChain(newBlockchain)
spew.Dump(Blockchain)
}
respondWithJSON(w, r, http.StatusCreated, newBlock)
}
func respondWithJSON(w http.ResponseWriter, r *http.Request, code int, payload interface{}) {
response, err := json.MarshalIndent(payload, "", " ")
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("HTTP 500: Internal Server Error"))
return
}
w.WriteHeader(code)
w.Write(response)
}
func isHashValid(hash string, difficulty int) bool {
prefix := strings.Repeat("0", difficulty)
return strings.HasPrefix(hash, prefix)
}
func main() {
err := godotenv.Load()
if err != nil {
log.Fatal(err)
}
go func() {
t := time.Now()
genesisBlock := Block{}
genesisBlock = Block{0, t.String(), 0, calculateHash(genesisBlock), "", difficulty, ""}
spew.Dump(generateBlock)
mutex.Lock()
Blockchain = append(Blockchain, genesisBlock)
mutex.Unlock()
}()
log.Fatal(run())
}
在命令行執(zhí)行 go run main.go 來(lái)試試看。
我們?cè)囋囃?localhost:8080 寫入新的數(shù)據(jù) {"Data": 99}, 會(huì)發(fā)現(xiàn)不像第一部分那樣可以馬上生成新的區(qū)塊,而是要先進(jìn)行挖擴(kuò),當(dāng)找到了我們?cè)?Difficulty 常量所定義的數(shù)字開(kāi)頭的多少個(gè)0開(kāi)頭的哈希值后,然后才允許往區(qū)塊鏈中寫入新的區(qū)塊。

這就是帶有PoW機(jī)制的區(qū)塊鏈?,F(xiàn)實(shí)中,比如比特幣或者以太坊區(qū)塊鏈,所要求的難度會(huì)高很多,比如需要找出幾十個(gè)0開(kāi)頭的哈希值,這需要非常大的計(jì)算量。而且,他們還會(huì)根據(jù)生成新區(qū)塊的速度(間隔時(shí)間)來(lái)相應(yīng)的調(diào)整難度。
Part 3 為區(qū)塊鏈添加節(jié)點(diǎn)
1.1 前期準(zhǔn)備
這是目前為part3準(zhǔn)備的基本代碼:
package main
import (
"bufio"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"io"
"log"
"net"
"os"
"strconv"
"sync"
"time"
"github.com/davecgh/go-spew/spew"
"github.com/joho/godotenv"
)
type Block struct {
Index int
Timestamp string
Data int
Hash string
PrevHash string
}
type Message struct {
Data int
}
var Blockchain []Block
func calculateHash(block Block) string {
record := string(block.Index) + block.Timestamp + string(block.Data) + block.PrevHash
h := sha256.New()
h.Write([]byte(record))
hashed := h.Sum(nil)
return hex.EncodeToString(hashed)
}
func generateBlock(oldBlock Block, Data int) (Block, error) {
var newBlock Block
t := time.Now()
newBlock.Index = oldBlock.Index + 1
newBlock.Timestamp = t.String()
newBlock.Data = Data
newBlock.PrevHash = oldBlock.Hash
newBlock.Hash = calculateHash(newBlock)
return newBlock, nil
}
func isBlockValid(newBlock, oldBlock Block) bool {
if oldBlock.Index+1 != newBlock.Index {
return false
}
if oldBlock.Hash != newBlock.PrevHash {
return false
}
if calculateHash(newBlock) != newBlock.Hash {
return false
}
return true
}
func replaceChain(newBlocks []Block) {
if len(newBlocks) > len(Blockchain) {
Blockchain = newBlocks
}
}
我們用 tcp 來(lái)實(shí)現(xiàn)添加新節(jié)點(diǎn)的連接:

首先,定義一個(gè)channel來(lái)處理連接過(guò)來(lái)的區(qū)塊:
var bcServer chan []Block
GoRoutine 和 channel 是Go語(yǔ)言很重要的兩大功能 詳細(xì)可以看這里學(xué)習(xí)
接下來(lái),開(kāi)始寫我們的 main() 方法:
func main() {
err := godotenv.Load()
if err != nil {
log.Fatal(err)
}
bcServer = make(chan []Block)
// create genesis block
t := time.Now()
genesisBlock := Block{0, t.String(), 0, "", ""}
spew.Dump(genesisBlock)
Blockchain = append(Blockchain, genesisBlock)
// start TCP and serve TCP server
server, err := net.Listen("tcp", ":"+os.Getenv("ADDR"))
if err != nil {
log.Fatal(err)
}
defer server.Close()
for {
conn, err := server.Accept()
if err != nil {
log.Fatal(err)
}
go handleConn(conn)
}
}
在這里,我們初始化了 bcServer, 創(chuàng)造創(chuàng)世區(qū)塊, 以及用 tcp server 來(lái)監(jiān)聽(tīng)端口, 最后用一個(gè) for loop 以及 handleConn的方法來(lái)處理想要進(jìn)行連接的節(jié)點(diǎn)。
然后,我們來(lái)實(shí)現(xiàn) handleConn 方法:
func handleConn(conn net.Conn) {
defer conn.Close()
io.WriteString(conn, "Enter a new Data:")
scanner := bufio.NewScanner(conn)
go func() {
for scanner.Scan() {
data, err := strconv.Atoi(scanner.Text())
if err != nil {
log.Printf("%v not a number: %v", scanner.Text(), err)
continue
}
newBlock, err := generateBlock(Blockchain[len(Blockchain)-1], data)
if err != nil {
log.Println(err)
continue
}
if isBlockValid(newBlock, Blockchain[len(Blockchain)-1]) {
newBlockchain := append(Blockchain, newBlock)
replaceChain(newBlockchain)
}
bcServer <- Blockchain
io.WriteString(conn, "\n Enter a new Data:")
}
}()
go func() {
for {
time.Sleep(30 * time.Second)
output, err := json.Marshal(Blockchain)
if err != nil {
log.Fatal(err)
}
io.WriteString(conn, string(output))
}
}()
for _ = range bcServer {
spew.Dump(Blockchain)
}
}
這個(gè)方法里我們實(shí)現(xiàn)了:
- defer conn.Close() 是當(dāng)完成我們需要執(zhí)行的功能時(shí),就把端口關(guān)閉。
- 連接的端口可以傳一個(gè)Data數(shù)據(jù)過(guò)來(lái),寫成新的區(qū)塊,包括驗(yàn)證區(qū)塊等,最后把我們的整個(gè)區(qū)塊鏈發(fā)送給bcServer這個(gè)channel。
- 每隔30秒會(huì)同步第一個(gè)終端的最新的區(qū)塊鏈數(shù)據(jù)給所有連接的節(jié)點(diǎn)。
運(yùn)行 go run main.go

開(kāi)啟任何新的終端 輸入 nc localhost 8080 會(huì)連接到第一個(gè)終端,輸入Data值后會(huì)生成新的區(qū)塊,并且每隔30秒會(huì)給新的所有連接的節(jié)點(diǎn)同步最新的區(qū)塊鏈數(shù)據(jù)信息。

至此,我們就完成了加入一個(gè)模擬p2p網(wǎng)絡(luò)功能的區(qū)塊鏈。當(dāng)然,這個(gè)不是真的實(shí)現(xiàn)了p2p網(wǎng)絡(luò),這是在本地機(jī)器上的一個(gè)不同終端之間的模擬,真實(shí)的p2p網(wǎng)絡(luò)是復(fù)雜很多的。Go語(yǔ)言有一個(gè) libp2p 的庫(kù),大家如果有興趣可以去看看學(xué)習(xí),這個(gè)庫(kù)是真正的實(shí)現(xiàn)了p2p網(wǎng)絡(luò)的功能,可以嘗試在這個(gè)庫(kù)的基礎(chǔ)上開(kāi)發(fā)一個(gè)真正的p2p區(qū)塊鏈。