PBFT概念与Go语言入门(Tendermint基础)
Tendermint作为当前最知名且实用的PBFT框架,网上资料并不很多,而实现Tendermint和以太坊的Go语言,由于相对小众,也存在资料匮乏和模糊错漏的问题。本文简单介绍PBFT概念和Go语言[&开发环境]关键知识点,其中大部分都可单独成篇,限于篇幅,文中提供诸多链接供大家深入。日后可能会基于Tendermint出系列博文,此篇纯当基础。
概念
下述一部分在前篇区块链初探中亦有涉及,可结合着看。
分布式系统中的异步和共识
异步:这里的异步不同于通常技术术语中的异步调用的异步,而是指在一个分布式系统中,对消息的处理速度或者消息送达时间不做任何假设。
共识:互相独立的多个系统参与方(进程或者节点)间对某个问题达成一致的结果。
FLP Impossibility:在异步的场景下,即便没有拜占庭故障,即便消息系统足够可靠(所有的消息都可以被正确的送达刚好一次),只要有至少一个进程失效(比如掉线等无法响应的情形),也没有任何算法能保证非失效进程达到一致性(共识)。
个人认为理解FLP的重点是:一个进程无法探测未响应进程的当前状态(失效or网络延迟导致),导致该进程无法确认自己是否应该[不管未响应进程]作出决策或者[不管未响应进程]作出的决策是否正确,从而导致整个系统状态的不确定性。
PBFT:实用拜占庭容错算法。现实生活中,完全异步的场景较少,或者我们可以设置超时等规则,绕过FLP的限制,称之为实用的一致性算法。比如PBFT,它的核心三阶段协议,依靠的是弱同步的网络环境(如因特网,虽有延迟但总体可控),具体流程可看 PBFT实用拜占庭容错算法深入详解。
基于PBFT的区块链共识框架,最知名的当属Tendermint。网上资料常拿他与以太坊的Casper协议比较优劣,Casper是基于链的共识算法,且内部实现了较为严厉的惩罚措施制约恶意节点。达成共识的手段上,Tendermint是基于轮次的投票机制,而Casper是基于赌博的投注机制。目前我对Casper节点具体的投注规则和流程仍然一头雾水,有兴趣的朋友可参看 干货 | 理解 Serenity,Part-2:Casper。
Tendermint的数据采用Amino编码,Amino继承自Protobuf3,Protobuf3主要是使用变长编码Varint编码数值,以减少存储空间大小和传输量,可参看 Varint与ZigZag编码、Protobuf3语言指南。SPV实现,同比特币一样依靠的是Merkel Tree,原理参看梅克尔树和简单支付验证
CAP理论:这玩意儿貌似很有名,然而我刚看到就有点糊涂,总感觉CP两个貌似就凑不到一起,后来发现果然有些质疑之声,参看 CAP理论 。说到底,CAP理论需要基于具体场景讨论,几个字母的意思在不同场景中未必一样,不可泛泛而谈。
一秒入门Go语言
Go或者Go语言,而不是Golang,Golang是网站的名字。
与其它大多数OO语言不同,Go的变量间赋值与传参几乎都是值拷贝。以数组为例:
func main() { var array1 = [3]int{0, 1, 2} var array2 = array1 fmt.Println(array1, array2) fmt.Printf("%p, %p, %p, %p\n", &array1, &array2, &array1[0], &array2[0]) } /*输出: [0 1 2] [0 1 2] 0xc04204c0a0, 0xc04204c0c0, 0xc04204c0a0, 0xc04204c0c0 */
可知两个变量指向不同的内存块。
再看slice
func main() { var s1 = []int{0, 1, 2} //[]表示切片类型,若数组要忽略元素个数,使用[...] var s2 = s1 fmt.Printf("%p, %p, %p, %p\n", &s1, &s2, &s1[0], &s2[0]) } /*输出: 0xc0420483a0, 0xc0420483c0, 0xc04204c0a0, 0xc04204c0a0 */
可得两个slice变量指向不同地址(值拷贝),但是其中的元素指向的是同一个地址;假如设置s2[1]=5,那么s1[1]也会变为5。这就涉及到slice的实现原理了,简单的说,一个slice对应一个数组,可以认为slice指向该数组的一个片段(所谓切片的由来)。上述代码中,s1、s2对应同一个数组。我们知道,数组是固定大小的,而在编程中,常常需要给slice追加数据,当数据项个数超过对应的数组长度时,系统会new一个新数组,原来的数据复制到新数组,这个新的array则为slice新的底层依赖。具体可参看 Golang 切片与函数参数“陷阱”, 其它类型可参看 golang传值和传引用
注意:fmt.Printf("%p", s1)和fmt.Printf("%p", &s1[0])输出结果相同。
所以为了避免内存浪费和数据不一致,我们常使用指针(虽然指针拷贝了,但是它们指向的数据还是同一个)。
interface:Go的OO特性主要依托的就是interface,语法同主流OO语言完全不同,其中有许多值得注意的特性和用法,当然我们亦要考虑前述的值传递or引用传递的问题(我们会看到有很多地方是指针实现接口方式,就是缘于此)。一些概念的运用可参看 Go语言基础:Interface。一个类型只有实现了interface的所有方法,才能认为是实现了这个interface,若只实现了部分方法,项目中又有它们之间的类型转换代码时,编译时将报“does not implement IXXX(missing X method)”错。若interfaceA包含了interfaceB,那么A就自动拥有了B中定义的所有方法,效果同C#中IA:IB的继承模式。
若类型XXX的指针*XXX实现了interfaceA,那么XXX的对象xxx和指针*XXX的对象都可以点出interfaceA中的方法,但只有*XXX才可以与interfaceA做类型转换。
struct:有个tag的概念,参看 GoLang structTag说明 。在tag中可以写第三方库的类型而不需要Import,如:RootDir string `mapstructure:"home"`,mapstructure就是第三方库,定义struct字段时不需引入,在实际调用的文件中再引入。struct也有类似上述interface的“继承”语法。更多可参看 Golang struct结构。
类型断言(Comma-ok断言):即判断对象的运行时类型,起到的作用类似于Java的instanceof、C#的is/as。它的语法有点奇怪:value, ok := em.(T)——em为某个interface变量,T是期望的运行时类型,ok表示em的运行时类型是否就是T,value是转换后的对象。如:
b,ok:=a.([]int) if ok{ //... }
等价于
if b,ok:=a.([]int);ok{ //... }
若我们知道b就是[]int,那么也可以省略ok的赋值 b,_:=a.([]int) 或者b:=a.([]int)
或者b:=[]int(a),(a为interface时貌似必须要用断言语法进行类型转换)。另外还有switch value := e.(type){ case int:...}的写法。
若interfaceA包含interfaceB,某个类XXX实现了interfaceA,那么XXX对象xxx转成interfaceA/interfaceB.(type),case interfaceA/interfaceB/XXX 都成立。
Go支持多返回值,简直完美。
变量声明,若不赋值,则为默认值,注意并非nil,以struct为例:
type State struct { Height int64 AppHash string } func main() { var state State println(state.Height) println(state.AppHash) } /*输出 0 "" */
Go没有工程文件的概念,是通过目录结构来体现工程的结构关系。也有诸多环境变量,接触较多的是GOPATH,在其中可以设置多个目录,每个目录就是每个项目的根目录。具体可参看 Go项目的目录结构。同属于某个包的代码文件要有单独目录(目录名即包名),不同包的文件不能放置于同一个目录下。GOPATH有个bin目录,用于存放可执行文件,为了方便,我们常把%GOPATH%/bin加入到PATH中,不过假如GOPATH有多个目录,那么在windows下只能分别加入了(Linux可以用${GOPATH//://bin:}/bin添加所有的bin目录,windows不知道有没有类似的语法)。
go build、go install的区别:前者用于编译检查,除可执行文件外,不会有其它文件生成,它的作用就是为了检查是否有编译时错误;后者当然也包括前者环节,同时会生成依赖包文件。
Go语言没有像其它语言一样有public、protected、private等访问控制修饰符,它是通过字母大小写来控制可见性的,如果定义的常量、变量、类型、接口、结构、函数等的名称是大写字母开头表示能被其它包访问或调用(相当于public),非大写开头就只能在包内使用(相当于private,当然同个包的不同类型也可以使用。变量或常量也可以下划线开头)。
tips:与C#不同的是,我们可以定义一个private struct/others,实现public interface,该特性提供了一个对外隐藏对象细节,但又不妨碍调用其逻辑的途径。
Go的function types:可以给包含相同参数和返回类型的函数集合抽象出一个函数类型,如此我们也可以给函数增加新方法(因为其是一个类型嘛)。参看 理解go的function types。本人现在尚不知为何要搞出这一个概念,因为如果是管理方法和多态特性的话,还是使用传统的类型比如interface/struct比较合理;如果只是为了函数能作为参数传递,更没有必要,因为函数本身就支持将自己传给另外的函数。其实可以直接使用类似 var delegate func(int) 声明一个符合特定规则的函数变量。
让人困惑的一段代码,有知道的同学请不吝赐教:
1 func main() { 2 fmt.Println("anything") //加上这句,且与第7行都为fmt.Println 3 4 func() { println("顺序测试") }() //若该行也改为fmt.Println,则顺序正常 5 6 x := 10000 7 fmt.Println(byte(x)) 8 } 9 10 /*输出 11 anything 12 16 13 顺序测试 14 */
命令行交互:很多程序特别是在linux系统下,都以命令行交互为主。Go语言有命令行库cobra,能非常快捷地让程序支持命令形式。首先我们要安装cobra:
go get -v github.com/spf13/cobra/cobra
cobra库较大,同时依赖其它的一些库,因此最好开个vpnFQ,免得中途断线(虽然github能在国内访问,但本人在下载源码过程中仍然几乎每次都出现断连情况)。开个vpn,在下载 https://golang.org/x/text/transform?go-get=1 时仍不成功,原因不知为何,在网上找了一份单独下载放到GOPATH\src下即可(参看 【Golang笔记】Golang工具包Cobra安装记录)。关于cobra的使用可参看 golang命令行库cobra的使用/Golang: Cobra命令行参数库的使用。
上面用到的go get是一种依赖管理方式,类似于Java的maven、Js的npm、python的pip、C#的nuget,使用它来下载或更新当前项目依赖的第三方库。所不同的是,go get 用来动态获取远程代码包后再install成pkg,要是非开源的库应该就不能用这种方法了。go get是通过解析代码中的import语句来查看依赖包(而非需要我们人工提供一个依赖库的列表文件),当下载了依赖库之后,它会继续分析该依赖库依赖的其它库,直到所需要的库全部下载完毕。
go get的缺陷是没有库版本信息,第三方库管理工具godep、govender、glide和官方1.9版本推出的dep倒是可以了。它们同go get方式一样分析所需的依赖库,并将依赖库及其版本信息记录在生成的文件里,下载的依赖会放到一个叫vendor的目录下——Go1.5开始引入了另一种包的发现方法,如果项目中包含一个叫vendor的目录,go将会从这个目录搜索依赖的包,这些包会在标准库之前被找到。vendor目录是放在当前工程目录下,避免了go get方式的将所有依赖库存放于GOPATH的第一个目录导致的迁移问题和不同项目引用的库版本冲突问题。需要注意,vendor本身只是一个目录,不承担库版本控制的职责,这方面工作还是得由dep等去完成。
等到dep正式集成到Go环境中时候,也许是Go 1.10 ,广大吃瓜群众就可以直接使用go dep命令,现在还是需要自己安装。使用要点可参看Go语言入门——dep入门。需要注意的是Gopkg.toml中override特性的作用,用于解决多个关联项目引用不同版本的依赖库的版本冲突问题,可参看 使用 override 解决 dep 中的依赖冲突(其实并不能算是解决,只是强制所有项目引用同一个依赖版本库而已,是否运行时兼容还是得自己搞定)。
由于墙的缘故,很多官方(golang.org)的库无法下载,虽然基本上的库都可以在github上找到相应的源码,但是若要手动下载install啥的,就回到原点,失去依赖管理工具带来的便捷了。网上也有同仁遇到这种烦恼:dep ensure无法拉取golang.cn以及google.golang.org的依赖问题,按照回答里的信息,我打算买台境外服务器装ss5作为代理。于是我去买了阿里云位于香港的ECS,登录ECS访问大陆被墙的网站妥妥的没问题,我满心欢喜;接着照着 CentOS7 配置SOCKS5代理服务 中的步骤安装了ss5,然后在服务器和阿里云控制台都打开了1080端口(ss5默认端口),并在火狐中进行了代理设置,结果是IP显示的确是香港的,然而原本被墙的[几大]网站(google、facebook、youtube)还是上不了,本来可访问的站点变得很慢甚至连接不上,和 centos 搭建了 ss5,为什么不能访问 google 中的问题几乎一样,我估计是阿里云后台做了限制。
换成AWS也一样(EC2选在美国东部),但是访问非被墙的国外网站确实快了不少,访问国内站点亦变得较慢,所以ss5应该还是起作用了,至于为何翻不了墙,估计是政府要求这些云厂商都做了处理;后来偶然发现,一些原本无法访问的国外站点(笔者暂试了sex类),代理之后就能访问了(推测依旧无法访问的只有少数的政府重点关注的网站)。并且不但设置代理的火狐可以访问,未设置的chrome偶尔也可以(此时chrome访问国内站点速度并未减慢,对外IP仍为本地),取消代理后一段时间内仍能访问,种种状况,不知何故;有次发现在无法访问时ping得的IP和可访问时ping得的IP不一样,估计是连上ss5之后,代理端的dns服务器返回了可访问的IP(域名解析过程可参看 DNS解释),且该IP和域名的映射被同时缓存到了本地全局域里,这倒是可以解释前述情景;不过我将hosts显式配置了可访问的IP,然后取消代理,该网站又无法访问了,真的是难以捉摸;大部分情况两次ping得的IP是一样的,且是否设置代理确实直接影响到网站是否能访问,即[代理后]能否ping通和[代理后]能否访问并无关联。(另:Amazon自己的Linux版本安装不了ss5,会报“undefined reference to S5ChildClose”,貌似是ss5不兼容gcc5之类的原因)
不过幸好golang.org是可以访问了,为了使得命令行也能用上ss5代理,去下个Proxifier,然后再dep ensure,妥妥的没问题。(目前AWS的t2.micro类型实例有一年的免费期,1C1G1M,作为自己的独家代理够用了)
goroutine:网上资料很多,引出的是协程的概念,这个概念在我之前的博文中有所涉及,可参看。常使用channel为协程间协调和通信,channel有只读只写的用法,主要用在一段逻辑中,表明在这段逻辑里,该channel只能读或只能写,否则编译报错,它并不表示真的有只读只写的channel,可参看 Go 只读/只写channel。
其它语言不常见的select控制结构:Go 语言 select 语句
测试:用到testing包,参看Golang 语言的单元测试和性能测试(也叫压力测试)。
其它
vpn:我们常用vpnFQ,可以看作一种代理模式,其实vpn的初衷是为了方便非本地局域网的合法用户访问本地局域网资源,通俗例子可看vpn的实现原理。外部网络默认无法访问局域网资源,就如同我们无法访问墙的那一边[被墙的资源],可能因为这暗合了vpn的用途,所以当前市场各色FQ软件以vpn为主。目前工信部严管VPN提速,另有其它技术如socks可实现代理功能。
除了上面提到的ss5+Proxifier外,还有ss-fly+Shadowsocks。使用起来感觉后者比较稳定,不存在部分能上部分不能上的问题,特别是提供侵入性小的PAC模式(Proxifier似乎也有提供),如果软件支持代理设置(如浏览器),则设置为使用系统代理或者设置代理地址为http://127.0.0.1:port即可,其中port是Shadowsocks暴露给本机的代理端口;不过如果软件没有地方设置代理,那么可以用前者,在Proxifier中设置哪些软件使用代理;当然也可以ss-fly+Shadowsocks+Proxifier结合使用()设置Proxifier的代理为Shadowsocks提供的代理地址http://127.0.0.1:port)。
pv操作:P和V是来源于两个荷兰语词汇,P—— passeren,中文译为"通过";V—— vrijgeven,中文译为"释放"。P操作和V操作是执行时不被打断的两个操作系统原语(在执行这两个语句时不允许系统发生中断,从而保证语句的原子性执行),它们操作的是信号量S。线程/进程要执行时,先P一下看是否通过(S是否>=0,即是否可以运行),若否则等待;运行完毕V一下将S+1,表示资源被释放,其它线程可以开始运行。
以太网智能合约:
智能合约就是一段程序,一段逻辑(这段代码可以有状态变量),我们将它编译后的字节码部署到区块链上(需要发起一个交易),合约部署后会创建一个合约账户,合约账户里保存着智能合约的可执行字节码,并且有存储空间用于存变量值(storage)。有个abi的概念,abi是一个接口结构,利用abiDefinition可以创建调用该合约的结构,abi应该由合约所有方自己保存和提供。
要执行智能合约时,调用方从区块链上[通过地址]获取这段代码并调用(一般也会发起一个交易),调用时可能会改变状态变量的值,这些状态量的更改反映到storage中。storage的物理存储结构时怎样的,根据Solidity首席工程师Chriseth的说法,“你可以把storage想像成一个大数组”,就跟 了解以太坊智能合约存储 写的一样,深入了解以太坊虚拟机第2部分——固定长度数据类型的表示方法 中也有无限量的内存的说法,如此即可将值存入hash(key)后的最大为2256内存地址中,且几乎肯定的不会产生冲突(即无需使用类似hashmap的冲突处理)。然而实际的存储空间肯定远小于此,网上搜了一圈没看到实际的结构介绍,先将此疑问记录于此,日后查看。
另外,我们不要被网上智能合约的概念欺骗,目前,智能合约远未到预期的设想,主要的障碍有两点:
- 智能合约的应用依赖于基于区块链资产的数字化,但是目前来讲,这种数字化程度还远远不够。即智能合约只有在数字版本与实体之间存在某种明确的联系时才能有效代替普通合约,且实体关系需要随着数字资产变化而自觉变化,反之亦然,案例设想可参看 概念炒作的背后,“智能合约”的真相是什么?
- 智能合约只能被动响应外部访问请求,根本无法做到内部合同条款的自动执行。而外部请求一般都是中心化的,这进一步会极大降低智能合约作为一个去中心化系统的有效性。
其它参考资料: