使用 go 实现 Proof of Stake 共识机制
使用 go 實現 Proof of Stake 共識機制
什么是 Proof of Stake
在PoW中,節點之間通過hash的計算力來競賽以獲取下一個區塊的記賬權,而在PoS中,塊是已經鑄造好的,鑄造的過程是基于每個節點(Node)愿意作為抵押的令牌(Token)數量。如果驗證者愿意提供更多的令牌作為抵押品,他們就有更大的機會記賬下一個區塊并獲得獎勵。
實現 Proof of Stake 主要功能點
- 我們將有一個中心化的TCP服務節點,其他節點可以連接該服務器
- 最新的區塊鏈狀態將定期廣播到每個節點
- 每個節點都能提議建立新的區塊
- 基于每個節點的令牌數量,其中一個節點將隨機地(以令牌數作為加權值)作為獲勝者,并且將該區塊添加到區塊鏈中
實現 Proof of Stake
設置 TCP 服務器的端口
新建 .env,添加如下內容 PORT=9000
安裝依賴軟件
$ go get github.com/davecgh/go-spew/spew$ go get github.com/joho/godotenvspew 在控制臺中格式化輸出相應的結果。
godotenv 可以從我們項目的根目錄的 .env 文件中讀取數據。
引入相應的包
新建 main.go,引入相應的包
package mainimport ("bufio""crypto/sha256""encoding/hex""encoding/json""fmt""io""log""math/rand""net""os""strconv""sync""time""github.com/davecgh/go-spew/spew""github.com/joho/godotenv" )全局變量
// Block represents each 'item' in the blockchain type Block struct {Index intTimestamp stringBPM intHash stringPrevHash stringValidator string }// Blockchain is a series of validated Blocks var Blockchain []Block var tempBlocks []Block// candidateBlocks handles incoming blocks for validation var candidateBlocks = make(chan Block)// announcements broadcasts winning validator to all nodes var announcements = make(chan string)var mutex = &sync.Mutex{}// validators keeps track of open validators and balances var validators = make(map[string]int)- Block 是每個區塊的內容
- Blockchain 是我們的官方區塊鏈,它只是一串經過驗證的區塊集合。每個區塊中的 PrevHash 與前面塊的 Hash 相比較,以確保我們的鏈是正確的。 tempBlocks 是臨時存儲單元,在區塊被選出來并添加到 BlockChain 之前,臨時存儲在這里
- candidateBlocks 是 Block 的通道,任何一個節點在提出一個新塊時都將它發送到這個通道
- announcements 也是一個通道,我們的主Go TCP服務器將向所有節點廣播最新的區塊鏈
- mutex是一個標準變量,允許我們控制讀/寫和防止數據競爭
- validators 是節點的存儲map,同時也會保存每個節點持有的令牌數
生成區塊
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 是用來創建新塊的。
newBlock.PrevHash 存儲的是上一個區塊的 Hash
newBlock.Hash 是通過 calculateBlockHash(newBlock) 生成的 Hash 。
newBlock.Validator 存儲的是獲取記賬權的節點地址
calculateHash 函數會接受一個 string ,并且返回一個SHA256 hash 。
calculateBlockHash 是對一個 block 進行 hash,將一個 block 的所有字段連接到一起后,再調用 calculateHash 將字符串轉為 SHA256 hash 。
驗證區塊
我們通過檢查 Index 來確保它們按預期遞增。我們也檢查以確保我們 PrevHash 的確與 Hash 前一個區塊相同。最后,我們希望通過在當前塊上 calculateBlockHash 再次運行該函數來檢查當前塊的散列。
// isBlockValid makes sure block is valid by checking index // and comparing the hash of the previous block func 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 }驗證者
當一個驗證者連接到我們的TCP服務,我們需要提供一些函數達到以下目標:
- 輸入令牌的余額(之前提到過,我們不做錢包等邏輯)
- 接收區塊鏈的最新廣播
- 接收驗證者贏得區塊的廣播信息
- 將自身節點添加到全局的驗證者列表中(validators)
- 輸入Block的BPM數據- BPM是每個驗證者的人體脈搏值
- 提議創建一個新的區塊
io.WriteString(conn, "Enter token balance:")允許驗證者輸入他持有的令牌數量,然后,該驗證者被分配一個 SHA256地址,隨后該驗證者地址和驗證者的令牌數被添加到驗證者列表validators 中。
接著我們輸入BPM,驗證者的脈搏值,并創建一個單獨的Go協程來處理這塊兒邏輯
delete(validators, address) 如果驗證者試圖提議一個被污染(例如偽造)的 block,例如包含一個不是整數的BPM,那么程序會拋出一個錯誤,我們會立即從我們的驗證器列表 validators 中刪除該驗證者,他們將不再有資格參與到新塊的鑄造過程同時丟失相應的抵押令牌。
正是因為這種抵押令牌的機制,使得PoS協議是一種更加可靠的機制。如果一個人試圖偽造和破壞,那么他將被抓住,并且失去所有抵押和未來的權益,因此對于惡意者來說,是非常大的威懾。
接著,我們用 generateBlock 函數創建一個新的 block,然后將其發送到 candidateBlocks 通道進行進一步處理。將Block 發送到通道使用的語法: candidateBlocks <- newBlock
最后會循環打印出最新的區塊鏈,這樣每個驗證者都能獲知最新的狀態。
選擇獲取記賬權的節點
下面是PoS的主要邏輯。我們需要編寫代碼以實現獲勝驗證者的選擇;他們所持有的令牌數量越高,他們就越有可能被選為勝利者。
// pickWinner creates a lottery pool of validators and chooses the validator who gets to forge a block to the blockchain // by random selecting from the pool, weighted by amount of tokens staked func pickWinner() {time.Sleep(30 * time.Second)mutex.Lock()temp := tempBlocksmutex.Unlock()lotteryPool := []string{}if len(temp) > 0 { // slightly modified traditional proof of stake algorithm // from all validators who submitted a block, weight them by the number of staked tokens // in traditional proof of stake, validators can participate without submitting a block to be forgedOUTER:for _, block := range temp { // if already in lottery pool, skipfor _, node := range lotteryPool {if block.Validator == node {continue OUTER}} // lock list of validators to prevent data racemutex.Lock()setValidators := validatorsmutex.Unlock() // 獲取驗證者的tokensk, ok := setValidators[block.Validator]if ok { // 向 lotteryPool 追加 k 條數據,k 代表的是當前驗證者的tokensfor i := 0; i < k; i++ {lotteryPool = append(lotteryPool, block.Validator)}}} // 通過隨機獲得獲勝節點的地址s := rand.NewSource(time.Now().Unix())r := rand.New(s)lotteryWinner := lotteryPool[r.Intn(len(lotteryPool))] // 把獲勝者的區塊添加到整條區塊鏈上,然后通知所有節點關于勝利者的消息for _, block := range temp {if block.Validator == lotteryWinner {mutex.Lock()Blockchain = append(Blockchain, block)mutex.Unlock()for _ = range validators {announcements <- "\nwinning validator: " + lotteryWinner + "\n"}break}}}mutex.Lock()tempBlocks = []Block{}mutex.Unlock() }每隔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,以便下次提議的進行。
主函數
func main() {err := godotenv.Load()if err != nil {log.Fatal(err)}// 創建初始區塊t := time.Now()genesisBlock := Block{}genesisBlock = Block{0, t.String(), 0, calculateBlockHash(genesisBlock), "", ""}spew.Dump(genesisBlock)Blockchain = append(Blockchain, genesisBlock)httpPort := os.Getenv("PORT")// 啟動 TCP 服務server, err := net.Listen("tcp", ":"+httpPort)if err != nil {log.Fatal(err)}log.Println("HTTP Server Listening on port :", httpPort)defer server.Close()// 啟動了一個Go routine 從 candidateBlocks 通道中獲取提議的區塊,然后填充到臨時緩沖區 tempBlocks 中go func() {for candidate := range candidateBlocks {mutex.Lock()tempBlocks = append(tempBlocks, candidate)mutex.Unlock()}}()// 啟動了一個Go routine 完成 pickWinner 函數go func() {for {pickWinner()}}()// 接收驗證者節點的連接for {conn, err := server.Accept()if err != nil {log.Fatal(err)}go handleConn(conn)} }- godotenv.Load() 會解析 .env 文件并將相應的Key/Value對都放到環境變量中,通過 os.Getenv 獲取
- 然后創建一個創世區塊genesisBlock,形成了區塊鏈。
- 接著啟動了Tcp服務,等待所有驗證者的連接。
- 啟動了一個Go協程從 candidateBlocks 通道中獲取提議的區塊,然后填充到臨時緩沖區 tempBlocks 中,最后啟動了另外一個Go協程來完成 pickWinner 函數。
- 最后的for循環,用來接收驗證者節點的連接。
運行
go run main.go 啟動您的Go程序和TCP服務器,并會打印出初始區塊的信息。
$ go run main.go (main.Block) {Index: (int) 0,Timestamp: (string) (len=50) "2018-05-08 16:45:27.14287 +0800 CST m=+0.000956793",BPM: (int) 0,Hash: (string) (len=64) "96a296d224f285c67bee93c30f8a309157f0daa35dc5b87e410b78630a09cfc7",PrevHash: (string) "",Validator: (string) "" } 2018/05/08 16:45:27 HTTP Server Listening on port : 9000打開新的終端,運行 nc localhost 9000,
輸入 tokens , 然后輸入 BPM
可以打開多個終端,輸入不同的 tokens ,來檢驗 PoS 算法
總結
以上是生活随笔為你收集整理的使用 go 实现 Proof of Stake 共识机制的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: md5加密算法原理及其GO语言实现
- 下一篇: 共识机制-权益证明 PoS