技术详解 | 系列一:Go语言实现Pow共识算法

栏目: Go · 发布时间: 6年前

内容简介:PoW呢...Proof of Work ,工作量证明机制,可能这个名字大家不熟悉,说比特币的话,大家就熟悉了。没错,PoW就是比特币所使用的共识机制。通过计算一个数值( nonce ),使得拼揍上交易数据后内容的 Hash 值满足规定的上限。在节点成功找到满足的Hash值之后,会马上对全网进行广播打包区块,网络的节点收到广播打包区块,会立刻对其进行验证。

技术详解 | 系列一:Go语言实现Pow共识算法

PoW呢...Proof of Work ,工作量证明机制,可能这个名字大家不熟悉,说比特币的话,大家就熟悉了。没错,PoW就是比特币所使用的共识机制。

通过计算一个数值( nonce ),使得拼揍上交易数据后内容的 Hash 值满足规定的上限。在节点成功找到满足的Hash值之后,会马上对全网进行广播打包区块,网络的节点收到广播打包区块,会立刻对其进行验证。

如果验证通过,则表明已经有节点成功解迷,自己就不再竞争当前区块打包,而是选择接受这个区块,记录到自己的账本中,然后进行下一个区块的竞争猜谜。网络中只有最快解谜的区块,才会添加的账本中,其他的节点进行复制,这样就保证了整个账本的唯一性。

假如节点有任何的作弊行为,都会导致网络的节点验证不通过,直接丢弃其打包的区块,这个区块就无法记录到总账本中,作弊的节点耗费的成本就白费了,因此在巨大的挖矿成本下,也使得矿工自觉自愿的遵守比特币系统的共识协议,也就确保了整个系统的安全。

举个例子,给定的一个基本的字符串”Hello, world!”,我们给出的工作量要求是,可以在这个字符串后面添加一个叫做nonce的整数值,对变更后(添加nonce)的字符串进行SHA256哈希运算,如果得到的哈希结果(以16进制的形式表示)是以”0000”开头的,则验证通过。为了达到这个工作量证明的目标。我们需要不停的递增nonce值,对得到的新字符串进行SHA256哈希运算。按照这个规则,我们需要经过4251次计算才能找到恰好前4位为0的哈希散列。

如图所示

技术详解 | 系列一:Go语言实现Pow共识算法

然后我们今天呢,就用 Go 语言来实现它。

话不多说,首先从环境搭建教起,对,没错, Golanghttps://www.golangtc.com/download )。

技术详解 | 系列一:Go语言实现Pow共识算法

选择你需要的,笔者话,选择的是 go1.9.2windows-amd64.msi ,不用配置环境,这里展示一下

技术详解 | 系列一:Go语言实现Pow共识算法

这样的话就妥了,然后呢,下载git, githttps://git-scm.com/downloads )。

下载后打开,安装我们本次所需要的三个第三方库,spew,gorilla/mux,godotenv

spew 在控制台中格式化输出相应的结果。

gorilla/mux 是编写web处理程序的流行软件包。

godotenv 可以从我们项目的根目录的 .env 文件中读取数据。

事实上如果你打算做公链开发相关,这三个依赖是一直都要用的

在git里分别输入

$ go get github.com/davecgh/go-spew/spew

$ go get github.com/gorilla/mux

$ go get github.com/joho/godotenv

友情提示这里会遇到一个很尴尬的问题, bash: $'\302\226go‘:   command notfound ,这个错误是这样的

只是因为你多加了一个空格在前面,删掉空格就好了,亲测有效

技术详解 | 系列一:Go语言实现Pow共识算法

好了我们继续,安装后环境之后呢

新建一个文件,叫.env,里面输入ADDR=8080,这是调用8080端口的意思,win环境下的话,会提示你必须键入文件名,所以我们将文件名修改成.env.   就可以创建.env这种.开头的文件了。

再建一个文件叫,mian.go。这就是咱们的“源码”了,打开它开始编程。

首先是引入相应的包 ,来咱们逐个解释一下

package main  /*定义报名,package main表示一个可独立执行的程序,每个 Go 应用程序都包含一个名为 main 的包 */

