go与区块链吧(Go语言200行写区块链源代码分析)

  go与区块链吧(Go语言200行写区块链源代码分析)

以下文章来源于飞雪无情 ,作者飞雪无情

  

Github上有一个Repo,是一个使用Go语言(golang),不到200行代码写的区块链源代码,准确的说是174行。原作者起了个名字是 Code your own blockchain in less than 200 lines of Go! 而且作者也为此写了一篇文章。https!//medium。com/@mycoralhealth/code-your-own-blockchain-in-less-than-200-lines-of-go-e296282bcffc

  

这篇文章是一个大概的思路和代码的实现,当然还有很多代码的逻辑没有涉及,所以我就针对这不到200行的代码进行一个分析,包含原文章里没有涉及到的知识点,对Go语言,区块链都会有一个更深的认识。

  

所有的源代码都在这里:https!//github。com/nosequeldeebee/blockchain-tutorial/blob/master/main。go

  

import(";crypto/sha256";";encoding/hex";";encoding/json";";io";";log";";net/http";";os";";strconv";";sync";";time";";github。com/davecgh/go-spew/spew";";github。com/gorilla/mux";";github。com/joho/godotenv";)

在源代码的开头,是作者引入的一些包,有标准的,也有第三方的。像sha256,hex这些标准包是为了sha-256编码用的,其他还有启动http服务,打印日志的log,并发控制的sync,时间戳的time。

  

第三方包有三个,其中两个我都详细介绍过,相信大家不会陌生。

  

go-spew是一个变量结构体的调试利器,可以打印出变量结构体对应的数据和结构,调试非常方便

  

gorilla/mux是一个web路由服务,可以很简单的帮我们构建Web服务

  

不过目前用gin的比较多,也推荐使用gin https!//github。com/gin-gonic/gin。

  

godotenv是一个读取配置文章的库,可以让我们读取。env格式的配置文件,比如从配置文件里读取IP、PORT等。不过目前配置文件还是推荐YAML和TOML,对应的第三方库是:

  

gopkg。in/yaml。v21https!//github。com/BurntSushi/toml

  

既然要写一个区块链,那么肯定的有一个区块的实体,我们通过golang的struct来实现。

  

//Blockrepresentseach';item';intheblockchaintypeBlockstruct{IndexintTimestampstringBPMintHashstringPrevHashstring}

Block里包含几个字段:

  

  1. Index 就是Block的顺序索引
  2. Timestamp是生成Block的时间戳
  3. BPM,作者说代表心率,每分钟心跳数
  4. Hash是通过sha256生成的散列值,对整个Block数据的Hash
  5. PrevHash 上一个Block的Hash,这样区块才能连在一起构成区块链

有了区块Block了,那么区块链就非常好办了。

  

//BlockchainisaseriesofvalidatedBlocksvarBlockchain[]Block

就是这么简单,一个Block数组就是一个区块链。区块链的构成关键在于Hash和PrevHash,通过他们一个个串联起来,就是一串Block,也就是区块链。因为相互之间通过Hash和PrevHash进行关联,所以整个链很难被篡改,链越长被篡改的成本越大,因为要把整个链全部改掉才能完成篡改的目的,这样的话,其他节点验证这次篡改肯定是不能通过的。

  

既然关键点在于Hash,所以我们要先算出来一个Block的数据的Hash,也就是对Block里的字段进行Hash,计算出一个唯一的Hash值。

  

//SHA256hasingfunccalculateHash(blockBlock)string{record!=strconv。Itoa(block。Index)+block。Timestamp+strconv。Itoa(block。BPM)+block。PrevHashh!=sha256。New()h。Write([]byte(record))hashed!=h。Sum(nil)returnhex。EncodeToString(hashed)}

sha256是golang内置的sha256的散列标准库,可以让我们很容易的生成对应数据的散列值。从源代码看,是把Block的所有字段进行字符串拼接,然后通过sha256进行散列,散列的数据再通过hex。EncodeToString转换为16进制的字符串,这样就得到了我们常见的sha256散列值,类似这样的字符串8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92。

  

