使用Go语言从零编写PoS区块链(译)
PoS簡介
在上一篇文章中,我們討論了工作量證明(Proof of Work),并向您展示了如何編寫自己的工作量證明區塊鏈。當前最流行的兩個區塊鏈平臺,比特幣和Ethereum都是基于工作量證明的。
但是工作證明的缺點是什么呢?其中一個主要的問題是電力能源的消耗。為了挖掘更多的比特幣,就需要建立更多的挖礦硬件池,現在在世界各地,挖礦池都在不斷建立中,而且呈現出規模越來越大的趨勢。例如以下這張照片(僅僅是礦池的一角):
挖礦工作需要耗費大量的電力,僅比特幣開采耗費的能源就超過了159個國家的電力能源消耗總和!!這種能源消耗是非常非常不合理的,而且,從技術的角度來看,工作量證明還有其他不足之處:隨著越來越多的人參與到挖礦工作中,共識算法的難度就需要提高,難度的提高意味著需要更多、更長時間的挖礦,也意味著區塊和交易需要更長的時間才能得到處理,因此能源的消耗就會越發的高。總之,工作量證明的方式就是一場競賽,你需要更多的計算能力才能有更大的概率贏得比賽。
有很多區塊鏈學者都試圖找到工作量證明的替代品,到目前為止最有希望的就是PoS(權益證明或者股權證明,Proof of Stake)。目前在生產環境,已經有數個區塊鏈平臺使用了PoS,例如Nxt?和Neo。以太坊Ethereum在不遠的未來也很可能會使用PoS——他們的Casper項目已經在測試網絡上運行和測試了。
那么,到底什么才是股權證明PoS呢?
在PoW中,節點之間通過hash的計算力來競賽以獲取下一個區塊的記賬權,而在PoS中,塊是已經鑄造好的(這里沒有“挖礦”的概念,所以我們不用這個詞來證明股份),鑄造的過程是基于每個節點(Node)愿意作為抵押的令牌(Token)數量。
這些參與抵押的節點被稱為驗證者(Validator),**注意在本文后續內容中,驗證者和節點的概念是等同的!**令牌的含義對于不同的區塊鏈平臺是不同的,例如,在以太坊中,每個驗證者都將Ether作為抵押品。
如果驗證者愿意提供更多的令牌作為抵押品,他們就有更大的機會記賬下一個區塊并獲得獎勵。你可以把獎勵的區塊看作是存款利息,你在銀行存的錢越多,你每月的利息就會越高。
因此,這種共識機制被稱為股權證明PoS。
PoS的缺陷是什么?
您可能已經猜到,一個擁有大量令牌的驗證者會在創建新塊時根據持有的令牌數量獲得更高的概率。然而,這與我們在工作量證明中看到的并沒有什么不同:比特幣礦場變得越來越強大,普通人在自己的電腦上開采多年也未必能獲得一個區塊。
因此,許多人認為,使用了PoS后,區塊的分配將更加民主化,因為任何人都可以在自己的筆記本上參與,而不需要建立一個巨大的采礦平臺,他們不需要昂貴的硬件,只需要一定的籌碼,就算籌碼不多,也有一定概率能獲得區塊的記賬權,希望總是有的,你說呢?
從技術和經濟的角度來看,還有其他不利因素。我們不會一一介紹,但這里有一個很好的介紹。在實際應用中,PoS和PoW都有自己的優點和缺點,因此以太坊的Casper具有兩者混合的特征。
像往常一樣,了解PoS的方法是編寫自己的代碼,那么,我們開始吧!
編寫PoS代碼
我們建議在繼續之前看一下200行Go代碼編寫區塊鏈Part2,因為在接下來的文章中,一些基礎知識不再會介紹,因此這篇文章能幫助你回顧一下。
注意
我們將實現PoS的核心概念,然后因為文章長度有限,因此一些不必要的代碼獎省去!
P2P網絡的實現。文中的網絡是模擬的,區塊鏈狀態只在其中一個中心化節點持有,而不是每個節點,同時狀態通過該持有節點廣播到其它節點
錢包和余額變動。本文沒有實現一個錢包,持有的令牌數量是通過stdin(標準輸入)輸入的,你可以輸入你想要的任何數量。一個完整的實現會為每個節點分配一個hash地址,并在節點中跟蹤余額的變動
架構圖
- 我們將有一個中心化的TCP服務節點,其他節點可以連接該服務器
- 最新的區塊鏈狀態將定期廣播到每個節點
- 每個節點都能提議建立新的區塊
- 基于每個節點的令牌數量,其中一個節點將隨機地(以令牌數作為加權值)作為獲勝者,并且將該區塊添加到區塊鏈中
設置和導入
在開始寫代碼之前,我們需要一個環境變量來設置TCP服務器的端口,首先在工作文件夾中創建.env文件,寫入一行配置:
ADDR=9000
我們的Go程序將讀取該文件,并且暴露出9000端口。同時在工作目錄下,再創建一個main.go文件。
- spew?可以把我們的區塊鏈用漂亮的格式打印到終端terminal中
- godotenv?允許我們從之前創建的.evn文件讀取配置
快速脈搏檢查
如果你讀過我們的其他教程,就會知道我們是一家醫療保健公司,目前要去收集人體脈搏信息,同時添加到我們的區塊上。把兩個手指放在你的手腕上,數一下你一分鐘能感覺到多少次脈搏,這將是您的BPM整數,我們將在接下來的文章中使用。
全局變量
現在,讓我們聲明我們需要的所有全局變量(main.go中)。
// Block represents each 'item' in the blockchaintype Block struct {Index intTimestamp stringBPM intHash stringPrevHash stringValidator string}// Blockchain is a series of validated Blocksvar Blockchain []Blockvar tempBlocks []Block// candidateBlocks handles incoming blocks for validationvar candidateBlocks = make(chan Block)// announcements broadcasts winning validator to all nodesvar announcements = make(chan string)var mutex = &sync.Mutex{}// validators keeps track of open validators and balancesvar validators = make(map[string]int- Block是每個區塊的內容
- Blockchain是我們的官方區塊鏈,它只是一串經過驗證的區塊集合。每個區塊中的PrevHash與前面塊的Hash相比較,以確保我們的鏈是正確的。tempBlocks是臨時存儲單元,在區塊被選出來并添加到BlockChain之前,臨時存儲在這里
- candidateBlocks是Block的通道,任何一個節點在提出一個新塊時都將它發送到這個通道
- announcements也是一個通道,我們的主Go TCP服務器將向所有節點廣播最新的區塊鏈
- mutex是一個標準變量,允許我們控制讀/寫和防止數據競爭
- validators是節點的存儲map,同時也會保存每個節點持有的令牌數
基本的區塊鏈函數
在繼續PoS算法之前,我們先來實現標準的區塊鏈函數。如果你之前看過200行Go代碼編寫區塊鏈,那接下來應該更加熟悉。
main.go
這里先從hash函數開始,calculateHash函數會接受一個string,并且返回一個SHA256 hash。calculateBlockHash是對一個block進行hash,將一個block的所有字段連接到一起后,再進行hash。main.go
func generateBlock(oldBlock Block, BPM int, address string) (Block, error) {var newBlock Blockt := time.Now()newBlock.Index = oldBlock.Index + 1newBlock.Timestamp = t.String()newBlock.BPM = BPMnewBlock.PrevHash = oldBlock.HashnewBlock.Hash = calculateBlockHash(newBlock)newBlock.Validator = addressreturn newBlock, nil}generateBlock是用來創建新塊的。每個新塊都有的一個重要字段是它的hash簽名(通過calculateBlockHash計算的)和上一個連接塊的PrevHash(因此我們可以保持鏈的完整性)。我們還添加了一個Validator字段,這樣我們就知道了該構建塊的獲勝節點。
main.go
// isBlockValid makes sure block is valid by checking index// and comparing the hash of the previous blockfunc isBlockValid(newBlock, oldBlock Block) bool {if oldBlock.Index+1 != newBlock.Index {return false}if oldBlock.Hash != newBlock.PrevHash {return false}if calculateBlockHash(newBlock) != newBlock.Hash {return false}return true}isBlockValid會驗證Block的當前hash和PrevHash,來確保我們的區塊鏈不會被污染。
節點(驗證者)
當一個驗證者連接到我們的TCP服務,我們需要提供一些函數達到以下目標:
- 輸入令牌的余額(之前提到過,我們不做錢包等邏輯)
- 接收區塊鏈的最新廣播
- 接收驗證者贏得區塊的廣播信息
- 將自身節點添加到全局的驗證者列表中(validators)
- 輸入Block的BPM數據- BPM是每個驗證者的人體脈搏值
- 提議創建一個新的區塊
這些目標,我們用handleConn函數來實現main.go
func handleConn(conn net.Conn) {defer conn.Close()go func() {for {msg := <-announcementsio.WriteString(conn, msg)}}()// validator addressvar address string// allow user to allocate number of tokens to stake// the greater the number of tokens, the greater chance to forging a new blockio.WriteString(conn, "Enter token balance:")scanBalance := bufio.NewScanner(conn)for scanBalance.Scan() {balance, err := strconv.Atoi(scanBalance.Text())if err != nil {log.Printf("%v not a number: %v", scanBalance.Text(), err)return}t := time.Now()address = calculateHash(t.String())validators[address] = balancefmt.Println(validators)break}io.WriteString(conn, "\nEnter a new BPM:")scanBPM := bufio.NewScanner(conn)go func() {for {// take in BPM from stdin and add it to blockchain after conducting necessary validationfor scanBPM.Scan() {bpm, err := strconv.Atoi(scanBPM.Text())// if malicious party tries to mutate the chain with a bad input, delete them as a validator and they lose their staked tokensif err != nil {log.Printf("%v not a number: %v", scanBPM.Text(), err)delete(validators, address)conn.Close()}mutex.Lock()oldLastIndex := Blockchain[len(Blockchain)-1]mutex.Unlock()// create newBlock for consideration to be forgednewBlock, err := generateBlock(oldLastIndex, bpm, address)if err != nil {log.Println(err)continue}if isBlockValid(newBlock, oldLastIndex) {candidateBlocks <- newBlock}io.WriteString(conn, "\nEnter a new BPM:")}}}()// simulate receiving broadcastfor {time.Sleep(time.Minute)mutex.Lock()output, err := json.Marshal(Blockchain)mutex.Unlock()if err != nil {log.Fatal(err)}io.WriteString(conn, string(output)+"\n")}}第一個Go協程接收并打印出來自TCP服務器的任何通知,這些通知包含了獲勝驗證者的通知。
io.WriteString(conn, “Enter token balance:”)允許驗證者輸入他持有的令牌數量,然后,該驗證者被分配一個SHA256地址,隨后該驗證者地址和驗證者的令牌數被添加到驗證者列表validators中。
接著我們輸入BPM,驗證者的脈搏值,并創建一個單獨的Go協程來處理這塊兒邏輯,下面這一行代碼很重要:
delete(validators, address)
如果驗證者試圖提議一個被污染(例如偽造)的block,例如包含一個不是整數的BPM,那么程序會拋出一個錯誤,我們會立即從我們的驗證器列表validators中刪除該驗證者,他們將不再有資格參與到新塊的鑄造過程同時丟失相應的抵押令牌。
正式因為這種抵押令牌的機制,使得PoS協議是一種更加可靠的機制。如果一個人試圖偽造和破壞,那么他將被抓住,并且失去所有抵押和未來的權益,因此對于惡意者來說,是非常大的威懾。
接著,我們用generateBlock函數創建一個新的block,然后將其發送到candidateBlocks通道進行進一步處理。將Block發送到通道使用的語法:
candidateBlocks <- newBlock
上面代碼中最后一段的循環會周期性的打印出最新的區塊鏈,這樣每個驗證者都能獲知最新的狀態
選擇獲勝者
這里是PoS的主題邏輯。我們需要編寫代碼以實現獲勝驗證者的選擇;他們所持有的令牌數量越高,他們就越有可能被選為勝利者。
為了簡化代碼,我們只會讓提出新塊兒的驗證者參與競爭。在傳統的PoS,一個驗證者即使沒有提出一個新的區塊,也可以被選為勝利者。切記,PoS不是一種確定的定義(算法),而是一種概念,因此對于不同的平臺來說,可以有不同的PoS實現。
下面來看看pickWinner函數:
main.go
每隔30秒,我們選出一個勝利者,這樣對于每個驗證者來說,都有時間提議新的區塊,參與到競爭中來。接著創建一個lotteryPool,它會持有所有驗證者的地址,這些驗證者都有機會成為一個勝利者。然后,對于提議塊的暫存區域,我們會通過if len(temp) > 0來判斷是否已經有了被提議的區塊。
在OUTER FOR循環中,要檢查暫存區域是否和lotteryPool中存在同樣的驗證者,如果存在,則跳過。
在以k, ok := setValidators[block.Validator]開始的代碼塊中,我們確保了從temp中取出來的驗證者都是合法的,即這些驗證者在驗證者列表validators已存在。若合法,則把該驗證者加入到lotteryPool中。
那么我們怎么根據這些驗證者持有的令牌數來給予他們合適的隨機權重呢?
首先,用驗證者的令牌填充lotteryPool數組,例如一個驗證者有100個令牌,那么在lotteryPool中就將有100個元素填充;如果有1個令牌,那么將僅填充1個元素。
然后,從lotteryPool中隨機選擇一個元素,元素所屬的驗證者即是勝利者,把勝利驗證者的地址賦值給lotteryWinner。這里能夠看出來,如果驗證者持有的令牌越多,那么他在數組中的元素也越多,他獲勝的概率就越大;同時,持有令牌很少的驗證者,也是有概率獲勝的。
接著我們把獲勝者的區塊添加到整條區塊鏈上,然后通知所有節點關于勝利者的消息:announcements <- “\nwinning validator: “ + lotteryWinner + “\n”。
最后,清空tempBlocks,以便下次提議的進行。
以上便是PoS一致性算法的核心內容,該算法簡單、明了、公正,所以很酷!
收尾
下面我們把之前的內容通過代碼都串聯起來main.go
func main() {err := godotenv.Load()if err != nil {log.Fatal(err)}// create genesis blockt := time.Now()genesisBlock := Block{}genesisBlock = Block{0, t.String(), 0, calculateBlockHash(genesisBlock), "", ""}spew.Dump(genesisBlock)Blockchain = append(Blockchain, genesisBlock)// start TCP and serve TCP serverserver, err := net.Listen("tcp", ":"+os.Getenv("ADDR"))if err != nil {log.Fatal(err)}defer server.Close()go func() {for candidate := range candidateBlocks {mutex.Lock()tempBlocks = append(tempBlocks, candidate)mutex.Unlock()}}()go func() {for {pickWinner()}}()for {conn, err := server.Accept()if err != nil {log.Fatal(err)}go handleConn(conn)}}這里從.env文件開始,然后創建一個創世區塊genesisBlock,形成了區塊鏈。接著啟動了Tcp服務,等待所有驗證者的連接。
啟動了一個Go協程從candidateBlocks通道中獲取提議的區塊,然后填充到臨時緩沖區tempBlocks中,最后啟動了另外一個Go協程來完成pickWinner函數。
最后面的for循環,用來接收驗證者節點的連接。
這里是所有的源代碼:mycoralhealth/blockchain-tutorial
結果
下面來運行程序,打開一個終端窗口,通過go run main.go來啟動整個TCP程序,如我們所料,首先創建了創始區塊genesisBlock。
接著,我們啟動并連接一個驗證者。打開一個新的終端窗口,通過linux命令nc localhost 9000來連接到之前的TCP服務。然后在命令提示符后輸入一個持有的令牌數額,最后再輸入一個驗證者的脈搏速率BPM。
然后觀察第一個窗口(主程序),可以看到驗證者被分配了地址,而且每次有新的驗證者加入時,都會打印所有的驗證者列表
稍等片刻,檢查下你的新窗口(驗證者),可以看到正在發生的事:我們的程序在花費時間選擇勝利者,然后Boom一聲,一個勝利者就誕生了!
再稍等一下,boom! 我們看到新的區塊鏈被廣播給所有的驗證者窗口,包含了勝利者的區塊和他的BPM信息。很酷吧!
下一步做什么
你應該為能通過本教程感到驕傲。大多數區塊鏈的發燒友和許多程序員都聽說過PoS的證明,但他們很多都無法解釋它到底是什么。你已經做得更深入了,而且實際上已經從頭開始實現了一遍,你離成為下一代區塊鏈技術的專家又近了一步!
因為這是一個教程,我們可以做更多的事情來讓它成為區塊鏈,例如:
- 閱讀我們的PoW,然后結合PoS,看看你是否可以創建一個混合區塊鏈
- 添加時間機制,驗證者根據時間塊來獲得提議新區快的概率。我們這個版本的代碼讓驗證者可以在任何時候提議新的區塊。
- 添加完整的點對點的能力。這基本上意味著每個驗證者將運行自己的TCP服務器,并連接到其他的驗證者節點。這里需要添加邏輯,這樣每個節點都可以找到彼此,這里有更多的內容。
或者你可以學習一下我們其它的教程:
- 200行Go代碼編寫區塊鏈
- 使用IPFS在區塊鏈上存儲文件
- 區塊鏈網絡
- 從零編寫PoW
轉載請在文章開頭注明作者和出處
作者: ChainGod(孫飛)
原文鏈接: http://chaingod.io/article/16
總結
以上是生活随笔為你收集整理的使用Go语言从零编写PoS区块链(译)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Plasma链0x1的构造
- 下一篇: gRPC学习记录(六)--客户端连接池