一个基于PoS共识算法的区块链实例解析(升级版)

举报
Regan Yue 发表于 2021/11/17 20:42:22 2021/11/17
【摘要】 本文首发于华为云博客一个基于PoS共识算法的区块链实例解析(升级版)一、前言前面我们简单的介绍了一个基于PoS共识算法的例子,今天我们来解析一个升级版的例子。如果喜欢博主的话,记得点赞,关注,收藏哦~二、本例中的一些数据结构type Block struct { Index int TimeStamp string BPM int HashCode...

本文首发于华为云博客

一个基于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.

image-20211117174759924

然后是创建创世区块,创世区块注意它的高度为0.

spew.Dump(genesisBlock)

就是把创世区块通过命令行格式化输出。

Blockchain = append(Blockchain, genesisBlock)

这行代码是将创世区块添加到区块链。

port := os.Getenv("PORT")

前面说.env文件中存储了端口号,这里就获取这个文件中的端口号到port变量中。

然后启动服务进程监听上面获取的端口。

defer server.Close()

要养成启动服务就书写延迟关闭的习惯,不然后面任意忘记释放资源。

然后是并发操作,循环读取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
}

先是延时释放连接资源。

defer conn.Close()

然后从管道中读取选出幸运儿的消息,并将其输出到连接conn。

然后在命令行窗口接收该节点的tokens数量。

然后根据当前时间生成验证者的地址。

address = calculateHash(time.Now().String())

再将验证者地址和他拥有的tokens存到validators中。

然后再根据提示输入交易信息。如果输入的交易信息非法,就将该节点删除。

delete(validators, address)
conn.Close()

之后的逻辑是取上一个区块,然后生成新的区块信息,然后简单的验证区块是否合法,合法的话就将区块放入candidateBlocks管道,等待抽取幸运儿。


此处验证区块是否合法的方法很简单,就是验证当前区块的高度是不是上一个模块加一,然后判断新区块的PrevHash是不是等于上一个区块的哈希值。然后再一次检验哈希值是否正确。

七、运行结果

image-20211117202628046

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。