import( //导入包(的函数或者其他元素)

"crypto/sha256" /* 软件包sha256 实现FIPS 180-4 中定义的 SHA224 和 SHA256哈希算法。*/

"encoding/hex" //包十六进制实现十六进制编码和解码。

"encoding/json" /* 包json实现了RFC 4627中定义的JSON的编码和解码。JSON和Go值之间的映射在Marshal和 Unmarshal函数的文档中进行了描述。有关此包的介绍,请参阅“JSON和Go”: https://golang.org/doc/articles/json_and_go.html */

"fmt"  /* fmt 包使用函数实现 I/O 格式化(类似于 C 的 printf 和 scanf 的函数), 格式化参数源自C,但更简单 */

"io" /* Package io 为 I/O 原语提供基本接口。它的主要工作是将这些原语的现有实现(例如包 os 中的那些原语)包装到抽象功能的共享公共接口中,以及一些其他相关原语中 */

"log" /* Log包实现了一个简单的日志包。它定义了一个类型,记录器,用于格式化输出的方法。它还有一个预定义的“standard”记录器,可以通过帮助函数 Printf|ln,Fatalf|ln 和 Panicf|ln 访问,比手动创建记录器更易于使用。该记录器写入标准错误并打印每条记录的消息的日期和时间。每条日志消息都在一个单独的行上输出:如果正在打印的消息不以换行符结尾,则记录器将添加一条。写入日志消息后,致命函数调用 os.Exit(1) 。写入日志消息后, Panic 函数调用 panic */

"net/http" //http包提供HTTP客户端和服务器实现

"os" /* Package os为操作系统功能提供了一个平台无关的接口。虽然错误处理类似于 Go,但设计类似 Unix,失败的调用返回类型错误的值而不是错误号  */

"strconv" //包strconv实现了对基本数据类型的字符串表示的转换

"strings" //打包字符串实现简单的函数来操纵 UTF-8 编码的字符串

"sync" /* 程序包 sync 提供基本的同步原语,如互斥锁。除了 Once 和 WaitGroup 类型之外,大多数类型都是供低级库例程使用的。通过 Channel 和沟通可以更好地完成更高级别的同步 */

"time" //打包时间提供了测量和显示时间的功能。日历计算总是假定公历,没有闰秒

"github.com/davecgh/go-spew/spew" //这三就是刚才那三个倒霉孩子

"github.com/gorilla/mux"

"github.com/joho/godotenv"

)

定义一下区块中有的

const difficulty = 1 /*difficulty 代表难度系数,如果赋值为 1,则需要判断生成区块时所产生的Hash 前缀至少包含1个 0 */

type Block struct //Block 是我们定义的结构体,它代表组成区块链的每一个块的数据模型

{

Index int //区块链中数据记录的位置

Timestamp string //时间戳,是自动确定的,并且是写入数据的时间

Bike int /*假定我们现在做的是一个共享单车的区块链,Bike就是一定区域内的自行车数量*/

Hash string //是代表这个数据记录的SHA256标识符

 PrevHash string //是链中上一条记录的SHA256标识符

 Difficulty int //挖矿的难度

  Nonce string //PoW中符合条件的数字

}

var Blockchain []Block  // 存放区块数据

type Message struct{ // 定义结构体,请求的数据

Bike int

}

var mutex = &sync.Mutex{} //用sync防止同一时间产生多个区块

定义完成之后,就是该生成区块了

func generateBlock(oldBlock Block, Bike int)Block {  //定义函数

var newBlock Block //新区块

t := time.Now()

newBlock.Index = oldBlock.Index + 1 //区块的增加,index也加一

 newBlock.Timestamp = t.String() //时间戳

newBlock.Bike = Bike 

newBlock.PrevHash = oldBlock.Hash  //新区块的PrevHash存储上一个区块的Hash

newBlock.Difficulty = difficulty

for i := 0; ; i++ { //通过循环改变 Nonce

hex := fmt.Sprintf("%x", i)

newBlock.Nonce= hex //选出符合难度系数的Nonce

if !isHashValid(calculateHash(newBlock),newBlock.Difficulty) {  

//判断Hash的0的个数,是否与难度系数一致

fmt.Println(calculateHash(newBlock), " do morework!") //挖矿中

time.Sleep(time.Second)

continue

} else {

 fmt.Println(calculateHash(newBlock), " workdone!") //挖矿成功

newBlock.Hash = calculateHash(newBlock)

break

}

}

return newBlock

}

