共识算法是区块链非常重要的一种算法,简单来说,共识算法就是为了使网络中的节点得到一致的认可。就比如说合作双方达成一致,共同遵守某个合约。

传统的分布式系统中,由于有着中心服务器进行统一管理,各个子服务器只需要跟着中心服务器进行操作即可。而区块链作为一种去中心化分布式系统,并没有传统意义上的中心服务器,因此传统的分布一致性算法并不适用于区块链网络。每个服务器都可以是中心服务器,那么这样就需要一种新的算法来维护系统中的一致性了。现在区块链中,主流的共识算法一般为三种:POW,POS和DPOS。

工作量证明(Proof Of Work)

工作量证明算法之前已经提到过,在实现微型区块链时用的就是pow算法,这是最传统最经典的共识算法,应用在比特币中。它的原理就是寻找一个以若干个0为开头的哈希串。举个例子,给定字符串“blockchain”,我们给出的工作量要求是,可以在这个字符串后面连接一个称为nonce的整数值串,对连接后的字符串进行sha256哈希运算,如果得到的哈希结果(以十六进制的形式表示)是以若干个0开头的,则验证通过。为了达到这个工作量证明的目标,我们需要不停地递增nonce值,对得到的新字符串进行sha256哈希运算。

原理非常的简单,也可以说是只需要算就行了。

这样也存在一个问题,那就是它只要求算力,不要求理解区块链的本身,也就造成了现如今比特币的大量矿机无脑挖矿。就像演唱会的黄牛,买票只为了获取它的利益,并不在意演唱会本身的内容。为了避免这种靠纯运算的共识机制,相继提出来很多新的共识算法。

权益证明(Proof Of Stake)

今天重点讲讲POS机制。

权益证明,它提出来币龄的概念,币龄 = 持有的货币数 * 持有时间,比如说我有100币,持有了30天,那么我的币龄就是3000。币龄越大的节点呢获取记账权(也就是生成区块)的概率越大。每次记完账之后,该节点的币龄就清空。当然,刚获取的币不能直接参与币龄计算,一般是30天之后开始计算币龄。

那么这样会产生一个问题:囤币来获取绝对记账权。为了防止这种情况发生,会设置一个最大概率,一般设置90天时获得最大记账概率,之后不再增加。

但是POS本质上还是类似POW的暴力计算,只不过币多的人更有可能挖到矿,降低了POW的全网一致难度,谁算力强谁记账的局限性。

具体来看代码怎么实现它的吧:


导入所需包:

package main

import (
"bytes"
"crypto/sha256"
"encoding/binary"
"fmt"
"math"
"math/big"
"math/rand"
"time"
)

定义常量设置最大最小概率,这里由于不可能等30天之后再计算币龄,我设置10秒之后开始计算,并且防止数据过大,按分钟计时:

const (
dif = 2
INT64_MAX = math.MaxInt64
MaxProbably = 255
MinProbably = 235
MaxCoinAge = 10
Minute = 60
)

定义币的数据结构和一个币池,这里币中提出了地址Address概念,一般理解为钱包地址,一般一个钱包属于一个用户,表明这个币的所有权是这个地址对应的用户:

type Coin struct {
Time int64
Num int
Address string
}

var CoinPool []Coin

定义区块和链:

type Block struct {
PrevHash []byte
Hash []byte
Data string
Height int64
Timestamp int64
Coin Coin
Nonce int
Dif int64
}

type BlockChain struct {
Blocks []Block
}

init函数,设置随机数种子和币池初始化,会在main函数开始前自动执行:

func init() {
rand.Seed(time.Now().UnixNano())
CoinPool = make([]Coin, 0)
}

生成创世块,传入的参数是区块上的数据data和挖矿地址addr,这里每个区块的币随机给出1到5,币池增加创世块的币:

func GenesisBlock(data string, addr string) *BlockChain {
var bc BlockChain
bc.Blocks = make([]Block, 1)
newCoin := Coin{
Time: time.Now().Unix(),
Num: 1 + rand.Intn(5),
Address: addr,
}
bc.Blocks[0] = Block{
PrevHash: []byte(""),
Data: data,
Height: 1,
Timestamp: time.Now().Unix(),
Coin: newCoin,
Nonce: 0,
}
bc.Blocks[0].Hash, bc.Blocks[0].Nonce, bc.Blocks[0].Dif = ProofOfStake(dif, addr, bc.Blocks[0])
CoinPool = append(CoinPool, newCoin)
return &bc
}

生成新区块函数,还是一样,prevHash对应上一个节点的Hash,随机给出1-5币奖励,记录该币的所有者地址addr,币池增加新区块的币:

