(譯)使用Go語言從零編寫PoS區塊鏈

原文發表日期: 2018-03-26
原文鏈接:https://medium.com/@mycoralhealth/code-your-own-proof-of-stake-blockchain-in-go-610cd99aa658

轉載請在文章開頭註明作者和出處
作者: ChainGod(孫飛)
原文鏈接: http://chaingod.io/article/16

PoS簡介

上一篇文章中,我們討論了工作量證明(Proof of Work),並向您展示瞭如何編寫自己的工作量證明區塊鏈。當前最流行的兩個區塊鏈平臺,比特幣和Ethereum都是基於工作量證明的。

但是工作證明的缺點是什麼呢?其中一個主要的問題是電力能源的消耗。爲了挖掘更多的比特幣,就需要建立更多的挖礦硬件池,現在在世界各地,挖礦池都在不斷建立中,而且呈現出規模越來越大的趨勢。例如以下這張照片(僅僅是礦池的一角):
挖礦場的一角.png

挖礦工作需要耗費大量的電力,僅比特幣開採耗費的能源就超過了159個國家的電力能源消耗總和!!這種能源消耗是非常非常不合理的,而且,從技術的角度來看,工作量證明還有其他不足之處:隨着越來越多的人蔘與到挖礦工作中,共識算法的難度就需要提高,難度的提高意味着需要更多、更長時間的挖礦,也意味着區塊和交易需要更長的時間才能得到處理,因此能源的消耗就會越發的高。總之,工作量證明的方式就是一場競賽,你需要更多的計算能力纔能有更大的概率贏得比賽。

有很多區塊鏈學者都試圖找到工作量證明的替代品,到目前爲止最有希望的就是PoS(權益證明或者股權證明,Proof of Stake)。目前在生產環境,已經有數個區塊鏈平臺使用了PoS,例如NxtNeo。以太坊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地址,並在節點中跟蹤餘額的變動
架構圖

架構.png

  • 我們將有一箇中心化的TCP服務節點,其他節點可以連接該服務器
  • 最新的區塊鏈狀態將定期廣播到每個節點
  • 每個節點都能提議建立新的區塊
  • 基於每個節點的令牌數量,其中一個節點將隨機地(以令牌數作爲加權值)作爲獲勝者,並且將該區塊添加到區塊鏈中
設置和導入

在開始寫代碼之前,我們需要一個環境變量來設置TCP服務器的端口,首先在工作文件夾中創建.env文件,寫入一行配置:
ADDR=9000
我們的Go程序將讀取該文件,並且暴露出9000端口。同時在工作目錄下,再創建一個main.go文件。

package main

import (
    "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"
)
  • spew 可以把我們的區塊鏈用漂亮的格式打印到終端terminal中
  • godotenv 允許我們從之前創建的.evn文件讀取配置
快速脈搏檢查

如果你讀過我們的其他教程,就會知道我們是一家醫療保健公司,目前要去收集人體脈搏信息,同時添加到我們的區塊上。把兩個手指放在你的手腕上,數一下你一分鐘能感覺到多少次脈搏,這將是您的BPM整數,我們將在接下來的文章中使用。

全局變量

現在,讓我們聲明我們需要的所有全局變量(main.go中)。

// Block represents each 'item' in the blockchain
type Block struct {
    Index     int
    Timestamp string
    BPM       int
    Hash      string
    PrevHash  string
    Validator 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,同時也會保存每個節點持有的令牌數
基本的區塊鏈函數

在繼續PoS算法之前,我們先來實現標準的區塊鏈函數。如果你之前看過200行Go代碼編寫區塊鏈,那接下來應該更加熟悉。
main.go

// SHA256 hasing
// calculateHash is a simple SHA256 hashing function
func calculateHash(s string) string {
    h := sha256.New()
    h.Write([]byte(s))
    hashed := h.Sum(nil)
    return hex.EncodeToString(hashed)
}

//calculateBlockHash returns the hash of all block information
func calculateBlockHash(block Block) string {
    record := string(block.Index) + block.Timestamp + string(block.BPM) + block.PrevHash
    return calculateHash(record)
}

這裏先從hash函數開始,calculateHash函數會接受一個string,並且返回一個SHA256 hash。calculateBlockHash是對一個block進行hash,將一個block的所有字段連接到一起後,再進行hash。
main.go

func generateBlock(oldBlock Block, BPM int, address string) (Block, error) {

    var newBlock Block

    t := time.Now()

    newBlock.Index = oldBlock.Index + 1
    newBlock.Timestamp = t.String()
    newBlock.BPM = BPM
    newBlock.PrevHash = oldBlock.Hash
    newBlock.Hash = calculateBlockHash(newBlock)
    newBlock.Validator = address

    return 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 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
}

isBlockValid會驗證Block的當前hash和PrevHash,來確保我們的區塊鏈不會被污染。

節點(驗證者)

當一個驗證者連接到我們的TCP服務,我們需要提供一些函數達到以下目標:
- 輸入令牌的餘額(之前提到過,我們不做錢包等邏輯)
- 接收區塊鏈的最新廣播
- 接收驗證者贏得區塊的廣播信息
- 將自身節點添加到全局的驗證者列表中(validators)
- 輸入Block的BPM數據- BPM是每個驗證者的人體脈搏值
- 提議創建一個新的區塊

這些目標,我們用handleConn函數來實現
main.go

func handleConn(conn net.Conn) {
    defer conn.Close()

    go func() {
        for {
            msg := <-announcements
            io.WriteString(conn, msg)
        }
    }()
    // validator address
    var address string

    // allow user to allocate number of tokens to stake
    // the greater the number of tokens, the greater chance to forging a new block
    io.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] = balance
        fmt.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 validation
            for 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 tokens
                if 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 forged
                newBlock, 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 broadcast
    for {
        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

// 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 := tempBlocks
    mutex.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 forged
    OUTER:
        for _, block := range temp {
            // if already in lottery pool, skip
            for _, node := range lotteryPool {
                if block.Validator == node {
                    continue OUTER
                }
            }

            // lock list of validators to prevent data race
            mutex.Lock()
            setValidators := validators
            mutex.Unlock()

            k, ok := setValidators[block.Validator]
            if ok {
                for i := 0; i < k; i++ {
                    lotteryPool = append(lotteryPool, block.Validator)
                }
            }
        }

        // randomly pick winner from lottery pool
        s := rand.NewSource(time.Now().Unix())
        r := rand.New(s)
        lotteryWinner := lotteryPool[r.Intn(len(lotteryPool))]

        // add block of winner to blockchain and let all the other nodes know
        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,以便下次提議的進行。

以上便是PoS一致性算法的核心內容,該算法簡單、明瞭、公正,所以很酷!

收尾

下面我們把之前的內容通過代碼都串聯起來
main.go

func main() {
    err := godotenv.Load()
    if err != nil {
        log.Fatal(err)
    }

    // create genesis block
    t := time.Now()
    genesisBlock := Block{}
    genesisBlock = Block{0, t.String(), 0, calculateBlockHash(genesisBlock), "", ""}
    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()

    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

歡迎大家加入區塊鏈研習社,QQ羣號21911041,網址:http://bclearn.org

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章