接着定义之前提到的isHashValid函数,这个函数的作用是判断Hash的0的个数,是否与难度系数一致

func isHashValid(hash string, difficulty int) bool {

prefix := strings.Repeat("0",difficulty) //复制 difficulty 个0,并返回新字符串,当 difficulty 为 4 ,则 prefix 为 0000

return strings.HasPrefix(hash, prefix ) // 判断字符串 hash 是否包含前缀 prefix

}

接着,我们要开始生成Hash值

func calculateHash(block Block) string {

record:=strconv.Itoa(block.Index)+block.Timestamp+strconv.Itoa(block.BPM)+block.PrevHash+ block.Nonce

h := sha256.New()

h.Write([]byte(record))

hashed := h.Sum(nil)

return hex.EncodeToString(hashed)

}

Hash值完成之后,我们就要来验证区块了,定义一个isBlockValid函数

func isBlockValid(newBlock, oldBlock Block) bool {

ifoldBlock.Index+1 != newBlock.Index { 

return false //确认Index的增长正确 

 } //双重否定(笑)

if oldBlock.Hash != newBlock.PrevHash {

return false //确认PrevHash与前一个块的Hash相同   

}

if calculateHash(newBlock) != newBlock.Hash

//在当前块上 calculateHash 再次运行该函数来检查当前块的Hash

return false

}

return true

ok,区块的定义到此就告一段落,接下来我们需要定义web服务器方面

func run() error { //run函数作为启动http服务器的函数

mux := makeMuxRouter() 

//makeMuxRouter 主要定义路由处理

httpAddr := os.Getenv("ADDR") //.env

log.Println("Listening on ",os.Getenv("ADDR"))

s := &http.Server{

Addr: ":" +httpAddr,  

Handler: mux,

ReadTimeout: 10 * time.Second,

WriteTimeout: 10 * time.Second,

MaxHeaderBytes: 1<<20,

}

if err := s.ListenAndServe(); err != nil {

return err

}

return nil

}

func makeMuxRouter() http.Handler {

muxRouter :=mux.NewRouter()

muxRouter.HandleFunc("/",handleGetBlockchain).Methods("GET")

//当收到GET请求,调用handleGetBlockchain函数

muxRouter.HandleFunc("/",handleWriteBlock).Methods("POST")

//当收到POST请求,调用handleWriteBlock函数

  return muxRouter

}

接下来,就是需要在服务器这边,遍历,就是获取所有区块的列表信息,主要作用当然还是处理HTTP的GET请求

func handleGetBlockchain(w http.ResponseWriter, r*http.Request) {

//处理HTTP的GET请求

bytes, err := json.MarshalIndent(Blockchain,"", " ")

if err != nil {

http.Error(w, err.Error(), http.StatusInternalServerError) return

}

io.WriteString(w, string(bytes))

}

handleWriteBlock 主要是生成新的区块,以及处理HTTP的GET请求

func handleWriteBlock(w http.ResponseWriter, r*http.Request) {

w.Header().Set("Content-Type","application/json")

var m Message //当服务器错误,返回相应信息

decoder :=json.NewDecoder(r.Body)

i f err := decoder.Decode(&m); err != nil {

respondWithJSON(w, r, http.StatusBadRequest, r.Body)

return

}

defer r.Body.Close()  

 mutex.Lock() //产生区块

newBlock :=generateBlock(Blockchain[len(Blockchain)-1],m.BPM)

mutex.Unlock() //判断区块的合法性

ifisBlockValid(newBlock, Blockchain[len(Blockchain)-1]) {

//通过数组维护区块链

Blockchain =append(Blockchain, newBlock)

spew.Dump(Blockchain)

}

respondWithJSON(w, r, http.StatusCreated, newBlock)

}

错误部分

