Go语言实现PoS共识算法
1. 实现简单的PoS共识算法
创建名为PoSdemo
的文件夹,与PoW共识算法类似的,都应该包含区块结构,链表结构,在PoSdemo
文件夹下创建名为Block
的文件夹,和名为BlockChain
的文件夹。在Blcok
文件夹下创建blcok.go
的go文件。
block.go
文件内容如下:
1 | package Block |
在BlockChain
文件夹下创建名为blockChain.go
的go文件,文件内容如下:
引入头文件:
1 | package BlockChain |
生成区块,计算哈希值:
1 | //创建区块链 |
根据PoS算法描述,在PoSdemo下创建main.go
文件,内容如下:
1 | package main |
运行打印输出
1 | [{100 0 d9b803bdc327fbdaf7afc48c27b96214e4073096b93038793024eb13ad421447 2018-05-23 14:01:52.502191002 +0800 CST m=+0.000412870 1 abc123} {200 d9b803bdc327fbdaf7afc48c27b96214e4073096b93038793024eb13ad421447 1c67daf321867feda102348aa512102f220ee4026f529567ea3fe967484a4cf9 2018-05-23 14:01:52.50227681 +0800 CST m=+0.000498680 2 bcd234}] |
有打印信息看出第二个新产生的区块是由地址为“bcd234”挖出的
2. 结合http服务实现PoS算法
设置 TCP 服务器的端口
新建 .env
,添加如下内容 PORT=9000
安装依赖软件
1 | $ go get github.com/davecgh/go-spew/spew |
spew
在控制台中格式化输出相应的结果。godotenv
可以从我们项目的根目录的.env
文件中读取数据。
引入相应的包
新建 main.go
,引入相应的包
1 | package main |
全局变量
1 | // Block represents each 'item' in the blockchain |
Block
是每个区块的内容Blockchain
是我们的官方区块链,它只是一串经过验证的区块集合。每个区块中的PrevHash
与前面块的Hash
相比较,以确保我们的链是正确的。tempBlocks
是临时存储单元,在区块被选出来并添加到BlockChain
之前,临时存储在这里candidateBlocks
是Block
的通道,任何一个节点在提出一个新块时都将它发送到这个通道announcements
也是一个通道,我们的主Go TCP服务器将向所有节点广播最新的区块链mutex
是一个标准变量,允许我们控制读/写和防止数据竞争validators
是节点的存储map,同时也会保存每个节点持有的令牌数(持币数)
生成区块
1 | func generateBlock(oldBlock Block, BPM int, address string) (Block, error) { |
generateBlock
是用来创建新块的。newBlock.PrevHash
存储的是上一个区块的 Hash
newBlock.Hash
是通过 calculateBlockHash(newBlock)
生成的 Hash 。newBlock.Validator
存储的是获取记账权的节点地址
1 | // SHA256 hasing |
calculateHash
函数会接受一个 string
,并且返回一个SHA256 hash
。
calculateBlockHash
是对一个 block
进行 hash
,将一个 block
的所有字段连接到一起后,再调用 calculateHash
将字符串转为 SHA256 hash
。
验证区块
我们通过检查 Index
来确保它们按预期递增。我们也检查以确保我们 PrevHash
的确与 Hash
前一个区块相同。最后,我们希望通过在当前块上 calculateBlockHash
再次运行该函数来检查当前块的散列。
1 | // isBlockValid makes sure block is valid by checking index |
验证者
当一个验证者连接到我们的TCP服务,我们需要提供一些函数达到以下目标:
- 输入令牌的余额(之前提到过,我们不做钱包等逻辑)
- 接收区块链的最新广播
- 接收验证者赢得区块的广播信息
- 将自身节点添加到全局的验证者列表中(validators)
- 输入Block的BPM数据- BPM是每个验证者的人体脉搏值
- 提议创建一个新的区块
1 | func handleConn(conn net.Conn) { |
io.WriteString(conn, "Enter token balance:")
允许验证者输入他持有的令牌数量,然后,该验证者被分配一个SHA256
地址,随后该验证者地址和验证者的令牌数被添加到验证者列表validators
中。接着我们输入BPM,验证者的脉搏值,并创建一个单独的Go协程来处理这块儿逻辑
delete(validators, address)
如果验证者试图提议一个被污染(例如伪造)的block
,例如包含一个不是整数的BPM,那么程序会抛出一个错误,我们会立即从我们的验证器列表validators
中删除该验证者,他们将不再有资格参与到新块的铸造过程同时丢失相应的抵押令牌。正是因为这种抵押令牌的机制,使得PoS协议是一种更加可靠的机制。如果一个人试图伪造和破坏,那么他将被抓住,并且失去所有抵押和未来的权益,因此对于恶意者来说,是非常大的威慑。
接着,我们用
generateBlock
函数创建一个新的block
,然后将其发送到candidateBlocks
通道进行进一步处理。将Block
发送到通道使用的语法:candidateBlocks <- newBlock
最后会循环打印出最新的区块链,这样每个验证者都能获知最新的状态。
选择获取记账权的节点
下面是PoS的主要逻辑。我们需要编写代码以实现获胜验证者的选择;他们所持有的令牌数量越高,他们就越有可能被选为胜利者。
1 | // pickWinner creates a lottery pool of validators and chooses the validator who gets to forge a block to the blockchain |
每隔30秒,我们选出一个胜利者,这样对于每个验证者来说,都有时间提议新的区块,参与到竞争中来。接着创建一个
lotteryPool
,它会持有所有验证者的地址,这些验证者都有机会成为一个胜利者。然后,对于提议块的暂存区域,我们会通过if len(temp) > 0
来判断是否已经有了被提议的区块。在
OUTER FOR
循环中,要检查暂存区域是否和lotteryPool
中存在同样的验证者,如果存在,则跳过。在以
k, ok := setValidators[block.Validator]
开始的代码块中,我们确保了从temp
中取出来的验证者都是合法的,即这些验证者在验证者列表validators
已存在。若合法,则把该验证者加入到lotteryPool
中。那么我们怎么根据这些验证者持有的令牌数来给予他们合适的随机权重呢?
首先,用验证者的令牌填充
lotteryPool
数组,例如一个验证者有100个令牌,那么在lotteryPool
中就将有100个元素填充;如果有1个令牌,那么将仅填充1个元素。然后,从
lotteryPool
中随机选择一个元素,元素所属的验证者即是胜利者,把胜利验证者的地址赋值给lotteryWinner。这里能够看出来,如果验证者持有的令牌越多,那么他在数组中的元素也越多,他获胜的概率就越大;同时,持有令牌很少的验证者,也是有概率获胜的。
接着我们把获胜者的区块添加到整条区块链上,然后通知所有节点关于胜利者的消息:
announcements <- "\nwinning validator: " + lotteryWinner + "\n"
。最后,清空tempBlocks,以便下次提议的进行。
主函数
1 | func main() { |
godotenv.Load()
会解析.env
文件并将相应的Key/Value对都放到环境变量中,通过os.Getenv
获取- 然后创建一个创世区块genesisBlock,形成了区块链。
- 接着启动了Tcp服务,等待所有验证者的连接。
- 启动了一个Go协程从
candidateBlocks
通道中获取提议的区块,然后填充到临时缓冲区tempBlocks
中,最后启动了另外一个Go协程来完成pickWinner
函数。 - 最后的for循环,用来接收验证者节点的连接。
运行
打开终端进入到项目文件中,输入如下指令:go run main.go
启动您的Go程序和TCP服务器,并会打印出初始区块的信息。
1 | $ go run main.go |
打开新的终端,运行 nc localhost 9000
,
输入 tokens
, 然后输入 BPM
可以打开多个终端,输入不同的 tokens
,来检验 PoS 算法