Ethereum Foundation Blog
https://blog.ethereum.org/2015/11/15/merkling-in-ethereum/
以太坊的一篇博客对MT做了一个简单的描述:
Merkle trees是区块链的基本组成部分。虽然理论上完全可以不使用Merkle trees来构建区块链,直接通过创建巨大头部来处理每一笔交易,但这样做会带来巨大的可扩展性挑战,从长远来看,除了最强大的计算机之外,其他所有计算机都无法可靠地使用区块链。
由于有了Merkle trees,我们就有可能在所有大大小小的电脑和笔记本电脑、智能手机、甚至物联网设备上构建以太节点。
最简单的Merkle Tree的形式是下图展示的这种二叉树。每个节点有两个孩子, 叶子节点是数据的哈希值。
Merkle Proofs
这种结构可以提供一种叫Merkle Proofs的机制。使用默克尔证明能够实现轻节点的扩展。
默克尔证明指的是一个轻节点向一个全节点发起一次证明请求,询问全节点完整的默克尔树中,是否存在某个指定的节点;全节点向轻节点返回一个默克尔证明路径,由轻节点进行计算,验证存在性。
首先Merkle树的根是公开的、受信任的。Merkle Proofs包含三部分: 一个待验证的块数据的哈希(如图中的9Dog:64), 一个根哈希(如图中的6c0a)以及验证路径(图中黄色部分: 1FXq:18, ec20, 8f74)。
具体的验证过程如图所示:一颗 merkle tree,如果某个轻节点想要验证图中绿色的这个节点是否存在,只需要向全节点发送该请求,全节点会返回一个如图中黄色块显示的路径。得到路径之后,轻节点依次求哈希,最后将得到的结果与本地维护的根哈希相比,判断其是否相等。
Merkle Proofs in Bitcoin
Merkle Proofs最早应用在比特币中, 如图所示比特币用所有交易的哈希构造了一颗Merkle Tree, 而Merkle Tree的根哈希写在区块头中:
这种设计可以支持SPV(简单支付验证): 当验证一笔交易的时候,它不需要下载所有区块和交易信息, 只需下载80字节左右的区块头就可以了。区块头包含:前一个区块头的哈希值、工作量证明nonce以及 该区块中所有交易组成的Merkle Tree的根哈希和一个时间戳。
如果客户端想验证一笔交易的可靠性, 只需要按照前面说的Merkle Proofs过程提供交易哈希和路径, 再经过一系列哈希运算后比对根哈希就可以了。
这样客户端避免了下载所有区块数据进行交易验证的噩梦, 我们称这种客户端为轻客户端。
Merkle Proofs in Ethereum
以太坊中的区块头包含三颗Merkle Tree, 分别是: 交易树, 单据树, 状态树。
这种设计使得它能处理一些比特币不能处理的问题 。比如:可以查看账户信息等等。
它的Merkle Proofs证明过程和比特币是一样的。也是提供路径,经过哈希计算,比对跟哈希值是否相同。
MT
区块链中的一个比较重要的概念就是hash指针。
Hash Pointers:与普通的指针不同的是它不仅要保存结构体的地址还要保存结构体的hash值。然后从这个Hash指针不仅能找到这个结构体的位置还能检测这个结构体是否被篡改。区块链就是使用Hash指针代替普通指针。
用hash指针来实现链表就是区块链;用hash指针来实现二叉树就是默克尔树。MT是Hash List的一个泛化。
Hash List
Hash List:当传输一个大的数据时可以将其分成多个小的数据块,为了判断每一个小的数据块在传输过程中没有损坏。在得到真正的数据之前,会得到一个哈希列表。
把每个小块数据的Hash值拼到一起,然后对这个长字符串在作一次Hash运算,这样就得到Hash列表的根Hash。传输数据的时候,首先从可信的数据源得到正确的根Hash,就可以用它来校验Hash列表了,然后通过校验Hash列表来校验数据块是否被篡改。
Merkle Tree
Merkle Tree就是在Hash List上作了相应的改进。他的最下面一层是也是数据块或者是交易,内部节点还是Hash指针 。但是不同的是它不是直接将所有的hash值拼到一起去运算根哈希,而是把相邻的两个哈希合并成一个字符串,然后运算这个字符串的哈希,得到一个“子哈希”。如果最底层的哈希总数是单数,就会有一个哈希值不用合并直接进行哈希运算。依次往上推,最终能得到一棵Merkle tree,树根处只有一个根哈希,记为Merkle Root。
在P2P网络下传输文件之前,先从可信的源获得文件的Merkle Root,就可以从其他不可信的源获取Merkle tree。通过可信的Merkle Root来验证Merkle tree是否损坏或者是虚假的。
Merkle Tree和Hash List的主要区别是,可以直接下载并立即验证Merkle Tree的一个分支。因此merkle tree可以一次下载一个分支,然后立即验证这个分支,如果验证通过,就可以下载这个分支。而Hash List是只有下载整个Hash List才能验证。MT与binary tree的区别就是使用Hash指针代替了普通指针。
Merkle Tree的主要特点
快速重哈希
就是当树节点内容发生变化时,能够在前一次哈希计算的基础上,仅仅将被修改的树节点进行哈希重计算,便能得到一个新的根哈希用来代表整棵树的状态。
轻节点扩展
采用默克尔树,可以在公链环境下扩展一种“轻节点”。轻节点的特点是对于每个区块,仅仅需要存储约80个字节大小的区块头数据,而不存储交易列表,回执列表等数据。然而通过轻节点,可以实现在非信任的公链环境中验证某一笔交易是否被收录在区块链账本的功能。这使得像比特币,以太坊这样的区块链能够运行在个人PC,智能手机等小存储容量的终端上。
Merkle Tree的相关操作
Merkle Tree创建:
step1:(红色线)对数据块做hash运算,Node0i = hash(Data0i), i=1,2,…,9
step2: (橙色线)相邻两个hash块串联,然后做hash运算,Node1((i+1)/2) = hash(Node0i+Node0(i+1)), i=1,3,5,7;对于i=9, Node1((i+1)/2) = hash(Node0i)
step3: (黄色线)重复step2
step4:(绿色线)重复step2
step5:(蓝色线)重复step2,生成Merkle Tree Root
检索
检索错误节点的过程如图所示:
Step1. 首先比较v0是否相同,如果不同,检索其孩子node1和node2.
Step2. v1 相同,v2不同。检索node2的孩子node5 node6;
Step3. v5不同,v6相同,检索比较node5的孩子node 11 和node 12
Step4. v11不同,v12相同。node 11为叶子节点,获取其目录信息。
Step5. 检索比较完毕
MPT(Merkle Patricia Tree)
Tire
Trie是一种字典树,用于存储文本字符,并利用了单词之间共享前缀的特点,所以叫做前缀树。如左图所示,Trie的高度只与最长的文本串的长度s有关系,而与单词的数量n无关。Trie的节点分两种:内部结点和叶子结点,内部结点用来存储单词的共享前缀字母,叶子结点存储该单词的数据内容(data)。但内部内部结点和叶子结点并不是互斥的,一个内部结点本身可以有叶子结点,同时它也可以是一个叶子结点。
3个基本性质:
1、根节点不包含字符,除根节点外每个节点都只含一个字符
2、从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。
3、每个节点的所有子节点包含的字符都不相同。
Patricia Trie
压缩前缀树:是一种更节省空间的Trie。对于树的每个节点,如果该节点是唯一的儿子的话,就和父节点合并。
MPT
Merkle Patricia Tree(又称为Merkle Patricia Trie)是一种经过改良的、融合了默克尔树和前缀树两种树结构优点的数据结构,是以太坊中用来组织管理账户数据、生成交易集合哈希的重要数据结构。
MPT树中的节点包括空节点、叶子节点、扩展结点和分支节点。
1、空节点(NULL):简单的表示为空,在代码中就是一个空串。
2、叶子节点(leaf):2元组[key,value]。第一个字段是剩下的Key的RLP编码。第二个字段是value值。
3、拓展节点(extension):也是[key,value],但是这里的value是其他节点的hash,通过这个hash链接到其他节点。
4、分支节点(branch):MPT中的key被序列化成一种特殊的16进制编码,在加上最后的value,所以分支节点是一个长度为17的list,前16个元素对应着key中的16个可能的十六进制字符,如果有一个[key,value]对在这个分支节点终止,最后一个元素代表一个值(例如,有三个key,分别是(abc,abd,ab)第17个字段存储了ab节点的值)即分支节点既可以搜索路径的终止也可以是路径的中间节点。
MPT树中另外一个重要的概念是一个特殊的十六进制前缀(hex-prefix, HP)编码,用来对key进行编码。因为字母表是16进制的,所以每个节点可能有16个孩子。因为有两种[key,value]节点(叶节点和扩展节点),它们的存储结构是完全相同的,所以引进一种特殊的终止符标识来标识key所对应的是值是真实的值,还是其他节点的hash。如果终止符标记被打开,那么key对应的是标准叶子节点,它的值是真实的value。如果终止符标记被关闭,那么值就是用于在数据块中查询对应的节点的hash。
如图所示是以太坊中的一个世界状态,假定有四个账户,那么他就对应了如图所示的一棵MPT。首先依据压缩前缀树的特性,判断的key的公共前缀,就是图中的第一个拓展节点a7,然后依据HP编码规则,给拓展节点加一个标记符。图中对标记符做了相应的定义,一个半字节nibble被加到key前(上图中的prefix),对终止符的状态和奇偶性进行编码。最低位表示奇偶性,第二低位编码终止符状态。可以看到如果key长度是偶数,就会再加上另外一个值为0的半字节,保持整体的偶特性。拓展节点下面接的是一个分支节点,如果某个账户的key刚好到分支节点结束,那么它的值就直接存储到分支节点的value域中,如果没有就继续向下寻找公共前缀。可以看到1355这个账户和其他账户没有公共前缀,所以它直接被定义为一个叶子节点。有公共前缀就继续之前的过程,直到所有的账户都被写入MPT中。这样一颗MPT就被构造完了。
MPT的相关操作
检索
首先将需要查找的key转换为十六进制拓展编码,得到的内容就是搜索路径。从根节点开始搜索与搜索路径内容一致的路径:然后分几种情况
1)若当前节点为叶子节点,存储的内容是数据项的内容,且搜索路径的内容与叶子节点的key一致,则表示找到该节点;反之则表示该节点在树中不存在。
2)若当前节点为扩展节点,存储的内容是另外一个节点的引用,且当前节点的key是搜索路径的前缀,那么就将将搜索路径减去当前节点的key,将剩余的搜索路径作为参数,对其子节点递归地调用查找函数;若当前节点的key不是搜索路径的前缀,表示该节点在树中不存在。
3)若当前节点为分支节点,若搜索路径为空,则返回分支节点的存储内容;反之利用搜索路径的第一个字节选择分支节点的孩子节点,将剩余的搜索路径作为参数递归地调用查找函数。
上图是一次查找key为”cat“节点的过程。
将key"cat"转换成16进制前缀编码[3,15,3,13,4,10,T] (在末尾添加了也该终止符T,来判断终止);
当前节点是根节点,且是扩展节点,其key为3,15,则递归地对其子节点进行查找调用,剩余的搜索路径为[3,13,4,10,T];
当前节点是分支节点,以搜索路径的第一个字节内容3选择第4个孩子节点递归进行查找,剩余的搜索路径为[13,4,10,T];
当前节点是叶子节点,且key与剩余的搜索路径一致,表示找到了该节点,返回Val为“dog”;
插入
插入操作是基于查找操作完成的,插入过程如图所示:
1、首先找到与新插入节点拥有最长相同路径前缀的节点
2、若该节点是分支节点:
1)剩余的搜索路径不为空,那么就将新节点作为一个叶子节点插入到对应的孩子列表中;
2)如果剩余的搜索路径为空(完全匹配),则将新节点的内容存储在分支节点的Value中;
3、若该节点为叶子/扩展节点:
1)剩余的搜索路径与当前节点的key一致,则把当前节点的更新即可;
2)剩余的搜索路径与当前节点的key不完全一致,则将叶子/扩展节点的孩子节点替换成分支节点,将新节点与当前节点key的共同前缀作为当前节点的key,将新节点与当前节点的孩子节点作为两个孩子插入到分支节点的孩子列表中,同时当前节点转换成了一个扩展节点(若新节点与当前节点没有共同前缀,则直接用生成的分支节点替换当前节点);
删除
删除操作与插入操作类似,都需要借助查找过程完成,删除过程:
1、若节点不存在则删除失败。
2、若节点是叶子节点则删除该节点,并判断删除后该分支是否只剩一个叶子节点,若是,则将该节点的值给分支节点的value。
3、若与扩展节点的值完全相同,且分支节点的value存在,则删除分支节点的value。
MPT的主要特点
MPT的主要特点就是能够实现快速状态回滚
区块链公链的环境下,可能会造成分叉而导致区块链状态需要进行回滚的。在以太坊中,由于出块时间短,这种分叉的几率很大,区块链状态回滚的现象很频繁。
所谓的状态回滚指的是:(1)区块链内容发生了重组织,链头发生切换(2)区块链的世界状态(账户信息)需要进行回滚,即对之前的操作进行撤销。
MPT树就提供了一种机制,当区块发生了碰撞,零延迟地完成世界状态的回滚。这种优势的代价就是需要浪费存储空间去冗余地存储每个节点的历史状态。(虽然每生成一个新的区块就会有一棵状态树,但它与前一区块的大部分节点是共享的)
每个节点在数据库中的存储都是值驱动的。当一个节点的内容发生了变化,其哈希相应改变,而MPT将哈希作为数据库中的索引,也就实现了对于每一个值,在数据库中都有一条确定的记录。而MPT是根据节点哈希来关联父子节点的,因此每当一个节点的内容发生变化,最终对于父节点来说,改变的只是一个哈希索引值;父节点的内容也由此改变,产生了一个新的父节点,递归地将这种影响传递到根节点。最终,一次改变对应创建了一条从被改节点到根节点的新路径,而旧节点依然可以根据旧根节点通过旧路径访问得到。
如图所示,当MPT中一个节点的内容由27变为45,就对应成创建了一条的新路径,通过复用前一个中的未修改节点信息,构造一棵新树,但是它的旧路径依旧保留。因此通过旧stateRoot,依旧能够查询到该节点的值为27。
所以,在以太坊中,发生分叉而进行世界状态回滚时,只需要用旧的MPT根节点作为入口,就能完成一次“状态回滚”。