func respondWithJSON(w http.ResponseWriter, r*http.Request, code int, payload interface{}) {

w.Header().Set("Content-Type", "application/json")

response, err := json.MarshalIndent(payload,"", " ") 

if err != nil {

w.WriteHeader(http.StatusInternalServerError)

 w.Write([]byte("HTTP500: Internal Server Error")) return /*如果出错,返回服务器500错误*/

}

w.WriteHeader(code)

w.Write(response)

}

然后是主函数

func main() {

err := godotenv.Load()  //允许我们读取.env

if err != nil {

log.Fatal(err)

}

go func() {

t := time.Now()

genesisBlock := Block{} //此乃创世区块

genesisBlock= Block{0, t.String(), 0, calculateHash(genesisBlock), "",difficulty, ""}

spew.Dump(genesisBlock)

mutex.Lock()

Blockchain = append(Blockchain, genesisBlock)

mutex.Unlock() }()

log.Fatal(run()) //启动web服务

}

到这里main.go就写完了,我们开始测试
首先打开cmd...命令行工具,进入mian.go所在的目录

技术详解 | 系列一:Go语言实现Pow共识算法

然后运行代码,go run main.go

技术详解 | 系列一:Go语言实现Pow共识算法 我们可以看到区块信息,当然仅有一个区块肯定是不够的,运算和验证都无法完成,这时我们打开Postman,一个很好用的测试API接口的工具,我们在Postman里使用POST方式访问localhost:8080,因为.env文件调用的是8080端口有,随后在body里的raw,因为我们是共享单车类的,所以修改Bike的值为100。

技术详解 | 系列一:Go语言实现Pow共识算法

保存,让我们看看命令行这边会不会开始挖矿。

技术详解 | 系列一:Go语言实现Pow共识算法

可以看到一次就完成了word done的挖矿成功,是因为我们的运气真是非常之好,如果在挖比特币的时候也这么好运就。。。好因为这一次的巧合,不能算在成功的测试,我们再来一次,将Bike的值修改为120。

技术详解 | 系列一:Go语言实现Pow共识算法

然后返回命令行

技术详解 | 系列一:Go语言实现Pow共识算法

可以看到这次呢,就经过了非常多的运算才完成挖矿,三个区块都有,让我们验证一下

可以看到index的值分别是0,1,2.自增了1,而且第二块的PrevHash值正是第一块的Hash值,第三块同理。

到这里我们就算是完成了PoW机制的实现。

这里放一下统一的源码吧,是没有注释的版本

https://github.com/redshround/PoW ,ok就这个网址

这里推荐几个好的go语言学习教程,适合萌新的学习

http://www.runoob.com/go/go-tutorial.html ,菜鸟教程的名号大家应该都听过,有比较统一完善的教程,基础实战在线编译一气呵成

https://studygolang.com ,GO语言中文网,这里聚集了很多GO语言的学习者和开发者,都有不同风格的教程,也可以直接和大佬们交流

http://c.biancheng.net/golang ,C语言中文网的Golang部分,凑活能用

https://www.jianshu.com/p/226c80c481a1 ,简书上的一个很不错的博客,全是实战案例,适合学习了一段时间求成的开发者

在以后的日子里,会逐渐更新go语言实现其他的共识机制,到最后会讲到,完全由自己实现的一条公链,敬请期待关注。

作者:张详,闫思

文章声明:本文为火星财经专栏作者作品,版权归作者所有。文章为作者本人观点,不代表火星财经立场。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

腾讯传

腾讯传

吴晓波 / 浙江大学出版社 / 2017-1-1 / 58.00元

腾讯官方唯一授权的权威传记 著名财经作家吴晓波倾力之作 当市值最高的中国互联网公司,遇上中国财经界最冷静的一双眼睛 读懂腾讯,读懂中国互联网 . 内容简介 本书全景式地记录了腾讯崛起的经历,并以互联网的视角重新诠释了中国在融入全球化进程中的曲折与独特性。 从1998年开始创业到成为世界级互联网巨头,腾讯以即时通信工具起步,逐渐进入社交网络、互动娱乐、网络媒......一起来看看 《腾讯传》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

随机密码生成器
随机密码生成器

多种字符组合密码

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具