Block的散列值被我们计算出来了,Block的Hash和PrevHash这两个字段搞定了,那么我们现在就可以生成一个区块了,因为其他几个字段都是可以自动生成的。

  

//createanewblockusingpreviousblock';shashfuncgenerateBlock(oldBlockBlock,BPMint)Block{varnewBlockBlockt!=time。Now()newBlock。Index=oldBlock。Index+1newBlock。Timestamp=t。String()newBlock。BPM=BPMnewBlock。PrevHash=oldBlock。HashnewBlock。Hash=calculateHash(newBlock)returnnewBlock}

因为区块链是顺序相连的,所以我们在生成一个新的区块的时候,必须知道上一个区块,也就是源代码里的oldBlock。另外一个参数BPM就是我们需要在区块里存储的数据信息了,这里作者演示的例子是心率,我们可以换成其他业务中想要的数据。

  

Index是上一个区块的Index+1,保持顺序;Timestamp通过time。Now()可以得到;Hash通过calculateHash方法计算出来。这样我们就生成了一个新的区块。

  

在这里作者并没有使用POW(工作量证明)这类算法来生成区块,而是没有任何条件的,这里主要是为了模拟区块的生成,演示方便。

  

区块可以生成了,但是生成的区块是否可信,我们还得对他进行校验,不能随便生成一个区块。在比特币(BitCoin)中校验比较复杂,这里作者采用了简单模拟的方式。

  

//makesureblockisvalidbycheckingindex,andcomparingthehashofthepreviousblockfuncisBlockValid(newBlock,oldBlockBlock)bool{ifoldBlock。Index+1!=newBlock。Index{returnfalse}ifoldBlock。Hash!=newBlock。PrevHash{returnfalse}ifcalculateHash(newBlock)!=newBlock。Hash{returnfalse}returntrue}

简单的对比Index,Hash是否是正确的,并且重新计算了一遍Hash,防止被篡改。

  

到了这里,关于区块链的代码已经全部完成了,剩下的就是把区块链的生成、查看等包装成一个Web服务,可以通过API、浏览器访问查看。因为作者这里没有实现P2P网络,所以采用的是WEB服务的方式。

  

//createhandlersfuncmakeMuxRouter()http。Handler{muxRouter!=mux。NewRouter()muxRouter。HandleFunc(";/";,handleGetBlockchain)。Methods(";GET";)muxRouter。HandleFunc(";/";,handleWriteBlock)。Methods(";POST";)returnmuxRouter}

通过mux定义了两个Handler,URL都是/,但是对应的Method是不一样的。

  

GET方法通过handleGetBlockchain函数实现,用于获取区块链的信息。比特币价格

  

funchandleGetBlockchain(whttp。ResponseWriter,r*http。Request){bytes,err!=json。MarshalIndent(Blockchain,";";,";";)iferr!=nil{http。Error(w,err。Error(),http。StatusInternalServerError)return}io。WriteString(w,string(bytes))}

Blockchain是一个[]Block,handleGetBlockchain函数的作用是把Blockchain格式化为JSON字符串,然后显示出来。io。WriteString是一个很好用的函数,可以往Writer里写入字符串。更多参考 Go语言实战笔记(十九) Go Writer 和 Reader

  

';POST';方法通过handleWriteBlock函数实现,用于模拟区块的生成。

  

