这篇文章的原文为英文,出自 Coral Health公司: Code your own Proof of Stake blockchain in Go!
如果对本教程有任何疑问,请加入我们的电报群Telegram
在上一篇文章中,我们讨论了PoW算法,并展示了如何编写Pow区块链的代码。最为流行的2个加密货币,比特币(Bitcoin)和Eth都是基于工作量证明算法的。
工作量证明(PoW)算法的缺点是什么呢?其中一个最主要的就是电力的消耗。为了获得挖比特币所需要的硬件力量,人们展开了一场规模越来越大的矿机竞赛。看看下图中这个疯狂的挖矿装置:
这消耗了大量的电力。比特币挖矿甚至超过了159个国家所消耗的能源。这是非常不负责任的;同时,从技术的角度来看,工作量证明也有其他的缺点。随着越来越多的人参与挖掘,一致性算法的难度需要增加,从而需要更多的散列运算能力。这意味着产生区块和交易需要更长的时间来处理,并需要更昂贵的挖矿。工作量证明是一个竞赛。
有许多思想领袖试图寻找工作量证明(PoW)算法的替代品。到目前为止,最有希望的是权益证明(PoS)。基于NXT和Neo的i权益证明,已经准备好了生产链。Ethereum也很可能是为了证明他们的Casper项目已经在他们的测试网络上存在了。
那么到底什么是权益证明(PoS)呢?
用基于每个节点愿意作为抵押物的Token(令牌)数量,而不是节点相互竞争来获得记账权益。在Proof Stake中,块是“minted”或“forged”(不存在“挖掘”,所以我们在Proof Stake中不使用该词)。在本教程中,我们将交替使用术语“节点”和“验证器”。令牌是特定于链链的。所以在Ethereum,每个节点(验证器)都会把以太作为抵押物。
每个验证者愿意作为抵押品提供的令牌越多,他们就越有机会创造下一个块并获得奖励。你可以认为这是存款利息。你可以认为这是存款利息。
类似地,你创造下一个块的概率增加了你作为抵押的令牌。你正在“下注”你的令牌,这就是为什么这种共识机制被称为利害关系的证据。
权益证明的缺点是什么呢?
您可能已经猜到,一个拥有大量标记的验证器将享有不成比例的高概率制造新的区块。然而,这与我们在工作证明中所看到的并不完全不同。比特币矿场变得如此强大,以至于普通人多年来无法在自己的笔记本电脑上开采。因此,许多人认为“赌注证明”实际上更加民主化,因为任何人至少都可以在自己的笔记本电脑上参与进来,而无需建立巨大的采矿平台。他们不需要昂贵的硬件,只需要足够的令牌就能获利。
从技术和经济的角度来看,权益证明有其他不利之处。在这里,我们就不继续深入了,但会有一个很好的介绍。在现实中,权益证明和工作量证明都有自己的优势,像Ethereum's Casper这样的项目融合了两者的特点。
像往常一样,来理解权益证明是如何工作的最好方法就是写自己的代码!
让我们来编码一个基于权益证明的区块链! 我们建议在开始之前,先查看下我们之前的文章。当然,这不是必须的,但在下面的教程的某些部分,我们将快速过一下,所以它将帮助您审查它。
事先声明
我们的blockchain将实现PoS的核心概念。然而,由于我们需要合理地设置文章长度,所以下面将省略PoS区块链的生产级元素。如果你想在未来看到这些,请务必在我们的Telegram中告诉我们!
完全对等实现。网络是模拟的,中央块链状态是由一个单一的GO TCP服务器保持。在本教程中,状态从单个服务器广播到每个节点。
钱包和余额追踪。我们没有在这个代码中实现一个钱包。节点在网络中被转出去,令牌数量从标准输入输出接口(STDIN)中输入。所以你可以输入任何你想要的数量。一个完整的实现将每个节点与散列地址相关联,并跟踪每个节点的令牌余额。
架构
我们将有一个基于GO的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
是一个很方便的包,将我们的区块链优雅的输出到终端。godotenv
让我们能够从前面创建的.env
文件中读取配置。
快速脉冲检测
如果你看过我们的其他教程,你会知道在这个阶段,我们将要介绍我们的脉冲。我们是一家医疗保健公司,所以当我们把数据添加到我们的区块时,不会选择像比特币一样无意义的内容。把两个手指放在手腕上,数一下你的脉搏数。这就是您的BPM
数,我们将在整个教程中使用。
全局变量
现在,我们声明一下将会使用到的所有全局变量。
// 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与前一个块的散列进行比较,以确保我们的链是健壮的。tempBlocks
仅仅是一个区块的存储箱,然后在其中一个被选为获胜者将被添加到Blockchain。candidateBlocks
是一个块的通道;每个节点提出一个新的块将它发送到这个通道。announcements
是一个通道,在这里,我们主要的TCP服务器向所有节点广播最新的BooStand链。mutex
是一个标准变量,允许我们控制读/写和防止数据竞争。validators
是节点的映射和它们所持有的令牌的数量。
基本区块链函数
在进行权益证明算法的证明之前,让我们写出我们的标准区块链函数函数。如果你看过我们以前的教程,应该复习一下。如果你没有,没关系,但是我们会很快完成这个任务。
// 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)
}
我们从散列函数开始。calculateHash
获取一个字符串并返回其Sh256哈希表示。calculateHash
通过连接所有字段来散列块的内容。
// generateBlock creates a new block using previous block's hash
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
是如何创建一个新的块。我们在每个新块中包括的重要字段是它的哈希签名(以前通过calculateBlockHash
计算)和前一个块PrevHash
的哈希(因此我们可以保持链的完整性)。我们还添加了一个Validator
字段,这样我们就知道了伪造块的获胜节点。
// 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
哈希链prevhash
确保我们没有被损坏。
节点(验证器)
当一个验证器连接到我们的TCP服务器时,我们需要为它提供一些实现一些功能的功能:
允许它输入令牌余额(请记住本教程,由于没有钱包逻辑,我们不执行任何余额检查)
接收最新的链链广播
接收网络中的验证程序获得最新块的广播。
将自己添加到验证器的整个列表中
输入块数据
BPM
-记住,这是每个验证器的脉冲率提出一个新的区块
我们将用手写功能把这些都写出来。在这里。别担心!我们会陪你度过难关。
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")
}
}
以io.WriteString(conn,“Enter token.:”)开头的部分允许验证器输入他希望投资的令牌数量。然后,验证器被分配一个SHA256地址,该地址被添加到我们之前声明的全局验证器映射中,以及我们新验证器的标记数量。
以io.WriteString(conn,“Enter token.:”)开始的部分允许验证器输入他希望投资的令牌数量。然后,验证器被分配一个SHA256地址,该地址被添加到我们之前声明的全局验证器映射中,以及我们新验证器的标记数量。
然后,我们进入BPM,这是验证器的脉冲率,并创建一个单独的GO协程来处理我们的块逻辑。下面的一行很重要:
delete(validators, address)
如果验证器试图提出一个受污染的块,在我们的例子中,一个不是整数的BPM,它抛出一个错误,我们立即从验证器列表中删除验证器。他们不再有资格创造新的区块并失去余额。
失去令牌余额的可能性是证明股权普遍安全的一个主要原因。如果你试图为你的利益改变区块,你会被抓住,你会失去所有的令牌余额股份。这是对坏演员的主要威慑。
然后,我们使用前面的generateBlock函数创建一个新块,并将其发送到候选Blocks通道以进行进一步处理。将数据发送到信道使用此语法:
candidateBlocks <- newBlock
The last for loop periodically prints the latest blockchain so each validator knows the latest state. 最后一个循环周期性地打印最新的块链,因此每个验证器都知道最新的状态。
Picking a Winner
选举胜出者
这就是权益证明的逻辑。我们需要写下如何选择获胜的验证器;它们所赌的令牌数量越高,它们被选择为伪造它们的块的获胜者的概率就越高。
为了简化我们的代码,我们只会让验证者提出有资格被选为获胜者的新块。在传统的权益证明中,验证者可以选择为赢家,即使他们不提出新的块。记住,权益证明不是一个定义,它是一个概念;权益证明有很多不同的实现,就像工作量证明,每个实现都有自己的细微差别。
这是我们的pickWinner函数。我们将会和你一起完成它:
// 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循环中,我们检查以确保我们在临时切片中没有遇到相同的验证器。如果我们这样做,跳过该块并寻找下一个唯一的验证器。
在以k, ok := setValidators[block.Validator]开头的小节中我们确保从临时块数据中获得的验证器实际上是位于验证器映射中的合格验证器。如果它们存在,我们将它们添加到我们的lotteryPool池中。
我们如何根据他们所持有的令牌的数量分配适当的权重?
我们把验证者地址的副本填在我们的lotteryPool上。他们为他们所签的每一个令牌得到一份拷贝。因此,放置100个令牌的验证者将在lotteryPool中获得100个条目。只放入1个令牌的验证器只能获得1个条目。
我们随机从我们的LoopyCype中挑选优胜者,并把他们的地址分配给lotteryWinner。
然后,我们将它们的块添加到我们的块链中,并使用这种语法向其余节点通知获胜者,该语法将消息发送到通知通道:
announcements <- "\nwinning validator: " + lotteryWinner + "\n"
我们清除了我们的防爆坦克,这样就可以用下一组建议的块再次填充。
这是权益证明一致性算法的核心!不算太差,不是吗?
即将完成
现在我们完成main函数,请看下面的代码:
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文件的内容开始,这只是我们为TCP服务器使用的端口号。然后,我们创建一个genesisBlock创世区块,新块将被添加到它之后,以形成我们的区块链。
我们启动TCP服务器,并将.env文件中的端口暴露到新的验证器可以连接的端口中。
我们开启一个Go协程,从candidateBlocks通道中取出区块,并将它们填充到tempBlocks保持箱中,以便通过我们刚刚编写的pickWinner函数进行进一步处理。然后我们为pickWinner函数启动另一个GO协程。
最后一个for循环接受来自新的验证器的连接。
耶!我们完成了!
你在这里查看完整代码:
mycoralhealth/blockchain-tutorial
What we just accomplished is pretty cool. We wrote up a robust Proof of Stake consensus algorithm from scratch and integrated it with actual TCP networking.
我们刚刚完成的事情很酷。我们从头开始写了一个健壮的权益证明一致性算法,并将其与实际的TCP网络集成。
有趣的事情
让我们试试看!打开一个终端窗口,启动GO程序和TCP服务器,运行go run main.go
。正如我们预期的那样,我们可以在控制台上打印创世区块(genesisBlock)。
现在让我们启动一个验证器。打开一个新的终端窗口并使用nc localhost 9000
连接到我们的TCP服务器。
然后,我们提示添加一个令牌余额到股权,输入您希望验证器共享的令牌数,然后输入那个验证器的脉冲率。
因为我们可以有很多验证器,让我们用另一个终端窗口做同样的事情。
当你添加新的终端时,注意你的第一个终端。我们看到验证器得到指定的地址,并且每次添加新的验证码时,我们都会收到一个验证器列表。
稍等一下,看看你的新终端。正在发生的是我们的项目花了一些时间来挑选一个胜利者。然后繁荣!获胜者被选中!
在我们的例子中,选择了第一个验证器(我们可以通过将验证器的地址与主终端中打印的验证器列表进行比较来验证它)。
再等一会儿,咚!我们看到了我们的新的块链广播到所有的终端,我们的获胜验证器的块包含他的BPM在最新的块中!酷,对吧?
下一步
你应该为通过本教程而感到自豪。大多数区块链爱好者和许多程序员都听说过权益证明,但却无法清楚的解释它是什么。你已经走得更远,实际上从零开始建立了一个证明链链的证据!你更接近下一代区块链技术的专家。
因为这是一个教程,我们可以做更多的事情来制作一个能用于生产环境的区块链。接下来要探索的是:
阅读我们的工作证明教程并修补它,看看是否可以创建混合链链。
添加时间块,其中验证器有机会提出新的块。我们的代码版本允许验证器随时提出新的块,所以一些块可以周期性地从考虑中被切断。
添加完整的对等能力。这基本上意味着每个验证器将运行自己的TCP服务器以及连接到其他服务器。我们需要添加逻辑,以便每个节点可以找到彼此。在这里阅读更多。
也看看我们的其他教程
希望你能喜欢这些教程。像往常一样,请务必加入我们的电报群(Telegram)!它是咨询问题并得到技术支持的最佳方式。当然,它是免费的!我们也会在那里提供更热门的教程。
要了解更多关于Coral Health的信息,以及我们如何使用这个区块链来推进个性化医学研究,请访问我们的网站并在Twitter上关注我们。