本文主要借demo介绍基于Tendermint的区块链应用开发,这个demo很简单,主要包含如下功能:html
代码已上传至github。node
Tendermintpython
Tendermint帮咱们实现了PBFT,至关于搭了一个共识框架,包含两部分:mysql
能够将其类比为传统应用的开发框架(如MVC),而咱们要作的就是基于abci编写具体的区块链逻辑(为方便和清晰起见,本文用Go编写具体逻辑,天然abci就用官方的了),这就实现了服务端;而用户也须要一个客户端用来与区块链交互。linux
以上,Tendermint、服务端逻辑、客户端,三者组成了一个完整的区块链应用。git
数据库github
在动手编码以前,要考虑数据存储的问题,选择文本文件仍是Oracle呢?区块链网络里大部分是普通电子设备,使用者亦是普通人,让他们事先安装大型数据库显然不现实,更不用说区块链自己不会出现复杂操做数据的业务。另外因为全节点数据的完备性,用不着经过网络去其它设备上查询数据,不少数据库自带的网络服务也不须要(SPV这种,业务单一,彻底能够单独开放一个远程接口)。而文本文件、excel之类的,只适合人类使用,根本不能算做数据引擎。咱们须要的是一个知足基本CUID的高效的本地数据库,目前大多区块链使用LevelDB做为存储引擎,这是C/C++编写的本地kv数据库,原做者也写了Go实现的版本,其原理可参看 半小时学会LevelDB原理及应用 ,godoc地址:https://godoc.org/github.com/syndtr/goleveldb/leveldb。LevelDB整体上采用了LSM-Tree的设计思想(LSM-Tree的虽然说是数据结构,但更偏重于设计思路)。golang
LevelDB同时只能被一个进程使用。另,以太坊的数据存储于/chaindata目录下,运行后其下会生成一坨.ldb文件,而非网上常说的sst文件,这多是跟13年的一次版本更新有关,Release LevelDB 1.14。另:LevelDB的k-v模式(顺序读效率不高)不适合relationship,即不适合有必定数据关联度的业务场景。正则表达式
为方便使用,能够封装一些经常使用的数据库操做。顺便尝试下提供新操做的几种思路。算法
// 给leveldb.DB增长Set方法 func (db *leveldb.DB) Set(key []byte, value []byte) { //... err := db.Put(key, value, nil) //... }
func (db *GoLevelDB) Get(key []byte) []byte { //... //Go不支持重载,或者说Go只把方法名做为惟一签名。 //这里原意是调用的父类的Get方法,但该方法被当前类的Get方法覆盖了,参数不一致致使编译失败 res, err := db.Get(key, nil) //... return res }
不支持重载,只能修改子类的方法名,蛋疼;或者改为以下方式。
type GoLevelDB struct { db *leveldb.DB }
和第2种的区别就是把is-a改成has-a,也不用担忧方法重名的问题。不过我私觉得若Go支持重载,第2种方式会好一点,至少不会嵌套太多层。
服务端
abci定义了以下接口:
type Application interface { // Info/Query Connection Info(RequestInfo) ResponseInfo // Return application info SetOption(RequestSetOption) ResponseSetOption // Set application option Query(RequestQuery) ResponseQuery // Query for state // Mempool Connection CheckTx(tx []byte) ResponseCheckTx // Validate a tx for the mempool // Consensus Connection InitChain(RequestInitChain) ResponseInitChain // Initialize blockchain with validators and other info from TendermintCore BeginBlock(RequestBeginBlock) ResponseBeginBlock // Signals the beginning of a block DeliverTx(tx []byte) ResponseDeliverTx // Deliver a tx for full processing EndBlock(RequestEndBlock) ResponseEndBlock // Signals the end of a block, returns changes to the validator set Commit() ResponseCommit // Commit the state and return the application Merkle root hash }
很明显,后面几个方法参与了区块链状态的更迭,咱们就来捋捋交易从客户端提交到最终上链的过程(不精确):
假如将要打包的tx缓存起来,咱们就能够在DeliverTx、EndBlock、Commit三个方法中选择其一实际执行tx,可是通常来讲,交易执行都是放在DeliverTx,比较符合语义。EndBlock用于更新共识参数和Val集合,Commit用于更新整个应用状态(apphash),须要注意的是,本次提交的apphash若与上次提交的不一样,则会继续产生新的区块(无论有没有新交易,就算设置consensus.create_empty_blocks=false,tendermint也会产生空区块,可参看 Enable no empty blocks #308 。),这彷佛是tendermint的有意设计,但不知为什么。
另Query方法接收的RequestQuery类型参数有Path和Data两个字段,Path是string类型,Data是[]byte,应该是对应于Http的get、post。示例代码中我是经过正则表达式解析Path查询各种数据,其实如果复杂查询/结构化查询,仍是Data字段比较实用。
正则表达式的所谓零宽断言:只匹配位置,而不消费字符。下面举个例子。如 \b\w*q[^u]\w*\b,它能匹配“Iraq,Benq”。由于[^u]老是匹配一个字符,因此若是q是单词的最后一个字符的话,后面的[^u]将会匹配q后面的单词分隔符(多是空格,或者是句号或其它的什么),接着后面的\w+\b将会匹配下一个单词,因而\b\w*q[^u]\w*\b就能匹配整个Iraq fighting。若是在这个例子中,咱们只想匹配到Iraq,那么能够采用零宽负向先行断言(?!exp)的方式,\b\w*q(?!u)\w*\b,它将不会消费Iraq后面的空格或逗号等字符,所以\w*也不会匹配到下一个单词。参看 【详细】正则表达式30分钟入门教程 之位置指定和后向位置指定部分。
客户端
demo采用命令行终端,基于cobra库。
1 var rootCmd = &cobra.Command{ 2 Use: "dbcli", 3 //throw:丢;salvage:捞;reply:回应。 ValidArgs要有定义Run[E],并与Args: cobra.OnlyValidArgs结合才起做用,表示参数值只能是预设值 4 //ValidArgs: []string{"throw", "salvage", "reply", "bbalj"}, 5 //Args主要是用来校验参数的 6 //Args: cobra.OnlyValidArgs, //cobra.ExactArgs(0), 7 // RunE: func(cmd *cobra.Command, args []string) error { //args并不包含flag;os.Args是包含flag的 8 // }, 9 } 10 11 func main() { 12 if err := rootCmd.Execute(); err != nil { 13 fmt.Println(err) 14 os.Exit(-1) 15 } 16 }
本来我想实现交互模式(相似mysql>),但cobra彷佛没有提供相关方法,咱们只好本身想办法,须要注意的是须要自解析用户输入,好比用户输入有空格,该空格是分隔参数仍是参数内部的,要作区分。本来打算参考cobra解析命令行的源码,发现实际解析使用的是spf13/pflag库,而pflag只是增强了go标准库flag,而flag库也并无涉及到参数值自己的具体解析,这部分工做依靠的是oa库,主要是oa.Args属性,它依赖更底层的代码。
// 摘自go/src/os/proc.go // Args hold the command-line arguments, starting with the program name. var Args []string func init() { if runtime.GOOS == "windows" { // Initialized in exec_windows.go. return } Args = runtime_args() } func runtime_args() []string // in package runtime
如注释所示,windows下是在exec_windows.go中实现,其它操做系统的实现没找到,应该是使用其它语言编写或直接调用的系统api。进exec_windows.go中,发现关键函数readNextArg:
1 // readNextArg splits command line string cmd into next 2 // argument and command line remainder. 3 func readNextArg(cmd string) (arg []byte, rest string) { 4 var b []byte 5 var inquote bool 6 var nslash int 7 for ; len(cmd) > 0; cmd = cmd[1:] { 8 c := cmd[0] 9 switch c { 10 case ' ', '\t': 11 if !inquote { 12 return appendBSBytes(b, nslash), cmd[1:] 13 } 14 case '"': 15 b = appendBSBytes(b, nslash/2) 16 if nslash%2 == 0 { 17 // use "Prior to 2008" rule from 18 // http://daviddeley.com/autohotkey/parameters/parameters.htm 19 // section 5.2 to deal with double double quotes 20 if inquote && len(cmd) > 1 && cmd[1] == '"' { 21 b = append(b, c) 22 cmd = cmd[1:] 23 } 24 inquote = !inquote 25 } else { 26 b = append(b, c) 27 } 28 nslash = 0 29 continue 30 case '\\': 31 nslash++ 32 continue 33 } 34 b = appendBSBytes(b, nslash) 35 nslash = 0 36 b = append(b, c) 37 } 38 return appendBSBytes(b, nslash), "" 39 }
其中对双引号作了处理,注释中还提供了一个网址How Command Line Parameters Are Parsed,应该是关于这方面的算法说明,往后再看。
序列化
当咱们在说序列化的时候,咱们在说什么。序列化说白了就是数据转化,或者说一一对应的映射关系。就内存场景来讲,一个对象序列化为另外一个对象,本质上它们都同样,都是存储在内存中的0、1序列,只是同一个东西不一样的数据表达。好比将一个数值序列化(或者说转化)成字符串类型,或者将数值int32转为数值int8,那么内存中的存储空间和存储数据都不会同样,字符串还要看用的什么编码。再如咱们将一个对象序列化为byte[],不一样的方案会产生不一样的结果。好比使用C指针将物理数据直接映射出来,或者以json方式序列化,或者protobuf序列化,会产生不一样的byte[];反之亦然。
无论是json编码仍是二进制编码,物理上存储的都是二进制,json编码包含于二进制编码,咱们能够根据须要自定义二进制编码,通常是为了减小存储占用的空间。好比json编码,对一、2等数值类型是按字符串格式编码(如utf8格式,1编码的就是0x31,12占两个字节0x310x32),而咱们自定义二进制,彻底能够把12存储在一个字节里面,该字节值就是数值自己;就算不是数值,而是字符串自己编码,咱们也能够在utf8编码后再压缩,相似gzip。
go中的序列化方式,可参看 Golang 序列化方式及对比,可是文中gob的测试代码其实能够改良下,将enc/dec两个变量移到循环外,如此可在循环内复用,这将发挥gob上下文的优点。
protobuf的变长编码针对的是数值类型,so应该只对数值字段多的类型有压缩的意义。
go对字符串是utf8编码,基本不用担忧中文乱码问题。
vscode-go开发环境
在国内,搭建Go开发环境都不会太顺利,下面我就说说在vscode中搭建环境可能会遇到的问题和解决方法。
Go开发环境须要vscode安装一些插件,而项目中也有引用的类库,这二者均可能涉及到相关站点在墙外的状况,而咱们也要分别设置代理。首先,给vscode自己设置代理,使得安装插件没有问题;其次,在命令行窗口设置http_proxy,使得dep顺利进行。也能够在vscode终端窗口设置http_proxy(vscode的终端就是个命令行交互环境,使用的仍是操做系统的shell,本质上独立于vscode),但博主发现彷佛并不起做用。
在代理什么都设置好后,vscode安装插件时仍可能遇到问题,好比文件中已经存在的golang.org\x\tools目录关联的git源码网址不是插件要求的源码网址,缘由多是以前手动到github里下载的tools源码,将tools目录移除从新跑一遍安装插件的步骤便可。
安装goimports时可能会timeout等错误,参考 安装goimports 解决。
项目方面,具体到咱们这个demo,遵守tendermint官方文档,make get_tools。我是windows10系统,使用bash命令进入到自带的Ubuntu子系统,就可使用内置的make了。须要注意的是,若设置了系统变量GOPATH,且是以分号分隔的多个文件夹,那么切换到Ubuntu后,因为linux系统是按冒号分隔的,因此它会把分号当作文件夹名的一部分,致使自动建立一些奇怪目录。若是是其它windows系统,能够安装mingw,定位到安装目录的bin目录下,就可使用mingw-make操做了(能够将mingw-make重命名为make),可能会报错:
process_begin: CreateProcess(NULL, env bash F:\Document\code\tendermint\tendermint\scripts\get_tools.sh, ...) failed. make (e=2): 系统找不到指定的文件。
若是不是get_tools.sh的路径问题,那就应该是bash冲突了(好比系统中安装了git,同时把git目录也配置到PATH下,实际定位的可能就是git的bash了)。
注意tendermint所需的最低Go版本。
咱们要严格遵循Go的目录规范,若将代码直接置于src\目录下,则执行dep相关操做时,会抛出“root project import: dep does not currently support using GOPATH/src as the project root”错误。须要在src\下再建一个目录,把代码拷进这个子目录再执行dep。Go遵循约定大于配置的原则,它在项目中引入全部依赖类库的代码,而这些类库也是放置于src目录下,因此须要按子目录分开。另关于依赖项搜寻Support vendor directory as $GOPATH/src/vendor #313 应该有参考价值,另可参看 dep init fails if in not in $GOPATH[...]/src/{somedir..} #148。
dep彷佛会将GOPATH\src下的依赖也复制到vendor下,感受是否是没这必要。
经验:最好在项目刚开始搭建就 dep init,不然在代码敲了一个阶段后,已经import了多个外部依赖,当这时候再 dep init,若是出现错误,将不会生成Gopkg.toml,若是是由于版本问题致使的错误,你都没办法经过编辑Gopkg.toml的方式解决。好比我就遇到这种状况,dep init -gopath, -gopath表示先去本地GOPATH目录找依赖库,找不到再去网上拉取,结果个人本地库版本不是master分支,而貌似dep默认的就是master,致使“v0.30.2: Could not introduce github.com/tendermint/tendermint@v0.30.2, as it is not allowed by constraint master from project tuoxie/driftbottle.”这样的错误提示(dep也是一根筋,它会把这个库的全部release版本都比对一遍看满不知足constraint)。此时也不是没办法,咱们能够把入口函数main所在文件整个注释掉,这样dep就不会遍历代码文件,但仍然会生成Gopkg.toml,这个时候就能够手动编辑约束版本号了。
go install 不会把vendor目录下的全部包无脑打包进exe文件,而是会根据实际依赖打包,这样也使得咱们能够多个[子]项目使用同一个vendor,减少磁盘占用和复用已下载的依赖包,而没必要担忧exe文件过大的问题。
目前vscode调试go尚不能支持交互模式的命令行调试,没有如python那样能够在launch.json设置console属性[为externalTerminal]。
其它
做为区块链最普遍应用的数字货币已经再也不像不久之前同样可以随意撩拨投机者的神经,但这项技术在其它更实用的领域或许仍值得期待。好比区块链的共识机制、区块时间戳、防篡改特性,彷佛天生是为知识产权保护打造的,然而迄今为止市面上还没有出现让人眼前一亮的产品。前段时间看到一则新闻,说百度上线了一个保护图片版权的区块链项目“图腾”,有兴趣的同窗能够去了解下。若是我要实现相似的知识产权链,会考虑文件类似度判别、[使用代币]支付版权费及支付策略(买断or按次付款等)等等,交易媒介和交易标的都在链上,造成闭环。链上闭环可不受外部实体困扰,以区块链二代的明星特性“智能合约”为例,一旦与外部有所关联,就没法保证合约的事务完整性,可参看我以前的观点。
Tendermint里有不少ethereum的影子,好比gas、db的封装等,部分思路和代码应该是参考了ethereum的实现。
ethereum(以太坊)相关概念:
MPT:即Merkle Patricia Tree,是Merkle Tree 和Patricia Tree结合的产物。Patricia Tree又是Trie Tree的一种变化。参考资料:Trie原理以及应用于搜索提示,以太坊MPT原理,你最值得看的一篇。这两篇偏向于原理,若要了解具体细节,可看 干货 | Merkle Patricia Tree 详解。
叔区块
gas:一直很好奇以太坊是怎么作到计算实际使用gas量的,特别是有控制跳转语句的时候,最可靠的方式是实际运行时实时计算gas,那这个应该是由EVM实现的。具体可看 以太坊虚拟机及交易的执行,以太坊智能合约虚拟机(EVM)原理与实现。
数据结构与存储方式:以太坊源码情景分析之数据结构,[以太坊源代码分析] II. 数据的呈现和组织,缓存和更新
我的认为区块链目前广泛存在的问题:
更多资料:
以太坊源码深刻分析(7)-- 以太坊Downloader源码分析
转载请注明本文出处:http://www.javashuo.com/article/p-egfhuhoh-co.html