funchandleWriteBlock(whttp。ResponseWriter,r*http。Request){w。Header()。Set(";Content-Type";,";application/json";)//使用了一个Mesage结构体,更方便的存储BPMvarmsgMessage//接收请求的数据信息,类似{";BPM";!60}这样的格式decoder!=json。NewDecoder(r。Body)iferr!=decoder。Decode(&msg);err!=nil{respondWithJSON(w,r,http。StatusBadRequest,r。Body)return}deferr。Body。Close()//控制并发,生成区块链,并且校验mutex。Lock()prevBlock!=Blockchain[len(Blockchain)-1]newBlock!=generateBlock(prevBlock,msg。BPM)//校验区块链ifisBlockValid(newBlock,prevBlock){Blockchain=append(Blockchain,newBlock)spew。Dump(Blockchain)}mutex。Unlock()//返回新的区块信息respondWithJSON(w,r,http。StatusCreated,newBlock)}

以上代码我进行了注释,便于理解。主要是通过POST发送一个{";BPM";!60}格式的BODY来添加区块,如果格式正确,那么就生成区块进行校验,合格了就加入到区块里;如果格式不对,那么返回错误信息。

  

用于控制并发的锁可以参考Go语言实战笔记(十七) Go 读写锁

  

这个方法里有个Message结构体,主要是为了便于操作方便。

  

//MessagetakesincomingJSONpayloadforwritingheartratetypeMessagestruct{BPMint}

返回的JSON信息,也被抽取成了一个函数respondWithJSON,便于公用。

  

funcrespondWithJSON(whttp。ResponseWriter,r*http。Request,codeint,payloadinterface{}){response,err!=json。MarshalIndent(payload,";";,";";)iferr!=nil{w。WriteHeader(http。StatusInternalServerError)w。Write([]byte(";HTTP500!InternalServerError";))return}w。WriteHeader(code)w。Write(response)}

好了,快完成了,以上Web的Handler已经好了,现在我们要启动我们的Web服务了。

  

//webserverfuncrun()error{mux!=makeMuxRouter()//从配置文件里读取监听的端口httpPort!=os。Getenv(";PORT";)log。Println(";HTTPServerListeningonport!";,httpPort)s!=&http。Server{Addr!";!";+httpPort,Handler!mux,ReadTimeout!10*time。Second,WriteTimeout!10*time。Second,MaxHeaderBytes!1<;<;20,}iferr!=s。ListenAndServe();err!=nil{returnerr}returnnil}

和原生的http。Server基本一样,应该比较好理解。mux其实也是一个Handler,这就是整个Handler处理链。现在我们就差一个main主函数来启动我们整个程序了。

  

  

//控制并发的锁varmutex=&sync。Mutex{}funcmain(){//加载env配置文件err!=godotenv。Load()iferr!=nil{log。Fatal(err)}//开启一个goroutine生成一个创世区块gofunc(){t!=time。Now()genesisBlock!=Block{}genesisBlock=Block{0,t。String(),0,calculateHash(genesisBlock),";";}spew。Dump(genesisBlock)mutex。Lock()Blockchain=append(Blockchain,genesisBlock)mutex。Unlock()}()log。Fatal(run())}

整个main函数并不太复杂,主要就是加载env配置文件,开启一个go协程生成一个创世区块并且添加到区块链的第一个位置,然后就是通过run函数启动Web服务。

  

一个区块链都有一个创世区块,也就是第一个区块。有了第一个区块我们才能添加第二个,第三个,第N个区块。创世区块因为是第一个区块,所以它是没有PrevHash的。

  

终于可以运行了,假设我们设置的PORT是8080,现在我们通过go run main。go启动这个简易的区块链程序,就可以看到控制台输出的创世区块信息。然后我们通过浏览器打开http!//localhost!8080也可以看到这个区块链的信息,里面只有一个创世区块。

  

如果我们要新增一个区块,通过curl或者postman,向http!//localhost!8080 发送body格式为{";BPM";!60}的POST的信息即可。然后在通过浏览器访问http!//localhost!8080查看区块链信息,验证是否已经添加成功。

  

到这里,整个源代码的分析已经完了,我们看下这个简易的区块链涉及到多少知识:

  

  1. sha256散列
  2. 字节到16进制转换
  3. 并发同步锁
  4. Web服务
  5. 配置文件
  6. 后向式链表
  7. 结构体
  8. JSON
  9. ……

等等,上面的很多知识,我已经在文章中讲解或者通过以前些的文章说明,大家可以看一下,详细了解。

  
","content_hash"!"945106fa

版权声明

本文仅代表作者观点,不代表本站立场。
本文系作者授权发表,未经许可,不得转载。

评论