前言

从基础的链表到复杂的多叉树等,我们在接触到这些经典的数据结构时,一般都是直接去学习其内部数据组成,了解其功能,然后范范的去使用,然后感觉数据结构是一门非常复杂、神秘、高深的课程。
这些认知当然是有道理的,不过我们换个角度来思考一下,这些精妙的数据结构,一开始是怎么设计的?为什么要定义这些字段?以最简单的单向链表为例,其实质只不过是在其成员中加了一个名为“next”的变量,它设计之初,很可能就是仅仅因为一个需求:我想知道下一个数据是什么。如果是面对这样的需求,那绝大部分的小伙伴都能很自然而然的想到定义一个“next”变量,然后这个时候再新增一个需求:我还想知道我上一个数据是什么,那同样我们也会很自然的想到去定义一个“before”变量。看,就是简单的2个很基础的需求,我们就很自然的已经完成了一个双向链表的设计。所以,很多时候,数据结构设计是非常简单的,用简明的数据成员解决需求,这就是一份好的数据结构设计。为了更好的理解这个感悟,我们这篇文章就以区块链为例,从数据结构设计的角度,来一点点的剖析它。本文选说的区块链是基于比特币而言。

区块链的基本定义

什么是区块链?本文我们暂将它定义为一个去中心化的公开账本。很好,这又引用了2个新的概念,我们对其给出解释:

  • 公开账本
    账本其就是记录数据的,或者说其就是一个记事本,任何参与者都能完全的查看这个记事本的内容,并且要保证每个参与者看到的内容都是完全一致的。

  • 去中心化
    是指该账本不能存在中心集权化的单位,不能是由某个单位就能决策该账本的数据,必须是所有的参与者都能公平的参与到账本的改动,这样,该账本对于每个参与者才是公平的。

一步步设计区块链

由以上的区块链的定义,我们可以得知区块链首先是一个账本,其需要记录所有的交易记录,这个账本需满足如下的基本需求

  • A: 账本数据量巨大,并且会持续更新,变大
  • B: 新的交易必须有时效性,要尽快的给到响应
  • C: 每个交易一旦被记录,不可更改,并且能同步到所有用户
  • D: 每个参与者得到的交易记录都是一致的,并且能够有效的验证

分块存储

面对以上需求,尤其是需求A,要存储大份量的数据,很自然的就会想到要将数据分开存储,以时间为基础顺序,将一定时间内的数据存在为一个块,一点点的累加即可,为了保证块与块直接的连续性,我们每个块,都需要保留对上一个块的引用,这样就将所有的块串链起来,形成完整的数据链,并且可以持续扩展。这么一块,区块链的模型,是不是已经出来了,非常简单对不对。这里我们给出单个数据块的基础数据结构

// 块结构 版本1class Block {time; // 当前区块创建的时间戳blockNum; // 当前区块的编号,从1开始自增,也代表当前块的深度priviousBlock; // 上一个区块的引用Tx[]; // 当前块的交易记录,Tx表示交易,}

压缩数据

然而,上面的方案其有一个本质的问题没有解决:整个完整的链仍然非常大,并没有将需要存储的数据减少,仅仅只是分块而已。参与者需要验证自己的账本是否可信,仍然需要用比完整的链,一条条的对比数据。要解决这个问题,这里我们需要再次思考,参与者真正的需要知道的是什么?是完整的每个人的交易记录,或者只是自己相关的交易?很显然,参与者只是想知道自己的交易,并确定自己的交易是有效的而已。换句话说:区块链不需要总是提供完整的数据,其只需要能提供一个数据验证即可。那什么技术可以很轻便的做到对大数据的验证呢?多数程序小伙伴应该都能很自然的想到:Hash1(哈希)。我们记录当前包含n个交易的完整账本的哈希值S(n),然后每新增一个交易T(n+1),就结合之前的哈希值S(n),再哈希一次,得到S(n+1),其就是最新的当前完整账本的验证,示意代码如下:

// S(n)表示包含了前n个交易记录的哈希值// T(n)表示第n次交易记录S(n + 1) = Hash(S(n) + T(n + 1))

以上方案确实能解决所有交易记录数据量大的问题,但另外一个问题:其是怎么证明某次交易的有效性,比如为了验证第k次交易的有效性,我们需要从k + 1 开始,到最后的第n次之间的所以交易依次进行Hash校验,以确定最后的结果是否和当前的记录S(n)相等,这种O(n)复杂度的验证方式,随着交易次数变多,校验成本是等比例增加的。那这个要怎么优化呢?
大家有没有发现,刚刚提到的验证过程,很像一次线性的遍历过程,对快速排序算法等有一定经验的小伙伴会比较自然想到,其复杂度往往可以优化到O(log2 N),最常用的就是二分思想,尽量用常量缓存一些可复用的计算过程。有思路的小伙伴可以按照自己的方案去设计实现,这里直接列出区块链的解决方案:Merkle trees(梅克尔树)2

  • 用二叉树的数据结构,将所有交易分别存在末端的叶节点上
  • 从叶节点往上,每个父节点,存储子节点的Hash值
  • 最终二叉树的根节点即是所以交易记录的Hash验证

示意图如下

那么其是怎么做到优化验证过程呢,以下图的示例情况来解释


如上图左半部分所示,我们需要知道底部Alice->Bod的交易记录,只需要获取图中蓝框所示的节点的Hash数据即可,经过log2n次的计算,就能得到根节点的Hash,并进行对比。
如上图右半部分所示,如果有用户修改交易,其经过上述流程之后,得到的根节点Hash就是错误的。这样,我们就将校验过程的算法复杂度优化到O(log2 N)
有了以上方案,我们就可以将区块的关键数据提取出来,叫做区块头,它包含能代表、验证这个区块的关键数据,类似具体交易列表等不是所有人都关心的数据,就不需要每次都获取,这样大家更新验证账本的时候就会更简便快捷。
至此,我们对区块链的超大交易数据的存储和有效性验证有了初步解决思路,是时候更新我们的数据结构

// 块的头部数据,存储对应区块的关键信息,便于验证交易有效性class BlockHead {time; // 当前区块创建的时间戳blockNum; // 当前区块的编号,从1开始自增,也代表当前块的深度priviousBlockHash; // 上一个区块的引用,通过Hash实现,只为了校验上个区块的有效性即可txHashRoot; // 交易列表的哈希根,即Merkle trees的根,用于验证交易的有效性}// 块结构 版本2class Block {head; // 区块的交易头,类型为BlockHead,Tx[]; // 当前块的交易记录,Tx表示交易,}

记账成本

这里先强调一个前提认知:一个可信的区块链,链越长,越可信。换个通俗的说法,公开的账本,哪个版本记录的信息越多,越有效。
一个账本,最重要的就是要持续的记账,一个公开的账本,更是谁都可以在上面记账,也因为此,出于各种目的,肯定会有人胡作非为。比如:A要转账10个币给B,那A可能就想记成只转1个,B却会想记账成对方转100个。按照上面的认知,只要他们谁记录的越多,越快,谁就是可信的。
那这就引出另一个思考:新增的区块,到底谁的数据说了算呢?怎么能既保证每个参与者的公平性,又保证整个区块链的长期稳定的发展呢?又保证每个参与者需要付出实际的代价呢?大家也可以思考一下,这里列举简单的常用规则:

规则
按照权益,资源占比随机能更好的保证权益稳定,鼓励参与者持续付出权益会越来越集中,最后越来越中心化
每个参与者均等随机每个参与者的机会均等新增权益不稳定,参与者可以随机造假,不利于长期投入者