func GenerateBlock(bc *BlockChain, data string, addr string) {
prevBlock := bc.Blocks[len(bc.Blocks)-1]
newCoin := Coin{
Time: time.Now().Unix(),
Num: 1 + rand.Intn(5),
Address: addr,
}
b := Block{
PrevHash: prevBlock.Hash,
Data: data,
Height: prevBlock.Height + 1,
Timestamp: time.Now().Unix(),
}
b.Hash, b.Nonce, b.Dif = ProofOfStake(dif, addr, b)
b.Coin = newCoin
bc.Blocks = append(bc.Blocks, b)
CoinPool = append(CoinPool, newCoin)
}

权益证明算法:

POS机制的核心算法了,设置的是10s之后开始计算币龄,根据币龄coinAge调整当前难度Dif以获取真实难度realDif。挖矿还是类似POW寻找一个nonce加在尾部,寻找到第一个满足真实难度realDif的sha256哈希串:

func IntToHex(num int64) []byte {
buff := new(bytes.Buffer)
err := binary.Write(buff, binary.BigEndian, num)
if err != nil {
panic(err)
}
return buff.Bytes()
}

func ProofOfStake(dif int, addr string, b Block) ([]byte, int, int64) {
var coinAge int64
var realDif int64
realDif = int64(MinProbably)
curTime := time.Now().Unix()

for k, i := range CoinPool {
if i.Address == addr && i.Time+MaxCoinAge < curTime {
//币龄增加, 并设置上限
var curCoinAge int64
if curTime-i.Time < 3*MaxCoinAge {
curCoinAge = curTime - i.Time
} else {
curCoinAge = 3 * MaxCoinAge
}
coinAge += int64(i.Num) * curCoinAge
//参与挖矿的币龄置为0
CoinPool[k].Time = curTime
}
}

if realDif+int64(dif)*coinAge/Minute > int64(MaxProbably) {
realDif = MaxProbably
} else {
realDif += int64(dif) * coinAge / Minute
}

target := big.NewInt(1)
target.Lsh(target, uint(realDif))
nonce := 0
for ; nonce < INT64_MAX; nonce++ {
check := bytes.Join(
[][]byte{
b.PrevHash,
[]byte(b.Data),
IntToHex(b.Height),
IntToHex(b.Timestamp),
IntToHex(int64(nonce)),
},
[]byte{})
hash := sha256.Sum256(check)
var hashInt big.Int
hashInt.SetBytes(hash[:])
if hashInt.Cmp(target) == -1 {
return hash[:], nonce, 255 - realDif
}
}

return []byte(""), -1, 255 - realDif
}

再写个打印区块和打印币池函数:

func Print(bc *BlockChain) {
for _, i := range bc.Blocks {
fmt.Printf("PrevHash: %x\n", i.PrevHash)
fmt.Printf("Hash: %x\n", i.Hash)
fmt.Println("Block's Data: ", i.Data)
fmt.Println("Current Height: ", i.Height)
fmt.Println("Timestamp: ", i.Timestamp)
fmt.Println("Nonce: ", i.Nonce)
fmt.Println("Dif: ", i.Dif)
}
}

func PrintCoinPool() {
for _, i := range CoinPool {
fmt.Println("Coin's Num: ", i.Num)
fmt.Println("Coin's Time: ", i.Time)
fmt.Println("Coin's Owner: ", i.Address)
}
}

写个main函数测试看看,虚拟两个地址,当然啊真实区块链的地址不可能这么简单,需要使用公钥生成地址算法。

给addr1先记账,获取一定币,等待可以计算币龄时再次记账,再给新地址addr2记账,看看难度对比:

func main() {
addr1 := "192.168.1.1"
addr2 := "192.168.1.2"
bc := GenesisBlock("reigns", addr1)
GenerateBlock(bc, "send 1$ to alice", addr1)
GenerateBlock(bc, "send 1$ to bob", addr1)
GenerateBlock(bc, "send 2$ to alice", addr1)
time.Sleep(11 * time.Second)
GenerateBlock(bc, "send 3$ to alice", addr1)
GenerateBlock(bc, "send 4$ to alice", addr2)
Print(bc)
PrintCoinPool()
}

看看打印结果吧:

这可能是全网最简单的POS共识机制算法_服务器

可以看到啊,addr1记前四笔账的时候难度都是20,第五笔的时候,由于之前延迟了11秒,开始计算币龄,难度减小到了15,第六笔账由addr2记账,难度又回到了20,同时看币池,参与币龄计算的币的Time统一到第5笔记账时间,也就是刷新了币龄。

当然啊,真实环境中肯定不是顺序记账,每个节点都会竞争记账权,最先计算出满足难度的hash串的节点优先记账,并向全网广播,开始下一次记账权争夺。

好了,今天到这里了~下次讲讲更有效率的共识算法,也是三大主流算法的最后一个:DPOS。

如果对区块链感兴趣,可以关注下面这个公众号哦,推送的全是区块链干货~

这可能是全网最简单的POS共识机制算法_unix_02