本节将讨论哈希指针(hash pointer)及其应用。哈希指针是一种数据结构,这种数据结构在我们即将讨论的很多系统中都很有用。简单来说,哈希指针是一个指向数据存储位置及其位置数据的哈希值的指针。一个普通的指针可以告诉你数据存储的位置,哈希指针不但可以告诉你数据存储的位置,并且还可以给你一种方式,让你验证数据没有被篡改过(见图1.4)。
图1.4 哈希指针
注:哈希指针是一个不但可以指向数据存储的位置,还可以明晰某个时间戳下该数据的哈希值的指针。
我们可以利用哈希指针构建各种各样的数据结构。为求直观,我们可以把原来用普通指针实现的数据链表和二叉查找树通过哈希指针来实现。
区块链
如图1.5所示,我们通过哈希指针构建一个链表,我们将这个数据结构称为区块链(block chain)。在普通链表中有一系列区块,每个a区块既有数据也有一个指向上一个区块的指针。而在区块链中,上一个区块指针被置换为哈希指针。因此,每个区块不仅能告诉我们上一个区块的值在哪里,还包含了该值的摘要,使我们能够验证那个值没有改变。我们存储链表头部(the head of list),即一个普通的哈希指针指向最近使用的数据区块。
图1.5 区块链
注:通过哈希指针而不是普通指针构建的一个链表,我们把这个链表称为区块链。
区块链的一个应用就是“防篡改日志”。也就是说,我们要建立一个存储很多数据的日志数据结构,使我们能将数据附加到日志的末尾。但是如果有人篡改日志前面的数据,我们可以检测到。
要理解区块链如何实现这一防篡改特性,我们先看一下如果对手要篡改区块链中间的数据会发生什么。具体来说,通过这种方式,对手的目的是让只记得区块链头部哈希指针的人无法检测到篡改行为。为达到这个目标,对手会改变某区块k的数据。既然数据已经被改变,区块k+1的哈希值(即整个区块k的哈希值)将不会匹配。记住,因为哈希函数具有碰撞阻力,我们可以确定新的哈希值与改变后的内容不会匹配。因此,我们会检测到区块k中的新数据以及区块k+1中的哈希指针的不一致性。当然,对手可以继续尝试,并通过篡改下一个区块的哈希值掩盖这次篡改。他可以一直这样做,但是当他到达链表的头部时,这个策略将会失败。具体来说,只要我们将链表头部的哈希指针存储在对手无法改动的地方,对手将不能做到在不被检测到的前提下,篡改任何区块(见图1.6)。
图1.6 防篡改日志
注:如果对手修改了区块链中的任意部位的数据,那么将会导致下一个数据块的哈希指针不正确。如果我们锁定区块链的头部数据,那么即使对手修改了所有哈希指针使其与修改过的数据一致,那么他也无法修改头部数据,从而我们就可以检测到篡改行为。
这样做的结果是,如果对手想要篡改区块链中任意地方的数据,为了保证整个内容一致,他需要篡改所有的哈希指针直至最开始的地方。他最终将碰到障碍,因为他不能篡改链表头部的指针。这样,我们便知道,仅通过记住一个哈希指针,我们就基本记住了整个链表的防篡改哈希值。因此,我们可以搭建一个包含很多区块的区块链网络,链表头部的哈希指针被称作创世区块(genesis block)。
你可能已经注意到了,区块链的结构与我们上一节见到的MD变换类似。的确,它们很相似,同一个安全论证对于两者都适用。
梅克尔树
另一个我们可以用哈希指针建立的有用的数据结构是二叉树。使用哈希指针的二叉树也叫作梅克尔树(Merkle trees),以其发明者拉尔夫·梅克尔(Ralph Merkle)的名字命名。如图1.7所示,假设我们有很多包含数据的区块,这些区块就构成了树的叶子(节点)。我们将这些数据区块两两分组,然后为每一组建立一个有两个哈希指针的数据结构,每个指针对应一个区块,这些数据结构就构成了树的下一个层次。我们轮流将这些区块组两两分组,为每一组建立一个包含每个区块组哈希指针的新的数据结构。以此类推,直到我们得到一个单一区块,即树根节点。
图1.7 梅克尔树
注:在梅克尔树的数据结构中,所有的数据区块都被两两分组,指向这些数据区块的指针被存储在上一层的父节点(parent node)中,而这些父节点再次被两两分组,并且指向父节点的指针被存储在上一层的父节点中,一直持续这个过程,直到最后我们到达树的根节点。
如上所述,我们要记住树最前面的哈希指针。我们现在可以通过哈希指针回溯到列表中的任何位置,这让我们能保证数据确实未经篡改,正如我们在区块链见过的一样,如果对手篡改了树底部的一些数据区块,会导致上一层的哈希指针不匹配,即使他继续篡改这个区块,改动数据行为将最终传递到树的顶端,而此时,他将不能篡改我们存储的哈希指针。因此,同样地仅仅通过记住顶部的哈希指针,任何企图篡改任何数据的行为都会被检测到。
隶属证明
与我们之前建立的区块链不同,梅克尔树的另一个特点是它可以实现简洁的隶属证明。假设某人想要证明某个数据区块隶属于梅克尔树。同样地,我们只记住树根节点,然后他需要展示给我们数据块信息,以及从该数据区块通向树根节点的那些区块,我们可以忽略树的其余部分,这样做是因为这些区块已经足够让我们验证通往树根节点过程中所有的哈希值。其工作原理图解参见图1.8。
图1.8 隶属证明
注:为了证明某个数据区块来自一个梅克尔树,我们只需要找到该数据区块到树根节点的路径。
如果整棵树上有n个节点,只需要展示约log(n)个项目,因为每个步骤仅需要计算子区块的哈希值,验证过程需要时间约为log(n)。因此,即使梅克尔树包含大量的区块,我们仍可以在相对较短时间内证明隶属关系。因此,验证需要花的时间和涉及空间(树节点)与log(n)同级。
一个排序梅克尔树是把底层的数据通过某些排序得到的梅克尔树,这里排序规则可以是字母表排序、词典排序、数字化排序,或者其他约定的排序方式。
非隶属证明
有了排序梅克尔树,我们可以在一个对数复杂度的条件下验证某一个数据区块并非来自某梅克尔树。也就是说,我们可以证明某个特定区块不属于梅克尔树,而我们只是简单通过展示被验证区块之前的区块路径,以及被验证区块之后的区块路径,就可以达到目的。如果之前、之后两个区块在树上是连续的,那么这说明了被验证区块与该梅克尔树之间是非隶属关系。因为被验证区块确实隶属于梅克尔树,它需要在两个条目之间,而如果两个条目是连续的话,二者之间则并没有空间。
我们讨论过在链表及二叉树中使用哈希指针,但更广泛地说,我们可以在任何以指针为基础的数据结构中使用哈希指针,条件是数据结构不存在循环。如果数据结构中存在循环,那么我们将不能使所有哈希值得到匹配。想一下,在一个非循环的数据结构中,我们可以在靠近节点的地方开始,或者在没有指针的数据区块开始,计算其哈希值,然后从后往前进行计算。但是在一个有循环结构的网络中,并没有一个根节点,可以让我们去追溯。
因此,试想另一个例子,我们可以建立一个哈希指针定向的非循环图。
我们能够在该图中非常有效地验证隶属关系,同时也方便计算。这样的哈希指针使用方式是一个常见技巧,在分布数据结构中、在本章后面会讨论到的算法中以及本书中都会反复提到。