虽然没有完美的解决方案,但是不难分析得到,每个参与者,必须要付出一定的代价才行,可不能零成本的胡作非为,不然大家都疯狂的随意记账,最终这个账本完全缺乏可信性。且其付出的代价最好不跟区块链本身的权益有关联,这样就不会出现权益中心化的情况。
举个通俗的例子:有A、B两个成年人,他们对梁朝伟和刘德华谁更帅,各执一词,这时候我们该相信谁?切记,这时候,我们不能去分析A、B两人谁以前更有威信,谁的信用积分更高,谁看的电影更多,而是让他们都数数,从1数到10000,谁先数完,就听谁的。这个方案是不是非常不合理?但是,这个规则对于多数情况,却是合理而公正的。从1数到10000,每个人从理论上都能做到,但是却需要付出自己的实际的工作量,这样既保证勒参与的公平性,又不是谁都可以超低成本的参与。并且该规则没有根据以往的数据,觉得A是一直可信的人,就直接认为A对B错,如果这样,A就很容易徇私舞弊,而且大家还在一个劲的相信他,这将会导致A以后可以更是胡作非为,而大家却反而越来越相信他,显示会进入恶性循环。
有了以上例子的思考,回到区块链中来,我们有没有类似的技术方案,让每个参与者只要付出适当的都能承担得起代价,就能参与新区块的生成呢?哈哈,让我们再次回想哈希。
我们都知道,哈希返回的字符串是固定长度的随机的字母或数字。而且单次的哈希运算其本身是门槛、成本都非常低的,基本有计算能力的硬件都能参与计算,就像数数一样,数一个1,大家是不是有嘴就行,都能参与。那怎么解决成本过低的问题呢?就让参与者计算10000,1000万次哈希吗???那岂不是谁硬件好就越快,最终还是变成只有少部分人掌握勒新区块的生成权?所以,我们还得增加计算的随机性,我们再增加一个规则,计算出来的哈希值,前X位字符,都必须是‘0’。我们都知道,哈希值的每一位字符都是完全随机的,假设哈希值为64位字符,则每一位有64(26)种可能,暂定X为10,那为了满足条件,需要进行6410(260)次计算,而且每个参与者的每一次计算其实都是随机的,这样也使得计算过程充满了不确定性,而不是性能好的设备总能先计算出结果。另外,计算次数的期望值,完全可以通过调整X来动态调整,这也能保证新区块的生成难度是可选择、可调整的。
有了以上思路,具体实施起来,我们只需要在区块头上面,加一个随机数nonce,每次去进行哈希计算的时候,都调整这个随机数,这样整个区块的数据也会变,也会导致得到区块哈希值是变化的,直到找到区块哈希值前X位为0的结果,则其对应的nonce,就是我们的所要的值,这个过程,其实就是所谓的‘挖矿’。至此,我们再次更新我们的数据结构

