使用 go 实现 Proof of Stake 共识机制
什么是 Proof of Stake
在PoW中,节点之间通过hash的计算力来竞赛以获取下一个区块的记账权,而在PoS中,块是已经铸造好的,铸造的过程是基于每个节点(Node)愿意作为抵押的令牌(Token)数量。如果验证者愿意提供更多的令牌作为抵押品,他们就有更大的机会记账下一个区块并获得奖励。
实现 Proof of Stake 主要功能点
- 我们将有一个中心化的TCP服务节点,其他节点可以连接该服务器
- 最新的区块链状态将定期广播到每个节点
- 每个节点都能提议建立新的区块
- 基于每个节点的令牌数量,其中一个节点将随机地(以令牌数作为加权值)作为获胜者,并且将该区块添加到区块链中
实现 Proof of Stake
设置 TCP 服务器的端口
新建 .env.text
文件,添加如下内容 PORT=9000
安装依赖软件
$ go get github.com/davecgh/go-spew/spew
$ go get github.com/joho/godotenv
-
spew
在控制台中格式化输出相应的结果。 -
godotenv
可以从我们项目的根目录的.env
文件中读取数据。
引入相应的包
新建 main.go
,引入相应的包
package main
import (
"sync"
"crypto/sha256"
"encoding/hex"
"github.com/joho/godotenv"
"log"
"time"
"github.com/davecgh/go-spew/spew"
"os"
"net"
"io"
"encoding/json"
"bufio"
"strconv"
"fmt"
"math/rand"
)
//创建区块
type Block struct {
//区块高度
Index int
//时间戳
Timestamp string
//是每分钟跳动的次数,是你的脉率
BPM int
//当前区块hash
Hash string
//上一个区块hash
PrevHash string
//区块验证者
Validator string
}
//声明区块链数组
var Blockchain []Block
var tempBlocks []Block
//创建通道,确保线程间传参,candidateBlocks保存block
var candidateBlocks = make(chan Block)
//存放网络中广播的内容
var announcements = make(chan string)
//确保线程互斥
var mutex = &sync.Mutex{}
//创建map,存放节点的地址和tokens
var validators = make(map[string]int)
//随机生成代理验证人地址
func calculateHash(s string) string {
h := sha256.New()
h.Write([]byte(s))
hashed := h.Sum(nil)
return hex.EncodeToString(hashed)
}
//创建blockhash值
func calculateBlockHash(block Block) string {
record := string(block.Index) + block.Timestamp + string(block.BPM) +
block.PrevHash
return calculateHash(record)
}
//验证区块的合法性
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
}
func main() {
//读取.env文件内容
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)
//读取.env文件内容
httpPort := os.Getenv("PORT")
// 监听网络端口
server, err := net.Listen("tcp", ":"+httpPort)
//推迟关闭
defer server.Close()
// 启动了一个Go routine 从 candidateBlocks 通道中获取提议的区块,然后填充到临时缓冲区 tempBlocks 中
go func() {
for candidate := range candidateBlocks {
//添加线程所,确保线程互斥
mutex.Lock()
//当candidateblocks通道有数据时,就会添加临时缓冲区
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)
}
}
//输入令牌的余额(之前提到过,我们不做钱包等逻辑)
//接收区块链的最新广播
//接收验证者赢得区块的广播信息
//将自身节点添加到全局的验证者列表中(validators)
//输入Block的BPM数据- BPM是每个验证者的人体脉搏值
//提议创建一个新的区块
//模拟获取连接者的tokens(币)和地址和BPM(心跳值)
func handleConn(conn net.Conn) {
defer conn.Close()
//广播已经被验证的区块
go func() {
for {
msg := <-announcements
//在每个服务器控制台进行广播
io.WriteString(conn, msg)
}
}()
// 验证者地址
var address string
// 验证者输入他所拥有的 tokens,tokens 的值越大,越容易获得新区块的记账权
io.WriteString(conn, "Enter token balance:")
scanBalance := bufio.NewScanner(conn)
for scanBalance.Scan() {
// 获取输入的数据,并将输入的值转为 int
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())
// 将验证者的地址和token 存储到 validators
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())
// 如果验证者试图提议一个被污染(例如伪造)的block,例如包含一个不是整数的BPM,那么程序会抛出一个错误,我们会立即从我们的验证器列表validators中删除该验证者,他们将不再有资格参与到新块的铸造过程同时丢失相应的抵押令牌。
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()
// 创建新的区块,然后将其发送到 candidateBlocks 通道
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:")
}
}
}()
// 每个连入服务器的代理人会循环会周期性的打印出最新的区块链信息
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")
}
}
// pickWinner创建了一个验证者的彩票池并选择 可以为区块链伪造一个块的验证器
// 通过从池中随机选择,用标记的数量加权
func pickWinner() {
//保证有服务器连入使temp切片中有值
time.Sleep(30 * time.Second)
mutex.Lock()
temp := tempBlocks
mutex.Unlock()
lotteryPool := []string{}
if len(temp) > 0 {
// 稍微修改了传统的赌注算法
// 所有提交一个块的验证器, 通过标记标记的数量来增加它们的权重。
// 在传统的股权证明中, 确认器可以参与,而不需要提交一个块来伪造
OUTER:
for _, block := range temp {
//temp=tempBlocks=candidateBlocks
// 如果遍历中已经存在代理人的地址值跳过循环执行下一次遍历
for _, node := range lotteryPool {
if block.Validator == node {
continue OUTER
}
}
// 锁定验证器列表,以防止数据竞争
mutex.Lock()
setValidators := validators
mutex.Unlock()
//在以 k, ok := setValidators[block.Validator]开始的代码块中,
// 我们确保了从temp中取出来的验证者都是合法的,
// 即这些验证者在验证者列表validators已存在。若合法,则把该验证者加入到lotteryPool中。
// 获取验证者的tokens
k, ok := setValidators[block.Validator]
if ok {
// 向 lotteryPool 追加 k 条数据,k 代表的是当前验证者的tokens
for 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
}
}
}
//清空tempBlocks,以便下次提议的进行。
mutex.Lock()
tempBlocks = []Block{}
mutex.Unlock()
}
//区块验证者添加自己的区块
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
}