区块链和区块链应用这两个概念是不一样的。对于一个纯区块链而言,只需要保证它拥有三个要素:1.链表结构的区块存储结构设计,2.点对点网络,3.加密技术保证不可伪造。但是对于一款应用,特别是现代的应用,这三个要素完全不够。对于一个应用,还要考虑:1.用户(账号)体系,2.鉴权体系,3.业务数据。因此,我们这篇文章要来探讨一下,一个区块链应用应该如何设计它本地的数据库存储结构。
选择底层数据库
对于区块链应用而言,有两个部分的数据:区块链数据和应用状态数据。以比特币为例,区块链数据中保存着历史交易数据,而这些数据包含了上述提到的那几个方面,除了历史交易数据,比特币客户端(应用)还需要保存没有被写入区块链的一些数据,比如过去一段时间发起的新交易。比特币的区块链数据被存储在很多.dat文件中,它采用的数据结构我至今没有搞懂,阅读了很多材料,都没有明白比特币raw data使用的是什么技术,不过市面上已经有很多软件可以直接解析比特币区块链数据,所以读取比特币区块链也不是什么问题。而比特币的这些.dat区块数据不是以我们熟悉的数据结构存储的,所以在查询数据的时候,无法像现代数据库一样通过查询语句或方法快速查询。为了解决验证某个交易的速度,比特币采用了leveldb作为key-value数据库保存着整个区块链的索引,以保证可能通过各种key快速找到需要的数据。另外,那些还没有写入区块链的chainstate数据也是用leveldb保存在另外一个库中。
那么对于非币应用,或者代币应用,怎么来搞自己的数据库呢?
上面你可以看到,实际上,一个区块链数据库结构里面包含三个(或更多)部分:1.只能执行插入和查询动作的块链数据库,2.块链数据的索引,3.chainstate数据,这个数据是可变的。而怎么把账号体系、业务体系等数据加入进去呢?很明显,账号体系、鉴权体系、业务体系的数据全部要入链。在p2p同步时,只有block数据被同步,其他数据都是通过网络广播等方式传递和接收的,索引数据在同步时创建。
用sql数据库保存区块链。
我们抓住区块链表的特征,即下一个区块保存着上一个区块的hash,同时通过merkle算法保存了区块内容的hash。所以,我们其实很好利用sql数据库来保存block数据,而且,sql数据库可以自定义索引,所以,索引数据库都省了。因此,我们的应用里面,可以最终总结为两类数据:块链数据、chainstate数据。其中块链数据使用sql数据库保存,只具备insert和select的权限。而chainstate使用kek-value数据库保存,读取效率更高。
用sqlite和leveldb作为底层数据库。
sql数据库和key-value数据库都有很多很多,比如orcal、redis等等,但是我们开发一个区块链应用,常常需要保证把数据放在客户端本地,而非服务器,所以,这个时候,数据库必须和应用代码一起发布。当然,如果你能强制用户在安装你的应用之前安装一个本地数据库服务,也是可行的,比如微软的很多软件,就要求你事先安装.net framework。不过这种需求显然体验感不是很好,除非是企业级用户。
sqlite是非常轻量级的sql数据库,最重要的是,它是基于文件的,而非内存的,它的所有数据被保存在一个.db文件里面,而且不依赖任何服务。也就是说,你可以将这个数据库打包进自己的应用,而不用要求用户在安装你的应用之前再安装一个其他什么数据库了。不过当然,sqlite的性能在遇到大数据量的时候是不怎么理想的,因此,我们需要通过各种优化手段和架构来控制单个数据库的数据量,采用分库等方式,提高后期可能的查询能力。
leveldb和sqlite一样,也是基于文件系统的,可以被打包进应用。它具备非常高的写性能,由于设计简单,它的读性能也完全不输给内存级的key-value数据库。当然,因为它的设计简单,它没有分库等功能。我们需要通过合理的控制key的前缀,来保证我们可以通过有一定规律的key来找到想要的数据集合。
数据库表结构
我们以一个博客系统为例来进行分析,毕竟基于数据库技术的应用千变万化,结构不可能只有一种。但是,我们通过这个案例,可以分析出,区块链应用怎么在业务、用户、块链三者之间建立“不可篡改”的存储数据。
首先是区块表(block table)。
一个区块的表,需要记录这个区块的各种信息,特别是区块头信息。我的设计如下:
字段名 | 类型 | 长度 | 备注 |
version | string | 8 | 客户端的版本 |
block_hash | string | 128 | 区块hash |
previous_block_hash | string | 128 | 上一个区块hash |
timestamp | number | 16 | 挖矿时间戳 |
diffculty | number | 32 | 挖矿难度 |
nonce | number | 16 | 挖矿随机数 |
merkle_root | string | 128 | 业务数据的merkle root hash |
这就是一个区块链的区块表,这些是基础字段,作为一个应用,可以在这个基础上进行扩展,但是应该遵循一个规则,就是尽可能的保证存储的数据更少,能够通过计算得到的数据,就不要设计在sql数据库里面去,可以考虑放在key-value数据库里面去。要知道,当一个应用运行N年以后,即使看上去这么几个字段的表,也会多到爆炸。
业务数据表。
下面是业务数据,也就是我们这里假设的博客的内容。对于一篇博客,我们必要的字段其实不多:
字段名 | 类型 | 长度 | 备注 |
version | string | 8 | 客户端的版本 |
article_hash | string | 128 | 文章hash(相当于文章ID) |
article_title | string | 128 | 文章标题 |
article_timestamp | number | 16 | 文章时间戳 |
article_content | longtext | 文章内容 | |
article_author | string | 128 | 撰写文章的用户hash |
block_hash | string | 128 | 这篇文章所在的区块hash |
你看到block_hash可能会觉得奇怪,没错,在插入文章数据前,block_hash已经被计算出来了。
既然是博客系统,那么评论也是必须的:
字段名 | 类型 | 长度 | 备注 |
version | string | 8 | 客户端的版本 |
comment_hash | string | 128 | 评论hash(相当于评论ID) |
comment_article_hash | string | 128 | 这条评论是属于哪一篇文章的 |
comment_parent_hash | string | 128 | 这条评论是回复给哪一条评论的,默认为0,表示不是回复给评论,而是直接评论给文章 |
comment_timestamp | number | 16 | 评论时间戳 |
comment_content | longtext | 评论内容 | |
comment_author | string | 128 | 评论的用户hash |
block_hash | string | 128 | 这条评论所在的区块hash |
上面就是区块链的数据库表了。
等一等,用户数据呢?难道你都不把用户数据保存下来,这顶什么用?难道界面上直接显示用户的hash就算了?别慌。用户数据属于加密体系的一部分,不会被记录到区块链中,而是记录在chainstate中,你想想,用户难道还不要经常换换头像、改改昵称什么的么?而且,我们可以通过用户的密钥来对用户的权限进行验证。这下面的部分再详细阐述。
总之,对于一个博客系统,就上面这些字段就够用啦。但是,要保证区块链数据的不可篡改性,我们还要做一些设计。
merkle_root = sha256(sha256( article_hash1 + article_hash2 + ... comment_hash1 + comment_hash2 + ... ))
在验证一个数据是否被改动时,利用merkle的验证算法,从区块链里面查询出对应的数据进行merkle验证就可以知道是不是有问题了。
chainstate数据结构
前面提到我们用key-value数据库保存chainstate数据,我们尽量让key-value更加扁平,不要做多层嵌套。现在我们来看下用户数据:
"author_03a89fc89aa9c7b7..." => { "nickname": "tony", "publicKey": "a8fcdc982eda..", "avatar": "data:image/png;base64,...", "description": "I'm a clear boy." }
而一个用户的hash是通过该用户的公钥经过加密得到的,要验证该用户的真实性,只需要用他的公钥加密某个随机数,让他用私钥解密进行验证即可。
另外,有很多发布了但还没有记录到区块链里面的文章,等着被矿工写入。为了防止矿工篡改用户发布的内容,用户发布时,仅广播内容的hash,原始内容经过加密,矿工仅利用这些hash进行挖矿,挖矿成功后才能解锁原始内容:
"block_0ca452ced99..._merkle_hashes" => [ { "author_hash": "03a89fc89aa9c7b7...", "article_hash": "0034cd00e37...", "article_raw_script": "xxx..." }, ... ]
评论数据也是一样,它们被暂时保存在chainstate里面,等到区块生成之后,这些临时数据就可以从chainstate里面清除。
总结
本文从一个区块链应用的底层数据库出发去看区块链应用。当然,这里面还有很多细节,需要在开发中去解决,这里只是提供了底层数据库选择和设计的一个思路。在一些具体问题上,比如共识机制,比如如何刺激用户贡献内容等等,都是一个待解决的问题。