class BlockHead {time; // 当前区块创建的时间戳blockNum; // 当前区块的编号,从1开始自增,也代表当前块的深度priviousBlockHash; // 上一个区块的引用,通过Hash实现,只为了校验上个区块的有效性即可txHashRoot; // 交易列表的哈希根,即Merkle trees的根,用于验证交易的有效性nonce; // 随机数,以保证当前区块的哈希值满足前X位为0}...

为什么要挖矿

前面的内容可能稍显专业,现在可以讨论一个很重要但比较好理解的需求:为什么我们要参与到新区块的生成,即“挖矿”?!
这个问题很简单,利益驱动,“不就是钱吗?给!”我们只需要给新区块的所有者,奖励一定量的利益即可。其实由区块链发展出来的各种币,也是基于这个逻辑。区块链中,用户的钱包,其实是对应一个地址,这个地址是一个26位到34位字符的长串。比特币在每个区块新生成的时候,就会往这个区块的所有者的钱包发送一定量的比特币。所以,驱使用户去挖矿的,只需要提高给区块所有者的奖励的价值即可,可以是发送的货币的数量变多,也可以货币对应的实际单价变高。
这里最好强调一个点,挖矿给的奖励一定要平等随机,不可由某些特殊单位才能参与挖矿,因为区块链的第一核心价值,就是去中心化,一定是要鼓励所有人都参与其中,而不是只有少数人来操控。

如何记账

区块链的一个比较明显的短板,就是交易的tps(Transactions Per Second每秒传输的事物处理个数)过低,而且交易具有延后性。我们简述下原因,当由大量用户往链上发生交易请求的时候,其数据量很可能原因大于一个区块所能承受的数据量大小,另外,区块的生成,都是挖矿者去通过一次次哈希碰撞来试运气产生的。当其进行哈希碰撞时,对应区块的交易列表应该固定好,不然交易列表和随机数nonce两者都动态变化,即增加勒单次碰撞的准备时间,也会降低命中概率。
这就解释勒交易是有延时性(顺便提一下,在区块链中,一个交易被一个区块记账后,还需要再等6个新区块的生成,才算真正有效)。交易的延时性,越会导致交易的堵塞,所以,当挖矿者再准备打包区块的时候,他往往是需要挑选部分交易进行打包,不能把所有堆积的交易都打包。这就引申出一个问题,挖矿者要挑选交易的规则是什么?

当个用户的交易必须有序

为了简化逻辑,我们假设交易类型就是货币的转发,那单个用户的交易顺序是非常重要的。如一个用户A当前只有100个币,其需要先给用户B发送50币,成功,然后再给用户C发送80币,失败。如果挖矿者无视用户的交易顺序,先给用户C发送80币,成功,再给用户B发生50币,失败,会造成非常明显的逻辑错误。这个其实很好理解,同时非常简单,给用户的每次交易,附带一个对应该用户的自增的序列号即可,挖矿者必须按照用户的序列号,顺序处理交易。

利润越高的交易越容易被挑选

这个规则,也非常好理解,在区块链中,这个利益被定义成gas。用户可以指定一定的手续费给到挖矿者,以激励其将自己这笔交易优先记账,反过来,挖矿者也需要从众多的交易列表中去找寻足够多的、有更多gas的交易,将其记账到交易列表,这个行为是不是也很像“挖矿”?!!!
当然,实际的区块链的设计比我们说到的要负责一些,其涉及到底层货币的产生机制,而且第二代的支持智能合约的区块链,如以太链,其gas费的机制又不大一样,其是用户指定交易所需的最大执行步骤,以及执行每个步骤所需花费的单价,来更细化的指定gas,这里就不过多展开,个人认为这些做法其实已经脱离区块链本身的设计理念,更多是因为实现技术的软硬件限制导致的解决方案而已。大家能理解gas费这个概念即可。

总结

本文主要是从数据结构设计的角度,通过非常通俗易懂的文字,来让更多人认知区块链,并能了解其底层的一些技术设计。其用到的数据压缩,数据简便验证,工作量证明等技术解决方案,在类似的问题上,都是可以借鉴的。本文可以避开了“挖矿”,“Gas”,“货币”等等便金融性的字眼,主要也是希望大家更多的停留在技术本身的关注上。当然成熟“商业化”的区块链,远不止本文所说的这么简单,比如随机数的确定、节点的全球同步等等,其真正的数据结构跟本文给到的也不一样,本文给到的数据结构更多是服务于原理讲解,想知道区块链背后更多的技术细节,需要大家一起继续保持探索。
说回数据结构设计,回顾上文内容,我们会有感觉,数据结构的设计有以下两个很基础而又必要的过程:

  • 尽早的充分的剖析需求
  • 丰富的知识储备,对具体的、基础的需求有解决方案

我们也是一步步从理解、剖析需求开始,一步步的从因到果的剖析区块链的数据为何这样设计。以上也是个人的简单思考,希望各位小伙伴多多补充交流。

参考资料

以太坊白皮书

注释


  1. 哈希:把任意长度的输入通过散列算法变换成固定长度的输出。非专业人士也可以这样理解:任何长度的数据,都可以通过用一个固定的非常短的标签去表示,并且这个标签是独一无二的 ↩︎

  2. 梅克尔树 ↩︎