使用Go语言从零编写PoS区块链(译)
PoS簡(jiǎn)介
在上一篇文章中,我們討論了工作量證明(Proof of Work),并向您展示了如何編寫自己的工作量證明區(qū)塊鏈。當(dāng)前最流行的兩個(gè)區(qū)塊鏈平臺(tái),比特幣和Ethereum都是基于工作量證明的。
但是工作證明的缺點(diǎn)是什么呢?其中一個(gè)主要的問題是電力能源的消耗。為了挖掘更多的比特幣,就需要建立更多的挖礦硬件池,現(xiàn)在在世界各地,挖礦池都在不斷建立中,而且呈現(xiàn)出規(guī)模越來越大的趨勢(shì)。例如以下這張照片(僅僅是礦池的一角):
挖礦工作需要耗費(fèi)大量的電力,僅比特幣開采耗費(fèi)的能源就超過了159個(gè)國家的電力能源消耗總和!!這種能源消耗是非常非常不合理的,而且,從技術(shù)的角度來看,工作量證明還有其他不足之處:隨著越來越多的人參與到挖礦工作中,共識(shí)算法的難度就需要提高,難度的提高意味著需要更多、更長時(shí)間的挖礦,也意味著區(qū)塊和交易需要更長的時(shí)間才能得到處理,因此能源的消耗就會(huì)越發(fā)的高。總之,工作量證明的方式就是一場(chǎng)競(jìng)賽,你需要更多的計(jì)算能力才能有更大的概率贏得比賽。
有很多區(qū)塊鏈學(xué)者都試圖找到工作量證明的替代品,到目前為止最有希望的就是PoS(權(quán)益證明或者股權(quán)證明,Proof of Stake)。目前在生產(chǎn)環(huán)境,已經(jīng)有數(shù)個(gè)區(qū)塊鏈平臺(tái)使用了PoS,例如Nxt?和Neo。以太坊Ethereum在不遠(yuǎn)的未來也很可能會(huì)使用PoS——他們的Casper項(xiàng)目已經(jīng)在測(cè)試網(wǎng)絡(luò)上運(yùn)行和測(cè)試了。
那么,到底什么才是股權(quán)證明PoS呢?
在PoW中,節(jié)點(diǎn)之間通過hash的計(jì)算力來競(jìng)賽以獲取下一個(gè)區(qū)塊的記賬權(quán),而在PoS中,塊是已經(jīng)鑄造好的(這里沒有“挖礦”的概念,所以我們不用這個(gè)詞來證明股份),鑄造的過程是基于每個(gè)節(jié)點(diǎn)(Node)愿意作為抵押的令牌(Token)數(shù)量。
這些參與抵押的節(jié)點(diǎn)被稱為驗(yàn)證者(Validator),**注意在本文后續(xù)內(nèi)容中,驗(yàn)證者和節(jié)點(diǎn)的概念是等同的!**令牌的含義對(duì)于不同的區(qū)塊鏈平臺(tái)是不同的,例如,在以太坊中,每個(gè)驗(yàn)證者都將Ether作為抵押品。
如果驗(yàn)證者愿意提供更多的令牌作為抵押品,他們就有更大的機(jī)會(huì)記賬下一個(gè)區(qū)塊并獲得獎(jiǎng)勵(lì)。你可以把獎(jiǎng)勵(lì)的區(qū)塊看作是存款利息,你在銀行存的錢越多,你每月的利息就會(huì)越高。
因此,這種共識(shí)機(jī)制被稱為股權(quán)證明PoS。
PoS的缺陷是什么?
您可能已經(jīng)猜到,一個(gè)擁有大量令牌的驗(yàn)證者會(huì)在創(chuàng)建新塊時(shí)根據(jù)持有的令牌數(shù)量獲得更高的概率。然而,這與我們?cè)诠ぷ髁孔C明中看到的并沒有什么不同:比特幣礦場(chǎng)變得越來越強(qiáng)大,普通人在自己的電腦上開采多年也未必能獲得一個(gè)區(qū)塊。
因此,許多人認(rèn)為,使用了PoS后,區(qū)塊的分配將更加民主化,因?yàn)槿魏稳硕伎梢栽谧约旱墓P記本上參與,而不需要建立一個(gè)巨大的采礦平臺(tái),他們不需要昂貴的硬件,只需要一定的籌碼,就算籌碼不多,也有一定概率能獲得區(qū)塊的記賬權(quán),希望總是有的,你說呢?
從技術(shù)和經(jīng)濟(jì)的角度來看,還有其他不利因素。我們不會(huì)一一介紹,但這里有一個(gè)很好的介紹。在實(shí)際應(yīng)用中,PoS和PoW都有自己的優(yōu)點(diǎn)和缺點(diǎn),因此以太坊的Casper具有兩者混合的特征。
像往常一樣,了解PoS的方法是編寫自己的代碼,那么,我們開始吧!
編寫PoS代碼
我們建議在繼續(xù)之前看一下200行Go代碼編寫區(qū)塊鏈Part2,因?yàn)樵诮酉聛淼奈恼轮?#xff0c;一些基礎(chǔ)知識(shí)不再會(huì)介紹,因此這篇文章能幫助你回顧一下。
注意
我們將實(shí)現(xiàn)PoS的核心概念,然后因?yàn)槲恼麻L度有限,因此一些不必要的代碼獎(jiǎng)省去!
P2P網(wǎng)絡(luò)的實(shí)現(xiàn)。文中的網(wǎng)絡(luò)是模擬的,區(qū)塊鏈狀態(tài)只在其中一個(gè)中心化節(jié)點(diǎn)持有,而不是每個(gè)節(jié)點(diǎn),同時(shí)狀態(tài)通過該持有節(jié)點(diǎn)廣播到其它節(jié)點(diǎn)
錢包和余額變動(dòng)。本文沒有實(shí)現(xiàn)一個(gè)錢包,持有的令牌數(shù)量是通過stdin(標(biāo)準(zhǔn)輸入)輸入的,你可以輸入你想要的任何數(shù)量。一個(gè)完整的實(shí)現(xiàn)會(huì)為每個(gè)節(jié)點(diǎn)分配一個(gè)hash地址,并在節(jié)點(diǎn)中跟蹤余額的變動(dòng)
架構(gòu)圖
- 我們將有一個(gè)中心化的TCP服務(wù)節(jié)點(diǎn),其他節(jié)點(diǎn)可以連接該服務(wù)器
- 最新的區(qū)塊鏈狀態(tài)將定期廣播到每個(gè)節(jié)點(diǎn)
- 每個(gè)節(jié)點(diǎn)都能提議建立新的區(qū)塊
- 基于每個(gè)節(jié)點(diǎn)的令牌數(shù)量,其中一個(gè)節(jié)點(diǎn)將隨機(jī)地(以令牌數(shù)作為加權(quán)值)作為獲勝者,并且將該區(qū)塊添加到區(qū)塊鏈中
設(shè)置和導(dǎo)入
在開始寫代碼之前,我們需要一個(gè)環(huán)境變量來設(shè)置TCP服務(wù)器的端口,首先在工作文件夾中創(chuàng)建.env文件,寫入一行配置:
ADDR=9000
我們的Go程序?qū)⒆x取該文件,并且暴露出9000端口。同時(shí)在工作目錄下,再創(chuàng)建一個(gè)main.go文件。
- spew?可以把我們的區(qū)塊鏈用漂亮的格式打印到終端terminal中
- godotenv?允許我們從之前創(chuàng)建的.evn文件讀取配置
快速脈搏檢查
如果你讀過我們的其他教程,就會(huì)知道我們是一家醫(yī)療保健公司,目前要去收集人體脈搏信息,同時(shí)添加到我們的區(qū)塊上。把兩個(gè)手指放在你的手腕上,數(shù)一下你一分鐘能感覺到多少次脈搏,這將是您的BPM整數(shù),我們將在接下來的文章中使用。
全局變量
現(xiàn)在,讓我們聲明我們需要的所有全局變量(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是每個(gè)區(qū)塊的內(nèi)容
- Blockchain是我們的官方區(qū)塊鏈,它只是一串經(jīng)過驗(yàn)證的區(qū)塊集合。每個(gè)區(qū)塊中的PrevHash與前面塊的Hash相比較,以確保我們的鏈?zhǔn)钦_的。tempBlocks是臨時(shí)存儲(chǔ)單元,在區(qū)塊被選出來并添加到BlockChain之前,臨時(shí)存儲(chǔ)在這里
- candidateBlocks是Block的通道,任何一個(gè)節(jié)點(diǎn)在提出一個(gè)新塊時(shí)都將它發(fā)送到這個(gè)通道
- announcements也是一個(gè)通道,我們的主Go TCP服務(wù)器將向所有節(jié)點(diǎn)廣播最新的區(qū)塊鏈
- mutex是一個(gè)標(biāo)準(zhǔn)變量,允許我們控制讀/寫和防止數(shù)據(jù)競(jìng)爭(zhēng)
- validators是節(jié)點(diǎn)的存儲(chǔ)map,同時(shí)也會(huì)保存每個(gè)節(jié)點(diǎn)持有的令牌數(shù)
基本的區(qū)塊鏈函數(shù)
在繼續(xù)PoS算法之前,我們先來實(shí)現(xiàn)標(biāo)準(zhǔn)的區(qū)塊鏈函數(shù)。如果你之前看過200行Go代碼編寫區(qū)塊鏈,那接下來應(yīng)該更加熟悉。
main.go
這里先從hash函數(shù)開始,calculateHash函數(shù)會(huì)接受一個(gè)string,并且返回一個(gè)SHA256 hash。calculateBlockHash是對(duì)一個(gè)block進(jìn)行hash,將一個(gè)block的所有字段連接到一起后,再進(jìn)行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是用來創(chuàng)建新塊的。每個(gè)新塊都有的一個(gè)重要字段是它的hash簽名(通過calculateBlockHash計(jì)算的)和上一個(gè)連接塊的PrevHash(因此我們可以保持鏈的完整性)。我們還添加了一個(gè)Validator字段,這樣我們就知道了該構(gòu)建塊的獲勝節(jié)點(diǎn)。
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會(huì)驗(yàn)證Block的當(dāng)前hash和PrevHash,來確保我們的區(qū)塊鏈不會(huì)被污染。
節(jié)點(diǎn)(驗(yàn)證者)
當(dāng)一個(gè)驗(yàn)證者連接到我們的TCP服務(wù),我們需要提供一些函數(shù)達(dá)到以下目標(biāo):
- 輸入令牌的余額(之前提到過,我們不做錢包等邏輯)
- 接收區(qū)塊鏈的最新廣播
- 接收驗(yàn)證者贏得區(qū)塊的廣播信息
- 將自身節(jié)點(diǎn)添加到全局的驗(yàn)證者列表中(validators)
- 輸入Block的BPM數(shù)據(jù)- BPM是每個(gè)驗(yàn)證者的人體脈搏值
- 提議創(chuàng)建一個(gè)新的區(qū)塊
這些目標(biāo),我們用handleConn函數(shù)來實(shí)現(xiàn)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")}}第一個(gè)Go協(xié)程接收并打印出來自TCP服務(wù)器的任何通知,這些通知包含了獲勝驗(yàn)證者的通知。
io.WriteString(conn, “Enter token balance:”)允許驗(yàn)證者輸入他持有的令牌數(shù)量,然后,該驗(yàn)證者被分配一個(gè)SHA256地址,隨后該驗(yàn)證者地址和驗(yàn)證者的令牌數(shù)被添加到驗(yàn)證者列表validators中。
接著我們輸入BPM,驗(yàn)證者的脈搏值,并創(chuàng)建一個(gè)單獨(dú)的Go協(xié)程來處理這塊兒邏輯,下面這一行代碼很重要:
delete(validators, address)
如果驗(yàn)證者試圖提議一個(gè)被污染(例如偽造)的block,例如包含一個(gè)不是整數(shù)的BPM,那么程序會(huì)拋出一個(gè)錯(cuò)誤,我們會(huì)立即從我們的驗(yàn)證器列表validators中刪除該驗(yàn)證者,他們將不再有資格參與到新塊的鑄造過程同時(shí)丟失相應(yīng)的抵押令牌。
正式因?yàn)檫@種抵押令牌的機(jī)制,使得PoS協(xié)議是一種更加可靠的機(jī)制。如果一個(gè)人試圖偽造和破壞,那么他將被抓住,并且失去所有抵押和未來的權(quán)益,因此對(duì)于惡意者來說,是非常大的威懾。
接著,我們用generateBlock函數(shù)創(chuàng)建一個(gè)新的block,然后將其發(fā)送到candidateBlocks通道進(jìn)行進(jìn)一步處理。將Block發(fā)送到通道使用的語法:
candidateBlocks <- newBlock
上面代碼中最后一段的循環(huán)會(huì)周期性的打印出最新的區(qū)塊鏈,這樣每個(gè)驗(yàn)證者都能獲知最新的狀態(tài)
選擇獲勝者
這里是PoS的主題邏輯。我們需要編寫代碼以實(shí)現(xiàn)獲勝驗(yàn)證者的選擇;他們所持有的令牌數(shù)量越高,他們就越有可能被選為勝利者。
為了簡(jiǎn)化代碼,我們只會(huì)讓提出新塊兒的驗(yàn)證者參與競(jìng)爭(zhēng)。在傳統(tǒng)的PoS,一個(gè)驗(yàn)證者即使沒有提出一個(gè)新的區(qū)塊,也可以被選為勝利者。切記,PoS不是一種確定的定義(算法),而是一種概念,因此對(duì)于不同的平臺(tái)來說,可以有不同的PoS實(shí)現(xiàn)。
下面來看看pickWinner函數(shù):
main.go
每隔30秒,我們選出一個(gè)勝利者,這樣對(duì)于每個(gè)驗(yàn)證者來說,都有時(shí)間提議新的區(qū)塊,參與到競(jìng)爭(zhēng)中來。接著創(chuàng)建一個(gè)lotteryPool,它會(huì)持有所有驗(yàn)證者的地址,這些驗(yàn)證者都有機(jī)會(huì)成為一個(gè)勝利者。然后,對(duì)于提議塊的暫存區(qū)域,我們會(huì)通過if len(temp) > 0來判斷是否已經(jīng)有了被提議的區(qū)塊。
在OUTER FOR循環(huán)中,要檢查暫存區(qū)域是否和lotteryPool中存在同樣的驗(yàn)證者,如果存在,則跳過。
在以k, ok := setValidators[block.Validator]開始的代碼塊中,我們確保了從temp中取出來的驗(yàn)證者都是合法的,即這些驗(yàn)證者在驗(yàn)證者列表validators已存在。若合法,則把該驗(yàn)證者加入到lotteryPool中。
那么我們?cè)趺锤鶕?jù)這些驗(yàn)證者持有的令牌數(shù)來給予他們合適的隨機(jī)權(quán)重呢?
首先,用驗(yàn)證者的令牌填充lotteryPool數(shù)組,例如一個(gè)驗(yàn)證者有100個(gè)令牌,那么在lotteryPool中就將有100個(gè)元素填充;如果有1個(gè)令牌,那么將僅填充1個(gè)元素。
然后,從lotteryPool中隨機(jī)選擇一個(gè)元素,元素所屬的驗(yàn)證者即是勝利者,把勝利驗(yàn)證者的地址賦值給lotteryWinner。這里能夠看出來,如果驗(yàn)證者持有的令牌越多,那么他在數(shù)組中的元素也越多,他獲勝的概率就越大;同時(shí),持有令牌很少的驗(yàn)證者,也是有概率獲勝的。
接著我們把獲勝者的區(qū)塊添加到整條區(qū)塊鏈上,然后通知所有節(jié)點(diǎn)關(guān)于勝利者的消息:announcements <- “\nwinning validator: “ + lotteryWinner + “\n”。
最后,清空tempBlocks,以便下次提議的進(jìn)行。
以上便是PoS一致性算法的核心內(nèi)容,該算法簡(jiǎn)單、明了、公正,所以很酷!
收尾
下面我們把之前的內(nèi)容通過代碼都串聯(lián)起來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文件開始,然后創(chuàng)建一個(gè)創(chuàng)世區(qū)塊genesisBlock,形成了區(qū)塊鏈。接著啟動(dòng)了Tcp服務(wù),等待所有驗(yàn)證者的連接。
啟動(dòng)了一個(gè)Go協(xié)程從candidateBlocks通道中獲取提議的區(qū)塊,然后填充到臨時(shí)緩沖區(qū)tempBlocks中,最后啟動(dòng)了另外一個(gè)Go協(xié)程來完成pickWinner函數(shù)。
最后面的for循環(huán),用來接收驗(yàn)證者節(jié)點(diǎn)的連接。
這里是所有的源代碼:mycoralhealth/blockchain-tutorial
結(jié)果
下面來運(yùn)行程序,打開一個(gè)終端窗口,通過go run main.go來啟動(dòng)整個(gè)TCP程序,如我們所料,首先創(chuàng)建了創(chuàng)始區(qū)塊genesisBlock。
接著,我們啟動(dòng)并連接一個(gè)驗(yàn)證者。打開一個(gè)新的終端窗口,通過linux命令nc localhost 9000來連接到之前的TCP服務(wù)。然后在命令提示符后輸入一個(gè)持有的令牌數(shù)額,最后再輸入一個(gè)驗(yàn)證者的脈搏速率BPM。
然后觀察第一個(gè)窗口(主程序),可以看到驗(yàn)證者被分配了地址,而且每次有新的驗(yàn)證者加入時(shí),都會(huì)打印所有的驗(yàn)證者列表
稍等片刻,檢查下你的新窗口(驗(yàn)證者),可以看到正在發(fā)生的事:我們的程序在花費(fèi)時(shí)間選擇勝利者,然后Boom一聲,一個(gè)勝利者就誕生了!
再稍等一下,boom! 我們看到新的區(qū)塊鏈被廣播給所有的驗(yàn)證者窗口,包含了勝利者的區(qū)塊和他的BPM信息。很酷吧!
下一步做什么
你應(yīng)該為能通過本教程感到驕傲。大多數(shù)區(qū)塊鏈的發(fā)燒友和許多程序員都聽說過PoS的證明,但他們很多都無法解釋它到底是什么。你已經(jīng)做得更深入了,而且實(shí)際上已經(jīng)從頭開始實(shí)現(xiàn)了一遍,你離成為下一代區(qū)塊鏈技術(shù)的專家又近了一步!
因?yàn)檫@是一個(gè)教程,我們可以做更多的事情來讓它成為區(qū)塊鏈,例如:
- 閱讀我們的PoW,然后結(jié)合PoS,看看你是否可以創(chuàng)建一個(gè)混合區(qū)塊鏈
- 添加時(shí)間機(jī)制,驗(yàn)證者根據(jù)時(shí)間塊來獲得提議新區(qū)快的概率。我們這個(gè)版本的代碼讓驗(yàn)證者可以在任何時(shí)候提議新的區(qū)塊。
- 添加完整的點(diǎn)對(duì)點(diǎn)的能力。這基本上意味著每個(gè)驗(yàn)證者將運(yùn)行自己的TCP服務(wù)器,并連接到其他的驗(yàn)證者節(jié)點(diǎn)。這里需要添加邏輯,這樣每個(gè)節(jié)點(diǎn)都可以找到彼此,這里有更多的內(nèi)容。
或者你可以學(xué)習(xí)一下我們其它的教程:
- 200行Go代碼編寫區(qū)塊鏈
- 使用IPFS在區(qū)塊鏈上存儲(chǔ)文件
- 區(qū)塊鏈網(wǎng)絡(luò)
- 從零編寫PoW
轉(zhuǎn)載請(qǐng)?jiān)谖恼麻_頭注明作者和出處
作者: ChainGod(孫飛)
原文鏈接: http://chaingod.io/article/16
總結(jié)
以上是生活随笔為你收集整理的使用Go语言从零编写PoS区块链(译)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Plasma链0x1的构造
- 下一篇: gRPC学习记录(六)--客户端连接池