一个基于 PoS 共识算法的区块链实例解析(升级版)
一、前言
前面我们简单的介绍了一个基于 PoS 共识算法的例子,今天我们来解析一个升级版的例子。如果喜欢博主的话,记得点赞,关注,收藏哦~
二、本例中的一些数据结构
type Block struct {
Index int
TimeStamp string
BPM int
HashCode string
PrevHash string
Validator string
}
var Blockchain []Block
var tempBlocks []Block
var candidateBlocks = make(chan Block)
var announcements = make(chan string)
var validators = make(map[string]int)
复制代码
首先是定义了一个区块结构体 Block,然后定义一条区块链 Blockchain,其实就是区块数组。这个 tempBlocks 是区块缓冲区。candidateBlocks 是候选区块,任何一个节点提议一个新块时,都会将它发送到这个管道。announcements 是来广播的通道。validators 是验证者列表,存节点地址和他拥有的 tokens。
三、生成区块和计算哈希
func generateBlock(oldBlock Block, BPM int, address string) Block {
var newBlock Block
newBlock.Index = oldBlock.Index + 1
newBlock.TimeStamp = time.Now().String()
newBlock.BPM = BPM
newBlock.PrevHash = oldBlock.HashCode
newBlock.Validator = address
newBlock.HashCode = GenerateHashValue(newBlock)
return newBlock
}
func GenerateHashValue(block Block) string {
var hashcode = block.PrevHash +
block.TimeStamp + block.Validator +
strconv.Itoa(block.BPM) + strconv.Itoa(block.Index)
return calculateHash(hashcode)
}
func calculateHash(s string) string {
var sha = sha256.New()
sha.Write([]byte(s))
hashed := sha.Sum(nil)
return hex.EncodeToString(hashed)
}
复制代码
这个真的前面每个例子都在讲,这里真的不想再讲了,不理解的小伙伴可以看一看本专栏前面的例子。
四、主逻辑
func main() {
err := godotenv.Load()
if err != nil {
log.Fatal(err)
}
genesisBlock := Block{}
genesisBlock = Block{0, time.Now().String(), 0,
GenerateHashValue(genesisBlock), "", ""}
spew.Dump(genesisBlock)
Blockchain = append(Blockchain, genesisBlock)
port := os.Getenv("PORT")
server, err := net.Listen("tcp", ":"+port)
if err != nil {
log.Fatal(err)
}
log.Println("HTTP Server Listening on port :", port)
defer server.Close()
go func() {
for cadidate := range candidateBlocks {
mutex.Lock()
tempBlocks = append(tempBlocks, cadidate)
mutex.Unlock()
}
}()
go func() {
for {
pickWinner()
}
}()
for {
conn, err := server.Accept()
if err != nil {
log.Fatal(err)
}
go handleConn(conn)
}
}
复制代码
我们先来看一看主逻辑,先是加载本地的.env 文件,这个文件可以存储很多参数,这里我们存储一个端口号 9000.
然后是创建创世区块,创世区块注意它的高度为 0.
就是把创世区块通过命令行格式化输出。
Blockchain = append(Blockchain, genesisBlock)
复制代码
这行代码是将创世区块添加到区块链。
port := os.Getenv("PORT")
复制代码
前面说.env 文件中存储了端口号,这里就获取这个文件中的端口号到 port 变量中。
然后启动服务进程监听上面获取的端口。
要养成启动服务就书写延迟关闭的习惯,不然后面任意忘记释放资源。
然后是并发操作,循环读取 candidateBlocks,一旦这个管道有一个区块进入,马上把它读取到缓冲区。接着并发判断哪个节点应该去挖矿。
然后不断接收验证者节点的连接,连上就处理终端发送过来的信息。
五、获取记账权的节点
func pickWinner() {
time.Sleep(30 * time.Second)
mutex.Lock()
temp := tempBlocks
mutex.Unlock()
lotteryPool := []string{}
if len(temp) > 0 {
OUTER:
for _, block := range temp {
for _, node := range lotteryPool {
if block.Validator == node {
continue OUTER
}
}
mutex.Lock()
setValidators := validators
mutex.Unlock()
k, ok := setValidators[block.Validator]
if ok {
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 <- "\nvalidator:" + lotteryWinner + "\n"
}
break
}
}
}
mutex.Lock()
tempBlocks = []Block{}
mutex.Unlock()
}
复制代码
这里就是 PoS 的精髓,根据代币 tokens 数量来确定拥有记账权的节点。
先是每次选出拥有记账权的节点就得休息 30 秒,不能一直不停的选吧。
每次选拥有记账权的节点之前,将缓冲区的区块拷贝一份部分,然后操作副本。
我们先声明一个彩票池来放置验证者地址。
然后判断缓冲区是否为空,如果缓冲区副本不为空,就遍历缓冲区副本,然后如果区块的验证者在彩票池就继续遍历,如果不在就执行后面的内容。
然后是获取一个验证者列表副本,获取上面不在彩票池中的验证者节点的 token 代币数量,然后向彩票池中添加和代币数量一样多的验证者地址字符串放入彩票池。
彩票池填充完毕后,就开始选幸运儿了。通过随机数来选取,然后将获胜者的区块加到区块链上面,再广播这个获胜者的区块消息。
如果临时缓冲区为空,我们就将让他等于一个空区块。
六、处理命令行的请求
func handleConn(conn net.Conn) {
defer conn.Close()
go func() {
for {
msg := <-announcements
io.WriteString(conn, msg)
}
}()
var address string
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
}
address = calculateHash(time.Now().String())
validators[address] = balance
fmt.Println(validators)
break
}
io.WriteString(conn, "\nEnter a new BPM:")
scanBPM := bufio.NewScanner(conn)
go func() {
for {
for scanBPM.Scan() {
bmp, err := strconv.Atoi(scanBPM.Text())
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()
newBlock := generateBlock(oldLastIndex, bmp, address)
if err != nil {
log.Println(err)
continue
}
if isBlockValid(newBlock, oldLastIndex) {
candidateBlocks <- newBlock
}
}
}
}()
for {
time.Sleep(time.Second * 20)
mutex.Lock()
output, err := json.Marshal(Blockchain)
mutex.Unlock()
if err != nil {
log.Fatal(err)
}
io.WriteString(conn, string(output)+"\n")
}
}
func isBlockValid(newBlock, oldBlock Block) bool {
if oldBlock.Index+1 != newBlock.Index {
return false
}
if oldBlock.HashCode != newBlock.PrevHash {
return false
}
if GenerateHashValue(newBlock) != newBlock.HashCode {
return false
}
return true
}
复制代码
先是延时释放连接资源。
然后从管道中读取选出幸运儿的消息,并将其输出到连接 conn。
然后在命令行窗口接收该节点的 tokens 数量。
然后根据当前时间生成验证者的地址。
address = calculateHash(time.Now().String())
复制代码
再将验证者地址和他拥有的 tokens 存到 validators 中。
然后再根据提示输入交易信息。如果输入的交易信息非法,就将该节点删除。
delete(validators, address)
conn.Close()
复制代码
之后的逻辑是取上一个区块,然后生成新的区块信息,然后简单的验证区块是否合法,合法的话就将区块放入 candidateBlocks 管道,等待抽取幸运儿。
此处验证区块是否合法的方法很简单,就是验证当前区块的高度是不是上一个模块加一,然后判断新区块的 PrevHash 是不是等于上一个区块的哈希值。然后再一次检验哈希值是否正确。
七、运行结果
评论